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.
Files changed (174) hide show
  1. acp/__init__.py +13 -0
  2. acp/bridge/README.md +15 -2
  3. acp/bridge/__init__.py +3 -2
  4. acp/bridge/__main__.py +60 -19
  5. acp/bridge/ws_server.py +173 -0
  6. acp/bridge/ws_server_cli.py +89 -0
  7. acp/notifications.py +2 -1
  8. acp/stdio.py +39 -9
  9. acp/transports.py +362 -2
  10. acp/utils.py +15 -2
  11. agentpool/__init__.py +4 -1
  12. agentpool/agents/__init__.py +2 -0
  13. agentpool/agents/acp_agent/acp_agent.py +203 -88
  14. agentpool/agents/acp_agent/acp_converters.py +46 -21
  15. agentpool/agents/acp_agent/client_handler.py +157 -3
  16. agentpool/agents/acp_agent/session_state.py +4 -1
  17. agentpool/agents/agent.py +314 -107
  18. agentpool/agents/agui_agent/__init__.py +0 -2
  19. agentpool/agents/agui_agent/agui_agent.py +90 -21
  20. agentpool/agents/agui_agent/agui_converters.py +0 -131
  21. agentpool/agents/base_agent.py +163 -1
  22. agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
  23. agentpool/agents/claude_code_agent/converters.py +71 -3
  24. agentpool/agents/claude_code_agent/history.py +474 -0
  25. agentpool/agents/context.py +40 -0
  26. agentpool/agents/events/__init__.py +2 -0
  27. agentpool/agents/events/builtin_handlers.py +2 -1
  28. agentpool/agents/events/event_emitter.py +29 -2
  29. agentpool/agents/events/events.py +20 -0
  30. agentpool/agents/modes.py +54 -0
  31. agentpool/agents/tool_call_accumulator.py +213 -0
  32. agentpool/common_types.py +21 -0
  33. agentpool/config_resources/__init__.py +38 -1
  34. agentpool/config_resources/claude_code_agent.yml +3 -0
  35. agentpool/delegation/pool.py +37 -29
  36. agentpool/delegation/team.py +1 -0
  37. agentpool/delegation/teamrun.py +1 -0
  38. agentpool/diagnostics/__init__.py +53 -0
  39. agentpool/diagnostics/lsp_manager.py +1593 -0
  40. agentpool/diagnostics/lsp_proxy.py +41 -0
  41. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  42. agentpool/diagnostics/models.py +398 -0
  43. agentpool/mcp_server/__init__.py +0 -2
  44. agentpool/mcp_server/client.py +12 -3
  45. agentpool/mcp_server/manager.py +25 -31
  46. agentpool/mcp_server/registries/official_registry_client.py +25 -0
  47. agentpool/mcp_server/tool_bridge.py +78 -66
  48. agentpool/messaging/__init__.py +0 -2
  49. agentpool/messaging/compaction.py +72 -197
  50. agentpool/messaging/message_history.py +12 -0
  51. agentpool/messaging/messages.py +52 -9
  52. agentpool/messaging/processing.py +3 -1
  53. agentpool/models/acp_agents/base.py +0 -22
  54. agentpool/models/acp_agents/mcp_capable.py +8 -148
  55. agentpool/models/acp_agents/non_mcp.py +129 -72
  56. agentpool/models/agents.py +35 -13
  57. agentpool/models/claude_code_agents.py +33 -2
  58. agentpool/models/manifest.py +43 -0
  59. agentpool/repomap.py +1 -1
  60. agentpool/resource_providers/__init__.py +9 -1
  61. agentpool/resource_providers/aggregating.py +52 -3
  62. agentpool/resource_providers/base.py +57 -1
  63. agentpool/resource_providers/mcp_provider.py +23 -0
  64. agentpool/resource_providers/plan_provider.py +130 -41
  65. agentpool/resource_providers/pool.py +2 -0
  66. agentpool/resource_providers/static.py +2 -0
  67. agentpool/sessions/__init__.py +2 -1
  68. agentpool/sessions/manager.py +31 -2
  69. agentpool/sessions/models.py +50 -0
  70. agentpool/skills/registry.py +13 -8
  71. agentpool/storage/manager.py +217 -1
  72. agentpool/testing.py +537 -19
  73. agentpool/utils/file_watcher.py +269 -0
  74. agentpool/utils/identifiers.py +121 -0
  75. agentpool/utils/pydantic_ai_helpers.py +46 -0
  76. agentpool/utils/streams.py +690 -1
  77. agentpool/utils/subprocess_utils.py +155 -0
  78. agentpool/utils/token_breakdown.py +461 -0
  79. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
  80. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
  81. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
  82. agentpool_cli/__main__.py +4 -0
  83. agentpool_cli/serve_acp.py +41 -20
  84. agentpool_cli/serve_agui.py +87 -0
  85. agentpool_cli/serve_opencode.py +119 -0
  86. agentpool_commands/__init__.py +30 -0
  87. agentpool_commands/agents.py +74 -1
  88. agentpool_commands/history.py +62 -0
  89. agentpool_commands/mcp.py +176 -0
  90. agentpool_commands/models.py +56 -3
  91. agentpool_commands/tools.py +57 -0
  92. agentpool_commands/utils.py +51 -0
  93. agentpool_config/builtin_tools.py +77 -22
  94. agentpool_config/commands.py +24 -1
  95. agentpool_config/compaction.py +258 -0
  96. agentpool_config/mcp_server.py +131 -1
  97. agentpool_config/storage.py +46 -1
  98. agentpool_config/tools.py +7 -1
  99. agentpool_config/toolsets.py +92 -148
  100. agentpool_server/acp_server/acp_agent.py +134 -150
  101. agentpool_server/acp_server/commands/acp_commands.py +216 -51
  102. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
  103. agentpool_server/acp_server/server.py +23 -79
  104. agentpool_server/acp_server/session.py +181 -19
  105. agentpool_server/opencode_server/.rules +95 -0
  106. agentpool_server/opencode_server/ENDPOINTS.md +362 -0
  107. agentpool_server/opencode_server/__init__.py +27 -0
  108. agentpool_server/opencode_server/command_validation.py +172 -0
  109. agentpool_server/opencode_server/converters.py +869 -0
  110. agentpool_server/opencode_server/dependencies.py +24 -0
  111. agentpool_server/opencode_server/input_provider.py +269 -0
  112. agentpool_server/opencode_server/models/__init__.py +228 -0
  113. agentpool_server/opencode_server/models/agent.py +53 -0
  114. agentpool_server/opencode_server/models/app.py +60 -0
  115. agentpool_server/opencode_server/models/base.py +26 -0
  116. agentpool_server/opencode_server/models/common.py +23 -0
  117. agentpool_server/opencode_server/models/config.py +37 -0
  118. agentpool_server/opencode_server/models/events.py +647 -0
  119. agentpool_server/opencode_server/models/file.py +88 -0
  120. agentpool_server/opencode_server/models/mcp.py +25 -0
  121. agentpool_server/opencode_server/models/message.py +162 -0
  122. agentpool_server/opencode_server/models/parts.py +190 -0
  123. agentpool_server/opencode_server/models/provider.py +81 -0
  124. agentpool_server/opencode_server/models/pty.py +43 -0
  125. agentpool_server/opencode_server/models/session.py +99 -0
  126. agentpool_server/opencode_server/routes/__init__.py +25 -0
  127. agentpool_server/opencode_server/routes/agent_routes.py +442 -0
  128. agentpool_server/opencode_server/routes/app_routes.py +139 -0
  129. agentpool_server/opencode_server/routes/config_routes.py +241 -0
  130. agentpool_server/opencode_server/routes/file_routes.py +392 -0
  131. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  132. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  133. agentpool_server/opencode_server/routes/message_routes.py +705 -0
  134. agentpool_server/opencode_server/routes/pty_routes.py +299 -0
  135. agentpool_server/opencode_server/routes/session_routes.py +1205 -0
  136. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  137. agentpool_server/opencode_server/server.py +430 -0
  138. agentpool_server/opencode_server/state.py +121 -0
  139. agentpool_server/opencode_server/time_utils.py +8 -0
  140. agentpool_storage/__init__.py +16 -0
  141. agentpool_storage/base.py +103 -0
  142. agentpool_storage/claude_provider.py +907 -0
  143. agentpool_storage/file_provider.py +129 -0
  144. agentpool_storage/memory_provider.py +61 -0
  145. agentpool_storage/models.py +3 -0
  146. agentpool_storage/opencode_provider.py +730 -0
  147. agentpool_storage/project_store.py +325 -0
  148. agentpool_storage/session_store.py +6 -0
  149. agentpool_storage/sql_provider/__init__.py +4 -2
  150. agentpool_storage/sql_provider/models.py +48 -0
  151. agentpool_storage/sql_provider/sql_provider.py +134 -1
  152. agentpool_storage/sql_provider/utils.py +10 -1
  153. agentpool_storage/text_log_provider.py +1 -0
  154. agentpool_toolsets/builtin/__init__.py +0 -8
  155. agentpool_toolsets/builtin/code.py +95 -56
  156. agentpool_toolsets/builtin/debug.py +16 -21
  157. agentpool_toolsets/builtin/execution_environment.py +99 -103
  158. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  159. agentpool_toolsets/builtin/skills.py +86 -4
  160. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  161. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  162. agentpool_toolsets/fsspec_toolset/grep.py +74 -2
  163. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  164. agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
  165. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  166. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  167. agentpool_toolsets/mcp_discovery/toolset.py +454 -0
  168. agentpool_toolsets/mcp_run_toolset.py +84 -6
  169. agentpool_toolsets/builtin/agent_management.py +0 -239
  170. agentpool_toolsets/builtin/history.py +0 -36
  171. agentpool_toolsets/builtin/integration.py +0 -85
  172. agentpool_toolsets/builtin/tool_management.py +0 -90
  173. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
  174. {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",
@@ -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",