agentpool 2.1.9__py3-none-any.whl → 2.5.0__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 -4
- acp/acp_requests.py +20 -77
- acp/agent/connection.py +8 -0
- acp/agent/implementations/debug_server/debug_server.py +6 -2
- acp/agent/protocol.py +6 -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/client/connection.py +38 -29
- acp/client/implementations/default_client.py +3 -2
- acp/client/implementations/headless_client.py +2 -2
- acp/connection.py +2 -2
- acp/notifications.py +20 -50
- acp/schema/__init__.py +2 -0
- acp/schema/agent_responses.py +21 -0
- acp/schema/client_requests.py +3 -3
- acp/schema/session_state.py +63 -29
- acp/stdio.py +39 -9
- acp/task/supervisor.py +2 -2
- acp/transports.py +362 -2
- acp/utils.py +17 -4
- agentpool/__init__.py +6 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +407 -277
- agentpool/agents/acp_agent/acp_converters.py +196 -38
- agentpool/agents/acp_agent/client_handler.py +191 -26
- agentpool/agents/acp_agent/session_state.py +17 -6
- agentpool/agents/agent.py +607 -572
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +176 -110
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +632 -17
- agentpool/agents/claude_code_agent/FORKING.md +191 -0
- agentpool/agents/claude_code_agent/__init__.py +13 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
- agentpool/agents/claude_code_agent/converters.py +74 -143
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/claude_code_agent/models.py +77 -0
- agentpool/agents/claude_code_agent/static_info.py +100 -0
- agentpool/agents/claude_code_agent/usage.py +242 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +24 -0
- agentpool/agents/events/builtin_handlers.py +67 -1
- agentpool/agents/events/event_emitter.py +32 -2
- agentpool/agents/events/events.py +104 -3
- agentpool/agents/events/infer_info.py +145 -0
- agentpool/agents/events/processors.py +254 -0
- agentpool/agents/interactions.py +41 -6
- agentpool/agents/modes.py +67 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +56 -21
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/acp_assistant.yml +2 -2
- agentpool/config_resources/agents.yml +3 -0
- agentpool/config_resources/agents_template.yml +1 -0
- agentpool/config_resources/claude_code_agent.yml +10 -6
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +136 -289
- agentpool/delegation/team.py +58 -57
- agentpool/delegation/teamrun.py +51 -55
- 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/functional/run.py +10 -4
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +76 -32
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +34 -54
- agentpool/mcp_server/registries/official_registry_client.py +35 -1
- agentpool/mcp_server/tool_bridge.py +186 -139
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/connection_manager.py +11 -10
- agentpool/messaging/event_manager.py +5 -5
- agentpool/messaging/message_container.py +6 -30
- agentpool/messaging/message_history.py +99 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +54 -35
- agentpool/messaging/processing.py +12 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -24
- agentpool/models/acp_agents/mcp_capable.py +126 -157
- agentpool/models/acp_agents/non_mcp.py +129 -95
- agentpool/models/agents.py +98 -76
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +144 -19
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +113 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +11 -1
- agentpool/resource_providers/aggregating.py +56 -5
- agentpool/resource_providers/base.py +70 -4
- agentpool/resource_providers/codemode/code_executor.py +72 -5
- agentpool/resource_providers/codemode/helpers.py +2 -2
- agentpool/resource_providers/codemode/provider.py +64 -12
- agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
- agentpool/resource_providers/codemode/remote_provider.py +9 -12
- agentpool/resource_providers/filtering.py +3 -1
- agentpool/resource_providers/mcp_provider.py +89 -12
- agentpool/resource_providers/plan_provider.py +228 -46
- agentpool/resource_providers/pool.py +7 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +4 -2
- agentpool/sessions/__init__.py +4 -1
- agentpool/sessions/manager.py +33 -5
- agentpool/sessions/models.py +59 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +572 -49
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +538 -20
- agentpool/tool_impls/__init__.py +6 -0
- agentpool/tool_impls/agent_cli/__init__.py +42 -0
- agentpool/tool_impls/agent_cli/tool.py +95 -0
- agentpool/tool_impls/bash/__init__.py +64 -0
- agentpool/tool_impls/bash/helpers.py +35 -0
- agentpool/tool_impls/bash/tool.py +171 -0
- agentpool/tool_impls/delete_path/__init__.py +70 -0
- agentpool/tool_impls/delete_path/tool.py +142 -0
- agentpool/tool_impls/download_file/__init__.py +80 -0
- agentpool/tool_impls/download_file/tool.py +183 -0
- agentpool/tool_impls/execute_code/__init__.py +55 -0
- agentpool/tool_impls/execute_code/tool.py +163 -0
- agentpool/tool_impls/grep/__init__.py +80 -0
- agentpool/tool_impls/grep/tool.py +200 -0
- agentpool/tool_impls/list_directory/__init__.py +73 -0
- agentpool/tool_impls/list_directory/tool.py +197 -0
- agentpool/tool_impls/question/__init__.py +42 -0
- agentpool/tool_impls/question/tool.py +127 -0
- agentpool/tool_impls/read/__init__.py +104 -0
- agentpool/tool_impls/read/tool.py +305 -0
- agentpool/tools/__init__.py +2 -1
- agentpool/tools/base.py +114 -34
- agentpool/tools/manager.py +57 -1
- agentpool/ui/base.py +2 -2
- agentpool/ui/mock_provider.py +2 -2
- agentpool/ui/stdlib_provider.py +2 -2
- 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 +616 -2
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
- agentpool-2.5.0.dist-info/RECORD +579 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +24 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +100 -21
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +42 -5
- agentpool_commands/agents.py +75 -2
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/pool.py +260 -0
- agentpool_commands/session.py +1 -1
- agentpool_commands/text_sharing/__init__.py +119 -0
- agentpool_commands/text_sharing/base.py +123 -0
- agentpool_commands/text_sharing/github_gist.py +80 -0
- agentpool_commands/text_sharing/opencode.py +462 -0
- agentpool_commands/text_sharing/paste_rs.py +59 -0
- agentpool_commands/text_sharing/pastebin.py +116 -0
- agentpool_commands/text_sharing/shittycodingagent.py +112 -0
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +80 -30
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/converters.py +1 -1
- agentpool_config/event_handlers.py +42 -0
- agentpool_config/events.py +1 -1
- agentpool_config/forward_targets.py +1 -4
- agentpool_config/jinja.py +3 -3
- agentpool_config/mcp_server.py +132 -6
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +82 -38
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -22
- agentpool_config/toolsets.py +109 -233
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +234 -181
- agentpool_server/acp_server/commands/acp_commands.py +151 -156
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +24 -90
- agentpool_server/acp_server/session.py +173 -331
- agentpool_server/acp_server/session_manager.py +8 -34
- agentpool_server/agui_server/server.py +3 -1
- agentpool_server/mcp_server/server.py +5 -2
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +401 -0
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +19 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +975 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +421 -0
- agentpool_server/opencode_server/models/__init__.py +250 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +72 -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 +821 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +44 -0
- agentpool_server/opencode_server/models/message.py +179 -0
- agentpool_server/opencode_server/models/parts.py +323 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +111 -0
- agentpool_server/opencode_server/routes/__init__.py +29 -0
- agentpool_server/opencode_server/routes/agent_routes.py +473 -0
- agentpool_server/opencode_server/routes/app_routes.py +202 -0
- agentpool_server/opencode_server/routes/config_routes.py +302 -0
- agentpool_server/opencode_server/routes/file_routes.py +571 -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 +761 -0
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +300 -0
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +1276 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +475 -0
- agentpool_server/opencode_server/state.py +151 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +12 -0
- agentpool_storage/base.py +184 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/claude_provider/provider.py +1089 -0
- agentpool_storage/file_provider.py +278 -15
- agentpool_storage/memory_provider.py +193 -12
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
- agentpool_storage/opencode_provider/__init__.py +16 -0
- agentpool_storage/opencode_provider/helpers.py +414 -0
- agentpool_storage/opencode_provider/provider.py +895 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +26 -6
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +269 -3
- agentpool_storage/sql_provider/utils.py +12 -13
- agentpool_storage/zed_provider/__init__.py +16 -0
- agentpool_storage/zed_provider/helpers.py +281 -0
- agentpool_storage/zed_provider/models.py +130 -0
- agentpool_storage/zed_provider/provider.py +442 -0
- agentpool_storage/zed_provider.py +803 -0
- agentpool_toolsets/__init__.py +0 -2
- agentpool_toolsets/builtin/__init__.py +2 -12
- agentpool_toolsets/builtin/code.py +96 -57
- agentpool_toolsets/builtin/debug.py +118 -48
- agentpool_toolsets/builtin/execution_environment.py +115 -230
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +9 -4
- agentpool_toolsets/builtin/subagent_tools.py +64 -51
- agentpool_toolsets/builtin/workers.py +4 -2
- agentpool_toolsets/composio_toolset.py +2 -2
- agentpool_toolsets/entry_points.py +3 -1
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +99 -7
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +511 -0
- agentpool_toolsets/mcp_run_toolset.py +87 -12
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool-2.1.9.dist-info/RECORD +0 -474
- agentpool_config/resources.py +0 -33
- agentpool_server/acp_server/acp_tools.py +0 -43
- agentpool_server/acp_server/commands/spawn.py +0 -210
- agentpool_storage/text_log_provider.py +0 -275
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
acp/__init__.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from acp.client import DefaultACPClient, HeadlessACPClient, NoOpClient, ClientSideConnection
|
|
4
4
|
from acp.agent import AgentSideConnection
|
|
5
5
|
from acp.bridge import ACPBridge, BridgeSettings
|
|
6
|
-
from acp.filesystem import ACPFileSystem, ACPPath
|
|
7
6
|
from acp.agent.protocol import Agent
|
|
8
7
|
from acp.client.protocol import Client
|
|
9
8
|
from acp.terminal_handle import TerminalHandle
|
|
@@ -73,6 +72,13 @@ from acp.schema import (
|
|
|
73
72
|
ToolCall,
|
|
74
73
|
)
|
|
75
74
|
from acp.stdio import stdio_streams, run_agent, connect_to_agent
|
|
75
|
+
from acp.transports import (
|
|
76
|
+
serve,
|
|
77
|
+
StdioTransport,
|
|
78
|
+
WebSocketTransport,
|
|
79
|
+
StreamTransport,
|
|
80
|
+
Transport,
|
|
81
|
+
)
|
|
76
82
|
from acp.exceptions import RequestError
|
|
77
83
|
|
|
78
84
|
__version__ = "0.0.1"
|
|
@@ -166,7 +172,10 @@ __all__ = [ # noqa: RUF022
|
|
|
166
172
|
"FileSystemCapability",
|
|
167
173
|
# stdio helper
|
|
168
174
|
"stdio_streams",
|
|
169
|
-
#
|
|
170
|
-
"
|
|
171
|
-
"
|
|
175
|
+
# transport
|
|
176
|
+
"serve",
|
|
177
|
+
"StdioTransport",
|
|
178
|
+
"WebSocketTransport",
|
|
179
|
+
"StreamTransport",
|
|
180
|
+
"Transport",
|
|
172
181
|
]
|
acp/acp_requests.py
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
7
9
|
|
|
8
10
|
from acp.schema import (
|
|
9
11
|
CreateTerminalRequest,
|
|
@@ -19,7 +21,6 @@ from acp.schema import (
|
|
|
19
21
|
WriteTextFileRequest,
|
|
20
22
|
)
|
|
21
23
|
from acp.terminal_handle import TerminalHandle
|
|
22
|
-
from agentpool.log import get_logger
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
@@ -30,20 +31,10 @@ if TYPE_CHECKING:
|
|
|
30
31
|
WaitForTerminalExitResponse,
|
|
31
32
|
)
|
|
32
33
|
|
|
33
|
-
logger = get_logger(__name__)
|
|
34
|
+
logger = structlog.get_logger(__name__)
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
RULES_FILE_NAMES = [
|
|
37
|
-
".rules",
|
|
38
|
-
"CLAUDE.md",
|
|
39
|
-
"AGENT.md",
|
|
40
|
-
"AGENTS.md",
|
|
41
|
-
"GEMINI.md",
|
|
42
|
-
".cursorrules",
|
|
43
|
-
".windsurfrules",
|
|
44
|
-
".clinerules",
|
|
45
|
-
".github/copilot-instructions.md",
|
|
46
|
-
]
|
|
37
|
+
RULES_FILE_NAMES = ["CLAUDE.md", "AGENTS.md"]
|
|
47
38
|
|
|
48
39
|
|
|
49
40
|
class ACPRequests:
|
|
@@ -54,12 +45,7 @@ class ACPRequests:
|
|
|
54
45
|
"""
|
|
55
46
|
|
|
56
47
|
def __init__(self, client: Client, session_id: str) -> None:
|
|
57
|
-
"""Initialize requests helper.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
client: ACP client
|
|
61
|
-
session_id: Session ID
|
|
62
|
-
"""
|
|
48
|
+
"""Initialize requests helper."""
|
|
63
49
|
self.client = client
|
|
64
50
|
self.id = session_id
|
|
65
51
|
|
|
@@ -80,12 +66,7 @@ class ACPRequests:
|
|
|
80
66
|
Returns:
|
|
81
67
|
File content as string
|
|
82
68
|
"""
|
|
83
|
-
request = ReadTextFileRequest(
|
|
84
|
-
session_id=self.id,
|
|
85
|
-
path=path,
|
|
86
|
-
limit=limit,
|
|
87
|
-
line=line,
|
|
88
|
-
)
|
|
69
|
+
request = ReadTextFileRequest(session_id=self.id, path=path, limit=limit, line=line)
|
|
89
70
|
response = await self.client.read_text_file(request)
|
|
90
71
|
return response.content
|
|
91
72
|
|
|
@@ -104,12 +85,7 @@ class ACPRequests:
|
|
|
104
85
|
return None
|
|
105
86
|
|
|
106
87
|
async def write_text_file(self, path: str, content: str) -> None:
|
|
107
|
-
"""Write text content to a file.
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
path: File path to write
|
|
111
|
-
content: Text content to write
|
|
112
|
-
"""
|
|
88
|
+
"""Write text content to a file."""
|
|
113
89
|
request = WriteTextFileRequest(session_id=self.id, path=path, content=content)
|
|
114
90
|
await self.client.write_text_file(request)
|
|
115
91
|
|
|
@@ -146,47 +122,22 @@ class ACPRequests:
|
|
|
146
122
|
return TerminalHandle(terminal_id=response.terminal_id, requests=self)
|
|
147
123
|
|
|
148
124
|
async def terminal_output(self, terminal_id: str) -> TerminalOutputResponse:
|
|
149
|
-
"""Get output from a terminal session.
|
|
150
|
-
|
|
151
|
-
Args:
|
|
152
|
-
terminal_id: Terminal identifier
|
|
153
|
-
|
|
154
|
-
Returns:
|
|
155
|
-
Terminal output response
|
|
156
|
-
"""
|
|
125
|
+
"""Get output from a terminal session."""
|
|
157
126
|
request = TerminalOutputRequest(session_id=self.id, terminal_id=terminal_id)
|
|
158
127
|
return await self.client.terminal_output(request)
|
|
159
128
|
|
|
160
|
-
async def wait_for_terminal_exit(
|
|
161
|
-
|
|
162
|
-
terminal_id: str,
|
|
163
|
-
) -> WaitForTerminalExitResponse:
|
|
164
|
-
"""Wait for a terminal to exit.
|
|
165
|
-
|
|
166
|
-
Args:
|
|
167
|
-
terminal_id: Terminal identifier
|
|
168
|
-
|
|
169
|
-
Returns:
|
|
170
|
-
Terminal exit response with exit_code
|
|
171
|
-
"""
|
|
129
|
+
async def wait_for_terminal_exit(self, terminal_id: str) -> WaitForTerminalExitResponse:
|
|
130
|
+
"""Wait for a terminal to exit."""
|
|
172
131
|
request = WaitForTerminalExitRequest(session_id=self.id, terminal_id=terminal_id)
|
|
173
132
|
return await self.client.wait_for_terminal_exit(request)
|
|
174
133
|
|
|
175
134
|
async def kill_terminal(self, terminal_id: str) -> None:
|
|
176
|
-
"""Kill a terminal session.
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
terminal_id: Terminal identifier to kill
|
|
180
|
-
"""
|
|
135
|
+
"""Kill a terminal session."""
|
|
181
136
|
request = KillTerminalCommandRequest(session_id=self.id, terminal_id=terminal_id)
|
|
182
137
|
await self.client.kill_terminal(request)
|
|
183
138
|
|
|
184
139
|
async def release_terminal(self, terminal_id: str) -> None:
|
|
185
|
-
"""Release a terminal session.
|
|
186
|
-
|
|
187
|
-
Args:
|
|
188
|
-
terminal_id: Terminal identifier to release
|
|
189
|
-
"""
|
|
140
|
+
"""Release a terminal session."""
|
|
190
141
|
request = ReleaseTerminalRequest(session_id=self.id, terminal_id=terminal_id)
|
|
191
142
|
await self.client.release_terminal(request)
|
|
192
143
|
|
|
@@ -228,10 +179,8 @@ class ACPRequests:
|
|
|
228
179
|
try:
|
|
229
180
|
if timeout_seconds: # Wait for completion (with optional timeout)
|
|
230
181
|
try:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
timeout=timeout_seconds,
|
|
234
|
-
)
|
|
182
|
+
coro = self.wait_for_terminal_exit(terminal_id)
|
|
183
|
+
exit_result = await asyncio.wait_for(coro, timeout=timeout_seconds)
|
|
235
184
|
except TimeoutError: # Kill on timeout and get partial output
|
|
236
185
|
await self.kill_terminal(terminal_id)
|
|
237
186
|
output_response = await self.terminal_output(terminal_id)
|
|
@@ -250,6 +199,7 @@ class ACPRequests:
|
|
|
250
199
|
tool_call_id: str,
|
|
251
200
|
*,
|
|
252
201
|
title: str | None = None,
|
|
202
|
+
raw_input: Any | None = None,
|
|
253
203
|
options: list[PermissionOption] | None = None,
|
|
254
204
|
) -> RequestPermissionResponse:
|
|
255
205
|
"""Request permission from user before executing a tool call.
|
|
@@ -257,6 +207,7 @@ class ACPRequests:
|
|
|
257
207
|
Args:
|
|
258
208
|
tool_call_id: Unique identifier for the tool call
|
|
259
209
|
title: Human-readable description of the operation
|
|
210
|
+
raw_input: The raw input parameters for the tool call
|
|
260
211
|
options: Available permission options (defaults to allow/reject once)
|
|
261
212
|
|
|
262
213
|
Returns:
|
|
@@ -264,19 +215,11 @@ class ACPRequests:
|
|
|
264
215
|
"""
|
|
265
216
|
if options is None:
|
|
266
217
|
options = [
|
|
267
|
-
PermissionOption(
|
|
268
|
-
|
|
269
|
-
name="Allow once",
|
|
270
|
-
kind="allow_once",
|
|
271
|
-
),
|
|
272
|
-
PermissionOption(
|
|
273
|
-
option_id="reject-once",
|
|
274
|
-
name="Reject",
|
|
275
|
-
kind="reject_once",
|
|
276
|
-
),
|
|
218
|
+
PermissionOption(option_id="allow-once", name="Allow once", kind="allow_once"),
|
|
219
|
+
PermissionOption(option_id="reject-once", name="Reject", kind="reject_once"),
|
|
277
220
|
]
|
|
278
221
|
|
|
279
|
-
tool_call = ToolCall(tool_call_id=tool_call_id, title=title)
|
|
222
|
+
tool_call = ToolCall(tool_call_id=tool_call_id, title=title, raw_input=raw_input)
|
|
280
223
|
request = RequestPermissionRequest(
|
|
281
224
|
session_id=self.id,
|
|
282
225
|
tool_call=tool_call,
|
acp/agent/connection.py
CHANGED
|
@@ -30,6 +30,7 @@ from acp.schema import (
|
|
|
30
30
|
RequestPermissionRequest,
|
|
31
31
|
RequestPermissionResponse,
|
|
32
32
|
SessionNotification,
|
|
33
|
+
SetSessionConfigOptionRequest,
|
|
33
34
|
SetSessionModelRequest,
|
|
34
35
|
SetSessionModeRequest,
|
|
35
36
|
TerminalOutputRequest,
|
|
@@ -243,6 +244,13 @@ async def _agent_handler( # noqa: PLR0911
|
|
|
243
244
|
if (model_result := await agent.set_session_model(set_model_request))
|
|
244
245
|
else {}
|
|
245
246
|
)
|
|
247
|
+
case "session/set_config_option":
|
|
248
|
+
set_config_request = SetSessionConfigOptionRequest.model_validate(params)
|
|
249
|
+
return (
|
|
250
|
+
config_result.model_dump(by_alias=True, exclude_none=True)
|
|
251
|
+
if (config_result := await agent.set_session_config_option(set_config_request))
|
|
252
|
+
else {}
|
|
253
|
+
)
|
|
246
254
|
case "authenticate":
|
|
247
255
|
p = AuthenticateRequest.model_validate(params)
|
|
248
256
|
result = await agent.authenticate(p)
|
|
@@ -25,6 +25,7 @@ import anyio
|
|
|
25
25
|
from fastapi import FastAPI, HTTPException
|
|
26
26
|
from fastapi.responses import HTMLResponse
|
|
27
27
|
from pydantic import BaseModel, Field
|
|
28
|
+
import structlog
|
|
28
29
|
import uvicorn
|
|
29
30
|
|
|
30
31
|
from acp import AgentSideConnection
|
|
@@ -57,7 +58,6 @@ from acp.schema import (
|
|
|
57
58
|
WriteTextFileResponse,
|
|
58
59
|
)
|
|
59
60
|
from acp.stdio import stdio_streams
|
|
60
|
-
from agentpool.log import get_logger
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
if TYPE_CHECKING:
|
|
@@ -81,7 +81,7 @@ if TYPE_CHECKING:
|
|
|
81
81
|
)
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
logger = get_logger(__name__)
|
|
84
|
+
logger = structlog.get_logger(__name__)
|
|
85
85
|
MOCK_FILE = """\
|
|
86
86
|
# Mock file: {path}
|
|
87
87
|
# Generated by ACP Debug Server
|
|
@@ -190,6 +190,10 @@ class MockAgent(Agent):
|
|
|
190
190
|
"""Mock session model change."""
|
|
191
191
|
logger.info("Mock session model change")
|
|
192
192
|
|
|
193
|
+
async def set_session_config_option(self, params: Any) -> None:
|
|
194
|
+
"""Mock session config option change."""
|
|
195
|
+
logger.info("Mock session config option change")
|
|
196
|
+
|
|
193
197
|
async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
|
|
194
198
|
"""Mock extensibility notification."""
|
|
195
199
|
logger.info("Mock ext notification", method=method)
|
acp/agent/protocol.py
CHANGED
|
@@ -24,6 +24,8 @@ if TYPE_CHECKING:
|
|
|
24
24
|
PromptResponse,
|
|
25
25
|
ResumeSessionRequest,
|
|
26
26
|
ResumeSessionResponse,
|
|
27
|
+
SetSessionConfigOptionRequest,
|
|
28
|
+
SetSessionConfigOptionResponse,
|
|
27
29
|
SetSessionModelRequest,
|
|
28
30
|
SetSessionModelResponse,
|
|
29
31
|
SetSessionModeRequest,
|
|
@@ -60,6 +62,10 @@ class Agent(Protocol):
|
|
|
60
62
|
self, params: SetSessionModelRequest
|
|
61
63
|
) -> SetSessionModelResponse | None: ...
|
|
62
64
|
|
|
65
|
+
async def set_session_config_option(
|
|
66
|
+
self, params: SetSessionConfigOptionRequest
|
|
67
|
+
) -> SetSessionConfigOptionResponse | None: ...
|
|
68
|
+
|
|
63
69
|
async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]: ...
|
|
64
70
|
|
|
65
71
|
async def ext_notification(self, method: str, params: dict[str, Any]) -> None: ...
|
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
|