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/__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
|
-
|
|
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
|
|
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 -
|
|
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
|
-
|
|
5
|
-
uv run -m
|
|
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
|
|
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
|
-
"
|
|
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=
|
|
38
|
-
help="Port to serve
|
|
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="
|
|
73
|
-
datefmt="%H:%M:%S",
|
|
88
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
74
89
|
)
|
|
75
90
|
|
|
76
|
-
|
|
91
|
+
use_websocket = parsed.transport in ("websocket", "ws")
|
|
77
92
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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__":
|
acp/bridge/ws_server.py
ADDED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
+
log_stderr=log_stderr,
|
|
147
177
|
) as (reader, writer, process):
|
|
148
178
|
conn = AgentSideConnection(to_agent, writer, reader, **connection_kwargs)
|
|
149
179
|
try:
|