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
@@ -12,12 +12,14 @@ and communicating with it via JSON-RPC over stdio. This allows:
12
12
 
13
13
  Example:
14
14
  ```python
15
+ from agentpool.models.acp_agents import ACPAgentConfig
16
+
15
17
  config = ACPAgentConfig(
16
18
  command="claude-code-acp",
17
19
  name="claude_coder",
18
20
  cwd="/path/to/project",
19
21
  )
20
- async with ACPAgent(config) as agent:
22
+ async with ACPAgent(config=config) as agent:
21
23
  result = await agent.run("Write a hello world program")
22
24
  print(result.content)
23
25
  ```
@@ -26,10 +28,12 @@ Example:
26
28
  from __future__ import annotations
27
29
 
28
30
  import asyncio
31
+ from dataclasses import replace
32
+ from importlib.metadata import metadata
29
33
  import os
30
34
  from pathlib import Path
31
35
  import subprocess
32
- from typing import TYPE_CHECKING, Any, Self, overload
36
+ from typing import TYPE_CHECKING, Any, Self
33
37
  import uuid
34
38
 
35
39
  import anyio
@@ -45,17 +49,26 @@ from pydantic_ai import (
45
49
  UserPromptPart,
46
50
  )
47
51
 
48
- from agentpool.agents.acp_agent.acp_converters import convert_to_acp_content, mcp_configs_to_acp
49
- from agentpool.agents.acp_agent.client_handler import ACPClientHandler
50
52
  from agentpool.agents.acp_agent.session_state import ACPSessionState
51
53
  from agentpool.agents.base_agent import BaseAgent
52
- from agentpool.agents.events import RunStartedEvent, StreamCompleteEvent, ToolCallStartEvent
54
+ from agentpool.agents.events import (
55
+ RunStartedEvent,
56
+ StreamCompleteEvent,
57
+ ToolCallCompleteEvent,
58
+ ToolCallStartEvent,
59
+ resolve_event_handlers,
60
+ )
61
+ from agentpool.agents.events.processors import FileTracker
62
+ from agentpool.agents.modes import ModeInfo
63
+ from agentpool.common_types import (
64
+ IndividualEventHandler,
65
+ )
53
66
  from agentpool.log import get_logger
54
67
  from agentpool.messaging import ChatMessage
55
- from agentpool.messaging.processing import prepare_prompts
56
68
  from agentpool.models.acp_agents import ACPAgentConfig, MCPCapableACPAgentConfig
57
- from agentpool.talk.stats import MessageStats
58
69
  from agentpool.utils.streams import merge_queue_into_iterator
70
+ from agentpool.utils.subprocess_utils import SubprocessError, monitor_process
71
+ from agentpool.utils.token_breakdown import calculate_usage_from_parts
59
72
 
60
73
 
61
74
  if TYPE_CHECKING:
@@ -63,28 +76,27 @@ if TYPE_CHECKING:
63
76
  from types import TracebackType
64
77
 
65
78
  from anyio.abc import Process
66
- from evented.configs import EventConfig
79
+ from evented_config import EventConfig
67
80
  from exxec import ExecutionEnvironment
68
- from pydantic_ai import FinishReason
69
- from tokonomics.model_discovery import ProviderType
81
+ from pydantic_ai import UserContent
82
+ from slashed import BaseCommand
83
+ from tokonomics.model_discovery.model_info import ModelInfo
70
84
 
71
85
  from acp.agent.protocol import Agent as ACPAgentProtocol
72
86
  from acp.client.connection import ClientSideConnection
73
87
  from acp.client.protocol import Client
74
88
  from acp.schema import (
75
- InitializeResponse,
89
+ Implementation,
76
90
  RequestPermissionRequest,
77
91
  RequestPermissionResponse,
78
- StopReason,
79
92
  )
80
93
  from acp.schema.mcp import McpServer
81
94
  from agentpool.agents import AgentContext
95
+ from agentpool.agents.acp_agent.client_handler import ACPClientHandler
82
96
  from agentpool.agents.events import RichAgentStreamEvent
97
+ from agentpool.agents.modes import ModeCategory
83
98
  from agentpool.common_types import (
84
99
  BuiltinEventHandlerType,
85
- IndividualEventHandler,
86
- PromptCompatible,
87
- SimpleJsonType,
88
100
  )
89
101
  from agentpool.delegation import AgentPool
90
102
  from agentpool.mcp_server.tool_bridge import ToolManagerBridge
@@ -97,42 +109,6 @@ logger = get_logger(__name__)
97
109
 
98
110
  PROTOCOL_VERSION = 1
99
111
 
100
- STOP_REASON_MAP: dict[StopReason, FinishReason] = {
101
- "end_turn": "stop",
102
- "max_tokens": "length",
103
- "max_turn_requests": "length",
104
- "refusal": "content_filter",
105
- "cancelled": "error",
106
- }
107
-
108
-
109
- def extract_file_path_from_tool_call(tool_name: str, raw_input: dict[str, Any]) -> str | None:
110
- """Extract file path from a tool call if it's a file-writing tool.
111
-
112
- Uses simple heuristics by default:
113
- - Tool name contains 'write' or 'edit' (case-insensitive)
114
- - Input contains 'path' or 'file_path' key
115
-
116
- Override in subclasses for agent-specific tool naming conventions.
117
-
118
- Args:
119
- tool_name: Name of the tool being called
120
- raw_input: Tool call arguments
121
-
122
- Returns:
123
- File path if this is a file-writing tool, None otherwise
124
- """
125
- name_lower = tool_name.lower()
126
- if "write" not in name_lower and "edit" not in name_lower:
127
- return None
128
-
129
- # Try common path argument names
130
- for key in ("file_path", "path", "filepath", "filename", "file"):
131
- if key in raw_input and isinstance(val := raw_input[key], str):
132
- return val
133
-
134
- return None
135
-
136
112
 
137
113
  class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
138
114
  """MessageNode that wraps an external ACP agent subprocess.
@@ -148,57 +124,17 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
148
124
 
149
125
  Supports both blocking `run()` and streaming `run_iter()` execution modes.
150
126
 
151
- Example with config:
127
+ Example:
152
128
  ```python
129
+ # From config
153
130
  config = ClaudeACPAgentConfig(cwd="/project", model="sonnet")
154
- agent = ACPAgent(config, agent_pool=pool)
155
- ```
131
+ agent = ACPAgent(config=config, agent_pool=pool)
156
132
 
157
- Example with kwargs:
158
- ```python
159
- agent = ACPAgent(
160
- command="claude-code-acp",
161
- cwd="/project",
162
- providers=["anthropic"],
163
- )
133
+ # From kwargs
134
+ agent = ACPAgent(command="claude-code-acp", cwd="/project")
164
135
  ```
165
136
  """
166
137
 
167
- @overload
168
- def __init__(
169
- self,
170
- *,
171
- config: BaseACPAgentConfig,
172
- input_provider: InputProvider | None = None,
173
- agent_pool: AgentPool[Any] | None = None,
174
- enable_logging: bool = True,
175
- event_configs: Sequence[EventConfig] | None = None,
176
- event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
177
- ) -> None: ...
178
-
179
- @overload
180
- def __init__(
181
- self,
182
- *,
183
- command: str,
184
- name: str | None = None,
185
- description: str | None = None,
186
- display_name: str | None = None,
187
- args: list[str] | None = None,
188
- cwd: str | None = None,
189
- env_vars: dict[str, str] | None = None,
190
- env: ExecutionEnvironment | None = None,
191
- allow_file_operations: bool = True,
192
- allow_terminal: bool = True,
193
- providers: list[ProviderType] | None = None,
194
- input_provider: InputProvider | None = None,
195
- agent_pool: AgentPool[Any] | None = None,
196
- enable_logging: bool = True,
197
- event_configs: Sequence[EventConfig] | None = None,
198
- event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
199
- tool_confirmation_mode: ToolConfirmationMode = "always",
200
- ) -> None: ...
201
-
202
138
  def __init__(
203
139
  self,
204
140
  *,
@@ -210,16 +146,15 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
210
146
  args: list[str] | None = None,
211
147
  cwd: str | None = None,
212
148
  env_vars: dict[str, str] | None = None,
213
- env: ExecutionEnvironment | None = None,
214
149
  allow_file_operations: bool = True,
215
150
  allow_terminal: bool = True,
216
- providers: list[ProviderType] | None = None,
217
151
  input_provider: InputProvider | None = None,
218
152
  agent_pool: AgentPool[Any] | None = None,
219
153
  enable_logging: bool = True,
220
154
  event_configs: Sequence[EventConfig] | None = None,
221
155
  event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
222
156
  tool_confirmation_mode: ToolConfirmationMode = "always",
157
+ commands: Sequence[BaseCommand] | None = None,
223
158
  ) -> None:
224
159
  # Build config from kwargs if not provided
225
160
  if config is None:
@@ -237,21 +172,21 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
237
172
  allow_file_operations=allow_file_operations,
238
173
  allow_terminal=allow_terminal,
239
174
  requires_tool_confirmation=tool_confirmation_mode,
240
- providers=list(providers) if providers else [],
241
175
  )
242
176
 
243
177
  super().__init__(
244
178
  name=name or config.name or config.get_command(),
245
179
  description=description or config.description,
246
- display_name=display_name,
180
+ display_name=display_name or config.display_name,
247
181
  mcp_servers=config.mcp_servers,
248
182
  agent_pool=agent_pool,
249
183
  enable_logging=enable_logging,
250
184
  event_configs=event_configs or list(config.triggers),
251
- env=env or config.get_execution_environment(),
185
+ env=config.get_execution_environment(),
252
186
  input_provider=input_provider,
253
187
  tool_confirmation_mode=tool_confirmation_mode,
254
188
  event_handlers=event_handlers,
189
+ commands=commands,
255
190
  )
256
191
 
257
192
  # ACP-specific state
@@ -262,7 +197,7 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
262
197
  self._process: Process | None = None
263
198
  self._connection: ClientSideConnection | None = None
264
199
  self._client_handler: ACPClientHandler | None = None
265
- self._init_response: InitializeResponse | None = None
200
+ self._agent_info: Implementation | None = None
266
201
  self._session_id: str | None = None
267
202
  self._state: ACPSessionState | None = None
268
203
  self.deps_type = type(None)
@@ -274,6 +209,29 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
274
209
  # Track the prompt task for cancellation
275
210
  self._prompt_task: asyncio.Task[Any] | None = None
276
211
 
212
+ @classmethod
213
+ def from_config(
214
+ cls,
215
+ config: BaseACPAgentConfig,
216
+ *,
217
+ event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
218
+ input_provider: InputProvider | None = None,
219
+ agent_pool: AgentPool[Any] | None = None,
220
+ ) -> Self:
221
+ """Create an ACPAgent from a config object."""
222
+ # Merge config-level handlers with provided handlers
223
+ config_handlers = config.get_event_handlers()
224
+ merged_handlers: list[IndividualEventHandler | BuiltinEventHandlerType] = [
225
+ *config_handlers,
226
+ *(event_handlers or []),
227
+ ]
228
+ return cls(
229
+ config=config,
230
+ event_handlers=merged_handlers or None,
231
+ input_provider=input_provider,
232
+ agent_pool=agent_pool,
233
+ )
234
+
277
235
  @property
278
236
  def client_env(self) -> ExecutionEnvironment:
279
237
  """Execution environment for handling subprocess requests.
@@ -292,30 +250,31 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
292
250
 
293
251
  Args:
294
252
  data: Optional custom data to attach to the context
295
-
296
- Returns:
297
- A new AgentContext instance
298
253
  """
299
254
  from agentpool.agents.context import AgentContext
300
255
  from agentpool.models.manifest import AgentsManifest
301
256
 
302
257
  defn = self.agent_pool.manifest if self.agent_pool else AgentsManifest()
303
258
  return AgentContext(
304
- node=self, pool=self.agent_pool, config=self.config, definition=defn, data=data
259
+ node=self,
260
+ pool=self.agent_pool,
261
+ config=self.config,
262
+ definition=defn,
263
+ input_provider=self._input_provider,
264
+ data=data,
305
265
  )
306
266
 
307
267
  async def _setup_toolsets(self) -> None:
308
268
  """Initialize toolsets from config and create bridge if needed."""
309
269
  from agentpool.mcp_server.tool_bridge import BridgeConfig, ToolManagerBridge
310
270
 
311
- if not isinstance(self.config, MCPCapableACPAgentConfig) or not self.config.toolsets:
271
+ if not isinstance(self.config, MCPCapableACPAgentConfig) or not self.config.tools:
312
272
  return
313
- # Create providers from toolset configs and add to tool manager
314
- for toolset_config in self.config.toolsets:
315
- provider = toolset_config.get_provider()
273
+ # Create providers from tool configs and add to tool manager
274
+ for provider in self.config.get_tool_providers():
316
275
  self.tools.add_provider(provider)
317
276
  # Auto-create bridge to expose tools via MCP
318
- config = BridgeConfig(transport="sse", server_name=f"agentpool-{self.name}-tools")
277
+ config = BridgeConfig(server_name=f"agentpool-{self.name}-tools")
319
278
  self._tool_bridge = ToolManagerBridge(node=self, config=config)
320
279
  await self._tool_bridge.start()
321
280
  self._owns_bridge = True
@@ -327,9 +286,13 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
327
286
  """Start subprocess and initialize ACP connection."""
328
287
  await super().__aenter__()
329
288
  await self._setup_toolsets() # Setup toolsets before session creation
330
- await self._start_process()
331
- await self._initialize()
332
- await self._create_session()
289
+ process = await self._start_process()
290
+ try:
291
+ async with monitor_process(process, context="ACP initialization"):
292
+ await self._initialize()
293
+ await self._create_session()
294
+ except SubprocessError as e:
295
+ raise RuntimeError(str(e)) from e
333
296
  await anyio.sleep(0.3) # Small delay to let subprocess fully initialize
334
297
  return self
335
298
 
@@ -343,8 +306,12 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
343
306
  await self._cleanup()
344
307
  await super().__aexit__(exc_type, exc_val, exc_tb)
345
308
 
346
- async def _start_process(self) -> None:
347
- """Start the ACP server subprocess."""
309
+ async def _start_process(self) -> Process:
310
+ """Start the ACP server subprocess.
311
+
312
+ Returns:
313
+ The started Process instance
314
+ """
348
315
  prompt_manager = self.agent_pool.manifest.prompt_manager if self.agent_pool else None
349
316
  args = await self.config.get_args(prompt_manager)
350
317
  cmd = [self.config.get_command(), *args]
@@ -361,11 +328,13 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
361
328
  if not self._process.stdin or not self._process.stdout:
362
329
  msg = "Failed to create subprocess pipes"
363
330
  raise RuntimeError(msg)
331
+ return self._process
364
332
 
365
333
  async def _initialize(self) -> None:
366
334
  """Initialize the ACP connection."""
367
335
  from acp.client.connection import ClientSideConnection
368
336
  from acp.schema import InitializeRequest
337
+ from agentpool.agents.acp_agent.client_handler import ACPClientHandler
369
338
 
370
339
  if not self._process or not self._process.stdin or not self._process.stdout:
371
340
  msg = "Process not started"
@@ -382,21 +351,24 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
382
351
  input_stream=self._process.stdin,
383
352
  output_stream=self._process.stdout,
384
353
  )
354
+ pkg_meta = metadata("agentpool")
385
355
  init_request = InitializeRequest.create(
386
- title="AgentPool",
387
- version="0.1.0",
356
+ title=pkg_meta["Name"],
357
+ version=pkg_meta["Version"],
388
358
  name="agentpool",
389
359
  protocol_version=PROTOCOL_VERSION,
390
360
  terminal=self.config.allow_terminal,
391
361
  read_text_file=self.config.allow_file_operations,
392
362
  write_text_file=self.config.allow_file_operations,
393
363
  )
394
- self._init_response = await self._connection.initialize(init_request)
395
- self.log.info("ACP connection initialized", agent_info=self._init_response.agent_info)
364
+ init_response = await self._connection.initialize(init_request)
365
+ self._agent_info = init_response.agent_info
366
+ self.log.info("ACP connection initialized", agent_info=self._agent_info)
396
367
 
397
368
  async def _create_session(self) -> None:
398
369
  """Create a new ACP session with configured MCP servers."""
399
370
  from acp.schema import NewSessionRequest
371
+ from agentpool.agents.acp_agent.acp_converters import mcp_configs_to_acp
400
372
 
401
373
  if not self._connection:
402
374
  msg = "Connection not initialized"
@@ -415,6 +387,10 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
415
387
  self._session_id = response.session_id
416
388
  if self._state:
417
389
  self._state.session_id = self._session_id
390
+ # Store config_options if available (newer ACP protocol)
391
+ if response.config_options:
392
+ self._state.config_options = list(response.config_options)
393
+ # Legacy: Store models and modes for backward compatibility
418
394
  if response.models: # Store full model info from session response
419
395
  self._state.models = response.models
420
396
  self._state.current_model_id = response.models.current_model_id
@@ -422,10 +398,6 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
422
398
  model = self._state.current_model_id if self._state else None
423
399
  self.log.info("ACP session created", session_id=self._session_id, model=model)
424
400
 
425
- def add_mcp_server(self, server: McpServer) -> None:
426
- """Add an MCP server to be passed to the next session."""
427
- self._extra_mcp_servers.append(server)
428
-
429
401
  async def add_tool_bridge(self, bridge: ToolManagerBridge) -> None:
430
402
  """Add an external tool bridge to expose its tools via MCP.
431
403
 
@@ -476,61 +448,30 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
476
448
  self.log.exception("Error terminating ACP process")
477
449
  self._process = None
478
450
 
479
- async def run(
480
- self,
481
- *prompts: PromptCompatible,
482
- message_id: str | None = None,
483
- input_provider: InputProvider | None = None,
484
- message_history: MessageHistory | None = None,
485
- ) -> ChatMessage[str]:
486
- """Execute prompt against ACP agent.
487
-
488
- Args:
489
- prompts: Prompts to send (will be joined with spaces)
490
- message_id: Optional message id for the returned message
491
- input_provider: Optional input provider for permission requests
492
- message_history: Optional MessageHistory to use instead of agent's own
493
-
494
- Returns:
495
- ChatMessage containing the agent's aggregated text response
496
- """
497
- # Collect all events through run_stream
498
- final_message: ChatMessage[str] | None = None
499
- async for event in self.run_stream(
500
- *prompts,
501
- message_id=message_id,
502
- input_provider=input_provider,
503
- message_history=message_history,
504
- ):
505
- if isinstance(event, StreamCompleteEvent):
506
- final_message = event.message
507
-
508
- if final_message is None:
509
- msg = "No final message received from stream"
510
- raise RuntimeError(msg)
511
-
512
- return final_message
513
-
514
- async def run_stream( # noqa: PLR0915
451
+ async def _stream_events( # noqa: PLR0915
515
452
  self,
516
- *prompts: PromptCompatible,
453
+ prompts: list[UserContent],
454
+ *,
455
+ user_msg: ChatMessage[Any],
456
+ effective_parent_id: str | None,
517
457
  message_id: str | None = None,
458
+ conversation_id: str | None = None,
459
+ parent_id: str | None = None,
518
460
  input_provider: InputProvider | None = None,
519
461
  message_history: MessageHistory | None = None,
462
+ deps: TDeps | None = None,
463
+ event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
464
+ wait_for_connections: bool | None = None,
465
+ store_history: bool = True,
520
466
  ) -> AsyncIterator[RichAgentStreamEvent[str]]:
521
- """Stream native events as they arrive from ACP agent.
467
+ from anyenv import MultiEventHandler
522
468
 
523
- Args:
524
- prompts: Prompts to send (will be joined with spaces)
525
- message_id: Optional message id for the final message
526
- input_provider: Optional input provider for permission requests
527
- message_history: Optional MessageHistory to use instead of agent's own
528
-
529
- Yields:
530
- RichAgentStreamEvent instances converted from ACP session updates
531
- """
532
- from acp.schema import PromptRequest
469
+ from acp.schema import ForkSessionRequest, PromptRequest
533
470
  from acp.utils import to_acp_content_blocks
471
+ from agentpool.agents.acp_agent.acp_converters import (
472
+ convert_to_acp_content,
473
+ to_finish_reason,
474
+ )
534
475
 
535
476
  # Update input provider if provided
536
477
  if input_provider is not None:
@@ -541,14 +482,18 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
541
482
  msg = "Agent not initialized - use async context manager"
542
483
  raise RuntimeError(msg)
543
484
 
544
- # Capture state for use in nested function (avoids type narrowing issues)
545
- state = self._state
546
-
547
485
  conversation = message_history if message_history is not None else self.conversation
548
- # Prepare user message for history and convert to ACP content blocks
549
- user_msg, processed_prompts, _original_message = await prepare_prompts(*prompts)
486
+ # Use provided event handlers or fall back to agent's handlers
487
+ if event_handlers is not None:
488
+ handlers = resolve_event_handlers(event_handlers)
489
+ handler = MultiEventHandler[IndividualEventHandler](handlers)
490
+ else:
491
+ handler = self.event_handler
492
+
493
+ # Prepare for ACP content block conversion
494
+ processed_prompts = prompts
550
495
  run_id = str(uuid.uuid4())
551
- state.clear() # Reset state
496
+ self._state.clear() # Reset state
552
497
  # Track messages in pydantic-ai format: ModelRequest -> ModelResponse -> ...
553
498
  # This mirrors pydantic-ai's new_messages() which includes the initial user request.
554
499
  model_messages: list[ModelResponse | ModelRequest] = []
@@ -557,25 +502,33 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
557
502
  model_messages.append(initial_request)
558
503
  current_response_parts: list[TextPart | ThinkingPart | ToolCallPart] = []
559
504
  text_chunks: list[str] = [] # For final content string
560
- touched_files: set[str] = set() # Track files modified by tool calls
505
+ file_tracker = FileTracker() # Track files modified by tool calls
506
+ assert self.conversation_id is not None # Initialized by BaseAgent.run_stream()
561
507
  run_started = RunStartedEvent(
562
508
  thread_id=self.conversation_id,
563
509
  run_id=run_id,
564
510
  agent_name=self.name,
565
511
  )
566
- for handler in self.event_handler._wrapped_handlers:
567
- await handler(None, run_started)
512
+ await handler(None, run_started)
568
513
  yield run_started
569
514
  content_blocks = convert_to_acp_content(processed_prompts)
570
515
  pending_parts = conversation.get_pending_parts()
571
516
  final_blocks = [*to_acp_content_blocks(pending_parts), *content_blocks]
572
- prompt_request = PromptRequest(session_id=self._session_id, prompt=final_blocks)
573
- self.log.debug("Starting streaming prompt", num_blocks=len(final_blocks))
574
-
575
- # Reset cancellation state
576
- self._cancelled = False
577
- self._current_stream_task = asyncio.current_task()
578
517
 
518
+ # Handle ephemeral execution (fork session if store_history=False)
519
+ session_id = self._session_id
520
+ if not store_history and self._session_id:
521
+ # Fork the current session to execute without affecting main history
522
+
523
+ cwd = self.config.cwd or str(Path.cwd())
524
+ fork_request = ForkSessionRequest(session_id=self._session_id, cwd=cwd)
525
+ fork_response = await self._connection.fork_session(fork_request)
526
+ # Use the forked session ID for this prompt
527
+ session_id = fork_response.session_id
528
+ self.log.debug("Forked session", parent=self._session_id, fork=session_id)
529
+ prompt_request = PromptRequest(session_id=session_id, prompt=final_blocks)
530
+ self.log.debug("Starting streaming prompt", num_blocks=len(final_blocks))
531
+ self._cancelled = False # Reset cancellation state
579
532
  # Run prompt in background
580
533
  prompt_task = asyncio.create_task(self._connection.prompt(prompt_request))
581
534
  self._prompt_task = prompt_task
@@ -584,6 +537,7 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
584
537
  async def poll_acp_events() -> AsyncIterator[RichAgentStreamEvent[str]]:
585
538
  """Poll events from ACP state until prompt completes."""
586
539
  last_idx = 0
540
+ assert self._state
587
541
  while not prompt_task.done():
588
542
  if self._client_handler:
589
543
  try:
@@ -595,26 +549,59 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
595
549
  pass
596
550
 
597
551
  # Yield new events from state
598
- while last_idx < len(state.events):
599
- yield state.events[last_idx]
552
+ while last_idx < len(self._state.events):
553
+ yield self._state.events[last_idx]
600
554
  last_idx += 1
601
555
 
602
556
  # Yield remaining events after prompt completes
603
- while last_idx < len(state.events):
604
- yield state.events[last_idx]
557
+ while last_idx < len(self._state.events):
558
+ yield self._state.events[last_idx]
605
559
  last_idx += 1
606
560
 
561
+ # Set deps on tool bridge for access during tool invocations
562
+
563
+ # (ContextVar doesn't work because MCP server runs in a separate task)
564
+ if self._tool_bridge:
565
+ self._tool_bridge.current_deps = deps
566
+
567
+ # Accumulate metadata events by tool_call_id (workaround for MCP stripping _meta)
568
+ tool_metadata: dict[str, dict[str, Any]] = {}
569
+
607
570
  # Merge ACP events with custom events from queue
608
571
  try:
609
572
  async with merge_queue_into_iterator(
610
573
  poll_acp_events(), self._event_queue
611
574
  ) as merged_events:
612
- async for event in merged_events:
575
+ async for event in file_tracker(merged_events):
576
+ # Capture metadata events for correlation with tool results
577
+ from agentpool.agents.events import ToolResultMetadataEvent
578
+
579
+ if isinstance(event, ToolResultMetadataEvent):
580
+ tool_metadata[event.tool_call_id] = event.metadata
581
+ # Don't yield metadata events - they're internal correlation only
582
+ continue
583
+
613
584
  # Check for cancellation
614
585
  if self._cancelled:
615
586
  self.log.info("Stream cancelled by user")
616
587
  break
617
588
 
589
+ # Inject metadata into ToolCallCompleteEvent
590
+ # (converted from completed ToolCallProgress)
591
+ if isinstance(event, ToolCallCompleteEvent):
592
+ # Enrich with agent name and metadata from our accumulator
593
+ enriched_event = event
594
+ if not enriched_event.agent_name:
595
+ enriched_event = replace(enriched_event, agent_name=self.name)
596
+ if (
597
+ enriched_event.metadata is None
598
+ and enriched_event.tool_call_id in tool_metadata
599
+ ):
600
+ enriched_event = replace(
601
+ enriched_event, metadata=tool_metadata[enriched_event.tool_call_id]
602
+ )
603
+ event = enriched_event # noqa: PLW2901
604
+
618
605
  # Extract content from events and build parts in arrival order
619
606
  match event:
620
607
  case PartDeltaEvent(delta=TextPartDelta(content_delta=delta)):
@@ -628,134 +615,142 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
628
615
  current_response_parts.append(
629
616
  ToolCallPart(tool_name=tc_name, args=tc_input, tool_call_id=tc_id)
630
617
  )
631
- # Track files modified by write/edit tools
632
- if file_path := extract_file_path_from_tool_call(
633
- tc_name or "", tc_input or {}
634
- ):
635
- touched_files.add(file_path)
636
-
637
- # Distribute to handlers
638
- for handler in self.event_handler._wrapped_handlers:
639
- await handler(None, event)
618
+
619
+ await handler(None, event)
640
620
  yield event
641
621
  except asyncio.CancelledError:
642
622
  self.log.info("Stream cancelled via task cancellation")
643
623
  self._cancelled = True
624
+ finally:
625
+ # Clear deps from tool bridge
626
+ if self._tool_bridge:
627
+ self._tool_bridge.current_deps = None
644
628
 
645
629
  # Handle cancellation - emit partial message
646
630
  if self._cancelled:
647
- text_content = "".join(text_chunks)
648
- metadata: SimpleJsonType = {}
649
- if touched_files:
650
- metadata["touched_files"] = sorted(touched_files)
651
631
  message = ChatMessage[str](
652
- content=text_content,
632
+ content="".join(text_chunks),
653
633
  role="assistant",
654
634
  name=self.name,
655
635
  message_id=message_id or str(uuid.uuid4()),
656
636
  conversation_id=self.conversation_id,
637
+ parent_id=user_msg.message_id,
657
638
  model_name=self.model_name,
658
639
  messages=model_messages,
659
- metadata=metadata,
640
+ metadata=file_tracker.get_metadata(),
660
641
  finish_reason="stop",
661
642
  )
662
643
  complete_event = StreamCompleteEvent(message=message)
663
- for handler in self.event_handler._wrapped_handlers:
664
- await handler(None, complete_event)
644
+ await handler(None, complete_event)
665
645
  yield complete_event
666
- self._current_stream_task = None
667
646
  self._prompt_task = None
668
647
  return
669
648
 
670
649
  # Ensure we catch any exceptions from the prompt task
671
650
  response = await prompt_task
672
- finish_reason: FinishReason = STOP_REASON_MAP.get(response.stop_reason, "stop")
651
+ finish_reason = to_finish_reason(response.stop_reason)
673
652
  # Flush response parts to model_messages
674
653
  if current_response_parts:
675
- model_messages.append(ModelResponse(parts=current_response_parts))
654
+ model_messages.append(
655
+ ModelResponse(
656
+ parts=current_response_parts,
657
+ finish_reason=finish_reason,
658
+ model_name=self.model_name,
659
+ provider_name=self.config.type,
660
+ )
661
+ )
676
662
 
677
663
  text_content = "".join(text_chunks)
678
- # Build metadata with touched files if any
679
- metadata = {}
680
- if touched_files:
681
- metadata["touched_files"] = sorted(touched_files)
664
+ # Calculate approximate token usage from what we can observe
665
+ input_parts = [*processed_prompts, *pending_parts]
666
+ usage, cost_info = await calculate_usage_from_parts(
667
+ input_parts=input_parts,
668
+ response_parts=current_response_parts,
669
+ text_content=text_content,
670
+ model_name=self.model_name,
671
+ provider=self.config.type,
672
+ )
673
+
682
674
  message = ChatMessage[str](
683
675
  content=text_content,
684
676
  role="assistant",
685
677
  name=self.name,
686
678
  message_id=message_id or str(uuid.uuid4()),
687
679
  conversation_id=self.conversation_id,
680
+ parent_id=user_msg.message_id,
688
681
  model_name=self.model_name,
689
682
  messages=model_messages,
690
- metadata=metadata,
683
+ metadata=file_tracker.get_metadata(),
691
684
  finish_reason=finish_reason,
685
+ usage=usage,
686
+ cost_info=cost_info,
692
687
  )
693
688
  complete_event = StreamCompleteEvent(message=message)
694
- for handler in self.event_handler._wrapped_handlers:
695
- await handler(None, complete_event)
696
- yield complete_event # Emit final StreamCompleteEvent with aggregated message
697
- self.message_sent.emit(message)
698
- conversation.add_chat_messages([user_msg, message]) # Record to conversation history
699
-
700
- async def run_iter(
701
- self,
702
- *prompt_groups: Sequence[PromptCompatible],
703
- ) -> AsyncIterator[ChatMessage[str]]:
704
- """Run agent sequentially on multiple prompt groups.
705
-
706
- Args:
707
- prompt_groups: Groups of prompts to process sequentially
708
-
709
- Yields:
710
- Response messages in sequence
711
- """
712
- for prompts in prompt_groups:
713
- response = await self.run(*prompts)
714
- yield response
689
+ await handler(None, complete_event)
690
+ yield complete_event # Emit final StreamCompleteEvent - post-processing handled by base
715
691
 
716
692
  @property
717
693
  def model_name(self) -> str | None:
718
694
  """Get the model name in a consistent format."""
719
- if self._state and self._state.current_model_id:
720
- return self._state.current_model_id
721
- if self._init_response and self._init_response.agent_info:
722
- return self._init_response.agent_info.name
723
- return None
695
+ return model_id if self._state and (model_id := self._state.current_model_id) else None
724
696
 
725
697
  async def set_model(self, model: str) -> None:
726
- """Update the model and restart the ACP agent process.
698
+ """Update the model for the current session via ACP protocol.
699
+
700
+ Attempts to use the ACP protocol to change the model:
701
+ 1. If config_options exist with a 'model' category, use set_session_config_option
702
+ 2. Otherwise, use legacy set_session_model API
727
703
 
728
704
  Args:
729
- model: New model name to use
705
+ model: New model ID to use
730
706
 
731
707
  Raises:
732
- ValueError: If the config doesn't have a model field
733
- RuntimeError: If agent is currently processing (has active process but no session)
708
+ RuntimeError: If no active session or remote agent doesn't support model changes
734
709
  """
735
- # TODO: Once ACP protocol stabilizes, use set_session_model instead of restart
736
- # from acp.schema import SetSessionModelRequest # UNSTABLE
737
- # if self._connection and self._session_id:
738
- # request = SetSessionModelRequest(session_id=self._session_id, model_id=model)
739
- # await self._connection.set_session_model(request)
740
- # if self._state:
741
- # self._state.current_model_id = model
742
- # self.log.info("Model changed via ACP protocol", model=model)
743
- # return
744
-
745
- if not hasattr(self.config, "model"):
746
- msg = f"Config type {type(self.config).__name__} doesn't support model changes"
747
- raise ValueError(msg)
748
- # Prevent changes during active processing
749
- if self._process and not self._session_id:
750
- msg = "Cannot change model while agent is initializing"
710
+ from acp.schema import SetSessionConfigOptionRequest, SetSessionModelRequest
711
+
712
+ if not self._connection or not self._session_id:
713
+ msg = "Cannot set model: no active session"
751
714
  raise RuntimeError(msg)
752
- # Create new config with updated model
753
- new_config = self.config.model_copy(update={"model": model})
754
- if self._process: # Clean up existing process if any
755
- await self._cleanup()
756
- self.config = new_config # Update config and restart
757
- await self._start_process()
758
- await self._initialize()
715
+
716
+ if not self._state:
717
+ msg = "Cannot set model: no session state"
718
+ raise RuntimeError(msg)
719
+
720
+ # Try using the new unified config options API first
721
+ model_cfg = next((i for i in self._state.config_options if i.category == "model"), None)
722
+ if model_cfg:
723
+ # Use new unified API
724
+ request = SetSessionConfigOptionRequest(
725
+ session_id=self._session_id,
726
+ config_id=model_cfg.id,
727
+ value=model,
728
+ )
729
+ response = await self._connection.set_session_config_option(request)
730
+ if response:
731
+ # Update entire config_options state from response
732
+ self._state.config_options = list(response.config_options)
733
+ self.log.info("Model changed via SessionConfigOption", model=model)
734
+ return
735
+ msg = "set_session_config_option returned no response"
736
+ raise RuntimeError(msg)
737
+
738
+ # Fallback to legacy set_session_model API
739
+ request_legacy = SetSessionModelRequest(session_id=self._session_id, model_id=model)
740
+ response_legacy = await self._connection.set_session_model(request_legacy)
741
+ if response_legacy:
742
+ # Update legacy state
743
+ self._state.current_model_id = model
744
+ self.log.info("Model changed via legacy set_session_model", model=model)
745
+ return
746
+
747
+ # If we get here, the remote agent doesn't support model changes
748
+ msg = (
749
+ "Remote ACP agent does not support model changes. "
750
+ "No config_options with category='model' found and set_session_model "
751
+ "returned no response."
752
+ )
753
+ raise RuntimeError(msg)
759
754
 
760
755
  async def set_tool_confirmation_mode(self, mode: ToolConfirmationMode) -> None:
761
756
  """Set the tool confirmation mode for this agent.
@@ -790,10 +785,6 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
790
785
  else:
791
786
  self.log.info("Tool confirmation mode changed (local only)", mode=mode)
792
787
 
793
- async def get_stats(self) -> MessageStats:
794
- """Get message statistics."""
795
- return MessageStats(messages=list(self.conversation.chat_messages))
796
-
797
788
  async def interrupt(self) -> None:
798
789
  """Interrupt the currently running stream.
799
790
 
@@ -822,14 +813,153 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
822
813
  if self._current_stream_task and not self._current_stream_task.done():
823
814
  self._current_stream_task.cancel()
824
815
 
816
+ async def get_available_models(self) -> list[ModelInfo] | None:
817
+ """Get available models from the ACP session state.
818
+
819
+ Converts ACP ModelInfo to tokonomics ModelInfo format.
820
+
821
+ Returns:
822
+ List of tokonomics ModelInfo, or None if not available
823
+ """
824
+ from tokonomics.model_discovery.model_info import ModelInfo
825
+
826
+ if not self._state or not self._state.models:
827
+ return None
828
+
829
+ # Convert ACP ModelInfo to tokonomics ModelInfo
830
+ result: list[ModelInfo] = []
831
+ for acp_model in self._state.models.available_models:
832
+ toko_model = ModelInfo(
833
+ id=acp_model.model_id,
834
+ name=acp_model.name,
835
+ description=acp_model.description,
836
+ )
837
+ result.append(toko_model)
838
+ return result
839
+
840
+ async def get_modes(self) -> list[ModeCategory]:
841
+ """Get available modes from the ACP session state.
842
+
843
+ Passthrough from remote ACP server's mode and model state.
844
+ Prefers new config_options format, falls back to legacy modes/models.
845
+
846
+ Returns:
847
+ List of ModeCategory from remote server, empty if not available
848
+ """
849
+ from agentpool.agents.acp_agent.acp_converters import get_modes
850
+
851
+ if not self._state:
852
+ return []
853
+
854
+ # Prefer new SessionConfigOption format if available
855
+ return get_modes(
856
+ self._state.config_options,
857
+ available_modes=self._state.modes,
858
+ available_models=self._state.models,
859
+ )
860
+
861
+ async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
862
+ """Set a mode on the remote ACP server.
863
+
864
+ For ACPAgent, this forwards the mode/model change to the remote ACP server.
865
+ Prefers new set_session_config_option if config_options are available,
866
+ falls back to legacy set_session_mode/set_session_model.
867
+
868
+ Args:
869
+ mode: The mode to set - ModeInfo object or mode ID string
870
+ category_id: Category ID (config option ID)
871
+
872
+ Raises:
873
+ RuntimeError: If not connected to ACP server
874
+ ValueError: If mode is not available
875
+ """
876
+ from acp.schema import (
877
+ SetSessionConfigOptionRequest,
878
+ SetSessionModelRequest,
879
+ SetSessionModeRequest,
880
+ )
881
+
882
+ # Extract mode_id and category from ModeInfo if provided
883
+ if isinstance(mode, ModeInfo):
884
+ mode_id = mode.id
885
+ category_id = category_id or mode.category_id
886
+ else:
887
+ mode_id = mode
888
+
889
+ if not self._connection or not self._session_id or not self._state:
890
+ msg = "Not connected to ACP server"
891
+ raise RuntimeError(msg)
892
+
893
+ # Validate mode is available
894
+ available_modes = await self.get_modes()
895
+ matching_category = (
896
+ next((c for c in available_modes if c.id == category_id), None) if category_id else None
897
+ )
898
+
899
+ if matching_category:
900
+ valid_ids = {m.id for m in matching_category.available_modes}
901
+ if mode_id not in valid_ids:
902
+ msg = f"Unknown {category_id}: {mode_id}. Available: {valid_ids}"
903
+ raise ValueError(msg)
904
+ elif category_id:
905
+ # Category specified but not found
906
+ available_cats = {c.id for c in available_modes}
907
+ msg = f"Unknown category: {category_id}. Available: {available_cats}"
908
+ raise ValueError(msg)
909
+ else:
910
+ # No category specified and no match found
911
+ msg = "category_id is required when mode is a string"
912
+ raise ValueError(msg)
913
+
914
+ # Prefer new config_options API if available
915
+ if self._state.config_options:
916
+ assert category_id
917
+ config_request = SetSessionConfigOptionRequest(
918
+ session_id=self._session_id,
919
+ config_id=category_id,
920
+ value=mode_id,
921
+ )
922
+ response = await self._connection.set_session_config_option(config_request)
923
+ # Update local state from response
924
+ if response.config_options:
925
+ self._state.config_options = list(response.config_options)
926
+
927
+ self.log.info("ACP server Config option changed", config_id=category_id, value=mode_id)
928
+ return
929
+
930
+ # Legacy: Use old set_session_mode/set_session_model APIs
931
+ if category_id == "permissions":
932
+ mode_request = SetSessionModeRequest(session_id=self._session_id, mode_id=mode_id)
933
+ await self._connection.set_session_mode(mode_request)
934
+
935
+ # Update local state
936
+ if self._state.modes:
937
+ self._state.modes.current_mode_id = mode_id
938
+
939
+ self.log.info("Mode changed on remote ACP server (legacy)", mode_id=mode_id)
940
+
941
+ elif category_id == "model":
942
+ model_request = SetSessionModelRequest(session_id=self._session_id, model_id=mode_id)
943
+ await self._connection.set_session_model(model_request)
944
+
945
+ # Update local state
946
+ if self._state.models:
947
+ self._state.models.current_model_id = mode_id
948
+
949
+ self.log.info("Model changed on remote ACP server (legacy)", model_id=mode_id)
950
+
951
+ else:
952
+ msg = f"Unknown category: {category_id}. Available: permissions, model"
953
+ raise ValueError(msg)
954
+
825
955
 
826
956
  if __name__ == "__main__":
957
+ from agentpool.models.acp_agents import ACPAgentConfig
827
958
 
828
959
  async def main() -> None:
829
960
  """Demo: Basic call to an ACP agent."""
830
- args = ["run", "agentpool", "serve-acp", "--model-provider", "openai"]
831
- cwd = str(Path.cwd())
832
- async with ACPAgent(command="uv", args=args, cwd=cwd, event_handlers=["detailed"]) as agent:
961
+ config = ACPAgentConfig(command="uv", args=["run", "agentpool", "serve-acp"])
962
+ async with ACPAgent(config=config, event_handlers=["detailed"]) as agent:
833
963
  print("Response (streaming): ", end="", flush=True)
834
964
  async for chunk in agent.run_stream("Say hello briefly."):
835
965
  print(chunk, end="", flush=True)