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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. acp/__init__.py +0 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/client/connection.py +38 -29
  7. acp/client/implementations/default_client.py +3 -2
  8. acp/client/implementations/headless_client.py +2 -2
  9. acp/connection.py +2 -2
  10. acp/notifications.py +18 -49
  11. acp/schema/__init__.py +2 -0
  12. acp/schema/agent_responses.py +21 -0
  13. acp/schema/client_requests.py +3 -3
  14. acp/schema/session_state.py +63 -29
  15. acp/task/supervisor.py +2 -2
  16. acp/utils.py +2 -2
  17. agentpool/__init__.py +2 -0
  18. agentpool/agents/acp_agent/acp_agent.py +278 -263
  19. agentpool/agents/acp_agent/acp_converters.py +150 -17
  20. agentpool/agents/acp_agent/client_handler.py +35 -24
  21. agentpool/agents/acp_agent/session_state.py +14 -6
  22. agentpool/agents/agent.py +471 -643
  23. agentpool/agents/agui_agent/agui_agent.py +104 -107
  24. agentpool/agents/agui_agent/helpers.py +3 -4
  25. agentpool/agents/base_agent.py +485 -32
  26. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  27. agentpool/agents/claude_code_agent/__init__.py +13 -1
  28. agentpool/agents/claude_code_agent/claude_code_agent.py +654 -334
  29. agentpool/agents/claude_code_agent/converters.py +4 -141
  30. agentpool/agents/claude_code_agent/models.py +77 -0
  31. agentpool/agents/claude_code_agent/static_info.py +100 -0
  32. agentpool/agents/claude_code_agent/usage.py +242 -0
  33. agentpool/agents/events/__init__.py +22 -0
  34. agentpool/agents/events/builtin_handlers.py +65 -0
  35. agentpool/agents/events/event_emitter.py +3 -0
  36. agentpool/agents/events/events.py +84 -3
  37. agentpool/agents/events/infer_info.py +145 -0
  38. agentpool/agents/events/processors.py +254 -0
  39. agentpool/agents/interactions.py +41 -6
  40. agentpool/agents/modes.py +13 -0
  41. agentpool/agents/slashed_agent.py +5 -4
  42. agentpool/agents/tool_wrapping.py +18 -6
  43. agentpool/common_types.py +35 -21
  44. agentpool/config_resources/acp_assistant.yml +2 -2
  45. agentpool/config_resources/agents.yml +3 -0
  46. agentpool/config_resources/agents_template.yml +1 -0
  47. agentpool/config_resources/claude_code_agent.yml +9 -8
  48. agentpool/config_resources/external_acp_agents.yml +2 -1
  49. agentpool/delegation/base_team.py +4 -30
  50. agentpool/delegation/pool.py +104 -265
  51. agentpool/delegation/team.py +57 -57
  52. agentpool/delegation/teamrun.py +50 -55
  53. agentpool/functional/run.py +10 -4
  54. agentpool/mcp_server/client.py +73 -38
  55. agentpool/mcp_server/conversions.py +54 -13
  56. agentpool/mcp_server/manager.py +9 -23
  57. agentpool/mcp_server/registries/official_registry_client.py +10 -1
  58. agentpool/mcp_server/tool_bridge.py +114 -79
  59. agentpool/messaging/connection_manager.py +11 -10
  60. agentpool/messaging/event_manager.py +5 -5
  61. agentpool/messaging/message_container.py +6 -30
  62. agentpool/messaging/message_history.py +87 -8
  63. agentpool/messaging/messagenode.py +52 -14
  64. agentpool/messaging/messages.py +2 -26
  65. agentpool/messaging/processing.py +10 -22
  66. agentpool/models/__init__.py +1 -1
  67. agentpool/models/acp_agents/base.py +6 -2
  68. agentpool/models/acp_agents/mcp_capable.py +124 -15
  69. agentpool/models/acp_agents/non_mcp.py +0 -23
  70. agentpool/models/agents.py +66 -66
  71. agentpool/models/agui_agents.py +1 -1
  72. agentpool/models/claude_code_agents.py +111 -17
  73. agentpool/models/file_parsing.py +0 -1
  74. agentpool/models/manifest.py +70 -50
  75. agentpool/prompts/conversion_manager.py +1 -1
  76. agentpool/prompts/prompts.py +5 -2
  77. agentpool/resource_providers/__init__.py +2 -0
  78. agentpool/resource_providers/aggregating.py +4 -2
  79. agentpool/resource_providers/base.py +13 -3
  80. agentpool/resource_providers/codemode/code_executor.py +72 -5
  81. agentpool/resource_providers/codemode/helpers.py +2 -2
  82. agentpool/resource_providers/codemode/provider.py +64 -12
  83. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  84. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  85. agentpool/resource_providers/filtering.py +3 -1
  86. agentpool/resource_providers/mcp_provider.py +66 -12
  87. agentpool/resource_providers/plan_provider.py +111 -18
  88. agentpool/resource_providers/pool.py +5 -3
  89. agentpool/resource_providers/resource_info.py +111 -0
  90. agentpool/resource_providers/static.py +2 -2
  91. agentpool/sessions/__init__.py +2 -0
  92. agentpool/sessions/manager.py +2 -3
  93. agentpool/sessions/models.py +9 -6
  94. agentpool/sessions/protocol.py +28 -0
  95. agentpool/sessions/session.py +11 -55
  96. agentpool/storage/manager.py +361 -54
  97. agentpool/talk/registry.py +4 -4
  98. agentpool/talk/talk.py +9 -10
  99. agentpool/testing.py +1 -1
  100. agentpool/tool_impls/__init__.py +6 -0
  101. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  102. agentpool/tool_impls/agent_cli/tool.py +95 -0
  103. agentpool/tool_impls/bash/__init__.py +64 -0
  104. agentpool/tool_impls/bash/helpers.py +35 -0
  105. agentpool/tool_impls/bash/tool.py +171 -0
  106. agentpool/tool_impls/delete_path/__init__.py +70 -0
  107. agentpool/tool_impls/delete_path/tool.py +142 -0
  108. agentpool/tool_impls/download_file/__init__.py +80 -0
  109. agentpool/tool_impls/download_file/tool.py +183 -0
  110. agentpool/tool_impls/execute_code/__init__.py +55 -0
  111. agentpool/tool_impls/execute_code/tool.py +163 -0
  112. agentpool/tool_impls/grep/__init__.py +80 -0
  113. agentpool/tool_impls/grep/tool.py +200 -0
  114. agentpool/tool_impls/list_directory/__init__.py +73 -0
  115. agentpool/tool_impls/list_directory/tool.py +197 -0
  116. agentpool/tool_impls/question/__init__.py +42 -0
  117. agentpool/tool_impls/question/tool.py +127 -0
  118. agentpool/tool_impls/read/__init__.py +104 -0
  119. agentpool/tool_impls/read/tool.py +305 -0
  120. agentpool/tools/__init__.py +2 -1
  121. agentpool/tools/base.py +114 -34
  122. agentpool/tools/manager.py +57 -1
  123. agentpool/ui/base.py +2 -2
  124. agentpool/ui/mock_provider.py +2 -2
  125. agentpool/ui/stdlib_provider.py +2 -2
  126. agentpool/utils/streams.py +21 -96
  127. agentpool/vfs_registry.py +7 -2
  128. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
  129. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
  130. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  131. agentpool_cli/__main__.py +20 -0
  132. agentpool_cli/create.py +1 -1
  133. agentpool_cli/serve_acp.py +59 -1
  134. agentpool_cli/serve_opencode.py +1 -1
  135. agentpool_cli/ui.py +557 -0
  136. agentpool_commands/__init__.py +12 -5
  137. agentpool_commands/agents.py +1 -1
  138. agentpool_commands/pool.py +260 -0
  139. agentpool_commands/session.py +1 -1
  140. agentpool_commands/text_sharing/__init__.py +119 -0
  141. agentpool_commands/text_sharing/base.py +123 -0
  142. agentpool_commands/text_sharing/github_gist.py +80 -0
  143. agentpool_commands/text_sharing/opencode.py +462 -0
  144. agentpool_commands/text_sharing/paste_rs.py +59 -0
  145. agentpool_commands/text_sharing/pastebin.py +116 -0
  146. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  147. agentpool_commands/utils.py +31 -32
  148. agentpool_config/__init__.py +30 -2
  149. agentpool_config/agentpool_tools.py +498 -0
  150. agentpool_config/converters.py +1 -1
  151. agentpool_config/event_handlers.py +42 -0
  152. agentpool_config/events.py +1 -1
  153. agentpool_config/forward_targets.py +1 -4
  154. agentpool_config/jinja.py +3 -3
  155. agentpool_config/mcp_server.py +1 -5
  156. agentpool_config/nodes.py +1 -1
  157. agentpool_config/observability.py +44 -0
  158. agentpool_config/session.py +0 -3
  159. agentpool_config/storage.py +38 -39
  160. agentpool_config/task.py +3 -3
  161. agentpool_config/tools.py +11 -28
  162. agentpool_config/toolsets.py +22 -90
  163. agentpool_server/a2a_server/agent_worker.py +307 -0
  164. agentpool_server/a2a_server/server.py +23 -18
  165. agentpool_server/acp_server/acp_agent.py +125 -56
  166. agentpool_server/acp_server/commands/acp_commands.py +46 -216
  167. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
  168. agentpool_server/acp_server/event_converter.py +651 -0
  169. agentpool_server/acp_server/input_provider.py +53 -10
  170. agentpool_server/acp_server/server.py +1 -11
  171. agentpool_server/acp_server/session.py +90 -410
  172. agentpool_server/acp_server/session_manager.py +8 -34
  173. agentpool_server/agui_server/server.py +3 -1
  174. agentpool_server/mcp_server/server.py +5 -2
  175. agentpool_server/opencode_server/ENDPOINTS.md +53 -14
  176. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  177. agentpool_server/opencode_server/__init__.py +0 -8
  178. agentpool_server/opencode_server/converters.py +132 -26
  179. agentpool_server/opencode_server/input_provider.py +160 -8
  180. agentpool_server/opencode_server/models/__init__.py +42 -20
  181. agentpool_server/opencode_server/models/app.py +12 -0
  182. agentpool_server/opencode_server/models/events.py +203 -29
  183. agentpool_server/opencode_server/models/mcp.py +19 -0
  184. agentpool_server/opencode_server/models/message.py +18 -1
  185. agentpool_server/opencode_server/models/parts.py +134 -1
  186. agentpool_server/opencode_server/models/question.py +56 -0
  187. agentpool_server/opencode_server/models/session.py +13 -1
  188. agentpool_server/opencode_server/routes/__init__.py +4 -0
  189. agentpool_server/opencode_server/routes/agent_routes.py +33 -2
  190. agentpool_server/opencode_server/routes/app_routes.py +66 -3
  191. agentpool_server/opencode_server/routes/config_routes.py +66 -5
  192. agentpool_server/opencode_server/routes/file_routes.py +184 -5
  193. agentpool_server/opencode_server/routes/global_routes.py +1 -1
  194. agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
  195. agentpool_server/opencode_server/routes/message_routes.py +122 -66
  196. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  197. agentpool_server/opencode_server/routes/pty_routes.py +23 -22
  198. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  199. agentpool_server/opencode_server/routes/session_routes.py +139 -68
  200. agentpool_server/opencode_server/routes/tui_routes.py +1 -1
  201. agentpool_server/opencode_server/server.py +47 -2
  202. agentpool_server/opencode_server/state.py +30 -0
  203. agentpool_storage/__init__.py +0 -4
  204. agentpool_storage/base.py +81 -2
  205. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  206. agentpool_storage/claude_provider/__init__.py +42 -0
  207. agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
  208. agentpool_storage/file_provider.py +149 -15
  209. agentpool_storage/memory_provider.py +132 -12
  210. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  211. agentpool_storage/opencode_provider/__init__.py +16 -0
  212. agentpool_storage/opencode_provider/helpers.py +414 -0
  213. agentpool_storage/opencode_provider/provider.py +895 -0
  214. agentpool_storage/session_store.py +20 -6
  215. agentpool_storage/sql_provider/sql_provider.py +135 -2
  216. agentpool_storage/sql_provider/utils.py +2 -12
  217. agentpool_storage/zed_provider/__init__.py +16 -0
  218. agentpool_storage/zed_provider/helpers.py +281 -0
  219. agentpool_storage/zed_provider/models.py +130 -0
  220. agentpool_storage/zed_provider/provider.py +442 -0
  221. agentpool_storage/zed_provider.py +803 -0
  222. agentpool_toolsets/__init__.py +0 -2
  223. agentpool_toolsets/builtin/__init__.py +2 -4
  224. agentpool_toolsets/builtin/code.py +4 -4
  225. agentpool_toolsets/builtin/debug.py +115 -40
  226. agentpool_toolsets/builtin/execution_environment.py +54 -165
  227. agentpool_toolsets/builtin/skills.py +0 -77
  228. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  229. agentpool_toolsets/builtin/workers.py +4 -2
  230. agentpool_toolsets/composio_toolset.py +2 -2
  231. agentpool_toolsets/entry_points.py +3 -1
  232. agentpool_toolsets/fsspec_toolset/grep.py +25 -5
  233. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  234. agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
  235. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  236. agentpool_toolsets/mcp_discovery/toolset.py +74 -17
  237. agentpool_toolsets/mcp_run_toolset.py +8 -11
  238. agentpool_toolsets/notifications.py +33 -33
  239. agentpool_toolsets/openapi.py +3 -1
  240. agentpool_toolsets/search_toolset.py +3 -1
  241. agentpool_config/resources.py +0 -33
  242. agentpool_server/acp_server/acp_tools.py +0 -43
  243. agentpool_server/acp_server/commands/spawn.py +0 -210
  244. agentpool_storage/opencode_provider.py +0 -730
  245. agentpool_storage/text_log_provider.py +0 -276
  246. agentpool_toolsets/builtin/chain.py +0 -288
  247. agentpool_toolsets/builtin/user_interaction.py +0 -52
  248. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  249. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  250. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,31 +7,14 @@ between agents and ACP clients through the JSON-RPC protocol.
7
7
  from __future__ import annotations
8
8
 
9
9
  import asyncio
10
- from collections.abc import AsyncGenerator
11
10
  from dataclasses import dataclass, field
12
11
  import re
13
- from typing import TYPE_CHECKING, Any
12
+ from typing import TYPE_CHECKING, Any, Literal
14
13
 
14
+ import anyio
15
15
  from exxec.acp_provider import ACPExecutionEnvironment
16
16
  import logfire
17
- from pydantic_ai import (
18
- BuiltinToolCallPart,
19
- BuiltinToolReturnPart,
20
- FinalResultEvent,
21
- FunctionToolCallEvent,
22
- FunctionToolResultEvent,
23
- PartDeltaEvent,
24
- PartStartEvent,
25
- RetryPromptPart,
26
- TextPart,
27
- TextPartDelta,
28
- ThinkingPart,
29
- ThinkingPartDelta,
30
- ToolCallPartDelta,
31
- ToolReturnPart,
32
- UsageLimitExceeded,
33
- UserPromptPart,
34
- )
17
+ from pydantic_ai import UsageLimitExceeded, UserPromptPart
35
18
  from slashed import Command, CommandStore
36
19
  from tokonomics.model_discovery.model_info import ModelInfo
37
20
 
@@ -39,45 +22,26 @@ from acp import RequestPermissionRequest
39
22
  from acp.acp_requests import ACPRequests
40
23
  from acp.filesystem import ACPFileSystem
41
24
  from acp.notifications import ACPNotifications
42
- from acp.schema import (
43
- AvailableCommand,
44
- ClientCapabilities,
45
- ContentToolCallContent,
46
- PlanEntry,
47
- TerminalToolCallContent,
48
- ToolCallLocation,
49
- )
50
- from acp.tool_call_state import ToolCallState
51
- from acp.utils import generate_tool_title, infer_tool_kind, to_acp_content_blocks
25
+ from acp.schema import AvailableCommand, ClientCapabilities, SessionNotification
52
26
  from agentpool import Agent, AgentContext # noqa: TC001
53
27
  from agentpool.agents import SlashedAgent
54
28
  from agentpool.agents.acp_agent import ACPAgent
55
- from agentpool.agents.events import (
56
- CompactionEvent,
57
- PlanUpdateEvent,
58
- StreamCompleteEvent,
59
- ToolCallProgressEvent,
60
- ToolCallStartEvent,
61
- )
62
29
  from agentpool.agents.modes import ModeInfo
63
30
  from agentpool.log import get_logger
64
- from agentpool.utils.pydantic_ai_helpers import safe_args_as_dict
65
31
  from agentpool_commands import get_commands
66
32
  from agentpool_commands.base import NodeCommand
67
33
  from agentpool_server.acp_server.converters import (
68
34
  convert_acp_mcp_server_to_config,
69
35
  from_acp_content,
70
36
  )
37
+ from agentpool_server.acp_server.event_converter import ACPEventConverter
71
38
  from agentpool_server.acp_server.input_provider import ACPInputProvider
72
39
 
73
40
 
74
41
  if TYPE_CHECKING:
75
42
  from collections.abc import Callable, Sequence
76
43
 
77
- from pydantic_ai import (
78
- SystemPromptPart,
79
- UserContent,
80
- )
44
+ from pydantic_ai import SystemPromptPart, UserContent
81
45
  from slashed import CommandContext
82
46
 
83
47
  from acp import Client, RequestPermissionResponse
@@ -92,7 +56,6 @@ if TYPE_CHECKING:
92
56
  from agentpool import AgentPool
93
57
  from agentpool.agents import AGUIAgent
94
58
  from agentpool.agents.claude_code_agent import ClaudeCodeAgent
95
- from agentpool.agents.events import RichAgentStreamEvent
96
59
  from agentpool.prompts.manager import PromptManager
97
60
  from agentpool.prompts.prompts import MCPClientPrompt
98
61
  from agentpool_server.acp_server.acp_agent import AgentPoolACPAgent
@@ -245,6 +208,12 @@ class ACPSession:
245
208
  manager: ACPSessionManager | None = None
246
209
  """Session manager for managing sessions. Used for session management commands."""
247
210
 
211
+ subagent_display_mode: Literal["inline", "tool_box"] = "tool_box"
212
+ """How to display subagent output:
213
+ - 'inline': Subagent output flows into main message stream
214
+ - 'tool_box': Subagent output contained in the tool call's progress box (default)
215
+ """
216
+
248
217
  def __post_init__(self) -> None:
249
218
  """Initialize session state and set up providers."""
250
219
  from agentpool_server.acp_server.commands import get_commands as get_acp_commands
@@ -253,9 +222,7 @@ class ACPSession:
253
222
  self.log = logger.bind(session_id=self.session_id)
254
223
  self._task_lock = asyncio.Lock()
255
224
  self._cancelled = False
256
- self._title_generation_triggered = False
257
- self._current_tool_inputs: dict[str, dict[str, Any]] = {}
258
- self._tool_call_states: dict[str, ToolCallState] = {}
225
+ self._current_converter: ACPEventConverter | None = None
259
226
  self.fs = ACPFileSystem(self.client, session_id=self.session_id)
260
227
  cmds = [
261
228
  *get_commands(
@@ -270,7 +237,6 @@ class ACPSession:
270
237
  self.command_store._initialize_sync()
271
238
  self._update_callbacks: list[Callable[[], None]] = []
272
239
  self._remote_commands: list[AvailableCommand] = [] # Commands from nested ACP agents
273
-
274
240
  self.staged_content = StagedContent()
275
241
  # Inject Zed-specific instructions if client is Zed
276
242
  if self.client_info and self.client_info.name and "zed" in self.client_info.name.lower():
@@ -354,71 +320,44 @@ class ACPSession:
354
320
  self.log.debug("Forwarding config option update to client")
355
321
 
356
322
  notification = SessionNotification(session_id=self.session_id, update=update)
357
- await self.client.session_update(notification)
323
+ await self.client.session_update(notification) # pyright: ignore[reportArgumentType]
358
324
 
359
325
  async def initialize(self) -> None:
360
326
  """Initialize async resources. Must be called after construction."""
361
327
  await self.acp_env.__aenter__()
362
328
 
363
- def _get_or_create_tool_state(
364
- self,
365
- tool_call_id: str,
366
- tool_name: str,
367
- tool_input: dict[str, Any],
368
- ) -> ToolCallState:
369
- """Get existing tool call state or create a new one.
370
-
371
- Args:
372
- tool_call_id: Unique identifier for the tool call
373
- tool_name: Name of the tool being called
374
- tool_input: Input parameters for the tool
375
-
376
- Returns:
377
- ToolCallState instance (existing or newly created)
378
- """
379
- if tool_call_id not in self._tool_call_states:
380
- state = ToolCallState(
381
- notifications=self.notifications,
382
- tool_call_id=tool_call_id,
383
- tool_name=tool_name,
384
- title=generate_tool_title(tool_name, tool_input),
385
- kind=infer_tool_kind(tool_name),
386
- raw_input=tool_input,
387
- )
388
- self._tool_call_states[tool_call_id] = state
389
- return self._tool_call_states[tool_call_id]
390
-
391
- def _cleanup_tool_state(self, tool_call_id: str) -> None:
392
- """Remove tool call state after completion."""
393
- self._tool_call_states.pop(tool_call_id, None)
394
- self._current_tool_inputs.pop(tool_call_id, None)
395
-
396
329
  async def initialize_mcp_servers(self) -> None:
397
330
  """Initialize MCP servers if any are configured."""
398
331
  if not self.mcp_servers:
399
332
  return
400
333
  self.log.info("Initializing MCP servers", server_count=len(self.mcp_servers))
401
334
  cfgs = [convert_acp_mcp_server_to_config(s) for s in self.mcp_servers]
402
- # Define accessible roots for MCP servers
403
- # root = Path(self.cwd).resolve().as_uri() if self.cwd else None
404
- for _cfg in cfgs:
335
+ # Add each MCP server to the current agent's MCP manager dynamically
336
+ for cfg in cfgs:
405
337
  try:
406
- # Server will be initialized when MCP manager enters context
407
- self.log.info("Added MCP servers", server_count=len(cfgs))
408
- await self._register_mcp_prompts_as_commands()
338
+ await self.agent.mcp.setup_server(cfg)
339
+ self.log.info(
340
+ "Added MCP server to agent", server_name=cfg.name, agent=self.current_agent_name
341
+ )
409
342
  except Exception:
410
- self.log.exception("Failed to initialize MCP manager")
343
+ self.log.exception("Failed to setup MCP server", server_name=cfg.name)
411
344
  # Don't fail session creation, just log the error
345
+ # Register MCP prompts as commands after all servers are added
346
+ try:
347
+ await self._register_mcp_prompts_as_commands()
348
+ except Exception:
349
+ self.log.exception("Failed to register MCP prompts as commands")
412
350
 
413
351
  async def init_project_context(self) -> None:
414
- """Load AGENTS.md file and inject project context into all agents.
352
+ """Load AGENTS.md/CLAUDE.md file and stage as initial context.
415
353
 
416
- TODO: Consider moving this to __aenter__
354
+ The project context is staged as user message content rather than system prompts,
355
+ which ensures it's available for all agent types and avoids timing issues with
356
+ agent initialization.
417
357
  """
418
358
  if info := await self.requests.read_agent_rules(self.cwd):
419
- for agent in self.agent_pool.agents.values():
420
- prompt = f"## Project Information\n\n{info}"
421
- agent.sys_prompts.prompts.append(prompt)
359
+ # Stage as user message to be prepended to first prompt
360
+ self.staged_content.add_text(f"## Project Information\n\n{info}")
422
361
 
423
362
  async def init_client_skills(self) -> None:
424
363
  """Discover and load skills from client-side .claude/skills directory.
@@ -488,14 +427,14 @@ class ACPSession:
488
427
  This actively interrupts the running agent by calling its interrupt() method,
489
428
  which handles protocol-specific cancellation (e.g., sending CancelNotification
490
429
  for ACP agents, calling SDK interrupt for ClaudeCodeAgent, etc.).
430
+
431
+ Note:
432
+ Tool call cleanup is handled in process_prompt() to avoid race conditions
433
+ with the converter state being modified from multiple async contexts.
491
434
  """
492
435
  self._cancelled = True
493
436
  self.log.info("Session cancelled, interrupting agent")
494
437
 
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
-
499
438
  # Actively interrupt the agent's stream
500
439
  try:
501
440
  await self.agent.interrupt()
@@ -541,25 +480,61 @@ class ACPSession:
541
480
  has_staged=staged is not None,
542
481
  )
543
482
  event_count = 0
544
- self._current_tool_inputs.clear() # Reset tool inputs for new stream
483
+ # Create a new event converter for this prompt
484
+ converter = ACPEventConverter(subagent_display_mode=self.subagent_display_mode)
485
+ self._current_converter = converter # Track for cancellation
545
486
 
546
487
  try: # Use the session's persistent input provider
547
488
  async for event in self.agent.run_stream(
548
- *all_content, input_provider=self.input_provider, deps=self
489
+ *all_content,
490
+ input_provider=self.input_provider,
491
+ deps=self,
492
+ conversation_id=self.session_id, # Tie agent conversation to ACP session
549
493
  ):
550
494
  if self._cancelled:
551
- self.log.info("Cancelled during event loop")
495
+ self.log.info("Cancelled during event loop, cleaning up tool calls")
496
+ # Send cancellation notifications for any pending tool calls
497
+ # This happens in the same async context as the converter
498
+ async for cancel_update in converter.cancel_pending_tools():
499
+ notification = SessionNotification(
500
+ session_id=self.session_id, update=cancel_update
501
+ )
502
+ await self.client.session_update(notification) # pyright: ignore[reportArgumentType]
503
+ # CRITICAL: Allow time for client to process tool completion notifications
504
+ # before sending PromptResponse. Without this delay, the client may receive
505
+ # and process the PromptResponse before the tool notifications, causing UI
506
+ # state desync where subsequent prompts appear stuck/unresponsive.
507
+ # This is needed because even though send() awaits the write, the client
508
+ # may process messages asynchronously or out of order.
509
+ await anyio.sleep(0.05)
510
+ self._current_converter = None
552
511
  return "cancelled"
553
512
 
554
513
  event_count += 1
555
- await self.handle_event(event)
514
+ async for update in converter.convert(event):
515
+ notification = SessionNotification(
516
+ session_id=self.session_id, update=update
517
+ )
518
+ await self.client.session_update(notification) # pyright: ignore[reportArgumentType]
519
+ # Yield control to allow notifications to be sent immediately
520
+ await anyio.sleep(0.01)
556
521
  self.log.info("Streaming finished", events_processed=event_count)
557
522
 
558
523
  except asyncio.CancelledError:
559
524
  # Task was cancelled (e.g., via interrupt()) - return proper stop reason
560
525
  # This is critical: CancelledError doesn't inherit from Exception,
561
526
  # so we must catch it explicitly to send the PromptResponse
562
- self.log.info("Stream cancelled via CancelledError")
527
+ self.log.info("Stream cancelled via CancelledError, cleaning up tool calls")
528
+ # Send cancellation notifications for any pending tool calls
529
+ async for cancel_update in converter.cancel_pending_tools():
530
+ notification = SessionNotification(
531
+ session_id=self.session_id, update=cancel_update
532
+ )
533
+ await self.client.session_update(notification) # pyright: ignore[reportArgumentType]
534
+ # CRITICAL: Allow time for client to process tool completion notifications
535
+ # before sending PromptResponse. See comment in cancellation branch above.
536
+ await anyio.sleep(0.05)
537
+ self._current_converter = None
563
538
  return "cancelled"
564
539
  except UsageLimitExceeded as e:
565
540
  self.log.info("Usage limit exceeded", error=str(e))
@@ -573,6 +548,7 @@ class ACPSession:
573
548
  return "refusal"
574
549
  return "max_tokens" # Default to max_tokens for other usage limits
575
550
  except Exception as e:
551
+ self._current_converter = None # Clear converter reference
576
552
  self.log.exception("Error during streaming")
577
553
  # Send error notification asynchronously to avoid blocking response
578
554
  self.acp_agent.tasks.create_task(
@@ -581,37 +557,10 @@ class ACPSession:
581
557
  )
582
558
  return "end_turn"
583
559
  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
- )
560
+ # Title generation is now handled automatically by log_conversation
561
+ self._current_converter = None # Clear converter reference
591
562
  return "end_turn"
592
563
 
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
-
615
564
  async def _send_error_notification(self, message: str) -> None:
616
565
  """Send error notification, with exception handling."""
617
566
  if self._cancelled:
@@ -621,267 +570,8 @@ class ACPSession:
621
570
  except Exception:
622
571
  self.log.exception("Failed to send error notification")
623
572
 
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
-
629
- match event:
630
- case (
631
- PartStartEvent(part=TextPart(content=delta))
632
- | PartDeltaEvent(delta=TextPartDelta(content_delta=delta))
633
- ):
634
- await self.notifications.send_agent_text(delta)
635
-
636
- case (
637
- PartStartEvent(part=ThinkingPart(content=delta))
638
- | PartDeltaEvent(delta=ThinkingPartDelta(content_delta=delta))
639
- ):
640
- await self.notifications.send_agent_thought(delta or "\n")
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
-
672
- case PartStartEvent(part=part):
673
- self.log.debug("Received unhandled PartStartEvent", part=part)
674
-
675
- # Tool call streaming delta - create/update state and start notification
676
- case PartDeltaEvent(delta=ToolCallPartDelta() as delta):
677
- if delta_part := delta.as_part():
678
- tool_call_id = delta_part.tool_call_id
679
- try:
680
- tool_input = delta_part.args_as_dict()
681
- except ValueError:
682
- # Args still streaming, not valid JSON yet - skip this delta
683
- pass
684
- else:
685
- self._current_tool_inputs[tool_call_id] = tool_input
686
- # Create state and send initial notification
687
- state = self._get_or_create_tool_state(
688
- tool_call_id=tool_call_id,
689
- tool_name=delta_part.tool_name,
690
- tool_input=tool_input,
691
- )
692
- await state.start()
693
-
694
- # Tool call started - create/update state and start notification
695
- case FunctionToolCallEvent(part=part):
696
- tool_call_id = part.tool_call_id
697
- tool_input = safe_args_as_dict(part, default={})
698
- self._current_tool_inputs[tool_call_id] = tool_input
699
- # Create state and send initial notification
700
- state = self._get_or_create_tool_state(
701
- tool_call_id=tool_call_id,
702
- tool_name=part.tool_name,
703
- tool_input=tool_input,
704
- )
705
- await state.start()
706
-
707
- # Tool completed successfully - update state and finalize
708
- case FunctionToolResultEvent(
709
- result=ToolReturnPart(content=content, tool_name=tool_name) as result,
710
- tool_call_id=tool_call_id,
711
- ):
712
- if isinstance(content, AsyncGenerator):
713
- full_content = ""
714
- async for chunk in content:
715
- full_content += str(chunk)
716
- # Stream progress through state
717
- if tool_state := self._tool_call_states.get(tool_call_id):
718
- await tool_state.update(status="in_progress", raw_output=chunk)
719
-
720
- # Replace the AsyncGenerator with the full content to prevent errors
721
- result.content = full_content
722
- final_output = full_content
723
- else:
724
- final_output = result.content
725
-
726
- # Complete tool call through state (preserves accumulated content/locations)
727
- if complete_state := self._tool_call_states.get(tool_call_id):
728
- # Only add return value as content if no content was emitted during execution
729
- if complete_state.has_content:
730
- # Content already provided via progress events - just set raw_output
731
- await complete_state.complete(raw_output=final_output)
732
- else:
733
- # No content yet - convert return value for display
734
- converted_blocks = to_acp_content_blocks(final_output)
735
- content_items = [
736
- ContentToolCallContent(content=block) for block in converted_blocks
737
- ]
738
- await complete_state.complete(
739
- raw_output=final_output,
740
- content=content_items,
741
- )
742
- self._cleanup_tool_state(tool_call_id)
743
-
744
- # Tool failed with retry - update state with error
745
- case FunctionToolResultEvent(
746
- result=RetryPromptPart(tool_name=tool_name) as result,
747
- tool_call_id=tool_call_id,
748
- ):
749
- error_message = result.model_response()
750
- if fail_state := self._tool_call_states.get(tool_call_id):
751
- await fail_state.fail(error=error_message)
752
- self._cleanup_tool_state(tool_call_id)
753
-
754
- # Tool emits its own start event - update state with better title/content
755
- case ToolCallStartEvent(
756
- tool_call_id=tool_call_id,
757
- tool_name=tool_name,
758
- title=title,
759
- kind=kind,
760
- locations=loc_items,
761
- raw_input=raw_input,
762
- ):
763
- self.log.debug(
764
- "Tool call start event", tool_name=tool_name, tool_call_id=tool_call_id
765
- )
766
- # Get or create state (may already exist from FunctionToolCallEvent)
767
- state = self._get_or_create_tool_state(
768
- tool_call_id=tool_call_id,
769
- tool_name=tool_name,
770
- tool_input=raw_input or {},
771
- )
772
- # Convert LocationContentItem objects to ACP format
773
- acp_locations = [
774
- ToolCallLocation(path=loc.path, line=loc.line) for loc in loc_items
775
- ]
776
- # Update state with tool-provided details (better title, content, locations)
777
- await state.update(title=title, kind=kind, locations=acp_locations or None)
778
-
779
- # Tool progress event - update state with title and content
780
- case ToolCallProgressEvent(
781
- tool_call_id=tool_call_id,
782
- title=title,
783
- status=status,
784
- items=items,
785
- ) if tool_call_id and tool_call_id in self._tool_call_states:
786
- progress_state = self._tool_call_states[tool_call_id]
787
- self.log.debug("Progress event", tool_call_id=tool_call_id, title=title)
788
-
789
- # Convert items to ACP content
790
- from agentpool.agents.events import (
791
- DiffContentItem,
792
- FileContentItem,
793
- LocationContentItem,
794
- TerminalContentItem,
795
- TextContentItem,
796
- )
797
- from agentpool_server.acp_server.syntax_detection import (
798
- format_zed_code_block,
799
- )
800
-
801
- acp_content: list[Any] = []
802
- location_paths: list[str] = []
803
-
804
- for item in items:
805
- match item:
806
- case TerminalContentItem(terminal_id=tid):
807
- acp_content.append(TerminalToolCallContent(terminal_id=tid))
808
- case TextContentItem(text=text):
809
- acp_content.append(ContentToolCallContent.text(text=text))
810
- case FileContentItem(
811
- content=file_content,
812
- path=file_path,
813
- start_line=start_line,
814
- end_line=end_line,
815
- ):
816
- # Format as Zed-compatible code block with clickable path
817
- formatted = format_zed_code_block(
818
- file_content, file_path, start_line, end_line
819
- )
820
- acp_content.append(ContentToolCallContent.text(text=formatted))
821
- # Also add path to locations for "follow along" feature
822
- location_paths.append(file_path)
823
- case DiffContentItem(path=diff_path, old_text=old, new_text=new):
824
- # Send diff via direct notification
825
- await self.notifications.file_edit_progress(
826
- tool_call_id=tool_call_id,
827
- path=diff_path,
828
- old_text=old or "",
829
- new_text=new,
830
- status=status,
831
- changed_lines=[],
832
- )
833
- # Mark content as sent so completion doesn't override
834
- progress_state._has_content = True
835
- case LocationContentItem(path=loc_path):
836
- location_paths.append(loc_path)
837
-
838
- await progress_state.update(
839
- title=title,
840
- status="in_progress",
841
- content=acp_content if acp_content else None,
842
- locations=location_paths if location_paths else None,
843
- )
844
-
845
- case FinalResultEvent():
846
- self.log.debug("Final result received")
847
-
848
- case StreamCompleteEvent():
849
- pass
850
-
851
- case PlanUpdateEvent(entries=entries, tool_call_id=tool_call_id):
852
- acp_entries = [
853
- PlanEntry(content=e.content, priority=e.priority, status=e.status)
854
- for e in entries
855
- ]
856
- await self.notifications.update_plan(acp_entries)
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
-
877
- case _:
878
- self.log.debug("Unhandled event", event_type=type(event).__name__)
879
-
880
573
  async def close(self) -> None:
881
574
  """Close the session and cleanup resources."""
882
- self._current_tool_inputs.clear()
883
- self._tool_call_states.clear()
884
-
885
575
  try:
886
576
  await self.acp_env.__aexit__(None, None, None)
887
577
  except Exception:
@@ -964,11 +654,10 @@ class ACPSession:
964
654
  Returns:
965
655
  List of ACP AvailableCommand objects compatible with current node
966
656
  """
967
- all_commands = self.command_store.list_commands()
968
657
  current_node = self.agent
969
658
  # Filter commands by node compatibility
970
659
  compatible_commands = []
971
- for cmd in all_commands:
660
+ for cmd in self.command_store.list_commands():
972
661
  cmd_cls = cmd if isinstance(cmd, type) else type(cmd)
973
662
  # Check if command supports current node type
974
663
  if issubclass(cmd_cls, NodeCommand) and not cmd_cls.supports_node(current_node): # type: ignore[union-attr]
@@ -1022,11 +711,7 @@ class ACPSession:
1022
711
  )
1023
712
 
1024
713
  def register_update_callback(self, callback: Callable[[], None]) -> None:
1025
- """Register callback for command updates.
1026
-
1027
- Args:
1028
- callback: Function to call when commands are updated
1029
- """
714
+ """Register callback for command updates."""
1030
715
  self._update_callbacks.append(callback)
1031
716
 
1032
717
  def create_mcp_command(self, prompt: MCPClientPrompt) -> Command:
@@ -1047,16 +732,13 @@ class ACPSession:
1047
732
  ) -> None:
1048
733
  """Execute the MCP prompt with parsed arguments."""
1049
734
  # Map parsed args to prompt parameters
1050
-
1051
- result = {}
1052
735
  # Map positional args to prompt parameter names
1053
- for i, arg_value in enumerate(args):
1054
- if i < len(prompt.arguments):
1055
- param_name = prompt.arguments[i]["name"]
1056
- result[param_name] = arg_value
1057
- result.update(kwargs)
1058
- try:
1059
- # Get prompt components
736
+ result = {
737
+ prompt.arguments[i]["name"]: arg_value
738
+ for i, arg_value in enumerate(args)
739
+ if i < len(prompt.arguments)
740
+ } | kwargs
741
+ try: # Get prompt components
1060
742
  components = await prompt.get_components(result or None)
1061
743
  self.staged_content.add(components)
1062
744
  # Send confirmation
@@ -1067,15 +749,13 @@ class ACPSession:
1067
749
  logger.exception("MCP prompt execution failed", prompt=prompt.name)
1068
750
  await ctx.print(f"❌ Prompt error: {e}")
1069
751
 
1070
- usage_hint = (
1071
- " ".join(f"<{arg['name']}>" for arg in prompt.arguments) if prompt.arguments else None
1072
- )
752
+ usage = " ".join(f"<{i['name']}>" for i in args) if (args := prompt.arguments) else None
1073
753
  return Command.from_raw(
1074
754
  execute_prompt,
1075
755
  name=prompt.name,
1076
756
  description=prompt.description or f"MCP prompt: {prompt.name}",
1077
757
  category="mcp",
1078
- usage=usage_hint,
758
+ usage=usage,
1079
759
  )
1080
760
 
1081
761
  def create_prompt_hub_command(