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
|
@@ -15,6 +15,8 @@ from typing import TYPE_CHECKING, Any
|
|
|
15
15
|
from exxec.acp_provider import ACPExecutionEnvironment
|
|
16
16
|
import logfire
|
|
17
17
|
from pydantic_ai import (
|
|
18
|
+
BuiltinToolCallPart,
|
|
19
|
+
BuiltinToolReturnPart,
|
|
18
20
|
FinalResultEvent,
|
|
19
21
|
FunctionToolCallEvent,
|
|
20
22
|
FunctionToolResultEvent,
|
|
@@ -31,6 +33,7 @@ from pydantic_ai import (
|
|
|
31
33
|
UserPromptPart,
|
|
32
34
|
)
|
|
33
35
|
from slashed import Command, CommandStore
|
|
36
|
+
from tokonomics.model_discovery.model_info import ModelInfo
|
|
34
37
|
|
|
35
38
|
from acp import RequestPermissionRequest
|
|
36
39
|
from acp.acp_requests import ACPRequests
|
|
@@ -50,12 +53,15 @@ from agentpool import Agent, AgentContext # noqa: TC001
|
|
|
50
53
|
from agentpool.agents import SlashedAgent
|
|
51
54
|
from agentpool.agents.acp_agent import ACPAgent
|
|
52
55
|
from agentpool.agents.events import (
|
|
56
|
+
CompactionEvent,
|
|
53
57
|
PlanUpdateEvent,
|
|
54
58
|
StreamCompleteEvent,
|
|
55
59
|
ToolCallProgressEvent,
|
|
56
60
|
ToolCallStartEvent,
|
|
57
61
|
)
|
|
62
|
+
from agentpool.agents.modes import ModeInfo
|
|
58
63
|
from agentpool.log import get_logger
|
|
64
|
+
from agentpool.utils.pydantic_ai_helpers import safe_args_as_dict
|
|
59
65
|
from agentpool_commands import get_commands
|
|
60
66
|
from agentpool_commands.base import NodeCommand
|
|
61
67
|
from agentpool_server.acp_server.converters import (
|
|
@@ -76,6 +82,8 @@ if TYPE_CHECKING:
|
|
|
76
82
|
|
|
77
83
|
from acp import Client, RequestPermissionResponse
|
|
78
84
|
from acp.schema import (
|
|
85
|
+
AvailableCommandsUpdate,
|
|
86
|
+
ConfigOptionUpdate,
|
|
79
87
|
ContentBlock,
|
|
80
88
|
Implementation,
|
|
81
89
|
McpServer,
|
|
@@ -129,13 +137,27 @@ def _is_slash_command(text: str) -> bool:
|
|
|
129
137
|
|
|
130
138
|
def split_commands(
|
|
131
139
|
contents: Sequence[UserContent],
|
|
140
|
+
command_store: CommandStore,
|
|
132
141
|
) -> tuple[list[str], list[UserContent]]:
|
|
142
|
+
"""Split content into local slash commands and pass-through content.
|
|
143
|
+
|
|
144
|
+
Only commands that exist in the local command_store are extracted.
|
|
145
|
+
Remote commands (from nested ACP agents) stay in non_command_content
|
|
146
|
+
so they flow through to the agent and reach the nested server.
|
|
147
|
+
"""
|
|
133
148
|
commands: list[str] = []
|
|
134
149
|
non_command_content: list[UserContent] = []
|
|
135
150
|
for item in contents:
|
|
136
|
-
if
|
|
151
|
+
# Check if this is a LOCAL command we handle
|
|
152
|
+
if (
|
|
153
|
+
isinstance(item, str)
|
|
154
|
+
and _is_slash_command(item)
|
|
155
|
+
and (match := SLASH_PATTERN.match(item.strip()))
|
|
156
|
+
and command_store.get_command(match.group(1))
|
|
157
|
+
):
|
|
137
158
|
commands.append(item.strip())
|
|
138
159
|
else:
|
|
160
|
+
# Not a local command - pass through (may be remote command or regular text)
|
|
139
161
|
non_command_content.append(item)
|
|
140
162
|
return commands, non_command_content
|
|
141
163
|
|
|
@@ -231,6 +253,7 @@ class ACPSession:
|
|
|
231
253
|
self.log = logger.bind(session_id=self.session_id)
|
|
232
254
|
self._task_lock = asyncio.Lock()
|
|
233
255
|
self._cancelled = False
|
|
256
|
+
self._title_generation_triggered = False
|
|
234
257
|
self._current_tool_inputs: dict[str, dict[str, Any]] = {}
|
|
235
258
|
self._tool_call_states: dict[str, ToolCallState] = {}
|
|
236
259
|
self.fs = ACPFileSystem(self.client, session_id=self.session_id)
|
|
@@ -243,9 +266,10 @@ class ACPSession:
|
|
|
243
266
|
),
|
|
244
267
|
*get_acp_commands(),
|
|
245
268
|
]
|
|
246
|
-
self.command_store = CommandStore(
|
|
269
|
+
self.command_store = CommandStore(commands=cmds)
|
|
247
270
|
self.command_store._initialize_sync()
|
|
248
271
|
self._update_callbacks: list[Callable[[], None]] = []
|
|
272
|
+
self._remote_commands: list[AvailableCommand] = [] # Commands from nested ACP agents
|
|
249
273
|
|
|
250
274
|
self.staged_content = StagedContent()
|
|
251
275
|
# Inject Zed-specific instructions if client is Zed
|
|
@@ -294,8 +318,44 @@ class ACPSession:
|
|
|
294
318
|
return response
|
|
295
319
|
|
|
296
320
|
agent.acp_permission_callback = permission_callback
|
|
321
|
+
|
|
322
|
+
# Subscribe to state change signal for all agents
|
|
323
|
+
agent.state_updated.connect(self._on_state_updated)
|
|
297
324
|
self.log.info("Created ACP session", current_agent=self.current_agent_name)
|
|
298
325
|
|
|
326
|
+
async def _on_state_updated(
|
|
327
|
+
self, state: ModeInfo | ModelInfo | AvailableCommandsUpdate | ConfigOptionUpdate
|
|
328
|
+
) -> None:
|
|
329
|
+
"""Handle state update signal from agent - forward to ACP client."""
|
|
330
|
+
from acp.schema import (
|
|
331
|
+
AvailableCommandsUpdate as ACPAvailableCommandsUpdate,
|
|
332
|
+
ConfigOptionUpdate as ACPConfigOptionUpdate,
|
|
333
|
+
CurrentModelUpdate,
|
|
334
|
+
CurrentModeUpdate,
|
|
335
|
+
SessionNotification,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
update: CurrentModeUpdate | CurrentModelUpdate | ACPConfigOptionUpdate
|
|
339
|
+
match state:
|
|
340
|
+
case ModeInfo(id=mode_id):
|
|
341
|
+
update = CurrentModeUpdate(current_mode_id=mode_id)
|
|
342
|
+
self.log.debug("Forwarding mode change to client", mode_id=mode_id)
|
|
343
|
+
case ModelInfo(id=model_id):
|
|
344
|
+
update = CurrentModelUpdate(current_model_id=model_id)
|
|
345
|
+
self.log.debug("Forwarding model change to client", model_id=model_id)
|
|
346
|
+
case ACPAvailableCommandsUpdate(available_commands=cmds):
|
|
347
|
+
# Store remote commands and send merged list
|
|
348
|
+
self._remote_commands = list(cmds)
|
|
349
|
+
await self.send_available_commands_update()
|
|
350
|
+
self.log.debug("Merged and sent commands update to client")
|
|
351
|
+
return
|
|
352
|
+
case ACPConfigOptionUpdate():
|
|
353
|
+
update = state
|
|
354
|
+
self.log.debug("Forwarding config option update to client")
|
|
355
|
+
|
|
356
|
+
notification = SessionNotification(session_id=self.session_id, update=update)
|
|
357
|
+
await self.client.session_update(notification)
|
|
358
|
+
|
|
299
359
|
async def initialize(self) -> None:
|
|
300
360
|
"""Initialize async resources. Must be called after construction."""
|
|
301
361
|
await self.acp_env.__aenter__()
|
|
@@ -377,7 +437,14 @@ class ACPSession:
|
|
|
377
437
|
self.log.exception("Failed to discover client-side skills", error=e)
|
|
378
438
|
|
|
379
439
|
@property
|
|
380
|
-
def agent(
|
|
440
|
+
def agent(
|
|
441
|
+
self,
|
|
442
|
+
) -> (
|
|
443
|
+
Agent[ACPSession, str]
|
|
444
|
+
| ACPAgent[ACPSession]
|
|
445
|
+
| AGUIAgent[ACPSession]
|
|
446
|
+
| ClaudeCodeAgent[ACPSession]
|
|
447
|
+
):
|
|
381
448
|
"""Get the currently active agent."""
|
|
382
449
|
if self.current_agent_name in self.agent_pool.agents:
|
|
383
450
|
return self.agent_pool.get_agent(self.current_agent_name, deps_type=ACPSession)
|
|
@@ -425,6 +492,10 @@ class ACPSession:
|
|
|
425
492
|
self._cancelled = True
|
|
426
493
|
self.log.info("Session cancelled, interrupting agent")
|
|
427
494
|
|
|
495
|
+
# Clear pending tool call states to avoid stale data on next prompt
|
|
496
|
+
self._tool_call_states.clear()
|
|
497
|
+
self._current_tool_inputs.clear()
|
|
498
|
+
|
|
428
499
|
# Actively interrupt the agent's stream
|
|
429
500
|
try:
|
|
430
501
|
await self.agent.interrupt()
|
|
@@ -450,7 +521,7 @@ class ACPSession:
|
|
|
450
521
|
if not contents:
|
|
451
522
|
self.log.warning("Empty prompt received")
|
|
452
523
|
return "refusal"
|
|
453
|
-
commands, non_command_content = split_commands(contents)
|
|
524
|
+
commands, non_command_content = split_commands(contents, self.command_store)
|
|
454
525
|
async with self._task_lock:
|
|
455
526
|
if commands: # Process commands if found
|
|
456
527
|
for command in commands:
|
|
@@ -474,15 +545,22 @@ class ACPSession:
|
|
|
474
545
|
|
|
475
546
|
try: # Use the session's persistent input provider
|
|
476
547
|
async for event in self.agent.run_stream(
|
|
477
|
-
*all_content, input_provider=self.input_provider
|
|
548
|
+
*all_content, input_provider=self.input_provider, deps=self
|
|
478
549
|
):
|
|
479
550
|
if self._cancelled:
|
|
551
|
+
self.log.info("Cancelled during event loop")
|
|
480
552
|
return "cancelled"
|
|
481
553
|
|
|
482
554
|
event_count += 1
|
|
483
555
|
await self.handle_event(event)
|
|
484
556
|
self.log.info("Streaming finished", events_processed=event_count)
|
|
485
557
|
|
|
558
|
+
except asyncio.CancelledError:
|
|
559
|
+
# Task was cancelled (e.g., via interrupt()) - return proper stop reason
|
|
560
|
+
# This is critical: CancelledError doesn't inherit from Exception,
|
|
561
|
+
# so we must catch it explicitly to send the PromptResponse
|
|
562
|
+
self.log.info("Stream cancelled via CancelledError")
|
|
563
|
+
return "cancelled"
|
|
486
564
|
except UsageLimitExceeded as e:
|
|
487
565
|
self.log.info("Usage limit exceeded", error=str(e))
|
|
488
566
|
error_msg = str(e) # Determine which limit was hit based on error
|
|
@@ -503,16 +581,51 @@ class ACPSession:
|
|
|
503
581
|
)
|
|
504
582
|
return "end_turn"
|
|
505
583
|
else:
|
|
584
|
+
# Trigger title generation on first successful prompt
|
|
585
|
+
if not self._title_generation_triggered and self.agent_pool.storage:
|
|
586
|
+
self._title_generation_triggered = True
|
|
587
|
+
self.acp_agent.tasks.create_task(
|
|
588
|
+
self._generate_title(),
|
|
589
|
+
name=f"generate_title_{self.session_id}",
|
|
590
|
+
)
|
|
506
591
|
return "end_turn"
|
|
507
592
|
|
|
593
|
+
async def _generate_title(self) -> None:
|
|
594
|
+
"""Generate conversation title in the background."""
|
|
595
|
+
try:
|
|
596
|
+
messages = self.agent.conversation.get_history()
|
|
597
|
+
if not messages:
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
title = await self.agent_pool.storage.generate_conversation_title(
|
|
601
|
+
self.session_id,
|
|
602
|
+
messages,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# Persist to session store
|
|
606
|
+
if title and self.manager:
|
|
607
|
+
session_data = await self.manager.session_manager.store.load(self.session_id)
|
|
608
|
+
if session_data:
|
|
609
|
+
updated_data = session_data.with_title(title)
|
|
610
|
+
await self.manager.session_manager.store.save(updated_data)
|
|
611
|
+
self.log.info("Generated session title", title=title)
|
|
612
|
+
except Exception:
|
|
613
|
+
self.log.exception("Failed to generate conversation title")
|
|
614
|
+
|
|
508
615
|
async def _send_error_notification(self, message: str) -> None:
|
|
509
616
|
"""Send error notification, with exception handling."""
|
|
617
|
+
if self._cancelled:
|
|
618
|
+
return
|
|
510
619
|
try:
|
|
511
620
|
await self.notifications.send_agent_text(message)
|
|
512
621
|
except Exception:
|
|
513
622
|
self.log.exception("Failed to send error notification")
|
|
514
623
|
|
|
515
624
|
async def handle_event(self, event: RichAgentStreamEvent[Any]) -> None: # noqa: PLR0915
|
|
625
|
+
# Don't send notifications after cancellation to avoid stale updates
|
|
626
|
+
if self._cancelled:
|
|
627
|
+
return
|
|
628
|
+
|
|
516
629
|
match event:
|
|
517
630
|
case (
|
|
518
631
|
PartStartEvent(part=TextPart(content=delta))
|
|
@@ -526,6 +639,36 @@ class ACPSession:
|
|
|
526
639
|
):
|
|
527
640
|
await self.notifications.send_agent_thought(delta or "\n")
|
|
528
641
|
|
|
642
|
+
# Builtin tool call started (e.g., WebSearchTool, CodeExecutionTool)
|
|
643
|
+
case PartStartEvent(part=BuiltinToolCallPart() as part):
|
|
644
|
+
tool_call_id = part.tool_call_id
|
|
645
|
+
tool_input = safe_args_as_dict(part, default={})
|
|
646
|
+
self._current_tool_inputs[tool_call_id] = tool_input
|
|
647
|
+
state = self._get_or_create_tool_state(
|
|
648
|
+
tool_call_id=tool_call_id,
|
|
649
|
+
tool_name=part.tool_name,
|
|
650
|
+
tool_input=tool_input,
|
|
651
|
+
)
|
|
652
|
+
await state.start()
|
|
653
|
+
|
|
654
|
+
# Builtin tool completed
|
|
655
|
+
case PartStartEvent(part=BuiltinToolReturnPart() as part):
|
|
656
|
+
tool_call_id = part.tool_call_id
|
|
657
|
+
if complete_state := self._tool_call_states.get(tool_call_id):
|
|
658
|
+
final_output = part.content
|
|
659
|
+
if complete_state.has_content:
|
|
660
|
+
await complete_state.complete(raw_output=final_output)
|
|
661
|
+
else:
|
|
662
|
+
converted_blocks = to_acp_content_blocks(final_output)
|
|
663
|
+
content_items = [
|
|
664
|
+
ContentToolCallContent(content=block) for block in converted_blocks
|
|
665
|
+
]
|
|
666
|
+
await complete_state.complete(
|
|
667
|
+
raw_output=final_output,
|
|
668
|
+
content=content_items,
|
|
669
|
+
)
|
|
670
|
+
self._cleanup_tool_state(tool_call_id)
|
|
671
|
+
|
|
529
672
|
case PartStartEvent(part=part):
|
|
530
673
|
self.log.debug("Received unhandled PartStartEvent", part=part)
|
|
531
674
|
|
|
@@ -551,14 +694,7 @@ class ACPSession:
|
|
|
551
694
|
# Tool call started - create/update state and start notification
|
|
552
695
|
case FunctionToolCallEvent(part=part):
|
|
553
696
|
tool_call_id = part.tool_call_id
|
|
554
|
-
|
|
555
|
-
tool_input = part.args_as_dict()
|
|
556
|
-
except ValueError as e:
|
|
557
|
-
# Args might be malformed - use empty dict and log
|
|
558
|
-
self.log.warning(
|
|
559
|
-
"Failed to parse tool args", tool_name=part.tool_name, error=str(e)
|
|
560
|
-
)
|
|
561
|
-
tool_input = {}
|
|
697
|
+
tool_input = safe_args_as_dict(part, default={})
|
|
562
698
|
self._current_tool_inputs[tool_call_id] = tool_input
|
|
563
699
|
# Create state and send initial notification
|
|
564
700
|
state = self._get_or_create_tool_state(
|
|
@@ -694,6 +830,8 @@ class ACPSession:
|
|
|
694
830
|
status=status,
|
|
695
831
|
changed_lines=[],
|
|
696
832
|
)
|
|
833
|
+
# Mark content as sent so completion doesn't override
|
|
834
|
+
progress_state._has_content = True
|
|
697
835
|
case LocationContentItem(path=loc_path):
|
|
698
836
|
location_paths.append(loc_path)
|
|
699
837
|
|
|
@@ -717,6 +855,25 @@ class ACPSession:
|
|
|
717
855
|
]
|
|
718
856
|
await self.notifications.update_plan(acp_entries)
|
|
719
857
|
|
|
858
|
+
case CompactionEvent(trigger=trigger, phase=phase):
|
|
859
|
+
# Convert semantic CompactionEvent to text for ACP display
|
|
860
|
+
if phase == "starting":
|
|
861
|
+
if trigger == "auto":
|
|
862
|
+
text = (
|
|
863
|
+
"\n\n---\n\n"
|
|
864
|
+
"📦 **Context compaction** triggered. "
|
|
865
|
+
"Summarizing conversation..."
|
|
866
|
+
"\n\n---\n\n"
|
|
867
|
+
)
|
|
868
|
+
else:
|
|
869
|
+
text = (
|
|
870
|
+
"\n\n---\n\n"
|
|
871
|
+
"📦 **Manual compaction** requested. "
|
|
872
|
+
"Summarizing conversation..."
|
|
873
|
+
"\n\n---\n\n"
|
|
874
|
+
)
|
|
875
|
+
await self.notifications.send_agent_text(text)
|
|
876
|
+
|
|
720
877
|
case _:
|
|
721
878
|
self.log.debug("Unhandled event", event_type=type(event).__name__)
|
|
722
879
|
|
|
@@ -743,9 +900,14 @@ class ACPSession:
|
|
|
743
900
|
self.log.exception("Error closing session")
|
|
744
901
|
|
|
745
902
|
async def send_available_commands_update(self) -> None:
|
|
746
|
-
"""Send current available commands to client.
|
|
903
|
+
"""Send current available commands to client.
|
|
904
|
+
|
|
905
|
+
Merges local commands from command_store with any remote commands
|
|
906
|
+
from nested ACP agents.
|
|
907
|
+
"""
|
|
747
908
|
try:
|
|
748
|
-
commands = self.get_acp_commands()
|
|
909
|
+
commands = self.get_acp_commands() # Local commands
|
|
910
|
+
commands.extend(self._remote_commands) # Merge remote commands
|
|
749
911
|
await self.notifications.update_commands(commands)
|
|
750
912
|
except Exception:
|
|
751
913
|
self.log.exception("Failed to send available commands update")
|
|
@@ -908,8 +1070,8 @@ class ACPSession:
|
|
|
908
1070
|
usage_hint = (
|
|
909
1071
|
" ".join(f"<{arg['name']}>" for arg in prompt.arguments) if prompt.arguments else None
|
|
910
1072
|
)
|
|
911
|
-
return Command(
|
|
912
|
-
|
|
1073
|
+
return Command.from_raw(
|
|
1074
|
+
execute_prompt,
|
|
913
1075
|
name=prompt.name,
|
|
914
1076
|
description=prompt.description or f"MCP prompt: {prompt.name}",
|
|
915
1077
|
category="mcp",
|
|
@@ -960,8 +1122,8 @@ class ACPSession:
|
|
|
960
1122
|
# Create command name - prefix with provider if not builtin
|
|
961
1123
|
command_name = f"{provider}_{name}" if provider != "builtin" else name
|
|
962
1124
|
|
|
963
|
-
return Command(
|
|
964
|
-
|
|
1125
|
+
return Command.from_raw(
|
|
1126
|
+
execute_prompt,
|
|
965
1127
|
name=command_name,
|
|
966
1128
|
description=f"Prompt hub: {provider}:{name}",
|
|
967
1129
|
category="prompts",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# OpenCode-Compatible Server
|
|
2
|
+
|
|
3
|
+
This module implements an OpenCode-compatible API server, allowing OpenCode SDK clients
|
|
4
|
+
to interact with AgentPool agents.
|
|
5
|
+
|
|
6
|
+
## Reference Documentation
|
|
7
|
+
|
|
8
|
+
- **Python SDK**: https://github.com/sst/opencode-sdk-python
|
|
9
|
+
- **Server Docs**: https://raw.githubusercontent.com/sst/opencode/refs/heads/dev/packages/web/src/content/docs/server.mdx
|
|
10
|
+
- **OpenCode Main Repo**: https://github.com/sst/opencode
|
|
11
|
+
|
|
12
|
+
## SDK Clone (for reference during development)
|
|
13
|
+
|
|
14
|
+
To get the SDK locally for reference:
|
|
15
|
+
```bash
|
|
16
|
+
git clone --depth 1 https://github.com/sst/opencode-sdk-python.git /tmp/opencode-sdk-python
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Key SDK paths:
|
|
20
|
+
- Types: `src/opencode_ai/types/` - All Pydantic models
|
|
21
|
+
- Resources: `src/opencode_ai/resources/` - API client methods (shows endpoint signatures)
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
opencode_server/
|
|
27
|
+
├── __init__.py
|
|
28
|
+
├── server.py # FastAPI app factory and main server class
|
|
29
|
+
├── models/ # Pydantic models matching OpenCode API types
|
|
30
|
+
│ ├── __init__.py
|
|
31
|
+
│ ├── base.py # Shared base model with camelCase alias config
|
|
32
|
+
│ ├── session.py # Session, SessionStatus, etc.
|
|
33
|
+
│ ├── message.py # Message, Parts, etc.
|
|
34
|
+
│ ├── provider.py # Provider, Model, Config
|
|
35
|
+
│ ├── file.py # File operations models
|
|
36
|
+
│ └── events.py # SSE event models
|
|
37
|
+
├── routes/ # Route handlers grouped by domain
|
|
38
|
+
│ ├── __init__.py
|
|
39
|
+
│ ├── global_routes.py
|
|
40
|
+
│ ├── session_routes.py
|
|
41
|
+
│ ├── message_routes.py
|
|
42
|
+
│ ├── file_routes.py
|
|
43
|
+
│ ├── config_routes.py
|
|
44
|
+
│ └── agent_routes.py
|
|
45
|
+
├── state.py # Server state management
|
|
46
|
+
└── ENDPOINTS.md # Implementation status checklist
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Key Conventions
|
|
50
|
+
|
|
51
|
+
### Models
|
|
52
|
+
- All models inherit from `OpenCodeBaseModel` which sets `populate_by_name=True`
|
|
53
|
+
- Use `Field(alias="camelCase")` for fields that differ from snake_case
|
|
54
|
+
- OpenCode uses camelCase in JSON (e.g., `sessionID`, `messageID`, `providerID`)
|
|
55
|
+
|
|
56
|
+
### Routes
|
|
57
|
+
- Each route file defines a router with appropriate prefix/tags
|
|
58
|
+
- Routes are registered in `server.py`
|
|
59
|
+
- Use dependency injection for server state access
|
|
60
|
+
|
|
61
|
+
### Field Naming Examples
|
|
62
|
+
```python
|
|
63
|
+
session_id: str = Field(alias="sessionID")
|
|
64
|
+
message_id: str = Field(alias="messageID")
|
|
65
|
+
provider_id: str = Field(alias="providerID")
|
|
66
|
+
model_id: str = Field(alias="modelID")
|
|
67
|
+
parent_id: str | None = Field(default=None, alias="parentID")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Implementation Status
|
|
71
|
+
|
|
72
|
+
See `ENDPOINTS.md` for the full checklist of endpoints and their implementation status.
|
|
73
|
+
|
|
74
|
+
## Logging
|
|
75
|
+
|
|
76
|
+
Log file location: `~/.local/state/agentpool/log/opencode.log`
|
|
77
|
+
|
|
78
|
+
To tail the logs:
|
|
79
|
+
```bash
|
|
80
|
+
tail -f ~/.local/state/agentpool/log/opencode.log
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
To filter for SSE events:
|
|
84
|
+
```bash
|
|
85
|
+
tail -f ~/.local/state/agentpool/log/opencode.log | grep -i sse
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Testing with the SDK
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from opencode_ai import Opencode
|
|
92
|
+
|
|
93
|
+
client = Opencode(base_url="http://localhost:4096")
|
|
94
|
+
sessions = client.session.list()
|
|
95
|
+
```
|