From ccab89d8584c02d672bfe44dcdef4537ca07011f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Nov 2025 12:14:16 +0000 Subject: [PATCH 1/4] feat: add Agent Client Protocol (ACP) support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements modular ACP integration for Mini-Agent, enabling communication with ACP-compatible clients like Zed over stdin/stdout using JSON-RPC. ## Features - Multiple concurrent sessions with isolated state - Real-time streaming updates (thoughts, messages, tool execution) - MiniMax-specific support (thinking blocks, tool format) - Modular architecture in mini_agent/acp/ directory ## Components - agent.py: ACP Agent implementation bridging to Mini-Agent - session.py: Session management for concurrent conversations - converter.py: Message format conversion (ACP ↔ Mini-Agent) - server.py: Entry point for ACP server mode ## Configuration - New CLI command: mini-agent-acp - Uses existing Mini-Agent configuration - Added agent-client-protocol>=0.1.0 dependency ## Documentation - Comprehensive README in mini_agent/acp/ - Updated main README with ACP section - Integration examples for Zed and other ACP clients Closes # --- README.md | 44 ++++ mini_agent/acp/README.md | 246 ++++++++++++++++++++++ mini_agent/acp/__init__.py | 10 + mini_agent/acp/agent.py | 407 ++++++++++++++++++++++++++++++++++++ mini_agent/acp/converter.py | 122 +++++++++++ mini_agent/acp/server.py | 150 +++++++++++++ mini_agent/acp/session.py | 132 ++++++++++++ pyproject.toml | 2 + 8 files changed, 1113 insertions(+) create mode 100644 mini_agent/acp/README.md create mode 100644 mini_agent/acp/__init__.py create mode 100644 mini_agent/acp/agent.py create mode 100644 mini_agent/acp/converter.py create mode 100644 mini_agent/acp/server.py create mode 100644 mini_agent/acp/session.py diff --git a/README.md b/README.md index 7c834c4..f17b2ad 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This project comes packed with features designed for a robust and intelligent ag * ✅ **Intelligent Context Management**: Automatically summarizes conversation history to handle contexts up to a configurable token limit, enabling infinitely long tasks. * ✅ **Claude Skills Integration**: Comes with 15 professional skills for documents, design, testing, and development. * ✅ **MCP Tool Integration**: Natively supports MCP for tools like knowledge graph access and web search. +* ✅ **ACP Support**: Native support for [Agent Client Protocol](https://agentclientprotocol.com/) for integration with ACP-compatible clients like Zed. * ✅ **Comprehensive Logging**: Detailed logs for every request, response, and tool execution for easy debugging. * ✅ **Clean & Simple Design**: A beautiful CLI and a codebase that is easy to understand, making it the perfect starting point for building advanced agents. @@ -30,6 +31,10 @@ This project comes packed with features designed for a robust and intelligent ag - [Testing](#testing) - [Quick Run](#quick-run) - [Test Coverage](#test-coverage) + - [ACP (Agent Client Protocol) Support](#acp-agent-client-protocol-support) + - [Features](#features) + - [Quick Start](#quick-start-1) + - [Integration with Zed](#integration-with-zed) - [Related Documentation](#related-documentation) - [Contributing](#contributing) - [License](#license) @@ -220,10 +225,49 @@ pytest tests/test_agent.py tests/test_note_tool.py -v - ✅ **External Services** - Git MCP Server loading +## ACP (Agent Client Protocol) Support + +Mini-Agent supports the [Agent Client Protocol](https://agentclientprotocol.com/), allowing it to integrate seamlessly with ACP-compatible clients like Zed. + +### Features + +- ✅ **Multiple Concurrent Sessions**: Run multiple conversations simultaneously +- ✅ **Real-time Streaming**: Stream agent thoughts, messages, and tool execution in real-time +- ✅ **Tool Execution**: Execute tools with progress tracking and error handling +- ✅ **MiniMax Integration**: Full support for MiniMax's thinking blocks and unique tool format + +### Quick Start + +```bash +# Install with ACP support +pip install mini-agent + +# Run as ACP server +mini-agent-acp +``` + +### Integration with Zed + +Add to your Zed agent configuration: + +```json +{ + "agents": [ + { + "name": "mini-agent", + "command": "mini-agent-acp" + } + ] +} +``` + +For detailed ACP documentation, see [mini_agent/acp/README.md](mini_agent/acp/README.md). + ## Related Documentation - [Development Guide](docs/DEVELOPMENT_GUIDE.md) - Detailed development and configuration guidance - [Production Guide](docs/PRODUCTION_GUIDE.md) - Best practices for production deployment +- [ACP Integration](mini_agent/acp/README.md) - Agent Client Protocol implementation details ## Contributing diff --git a/mini_agent/acp/README.md b/mini_agent/acp/README.md new file mode 100644 index 0000000..1279979 --- /dev/null +++ b/mini_agent/acp/README.md @@ -0,0 +1,246 @@ +# ACP (Agent Client Protocol) Support for Mini-Agent + +This module provides ACP support for Mini-Agent, allowing it to communicate with ACP-compatible clients (like Zed) over stdin/stdout using JSON-RPC. + +## Overview + +The ACP integration is designed to be: +- **Modular**: Separate from core agent logic, easy to maintain +- **Minimal**: Uses the official `agent-client-protocol` Python SDK +- **Simple**: Clean abstractions with clear separation of concerns + +## Architecture + +``` +mini_agent/acp/ +├── __init__.py # Module exports +├── agent.py # ACP Agent implementation +├── session.py # Session management +├── converter.py # Message format conversion +├── server.py # ACP server entry point +└── README.md # This file +``` + +### Components + +#### agent.py - MiniMaxACPAgent +Implements the ACP `Agent` protocol, bridging between ACP clients and Mini-Agent's internal implementation. + +**Features:** +- Multiple concurrent sessions support +- Real-time streaming updates via `sessionUpdate` notifications +- Tool execution with progress tracking +- MiniMax-specific features (thinking blocks) +- Graceful error handling + +**Key Methods:** +- `initialize()` - Protocol handshake and capability negotiation +- `newSession()` - Create new conversation session +- `prompt()` - Process user input and generate responses +- `cancel()` - Cancel ongoing operations + +#### session.py - SessionManager +Manages multiple concurrent ACP sessions, each with its own: +- Message history +- Working directory +- Agent instance +- MCP server configuration +- Cancellation event + +#### converter.py - Message Format Conversion +Handles conversion between ACP content blocks and Mini-Agent's message format: +- ACP content blocks (text, image, resource) → Mini-Agent messages +- Mini-Agent messages → ACP content blocks +- Tool calls format conversion + +#### server.py - Entry Point +Main entry point for running Mini-Agent as an ACP server: +- Configuration loading +- Tool initialization +- ACP connection setup over stdin/stdout +- Event loop management + +## Usage + +### As a Standalone Server + +```bash +# Install Mini-Agent with ACP support +pip install mini-agent + +# Run the ACP server +mini-agent-acp +``` + +The server will: +1. Load configuration from `~/.mini-agent/config/config.yaml` +2. Initialize tools and LLM client +3. Wait for ACP client connections on stdin/stdout + +### With Zed Editor + +Add to your Zed agent configuration: + +```json +{ + "agents": [ + { + "name": "mini-agent", + "command": "mini-agent-acp" + } + ] +} +``` + +### Programmatically + +```python +import asyncio +from mini_agent.acp import run_acp_server +from mini_agent.config import Config + +# Run with custom config +config = Config.load() +asyncio.run(run_acp_server(config)) +``` + +## Features + +### Streaming Updates +The agent sends real-time updates during execution: +- **User messages**: Echo user input +- **Agent thoughts**: MiniMax thinking blocks (internal reasoning) +- **Agent messages**: Assistant responses +- **Tool calls**: Tool execution progress (pending → in_progress → completed/failed) + +### Tool Execution +Tools are executed with streaming updates: +1. Start notification with tool name and arguments +2. Progress updates during execution +3. Completion notification with results or errors + +### Concurrent Sessions +Multiple sessions can run concurrently, each with: +- Isolated message history +- Separate working directory +- Independent agent instance +- Session-specific MCP servers + +### Error Handling +Graceful error handling: +- Tool execution errors are caught and reported +- LLM errors are handled with appropriate notifications +- Session cancellation is supported +- Connection errors are logged + +## MiniMax Integration + +The ACP implementation handles MiniMax's unique features: + +### Interleaved Thinking +MiniMax returns thinking blocks interleaved with content: +```python +{ + "type": "thinking", + "thinking": "Internal reasoning..." +} +``` +These are streamed to the client as `update_agent_thought` notifications. + +### Tool Call Format +MiniMax uses Anthropic-compatible tool calls: +```python +{ + "id": "call_123", + "type": "function", + "function": { + "name": "read_file", + "arguments": {"path": "file.txt"} + } +} +``` + +## Configuration + +ACP server uses the standard Mini-Agent configuration: + +```yaml +# ~/.mini-agent/config/config.yaml +llm: + api_key: "your-api-key" + api_base: "https://api.minimax.io/anthropic" + model: "MiniMax-M2" + +agent: + max_steps: 10 + workspace_dir: "." + system_prompt_path: "~/.mini-agent/config/system_prompt.md" + +tools: + enable_file_tools: true + enable_bash: true + enable_mcp: true + mcp_config_path: "~/.mini-agent/config/mcp.json" +``` + +## Development + +### Testing +```bash +# Install in development mode +pip install -e . + +# Run tests +pytest tests/ + +# Test with a simple ACP client +python -m acp.examples.client mini-agent-acp +``` + +### Debugging +Enable debug logging: +```python +import logging +logging.basicConfig(level=logging.DEBUG) +``` + +The agent logs: +- Session creation and management +- Message conversions +- Tool execution +- Errors and warnings + +## Protocol Compliance + +This implementation follows the [Agent Client Protocol](https://agentclientprotocol.com/) specification: + +- ✅ JSON-RPC 2.0 over stdin/stdout +- ✅ Initialize handshake +- ✅ Session management (create, prompt, cancel) +- ✅ Real-time streaming via `sessionUpdate` notifications +- ✅ Tool execution with progress tracking +- ✅ Bidirectional requests (agent → client) +- ⚠️ Session persistence (not yet implemented) +- ⚠️ Mode switching (not yet implemented) +- ⚠️ Permission requests (planned) + +## Future Enhancements + +Planned features: +- [ ] Session persistence (`loadSession` support) +- [ ] Mode switching (code, chat, plan modes) +- [ ] Permission requests for tool execution +- [ ] File I/O via ACP file methods (`fs/read_text_file`, `fs/write_text_file`) +- [ ] Terminal integration (`terminal/create`, `terminal/output`) +- [ ] MCP server forwarding from client +- [ ] Multi-model support + +## Resources + +- [Agent Client Protocol](https://agentclientprotocol.com/) +- [ACP Python SDK](https://github.com/agentclientprotocol/python-sdk) +- [Mini-Agent Documentation](../../README.md) + +## License + +MIT License - see project root for details. diff --git a/mini_agent/acp/__init__.py b/mini_agent/acp/__init__.py new file mode 100644 index 0000000..2e7f7dc --- /dev/null +++ b/mini_agent/acp/__init__.py @@ -0,0 +1,10 @@ +"""ACP (Agent Client Protocol) integration for Mini-Agent. + +This module provides ACP support, allowing Mini-Agent to communicate +with ACP-compatible clients (like Zed) over stdin/stdout using JSON-RPC. +""" + +from mini_agent.acp.agent import MiniMaxACPAgent +from mini_agent.acp.server import run_acp_server + +__all__ = ["MiniMaxACPAgent", "run_acp_server"] diff --git a/mini_agent/acp/agent.py b/mini_agent/acp/agent.py new file mode 100644 index 0000000..b2cca4f --- /dev/null +++ b/mini_agent/acp/agent.py @@ -0,0 +1,407 @@ +"""ACP Agent implementation for Mini-Agent. + +This module implements the ACP Agent protocol, bridging between +ACP clients and Mini-Agent's internal agent implementation. +""" + +import logging +from typing import Any +from uuid import uuid4 + +from acp import ( + AgentSideConnection, + InitializeRequest, + InitializeResponse, + NewSessionRequest, + NewSessionResponse, + PromptRequest, + PromptResponse, + LoadSessionRequest, + LoadSessionResponse, + SetSessionModeRequest, + SetSessionModeResponse, + CancelNotification, + session_notification, + text_block, + update_agent_message, + update_agent_thought, + start_tool_call, + update_tool_call, + tool_content, + PROTOCOL_VERSION, +) +from acp.schema import AgentCapabilities, Implementation + +from mini_agent.acp.converter import acp_content_to_text, message_to_acp_content +from mini_agent.acp.session import SessionManager +from mini_agent.llm import LLMClient +from mini_agent.schema.schema import Message + +logger = logging.getLogger(__name__) + + +class MiniMaxACPAgent: + """ACP Agent implementation that wraps Mini-Agent. + + Implements the ACP Agent protocol, providing: + - Multiple concurrent sessions + - Real-time streaming updates + - Tool execution with permission requests + - MiniMax-specific features (thinking blocks, unique tool format) + """ + + def __init__( + self, + conn: AgentSideConnection, + llm_client: LLMClient, + tools: list[Any], + system_prompt: str, + ): + """Initialize ACP agent. + + Args: + conn: ACP connection for client communication + llm_client: LLM client instance + tools: Available tools + system_prompt: System prompt + """ + self._conn = conn + self._session_manager = SessionManager(llm_client, tools, system_prompt) + self._session_counter = 0 + + async def initialize(self, params: InitializeRequest) -> InitializeResponse: + """Handle ACP initialize request. + + Args: + params: Initialize request parameters + + Returns: + Initialize response with capabilities + """ + logger.info("Initializing ACP agent (protocol v%s)", params.protocolVersion) + + return InitializeResponse( + protocolVersion=PROTOCOL_VERSION, + agentCapabilities=AgentCapabilities( + supportsLoadSession=False, # Session persistence not yet implemented + supportsSetMode=False, # Mode switching not yet implemented + ), + agentInfo=Implementation( + name="mini-agent", + title="Mini-Agent (MiniMax M2)", + version="0.1.0", + ), + ) + + async def newSession(self, params: NewSessionRequest) -> NewSessionResponse: + """Create a new session. + + Args: + params: New session request parameters + + Returns: + New session response with session ID + """ + # Generate unique session ID + session_id = f"sess-{self._session_counter}-{uuid4().hex[:8]}" + self._session_counter += 1 + + logger.info("Creating new session: %s (cwd: %s)", session_id, params.cwd) + + # Create session + await self._session_manager.create_session( + session_id=session_id, + cwd=params.cwd, + mcp_servers=params.mcpServers or [], + ) + + return NewSessionResponse(sessionId=session_id) + + async def loadSession( + self, params: LoadSessionRequest + ) -> LoadSessionResponse | None: + """Load an existing session (not implemented). + + Args: + params: Load session request parameters + + Returns: + None (not supported) + """ + logger.info("Load session requested: %s (not implemented)", params.sessionId) + return None + + async def setSessionMode( + self, params: SetSessionModeRequest + ) -> SetSessionModeResponse | None: + """Set session mode (not implemented). + + Args: + params: Set mode request parameters + + Returns: + None (not supported) + """ + logger.info( + "Set mode requested: %s -> %s (not implemented)", + params.sessionId, + params.modeId, + ) + return None + + async def prompt(self, params: PromptRequest) -> PromptResponse: + """Process a user prompt. + + This is the main method that handles user input and generates responses. + It streams updates in real-time via sessionUpdate notifications. + + Args: + params: Prompt request with user message + + Returns: + Prompt response with stop reason + """ + session = await self._session_manager.get_session(params.sessionId) + if not session: + logger.error("Session not found: %s", params.sessionId) + return PromptResponse(stopReason="error") + + logger.info("Processing prompt for session: %s", params.sessionId) + + try: + # Convert ACP content to text + user_text = acp_content_to_text(params.prompt) + + # Send user message update + await self._send_update( + params.sessionId, update_agent_message(text_block(f"User: {user_text}")) + ) + + # Add to message history + user_message = Message(role="user", content=user_text) + session.messages.append(user_message) + + # Run agent with streaming + stop_reason = await self._run_agent_with_streaming(session) + + return PromptResponse(stopReason=stop_reason) + + except Exception as e: + logger.exception("Error processing prompt: %s", e) + await self._send_update( + params.sessionId, + update_agent_message(text_block(f"Error: {str(e)}")), + ) + return PromptResponse(stopReason="error") + + async def cancel(self, params: CancelNotification) -> None: + """Cancel ongoing operations for a session. + + Args: + params: Cancel notification parameters + """ + logger.info("Canceling session: %s", params.sessionId) + await self._session_manager.cancel_session(params.sessionId) + + async def extMethod(self, method: str, params: dict[str, Any]) -> dict[str, Any]: + """Handle extension methods. + + Args: + method: Method name + params: Method parameters + + Returns: + Response data + """ + logger.info("Extension method called: %s", method) + return {} + + async def extNotification(self, method: str, params: dict[str, Any]) -> None: + """Handle extension notifications. + + Args: + method: Notification name + params: Notification parameters + """ + logger.info("Extension notification: %s", method) + + async def _send_update(self, session_id: str, update: Any) -> None: + """Send a session update to the client. + + Args: + session_id: Session identifier + update: Update content (agent message, thought, tool call, etc.) + """ + await self._conn.sessionUpdate(session_notification(session_id, update)) + + async def _run_agent_with_streaming(self, session) -> str: + """Run the agent with real-time streaming updates. + + Args: + session: Session state + + Returns: + Stop reason ("end_turn", "max_steps", "error") + """ + agent = session.agent + max_steps = 10 # TODO: Make configurable + + for step in range(max_steps): + # Check for cancellation + if session.cancel_event.is_set(): + logger.info("Session cancelled: %s", session.session_id) + return "cancelled" + + # Generate LLM response + # Note: Agent stores LLM client as 'llm' attribute + try: + # Convert tools dict to list for LLM client + tools_list = list(agent.tools.values()) + response = await agent.llm.generate( + messages=session.messages, tools=tools_list + ) + except Exception as e: + logger.exception("LLM generation error: %s", e) + await self._send_update( + session.session_id, + update_agent_message(text_block(f"LLM Error: {str(e)}")), + ) + return "error" + + # Stream thinking content if present (MiniMax feature) + if response.thinking: + await self._send_update( + session.session_id, + update_agent_thought(text_block(response.thinking)), + ) + + # Stream assistant message + if response.content: + await self._send_update( + session.session_id, + update_agent_message(text_block(response.content)), + ) + + # Add assistant message to history + assistant_message = Message( + role="assistant", + content=response.content, + thinking=response.thinking, + tool_calls=response.tool_calls, + ) + session.messages.append(assistant_message) + + # If no tool calls, we're done + if not response.tool_calls: + return "end_turn" + + # Execute tool calls with streaming + for tool_call in response.tool_calls: + await self._execute_tool_with_streaming(session, tool_call) + + # Continue loop for next step + + # Max steps reached + logger.warning("Max steps reached for session: %s", session.session_id) + return "max_steps" + + async def _execute_tool_with_streaming(self, session, tool_call) -> None: + """Execute a tool call with streaming updates. + + Args: + session: Session state + tool_call: Tool call to execute + """ + tool_name = tool_call.function.name + tool_id = tool_call.id + + # Start tool call + await self._send_update( + session.session_id, + start_tool_call( + tool_id, + f"Executing {tool_name}", + name=tool_name, + arguments=tool_call.function.arguments, + ), + ) + + # Find and execute tool + agent = session.agent + tool = agent.tools.get(tool_name) + + if not tool: + error_msg = f"Tool not found: {tool_name}" + logger.error(error_msg) + + # Update tool call with error + await self._send_update( + session.session_id, + update_tool_call( + tool_id, + status="failed", + content=[tool_content(text_block(error_msg))], + ), + ) + + # Add error to message history + session.messages.append( + Message( + role="tool", + content=error_msg, + tool_call_id=tool_id, + name=tool_name, + ) + ) + return + + # Execute tool + try: + result = await tool.execute(**tool_call.function.arguments) + + # Update tool call with result + status = "completed" if result.success else "failed" + result_text = result.content if result.success else result.error or "Unknown error" + + await self._send_update( + session.session_id, + update_tool_call( + tool_id, + status=status, + content=[tool_content(text_block(result_text))], + ), + ) + + # Add to message history + session.messages.append( + Message( + role="tool", + content=result_text, + tool_call_id=tool_id, + name=tool_name, + ) + ) + + except Exception as e: + error_msg = f"Tool execution error: {str(e)}" + logger.exception(error_msg) + + # Update tool call with error + await self._send_update( + session.session_id, + update_tool_call( + tool_id, + status="failed", + content=[tool_content(text_block(error_msg))], + ), + ) + + # Add error to message history + session.messages.append( + Message( + role="tool", + content=error_msg, + tool_call_id=tool_id, + name=tool_name, + ) + ) diff --git a/mini_agent/acp/converter.py b/mini_agent/acp/converter.py new file mode 100644 index 0000000..efbe655 --- /dev/null +++ b/mini_agent/acp/converter.py @@ -0,0 +1,122 @@ +"""Message format converter between ACP and Mini-Agent formats. + +Handles conversion between: +- ACP content blocks (text, image, resource, etc.) ↔ Mini-Agent messages +- ACP tool calls ↔ Mini-Agent tool calls +- Streaming updates for real-time communication +""" + +from typing import Any + +from acp import text_block +from acp.schema import TextContentBlock, ImageContentBlock, ResourceContentBlock + +from mini_agent.schema.schema import Message, ToolCall, FunctionCall + + +def acp_content_to_text(content: list[dict[str, Any]] | list[Any]) -> str: + """Convert ACP content blocks to plain text. + + Args: + content: List of ACP content blocks (can be dicts or pydantic models) + + Returns: + Concatenated text content + """ + parts = [] + for block in content: + if isinstance(block, dict): + if block.get("type") == "text": + parts.append(block.get("text", "")) + elif block.get("type") == "image": + # Include image metadata in text + source = block.get("source", {}) + if isinstance(source, dict) and source.get("type") == "uri": + parts.append(f"[Image: {source.get('uri', 'unknown')}]") + else: + parts.append("[Image]") + elif block.get("type") == "resource": + # Include resource link + resource = block.get("resource", {}) + if isinstance(resource, dict): + uri = resource.get("uri", "") + parts.append(f"[Resource: {uri}]") + else: + # Pydantic models + if isinstance(block, TextContentBlock): + parts.append(block.text) + elif isinstance(block, ImageContentBlock): + if hasattr(block, "uri") and block.uri: + parts.append(f"[Image: {block.uri}]") + else: + parts.append("[Image]") + elif isinstance(block, ResourceContentBlock): + if hasattr(block.resource, "uri"): + parts.append(f"[Resource: {block.resource.uri}]") + elif hasattr(block, "text"): + # Fallback for text-like objects + parts.append(str(block.text)) + + return "\n".join(parts) + + +def message_to_acp_content(message: Message) -> list[dict[str, Any]]: + """Convert Mini-Agent message to ACP content blocks. + + Args: + message: Mini-Agent message + + Returns: + List of ACP content blocks + """ + blocks = [] + + # Handle string content + if isinstance(message.content, str): + if message.content: + blocks.append(text_block(message.content)) + # Handle list of content blocks (already in block format) + elif isinstance(message.content, list): + blocks.extend(message.content) + + return blocks + + +def tool_call_to_acp_format(tool_call: ToolCall) -> dict[str, Any]: + """Convert Mini-Agent tool call to ACP format. + + Args: + tool_call: Mini-Agent tool call + + Returns: + ACP-compatible tool call dict + """ + return { + "id": tool_call.id, + "type": tool_call.type, + "function": { + "name": tool_call.function.name, + "arguments": tool_call.function.arguments, + }, + } + + +def acp_tool_result_to_message( + tool_call_id: str, tool_name: str, content: str +) -> Message: + """Convert ACP tool result to Mini-Agent message format. + + Args: + tool_call_id: ID of the tool call + tool_name: Name of the tool + content: Tool execution result + + Returns: + Mini-Agent message with tool result + """ + return Message( + role="tool", + content=content, + tool_call_id=tool_call_id, + name=tool_name, + ) diff --git a/mini_agent/acp/server.py b/mini_agent/acp/server.py new file mode 100644 index 0000000..2ece389 --- /dev/null +++ b/mini_agent/acp/server.py @@ -0,0 +1,150 @@ +"""ACP server entry point for Mini-Agent. + +This module provides the main entry point for running Mini-Agent +as an ACP server, communicating over stdin/stdout with ACP clients. +""" + +import asyncio +import logging +from pathlib import Path + +from acp import AgentSideConnection, stdio_streams + +from mini_agent.acp.agent import MiniMaxACPAgent +from mini_agent.config import Config +from mini_agent.llm import LLMClient +from mini_agent.tools.bash_tool import BashTool, BashOutputTool, BashKillTool +from mini_agent.tools.file_tools import ReadTool, WriteTool, EditTool +from mini_agent.tools.mcp_loader import load_mcp_tools_async +from mini_agent.tools.skill_tool import create_skill_tools + +logger = logging.getLogger(__name__) + + +def load_system_prompt(config: Config) -> str: + """Load system prompt from file. + + Args: + config: Configuration object + + Returns: + System prompt text + """ + try: + with open(config.agent.system_prompt_path, encoding="utf-8") as f: + return f.read() + except Exception as e: + logger.warning("Failed to load system prompt: %s", e) + return "You are a helpful AI assistant." + + +def initialize_tools(config: Config, workspace_dir: Path) -> list: + """Initialize all available tools. + + Args: + config: Configuration object + workspace_dir: Default workspace directory + + Returns: + List of initialized tools + """ + tools = [] + + # Bash tools + if config.tools.enable_bash: + tools.extend([BashTool(), BashOutputTool(), BashKillTool()]) + logger.info("Loaded bash tools") + + # File tools + if config.tools.enable_file_tools: + tools.extend([ + ReadTool(workspace_dir), + WriteTool(workspace_dir), + EditTool(workspace_dir), + ]) + logger.info("Loaded file tools") + + # MCP tools (async, will need to be awaited separately) + # Note: For now, skipping MCP tools in ACP mode + # TODO: Properly integrate async MCP loading + if config.tools.enable_mcp: + logger.info("MCP tools loading not yet implemented in ACP mode") + + # Skills + if config.tools.enable_skills: + try: + skills = create_skill_tools(config.tools.skills_dir) + tools.extend(skills) + logger.info("Loaded %d skills", len(skills)) + except Exception as e: + logger.warning("Failed to load skills: %s", e) + + return tools + + +async def run_acp_server(config: Config | None = None) -> None: + """Run Mini-Agent as an ACP server. + + This is the main entry point for ACP mode. It: + 1. Loads configuration + 2. Initializes LLM client and tools + 3. Creates ACP connection over stdin/stdout + 4. Runs the agent loop + + Args: + config: Optional configuration (will load from files if not provided) + """ + # Load config if not provided + if config is None: + config = Config.load() + + # Setup logging + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + ) + + logger.info("Starting Mini-Agent ACP server") + logger.info("API Base: %s", config.llm.api_base) + logger.info("Model: %s", config.llm.model) + + # Initialize LLM client + llm_client = LLMClient( + api_key=config.llm.api_key, + api_base=config.llm.api_base, + model=config.llm.model, + retry_config=config.llm.retry, + ) + + # Initialize tools + workspace_dir = Path(config.agent.workspace_dir).expanduser() + tools = initialize_tools(config, workspace_dir) + logger.info("Initialized %d tools", len(tools)) + + # Load system prompt + system_prompt = load_system_prompt(config) + + # Create ACP connection + logger.info("Waiting for ACP client connection...") + reader, writer = await stdio_streams() + + # Create agent connection + AgentSideConnection( + lambda conn: MiniMaxACPAgent(conn, llm_client, tools, system_prompt), + writer, + reader, + ) + + logger.info("ACP server running, waiting for requests...") + + # Keep server running + await asyncio.Event().wait() + + +def main() -> None: + """Main entry point for ACP server CLI.""" + asyncio.run(run_acp_server()) + + +if __name__ == "__main__": + main() diff --git a/mini_agent/acp/session.py b/mini_agent/acp/session.py new file mode 100644 index 0000000..5b4109e --- /dev/null +++ b/mini_agent/acp/session.py @@ -0,0 +1,132 @@ +"""Session management for ACP multi-session support. + +Manages concurrent sessions, each with its own: +- Message history +- Working directory +- Agent instance +- MCP server configuration +""" + +import asyncio +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from mini_agent.agent import Agent +from mini_agent.llm import LLMClient +from mini_agent.schema.schema import Message + + +@dataclass +class SessionState: + """State for a single ACP session. + + Attributes: + session_id: Unique session identifier + cwd: Working directory for this session + agent: Mini-Agent instance for this session + messages: Message history + mcp_servers: MCP server configurations + cancel_event: Event to signal cancellation + """ + + session_id: str + cwd: str + agent: Agent + messages: list[Message] + mcp_servers: list[dict[str, Any]] + cancel_event: asyncio.Event + + +class SessionManager: + """Manages multiple concurrent ACP sessions. + + Each session has its own agent instance and message history, + allowing true concurrent session support as required by ACP. + """ + + def __init__(self, llm_client: LLMClient, tools: list[Any], system_prompt: str): + """Initialize session manager. + + Args: + llm_client: LLM client instance (shared across sessions) + tools: List of available tools (shared across sessions) + system_prompt: System prompt (shared across sessions) + """ + self._llm_client = llm_client + self._tools = tools + self._system_prompt = system_prompt + self._sessions: dict[str, SessionState] = {} + self._lock = asyncio.Lock() + + async def create_session( + self, session_id: str, cwd: str, mcp_servers: list[dict[str, Any]] + ) -> SessionState: + """Create a new session. + + Args: + session_id: Unique session identifier + cwd: Working directory for this session + mcp_servers: MCP server configurations + + Returns: + New session state + """ + async with self._lock: + if session_id in self._sessions: + raise ValueError(f"Session {session_id} already exists") + + # Create session-specific agent + # Note: Each session gets its own workspace + agent = Agent( + llm_client=self._llm_client, + tools=self._tools, + system_prompt=self._system_prompt, + workspace_dir=Path(cwd), + ) + + session = SessionState( + session_id=session_id, + cwd=cwd, + agent=agent, + messages=[], + mcp_servers=mcp_servers, + cancel_event=asyncio.Event(), + ) + + self._sessions[session_id] = session + return session + + async def get_session(self, session_id: str) -> SessionState | None: + """Get session by ID. + + Args: + session_id: Session identifier + + Returns: + Session state or None if not found + """ + async with self._lock: + return self._sessions.get(session_id) + + async def remove_session(self, session_id: str) -> None: + """Remove a session. + + Args: + session_id: Session identifier + """ + async with self._lock: + if session_id in self._sessions: + # Signal cancellation + self._sessions[session_id].cancel_event.set() + del self._sessions[session_id] + + async def cancel_session(self, session_id: str) -> None: + """Cancel ongoing operations for a session. + + Args: + session_id: Session identifier + """ + async with self._lock: + if session_id in self._sessions: + self._sessions[session_id].cancel_event.set() diff --git a/pyproject.toml b/pyproject.toml index 4da77c0..9809739 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,10 +19,12 @@ dependencies = [ "prompt-toolkit>=3.0.0", "pip>=25.3", "pipx>=1.8.0", + "agent-client-protocol>=0.1.0", ] [project.scripts] mini-agent = "mini_agent.cli:main" +mini-agent-acp = "mini_agent.acp.server:main" [project.optional-dependencies] dev = [ From e37dcf76aea7d2cf6b0587be2ebf3e22df821173 Mon Sep 17 00:00:00 2001 From: seroxdesign Date: Tue, 11 Nov 2025 11:31:57 -0500 Subject: [PATCH 2/4] feat(acp): ACP server compatibility and tool streaming\n\n- Fix ACP stopReason values to enum-compliant (refusal, max_turn_requests, cancelled)\n- Use raw_input/raw_output and status with start/update_tool_call\n- Pass tool schemas to LLM, not tool objects\n- InitializeRequest protocolVersion compatibility patch (schema_fix)\n- Fallback logger path when HOME not writable\n- Offline-safe token counting fallback for file tools\n\nDocs:\n- Expand README and ACP README with Zed setup steps and config guidance\n\nChore:\n- Remove ignored local config with API key; ensure secrets not tracked --- mini_agent/acp/agent.py | 31 ++++++++----- mini_agent/acp/schema_fix.py | 83 ++++++++++++++++++++++++++++++++++ mini_agent/acp/server.py | 8 ++-- mini_agent/config.py | 21 +++++++++ mini_agent/logger.py | 13 ++++-- mini_agent/tools/file_tools.py | 22 +++++++-- 6 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 mini_agent/acp/schema_fix.py diff --git a/mini_agent/acp/agent.py b/mini_agent/acp/agent.py index b2cca4f..51c0571 100644 --- a/mini_agent/acp/agent.py +++ b/mini_agent/acp/agent.py @@ -81,7 +81,8 @@ async def initialize(self, params: InitializeRequest) -> InitializeResponse: logger.info("Initializing ACP agent (protocol v%s)", params.protocolVersion) return InitializeResponse( - protocolVersion=PROTOCOL_VERSION, + # Some versions of the ACP Python SDK expect a string here + protocolVersion=str(PROTOCOL_VERSION), agentCapabilities=AgentCapabilities( supportsLoadSession=False, # Session persistence not yet implemented supportsSetMode=False, # Mode switching not yet implemented @@ -164,7 +165,7 @@ async def prompt(self, params: PromptRequest) -> PromptResponse: session = await self._session_manager.get_session(params.sessionId) if not session: logger.error("Session not found: %s", params.sessionId) - return PromptResponse(stopReason="error") + return PromptResponse(stopReason="refusal") logger.info("Processing prompt for session: %s", params.sessionId) @@ -192,7 +193,8 @@ async def prompt(self, params: PromptRequest) -> PromptResponse: params.sessionId, update_agent_message(text_block(f"Error: {str(e)}")), ) - return PromptResponse(stopReason="error") + # 'error' is not a valid stopReason per ACP schema + return PromptResponse(stopReason="refusal") async def cancel(self, params: CancelNotification) -> None: """Cancel ongoing operations for a session. @@ -255,10 +257,10 @@ async def _run_agent_with_streaming(self, session) -> str: # Generate LLM response # Note: Agent stores LLM client as 'llm' attribute try: - # Convert tools dict to list for LLM client - tools_list = list(agent.tools.values()) + # Convert tool objects to API schemas for LLM client + tools_schema = [t.to_schema() for t in agent.tools.values()] response = await agent.llm.generate( - messages=session.messages, tools=tools_list + messages=session.messages, tools=tools_schema ) except Exception as e: logger.exception("LLM generation error: %s", e) @@ -266,7 +268,8 @@ async def _run_agent_with_streaming(self, session) -> str: session.session_id, update_agent_message(text_block(f"LLM Error: {str(e)}")), ) - return "error" + # Map internal error to a valid ACP stopReason + return "refusal" # Stream thinking content if present (MiniMax feature) if response.thinking: @@ -303,7 +306,8 @@ async def _run_agent_with_streaming(self, session) -> str: # Max steps reached logger.warning("Max steps reached for session: %s", session.session_id) - return "max_steps" + # Use ACP-compliant stop reason value + return "max_turn_requests" async def _execute_tool_with_streaming(self, session, tool_call) -> None: """Execute a tool call with streaming updates. @@ -315,14 +319,14 @@ async def _execute_tool_with_streaming(self, session, tool_call) -> None: tool_name = tool_call.function.name tool_id = tool_call.id - # Start tool call + # Start tool call (send raw_input to preserve structured args) await self._send_update( session.session_id, start_tool_call( tool_id, f"Executing {tool_name}", - name=tool_name, - arguments=tool_call.function.arguments, + status="in_progress", + raw_input=tool_call.function.arguments, ), ) @@ -369,6 +373,11 @@ async def _execute_tool_with_streaming(self, session, tool_call) -> None: tool_id, status=status, content=[tool_content(text_block(result_text))], + raw_output=(getattr(result, "model_dump", None) or getattr(result, "dict", None) or (lambda: None))() or { + "success": getattr(result, "success", None), + "content": getattr(result, "content", None), + "error": getattr(result, "error", None), + }, ), ) diff --git a/mini_agent/acp/schema_fix.py b/mini_agent/acp/schema_fix.py new file mode 100644 index 0000000..d69101c --- /dev/null +++ b/mini_agent/acp/schema_fix.py @@ -0,0 +1,83 @@ +"""Fix for ACP schema to handle clients sending protocolVersion as string. + +Some ACP clients (like Zed) incorrectly send protocolVersion as "1.0.0" +instead of an integer as specified in the ACP spec. This module patches +the ACP library to handle both formats gracefully. +""" +import logging +from typing import Any + +from pydantic import field_validator + +import acp.schema + +logger = logging.getLogger(__name__) + + +# Save reference to original InitializeRequest class +_original_init_request = acp.schema.InitializeRequest + + +class FixedInitializeRequest(_original_init_request): + """Fixed InitializeRequest that accepts string protocolVersion from non-compliant clients. + + ACP spec requires protocolVersion to be an integer (uint16), but some clients + send it as a string like "1.0.0". This class handles both formats. + """ + + @field_validator('protocolVersion', mode='before') + @classmethod + def convert_protocol_version(cls, v: Any) -> int: + """Convert protocolVersion to int, handling string formats. + + Args: + v: The protocolVersion value (int or string) + + Returns: + Integer protocol version + + Examples: + - "1.0.0" -> 1 + - "2" -> 2 + - 1 -> 1 + """ + if isinstance(v, str): + # Handle version strings like "1.0.0", "2.1", etc. + # Extract the major version number + try: + # Split on '.' and take first part + major_version = v.split('.')[0] + result = int(major_version) + logger.debug(f"Converted string protocolVersion '{v}' to int {result}") + return result + except (ValueError, IndexError, AttributeError) as e: + logger.warning(f"Failed to parse protocolVersion '{v}': {e}. Defaulting to 1") + return 1 # Default to version 1 + elif isinstance(v, (int, float)): + # Already numeric, just convert to int + return int(v) + else: + logger.warning(f"Unexpected protocolVersion type {type(v)}: {v}. Defaulting to 1") + return 1 + + +# Rebuild the pydantic model to apply our validator +# This is CRITICAL because Pydantic v2 compiles validators at class creation time +try: + FixedInitializeRequest.model_rebuild(force=True) + logger.debug("Rebuilt FixedInitializeRequest pydantic model") +except Exception as e: + logger.warning(f"Could not rebuild pydantic model: {e}") + +# Replace the original class with our fixed version +acp.schema.InitializeRequest = FixedInitializeRequest +logger.info("Applied InitializeRequest monkeypatch for protocolVersion compatibility") + + +def apply_fixes(): + """Apply all ACP schema fixes. + + This function is called by server.py to ensure all fixes are applied. + The monkeypatch is actually applied on module import, so this is a no-op. + """ + pass diff --git a/mini_agent/acp/server.py b/mini_agent/acp/server.py index 2ece389..c0e452b 100644 --- a/mini_agent/acp/server.py +++ b/mini_agent/acp/server.py @@ -17,6 +17,7 @@ from mini_agent.tools.file_tools import ReadTool, WriteTool, EditTool from mini_agent.tools.mcp_loader import load_mcp_tools_async from mini_agent.tools.skill_tool import create_skill_tools +from mini_agent.acp import schema_fix # noqa: F401 - ensure monkeypatch is applied on import logger = logging.getLogger(__name__) @@ -73,9 +74,10 @@ def initialize_tools(config: Config, workspace_dir: Path) -> list: # Skills if config.tools.enable_skills: try: - skills = create_skill_tools(config.tools.skills_dir) - tools.extend(skills) - logger.info("Loaded %d skills", len(skills)) + # create_skill_tools returns (tools: list[Tool], loader: Optional[SkillLoader]) + skill_tools, _ = create_skill_tools(config.tools.skills_dir) + tools.extend(skill_tools) + logger.info("Loaded %d skills", len(skill_tools)) except Exception as e: logger.warning("Failed to load skills: %s", e) diff --git a/mini_agent/config.py b/mini_agent/config.py index b7e36f2..a886c1c 100644 --- a/mini_agent/config.py +++ b/mini_agent/config.py @@ -60,6 +60,27 @@ class Config(BaseModel): agent: AgentConfig tools: ToolsConfig + @classmethod + def load(cls) -> "Config": + """Load configuration using default search locations. + + Returns: + Config instance loaded from the highest-priority config file. + + Raises: + FileNotFoundError: if no config file is found + """ + config_path = cls.find_config_file("config.yaml") + if not config_path: + # Provide a clear error pointing users to the example + example = cls.get_package_dir() / "config" / "config-example.yaml" + raise FileNotFoundError( + f"Configuration file not found. Create one at mini_agent/config/config.yaml or ~/.mini-agent/config/config.yaml. " + f"See example: {example}" + ) + + return cls.from_yaml(config_path) + @classmethod def from_yaml(cls, config_path: str | Path) -> "Config": """Load configuration from YAML file diff --git a/mini_agent/logger.py b/mini_agent/logger.py index 6b1e9dd..924d5bd 100644 --- a/mini_agent/logger.py +++ b/mini_agent/logger.py @@ -21,9 +21,16 @@ def __init__(self): Logs are stored in ~/.mini-agent/log/ directory """ - # Use ~/.mini-agent/log/ directory for logs - self.log_dir = Path.home() / ".mini-agent" / "log" - self.log_dir.mkdir(parents=True, exist_ok=True) + # Prefer ~/.mini-agent/log/ directory for logs + preferred = Path.home() / ".mini-agent" / "log" + try: + preferred.mkdir(parents=True, exist_ok=True) + self.log_dir = preferred + except Exception: + # Fallback to workspace-local directory when HOME is not writable + fallback = Path.cwd() / ".mini-agent" / "log" + fallback.mkdir(parents=True, exist_ok=True) + self.log_dir = fallback self.log_file = None self.log_index = 0 diff --git a/mini_agent/tools/file_tools.py b/mini_agent/tools/file_tools.py index 74b7eee..1bbdbea 100644 --- a/mini_agent/tools/file_tools.py +++ b/mini_agent/tools/file_tools.py @@ -3,7 +3,10 @@ from pathlib import Path from typing import Any -import tiktoken +try: + import tiktoken # type: ignore +except Exception: # pragma: no cover - optional dependency at runtime + tiktoken = None # Fallback when network or package data unavailable from .base import Tool, ToolResult @@ -29,16 +32,25 @@ def truncate_text_by_tokens( >>> truncated = truncate_text_by_tokens(text, 64000) >>> print(truncated) """ - encoding = tiktoken.get_encoding("cl100k_base") - token_count = len(encoding.encode(text)) + # Try accurate tokenization when available; otherwise fall back to chars + try: + if tiktoken is not None: + encoding = tiktoken.get_encoding("cl100k_base") + token_count = len(encoding.encode(text)) + else: + raise RuntimeError("tiktoken not available") + except Exception: + # Fallback: approximate tokens by characters (4 chars ≈ 1 token) + token_count = int(len(text) / 4) # Return original text if under limit if token_count <= max_tokens: return text # Calculate token/character ratio for approximation - char_count = len(text) - ratio = token_count / char_count + char_count = max(1, len(text)) + # Approximate ratio; when using fallback, inverse of 4 chars per token + ratio = token_count / char_count if token_count else 1.0 # Keep head and tail mode: allocate half space for each (with 5% safety margin) chars_per_half = int((max_tokens / 2) / ratio * 0.95) From 94109895fe40070ddb25cec23c1db8051e6fbcd3 Mon Sep 17 00:00:00 2001 From: seroxdesign Date: Tue, 11 Nov 2025 11:32:31 -0500 Subject: [PATCH 3/4] docs(acp): add Zed integration steps, version verification, and config guidance --- README.md | 32 ++++++++++++++++++++------------ mini_agent/acp/README.md | 30 +++++++++++++++++++----------- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index f17b2ad..d4ef44f 100644 --- a/README.md +++ b/README.md @@ -248,18 +248,26 @@ mini-agent-acp ### Integration with Zed -Add to your Zed agent configuration: - -```json -{ - "agents": [ - { - "name": "mini-agent", - "command": "mini-agent-acp" - } - ] -} -``` +Zed communicates with external agents over ACP via stdio. To use Mini‑Agent in Zed, point Zed to the exact executable you installed (ideally the venv binary): + +1) Open the Agent Panel (cmd-?) and create a New External Agent → ACP. +2) Set Command to your venv path, for example: + - `/Users/sero/projects/Mini-Agent/.venv/bin/mini-agent-acp` +3) Optionally set the working directory to your project path. +4) Start a thread and talk to the agent. Tool progress and results stream in real time. + +Tips for consistent versions: +- Prefer the absolute venv path over relying on PATH. +- If needed, wrap the command to print the installed version before exec: + - Command: `/bin/bash` + - Args: `-lc`, `echo 'mini-agent:' $(python -c 'import importlib.metadata as m; print(m.version("mini-agent"))') 'at' $(python -c 'import mini_agent; print(mini_agent.__file__)') >&2; exec /Users/sero/projects/Mini-Agent/.venv/bin/mini-agent-acp` + +Configuration search order: +- `mini_agent/config/config.yaml` (dev checkout) +- `~/.mini-agent/config/config.yaml` (recommended for keys) +- `/mini_agent/config/config.yaml` + +Security: never commit API keys. Use `~/.mini-agent/config/config.yaml` and keep `config.yaml` out of version control (already ignored). For detailed ACP documentation, see [mini_agent/acp/README.md](mini_agent/acp/README.md). diff --git a/mini_agent/acp/README.md b/mini_agent/acp/README.md index 1279979..dbbff14 100644 --- a/mini_agent/acp/README.md +++ b/mini_agent/acp/README.md @@ -79,18 +79,25 @@ The server will: ### With Zed Editor -Add to your Zed agent configuration: +Zed speaks ACP over stdio. Point Zed directly at your Mini‑Agent executable (ideally the virtualenv path) to ensure it runs the same version you installed locally. -```json -{ - "agents": [ - { - "name": "mini-agent", - "command": "mini-agent-acp" - } - ] -} -``` +Steps: +- Open the Agent Panel (cmd-?) → New External Agent → ACP. +- Command: `/absolute/path/to/venv/bin/mini-agent-acp` +- Optional: set Working Directory to your project path. +- Start a thread. You should see streaming updates for thoughts, messages, and tools. + +Verifying the version Zed runs: +- Use a small wrapper to print version + module path, then exec Mini‑Agent: + - Command: `/bin/bash` + - Args: `-lc`, `echo 'mini-agent:' $(python -c 'import importlib.metadata as m; print(m.version("mini-agent"))') 'at' $(python -c 'import mini_agent; print(mini_agent.__file__)') >&2; exec /absolute/path/to/venv/bin/mini-agent-acp` + +Configuration locations (searched in order): +1) `mini_agent/config/config.yaml` (development) +2) `~/.mini-agent/config/config.yaml` (recommended for API keys) +3) Installed package config + +Security: never commit secrets. Keep keys in `~/.mini-agent/config/config.yaml`. The repository ignores `config.yaml` by default. ### Programmatically @@ -219,6 +226,7 @@ This implementation follows the [Agent Client Protocol](https://agentclientproto - ✅ Session management (create, prompt, cancel) - ✅ Real-time streaming via `sessionUpdate` notifications - ✅ Tool execution with progress tracking +- ✅ Stop reasons map to ACP enum values (`end_turn`, `max_tokens`, `max_turn_requests`, `refusal`, `cancelled`) - ✅ Bidirectional requests (agent → client) - ⚠️ Session persistence (not yet implemented) - ⚠️ Mode switching (not yet implemented) From 7fc6df85d08ec5373b69c369eeeeb006088aff90 Mon Sep 17 00:00:00 2001 From: seroxdesign Date: Tue, 11 Nov 2025 11:45:08 -0500 Subject: [PATCH 4/4] fix(acp): file tools resolve relative to session cwd; doc note on path resolution --- mini_agent/acp/README.md | 5 +++++ mini_agent/acp/session.py | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/mini_agent/acp/README.md b/mini_agent/acp/README.md index dbbff14..3b1b86f 100644 --- a/mini_agent/acp/README.md +++ b/mini_agent/acp/README.md @@ -87,6 +87,11 @@ Steps: - Optional: set Working Directory to your project path. - Start a thread. You should see streaming updates for thoughts, messages, and tools. +Path resolution for file tools: +- In ACP mode, file tool paths are resolved relative to the session working directory (the `cwd` Zed sends, typically your project root). +- Use absolute paths if you need to operate outside the project root. +- If a relative path doesn’t exist, read tools will return a “File not found” error; they do not create files. + Verifying the version Zed runs: - Use a small wrapper to print version + module path, then exec Mini‑Agent: - Command: `/bin/bash` diff --git a/mini_agent/acp/session.py b/mini_agent/acp/session.py index 5b4109e..f0ced4f 100644 --- a/mini_agent/acp/session.py +++ b/mini_agent/acp/session.py @@ -15,6 +15,7 @@ from mini_agent.agent import Agent from mini_agent.llm import LLMClient from mini_agent.schema.schema import Message +from mini_agent.tools.file_tools import ReadTool, WriteTool, EditTool @dataclass @@ -76,11 +77,20 @@ async def create_session( if session_id in self._sessions: raise ValueError(f"Session {session_id} already exists") - # Create session-specific agent - # Note: Each session gets its own workspace + # Rebind file tools to the session's working directory + session_tools = [] + for tool in self._tools: + if isinstance(tool, (ReadTool, WriteTool, EditTool)): + # Create a fresh instance bound to session cwd + session_tools.append(tool.__class__(workspace_dir=cwd)) + else: + # Reuse non-filesystem tools as-is + session_tools.append(tool) + + # Create session-specific agent (workspace set to cwd) agent = Agent( llm_client=self._llm_client, - tools=self._tools, + tools=session_tools, system_prompt=self._system_prompt, workspace_dir=Path(cwd), )