agentpool 2.1.9__py3-none-any.whl → 2.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acp/__init__.py +13 -0
- acp/bridge/README.md +15 -2
- acp/bridge/__init__.py +3 -2
- acp/bridge/__main__.py +60 -19
- acp/bridge/ws_server.py +173 -0
- acp/bridge/ws_server_cli.py +89 -0
- acp/notifications.py +2 -1
- acp/stdio.py +39 -9
- acp/transports.py +362 -2
- acp/utils.py +15 -2
- agentpool/__init__.py +4 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +203 -88
- agentpool/agents/acp_agent/acp_converters.py +46 -21
- agentpool/agents/acp_agent/client_handler.py +157 -3
- agentpool/agents/acp_agent/session_state.py +4 -1
- agentpool/agents/agent.py +314 -107
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +90 -21
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/base_agent.py +163 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
- agentpool/agents/claude_code_agent/converters.py +71 -3
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +2 -0
- agentpool/agents/events/builtin_handlers.py +2 -1
- agentpool/agents/events/event_emitter.py +29 -2
- agentpool/agents/events/events.py +20 -0
- agentpool/agents/modes.py +54 -0
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/common_types.py +21 -0
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/claude_code_agent.yml +3 -0
- agentpool/delegation/pool.py +37 -29
- agentpool/delegation/team.py +1 -0
- agentpool/delegation/teamrun.py +1 -0
- agentpool/diagnostics/__init__.py +53 -0
- agentpool/diagnostics/lsp_manager.py +1593 -0
- agentpool/diagnostics/lsp_proxy.py +41 -0
- agentpool/diagnostics/lsp_proxy_script.py +229 -0
- agentpool/diagnostics/models.py +398 -0
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +12 -3
- agentpool/mcp_server/manager.py +25 -31
- agentpool/mcp_server/registries/official_registry_client.py +25 -0
- agentpool/mcp_server/tool_bridge.py +78 -66
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/message_history.py +12 -0
- agentpool/messaging/messages.py +52 -9
- agentpool/messaging/processing.py +3 -1
- agentpool/models/acp_agents/base.py +0 -22
- agentpool/models/acp_agents/mcp_capable.py +8 -148
- agentpool/models/acp_agents/non_mcp.py +129 -72
- agentpool/models/agents.py +35 -13
- agentpool/models/claude_code_agents.py +33 -2
- agentpool/models/manifest.py +43 -0
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +9 -1
- agentpool/resource_providers/aggregating.py +52 -3
- agentpool/resource_providers/base.py +57 -1
- agentpool/resource_providers/mcp_provider.py +23 -0
- agentpool/resource_providers/plan_provider.py +130 -41
- agentpool/resource_providers/pool.py +2 -0
- agentpool/resource_providers/static.py +2 -0
- agentpool/sessions/__init__.py +2 -1
- agentpool/sessions/manager.py +31 -2
- agentpool/sessions/models.py +50 -0
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +217 -1
- agentpool/testing.py +537 -19
- agentpool/utils/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +690 -1
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +4 -0
- agentpool_cli/serve_acp.py +41 -20
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_commands/__init__.py +30 -0
- agentpool_commands/agents.py +74 -1
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +51 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/mcp_server.py +131 -1
- agentpool_config/storage.py +46 -1
- agentpool_config/tools.py +7 -1
- agentpool_config/toolsets.py +92 -148
- agentpool_server/acp_server/acp_agent.py +134 -150
- agentpool_server/acp_server/commands/acp_commands.py +216 -51
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
- agentpool_server/acp_server/server.py +23 -79
- agentpool_server/acp_server/session.py +181 -19
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +362 -0
- agentpool_server/opencode_server/__init__.py +27 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +869 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +269 -0
- agentpool_server/opencode_server/models/__init__.py +228 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +60 -0
- agentpool_server/opencode_server/models/base.py +26 -0
- agentpool_server/opencode_server/models/common.py +23 -0
- agentpool_server/opencode_server/models/config.py +37 -0
- agentpool_server/opencode_server/models/events.py +647 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +25 -0
- agentpool_server/opencode_server/models/message.py +162 -0
- agentpool_server/opencode_server/models/parts.py +190 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/session.py +99 -0
- agentpool_server/opencode_server/routes/__init__.py +25 -0
- agentpool_server/opencode_server/routes/agent_routes.py +442 -0
- agentpool_server/opencode_server/routes/app_routes.py +139 -0
- agentpool_server/opencode_server/routes/config_routes.py +241 -0
- agentpool_server/opencode_server/routes/file_routes.py +392 -0
- agentpool_server/opencode_server/routes/global_routes.py +94 -0
- agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
- agentpool_server/opencode_server/routes/message_routes.py +705 -0
- agentpool_server/opencode_server/routes/pty_routes.py +299 -0
- agentpool_server/opencode_server/routes/session_routes.py +1205 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +430 -0
- agentpool_server/opencode_server/state.py +121 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +16 -0
- agentpool_storage/base.py +103 -0
- agentpool_storage/claude_provider.py +907 -0
- agentpool_storage/file_provider.py +129 -0
- agentpool_storage/memory_provider.py +61 -0
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider.py +730 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +6 -0
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +134 -1
- agentpool_storage/sql_provider/utils.py +10 -1
- agentpool_storage/text_log_provider.py +1 -0
- agentpool_toolsets/builtin/__init__.py +0 -8
- agentpool_toolsets/builtin/code.py +95 -56
- agentpool_toolsets/builtin/debug.py +16 -21
- agentpool_toolsets/builtin/execution_environment.py +99 -103
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +86 -4
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +74 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +454 -0
- agentpool_toolsets/mcp_run_toolset.py +84 -6
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/licenses/LICENSE +0 -0
acp/transports.py
CHANGED
|
@@ -1,19 +1,331 @@
|
|
|
1
|
+
"""Transport abstractions for ACP agents.
|
|
2
|
+
|
|
3
|
+
This module provides transport configuration classes and utilities for running
|
|
4
|
+
ACP agents over different transports (stdio, WebSocket, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
from __future__ import annotations
|
|
2
8
|
|
|
9
|
+
import asyncio
|
|
3
10
|
import contextlib
|
|
4
11
|
from contextlib import asynccontextmanager
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
import logging
|
|
5
14
|
import os
|
|
6
15
|
import subprocess
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
8
17
|
|
|
9
18
|
import anyio
|
|
10
19
|
|
|
11
20
|
|
|
12
21
|
if TYPE_CHECKING:
|
|
13
|
-
from collections.abc import AsyncIterator, Mapping
|
|
22
|
+
from collections.abc import AsyncIterator, Callable, Mapping
|
|
14
23
|
from pathlib import Path
|
|
15
24
|
|
|
16
25
|
from anyio.abc import ByteReceiveStream, ByteSendStream, Process
|
|
26
|
+
from websockets.asyncio.server import ServerConnection
|
|
27
|
+
|
|
28
|
+
from acp.agent.connection import AgentSideConnection
|
|
29
|
+
from acp.agent.protocol import Agent
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# =============================================================================
|
|
35
|
+
# Transport Configuration Classes
|
|
36
|
+
# =============================================================================
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class StdioTransport:
|
|
41
|
+
"""Configuration for stdio transport.
|
|
42
|
+
|
|
43
|
+
This is the default transport for ACP agents, communicating over
|
|
44
|
+
stdin/stdout streams.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class WebSocketTransport:
|
|
50
|
+
"""Configuration for WebSocket server transport.
|
|
51
|
+
|
|
52
|
+
Runs an ACP agent as a WebSocket server that accepts client connections.
|
|
53
|
+
Each client connection gets its own agent instance.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
host: Host to bind the WebSocket server to.
|
|
57
|
+
port: Port for the WebSocket server.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
host: str = "localhost"
|
|
61
|
+
port: int = 8765
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class StreamTransport:
|
|
66
|
+
"""Configuration for custom stream transport.
|
|
67
|
+
|
|
68
|
+
Allows passing pre-created streams for the agent to use.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
reader: Stream to read incoming messages from.
|
|
72
|
+
writer: Stream to write outgoing messages to.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
reader: ByteReceiveStream
|
|
76
|
+
writer: ByteSendStream
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Type alias for all supported transports
|
|
80
|
+
Transport = StdioTransport | WebSocketTransport | StreamTransport | Literal["stdio", "websocket"]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# =============================================================================
|
|
84
|
+
# Transport Runner
|
|
85
|
+
# =============================================================================
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def serve(
|
|
89
|
+
agent: Agent | Callable[[AgentSideConnection], Agent],
|
|
90
|
+
transport: Transport = "stdio",
|
|
91
|
+
*,
|
|
92
|
+
shutdown_event: asyncio.Event | None = None,
|
|
93
|
+
debug_file: str | None = None,
|
|
94
|
+
**kwargs: Any,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Run an ACP agent with the specified transport.
|
|
97
|
+
|
|
98
|
+
This is the main entry point for running ACP agents. It handles transport
|
|
99
|
+
setup and lifecycle management automatically.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
agent: An Agent implementation or a factory callable that takes
|
|
103
|
+
an AgentSideConnection and returns an Agent.
|
|
104
|
+
transport: Transport configuration. Can be:
|
|
105
|
+
- "stdio" or StdioTransport(): Use stdin/stdout
|
|
106
|
+
- "websocket" or WebSocketTransport(...): Run WebSocket server
|
|
107
|
+
- StreamTransport(...): Use custom streams
|
|
108
|
+
shutdown_event: Optional event to signal shutdown. If not provided,
|
|
109
|
+
runs until cancelled.
|
|
110
|
+
debug_file: Optional file path for debug message logging.
|
|
111
|
+
**kwargs: Additional keyword arguments passed to AgentSideConnection.
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
```python
|
|
115
|
+
# Stdio (default)
|
|
116
|
+
await serve(MyAgent())
|
|
117
|
+
|
|
118
|
+
# WebSocket server
|
|
119
|
+
await serve(MyAgent(), WebSocketTransport(host="0.0.0.0", port=9000))
|
|
120
|
+
|
|
121
|
+
# With shutdown control
|
|
122
|
+
shutdown = asyncio.Event()
|
|
123
|
+
task = asyncio.create_task(serve(MyAgent(), shutdown_event=shutdown))
|
|
124
|
+
# ... later ...
|
|
125
|
+
shutdown.set()
|
|
126
|
+
await task
|
|
127
|
+
```
|
|
128
|
+
"""
|
|
129
|
+
# Normalize string shortcuts to config objects
|
|
130
|
+
match transport:
|
|
131
|
+
case "stdio":
|
|
132
|
+
transport = StdioTransport()
|
|
133
|
+
case "websocket":
|
|
134
|
+
transport = WebSocketTransport()
|
|
135
|
+
|
|
136
|
+
# Dispatch to appropriate runner
|
|
137
|
+
match transport:
|
|
138
|
+
case StdioTransport():
|
|
139
|
+
await _serve_stdio(agent, shutdown_event, debug_file, **kwargs)
|
|
140
|
+
case WebSocketTransport(host=host, port=port):
|
|
141
|
+
await _serve_websocket(agent, host, port, shutdown_event, debug_file, **kwargs)
|
|
142
|
+
case StreamTransport(reader=reader, writer=writer):
|
|
143
|
+
await _serve_streams(agent, reader, writer, shutdown_event, debug_file, **kwargs)
|
|
144
|
+
case _:
|
|
145
|
+
msg = f"Unsupported transport: {transport}"
|
|
146
|
+
raise ValueError(msg)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def _serve_stdio(
|
|
150
|
+
agent: Agent | Callable[[AgentSideConnection], Agent],
|
|
151
|
+
shutdown_event: asyncio.Event | None,
|
|
152
|
+
debug_file: str | None,
|
|
153
|
+
**kwargs: Any,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Run agent over stdio."""
|
|
156
|
+
from acp.agent.connection import AgentSideConnection
|
|
157
|
+
from acp.stdio import stdio_streams
|
|
158
|
+
|
|
159
|
+
agent_factory = _ensure_factory(agent)
|
|
160
|
+
reader, writer = await stdio_streams()
|
|
161
|
+
|
|
162
|
+
conn = AgentSideConnection(agent_factory, writer, reader, debug_file=debug_file, **kwargs)
|
|
163
|
+
try:
|
|
164
|
+
if shutdown_event:
|
|
165
|
+
await shutdown_event.wait()
|
|
166
|
+
else:
|
|
167
|
+
await asyncio.Event().wait() # Wait forever
|
|
168
|
+
except asyncio.CancelledError:
|
|
169
|
+
pass
|
|
170
|
+
finally:
|
|
171
|
+
await conn.close()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async def _serve_streams(
|
|
175
|
+
agent: Agent | Callable[[AgentSideConnection], Agent],
|
|
176
|
+
reader: ByteReceiveStream,
|
|
177
|
+
writer: ByteSendStream,
|
|
178
|
+
shutdown_event: asyncio.Event | None,
|
|
179
|
+
debug_file: str | None,
|
|
180
|
+
**kwargs: Any,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Run agent over custom streams."""
|
|
183
|
+
from acp.agent.connection import AgentSideConnection
|
|
184
|
+
|
|
185
|
+
agent_factory = _ensure_factory(agent)
|
|
186
|
+
conn = AgentSideConnection(agent_factory, writer, reader, debug_file=debug_file, **kwargs)
|
|
187
|
+
try:
|
|
188
|
+
if shutdown_event:
|
|
189
|
+
await shutdown_event.wait()
|
|
190
|
+
else:
|
|
191
|
+
await asyncio.Event().wait()
|
|
192
|
+
except asyncio.CancelledError:
|
|
193
|
+
pass
|
|
194
|
+
finally:
|
|
195
|
+
await conn.close()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
async def _serve_websocket(
|
|
199
|
+
agent: Agent | Callable[[AgentSideConnection], Agent],
|
|
200
|
+
host: str,
|
|
201
|
+
port: int,
|
|
202
|
+
shutdown_event: asyncio.Event | None,
|
|
203
|
+
debug_file: str | None,
|
|
204
|
+
**kwargs: Any,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Run agent as WebSocket server."""
|
|
207
|
+
import websockets
|
|
208
|
+
|
|
209
|
+
from acp.agent.connection import AgentSideConnection
|
|
210
|
+
|
|
211
|
+
agent_factory = _ensure_factory(agent)
|
|
212
|
+
shutdown = shutdown_event or asyncio.Event()
|
|
213
|
+
connections: list[AgentSideConnection] = []
|
|
214
|
+
|
|
215
|
+
async def handle_client(websocket: ServerConnection) -> None:
|
|
216
|
+
"""Handle a single WebSocket client connection."""
|
|
217
|
+
logger.info("WebSocket client connected")
|
|
218
|
+
|
|
219
|
+
# Create stream adapters for WebSocket
|
|
220
|
+
ws_reader = _WebSocketReadStream(websocket)
|
|
221
|
+
ws_writer = _WebSocketWriteStream(websocket)
|
|
222
|
+
|
|
223
|
+
conn = AgentSideConnection(
|
|
224
|
+
agent_factory, ws_writer, ws_reader, debug_file=debug_file, **kwargs
|
|
225
|
+
)
|
|
226
|
+
connections.append(conn)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
# Keep connection alive until client disconnects or shutdown
|
|
230
|
+
client_done = asyncio.Event()
|
|
231
|
+
|
|
232
|
+
async def monitor_websocket() -> None:
|
|
233
|
+
try:
|
|
234
|
+
async for _ in websocket:
|
|
235
|
+
pass # Messages handled by ws_reader
|
|
236
|
+
except websockets.exceptions.ConnectionClosed:
|
|
237
|
+
pass
|
|
238
|
+
finally:
|
|
239
|
+
client_done.set()
|
|
240
|
+
|
|
241
|
+
monitor_task = asyncio.create_task(monitor_websocket())
|
|
242
|
+
_done, _ = await asyncio.wait(
|
|
243
|
+
[asyncio.create_task(client_done.wait()), asyncio.create_task(shutdown.wait())],
|
|
244
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
245
|
+
)
|
|
246
|
+
monitor_task.cancel()
|
|
247
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
248
|
+
await monitor_task
|
|
249
|
+
except websockets.exceptions.ConnectionClosed:
|
|
250
|
+
logger.info("WebSocket client disconnected")
|
|
251
|
+
finally:
|
|
252
|
+
connections.remove(conn)
|
|
253
|
+
await conn.close()
|
|
254
|
+
|
|
255
|
+
logger.info("Starting WebSocket server on ws://%s:%d", host, port)
|
|
256
|
+
async with websockets.serve(handle_client, host, port):
|
|
257
|
+
logger.info("WebSocket server running on ws://%s:%d", host, port)
|
|
258
|
+
await shutdown.wait()
|
|
259
|
+
|
|
260
|
+
# Clean up remaining connections
|
|
261
|
+
for conn in connections:
|
|
262
|
+
await conn.close()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class _WebSocketReadStream(anyio.abc.ByteReceiveStream):
|
|
266
|
+
"""Adapter to read from WebSocket as a ByteReceiveStream."""
|
|
267
|
+
|
|
268
|
+
def __init__(self, websocket: Any) -> None:
|
|
269
|
+
self._websocket = websocket
|
|
270
|
+
self._buffer = b""
|
|
271
|
+
|
|
272
|
+
async def receive(self, max_bytes: int = 65536) -> bytes:
|
|
273
|
+
# If we have buffered data, return it
|
|
274
|
+
if self._buffer:
|
|
275
|
+
data = self._buffer[:max_bytes]
|
|
276
|
+
self._buffer = self._buffer[max_bytes:]
|
|
277
|
+
return data
|
|
278
|
+
|
|
279
|
+
# Read from WebSocket
|
|
280
|
+
try:
|
|
281
|
+
message = await self._websocket.recv()
|
|
282
|
+
if isinstance(message, str):
|
|
283
|
+
message = message.encode()
|
|
284
|
+
# Add newline for JSON-RPC line protocol
|
|
285
|
+
if not message.endswith(b"\n"):
|
|
286
|
+
message += b"\n"
|
|
287
|
+
self._buffer = message[max_bytes:]
|
|
288
|
+
return message[:max_bytes] # type: ignore[no-any-return]
|
|
289
|
+
except Exception as e:
|
|
290
|
+
raise anyio.EndOfStream from e
|
|
291
|
+
|
|
292
|
+
async def aclose(self) -> None:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class _WebSocketWriteStream(anyio.abc.ByteSendStream):
|
|
297
|
+
"""Adapter to write to WebSocket as a ByteSendStream."""
|
|
298
|
+
|
|
299
|
+
def __init__(self, websocket: Any) -> None:
|
|
300
|
+
self._websocket = websocket
|
|
301
|
+
|
|
302
|
+
async def send(self, item: bytes) -> None:
|
|
303
|
+
# WebSocket sends complete messages, strip newline if present
|
|
304
|
+
message = item.decode().strip()
|
|
305
|
+
if message:
|
|
306
|
+
await self._websocket.send(message)
|
|
307
|
+
|
|
308
|
+
async def aclose(self) -> None:
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _ensure_factory(
|
|
313
|
+
agent: Agent | Callable[[AgentSideConnection], Agent],
|
|
314
|
+
) -> Callable[[AgentSideConnection], Agent]:
|
|
315
|
+
"""Ensure agent is wrapped in a factory function."""
|
|
316
|
+
if callable(agent) and not hasattr(agent, "initialize"):
|
|
317
|
+
return agent # Already a factory
|
|
318
|
+
|
|
319
|
+
# Wrap instance in factory
|
|
320
|
+
def factory(connection: AgentSideConnection) -> Agent:
|
|
321
|
+
return agent # type: ignore[return-value]
|
|
322
|
+
|
|
323
|
+
return factory
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# =============================================================================
|
|
327
|
+
# Subprocess Transport Utilities (for spawning agents)
|
|
328
|
+
# =============================================================================
|
|
17
329
|
|
|
18
330
|
|
|
19
331
|
DEFAULT_INHERITED_ENV_VARS = (
|
|
@@ -50,6 +362,32 @@ def default_environment() -> dict[str, str]:
|
|
|
50
362
|
return env
|
|
51
363
|
|
|
52
364
|
|
|
365
|
+
async def _drain_stderr_to_log(
|
|
366
|
+
process: Process,
|
|
367
|
+
command: str,
|
|
368
|
+
log_level: int = logging.WARNING,
|
|
369
|
+
) -> None:
|
|
370
|
+
"""Read stderr from a process and log each line.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
process: The subprocess to read stderr from.
|
|
374
|
+
command: Command name for log messages.
|
|
375
|
+
log_level: Log level for stderr output (default WARNING).
|
|
376
|
+
"""
|
|
377
|
+
if process.stderr is None:
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
async for line_bytes in process.stderr:
|
|
382
|
+
line = line_bytes.decode(errors="replace").rstrip()
|
|
383
|
+
if line:
|
|
384
|
+
logger.log(log_level, "[%s stderr] %s", command, line)
|
|
385
|
+
except anyio.EndOfStream:
|
|
386
|
+
pass
|
|
387
|
+
except Exception: # noqa: BLE001
|
|
388
|
+
logger.debug("Error reading stderr from %s", command, exc_info=True)
|
|
389
|
+
|
|
390
|
+
|
|
53
391
|
@asynccontextmanager
|
|
54
392
|
async def spawn_stdio_transport(
|
|
55
393
|
command: str,
|
|
@@ -58,11 +396,23 @@ async def spawn_stdio_transport(
|
|
|
58
396
|
cwd: str | Path | None = None,
|
|
59
397
|
stderr: int | None = subprocess.PIPE,
|
|
60
398
|
shutdown_timeout: float = 2.0,
|
|
399
|
+
log_stderr: bool = False,
|
|
400
|
+
stderr_log_level: int = logging.WARNING,
|
|
61
401
|
) -> AsyncIterator[tuple[ByteReceiveStream, ByteSendStream, Process]]:
|
|
62
402
|
"""Launch a subprocess and expose its stdio streams as anyio streams.
|
|
63
403
|
|
|
64
404
|
This mirrors the defensive shutdown behaviour used by the MCP Python SDK:
|
|
65
405
|
close stdin first, wait for graceful exit, then escalate to terminate/kill.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
command: The command to execute.
|
|
409
|
+
*args: Arguments for the command.
|
|
410
|
+
env: Environment variables (merged with defaults).
|
|
411
|
+
cwd: Working directory for the subprocess.
|
|
412
|
+
stderr: How to handle stderr (default: subprocess.PIPE).
|
|
413
|
+
shutdown_timeout: Timeout for graceful shutdown.
|
|
414
|
+
log_stderr: If True, read stderr in background and log each line.
|
|
415
|
+
stderr_log_level: Log level for stderr output (default WARNING).
|
|
66
416
|
"""
|
|
67
417
|
merged_env = default_environment()
|
|
68
418
|
if env:
|
|
@@ -83,9 +433,19 @@ async def spawn_stdio_transport(
|
|
|
83
433
|
msg = "spawn_stdio_transport requires stdout/stdin pipes"
|
|
84
434
|
raise RuntimeError(msg)
|
|
85
435
|
|
|
436
|
+
stderr_task: asyncio.Task[None] | None = None
|
|
437
|
+
if log_stderr and process.stderr is not None:
|
|
438
|
+
stderr_task = asyncio.create_task(_drain_stderr_to_log(process, command, stderr_log_level))
|
|
439
|
+
|
|
86
440
|
try:
|
|
87
441
|
yield process.stdout, process.stdin, process
|
|
88
442
|
finally:
|
|
443
|
+
# Cancel stderr logging task
|
|
444
|
+
if stderr_task is not None:
|
|
445
|
+
stderr_task.cancel()
|
|
446
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
447
|
+
await stderr_task
|
|
448
|
+
|
|
89
449
|
# Attempt graceful stdin shutdown first
|
|
90
450
|
if process.stdin is not None:
|
|
91
451
|
with contextlib.suppress(Exception):
|
acp/utils.py
CHANGED
|
@@ -122,8 +122,16 @@ def generate_tool_title(tool_name: str, tool_input: dict[str, Any]) -> str: # n
|
|
|
122
122
|
"""
|
|
123
123
|
name_lower = tool_name.lower()
|
|
124
124
|
|
|
125
|
+
# Exact matches for common tool names (OpenCode compatibility)
|
|
126
|
+
if name_lower == "read" and (path := tool_input.get("path") or tool_input.get("file_path")):
|
|
127
|
+
return f"Reading: {path}"
|
|
128
|
+
if name_lower in ("write", "edit") and (
|
|
129
|
+
path := tool_input.get("path") or tool_input.get("file_path")
|
|
130
|
+
):
|
|
131
|
+
return f"Editing: {path}"
|
|
132
|
+
|
|
125
133
|
# Command/script execution - show the command
|
|
126
|
-
if any(k in name_lower for k in ["command", "execute", "run", "shell", "script"]) and (
|
|
134
|
+
if any(k in name_lower for k in ["command", "execute", "run", "shell", "script", "bash"]) and (
|
|
127
135
|
cmd := tool_input.get("command")
|
|
128
136
|
):
|
|
129
137
|
# Truncate long commands
|
|
@@ -217,6 +225,11 @@ def infer_tool_kind(tool_name: str) -> ToolCallKind: # noqa: PLR0911
|
|
|
217
225
|
Tool kind string for ACP protocol
|
|
218
226
|
"""
|
|
219
227
|
name_lower = tool_name.lower()
|
|
228
|
+
|
|
229
|
+
# Exact matches for common tool names (OpenCode compatibility)
|
|
230
|
+
if name_lower in ("read", "write", "edit"):
|
|
231
|
+
return "read" if name_lower == "read" else "edit"
|
|
232
|
+
|
|
220
233
|
if any(i in name_lower for i in ["read", "load", "get"]) and any(
|
|
221
234
|
i in name_lower for i in ["file", "path", "content"]
|
|
222
235
|
):
|
|
@@ -231,7 +244,7 @@ def infer_tool_kind(tool_name: str) -> ToolCallKind: # noqa: PLR0911
|
|
|
231
244
|
return "move"
|
|
232
245
|
if any(i in name_lower for i in ["search", "find", "query", "lookup"]):
|
|
233
246
|
return "search"
|
|
234
|
-
if any(i in name_lower for i in ["execute", "run", "exec", "command", "shell"]):
|
|
247
|
+
if any(i in name_lower for i in ["execute", "run", "exec", "command", "shell", "bash"]):
|
|
235
248
|
return "execute"
|
|
236
249
|
if any(i in name_lower for i in ["think", "plan", "reason", "analyze"]):
|
|
237
250
|
return "think"
|
agentpool/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ from agentpool.models.manifest import AgentsManifest
|
|
|
13
13
|
|
|
14
14
|
# Builtin toolsets imports removed to avoid circular dependency
|
|
15
15
|
# Import them directly from agentpool_toolsets.builtin when needed
|
|
16
|
-
from agentpool.agents import Agent, AgentContext
|
|
16
|
+
from agentpool.agents import Agent, AgentContext, ClaudeCodeAgent, ACPAgent, AGUIAgent
|
|
17
17
|
from agentpool.delegation import AgentPool, Team, TeamRun, BaseTeam
|
|
18
18
|
from dotenv import load_dotenv
|
|
19
19
|
from agentpool.messaging.messages import ChatMessage
|
|
@@ -40,6 +40,8 @@ __url__ = "https://github.com/phil65/agentpool"
|
|
|
40
40
|
load_dotenv()
|
|
41
41
|
|
|
42
42
|
__all__ = [
|
|
43
|
+
"ACPAgent",
|
|
44
|
+
"AGUIAgent",
|
|
43
45
|
"Agent",
|
|
44
46
|
"AgentContext",
|
|
45
47
|
"AgentPool",
|
|
@@ -49,6 +51,7 @@ __all__ = [
|
|
|
49
51
|
"BinaryContent",
|
|
50
52
|
"BinaryImage",
|
|
51
53
|
"ChatMessage",
|
|
54
|
+
"ClaudeCodeAgent",
|
|
52
55
|
"DocumentUrl",
|
|
53
56
|
"ImageUrl",
|
|
54
57
|
"MessageNode",
|
agentpool/agents/__init__.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from agentpool.agents.agent import Agent
|
|
6
6
|
from agentpool.agents.agui_agent import AGUIAgent
|
|
7
|
+
from agentpool.agents.acp_agent import ACPAgent
|
|
7
8
|
from agentpool.agents.claude_code_agent import ClaudeCodeAgent
|
|
8
9
|
from agentpool.agents.events import (
|
|
9
10
|
detailed_print_handler,
|
|
@@ -17,6 +18,7 @@ from agentpool.agents.sys_prompts import SystemPrompts
|
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
__all__ = [
|
|
21
|
+
"ACPAgent",
|
|
20
22
|
"AGUIAgent",
|
|
21
23
|
"Agent",
|
|
22
24
|
"AgentContext",
|