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
|
@@ -50,12 +50,17 @@ from agentpool.agents.acp_agent.client_handler import ACPClientHandler
|
|
|
50
50
|
from agentpool.agents.acp_agent.session_state import ACPSessionState
|
|
51
51
|
from agentpool.agents.base_agent import BaseAgent
|
|
52
52
|
from agentpool.agents.events import RunStartedEvent, StreamCompleteEvent, ToolCallStartEvent
|
|
53
|
+
from agentpool.agents.modes import ModeInfo
|
|
53
54
|
from agentpool.log import get_logger
|
|
54
55
|
from agentpool.messaging import ChatMessage
|
|
55
56
|
from agentpool.messaging.processing import prepare_prompts
|
|
56
57
|
from agentpool.models.acp_agents import ACPAgentConfig, MCPCapableACPAgentConfig
|
|
57
|
-
from agentpool.
|
|
58
|
-
|
|
58
|
+
from agentpool.utils.streams import (
|
|
59
|
+
FileTracker,
|
|
60
|
+
merge_queue_into_iterator,
|
|
61
|
+
)
|
|
62
|
+
from agentpool.utils.subprocess_utils import SubprocessError, monitor_process
|
|
63
|
+
from agentpool.utils.token_breakdown import calculate_usage_from_parts
|
|
59
64
|
|
|
60
65
|
|
|
61
66
|
if TYPE_CHECKING:
|
|
@@ -66,7 +71,8 @@ if TYPE_CHECKING:
|
|
|
66
71
|
from evented.configs import EventConfig
|
|
67
72
|
from exxec import ExecutionEnvironment
|
|
68
73
|
from pydantic_ai import FinishReason
|
|
69
|
-
from
|
|
74
|
+
from slashed import BaseCommand
|
|
75
|
+
from tokonomics.model_discovery.model_info import ModelInfo
|
|
70
76
|
|
|
71
77
|
from acp.agent.protocol import Agent as ACPAgentProtocol
|
|
72
78
|
from acp.client.connection import ClientSideConnection
|
|
@@ -80,6 +86,7 @@ if TYPE_CHECKING:
|
|
|
80
86
|
from acp.schema.mcp import McpServer
|
|
81
87
|
from agentpool.agents import AgentContext
|
|
82
88
|
from agentpool.agents.events import RichAgentStreamEvent
|
|
89
|
+
from agentpool.agents.modes import ModeCategory
|
|
83
90
|
from agentpool.common_types import (
|
|
84
91
|
BuiltinEventHandlerType,
|
|
85
92
|
IndividualEventHandler,
|
|
@@ -106,34 +113,6 @@ STOP_REASON_MAP: dict[StopReason, FinishReason] = {
|
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
|
|
109
|
-
def extract_file_path_from_tool_call(tool_name: str, raw_input: dict[str, Any]) -> str | None:
|
|
110
|
-
"""Extract file path from a tool call if it's a file-writing tool.
|
|
111
|
-
|
|
112
|
-
Uses simple heuristics by default:
|
|
113
|
-
- Tool name contains 'write' or 'edit' (case-insensitive)
|
|
114
|
-
- Input contains 'path' or 'file_path' key
|
|
115
|
-
|
|
116
|
-
Override in subclasses for agent-specific tool naming conventions.
|
|
117
|
-
|
|
118
|
-
Args:
|
|
119
|
-
tool_name: Name of the tool being called
|
|
120
|
-
raw_input: Tool call arguments
|
|
121
|
-
|
|
122
|
-
Returns:
|
|
123
|
-
File path if this is a file-writing tool, None otherwise
|
|
124
|
-
"""
|
|
125
|
-
name_lower = tool_name.lower()
|
|
126
|
-
if "write" not in name_lower and "edit" not in name_lower:
|
|
127
|
-
return None
|
|
128
|
-
|
|
129
|
-
# Try common path argument names
|
|
130
|
-
for key in ("file_path", "path", "filepath", "filename", "file"):
|
|
131
|
-
if key in raw_input and isinstance(val := raw_input[key], str):
|
|
132
|
-
return val
|
|
133
|
-
|
|
134
|
-
return None
|
|
135
|
-
|
|
136
|
-
|
|
137
116
|
class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
138
117
|
"""MessageNode that wraps an external ACP agent subprocess.
|
|
139
118
|
|
|
@@ -174,6 +153,7 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
174
153
|
enable_logging: bool = True,
|
|
175
154
|
event_configs: Sequence[EventConfig] | None = None,
|
|
176
155
|
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
156
|
+
commands: Sequence[BaseCommand] | None = None,
|
|
177
157
|
) -> None: ...
|
|
178
158
|
|
|
179
159
|
@overload
|
|
@@ -190,13 +170,13 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
190
170
|
env: ExecutionEnvironment | None = None,
|
|
191
171
|
allow_file_operations: bool = True,
|
|
192
172
|
allow_terminal: bool = True,
|
|
193
|
-
providers: list[ProviderType] | None = None,
|
|
194
173
|
input_provider: InputProvider | None = None,
|
|
195
174
|
agent_pool: AgentPool[Any] | None = None,
|
|
196
175
|
enable_logging: bool = True,
|
|
197
176
|
event_configs: Sequence[EventConfig] | None = None,
|
|
198
177
|
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
199
178
|
tool_confirmation_mode: ToolConfirmationMode = "always",
|
|
179
|
+
commands: Sequence[BaseCommand] | None = None,
|
|
200
180
|
) -> None: ...
|
|
201
181
|
|
|
202
182
|
def __init__(
|
|
@@ -213,13 +193,13 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
213
193
|
env: ExecutionEnvironment | None = None,
|
|
214
194
|
allow_file_operations: bool = True,
|
|
215
195
|
allow_terminal: bool = True,
|
|
216
|
-
providers: list[ProviderType] | None = None,
|
|
217
196
|
input_provider: InputProvider | None = None,
|
|
218
197
|
agent_pool: AgentPool[Any] | None = None,
|
|
219
198
|
enable_logging: bool = True,
|
|
220
199
|
event_configs: Sequence[EventConfig] | None = None,
|
|
221
200
|
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
222
201
|
tool_confirmation_mode: ToolConfirmationMode = "always",
|
|
202
|
+
commands: Sequence[BaseCommand] | None = None,
|
|
223
203
|
) -> None:
|
|
224
204
|
# Build config from kwargs if not provided
|
|
225
205
|
if config is None:
|
|
@@ -237,7 +217,6 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
237
217
|
allow_file_operations=allow_file_operations,
|
|
238
218
|
allow_terminal=allow_terminal,
|
|
239
219
|
requires_tool_confirmation=tool_confirmation_mode,
|
|
240
|
-
providers=list(providers) if providers else [],
|
|
241
220
|
)
|
|
242
221
|
|
|
243
222
|
super().__init__(
|
|
@@ -252,6 +231,7 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
252
231
|
input_provider=input_provider,
|
|
253
232
|
tool_confirmation_mode=tool_confirmation_mode,
|
|
254
233
|
event_handlers=event_handlers,
|
|
234
|
+
commands=commands,
|
|
255
235
|
)
|
|
256
236
|
|
|
257
237
|
# ACP-specific state
|
|
@@ -327,9 +307,13 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
327
307
|
"""Start subprocess and initialize ACP connection."""
|
|
328
308
|
await super().__aenter__()
|
|
329
309
|
await self._setup_toolsets() # Setup toolsets before session creation
|
|
330
|
-
await self._start_process()
|
|
331
|
-
|
|
332
|
-
|
|
310
|
+
process = await self._start_process()
|
|
311
|
+
try:
|
|
312
|
+
async with monitor_process(process, context="ACP initialization"):
|
|
313
|
+
await self._initialize()
|
|
314
|
+
await self._create_session()
|
|
315
|
+
except SubprocessError as e:
|
|
316
|
+
raise RuntimeError(str(e)) from e
|
|
333
317
|
await anyio.sleep(0.3) # Small delay to let subprocess fully initialize
|
|
334
318
|
return self
|
|
335
319
|
|
|
@@ -343,8 +327,12 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
343
327
|
await self._cleanup()
|
|
344
328
|
await super().__aexit__(exc_type, exc_val, exc_tb)
|
|
345
329
|
|
|
346
|
-
async def _start_process(self) ->
|
|
347
|
-
"""Start the ACP server subprocess.
|
|
330
|
+
async def _start_process(self) -> Process:
|
|
331
|
+
"""Start the ACP server subprocess.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
The started Process instance
|
|
335
|
+
"""
|
|
348
336
|
prompt_manager = self.agent_pool.manifest.prompt_manager if self.agent_pool else None
|
|
349
337
|
args = await self.config.get_args(prompt_manager)
|
|
350
338
|
cmd = [self.config.get_command(), *args]
|
|
@@ -361,9 +349,12 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
361
349
|
if not self._process.stdin or not self._process.stdout:
|
|
362
350
|
msg = "Failed to create subprocess pipes"
|
|
363
351
|
raise RuntimeError(msg)
|
|
352
|
+
return self._process
|
|
364
353
|
|
|
365
354
|
async def _initialize(self) -> None:
|
|
366
355
|
"""Initialize the ACP connection."""
|
|
356
|
+
from importlib.metadata import metadata
|
|
357
|
+
|
|
367
358
|
from acp.client.connection import ClientSideConnection
|
|
368
359
|
from acp.schema import InitializeRequest
|
|
369
360
|
|
|
@@ -382,9 +373,10 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
382
373
|
input_stream=self._process.stdin,
|
|
383
374
|
output_stream=self._process.stdout,
|
|
384
375
|
)
|
|
376
|
+
pkg_meta = metadata("agentpool")
|
|
385
377
|
init_request = InitializeRequest.create(
|
|
386
|
-
title="
|
|
387
|
-
version="
|
|
378
|
+
title=pkg_meta["Name"],
|
|
379
|
+
version=pkg_meta["Version"],
|
|
388
380
|
name="agentpool",
|
|
389
381
|
protocol_version=PROTOCOL_VERSION,
|
|
390
382
|
terminal=self.config.allow_terminal,
|
|
@@ -422,10 +414,6 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
422
414
|
model = self._state.current_model_id if self._state else None
|
|
423
415
|
self.log.info("ACP session created", session_id=self._session_id, model=model)
|
|
424
416
|
|
|
425
|
-
def add_mcp_server(self, server: McpServer) -> None:
|
|
426
|
-
"""Add an MCP server to be passed to the next session."""
|
|
427
|
-
self._extra_mcp_servers.append(server)
|
|
428
|
-
|
|
429
417
|
async def add_tool_bridge(self, bridge: ToolManagerBridge) -> None:
|
|
430
418
|
"""Add an external tool bridge to expose its tools via MCP.
|
|
431
419
|
|
|
@@ -517,6 +505,8 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
517
505
|
message_id: str | None = None,
|
|
518
506
|
input_provider: InputProvider | None = None,
|
|
519
507
|
message_history: MessageHistory | None = None,
|
|
508
|
+
deps: TDeps | None = None,
|
|
509
|
+
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
520
510
|
) -> AsyncIterator[RichAgentStreamEvent[str]]:
|
|
521
511
|
"""Stream native events as they arrive from ACP agent.
|
|
522
512
|
|
|
@@ -525,6 +515,8 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
525
515
|
message_id: Optional message id for the final message
|
|
526
516
|
input_provider: Optional input provider for permission requests
|
|
527
517
|
message_history: Optional MessageHistory to use instead of agent's own
|
|
518
|
+
deps: Optional dependencies accessible via ctx.data in tools
|
|
519
|
+
event_handlers: Optional event handlers for this run (overrides agent's handlers)
|
|
528
520
|
|
|
529
521
|
Yields:
|
|
530
522
|
RichAgentStreamEvent instances converted from ACP session updates
|
|
@@ -541,14 +533,26 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
541
533
|
msg = "Agent not initialized - use async context manager"
|
|
542
534
|
raise RuntimeError(msg)
|
|
543
535
|
|
|
544
|
-
# Capture state for use in nested function (avoids type narrowing issues)
|
|
545
|
-
state = self._state
|
|
546
|
-
|
|
547
536
|
conversation = message_history if message_history is not None else self.conversation
|
|
537
|
+
# Use provided event handlers or fall back to agent's handlers
|
|
538
|
+
if event_handlers is not None:
|
|
539
|
+
from anyenv import MultiEventHandler
|
|
540
|
+
|
|
541
|
+
from agentpool.agents.events import resolve_event_handlers
|
|
542
|
+
|
|
543
|
+
handler: MultiEventHandler[IndividualEventHandler] = MultiEventHandler(
|
|
544
|
+
resolve_event_handlers(event_handlers)
|
|
545
|
+
)
|
|
546
|
+
else:
|
|
547
|
+
handler = self.event_handler
|
|
548
548
|
# Prepare user message for history and convert to ACP content blocks
|
|
549
|
-
|
|
549
|
+
# Get parent_id from last message in history for tree structure
|
|
550
|
+
last_msg_id = conversation.get_last_message_id()
|
|
551
|
+
user_msg, processed_prompts, _original_message = await prepare_prompts(
|
|
552
|
+
*prompts, parent_id=last_msg_id
|
|
553
|
+
)
|
|
550
554
|
run_id = str(uuid.uuid4())
|
|
551
|
-
|
|
555
|
+
self._state.clear() # Reset state
|
|
552
556
|
# Track messages in pydantic-ai format: ModelRequest -> ModelResponse -> ...
|
|
553
557
|
# This mirrors pydantic-ai's new_messages() which includes the initial user request.
|
|
554
558
|
model_messages: list[ModelResponse | ModelRequest] = []
|
|
@@ -557,25 +561,22 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
557
561
|
model_messages.append(initial_request)
|
|
558
562
|
current_response_parts: list[TextPart | ThinkingPart | ToolCallPart] = []
|
|
559
563
|
text_chunks: list[str] = [] # For final content string
|
|
560
|
-
|
|
564
|
+
file_tracker = FileTracker() # Track files modified by tool calls
|
|
561
565
|
run_started = RunStartedEvent(
|
|
562
566
|
thread_id=self.conversation_id,
|
|
563
567
|
run_id=run_id,
|
|
564
568
|
agent_name=self.name,
|
|
565
569
|
)
|
|
566
|
-
|
|
567
|
-
await handler(None, run_started)
|
|
570
|
+
await handler(None, run_started)
|
|
568
571
|
yield run_started
|
|
569
572
|
content_blocks = convert_to_acp_content(processed_prompts)
|
|
570
573
|
pending_parts = conversation.get_pending_parts()
|
|
571
574
|
final_blocks = [*to_acp_content_blocks(pending_parts), *content_blocks]
|
|
572
575
|
prompt_request = PromptRequest(session_id=self._session_id, prompt=final_blocks)
|
|
573
576
|
self.log.debug("Starting streaming prompt", num_blocks=len(final_blocks))
|
|
574
|
-
|
|
575
577
|
# Reset cancellation state
|
|
576
578
|
self._cancelled = False
|
|
577
579
|
self._current_stream_task = asyncio.current_task()
|
|
578
|
-
|
|
579
580
|
# Run prompt in background
|
|
580
581
|
prompt_task = asyncio.create_task(self._connection.prompt(prompt_request))
|
|
581
582
|
self._prompt_task = prompt_task
|
|
@@ -584,6 +585,7 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
584
585
|
async def poll_acp_events() -> AsyncIterator[RichAgentStreamEvent[str]]:
|
|
585
586
|
"""Poll events from ACP state until prompt completes."""
|
|
586
587
|
last_idx = 0
|
|
588
|
+
assert self._state
|
|
587
589
|
while not prompt_task.done():
|
|
588
590
|
if self._client_handler:
|
|
589
591
|
try:
|
|
@@ -595,21 +597,26 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
595
597
|
pass
|
|
596
598
|
|
|
597
599
|
# Yield new events from state
|
|
598
|
-
while last_idx < len(
|
|
599
|
-
yield
|
|
600
|
+
while last_idx < len(self._state.events):
|
|
601
|
+
yield self._state.events[last_idx]
|
|
600
602
|
last_idx += 1
|
|
601
603
|
|
|
602
604
|
# Yield remaining events after prompt completes
|
|
603
|
-
while last_idx < len(
|
|
604
|
-
yield
|
|
605
|
+
while last_idx < len(self._state.events):
|
|
606
|
+
yield self._state.events[last_idx]
|
|
605
607
|
last_idx += 1
|
|
606
608
|
|
|
609
|
+
# Set deps on tool bridge for access during tool invocations
|
|
610
|
+
# (ContextVar doesn't work because MCP server runs in a separate task)
|
|
611
|
+
if self._tool_bridge:
|
|
612
|
+
self._tool_bridge.current_deps = deps
|
|
613
|
+
|
|
607
614
|
# Merge ACP events with custom events from queue
|
|
608
615
|
try:
|
|
609
616
|
async with merge_queue_into_iterator(
|
|
610
617
|
poll_acp_events(), self._event_queue
|
|
611
618
|
) as merged_events:
|
|
612
|
-
async for event in merged_events:
|
|
619
|
+
async for event in file_tracker.track(merged_events):
|
|
613
620
|
# Check for cancellation
|
|
614
621
|
if self._cancelled:
|
|
615
622
|
self.log.info("Stream cancelled by user")
|
|
@@ -628,40 +635,35 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
628
635
|
current_response_parts.append(
|
|
629
636
|
ToolCallPart(tool_name=tc_name, args=tc_input, tool_call_id=tc_id)
|
|
630
637
|
)
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
tc_name or "", tc_input or {}
|
|
634
|
-
):
|
|
635
|
-
touched_files.add(file_path)
|
|
636
|
-
|
|
637
|
-
# Distribute to handlers
|
|
638
|
-
for handler in self.event_handler._wrapped_handlers:
|
|
639
|
-
await handler(None, event)
|
|
638
|
+
|
|
639
|
+
await handler(None, event)
|
|
640
640
|
yield event
|
|
641
641
|
except asyncio.CancelledError:
|
|
642
642
|
self.log.info("Stream cancelled via task cancellation")
|
|
643
643
|
self._cancelled = True
|
|
644
|
+
finally:
|
|
645
|
+
# Clear deps from tool bridge
|
|
646
|
+
if self._tool_bridge:
|
|
647
|
+
self._tool_bridge.current_deps = None
|
|
644
648
|
|
|
645
649
|
# Handle cancellation - emit partial message
|
|
646
650
|
if self._cancelled:
|
|
647
651
|
text_content = "".join(text_chunks)
|
|
648
|
-
metadata: SimpleJsonType =
|
|
649
|
-
if touched_files:
|
|
650
|
-
metadata["touched_files"] = sorted(touched_files)
|
|
652
|
+
metadata: SimpleJsonType = file_tracker.get_metadata()
|
|
651
653
|
message = ChatMessage[str](
|
|
652
654
|
content=text_content,
|
|
653
655
|
role="assistant",
|
|
654
656
|
name=self.name,
|
|
655
657
|
message_id=message_id or str(uuid.uuid4()),
|
|
656
658
|
conversation_id=self.conversation_id,
|
|
659
|
+
parent_id=user_msg.message_id,
|
|
657
660
|
model_name=self.model_name,
|
|
658
661
|
messages=model_messages,
|
|
659
662
|
metadata=metadata,
|
|
660
663
|
finish_reason="stop",
|
|
661
664
|
)
|
|
662
665
|
complete_event = StreamCompleteEvent(message=message)
|
|
663
|
-
|
|
664
|
-
await handler(None, complete_event)
|
|
666
|
+
await handler(None, complete_event)
|
|
665
667
|
yield complete_event
|
|
666
668
|
self._current_stream_task = None
|
|
667
669
|
self._prompt_task = None
|
|
@@ -672,27 +674,44 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
672
674
|
finish_reason: FinishReason = STOP_REASON_MAP.get(response.stop_reason, "stop")
|
|
673
675
|
# Flush response parts to model_messages
|
|
674
676
|
if current_response_parts:
|
|
675
|
-
model_messages.append(
|
|
677
|
+
model_messages.append(
|
|
678
|
+
ModelResponse(
|
|
679
|
+
parts=current_response_parts,
|
|
680
|
+
finish_reason=finish_reason,
|
|
681
|
+
model_name=self.model_name,
|
|
682
|
+
provider_name=self.config.type,
|
|
683
|
+
)
|
|
684
|
+
)
|
|
676
685
|
|
|
677
686
|
text_content = "".join(text_chunks)
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
687
|
+
metadata = file_tracker.get_metadata()
|
|
688
|
+
|
|
689
|
+
# Calculate approximate token usage from what we can observe
|
|
690
|
+
input_parts = [*processed_prompts, *pending_parts]
|
|
691
|
+
usage, cost_info = await calculate_usage_from_parts(
|
|
692
|
+
input_parts=input_parts,
|
|
693
|
+
response_parts=current_response_parts,
|
|
694
|
+
text_content=text_content,
|
|
695
|
+
model_name=self.model_name,
|
|
696
|
+
provider=self.config.type,
|
|
697
|
+
)
|
|
698
|
+
|
|
682
699
|
message = ChatMessage[str](
|
|
683
700
|
content=text_content,
|
|
684
701
|
role="assistant",
|
|
685
702
|
name=self.name,
|
|
686
703
|
message_id=message_id or str(uuid.uuid4()),
|
|
687
704
|
conversation_id=self.conversation_id,
|
|
705
|
+
parent_id=user_msg.message_id,
|
|
688
706
|
model_name=self.model_name,
|
|
689
707
|
messages=model_messages,
|
|
690
708
|
metadata=metadata,
|
|
691
709
|
finish_reason=finish_reason,
|
|
710
|
+
usage=usage,
|
|
711
|
+
cost_info=cost_info,
|
|
692
712
|
)
|
|
693
713
|
complete_event = StreamCompleteEvent(message=message)
|
|
694
|
-
|
|
695
|
-
await handler(None, complete_event)
|
|
714
|
+
await handler(None, complete_event)
|
|
696
715
|
yield complete_event # Emit final StreamCompleteEvent with aggregated message
|
|
697
716
|
self.message_sent.emit(message)
|
|
698
717
|
conversation.add_chat_messages([user_msg, message]) # Record to conversation history
|
|
@@ -754,8 +773,9 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
754
773
|
if self._process: # Clean up existing process if any
|
|
755
774
|
await self._cleanup()
|
|
756
775
|
self.config = new_config # Update config and restart
|
|
757
|
-
await self._start_process()
|
|
758
|
-
|
|
776
|
+
process = await self._start_process()
|
|
777
|
+
async with monitor_process(process, context="ACP initialization"):
|
|
778
|
+
await self._initialize()
|
|
759
779
|
|
|
760
780
|
async def set_tool_confirmation_mode(self, mode: ToolConfirmationMode) -> None:
|
|
761
781
|
"""Set the tool confirmation mode for this agent.
|
|
@@ -790,10 +810,6 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
790
810
|
else:
|
|
791
811
|
self.log.info("Tool confirmation mode changed (local only)", mode=mode)
|
|
792
812
|
|
|
793
|
-
async def get_stats(self) -> MessageStats:
|
|
794
|
-
"""Get message statistics."""
|
|
795
|
-
return MessageStats(messages=list(self.conversation.chat_messages))
|
|
796
|
-
|
|
797
813
|
async def interrupt(self) -> None:
|
|
798
814
|
"""Interrupt the currently running stream.
|
|
799
815
|
|
|
@@ -822,12 +838,111 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
|
|
|
822
838
|
if self._current_stream_task and not self._current_stream_task.done():
|
|
823
839
|
self._current_stream_task.cancel()
|
|
824
840
|
|
|
841
|
+
async def get_available_models(self) -> list[ModelInfo] | None:
|
|
842
|
+
"""Get available models from the ACP session state.
|
|
843
|
+
|
|
844
|
+
Converts ACP ModelInfo to tokonomics ModelInfo format.
|
|
845
|
+
|
|
846
|
+
Returns:
|
|
847
|
+
List of tokonomics ModelInfo, or None if not available
|
|
848
|
+
"""
|
|
849
|
+
from tokonomics.model_discovery.model_info import ModelInfo
|
|
850
|
+
|
|
851
|
+
if not self._state or not self._state.models:
|
|
852
|
+
return None
|
|
853
|
+
|
|
854
|
+
# Convert ACP ModelInfo to tokonomics ModelInfo
|
|
855
|
+
result: list[ModelInfo] = []
|
|
856
|
+
for acp_model in self._state.models.available_models:
|
|
857
|
+
toko_model = ModelInfo(
|
|
858
|
+
id=acp_model.model_id,
|
|
859
|
+
name=acp_model.name,
|
|
860
|
+
description=acp_model.description,
|
|
861
|
+
)
|
|
862
|
+
result.append(toko_model)
|
|
863
|
+
return result
|
|
864
|
+
|
|
865
|
+
def get_modes(self) -> list[ModeCategory]:
|
|
866
|
+
"""Get available modes from the ACP session state.
|
|
867
|
+
|
|
868
|
+
Passthrough from remote ACP server's mode state.
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
List of ModeCategory from remote server, empty if not available
|
|
872
|
+
"""
|
|
873
|
+
from agentpool.agents.modes import ModeCategory, ModeInfo
|
|
874
|
+
|
|
875
|
+
if not self._state or not self._state.modes:
|
|
876
|
+
return []
|
|
877
|
+
|
|
878
|
+
# Convert ACP SessionModeState to our ModeCategory
|
|
879
|
+
acp_modes = self._state.modes
|
|
880
|
+
category_id = "remote"
|
|
881
|
+
modes = [
|
|
882
|
+
ModeInfo(
|
|
883
|
+
id=m.id,
|
|
884
|
+
name=m.name,
|
|
885
|
+
description=m.description or "",
|
|
886
|
+
category_id=category_id,
|
|
887
|
+
)
|
|
888
|
+
for m in acp_modes.available_modes
|
|
889
|
+
]
|
|
890
|
+
|
|
891
|
+
return [
|
|
892
|
+
ModeCategory(
|
|
893
|
+
id=category_id,
|
|
894
|
+
name="Mode",
|
|
895
|
+
available_modes=modes,
|
|
896
|
+
current_mode_id=acp_modes.current_mode_id,
|
|
897
|
+
)
|
|
898
|
+
]
|
|
899
|
+
|
|
900
|
+
async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
|
|
901
|
+
"""Set a mode on the remote ACP server.
|
|
902
|
+
|
|
903
|
+
For ACPAgent, this forwards the mode change to the remote ACP server.
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
mode: The mode to set - ModeInfo object or mode ID string
|
|
907
|
+
category_id: Optional category ID (ignored for ACP, only one category)
|
|
908
|
+
|
|
909
|
+
Raises:
|
|
910
|
+
RuntimeError: If not connected to ACP server
|
|
911
|
+
ValueError: If mode is not available
|
|
912
|
+
"""
|
|
913
|
+
from acp.schema import SetSessionModeRequest
|
|
914
|
+
|
|
915
|
+
# Extract mode_id from ModeInfo if provided
|
|
916
|
+
mode_id = mode.id if isinstance(mode, ModeInfo) else mode
|
|
917
|
+
|
|
918
|
+
if not self._connection or not self._session_id:
|
|
919
|
+
msg = "Not connected to ACP server"
|
|
920
|
+
raise RuntimeError(msg)
|
|
921
|
+
|
|
922
|
+
# Validate mode is available
|
|
923
|
+
available_modes = self.get_modes()
|
|
924
|
+
if available_modes:
|
|
925
|
+
valid_ids = {m.id for cat in available_modes for m in cat.available_modes}
|
|
926
|
+
if mode_id not in valid_ids:
|
|
927
|
+
msg = f"Unknown mode: {mode_id}. Available: {valid_ids}"
|
|
928
|
+
raise ValueError(msg)
|
|
929
|
+
|
|
930
|
+
# Forward mode change to remote ACP server
|
|
931
|
+
request = SetSessionModeRequest(session_id=self._session_id, mode_id=mode_id)
|
|
932
|
+
await self._connection.set_session_mode(request)
|
|
933
|
+
|
|
934
|
+
# Update local state
|
|
935
|
+
if self._state and self._state.modes:
|
|
936
|
+
self._state.modes.current_mode_id = mode_id
|
|
937
|
+
|
|
938
|
+
self.log.info("Mode changed on remote ACP server", mode_id=mode_id)
|
|
939
|
+
|
|
825
940
|
|
|
826
941
|
if __name__ == "__main__":
|
|
827
942
|
|
|
828
943
|
async def main() -> None:
|
|
829
944
|
"""Demo: Basic call to an ACP agent."""
|
|
830
|
-
args = ["run", "agentpool", "serve-acp"
|
|
945
|
+
args = ["run", "agentpool", "serve-acp"]
|
|
831
946
|
cwd = str(Path.cwd())
|
|
832
947
|
async with ACPAgent(command="uv", args=args, cwd=cwd, event_handlers=["detailed"]) as agent:
|
|
833
948
|
print("Response (streaming): ", end="", flush=True)
|
|
@@ -189,42 +189,56 @@ def acp_to_native_event(update: SessionUpdate) -> RichAgentStreamEvent[Any] | No
|
|
|
189
189
|
# Text message chunks -> PartDeltaEvent with TextPartDelta
|
|
190
190
|
case AgentMessageChunk(content=TextContentBlock(text=text)):
|
|
191
191
|
return PartDeltaEvent(index=0, delta=TextPartDelta(content_delta=text))
|
|
192
|
+
|
|
192
193
|
# Thought chunks -> PartDeltaEvent with ThinkingPartDelta
|
|
193
194
|
case AgentThoughtChunk(content=TextContentBlock(text=text)):
|
|
194
195
|
return PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta=text))
|
|
195
|
-
|
|
196
|
+
|
|
197
|
+
# User message echo - usually ignored
|
|
196
198
|
case UserMessageChunk():
|
|
197
|
-
return None
|
|
199
|
+
return None
|
|
200
|
+
|
|
198
201
|
# Tool call start -> ToolCallStartEvent
|
|
199
|
-
case ToolCallStart(
|
|
202
|
+
case ToolCallStart(
|
|
203
|
+
tool_call_id=tool_call_id,
|
|
204
|
+
title=title,
|
|
205
|
+
kind=kind,
|
|
206
|
+
content=content,
|
|
207
|
+
locations=locations,
|
|
208
|
+
raw_input=raw_input,
|
|
209
|
+
):
|
|
200
210
|
return ToolCallStartEvent(
|
|
201
|
-
tool_call_id=
|
|
202
|
-
tool_name=
|
|
203
|
-
title=
|
|
204
|
-
kind=
|
|
205
|
-
content=convert_acp_content(list(
|
|
206
|
-
locations=convert_acp_locations(list(
|
|
207
|
-
raw_input=
|
|
211
|
+
tool_call_id=tool_call_id,
|
|
212
|
+
tool_name=title, # ACP uses title, not separate tool_name
|
|
213
|
+
title=title,
|
|
214
|
+
kind=kind or "other",
|
|
215
|
+
content=convert_acp_content(list(content) if content else None),
|
|
216
|
+
locations=convert_acp_locations(list(locations) if locations else None),
|
|
217
|
+
raw_input=raw_input or {},
|
|
208
218
|
)
|
|
209
219
|
|
|
210
220
|
# Tool call progress -> ToolCallProgressEvent
|
|
211
|
-
case ToolCallProgress(
|
|
212
|
-
|
|
221
|
+
case ToolCallProgress(
|
|
222
|
+
tool_call_id=tool_call_id,
|
|
223
|
+
status=status,
|
|
224
|
+
title=title,
|
|
225
|
+
content=content,
|
|
226
|
+
raw_output=raw_output,
|
|
227
|
+
):
|
|
213
228
|
return ToolCallProgressEvent(
|
|
214
|
-
tool_call_id=
|
|
215
|
-
status=
|
|
216
|
-
title=
|
|
217
|
-
items=
|
|
218
|
-
message=str(
|
|
229
|
+
tool_call_id=tool_call_id,
|
|
230
|
+
status=status or "in_progress",
|
|
231
|
+
title=title,
|
|
232
|
+
items=convert_acp_content(list(content) if content else None),
|
|
233
|
+
message=str(raw_output) if raw_output else None,
|
|
219
234
|
)
|
|
220
235
|
|
|
221
236
|
# Plan update -> PlanUpdateEvent
|
|
222
|
-
case AgentPlanUpdate(entries=
|
|
237
|
+
case AgentPlanUpdate(entries=entries):
|
|
223
238
|
from agentpool.resource_providers.plan_provider import PlanEntry
|
|
224
239
|
|
|
225
240
|
native_entries = [
|
|
226
|
-
PlanEntry(content=e.content, priority=e.priority, status=e.status)
|
|
227
|
-
for e in acp_entries
|
|
241
|
+
PlanEntry(content=e.content, priority=e.priority, status=e.status) for e in entries
|
|
228
242
|
]
|
|
229
243
|
return PlanUpdateEvent(entries=native_entries)
|
|
230
244
|
|
|
@@ -251,6 +265,9 @@ def mcp_config_to_acp(config: MCPServerConfig) -> McpServer | None: ...
|
|
|
251
265
|
def mcp_config_to_acp(config: MCPServerConfig) -> McpServer | None:
|
|
252
266
|
"""Convert native MCPServerConfig to ACP McpServer format.
|
|
253
267
|
|
|
268
|
+
If the config has tool filtering (enabled_tools or disabled_tools),
|
|
269
|
+
the server is wrapped with mcp-filter proxy to apply the filtering.
|
|
270
|
+
|
|
254
271
|
Args:
|
|
255
272
|
config: agentpool MCP server configuration
|
|
256
273
|
|
|
@@ -265,19 +282,27 @@ def mcp_config_to_acp(config: MCPServerConfig) -> McpServer | None:
|
|
|
265
282
|
StreamableHTTPMCPServerConfig,
|
|
266
283
|
)
|
|
267
284
|
|
|
285
|
+
# If filtering is configured, wrap with mcp-filter first
|
|
286
|
+
if config.needs_tool_filtering():
|
|
287
|
+
config = config.wrap_with_mcp_filter()
|
|
288
|
+
|
|
268
289
|
match config:
|
|
269
290
|
case StdioMCPServerConfig(command=command, args=args):
|
|
270
291
|
env_vars = config.get_env_vars() if hasattr(config, "get_env_vars") else {}
|
|
292
|
+
env_list = [EnvVariable(name=k, value=v) for k, v in env_vars.items()]
|
|
271
293
|
return StdioMcpServer(
|
|
272
294
|
name=config.name or command,
|
|
273
295
|
command=command,
|
|
274
296
|
args=list(args) if args else [],
|
|
275
|
-
env=
|
|
297
|
+
env=env_list,
|
|
276
298
|
)
|
|
299
|
+
|
|
277
300
|
case SSEMCPServerConfig(url=url):
|
|
278
301
|
return SseMcpServer(name=config.name or str(url), url=url, headers=[])
|
|
302
|
+
|
|
279
303
|
case StreamableHTTPMCPServerConfig(url=url):
|
|
280
304
|
return HttpMcpServer(name=config.name or str(url), url=url, headers=[])
|
|
305
|
+
|
|
281
306
|
case _:
|
|
282
307
|
return None
|
|
283
308
|
|