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
@@ -9,7 +9,7 @@ This is the reverse of the conversion done in acp_server/session.py handle_event
9
9
  from __future__ import annotations
10
10
 
11
11
  import base64
12
- from typing import TYPE_CHECKING, Any, overload
12
+ from typing import TYPE_CHECKING, Any, assert_never, overload
13
13
 
14
14
  from pydantic_ai import (
15
15
  AudioUrl,
@@ -53,12 +53,24 @@ from agentpool.agents.events import (
53
53
  if TYPE_CHECKING:
54
54
  from collections.abc import Sequence
55
55
 
56
- from pydantic_ai import UserContent
57
-
58
- from acp.schema import ContentBlock, SessionUpdate
59
- from acp.schema.mcp import HttpMcpServer, McpServer, SseMcpServer, StdioMcpServer
60
- from acp.schema.tool_call import ToolCallContent, ToolCallLocation
56
+ from pydantic_ai import FinishReason, UserContent
57
+
58
+ from acp.schema import (
59
+ ContentBlock,
60
+ HttpMcpServer,
61
+ McpServer,
62
+ SessionConfigOption,
63
+ SessionModelState,
64
+ SessionModeState,
65
+ SessionUpdate,
66
+ SseMcpServer,
67
+ StdioMcpServer,
68
+ StopReason,
69
+ ToolCallContent,
70
+ ToolCallLocation,
71
+ )
61
72
  from agentpool.agents.events import RichAgentStreamEvent, ToolCallContentItem
73
+ from agentpool.agents.modes import ModeCategory, ModeInfo
62
74
  from agentpool_config.mcp_server import (
63
75
  MCPServerConfig,
64
76
  SSEMCPServerConfig,
@@ -66,15 +78,122 @@ if TYPE_CHECKING:
66
78
  StreamableHTTPMCPServerConfig,
67
79
  )
68
80
 
81
+ STOP_REASON_MAP: dict[StopReason, FinishReason] = {
82
+ "end_turn": "stop",
83
+ "max_tokens": "length",
84
+ "max_turn_requests": "length",
85
+ "refusal": "content_filter",
86
+ "cancelled": "error",
87
+ }
88
+
89
+
90
+ def get_modes(
91
+ config_options: list[SessionConfigOption],
92
+ available_modes: SessionModeState | None,
93
+ available_models: SessionModelState | None,
94
+ ) -> list[ModeCategory]:
95
+ from acp.schema import SessionConfigSelectGroup
96
+ from agentpool.agents.modes import ModeCategory, ModeInfo
97
+
98
+ categories: list[ModeCategory] = []
99
+
100
+ if config_options:
101
+ for config_opt in config_options:
102
+ # Extract options from the config (ungrouped or grouped)
103
+ mode_infos: list[ModeInfo] = []
104
+ if isinstance(config_opt.options, list):
105
+ for opt_item in config_opt.options:
106
+ if isinstance(opt_item, SessionConfigSelectGroup):
107
+ mode_infos.extend(
108
+ ModeInfo(
109
+ id=sub_opt.value,
110
+ name=sub_opt.name,
111
+ description=sub_opt.description or "",
112
+ category_id=config_opt.id,
113
+ )
114
+ for sub_opt in opt_item.options
115
+ )
116
+ else:
117
+ # Ungrouped options
118
+ mode_infos.append(
119
+ ModeInfo(
120
+ id=opt_item.value,
121
+ name=opt_item.name,
122
+ description=opt_item.description or "",
123
+ category_id=config_opt.id,
124
+ )
125
+ )
126
+
127
+ categories.append(
128
+ ModeCategory(
129
+ id=config_opt.id,
130
+ name=config_opt.name,
131
+ available_modes=mode_infos,
132
+ current_mode_id=config_opt.current_value,
133
+ category=config_opt.category or "other",
134
+ )
135
+ )
136
+ return categories
137
+
138
+ # Legacy: Convert ACP SessionModeState to ModeCategory
139
+ if available_modes:
140
+ acp_modes = available_modes
141
+ modes = [
142
+ ModeInfo(
143
+ id=m.id,
144
+ name=m.name,
145
+ description=m.description or "",
146
+ category_id="permissions",
147
+ )
148
+ for m in acp_modes.available_modes
149
+ ]
150
+ categories.append(
151
+ ModeCategory(
152
+ id="permissions",
153
+ name="Mode",
154
+ available_modes=modes,
155
+ current_mode_id=acp_modes.current_mode_id,
156
+ category="mode",
157
+ )
158
+ )
159
+
160
+ # Legacy: Convert ACP SessionModelState to ModeCategory
161
+ if available_models:
162
+ acp_models = available_models
163
+ models = [
164
+ ModeInfo(
165
+ id=m.model_id,
166
+ name=m.name,
167
+ description=m.description or "",
168
+ category_id="model",
169
+ )
170
+ for m in acp_models.available_models
171
+ ]
172
+ categories.append(
173
+ ModeCategory(
174
+ id="model",
175
+ name="Model",
176
+ available_modes=models,
177
+ current_mode_id=acp_models.current_model_id,
178
+ category="model",
179
+ )
180
+ )
181
+
182
+ return categories
183
+
184
+
185
+ def to_finish_reason(stop_reason: StopReason) -> FinishReason:
186
+ return STOP_REASON_MAP.get(stop_reason, "stop")
187
+
69
188
 
70
189
  def convert_acp_locations(
71
- locations: list[ToolCallLocation] | None,
190
+ locations: Sequence[ToolCallLocation] | None,
72
191
  ) -> list[LocationContentItem]:
73
192
  """Convert ACP ToolCallLocation list to native LocationContentItem list."""
74
193
  return [LocationContentItem(path=loc.path, line=loc.line) for loc in locations or []]
75
194
 
76
195
 
77
- def convert_acp_content(content: list[ToolCallContent] | None) -> list[ToolCallContentItem]:
196
+ def convert_acp_content(content: Sequence[ToolCallContent] | None) -> list[ToolCallContentItem]:
78
197
  """Convert ACP ToolCallContent list to native ToolCallContentItem list."""
79
198
  if not content:
80
199
  return []
@@ -189,42 +308,70 @@ def acp_to_native_event(update: SessionUpdate) -> RichAgentStreamEvent[Any] | No
189
308
  # Text message chunks -> PartDeltaEvent with TextPartDelta
190
309
  case AgentMessageChunk(content=TextContentBlock(text=text)):
191
310
  return PartDeltaEvent(index=0, delta=TextPartDelta(content_delta=text))
311
+
192
312
  # Thought chunks -> PartDeltaEvent with ThinkingPartDelta
193
313
  case AgentThoughtChunk(content=TextContentBlock(text=text)):
194
314
  return PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta=text))
195
- # User message echo - could emit as PartStartEvent if needed
315
+
316
+ # User message echo - usually ignored
196
317
  case UserMessageChunk():
197
- return None # Usually ignored
318
+ return None
319
+
198
320
  # Tool call start -> ToolCallStartEvent
199
- case ToolCallStart() as tc:
321
+ case ToolCallStart(
322
+ tool_call_id=tool_call_id,
323
+ title=title,
324
+ kind=kind,
325
+ content=content,
326
+ locations=locations,
327
+ raw_input=raw_input,
328
+ ):
200
329
  return ToolCallStartEvent(
201
- tool_call_id=tc.tool_call_id,
202
- tool_name=tc.title, # ACP uses title, not separate tool_name
203
- title=tc.title,
204
- kind=tc.kind or "other",
205
- content=convert_acp_content(list(tc.content) if tc.content else None),
206
- locations=convert_acp_locations(list(tc.locations) if tc.locations else None),
207
- raw_input=tc.raw_input or {},
330
+ tool_call_id=tool_call_id,
331
+ tool_name=title, # ACP uses title, not separate tool_name
332
+ title=title,
333
+ kind=kind or "other",
334
+ content=convert_acp_content(list(content) if content else None),
335
+ locations=convert_acp_locations(list(locations) if locations else None),
336
+ raw_input=raw_input or {},
208
337
  )
209
338
 
210
- # Tool call progress -> ToolCallProgressEvent
211
- case ToolCallProgress() as tc:
212
- items = convert_acp_content(list(tc.content) if tc.content else None)
339
+ # Tool call progress -> ToolCallProgressEvent or ToolCallCompleteEvent
340
+ case ToolCallProgress(
341
+ tool_call_id=tool_call_id,
342
+ status=status,
343
+ title=title,
344
+ content=content,
345
+ raw_output=raw_output,
346
+ ):
347
+ # If completed, return ToolCallCompleteEvent for metadata injection
348
+ if status == "completed":
349
+ from agentpool.agents.events import ToolCallCompleteEvent
350
+
351
+ return ToolCallCompleteEvent(
352
+ tool_call_id=tool_call_id,
353
+ tool_name=title or "unknown",
354
+ tool_input={}, # ACP doesn't provide input in progress updates
355
+ tool_result=str(raw_output) if raw_output else "",
356
+ agent_name="", # Will be set by agent
357
+ message_id="",
358
+ metadata=None, # Will be injected by agent from metadata accumulator
359
+ )
360
+ # Otherwise return progress event
213
361
  return ToolCallProgressEvent(
214
- tool_call_id=tc.tool_call_id,
215
- status=tc.status or "in_progress",
216
- title=tc.title,
217
- items=items,
218
- message=str(tc.raw_output) if tc.raw_output else None,
362
+ tool_call_id=tool_call_id,
363
+ status=status or "in_progress",
364
+ title=title,
365
+ items=convert_acp_content(list(content) if content else None),
366
+ message=str(raw_output) if raw_output else None,
219
367
  )
220
368
 
221
369
  # Plan update -> PlanUpdateEvent
222
- case AgentPlanUpdate(entries=acp_entries):
370
+ case AgentPlanUpdate(entries=entries):
223
371
  from agentpool.resource_providers.plan_provider import PlanEntry
224
372
 
225
373
  native_entries = [
226
- PlanEntry(content=e.content, priority=e.priority, status=e.status)
227
- for e in acp_entries
374
+ PlanEntry(content=e.content, priority=e.priority, status=e.status) for e in entries
228
375
  ]
229
376
  return PlanUpdateEvent(entries=native_entries)
230
377
 
@@ -233,24 +380,27 @@ def acp_to_native_event(update: SessionUpdate) -> RichAgentStreamEvent[Any] | No
233
380
 
234
381
 
235
382
  @overload
236
- def mcp_config_to_acp(config: StdioMCPServerConfig) -> StdioMcpServer | None: ...
383
+ def mcp_config_to_acp(config: StdioMCPServerConfig) -> StdioMcpServer: ...
237
384
 
238
385
 
239
386
  @overload
240
- def mcp_config_to_acp(config: SSEMCPServerConfig) -> SseMcpServer | None: ...
387
+ def mcp_config_to_acp(config: SSEMCPServerConfig) -> SseMcpServer: ...
241
388
 
242
389
 
243
390
  @overload
244
- def mcp_config_to_acp(config: StreamableHTTPMCPServerConfig) -> HttpMcpServer | None: ...
391
+ def mcp_config_to_acp(config: StreamableHTTPMCPServerConfig) -> HttpMcpServer: ...
245
392
 
246
393
 
247
394
  @overload
248
- def mcp_config_to_acp(config: MCPServerConfig) -> McpServer | None: ...
395
+ def mcp_config_to_acp(config: MCPServerConfig) -> McpServer: ...
249
396
 
250
397
 
251
- def mcp_config_to_acp(config: MCPServerConfig) -> McpServer | None:
398
+ def mcp_config_to_acp(config: MCPServerConfig) -> McpServer:
252
399
  """Convert native MCPServerConfig to ACP McpServer format.
253
400
 
401
+ If the config has tool filtering (enabled_tools or disabled_tools),
402
+ the server is wrapped with mcp-filter proxy to apply the filtering.
403
+
254
404
  Args:
255
405
  config: agentpool MCP server configuration
256
406
 
@@ -265,21 +415,29 @@ def mcp_config_to_acp(config: MCPServerConfig) -> McpServer | None:
265
415
  StreamableHTTPMCPServerConfig,
266
416
  )
267
417
 
418
+ # If filtering is configured, wrap with mcp-filter first
419
+ if config.needs_tool_filtering():
420
+ config = config.wrap_with_mcp_filter()
421
+
268
422
  match config:
269
423
  case StdioMCPServerConfig(command=command, args=args):
270
424
  env_vars = config.get_env_vars() if hasattr(config, "get_env_vars") else {}
425
+ env_list = [EnvVariable(name=k, value=v) for k, v in env_vars.items()]
271
426
  return StdioMcpServer(
272
427
  name=config.name or command,
273
428
  command=command,
274
429
  args=list(args) if args else [],
275
- env=[EnvVariable(name=k, value=v) for k, v in env_vars.items()],
430
+ env=env_list,
276
431
  )
432
+
277
433
  case SSEMCPServerConfig(url=url):
278
434
  return SseMcpServer(name=config.name or str(url), url=url, headers=[])
435
+
279
436
  case StreamableHTTPMCPServerConfig(url=url):
280
437
  return HttpMcpServer(name=config.name or str(url), url=url, headers=[])
281
- case _:
282
- return None
438
+
439
+ case _ as unreachable:
440
+ assert_never(unreachable)
283
441
 
284
442
 
285
443
  def mcp_configs_to_acp(configs: Sequence[MCPServerConfig]) -> list[McpServer]:
@@ -291,4 +449,4 @@ def mcp_configs_to_acp(configs: Sequence[MCPServerConfig]) -> list[McpServer]:
291
449
  Returns:
292
450
  List of ACP-compatible McpServer instances (skips unconvertible configs)
293
451
  """
294
- return [converted for config in configs if (converted := mcp_config_to_acp(config)) is not None]
452
+ return [mcp_config_to_acp(config) for config in configs]
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  from pathlib import Path
7
7
  from typing import TYPE_CHECKING, Any
8
- import uuid
9
8
 
10
9
  import anyio
11
10
 
@@ -21,13 +20,16 @@ from acp.schema import (
21
20
  WriteTextFileResponse,
22
21
  )
23
22
  from agentpool.log import get_logger
24
- from agentpool.tools.base import Tool
25
23
 
26
24
 
27
25
  if TYPE_CHECKING:
26
+ from collections.abc import Sequence
27
+
28
28
  from exxec import ExecutionEnvironment
29
+ from slashed import Command
29
30
 
30
31
  from acp.schema import (
32
+ AvailableCommand,
31
33
  CreateTerminalRequest,
32
34
  KillTerminalCommandRequest,
33
35
  ReadTextFileRequest,
@@ -75,7 +77,7 @@ class ACPClientHandler(Client):
75
77
  self.state = state
76
78
  self._input_provider = input_provider
77
79
  self._update_event = asyncio.Event()
78
- # Map ACP terminal IDs to process manager IDs
80
+ # Map ACP terminal IDs to process manager IDs (for local execution only)
79
81
  self._terminal_to_process: dict[str, str] = {}
80
82
  # Copy tool confirmation mode from agent (can be updated via set_tool_confirmation_mode)
81
83
  self.tool_confirmation_mode: ToolConfirmationMode = agent.tool_confirmation_mode
@@ -98,11 +100,93 @@ class ACPClientHandler(Client):
98
100
  return self._agent.config.allow_terminal
99
101
 
100
102
  async def session_update(self, params: SessionNotification[Any]) -> None:
101
- """Handle session update notifications from the agent."""
103
+ """Handle session update notifications from the agent.
104
+
105
+ Some updates are state changes (mode, model, config) that should update
106
+ session state. Others are stream events (text chunks, tool calls) that
107
+ should be queued for the run_stream consumer.
108
+ """
109
+ from acp.schema import (
110
+ AvailableCommandsUpdate,
111
+ ConfigOptionUpdate,
112
+ CurrentModelUpdate,
113
+ CurrentModeUpdate,
114
+ )
102
115
  from agentpool.agents.acp_agent.acp_converters import acp_to_native_event
103
116
 
104
117
  update = params.update
105
- # Convert to native event and queue it
118
+
119
+ # Handle state updates - these modify session state, not stream events
120
+ match update:
121
+ case CurrentModeUpdate(current_mode_id=mode_id):
122
+ if self.state.modes:
123
+ self.state.modes.current_mode_id = mode_id
124
+ # Find ModeInfo and emit signal
125
+ for acp_mode in self.state.modes.available_modes:
126
+ if acp_mode.id == mode_id:
127
+ from agentpool.agents.modes import ModeInfo
128
+
129
+ mode_info = ModeInfo(
130
+ id=acp_mode.id,
131
+ name=acp_mode.name,
132
+ description=acp_mode.description or "",
133
+ category_id="remote",
134
+ )
135
+ await self._agent.state_updated.emit(mode_info)
136
+ break
137
+ self.state.current_mode_id = mode_id
138
+ logger.debug("Mode updated", mode_id=mode_id)
139
+ self._update_event.set()
140
+ return
141
+
142
+ case CurrentModelUpdate(current_model_id=model_id):
143
+ self.state.current_model_id = model_id
144
+ if self.state.models:
145
+ self.state.models.current_model_id = model_id
146
+ # Find ModelInfo and emit signal
147
+ for acp_model in self.state.models.available_models:
148
+ if acp_model.model_id == model_id:
149
+ from tokonomics.model_discovery.model_info import (
150
+ ModelInfo as TokoModelInfo,
151
+ )
152
+
153
+ model_info = TokoModelInfo(
154
+ id=acp_model.model_id,
155
+ name=acp_model.name,
156
+ description=acp_model.description,
157
+ )
158
+ await self._agent.state_updated.emit(model_info)
159
+ break
160
+ logger.debug("Model updated", model_id=model_id)
161
+ self._update_event.set()
162
+ return
163
+
164
+ case ConfigOptionUpdate():
165
+ await self._agent.state_updated.emit(update)
166
+ logger.debug("Config option updated", update=update)
167
+ self._update_event.set()
168
+ return
169
+
170
+ case AvailableCommandsUpdate():
171
+ self.state.available_commands = update
172
+ # Populate command store with remote commands
173
+ self._populate_command_store(update.available_commands)
174
+ # Emit to parent session - remote commands will be merged with local ones.
175
+ # The "way back" works because session.split_commands() only extracts
176
+ # LOCAL commands; remote commands pass through to the agent prompt.
177
+ await self._agent.state_updated.emit(update)
178
+ logger.debug("Available commands updated", count=len(update.available_commands))
179
+ self._update_event.set()
180
+ return
181
+
182
+ # TODO: AgentPlanUpdate handling is complex and needs design work.
183
+ # Options:
184
+ # 1. Update pool.todos - requires merging with existing todos
185
+ # 2. Pass through to UI - but then todos aren't centrally managed
186
+ # 3. Switch to agent-owned todos instead of pool-owned
187
+ # For now, AgentPlanUpdate falls through to stream events.
188
+
189
+ # All other updates are stream events - convert and queue
106
190
  if native_event := acp_to_native_event(update):
107
191
  self.state.events.append(native_event)
108
192
  self._update_event.set()
@@ -138,11 +222,21 @@ class ACPClientHandler(Client):
138
222
 
139
223
  if self._input_provider:
140
224
  ctx = self._agent.get_context() # Use the agent's NodeContext
225
+ # Attach tool call metadata for permission event matching
226
+ ctx.tool_call_id = params.tool_call.tool_call_id
227
+ ctx.tool_name = params.tool_call.title
228
+ args = (
229
+ params.tool_call.raw_input if isinstance(params.tool_call.raw_input, dict) else {}
230
+ )
231
+ ctx.tool_input = args
141
232
  # Create a dummy tool representation from ACP params
142
- tool = Tool(callable=lambda: None, name=params.tool_call.tool_call_id, description=name)
143
- # Extract arguments - ACP doesn't expose them in ToolCall
233
+ from agentpool.tools import FunctionTool
234
+
235
+ tool = FunctionTool(
236
+ callable=lambda: None, name=params.tool_call.tool_call_id, description=name
237
+ )
144
238
  try:
145
- result = await self._input_provider.get_tool_confirmation(ctx, tool=tool, args={})
239
+ result = await self._input_provider.get_tool_confirmation(ctx, tool=tool, args=args)
146
240
  # Map confirmation result to ACP response
147
241
  if result == "allow":
148
242
  option_id = params.options[0].option_id if params.options else "allow"
@@ -177,9 +271,10 @@ class ACPClientHandler(Client):
177
271
  logger.debug("Read file", path=params.path, num_chars=len(content))
178
272
  return ReadTextFileResponse(content=content)
179
273
 
180
- except FileNotFoundError:
181
- logger.exception("File not found", path=params.path)
182
- raise
274
+ except (FileNotFoundError, KeyError):
275
+ # Match Zed behavior: return empty string for non-existent files
276
+ logger.debug("File not found, returning empty string", path=params.path)
277
+ return ReadTextFileResponse(content="")
183
278
  except Exception:
184
279
  logger.exception("Failed to read file", path=params.path)
185
280
  raise
@@ -202,11 +297,18 @@ class ACPClientHandler(Client):
202
297
  raise
203
298
 
204
299
  async def create_terminal(self, params: CreateTerminalRequest) -> CreateTerminalResponse:
205
- """Create a new terminal session via ProcessManager."""
300
+ """Create a new terminal session via the configured ExecutionEnvironment.
301
+
302
+ The ProcessManager implementation determines where the terminal runs
303
+ (local, Docker, E2B, SSH, or forwarded to parent ACP client like Zed).
304
+
305
+ The terminal_id returned by process_manager.start_process() is used directly.
306
+ """
206
307
  if not self.allow_terminal:
207
308
  raise RuntimeError("Terminal operations not allowed")
309
+
208
310
  try:
209
- process_id = await self.env.process_manager.start_process(
311
+ terminal_id = await self.env.process_manager.start_process(
210
312
  command=params.command,
211
313
  args=list(params.args) if params.args else None,
212
314
  cwd=params.cwd,
@@ -216,10 +318,10 @@ class ACPClientHandler(Client):
216
318
  logger.exception("Failed to create terminal", command=params.command)
217
319
  raise
218
320
  else:
219
- terminal_id = f"term_{uuid.uuid4().hex[:8]}"
220
- self._terminal_to_process[terminal_id] = process_id
221
- msg = "Created terminal"
222
- logger.info(msg, terminal_id=terminal_id, process_id=process_id, cmd=params.command)
321
+ # Use the ID from process_manager directly - for ACPProcessManager this
322
+ # is already the parent's terminal ID (e.g., Zed's)
323
+ self._terminal_to_process[terminal_id] = terminal_id
324
+ logger.info("Created terminal", terminal_id=terminal_id, command=params.command)
223
325
  return CreateTerminalResponse(terminal_id=terminal_id)
224
326
 
225
327
  async def terminal_output(self, params: TerminalOutputRequest) -> TerminalOutputResponse:
@@ -231,8 +333,7 @@ class ACPClientHandler(Client):
231
333
  if terminal_id not in self._terminal_to_process:
232
334
  raise ValueError(f"Terminal {terminal_id} not found")
233
335
 
234
- process_id = self._terminal_to_process[terminal_id]
235
- proc_output = await self.env.process_manager.get_output(process_id)
336
+ proc_output = await self.env.process_manager.get_output(terminal_id)
236
337
  output = proc_output.combined or proc_output.stdout or ""
237
338
  return TerminalOutputResponse(output=output, truncated=proc_output.truncated)
238
339
 
@@ -247,8 +348,7 @@ class ACPClientHandler(Client):
247
348
  if terminal_id not in self._terminal_to_process:
248
349
  raise ValueError(f"Terminal {terminal_id} not found")
249
350
 
250
- process_id = self._terminal_to_process[terminal_id]
251
- exit_code = await self.env.process_manager.wait_for_exit(process_id)
351
+ exit_code = await self.env.process_manager.wait_for_exit(terminal_id)
252
352
  logger.debug("Terminal exited", terminal_id=terminal_id, exit_code=exit_code)
253
353
  return WaitForTerminalExitResponse(exit_code=exit_code)
254
354
 
@@ -263,8 +363,7 @@ class ACPClientHandler(Client):
263
363
  if terminal_id not in self._terminal_to_process:
264
364
  raise ValueError(f"Terminal {terminal_id} not found")
265
365
 
266
- process_id = self._terminal_to_process[terminal_id]
267
- await self.env.process_manager.kill_process(process_id)
366
+ await self.env.process_manager.kill_process(terminal_id)
268
367
  logger.info("Killed terminal", terminal_id=terminal_id)
269
368
  return KillTerminalCommandResponse()
270
369
 
@@ -276,8 +375,8 @@ class ACPClientHandler(Client):
276
375
  terminal_id = params.terminal_id
277
376
  if terminal_id not in self._terminal_to_process:
278
377
  raise ValueError(f"Terminal {terminal_id} not found")
279
- process_id = self._terminal_to_process[terminal_id]
280
- await self.env.process_manager.release_process(process_id)
378
+
379
+ await self.env.process_manager.release_process(terminal_id)
281
380
  del self._terminal_to_process[terminal_id]
282
381
  logger.info("Released terminal", terminal_id=terminal_id)
283
382
  return ReleaseTerminalResponse()
@@ -301,13 +400,79 @@ class ACPClientHandler(Client):
301
400
  """Handle extension notifications."""
302
401
  logger.debug("Extension notification", method=method)
303
402
 
403
+ def _populate_command_store(self, commands: Sequence[AvailableCommand]) -> None:
404
+ """Populate the agent's command store with remote ACP commands.
405
+
406
+ Args:
407
+ commands: List of AvailableCommand objects from the remote agent
408
+ """
409
+ store = self._agent.command_store
410
+
411
+ for cmd in commands:
412
+ command = self._create_acp_command(cmd)
413
+ # Unregister if already exists (in case of update)
414
+ if store.get_command(cmd.name):
415
+ store.unregister_command(cmd.name)
416
+ store.register_command(command)
417
+
418
+ logger.debug("Populated command store", command_count=len(store.list_commands()))
419
+
420
+ def _create_acp_command(self, cmd: AvailableCommand) -> Command:
421
+ """Create a slashed Command from an ACP AvailableCommand.
422
+
423
+ The command, when executed, sends a prompt with the slash command
424
+ to the remote ACP agent.
425
+
426
+ Args:
427
+ cmd: AvailableCommand from remote agent
428
+
429
+ Returns:
430
+ A slashed Command that sends the command to the remote agent
431
+ """
432
+ from pydantic_ai import PartDeltaEvent, TextPartDelta
433
+ from slashed import Command
434
+
435
+ name = cmd.name
436
+ description = cmd.description
437
+ input_hint = cmd.input.root.hint if cmd.input else None
438
+
439
+ async def execute_command(
440
+ ctx: Any,
441
+ args: list[str],
442
+ kwargs: dict[str, str],
443
+ ) -> None:
444
+ """Execute the remote ACP slash command."""
445
+ # Build command string
446
+ args_str = " ".join(args) if args else ""
447
+ if kwargs:
448
+ kwargs_str = " ".join(f"{k}={v}" for k, v in kwargs.items())
449
+ args_str = f"{args_str} {kwargs_str}".strip()
450
+
451
+ full_command = f"/{name} {args_str}".strip()
452
+
453
+ # Execute via agent run_stream - the slash command goes as a prompt
454
+ async for event in self._agent.run_stream(full_command):
455
+ # Extract text from PartDeltaEvent with TextPartDelta
456
+ if isinstance(event, PartDeltaEvent):
457
+ delta = event.delta
458
+ if isinstance(delta, TextPartDelta):
459
+ await ctx.print(delta.content_delta)
460
+
461
+ return Command.from_raw(
462
+ execute_command,
463
+ name=name,
464
+ description=description,
465
+ category="remote",
466
+ usage=input_hint,
467
+ )
468
+
304
469
 
305
470
  if __name__ == "__main__":
306
471
  from agentpool.agents.acp_agent import ACPAgent
307
472
 
308
473
  async def main() -> None:
309
474
  """Demo: Basic call to an ACP agent."""
310
- args = ["run", "agentpool", "serve-acp", "--model-provider", "openai"]
475
+ args = ["run", "agentpool", "serve-acp"]
311
476
  cwd = str(Path.cwd())
312
477
  async with ACPAgent(command="uv", args=args, cwd=cwd, event_handlers=["detailed"]) as agent:
313
478
  print("Response (streaming): ", end="", flush=True)