Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions mini_agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@
from mini_agent.tools.base import Tool
from mini_agent.tools.bash_tool import BashKillTool, BashOutputTool, BashTool
from mini_agent.tools.file_tools import EditTool, ReadTool, WriteTool
from mini_agent.tools.mcp_loader import cleanup_mcp_connections, load_mcp_tools_async, set_mcp_timeout_config
from mini_agent.tools.note_tool import SessionNoteTool
from mini_agent.tools.skill_tool import create_skill_tools

# Try to import MCP tools, but don't fail if they're not available
try:
from mini_agent.tools.mcp_loader import cleanup_mcp_connections, load_mcp_tools_async, set_mcp_timeout_config
MCP_AVAILABLE = True
except ImportError:
MCP_AVAILABLE = False
from mini_agent.utils import calculate_display_width


Expand Down Expand Up @@ -193,6 +199,7 @@ def print_help():
help_text = f"""
{Colors.BOLD}{Colors.BRIGHT_YELLOW}Available Commands:{Colors.RESET}
{Colors.BRIGHT_GREEN}/help{Colors.RESET} - Show this help message
{Colors.BRIGHT_GREEN}/skills{Colors.RESET} - List all installed skills
{Colors.BRIGHT_GREEN}/clear{Colors.RESET} - Clear session history (keep system prompt)
{Colors.BRIGHT_GREEN}/history{Colors.RESET} - Show current session message count
{Colors.BRIGHT_GREEN}/stats{Colors.RESET} - Show session statistics
Expand Down Expand Up @@ -398,7 +405,7 @@ async def initialize_base_tools(config: Config):
print(f"{Colors.YELLOW}⚠️ Failed to load Skills: {e}{Colors.RESET}")

# 4. MCP tools (loaded with priority search)
if config.tools.enable_mcp:
if config.tools.enable_mcp and MCP_AVAILABLE:
print(f"{Colors.BRIGHT_CYAN}Loading MCP tools...{Colors.RESET}")
try:
# Apply MCP timeout configuration from config.yaml
Expand Down Expand Up @@ -426,6 +433,8 @@ async def initialize_base_tools(config: Config):
print(f"{Colors.YELLOW}⚠️ MCP config file not found: {config.tools.mcp_config_path}{Colors.RESET}")
except Exception as e:
print(f"{Colors.YELLOW}⚠️ Failed to load MCP tools: {e}{Colors.RESET}")
elif config.tools.enable_mcp:
print(f"{Colors.YELLOW}⚠️ MCP tools not available (mcp module not installed){Colors.RESET}")

print() # Empty line separator
return tools, skill_loader
Expand Down Expand Up @@ -469,18 +478,19 @@ def add_workspace_tools(tools: List[Tool], config: Config, workspace_dir: Path):

async def _quiet_cleanup():
"""Clean up MCP connections, suppressing noisy asyncgen teardown tracebacks."""
# Silence the asyncgen finalization noise that anyio/mcp emits when
# stdio_client's task group is torn down across tasks. The handler is
# intentionally NOT restored: asyncgen finalization happens during
# asyncio.run() shutdown (after run_agent returns), so restoring the
# handler here would still let the noise through. Since this runs
# right before process exit, swallowing late exceptions is safe.
loop = asyncio.get_event_loop()
loop.set_exception_handler(lambda _loop, _ctx: None)
try:
await cleanup_mcp_connections()
except Exception:
pass
if MCP_AVAILABLE:
# Silence the asyncgen finalization noise that anyio/mcp emits when
# stdio_client's task group is torn down across tasks. The handler is
# intentionally NOT restored: asyncgen finalization happens during
# asyncio.run() shutdown (after run_agent returns), so restoring the
# handler here would still let the noise through. Since this runs
# right before process exit, swallowing late exceptions is safe.
loop = asyncio.get_event_loop()
loop.set_exception_handler(lambda _loop, _ctx: None)
try:
await cleanup_mcp_connections()
except Exception:
pass


async def run_agent(workspace_dir: Path, task: str = None):
Expand Down Expand Up @@ -705,6 +715,20 @@ def _(event):
print_help()
continue

elif command == "/skills":
# List all installed skills
if skill_loader and skill_loader.loaded_skills:
skills = sorted(skill_loader.loaded_skills.keys())
print(f"\n{Colors.BRIGHT_CYAN}📦 Installed Skills ({len(skills)}):{Colors.RESET}\n")
for i, skill_name in enumerate(skills, 1):
skill = skill_loader.loaded_skills[skill_name]
desc = skill.description[:60] if skill.description else "No description"
print(f" {i}. {Colors.BRIGHT_GREEN}{skill_name}{Colors.RESET} - {desc}")
print(f"\n{Colors.DIM}Use get_skill(skill_name) to load a skill{Colors.RESET}\n")
else:
print(f"\n{Colors.YELLOW}⚠️ No skills loaded{Colors.RESET}\n")
continue

elif command == "/clear":
# Clear message history but keep system prompt
old_count = len(agent.messages)
Expand Down
5 changes: 3 additions & 2 deletions mini_agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

from pathlib import Path
from typing import Union, Optional

import yaml
from pydantic import BaseModel, Field
Expand Down Expand Up @@ -79,7 +80,7 @@ def load(cls) -> "Config":
return cls.from_yaml(config_path)

@classmethod
def from_yaml(cls, config_path: str | Path) -> "Config":
def from_yaml(cls, config_path: Union[str, Path]) -> "Config":
"""Load configuration from YAML file

Args:
Expand Down Expand Up @@ -174,7 +175,7 @@ def get_package_dir() -> Path:
return Path(__file__).parent

@classmethod
def find_config_file(cls, filename: str) -> Path | None:
def find_config_file(cls, filename: str) -> Optional[Path]:
"""Find configuration file with priority order

Search for config file in the following order of priority:
Expand Down
14 changes: 7 additions & 7 deletions mini_agent/llm/anthropic_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Anthropic LLM client implementation."""

import logging
from typing import Any
from typing import Any, Optional, Union

import anthropic

Expand All @@ -26,7 +26,7 @@ def __init__(
api_key: str,
api_base: str = "https://api.minimaxi.com/anthropic",
model: str = "MiniMax-M2.5",
retry_config: RetryConfig | None = None,
retry_config: Optional[RetryConfig] = None,
):
"""Initialize Anthropic client.

Expand All @@ -47,9 +47,9 @@ def __init__(

async def _make_api_request(
self,
system_message: str | None,
system_message: Optional[str],
api_messages: list[dict[str, Any]],
tools: list[Any] | None = None,
tools: Optional[list[Any]] = None,
) -> anthropic.types.Message:
"""Execute API request (core method that can be retried).

Expand Down Expand Up @@ -111,7 +111,7 @@ def _convert_tools(self, tools: list[Any]) -> list[dict[str, Any]]:
raise TypeError(f"Unsupported tool type: {type(tool)}")
return result

def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]:
def _convert_messages(self, messages: list[Message]) -> tuple[Optional[str], list[dict[str, Any]]]:
"""Convert internal messages to Anthropic format.

Args:
Expand Down Expand Up @@ -180,7 +180,7 @@ def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[d
def _prepare_request(
self,
messages: list[Message],
tools: list[Any] | None = None,
tools: Optional[list[Any]] = None,
) -> dict[str, Any]:
"""Prepare the request for Anthropic API.

Expand Down Expand Up @@ -257,7 +257,7 @@ def _parse_response(self, response: anthropic.types.Message) -> LLMResponse:
async def generate(
self,
messages: list[Message],
tools: list[Any] | None = None,
tools: Optional[list[Any]] = None,
) -> LLMResponse:
"""Generate response from Anthropic LLM.

Expand Down
10 changes: 5 additions & 5 deletions mini_agent/llm/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Base class for LLM clients."""

from abc import ABC, abstractmethod
from typing import Any
from typing import Any, Optional, Union

from ..retry import RetryConfig
from ..schema import LLMResponse, Message
Expand All @@ -19,7 +19,7 @@ def __init__(
api_key: str,
api_base: str,
model: str,
retry_config: RetryConfig | None = None,
retry_config: Optional[RetryConfig] = None,
):
"""Initialize the LLM client.

Expand All @@ -41,7 +41,7 @@ def __init__(
async def generate(
self,
messages: list[Message],
tools: list[Any] | None = None,
tools: Optional[list[Any]] = None,
) -> LLMResponse:
"""Generate response from LLM.

Expand All @@ -58,7 +58,7 @@ async def generate(
def _prepare_request(
self,
messages: list[Message],
tools: list[Any] | None = None,
tools: Optional[list[Any]] = None,
) -> dict[str, Any]:
"""Prepare the request payload for the API.

Expand All @@ -72,7 +72,7 @@ def _prepare_request(
pass

@abstractmethod
def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]:
def _convert_messages(self, messages: list[Message]) -> tuple[Optional[str], list[dict[str, Any]]]:
"""Convert internal message format to API-specific format.

Args:
Expand Down
5 changes: 3 additions & 2 deletions mini_agent/llm/llm_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""

import logging
from typing import Optional, Any

from ..retry import RetryConfig
from ..schema import LLMProvider, LLMResponse, Message
Expand Down Expand Up @@ -39,7 +40,7 @@ def __init__(
provider: LLMProvider = LLMProvider.ANTHROPIC,
api_base: str = "https://api.minimaxi.com",
model: str = "MiniMax-M2.5",
retry_config: RetryConfig | None = None,
retry_config: Optional[RetryConfig] = None,
):
"""Initialize LLM client with specified provider.

Expand Down Expand Up @@ -113,7 +114,7 @@ def retry_callback(self, value):
async def generate(
self,
messages: list[Message],
tools: list | None = None,
tools: Optional[list[Any]] = None,
) -> LLMResponse:
"""Generate response from LLM.

Expand Down
12 changes: 6 additions & 6 deletions mini_agent/llm/openai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import json
import logging
from typing import Any
from typing import Any, Optional, Union

from openai import AsyncOpenAI

Expand All @@ -27,7 +27,7 @@ def __init__(
api_key: str,
api_base: str = "https://api.minimaxi.com/v1",
model: str = "MiniMax-M2.5",
retry_config: RetryConfig | None = None,
retry_config: Optional[RetryConfig] = None,
):
"""Initialize OpenAI client.

Expand All @@ -48,7 +48,7 @@ def __init__(
async def _make_api_request(
self,
api_messages: list[dict[str, Any]],
tools: list[Any] | None = None,
tools: Optional[list[Any]] = None,
) -> Any:
"""Execute API request (core method that can be retried).

Expand Down Expand Up @@ -111,7 +111,7 @@ def _convert_tools(self, tools: list[Any]) -> list[dict[str, Any]]:
raise TypeError(f"Unsupported tool type: {type(tool)}")
return result

def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]:
def _convert_messages(self, messages: list[Message]) -> tuple[Optional[str], list[dict[str, Any]]]:
"""Convert internal messages to OpenAI format.

Args:
Expand Down Expand Up @@ -182,7 +182,7 @@ def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[d
def _prepare_request(
self,
messages: list[Message],
tools: list[Any] | None = None,
tools: Optional[list[Any]] = None,
) -> dict[str, Any]:
"""Prepare the request for OpenAI API.

Expand Down Expand Up @@ -261,7 +261,7 @@ def _parse_response(self, response: Any) -> LLMResponse:
async def generate(
self,
messages: list[Message],
tools: list[Any] | None = None,
tools: Optional[list[Any]] = None,
) -> LLMResponse:
"""Generate response from OpenAI LLM.

Expand Down
14 changes: 7 additions & 7 deletions mini_agent/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
from datetime import datetime
from pathlib import Path
from typing import Any
from typing import Any, Optional

from .schema import Message, ToolCall

Expand Down Expand Up @@ -40,7 +40,7 @@ def start_new_run(self):
f.write(f"Agent Run Log - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write("=" * 80 + "\n\n")

def log_request(self, messages: list[Message], tools: list[Any] | None = None):
def log_request(self, messages: list[Message], tools: Optional[list[Any]] = None):
"""Log LLM request

Args:
Expand Down Expand Up @@ -85,9 +85,9 @@ def log_request(self, messages: list[Message], tools: list[Any] | None = None):
def log_response(
self,
content: str,
thinking: str | None = None,
tool_calls: list[ToolCall] | None = None,
finish_reason: str | None = None,
thinking: Optional[str] = None,
tool_calls: Optional[list[ToolCall]] = None,
finish_reason: Optional[str] = None,
):
"""Log LLM response

Expand Down Expand Up @@ -124,8 +124,8 @@ def log_tool_result(
tool_name: str,
arguments: dict[str, Any],
result_success: bool,
result_content: str | None = None,
result_error: str | None = None,
result_content: Optional[str] = None,
result_error: Optional[str] = None,
):
"""Log tool execution result

Expand Down
8 changes: 4 additions & 4 deletions mini_agent/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import asyncio
import functools
import logging
from typing import Any, Callable, Type, TypeVar
from typing import Any, Callable, Type, TypeVar, Optional

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -71,8 +71,8 @@ def __init__(self, last_exception: Exception, attempts: int):


def async_retry(
config: RetryConfig | None = None,
on_retry: Callable[[Exception, int], None] | None = None,
config: Optional[RetryConfig] = None,
on_retry: Optional[Callable[[Exception, int], None]] = None,
) -> Callable:
"""Async function retry decorator

Expand All @@ -97,7 +97,7 @@ async def call_api():
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@functools.wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
last_exception: Exception | None = None
last_exception: Optional[Exception] = None

for attempt in range(config.max_retries + 1):
try:
Expand Down
Loading