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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. acp/__init__.py +13 -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/bridge/README.md +15 -2
  7. acp/bridge/__init__.py +3 -2
  8. acp/bridge/__main__.py +60 -19
  9. acp/bridge/ws_server.py +173 -0
  10. acp/bridge/ws_server_cli.py +89 -0
  11. acp/client/connection.py +38 -29
  12. acp/client/implementations/default_client.py +3 -2
  13. acp/client/implementations/headless_client.py +2 -2
  14. acp/connection.py +2 -2
  15. acp/notifications.py +20 -50
  16. acp/schema/__init__.py +2 -0
  17. acp/schema/agent_responses.py +21 -0
  18. acp/schema/client_requests.py +3 -3
  19. acp/schema/session_state.py +63 -29
  20. acp/stdio.py +39 -9
  21. acp/task/supervisor.py +2 -2
  22. acp/transports.py +362 -2
  23. acp/utils.py +17 -4
  24. agentpool/__init__.py +6 -1
  25. agentpool/agents/__init__.py +2 -0
  26. agentpool/agents/acp_agent/acp_agent.py +407 -277
  27. agentpool/agents/acp_agent/acp_converters.py +196 -38
  28. agentpool/agents/acp_agent/client_handler.py +191 -26
  29. agentpool/agents/acp_agent/session_state.py +17 -6
  30. agentpool/agents/agent.py +607 -572
  31. agentpool/agents/agui_agent/__init__.py +0 -2
  32. agentpool/agents/agui_agent/agui_agent.py +176 -110
  33. agentpool/agents/agui_agent/agui_converters.py +0 -131
  34. agentpool/agents/agui_agent/helpers.py +3 -4
  35. agentpool/agents/base_agent.py +632 -17
  36. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  37. agentpool/agents/claude_code_agent/__init__.py +13 -1
  38. agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
  39. agentpool/agents/claude_code_agent/converters.py +74 -143
  40. agentpool/agents/claude_code_agent/history.py +474 -0
  41. agentpool/agents/claude_code_agent/models.py +77 -0
  42. agentpool/agents/claude_code_agent/static_info.py +100 -0
  43. agentpool/agents/claude_code_agent/usage.py +242 -0
  44. agentpool/agents/context.py +40 -0
  45. agentpool/agents/events/__init__.py +24 -0
  46. agentpool/agents/events/builtin_handlers.py +67 -1
  47. agentpool/agents/events/event_emitter.py +32 -2
  48. agentpool/agents/events/events.py +104 -3
  49. agentpool/agents/events/infer_info.py +145 -0
  50. agentpool/agents/events/processors.py +254 -0
  51. agentpool/agents/interactions.py +41 -6
  52. agentpool/agents/modes.py +67 -0
  53. agentpool/agents/slashed_agent.py +5 -4
  54. agentpool/agents/tool_call_accumulator.py +213 -0
  55. agentpool/agents/tool_wrapping.py +18 -6
  56. agentpool/common_types.py +56 -21
  57. agentpool/config_resources/__init__.py +38 -1
  58. agentpool/config_resources/acp_assistant.yml +2 -2
  59. agentpool/config_resources/agents.yml +3 -0
  60. agentpool/config_resources/agents_template.yml +1 -0
  61. agentpool/config_resources/claude_code_agent.yml +10 -6
  62. agentpool/config_resources/external_acp_agents.yml +2 -1
  63. agentpool/delegation/base_team.py +4 -30
  64. agentpool/delegation/pool.py +136 -289
  65. agentpool/delegation/team.py +58 -57
  66. agentpool/delegation/teamrun.py +51 -55
  67. agentpool/diagnostics/__init__.py +53 -0
  68. agentpool/diagnostics/lsp_manager.py +1593 -0
  69. agentpool/diagnostics/lsp_proxy.py +41 -0
  70. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  71. agentpool/diagnostics/models.py +398 -0
  72. agentpool/functional/run.py +10 -4
  73. agentpool/mcp_server/__init__.py +0 -2
  74. agentpool/mcp_server/client.py +76 -32
  75. agentpool/mcp_server/conversions.py +54 -13
  76. agentpool/mcp_server/manager.py +34 -54
  77. agentpool/mcp_server/registries/official_registry_client.py +35 -1
  78. agentpool/mcp_server/tool_bridge.py +186 -139
  79. agentpool/messaging/__init__.py +0 -2
  80. agentpool/messaging/compaction.py +72 -197
  81. agentpool/messaging/connection_manager.py +11 -10
  82. agentpool/messaging/event_manager.py +5 -5
  83. agentpool/messaging/message_container.py +6 -30
  84. agentpool/messaging/message_history.py +99 -8
  85. agentpool/messaging/messagenode.py +52 -14
  86. agentpool/messaging/messages.py +54 -35
  87. agentpool/messaging/processing.py +12 -22
  88. agentpool/models/__init__.py +1 -1
  89. agentpool/models/acp_agents/base.py +6 -24
  90. agentpool/models/acp_agents/mcp_capable.py +126 -157
  91. agentpool/models/acp_agents/non_mcp.py +129 -95
  92. agentpool/models/agents.py +98 -76
  93. agentpool/models/agui_agents.py +1 -1
  94. agentpool/models/claude_code_agents.py +144 -19
  95. agentpool/models/file_parsing.py +0 -1
  96. agentpool/models/manifest.py +113 -50
  97. agentpool/prompts/conversion_manager.py +1 -1
  98. agentpool/prompts/prompts.py +5 -2
  99. agentpool/repomap.py +1 -1
  100. agentpool/resource_providers/__init__.py +11 -1
  101. agentpool/resource_providers/aggregating.py +56 -5
  102. agentpool/resource_providers/base.py +70 -4
  103. agentpool/resource_providers/codemode/code_executor.py +72 -5
  104. agentpool/resource_providers/codemode/helpers.py +2 -2
  105. agentpool/resource_providers/codemode/provider.py +64 -12
  106. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  107. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  108. agentpool/resource_providers/filtering.py +3 -1
  109. agentpool/resource_providers/mcp_provider.py +89 -12
  110. agentpool/resource_providers/plan_provider.py +228 -46
  111. agentpool/resource_providers/pool.py +7 -3
  112. agentpool/resource_providers/resource_info.py +111 -0
  113. agentpool/resource_providers/static.py +4 -2
  114. agentpool/sessions/__init__.py +4 -1
  115. agentpool/sessions/manager.py +33 -5
  116. agentpool/sessions/models.py +59 -6
  117. agentpool/sessions/protocol.py +28 -0
  118. agentpool/sessions/session.py +11 -55
  119. agentpool/skills/registry.py +13 -8
  120. agentpool/storage/manager.py +572 -49
  121. agentpool/talk/registry.py +4 -4
  122. agentpool/talk/talk.py +9 -10
  123. agentpool/testing.py +538 -20
  124. agentpool/tool_impls/__init__.py +6 -0
  125. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  126. agentpool/tool_impls/agent_cli/tool.py +95 -0
  127. agentpool/tool_impls/bash/__init__.py +64 -0
  128. agentpool/tool_impls/bash/helpers.py +35 -0
  129. agentpool/tool_impls/bash/tool.py +171 -0
  130. agentpool/tool_impls/delete_path/__init__.py +70 -0
  131. agentpool/tool_impls/delete_path/tool.py +142 -0
  132. agentpool/tool_impls/download_file/__init__.py +80 -0
  133. agentpool/tool_impls/download_file/tool.py +183 -0
  134. agentpool/tool_impls/execute_code/__init__.py +55 -0
  135. agentpool/tool_impls/execute_code/tool.py +163 -0
  136. agentpool/tool_impls/grep/__init__.py +80 -0
  137. agentpool/tool_impls/grep/tool.py +200 -0
  138. agentpool/tool_impls/list_directory/__init__.py +73 -0
  139. agentpool/tool_impls/list_directory/tool.py +197 -0
  140. agentpool/tool_impls/question/__init__.py +42 -0
  141. agentpool/tool_impls/question/tool.py +127 -0
  142. agentpool/tool_impls/read/__init__.py +104 -0
  143. agentpool/tool_impls/read/tool.py +305 -0
  144. agentpool/tools/__init__.py +2 -1
  145. agentpool/tools/base.py +114 -34
  146. agentpool/tools/manager.py +57 -1
  147. agentpool/ui/base.py +2 -2
  148. agentpool/ui/mock_provider.py +2 -2
  149. agentpool/ui/stdlib_provider.py +2 -2
  150. agentpool/utils/file_watcher.py +269 -0
  151. agentpool/utils/identifiers.py +121 -0
  152. agentpool/utils/pydantic_ai_helpers.py +46 -0
  153. agentpool/utils/streams.py +616 -2
  154. agentpool/utils/subprocess_utils.py +155 -0
  155. agentpool/utils/token_breakdown.py +461 -0
  156. agentpool/vfs_registry.py +7 -2
  157. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
  158. agentpool-2.5.0.dist-info/RECORD +579 -0
  159. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  160. agentpool_cli/__main__.py +24 -0
  161. agentpool_cli/create.py +1 -1
  162. agentpool_cli/serve_acp.py +100 -21
  163. agentpool_cli/serve_agui.py +87 -0
  164. agentpool_cli/serve_opencode.py +119 -0
  165. agentpool_cli/ui.py +557 -0
  166. agentpool_commands/__init__.py +42 -5
  167. agentpool_commands/agents.py +75 -2
  168. agentpool_commands/history.py +62 -0
  169. agentpool_commands/mcp.py +176 -0
  170. agentpool_commands/models.py +56 -3
  171. agentpool_commands/pool.py +260 -0
  172. agentpool_commands/session.py +1 -1
  173. agentpool_commands/text_sharing/__init__.py +119 -0
  174. agentpool_commands/text_sharing/base.py +123 -0
  175. agentpool_commands/text_sharing/github_gist.py +80 -0
  176. agentpool_commands/text_sharing/opencode.py +462 -0
  177. agentpool_commands/text_sharing/paste_rs.py +59 -0
  178. agentpool_commands/text_sharing/pastebin.py +116 -0
  179. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  180. agentpool_commands/tools.py +57 -0
  181. agentpool_commands/utils.py +80 -30
  182. agentpool_config/__init__.py +30 -2
  183. agentpool_config/agentpool_tools.py +498 -0
  184. agentpool_config/builtin_tools.py +77 -22
  185. agentpool_config/commands.py +24 -1
  186. agentpool_config/compaction.py +258 -0
  187. agentpool_config/converters.py +1 -1
  188. agentpool_config/event_handlers.py +42 -0
  189. agentpool_config/events.py +1 -1
  190. agentpool_config/forward_targets.py +1 -4
  191. agentpool_config/jinja.py +3 -3
  192. agentpool_config/mcp_server.py +132 -6
  193. agentpool_config/nodes.py +1 -1
  194. agentpool_config/observability.py +44 -0
  195. agentpool_config/session.py +0 -3
  196. agentpool_config/storage.py +82 -38
  197. agentpool_config/task.py +3 -3
  198. agentpool_config/tools.py +11 -22
  199. agentpool_config/toolsets.py +109 -233
  200. agentpool_server/a2a_server/agent_worker.py +307 -0
  201. agentpool_server/a2a_server/server.py +23 -18
  202. agentpool_server/acp_server/acp_agent.py +234 -181
  203. agentpool_server/acp_server/commands/acp_commands.py +151 -156
  204. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
  205. agentpool_server/acp_server/event_converter.py +651 -0
  206. agentpool_server/acp_server/input_provider.py +53 -10
  207. agentpool_server/acp_server/server.py +24 -90
  208. agentpool_server/acp_server/session.py +173 -331
  209. agentpool_server/acp_server/session_manager.py +8 -34
  210. agentpool_server/agui_server/server.py +3 -1
  211. agentpool_server/mcp_server/server.py +5 -2
  212. agentpool_server/opencode_server/.rules +95 -0
  213. agentpool_server/opencode_server/ENDPOINTS.md +401 -0
  214. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  215. agentpool_server/opencode_server/__init__.py +19 -0
  216. agentpool_server/opencode_server/command_validation.py +172 -0
  217. agentpool_server/opencode_server/converters.py +975 -0
  218. agentpool_server/opencode_server/dependencies.py +24 -0
  219. agentpool_server/opencode_server/input_provider.py +421 -0
  220. agentpool_server/opencode_server/models/__init__.py +250 -0
  221. agentpool_server/opencode_server/models/agent.py +53 -0
  222. agentpool_server/opencode_server/models/app.py +72 -0
  223. agentpool_server/opencode_server/models/base.py +26 -0
  224. agentpool_server/opencode_server/models/common.py +23 -0
  225. agentpool_server/opencode_server/models/config.py +37 -0
  226. agentpool_server/opencode_server/models/events.py +821 -0
  227. agentpool_server/opencode_server/models/file.py +88 -0
  228. agentpool_server/opencode_server/models/mcp.py +44 -0
  229. agentpool_server/opencode_server/models/message.py +179 -0
  230. agentpool_server/opencode_server/models/parts.py +323 -0
  231. agentpool_server/opencode_server/models/provider.py +81 -0
  232. agentpool_server/opencode_server/models/pty.py +43 -0
  233. agentpool_server/opencode_server/models/question.py +56 -0
  234. agentpool_server/opencode_server/models/session.py +111 -0
  235. agentpool_server/opencode_server/routes/__init__.py +29 -0
  236. agentpool_server/opencode_server/routes/agent_routes.py +473 -0
  237. agentpool_server/opencode_server/routes/app_routes.py +202 -0
  238. agentpool_server/opencode_server/routes/config_routes.py +302 -0
  239. agentpool_server/opencode_server/routes/file_routes.py +571 -0
  240. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  241. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  242. agentpool_server/opencode_server/routes/message_routes.py +761 -0
  243. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  244. agentpool_server/opencode_server/routes/pty_routes.py +300 -0
  245. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  246. agentpool_server/opencode_server/routes/session_routes.py +1276 -0
  247. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  248. agentpool_server/opencode_server/server.py +475 -0
  249. agentpool_server/opencode_server/state.py +151 -0
  250. agentpool_server/opencode_server/time_utils.py +8 -0
  251. agentpool_storage/__init__.py +12 -0
  252. agentpool_storage/base.py +184 -2
  253. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  254. agentpool_storage/claude_provider/__init__.py +42 -0
  255. agentpool_storage/claude_provider/provider.py +1089 -0
  256. agentpool_storage/file_provider.py +278 -15
  257. agentpool_storage/memory_provider.py +193 -12
  258. agentpool_storage/models.py +3 -0
  259. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  260. agentpool_storage/opencode_provider/__init__.py +16 -0
  261. agentpool_storage/opencode_provider/helpers.py +414 -0
  262. agentpool_storage/opencode_provider/provider.py +895 -0
  263. agentpool_storage/project_store.py +325 -0
  264. agentpool_storage/session_store.py +26 -6
  265. agentpool_storage/sql_provider/__init__.py +4 -2
  266. agentpool_storage/sql_provider/models.py +48 -0
  267. agentpool_storage/sql_provider/sql_provider.py +269 -3
  268. agentpool_storage/sql_provider/utils.py +12 -13
  269. agentpool_storage/zed_provider/__init__.py +16 -0
  270. agentpool_storage/zed_provider/helpers.py +281 -0
  271. agentpool_storage/zed_provider/models.py +130 -0
  272. agentpool_storage/zed_provider/provider.py +442 -0
  273. agentpool_storage/zed_provider.py +803 -0
  274. agentpool_toolsets/__init__.py +0 -2
  275. agentpool_toolsets/builtin/__init__.py +2 -12
  276. agentpool_toolsets/builtin/code.py +96 -57
  277. agentpool_toolsets/builtin/debug.py +118 -48
  278. agentpool_toolsets/builtin/execution_environment.py +115 -230
  279. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  280. agentpool_toolsets/builtin/skills.py +9 -4
  281. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  282. agentpool_toolsets/builtin/workers.py +4 -2
  283. agentpool_toolsets/composio_toolset.py +2 -2
  284. agentpool_toolsets/entry_points.py +3 -1
  285. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  286. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  287. agentpool_toolsets/fsspec_toolset/grep.py +99 -7
  288. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  289. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  290. agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
  291. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  292. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  293. agentpool_toolsets/mcp_discovery/toolset.py +511 -0
  294. agentpool_toolsets/mcp_run_toolset.py +87 -12
  295. agentpool_toolsets/notifications.py +33 -33
  296. agentpool_toolsets/openapi.py +3 -1
  297. agentpool_toolsets/search_toolset.py +3 -1
  298. agentpool-2.1.9.dist-info/RECORD +0 -474
  299. agentpool_config/resources.py +0 -33
  300. agentpool_server/acp_server/acp_tools.py +0 -43
  301. agentpool_server/acp_server/commands/spawn.py +0 -210
  302. agentpool_storage/text_log_provider.py +0 -275
  303. agentpool_toolsets/builtin/agent_management.py +0 -239
  304. agentpool_toolsets/builtin/chain.py +0 -288
  305. agentpool_toolsets/builtin/history.py +0 -36
  306. agentpool_toolsets/builtin/integration.py +0 -85
  307. agentpool_toolsets/builtin/tool_management.py +0 -90
  308. agentpool_toolsets/builtin/user_interaction.py +0 -52
  309. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  310. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  311. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,54 +7,26 @@ 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
- FinalResultEvent,
19
- FunctionToolCallEvent,
20
- FunctionToolResultEvent,
21
- PartDeltaEvent,
22
- PartStartEvent,
23
- RetryPromptPart,
24
- TextPart,
25
- TextPartDelta,
26
- ThinkingPart,
27
- ThinkingPartDelta,
28
- ToolCallPartDelta,
29
- ToolReturnPart,
30
- UsageLimitExceeded,
31
- UserPromptPart,
32
- )
17
+ from pydantic_ai import UsageLimitExceeded, UserPromptPart
33
18
  from slashed import Command, CommandStore
19
+ from tokonomics.model_discovery.model_info import ModelInfo
34
20
 
35
21
  from acp import RequestPermissionRequest
36
22
  from acp.acp_requests import ACPRequests
37
23
  from acp.filesystem import ACPFileSystem
38
24
  from acp.notifications import ACPNotifications
39
- from acp.schema import (
40
- AvailableCommand,
41
- ClientCapabilities,
42
- ContentToolCallContent,
43
- PlanEntry,
44
- TerminalToolCallContent,
45
- ToolCallLocation,
46
- )
47
- from acp.tool_call_state import ToolCallState
48
- from acp.utils import generate_tool_title, infer_tool_kind, to_acp_content_blocks
25
+ from acp.schema import AvailableCommand, ClientCapabilities, SessionNotification
49
26
  from agentpool import Agent, AgentContext # noqa: TC001
50
27
  from agentpool.agents import SlashedAgent
51
28
  from agentpool.agents.acp_agent import ACPAgent
52
- from agentpool.agents.events import (
53
- PlanUpdateEvent,
54
- StreamCompleteEvent,
55
- ToolCallProgressEvent,
56
- ToolCallStartEvent,
57
- )
29
+ from agentpool.agents.modes import ModeInfo
58
30
  from agentpool.log import get_logger
59
31
  from agentpool_commands import get_commands
60
32
  from agentpool_commands.base import NodeCommand
@@ -62,20 +34,20 @@ from agentpool_server.acp_server.converters import (
62
34
  convert_acp_mcp_server_to_config,
63
35
  from_acp_content,
64
36
  )
37
+ from agentpool_server.acp_server.event_converter import ACPEventConverter
65
38
  from agentpool_server.acp_server.input_provider import ACPInputProvider
66
39
 
67
40
 
68
41
  if TYPE_CHECKING:
69
42
  from collections.abc import Callable, Sequence
70
43
 
71
- from pydantic_ai import (
72
- SystemPromptPart,
73
- UserContent,
74
- )
44
+ from pydantic_ai import SystemPromptPart, UserContent
75
45
  from slashed import CommandContext
76
46
 
77
47
  from acp import Client, RequestPermissionResponse
78
48
  from acp.schema import (
49
+ AvailableCommandsUpdate,
50
+ ConfigOptionUpdate,
79
51
  ContentBlock,
80
52
  Implementation,
81
53
  McpServer,
@@ -84,7 +56,6 @@ if TYPE_CHECKING:
84
56
  from agentpool import AgentPool
85
57
  from agentpool.agents import AGUIAgent
86
58
  from agentpool.agents.claude_code_agent import ClaudeCodeAgent
87
- from agentpool.agents.events import RichAgentStreamEvent
88
59
  from agentpool.prompts.manager import PromptManager
89
60
  from agentpool.prompts.prompts import MCPClientPrompt
90
61
  from agentpool_server.acp_server.acp_agent import AgentPoolACPAgent
@@ -129,13 +100,27 @@ def _is_slash_command(text: str) -> bool:
129
100
 
130
101
  def split_commands(
131
102
  contents: Sequence[UserContent],
103
+ command_store: CommandStore,
132
104
  ) -> tuple[list[str], list[UserContent]]:
105
+ """Split content into local slash commands and pass-through content.
106
+
107
+ Only commands that exist in the local command_store are extracted.
108
+ Remote commands (from nested ACP agents) stay in non_command_content
109
+ so they flow through to the agent and reach the nested server.
110
+ """
133
111
  commands: list[str] = []
134
112
  non_command_content: list[UserContent] = []
135
113
  for item in contents:
136
- if isinstance(item, str) and _is_slash_command(item):
114
+ # Check if this is a LOCAL command we handle
115
+ if (
116
+ isinstance(item, str)
117
+ and _is_slash_command(item)
118
+ and (match := SLASH_PATTERN.match(item.strip()))
119
+ and command_store.get_command(match.group(1))
120
+ ):
137
121
  commands.append(item.strip())
138
122
  else:
123
+ # Not a local command - pass through (may be remote command or regular text)
139
124
  non_command_content.append(item)
140
125
  return commands, non_command_content
141
126
 
@@ -223,6 +208,12 @@ class ACPSession:
223
208
  manager: ACPSessionManager | None = None
224
209
  """Session manager for managing sessions. Used for session management commands."""
225
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
+
226
217
  def __post_init__(self) -> None:
227
218
  """Initialize session state and set up providers."""
228
219
  from agentpool_server.acp_server.commands import get_commands as get_acp_commands
@@ -231,8 +222,7 @@ class ACPSession:
231
222
  self.log = logger.bind(session_id=self.session_id)
232
223
  self._task_lock = asyncio.Lock()
233
224
  self._cancelled = False
234
- self._current_tool_inputs: dict[str, dict[str, Any]] = {}
235
- self._tool_call_states: dict[str, ToolCallState] = {}
225
+ self._current_converter: ACPEventConverter | None = None
236
226
  self.fs = ACPFileSystem(self.client, session_id=self.session_id)
237
227
  cmds = [
238
228
  *get_commands(
@@ -243,10 +233,10 @@ class ACPSession:
243
233
  ),
244
234
  *get_acp_commands(),
245
235
  ]
246
- self.command_store = CommandStore(enable_system_commands=True, commands=cmds)
236
+ self.command_store = CommandStore(commands=cmds)
247
237
  self.command_store._initialize_sync()
248
238
  self._update_callbacks: list[Callable[[], None]] = []
249
-
239
+ self._remote_commands: list[AvailableCommand] = [] # Commands from nested ACP agents
250
240
  self.staged_content = StagedContent()
251
241
  # Inject Zed-specific instructions if client is Zed
252
242
  if self.client_info and self.client_info.name and "zed" in self.client_info.name.lower():
@@ -294,44 +284,47 @@ class ACPSession:
294
284
  return response
295
285
 
296
286
  agent.acp_permission_callback = permission_callback
297
- self.log.info("Created ACP session", current_agent=self.current_agent_name)
298
287
 
299
- async def initialize(self) -> None:
300
- """Initialize async resources. Must be called after construction."""
301
- await self.acp_env.__aenter__()
288
+ # Subscribe to state change signal for all agents
289
+ agent.state_updated.connect(self._on_state_updated)
290
+ self.log.info("Created ACP session", current_agent=self.current_agent_name)
302
291
 
303
- def _get_or_create_tool_state(
304
- self,
305
- tool_call_id: str,
306
- tool_name: str,
307
- tool_input: dict[str, Any],
308
- ) -> ToolCallState:
309
- """Get existing tool call state or create a new one.
292
+ async def _on_state_updated(
293
+ self, state: ModeInfo | ModelInfo | AvailableCommandsUpdate | ConfigOptionUpdate
294
+ ) -> None:
295
+ """Handle state update signal from agent - forward to ACP client."""
296
+ from acp.schema import (
297
+ AvailableCommandsUpdate as ACPAvailableCommandsUpdate,
298
+ ConfigOptionUpdate as ACPConfigOptionUpdate,
299
+ CurrentModelUpdate,
300
+ CurrentModeUpdate,
301
+ SessionNotification,
302
+ )
310
303
 
311
- Args:
312
- tool_call_id: Unique identifier for the tool call
313
- tool_name: Name of the tool being called
314
- tool_input: Input parameters for the tool
304
+ update: CurrentModeUpdate | CurrentModelUpdate | ACPConfigOptionUpdate
305
+ match state:
306
+ case ModeInfo(id=mode_id):
307
+ update = CurrentModeUpdate(current_mode_id=mode_id)
308
+ self.log.debug("Forwarding mode change to client", mode_id=mode_id)
309
+ case ModelInfo(id=model_id):
310
+ update = CurrentModelUpdate(current_model_id=model_id)
311
+ self.log.debug("Forwarding model change to client", model_id=model_id)
312
+ case ACPAvailableCommandsUpdate(available_commands=cmds):
313
+ # Store remote commands and send merged list
314
+ self._remote_commands = list(cmds)
315
+ await self.send_available_commands_update()
316
+ self.log.debug("Merged and sent commands update to client")
317
+ return
318
+ case ACPConfigOptionUpdate():
319
+ update = state
320
+ self.log.debug("Forwarding config option update to client")
315
321
 
316
- Returns:
317
- ToolCallState instance (existing or newly created)
318
- """
319
- if tool_call_id not in self._tool_call_states:
320
- state = ToolCallState(
321
- notifications=self.notifications,
322
- tool_call_id=tool_call_id,
323
- tool_name=tool_name,
324
- title=generate_tool_title(tool_name, tool_input),
325
- kind=infer_tool_kind(tool_name),
326
- raw_input=tool_input,
327
- )
328
- self._tool_call_states[tool_call_id] = state
329
- return self._tool_call_states[tool_call_id]
322
+ notification = SessionNotification(session_id=self.session_id, update=update)
323
+ await self.client.session_update(notification) # pyright: ignore[reportArgumentType]
330
324
 
331
- def _cleanup_tool_state(self, tool_call_id: str) -> None:
332
- """Remove tool call state after completion."""
333
- self._tool_call_states.pop(tool_call_id, None)
334
- self._current_tool_inputs.pop(tool_call_id, None)
325
+ async def initialize(self) -> None:
326
+ """Initialize async resources. Must be called after construction."""
327
+ await self.acp_env.__aenter__()
335
328
 
336
329
  async def initialize_mcp_servers(self) -> None:
337
330
  """Initialize MCP servers if any are configured."""
@@ -339,26 +332,32 @@ class ACPSession:
339
332
  return
340
333
  self.log.info("Initializing MCP servers", server_count=len(self.mcp_servers))
341
334
  cfgs = [convert_acp_mcp_server_to_config(s) for s in self.mcp_servers]
342
- # Define accessible roots for MCP servers
343
- # root = Path(self.cwd).resolve().as_uri() if self.cwd else None
344
- for _cfg in cfgs:
335
+ # Add each MCP server to the current agent's MCP manager dynamically
336
+ for cfg in cfgs:
345
337
  try:
346
- # Server will be initialized when MCP manager enters context
347
- self.log.info("Added MCP servers", server_count=len(cfgs))
348
- 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
+ )
349
342
  except Exception:
350
- self.log.exception("Failed to initialize MCP manager")
343
+ self.log.exception("Failed to setup MCP server", server_name=cfg.name)
351
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")
352
350
 
353
351
  async def init_project_context(self) -> None:
354
- """Load AGENTS.md file and inject project context into all agents.
352
+ """Load AGENTS.md/CLAUDE.md file and stage as initial context.
355
353
 
356
- 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.
357
357
  """
358
358
  if info := await self.requests.read_agent_rules(self.cwd):
359
- for agent in self.agent_pool.agents.values():
360
- prompt = f"## Project Information\n\n{info}"
361
- 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}")
362
361
 
363
362
  async def init_client_skills(self) -> None:
364
363
  """Discover and load skills from client-side .claude/skills directory.
@@ -377,7 +376,14 @@ class ACPSession:
377
376
  self.log.exception("Failed to discover client-side skills", error=e)
378
377
 
379
378
  @property
380
- def agent(self) -> Agent[ACPSession, str] | ACPAgent | AGUIAgent | ClaudeCodeAgent:
379
+ def agent(
380
+ self,
381
+ ) -> (
382
+ Agent[ACPSession, str]
383
+ | ACPAgent[ACPSession]
384
+ | AGUIAgent[ACPSession]
385
+ | ClaudeCodeAgent[ACPSession]
386
+ ):
381
387
  """Get the currently active agent."""
382
388
  if self.current_agent_name in self.agent_pool.agents:
383
389
  return self.agent_pool.get_agent(self.current_agent_name, deps_type=ACPSession)
@@ -421,6 +427,10 @@ class ACPSession:
421
427
  This actively interrupts the running agent by calling its interrupt() method,
422
428
  which handles protocol-specific cancellation (e.g., sending CancelNotification
423
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.
424
434
  """
425
435
  self._cancelled = True
426
436
  self.log.info("Session cancelled, interrupting agent")
@@ -450,7 +460,7 @@ class ACPSession:
450
460
  if not contents:
451
461
  self.log.warning("Empty prompt received")
452
462
  return "refusal"
453
- commands, non_command_content = split_commands(contents)
463
+ commands, non_command_content = split_commands(contents, self.command_store)
454
464
  async with self._task_lock:
455
465
  if commands: # Process commands if found
456
466
  for command in commands:
@@ -470,19 +480,62 @@ class ACPSession:
470
480
  has_staged=staged is not None,
471
481
  )
472
482
  event_count = 0
473
- 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
474
486
 
475
487
  try: # Use the session's persistent input provider
476
488
  async for event in self.agent.run_stream(
477
- *all_content, input_provider=self.input_provider
489
+ *all_content,
490
+ input_provider=self.input_provider,
491
+ deps=self,
492
+ conversation_id=self.session_id, # Tie agent conversation to ACP session
478
493
  ):
479
494
  if self._cancelled:
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
480
511
  return "cancelled"
481
512
 
482
513
  event_count += 1
483
- 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)
484
521
  self.log.info("Streaming finished", events_processed=event_count)
485
522
 
523
+ except asyncio.CancelledError:
524
+ # Task was cancelled (e.g., via interrupt()) - return proper stop reason
525
+ # This is critical: CancelledError doesn't inherit from Exception,
526
+ # so we must catch it explicitly to send the PromptResponse
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
538
+ return "cancelled"
486
539
  except UsageLimitExceeded as e:
487
540
  self.log.info("Usage limit exceeded", error=str(e))
488
541
  error_msg = str(e) # Determine which limit was hit based on error
@@ -495,6 +548,7 @@ class ACPSession:
495
548
  return "refusal"
496
549
  return "max_tokens" # Default to max_tokens for other usage limits
497
550
  except Exception as e:
551
+ self._current_converter = None # Clear converter reference
498
552
  self.log.exception("Error during streaming")
499
553
  # Send error notification asynchronously to avoid blocking response
500
554
  self.acp_agent.tasks.create_task(
@@ -503,228 +557,21 @@ class ACPSession:
503
557
  )
504
558
  return "end_turn"
505
559
  else:
560
+ # Title generation is now handled automatically by log_conversation
561
+ self._current_converter = None # Clear converter reference
506
562
  return "end_turn"
507
563
 
508
564
  async def _send_error_notification(self, message: str) -> None:
509
565
  """Send error notification, with exception handling."""
566
+ if self._cancelled:
567
+ return
510
568
  try:
511
569
  await self.notifications.send_agent_text(message)
512
570
  except Exception:
513
571
  self.log.exception("Failed to send error notification")
514
572
 
515
- async def handle_event(self, event: RichAgentStreamEvent[Any]) -> None: # noqa: PLR0915
516
- match event:
517
- case (
518
- PartStartEvent(part=TextPart(content=delta))
519
- | PartDeltaEvent(delta=TextPartDelta(content_delta=delta))
520
- ):
521
- await self.notifications.send_agent_text(delta)
522
-
523
- case (
524
- PartStartEvent(part=ThinkingPart(content=delta))
525
- | PartDeltaEvent(delta=ThinkingPartDelta(content_delta=delta))
526
- ):
527
- await self.notifications.send_agent_thought(delta or "\n")
528
-
529
- case PartStartEvent(part=part):
530
- self.log.debug("Received unhandled PartStartEvent", part=part)
531
-
532
- # Tool call streaming delta - create/update state and start notification
533
- case PartDeltaEvent(delta=ToolCallPartDelta() as delta):
534
- if delta_part := delta.as_part():
535
- tool_call_id = delta_part.tool_call_id
536
- try:
537
- tool_input = delta_part.args_as_dict()
538
- except ValueError:
539
- # Args still streaming, not valid JSON yet - skip this delta
540
- pass
541
- else:
542
- self._current_tool_inputs[tool_call_id] = tool_input
543
- # Create state and send initial notification
544
- state = self._get_or_create_tool_state(
545
- tool_call_id=tool_call_id,
546
- tool_name=delta_part.tool_name,
547
- tool_input=tool_input,
548
- )
549
- await state.start()
550
-
551
- # Tool call started - create/update state and start notification
552
- case FunctionToolCallEvent(part=part):
553
- 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 = {}
562
- self._current_tool_inputs[tool_call_id] = tool_input
563
- # Create state and send initial notification
564
- state = self._get_or_create_tool_state(
565
- tool_call_id=tool_call_id,
566
- tool_name=part.tool_name,
567
- tool_input=tool_input,
568
- )
569
- await state.start()
570
-
571
- # Tool completed successfully - update state and finalize
572
- case FunctionToolResultEvent(
573
- result=ToolReturnPart(content=content, tool_name=tool_name) as result,
574
- tool_call_id=tool_call_id,
575
- ):
576
- if isinstance(content, AsyncGenerator):
577
- full_content = ""
578
- async for chunk in content:
579
- full_content += str(chunk)
580
- # Stream progress through state
581
- if tool_state := self._tool_call_states.get(tool_call_id):
582
- await tool_state.update(status="in_progress", raw_output=chunk)
583
-
584
- # Replace the AsyncGenerator with the full content to prevent errors
585
- result.content = full_content
586
- final_output = full_content
587
- else:
588
- final_output = result.content
589
-
590
- # Complete tool call through state (preserves accumulated content/locations)
591
- if complete_state := self._tool_call_states.get(tool_call_id):
592
- # Only add return value as content if no content was emitted during execution
593
- if complete_state.has_content:
594
- # Content already provided via progress events - just set raw_output
595
- await complete_state.complete(raw_output=final_output)
596
- else:
597
- # No content yet - convert return value for display
598
- converted_blocks = to_acp_content_blocks(final_output)
599
- content_items = [
600
- ContentToolCallContent(content=block) for block in converted_blocks
601
- ]
602
- await complete_state.complete(
603
- raw_output=final_output,
604
- content=content_items,
605
- )
606
- self._cleanup_tool_state(tool_call_id)
607
-
608
- # Tool failed with retry - update state with error
609
- case FunctionToolResultEvent(
610
- result=RetryPromptPart(tool_name=tool_name) as result,
611
- tool_call_id=tool_call_id,
612
- ):
613
- error_message = result.model_response()
614
- if fail_state := self._tool_call_states.get(tool_call_id):
615
- await fail_state.fail(error=error_message)
616
- self._cleanup_tool_state(tool_call_id)
617
-
618
- # Tool emits its own start event - update state with better title/content
619
- case ToolCallStartEvent(
620
- tool_call_id=tool_call_id,
621
- tool_name=tool_name,
622
- title=title,
623
- kind=kind,
624
- locations=loc_items,
625
- raw_input=raw_input,
626
- ):
627
- self.log.debug(
628
- "Tool call start event", tool_name=tool_name, tool_call_id=tool_call_id
629
- )
630
- # Get or create state (may already exist from FunctionToolCallEvent)
631
- state = self._get_or_create_tool_state(
632
- tool_call_id=tool_call_id,
633
- tool_name=tool_name,
634
- tool_input=raw_input or {},
635
- )
636
- # Convert LocationContentItem objects to ACP format
637
- acp_locations = [
638
- ToolCallLocation(path=loc.path, line=loc.line) for loc in loc_items
639
- ]
640
- # Update state with tool-provided details (better title, content, locations)
641
- await state.update(title=title, kind=kind, locations=acp_locations or None)
642
-
643
- # Tool progress event - update state with title and content
644
- case ToolCallProgressEvent(
645
- tool_call_id=tool_call_id,
646
- title=title,
647
- status=status,
648
- items=items,
649
- ) if tool_call_id and tool_call_id in self._tool_call_states:
650
- progress_state = self._tool_call_states[tool_call_id]
651
- self.log.debug("Progress event", tool_call_id=tool_call_id, title=title)
652
-
653
- # Convert items to ACP content
654
- from agentpool.agents.events import (
655
- DiffContentItem,
656
- FileContentItem,
657
- LocationContentItem,
658
- TerminalContentItem,
659
- TextContentItem,
660
- )
661
- from agentpool_server.acp_server.syntax_detection import (
662
- format_zed_code_block,
663
- )
664
-
665
- acp_content: list[Any] = []
666
- location_paths: list[str] = []
667
-
668
- for item in items:
669
- match item:
670
- case TerminalContentItem(terminal_id=tid):
671
- acp_content.append(TerminalToolCallContent(terminal_id=tid))
672
- case TextContentItem(text=text):
673
- acp_content.append(ContentToolCallContent.text(text=text))
674
- case FileContentItem(
675
- content=file_content,
676
- path=file_path,
677
- start_line=start_line,
678
- end_line=end_line,
679
- ):
680
- # Format as Zed-compatible code block with clickable path
681
- formatted = format_zed_code_block(
682
- file_content, file_path, start_line, end_line
683
- )
684
- acp_content.append(ContentToolCallContent.text(text=formatted))
685
- # Also add path to locations for "follow along" feature
686
- location_paths.append(file_path)
687
- case DiffContentItem(path=diff_path, old_text=old, new_text=new):
688
- # Send diff via direct notification
689
- await self.notifications.file_edit_progress(
690
- tool_call_id=tool_call_id,
691
- path=diff_path,
692
- old_text=old or "",
693
- new_text=new,
694
- status=status,
695
- changed_lines=[],
696
- )
697
- case LocationContentItem(path=loc_path):
698
- location_paths.append(loc_path)
699
-
700
- await progress_state.update(
701
- title=title,
702
- status="in_progress",
703
- content=acp_content if acp_content else None,
704
- locations=location_paths if location_paths else None,
705
- )
706
-
707
- case FinalResultEvent():
708
- self.log.debug("Final result received")
709
-
710
- case StreamCompleteEvent():
711
- pass
712
-
713
- case PlanUpdateEvent(entries=entries, tool_call_id=tool_call_id):
714
- acp_entries = [
715
- PlanEntry(content=e.content, priority=e.priority, status=e.status)
716
- for e in entries
717
- ]
718
- await self.notifications.update_plan(acp_entries)
719
-
720
- case _:
721
- self.log.debug("Unhandled event", event_type=type(event).__name__)
722
-
723
573
  async def close(self) -> None:
724
574
  """Close the session and cleanup resources."""
725
- self._current_tool_inputs.clear()
726
- self._tool_call_states.clear()
727
-
728
575
  try:
729
576
  await self.acp_env.__aexit__(None, None, None)
730
577
  except Exception:
@@ -743,9 +590,14 @@ class ACPSession:
743
590
  self.log.exception("Error closing session")
744
591
 
745
592
  async def send_available_commands_update(self) -> None:
746
- """Send current available commands to client."""
593
+ """Send current available commands to client.
594
+
595
+ Merges local commands from command_store with any remote commands
596
+ from nested ACP agents.
597
+ """
747
598
  try:
748
- commands = self.get_acp_commands()
599
+ commands = self.get_acp_commands() # Local commands
600
+ commands.extend(self._remote_commands) # Merge remote commands
749
601
  await self.notifications.update_commands(commands)
750
602
  except Exception:
751
603
  self.log.exception("Failed to send available commands update")
@@ -802,11 +654,10 @@ class ACPSession:
802
654
  Returns:
803
655
  List of ACP AvailableCommand objects compatible with current node
804
656
  """
805
- all_commands = self.command_store.list_commands()
806
657
  current_node = self.agent
807
658
  # Filter commands by node compatibility
808
659
  compatible_commands = []
809
- for cmd in all_commands:
660
+ for cmd in self.command_store.list_commands():
810
661
  cmd_cls = cmd if isinstance(cmd, type) else type(cmd)
811
662
  # Check if command supports current node type
812
663
  if issubclass(cmd_cls, NodeCommand) and not cmd_cls.supports_node(current_node): # type: ignore[union-attr]
@@ -860,11 +711,7 @@ class ACPSession:
860
711
  )
861
712
 
862
713
  def register_update_callback(self, callback: Callable[[], None]) -> None:
863
- """Register callback for command updates.
864
-
865
- Args:
866
- callback: Function to call when commands are updated
867
- """
714
+ """Register callback for command updates."""
868
715
  self._update_callbacks.append(callback)
869
716
 
870
717
  def create_mcp_command(self, prompt: MCPClientPrompt) -> Command:
@@ -885,16 +732,13 @@ class ACPSession:
885
732
  ) -> None:
886
733
  """Execute the MCP prompt with parsed arguments."""
887
734
  # Map parsed args to prompt parameters
888
-
889
- result = {}
890
735
  # Map positional args to prompt parameter names
891
- for i, arg_value in enumerate(args):
892
- if i < len(prompt.arguments):
893
- param_name = prompt.arguments[i]["name"]
894
- result[param_name] = arg_value
895
- result.update(kwargs)
896
- try:
897
- # 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
898
742
  components = await prompt.get_components(result or None)
899
743
  self.staged_content.add(components)
900
744
  # Send confirmation
@@ -905,15 +749,13 @@ class ACPSession:
905
749
  logger.exception("MCP prompt execution failed", prompt=prompt.name)
906
750
  await ctx.print(f"❌ Prompt error: {e}")
907
751
 
908
- usage_hint = (
909
- " ".join(f"<{arg['name']}>" for arg in prompt.arguments) if prompt.arguments else None
910
- )
911
- return Command(
912
- execute_func=execute_prompt,
752
+ usage = " ".join(f"<{i['name']}>" for i in args) if (args := prompt.arguments) else None
753
+ return Command.from_raw(
754
+ execute_prompt,
913
755
  name=prompt.name,
914
756
  description=prompt.description or f"MCP prompt: {prompt.name}",
915
757
  category="mcp",
916
- usage=usage_hint,
758
+ usage=usage,
917
759
  )
918
760
 
919
761
  def create_prompt_hub_command(
@@ -960,8 +802,8 @@ class ACPSession:
960
802
  # Create command name - prefix with provider if not builtin
961
803
  command_name = f"{provider}_{name}" if provider != "builtin" else name
962
804
 
963
- return Command(
964
- execute_func=execute_prompt,
805
+ return Command.from_raw(
806
+ execute_prompt,
965
807
  name=command_name,
966
808
  description=f"Prompt hub: {provider}:{name}",
967
809
  category="prompts",