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/__init__.py CHANGED
@@ -73,6 +73,13 @@ from acp.schema import (
73
73
  ToolCall,
74
74
  )
75
75
  from acp.stdio import stdio_streams, run_agent, connect_to_agent
76
+ from acp.transports import (
77
+ serve,
78
+ StdioTransport,
79
+ WebSocketTransport,
80
+ StreamTransport,
81
+ Transport,
82
+ )
76
83
  from acp.exceptions import RequestError
77
84
 
78
85
  __version__ = "0.0.1"
@@ -169,4 +176,10 @@ __all__ = [ # noqa: RUF022
169
176
  # filesystem
170
177
  "ACPFileSystem",
171
178
  "ACPPath",
179
+ # transport
180
+ "serve",
181
+ "StdioTransport",
182
+ "WebSocketTransport",
183
+ "StreamTransport",
184
+ "Transport",
172
185
  ]
acp/bridge/README.md CHANGED
@@ -1,10 +1,23 @@
1
1
  # ACP Bridge
2
2
 
3
- A stdio-to-HTTP bridge for ACP (Agent Client Protocol) agents.
3
+ Bridges for exposing **external** stdio-based ACP agents over network transports.
4
+
5
+ ## When to Use
6
+
7
+ **Use the bridge** when you need to expose an **external** stdio-based ACP agent (one you don't control) over HTTP or WebSocket.
8
+
9
+ **Use native transports** when building your own agent with the `acp` library. The `acp.serve()` function supports multiple transports directly:
10
+
11
+ ```python
12
+ from acp import serve, WebSocketTransport
13
+
14
+ # Native WebSocket transport (no bridge needed)
15
+ await serve(my_agent, WebSocketTransport(host="0.0.0.0", port=8765))
16
+ ```
4
17
 
5
18
  ## Overview
6
19
 
7
- The ACP Bridge allows you to expose stdio-based ACP agents via a streamable HTTP endpoint. This is useful when you want to run an agent as a subprocess but communicate with it over HTTP instead of stdio.
20
+ The ACP Bridge allows you to expose stdio-based ACP agents via HTTP or WebSocket endpoints. This is useful when you want to run an external agent as a subprocess but communicate with it over the network instead of stdio.
8
21
 
9
22
  ## Features
10
23
 
acp/bridge/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
- """ACP Bridge - stdio to streamable-http transport bridge for ACP agents."""
1
+ """ACP Bridge - transport bridges for ACP agents."""
2
2
 
3
3
  from acp.bridge.bridge import ACPBridge
4
4
  from acp.bridge.settings import BridgeSettings
5
+ from acp.bridge.ws_server import ACPWebSocketServer
5
6
 
6
- __all__ = ["ACPBridge", "BridgeSettings"]
7
+ __all__ = ["ACPBridge", "ACPWebSocketServer", "BridgeSettings"]
acp/bridge/__main__.py CHANGED
@@ -1,8 +1,13 @@
1
1
  """CLI entry point for running the ACP bridge.
2
2
 
3
3
  Usage:
4
- uv run -m acp_bridge <command> [args...]
5
- uv run -m acp_bridge --port 8080 -- your-agent-command --arg1 value1
4
+ # HTTP bridge (default)
5
+ uv run -m acp.bridge <command> [args...]
6
+ uv run -m acp.bridge --port 8080 -- your-agent-command --arg1 value1
7
+
8
+ # WebSocket bridge
9
+ uv run -m acp.bridge --transport websocket <command> [args...]
10
+ uv run -m acp.bridge -t ws --port 8765 -- uv run agentpool serve-acp
6
11
  """
7
12
 
8
13
  from __future__ import annotations
@@ -18,24 +23,35 @@ import anyio
18
23
  def main() -> None:
19
24
  """Run the ACP bridge from command line."""
20
25
  parser = argparse.ArgumentParser(
21
- description="Bridge a stdio ACP agent to streamable HTTP transport.",
26
+ description="Bridge a stdio ACP agent to HTTP or WebSocket transport.",
22
27
  epilog=(
23
28
  "Examples:\n"
29
+ " # HTTP bridge\n"
24
30
  " acp-bridge your-agent-command\n"
25
31
  " acp-bridge --port 8080 -- your-agent --config config.yml\n"
26
- " acp-bridge -H 0.0.0.0 -p 9000 -- uv run my-agent\n"
32
+ "\n"
33
+ " # WebSocket bridge\n"
34
+ " acp-bridge -t ws -- uv run agentpool serve-acp\n"
35
+ " acp-bridge --transport websocket --port 8765 -- uv run my-agent\n"
27
36
  ),
28
37
  formatter_class=argparse.RawTextHelpFormatter,
29
38
  )
30
39
 
31
40
  parser.add_argument("command", help="Command to spawn the ACP agent.")
32
41
  parser.add_argument("args", nargs="*", help="Arguments for the agent command.")
42
+ parser.add_argument(
43
+ "-t",
44
+ "--transport",
45
+ choices=["http", "websocket", "ws"],
46
+ default="http",
47
+ help="Transport type: http (default) or websocket/ws",
48
+ )
33
49
  parser.add_argument(
34
50
  "-p",
35
51
  "--port",
36
52
  type=int,
37
- default=8080,
38
- help="Port to serve the HTTP endpoint on. Default: 8080",
53
+ default=None,
54
+ help="Port to serve on. Default: 8080 (HTTP) or 8765 (WebSocket)",
39
55
  )
40
56
  parser.add_argument(
41
57
  "-H",
@@ -53,7 +69,7 @@ def main() -> None:
53
69
  "--allow-origin",
54
70
  action="append",
55
71
  default=[],
56
- help="Allowed CORS origins. Can be specified multiple times.",
72
+ help="Allowed CORS origins (HTTP only). Can be specified multiple times.",
57
73
  )
58
74
  parser.add_argument(
59
75
  "--cwd",
@@ -69,22 +85,47 @@ def main() -> None:
69
85
 
70
86
  logging.basicConfig(
71
87
  level=getattr(logging, parsed.log_level),
72
- format="[%(levelname)1.1s %(asctime)s.%(msecs)03d %(name)s] %(message)s",
73
- datefmt="%H:%M:%S",
88
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
74
89
  )
75
90
 
76
- from acp.bridge import ACPBridge, BridgeSettings
91
+ use_websocket = parsed.transport in ("websocket", "ws")
77
92
 
78
- settings = BridgeSettings(
79
- host=parsed.host,
80
- port=parsed.port,
81
- log_level=parsed.log_level,
82
- allow_origins=parsed.allow_origin if parsed.allow_origin else None,
83
- )
93
+ if use_websocket:
94
+ # Reduce websockets noise
95
+ logging.getLogger("websockets").setLevel(logging.WARNING)
96
+
97
+ from acp.bridge.ws_server import ACPWebSocketServer
98
+
99
+ port = parsed.port or 8765
100
+ server = ACPWebSocketServer(
101
+ command=parsed.command,
102
+ args=parsed.args,
103
+ host=parsed.host,
104
+ port=port,
105
+ cwd=parsed.cwd,
106
+ )
107
+
108
+ print(f"🔗 ACP WebSocket bridge starting on ws://{parsed.host}:{port}")
109
+ print(f" Agent command: {parsed.command} {' '.join(parsed.args)}")
110
+
111
+ with contextlib.suppress(KeyboardInterrupt):
112
+ anyio.run(server.serve)
113
+ else:
114
+ from acp.bridge import ACPBridge, BridgeSettings
115
+
116
+ port = parsed.port or 8080
117
+ settings = BridgeSettings(
118
+ host=parsed.host,
119
+ port=port,
120
+ log_level=parsed.log_level,
121
+ allow_origins=parsed.allow_origin if parsed.allow_origin else None,
122
+ )
84
123
 
85
- bridge = ACPBridge(command=parsed.command, args=parsed.args, cwd=parsed.cwd, settings=settings)
86
- with contextlib.suppress(KeyboardInterrupt):
87
- anyio.run(bridge.run)
124
+ bridge = ACPBridge(
125
+ command=parsed.command, args=parsed.args, cwd=parsed.cwd, settings=settings
126
+ )
127
+ with contextlib.suppress(KeyboardInterrupt):
128
+ anyio.run(bridge.run)
88
129
 
89
130
 
90
131
  if __name__ == "__main__":
@@ -0,0 +1,173 @@
1
+ """WebSocket server that bridges to a stdio ACP agent.
2
+
3
+ Spawns a stdio-based ACP agent subprocess and exposes it via WebSocket,
4
+ allowing clients like mcp-ws to connect.
5
+
6
+ Usage:
7
+ python -m acp.bridge.ws_server "uv run agentpool serve-acp" --port 8765
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import contextlib
14
+ import json
15
+ import logging
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ import anyenv
19
+ import websockets
20
+
21
+ from acp.transports import spawn_stdio_transport
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Mapping
26
+ from pathlib import Path
27
+
28
+ from anyio.abc import ByteReceiveStream, ByteSendStream, Process
29
+ from websockets.asyncio.server import ServerConnection
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class ACPWebSocketServer:
35
+ """WebSocket server that bridges to a stdio ACP agent."""
36
+
37
+ def __init__(
38
+ self,
39
+ command: str,
40
+ args: list[str] | None = None,
41
+ *,
42
+ host: str = "localhost",
43
+ port: int = 8765,
44
+ env: Mapping[str, str] | None = None,
45
+ cwd: str | Path | None = None,
46
+ ) -> None:
47
+ """Initialize the WebSocket server.
48
+
49
+ Args:
50
+ command: Command to spawn the ACP agent.
51
+ args: Arguments for the command.
52
+ host: Host to bind the WebSocket server to.
53
+ port: Port for the WebSocket server.
54
+ env: Environment variables for the subprocess.
55
+ cwd: Working directory for the subprocess.
56
+ """
57
+ self.command = command
58
+ self.args = args or []
59
+ self.host = host
60
+ self.port = port
61
+ self.env = env
62
+ self.cwd = cwd
63
+
64
+ self._process: Process | None = None
65
+ self._reader: ByteReceiveStream | None = None
66
+ self._writer: ByteSendStream | None = None
67
+ self._websocket: ServerConnection | None = None
68
+ self._shutdown = asyncio.Event()
69
+ self._read_buffer = b""
70
+
71
+ async def _read_jsonrpc_message(self) -> dict[str, Any] | None:
72
+ """Read a JSON-RPC message from the agent's stdout."""
73
+ if self._reader is None:
74
+ return None
75
+
76
+ while True:
77
+ # Check if we have a complete line in buffer
78
+ if b"\n" in self._read_buffer:
79
+ line, self._read_buffer = self._read_buffer.split(b"\n", 1)
80
+ if line.strip():
81
+ try:
82
+ return anyenv.load_json(line.decode(), return_type=dict)
83
+ except anyenv.JsonLoadError:
84
+ logger.exception("Failed to parse JSON from agent")
85
+ continue
86
+
87
+ # Read more data
88
+ try:
89
+ chunk = await self._reader.receive(65536)
90
+ if not chunk:
91
+ return None
92
+ self._read_buffer += chunk
93
+ except Exception: # noqa: BLE001
94
+ return None
95
+
96
+ async def _send_to_agent(self, message: dict[str, Any]) -> None:
97
+ """Send a JSON-RPC message to the agent's stdin."""
98
+ if self._writer is None:
99
+ return
100
+
101
+ data = json.dumps(message) + "\n"
102
+ await self._writer.send(data.encode())
103
+
104
+ async def _agent_to_websocket(self) -> None:
105
+ """Forward messages from agent stdout to WebSocket."""
106
+ while not self._shutdown.is_set():
107
+ try:
108
+ message = await self._read_jsonrpc_message()
109
+ if message is None:
110
+ logger.info("Agent stdout closed")
111
+ break
112
+
113
+ if self._websocket is not None:
114
+ await self._websocket.send(json.dumps(message))
115
+ logger.debug("Agent → WebSocket: %s", message.get("method", message.get("id")))
116
+ except Exception:
117
+ logger.exception("Error forwarding agent → WebSocket")
118
+ break
119
+
120
+ async def _handle_client(self, websocket: ServerConnection) -> None:
121
+ """Handle a WebSocket client connection."""
122
+ logger.info("WebSocket client connected")
123
+ self._websocket = websocket
124
+
125
+ try:
126
+ async for raw_message in websocket:
127
+ try:
128
+ message = json.loads(raw_message)
129
+ logger.debug("WebSocket → Agent: %s", message.get("method", message.get("id")))
130
+ await self._send_to_agent(message)
131
+ except json.JSONDecodeError:
132
+ logger.exception("Invalid JSON from WebSocket")
133
+ error_response = {
134
+ "jsonrpc": "2.0",
135
+ "id": None,
136
+ "error": {"code": -32700, "message": "Parse error"},
137
+ }
138
+ await websocket.send(json.dumps(error_response))
139
+ except websockets.exceptions.ConnectionClosed:
140
+ logger.info("WebSocket client disconnected")
141
+ finally:
142
+ self._websocket = None
143
+
144
+ async def serve(self) -> None:
145
+ """Start the WebSocket server and agent process."""
146
+ cmd_str = f"{self.command} {' '.join(self.args)}".strip()
147
+ logger.info("Starting ACP agent: %s", cmd_str)
148
+
149
+ async with spawn_stdio_transport(self.command, *self.args, env=self.env, cwd=self.cwd) as (
150
+ reader,
151
+ writer,
152
+ process,
153
+ ):
154
+ self._reader = reader
155
+ self._writer = writer
156
+ self._process = process
157
+
158
+ # Start forwarding agent output to WebSocket
159
+ forward_task = asyncio.create_task(self._agent_to_websocket())
160
+
161
+ try:
162
+ async with websockets.serve(
163
+ self._handle_client,
164
+ self.host,
165
+ self.port,
166
+ ):
167
+ logger.info("WebSocket server running on ws://%s:%d", self.host, self.port)
168
+ await self._shutdown.wait()
169
+ finally:
170
+ self._shutdown.set()
171
+ forward_task.cancel()
172
+ with contextlib.suppress(asyncio.CancelledError):
173
+ await forward_task
@@ -0,0 +1,89 @@
1
+ """CLI entry point for the ACP WebSocket server bridge.
2
+
3
+ Usage:
4
+ python -m acp.bridge.ws_server_cli "uv run agentpool serve-acp"
5
+ python -m acp.bridge.ws_server_cli --port 8765 -- uv run agentpool serve-acp
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import contextlib
12
+ import logging
13
+ import sys
14
+
15
+ import anyio
16
+
17
+
18
+ def main() -> None:
19
+ """Run the ACP WebSocket server bridge from command line."""
20
+ parser = argparse.ArgumentParser(
21
+ description="Bridge a stdio ACP agent to WebSocket transport.",
22
+ epilog=(
23
+ "Examples:\n"
24
+ ' acp-ws-server "uv run agentpool serve-acp"\n'
25
+ " acp-ws-server --port 8765 -- uv run agentpool serve-acp\n"
26
+ " acp-ws-server -H 0.0.0.0 -p 9000 -- uv run my-agent\n"
27
+ ),
28
+ formatter_class=argparse.RawTextHelpFormatter,
29
+ )
30
+
31
+ parser.add_argument("command", help="Command to spawn the ACP agent.")
32
+ parser.add_argument("args", nargs="*", help="Arguments for the agent command.")
33
+ parser.add_argument(
34
+ "-p",
35
+ "--port",
36
+ type=int,
37
+ default=8765,
38
+ help="Port for the WebSocket server. Default: 8765",
39
+ )
40
+ parser.add_argument(
41
+ "-H",
42
+ "--host",
43
+ default="localhost",
44
+ help="Host to bind the server to. Default: localhost",
45
+ )
46
+ parser.add_argument(
47
+ "--log-level",
48
+ choices=["DEBUG", "INFO", "WARNING", "ERROR"],
49
+ default="INFO",
50
+ help="Log level. Default: INFO",
51
+ )
52
+ parser.add_argument(
53
+ "--cwd",
54
+ default=None,
55
+ help="Working directory for the agent subprocess.",
56
+ )
57
+
58
+ parsed = parser.parse_args()
59
+
60
+ if not parsed.command:
61
+ parser.print_help()
62
+ sys.exit(1)
63
+
64
+ logging.basicConfig(
65
+ level=getattr(logging, parsed.log_level),
66
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
67
+ )
68
+ # Reduce websockets noise
69
+ logging.getLogger("websockets").setLevel(logging.WARNING)
70
+
71
+ from acp.bridge.ws_server import ACPWebSocketServer
72
+
73
+ server = ACPWebSocketServer(
74
+ command=parsed.command,
75
+ args=parsed.args,
76
+ host=parsed.host,
77
+ port=parsed.port,
78
+ cwd=parsed.cwd,
79
+ )
80
+
81
+ print(f"🔗 ACP WebSocket bridge starting on ws://{parsed.host}:{parsed.port}")
82
+ print(f" Agent command: {parsed.command} {' '.join(parsed.args)}")
83
+
84
+ with contextlib.suppress(KeyboardInterrupt):
85
+ anyio.run(server.serve)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
acp/notifications.py CHANGED
@@ -34,6 +34,7 @@ from acp.schema.tool_call import ToolCallLocation
34
34
  from acp.tool_call_reporter import ToolCallReporter
35
35
  from acp.utils import generate_tool_title, infer_tool_kind, to_acp_content_blocks
36
36
  from agentpool.log import get_logger
37
+ from agentpool.utils.pydantic_ai_helpers import safe_args_as_dict
37
38
 
38
39
 
39
40
  if TYPE_CHECKING:
@@ -703,7 +704,7 @@ class ACPNotifications:
703
704
 
704
705
  case ToolCallPart(tool_call_id=tool_call_id):
705
706
  # Store tool call inputs for later use with ToolReturnPart
706
- tool_input = part.args_as_dict()
707
+ tool_input = safe_args_as_dict(part)
707
708
  self._tool_call_inputs[tool_call_id] = tool_input
708
709
  # Skip sending notification - ACP protocol overrides previous
709
710
  # tool call state
acp/stdio.py CHANGED
@@ -87,10 +87,20 @@ async def spawn_stdio_connection(
87
87
  env: Mapping[str, str] | None = None,
88
88
  cwd: str | Path | None = None,
89
89
  observers: list[StreamObserver] | None = None,
90
- **transport_kwargs: Any,
90
+ log_stderr: bool = False,
91
91
  ) -> AsyncIterator[tuple[Connection, Process]]:
92
- """Spawn a subprocess and bind its stdio to a low-level Connection."""
93
- async with spawn_stdio_transport(command, *args, env=env, cwd=cwd, **transport_kwargs) as (
92
+ """Spawn a subprocess and bind its stdio to a low-level Connection.
93
+
94
+ Args:
95
+ handler: Method handler for the connection.
96
+ command: The command to execute.
97
+ *args: Arguments for the command.
98
+ env: Environment variables for the subprocess.
99
+ cwd: Working directory for the subprocess.
100
+ observers: Optional stream observers.
101
+ log_stderr: If True, log stderr output from the subprocess.
102
+ """
103
+ async with spawn_stdio_transport(command, *args, env=env, cwd=cwd, log_stderr=log_stderr) as (
94
104
  reader,
95
105
  writer,
96
106
  process,
@@ -109,16 +119,26 @@ async def spawn_agent_process(
109
119
  *args: str,
110
120
  env: Mapping[str, str] | None = None,
111
121
  cwd: str | Path | None = None,
112
- transport_kwargs: Mapping[str, Any] | None = None,
122
+ log_stderr: bool = False,
113
123
  **connection_kwargs: Any,
114
124
  ) -> AsyncIterator[tuple[ClientSideConnection, Process]]:
115
- """Spawn an ACP agent subprocess and return a ClientSideConnection to it."""
125
+ """Spawn an ACP agent subprocess and return a ClientSideConnection to it.
126
+
127
+ Args:
128
+ to_client: Factory function that creates a Client from an Agent.
129
+ command: The command to execute.
130
+ *args: Arguments for the command.
131
+ env: Environment variables for the subprocess.
132
+ cwd: Working directory for the subprocess.
133
+ log_stderr: If True, log stderr output from the subprocess.
134
+ **connection_kwargs: Additional arguments for ClientSideConnection.
135
+ """
116
136
  async with spawn_stdio_transport(
117
137
  command,
118
138
  *args,
119
139
  env=env,
120
140
  cwd=cwd,
121
- **(dict(transport_kwargs) if transport_kwargs else {}),
141
+ log_stderr=log_stderr,
122
142
  ) as (reader, writer, process):
123
143
  conn = ClientSideConnection(to_client, writer, reader, **connection_kwargs)
124
144
  try:
@@ -134,16 +154,26 @@ async def spawn_client_process(
134
154
  *args: str,
135
155
  env: Mapping[str, str] | None = None,
136
156
  cwd: str | Path | None = None,
137
- transport_kwargs: Mapping[str, Any] | None = None,
157
+ log_stderr: bool = False,
138
158
  **connection_kwargs: Any,
139
159
  ) -> AsyncIterator[tuple[AgentSideConnection, Process]]:
140
- """Spawn an ACP client subprocess and return an AgentSideConnection to it."""
160
+ """Spawn an ACP client subprocess and return an AgentSideConnection to it.
161
+
162
+ Args:
163
+ to_agent: Factory function that creates an Agent from an AgentSideConnection.
164
+ command: The command to execute.
165
+ *args: Arguments for the command.
166
+ env: Environment variables for the subprocess.
167
+ cwd: Working directory for the subprocess.
168
+ log_stderr: If True, log stderr output from the subprocess.
169
+ **connection_kwargs: Additional arguments for AgentSideConnection.
170
+ """
141
171
  async with spawn_stdio_transport(
142
172
  command,
143
173
  *args,
144
174
  env=env,
145
175
  cwd=cwd,
146
- **(dict(transport_kwargs) if transport_kwargs else {}),
176
+ log_stderr=log_stderr,
147
177
  ) as (reader, writer, process):
148
178
  conn = AgentSideConnection(to_agent, writer, reader, **connection_kwargs)
149
179
  try: