agentpool 2.1.9__py3-none-any.whl → 2.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. acp/__init__.py +13 -0
  2. acp/bridge/README.md +15 -2
  3. acp/bridge/__init__.py +3 -2
  4. acp/bridge/__main__.py +60 -19
  5. acp/bridge/ws_server.py +173 -0
  6. acp/bridge/ws_server_cli.py +89 -0
  7. acp/notifications.py +2 -1
  8. acp/stdio.py +39 -9
  9. acp/transports.py +362 -2
  10. acp/utils.py +15 -2
  11. agentpool/__init__.py +4 -1
  12. agentpool/agents/__init__.py +2 -0
  13. agentpool/agents/acp_agent/acp_agent.py +203 -88
  14. agentpool/agents/acp_agent/acp_converters.py +46 -21
  15. agentpool/agents/acp_agent/client_handler.py +157 -3
  16. agentpool/agents/acp_agent/session_state.py +4 -1
  17. agentpool/agents/agent.py +314 -107
  18. agentpool/agents/agui_agent/__init__.py +0 -2
  19. agentpool/agents/agui_agent/agui_agent.py +90 -21
  20. agentpool/agents/agui_agent/agui_converters.py +0 -131
  21. agentpool/agents/base_agent.py +163 -1
  22. agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
  23. agentpool/agents/claude_code_agent/converters.py +71 -3
  24. agentpool/agents/claude_code_agent/history.py +474 -0
  25. agentpool/agents/context.py +40 -0
  26. agentpool/agents/events/__init__.py +2 -0
  27. agentpool/agents/events/builtin_handlers.py +2 -1
  28. agentpool/agents/events/event_emitter.py +29 -2
  29. agentpool/agents/events/events.py +20 -0
  30. agentpool/agents/modes.py +54 -0
  31. agentpool/agents/tool_call_accumulator.py +213 -0
  32. agentpool/common_types.py +21 -0
  33. agentpool/config_resources/__init__.py +38 -1
  34. agentpool/config_resources/claude_code_agent.yml +3 -0
  35. agentpool/delegation/pool.py +37 -29
  36. agentpool/delegation/team.py +1 -0
  37. agentpool/delegation/teamrun.py +1 -0
  38. agentpool/diagnostics/__init__.py +53 -0
  39. agentpool/diagnostics/lsp_manager.py +1593 -0
  40. agentpool/diagnostics/lsp_proxy.py +41 -0
  41. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  42. agentpool/diagnostics/models.py +398 -0
  43. agentpool/mcp_server/__init__.py +0 -2
  44. agentpool/mcp_server/client.py +12 -3
  45. agentpool/mcp_server/manager.py +25 -31
  46. agentpool/mcp_server/registries/official_registry_client.py +25 -0
  47. agentpool/mcp_server/tool_bridge.py +78 -66
  48. agentpool/messaging/__init__.py +0 -2
  49. agentpool/messaging/compaction.py +72 -197
  50. agentpool/messaging/message_history.py +12 -0
  51. agentpool/messaging/messages.py +52 -9
  52. agentpool/messaging/processing.py +3 -1
  53. agentpool/models/acp_agents/base.py +0 -22
  54. agentpool/models/acp_agents/mcp_capable.py +8 -148
  55. agentpool/models/acp_agents/non_mcp.py +129 -72
  56. agentpool/models/agents.py +35 -13
  57. agentpool/models/claude_code_agents.py +33 -2
  58. agentpool/models/manifest.py +43 -0
  59. agentpool/repomap.py +1 -1
  60. agentpool/resource_providers/__init__.py +9 -1
  61. agentpool/resource_providers/aggregating.py +52 -3
  62. agentpool/resource_providers/base.py +57 -1
  63. agentpool/resource_providers/mcp_provider.py +23 -0
  64. agentpool/resource_providers/plan_provider.py +130 -41
  65. agentpool/resource_providers/pool.py +2 -0
  66. agentpool/resource_providers/static.py +2 -0
  67. agentpool/sessions/__init__.py +2 -1
  68. agentpool/sessions/manager.py +31 -2
  69. agentpool/sessions/models.py +50 -0
  70. agentpool/skills/registry.py +13 -8
  71. agentpool/storage/manager.py +217 -1
  72. agentpool/testing.py +537 -19
  73. agentpool/utils/file_watcher.py +269 -0
  74. agentpool/utils/identifiers.py +121 -0
  75. agentpool/utils/pydantic_ai_helpers.py +46 -0
  76. agentpool/utils/streams.py +690 -1
  77. agentpool/utils/subprocess_utils.py +155 -0
  78. agentpool/utils/token_breakdown.py +461 -0
  79. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
  80. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
  81. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
  82. agentpool_cli/__main__.py +4 -0
  83. agentpool_cli/serve_acp.py +41 -20
  84. agentpool_cli/serve_agui.py +87 -0
  85. agentpool_cli/serve_opencode.py +119 -0
  86. agentpool_commands/__init__.py +30 -0
  87. agentpool_commands/agents.py +74 -1
  88. agentpool_commands/history.py +62 -0
  89. agentpool_commands/mcp.py +176 -0
  90. agentpool_commands/models.py +56 -3
  91. agentpool_commands/tools.py +57 -0
  92. agentpool_commands/utils.py +51 -0
  93. agentpool_config/builtin_tools.py +77 -22
  94. agentpool_config/commands.py +24 -1
  95. agentpool_config/compaction.py +258 -0
  96. agentpool_config/mcp_server.py +131 -1
  97. agentpool_config/storage.py +46 -1
  98. agentpool_config/tools.py +7 -1
  99. agentpool_config/toolsets.py +92 -148
  100. agentpool_server/acp_server/acp_agent.py +134 -150
  101. agentpool_server/acp_server/commands/acp_commands.py +216 -51
  102. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
  103. agentpool_server/acp_server/server.py +23 -79
  104. agentpool_server/acp_server/session.py +181 -19
  105. agentpool_server/opencode_server/.rules +95 -0
  106. agentpool_server/opencode_server/ENDPOINTS.md +362 -0
  107. agentpool_server/opencode_server/__init__.py +27 -0
  108. agentpool_server/opencode_server/command_validation.py +172 -0
  109. agentpool_server/opencode_server/converters.py +869 -0
  110. agentpool_server/opencode_server/dependencies.py +24 -0
  111. agentpool_server/opencode_server/input_provider.py +269 -0
  112. agentpool_server/opencode_server/models/__init__.py +228 -0
  113. agentpool_server/opencode_server/models/agent.py +53 -0
  114. agentpool_server/opencode_server/models/app.py +60 -0
  115. agentpool_server/opencode_server/models/base.py +26 -0
  116. agentpool_server/opencode_server/models/common.py +23 -0
  117. agentpool_server/opencode_server/models/config.py +37 -0
  118. agentpool_server/opencode_server/models/events.py +647 -0
  119. agentpool_server/opencode_server/models/file.py +88 -0
  120. agentpool_server/opencode_server/models/mcp.py +25 -0
  121. agentpool_server/opencode_server/models/message.py +162 -0
  122. agentpool_server/opencode_server/models/parts.py +190 -0
  123. agentpool_server/opencode_server/models/provider.py +81 -0
  124. agentpool_server/opencode_server/models/pty.py +43 -0
  125. agentpool_server/opencode_server/models/session.py +99 -0
  126. agentpool_server/opencode_server/routes/__init__.py +25 -0
  127. agentpool_server/opencode_server/routes/agent_routes.py +442 -0
  128. agentpool_server/opencode_server/routes/app_routes.py +139 -0
  129. agentpool_server/opencode_server/routes/config_routes.py +241 -0
  130. agentpool_server/opencode_server/routes/file_routes.py +392 -0
  131. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  132. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  133. agentpool_server/opencode_server/routes/message_routes.py +705 -0
  134. agentpool_server/opencode_server/routes/pty_routes.py +299 -0
  135. agentpool_server/opencode_server/routes/session_routes.py +1205 -0
  136. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  137. agentpool_server/opencode_server/server.py +430 -0
  138. agentpool_server/opencode_server/state.py +121 -0
  139. agentpool_server/opencode_server/time_utils.py +8 -0
  140. agentpool_storage/__init__.py +16 -0
  141. agentpool_storage/base.py +103 -0
  142. agentpool_storage/claude_provider.py +907 -0
  143. agentpool_storage/file_provider.py +129 -0
  144. agentpool_storage/memory_provider.py +61 -0
  145. agentpool_storage/models.py +3 -0
  146. agentpool_storage/opencode_provider.py +730 -0
  147. agentpool_storage/project_store.py +325 -0
  148. agentpool_storage/session_store.py +6 -0
  149. agentpool_storage/sql_provider/__init__.py +4 -2
  150. agentpool_storage/sql_provider/models.py +48 -0
  151. agentpool_storage/sql_provider/sql_provider.py +134 -1
  152. agentpool_storage/sql_provider/utils.py +10 -1
  153. agentpool_storage/text_log_provider.py +1 -0
  154. agentpool_toolsets/builtin/__init__.py +0 -8
  155. agentpool_toolsets/builtin/code.py +95 -56
  156. agentpool_toolsets/builtin/debug.py +16 -21
  157. agentpool_toolsets/builtin/execution_environment.py +99 -103
  158. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  159. agentpool_toolsets/builtin/skills.py +86 -4
  160. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  161. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  162. agentpool_toolsets/fsspec_toolset/grep.py +74 -2
  163. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  164. agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
  165. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  166. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  167. agentpool_toolsets/mcp_discovery/toolset.py +454 -0
  168. agentpool_toolsets/mcp_run_toolset.py +84 -6
  169. agentpool_toolsets/builtin/agent_management.py +0 -239
  170. agentpool_toolsets/builtin/history.py +0 -36
  171. agentpool_toolsets/builtin/integration.py +0 -85
  172. agentpool_toolsets/builtin/tool_management.py +0 -90
  173. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
  174. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -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 isinstance(item, str) and _is_slash_command(item):
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(enable_system_commands=True, commands=cmds)
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(self) -> Agent[ACPSession, str] | ACPAgent | AGUIAgent | ClaudeCodeAgent:
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
- try:
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
- execute_func=execute_prompt,
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
- execute_func=execute_prompt,
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
+ ```