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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. acp/__init__.py +0 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/client/connection.py +38 -29
  7. acp/client/implementations/default_client.py +3 -2
  8. acp/client/implementations/headless_client.py +2 -2
  9. acp/connection.py +2 -2
  10. acp/notifications.py +18 -49
  11. acp/schema/__init__.py +2 -0
  12. acp/schema/agent_responses.py +21 -0
  13. acp/schema/client_requests.py +3 -3
  14. acp/schema/session_state.py +63 -29
  15. acp/task/supervisor.py +2 -2
  16. acp/utils.py +2 -2
  17. agentpool/__init__.py +2 -0
  18. agentpool/agents/acp_agent/acp_agent.py +278 -263
  19. agentpool/agents/acp_agent/acp_converters.py +150 -17
  20. agentpool/agents/acp_agent/client_handler.py +35 -24
  21. agentpool/agents/acp_agent/session_state.py +14 -6
  22. agentpool/agents/agent.py +471 -643
  23. agentpool/agents/agui_agent/agui_agent.py +104 -107
  24. agentpool/agents/agui_agent/helpers.py +3 -4
  25. agentpool/agents/base_agent.py +485 -32
  26. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  27. agentpool/agents/claude_code_agent/__init__.py +13 -1
  28. agentpool/agents/claude_code_agent/claude_code_agent.py +654 -334
  29. agentpool/agents/claude_code_agent/converters.py +4 -141
  30. agentpool/agents/claude_code_agent/models.py +77 -0
  31. agentpool/agents/claude_code_agent/static_info.py +100 -0
  32. agentpool/agents/claude_code_agent/usage.py +242 -0
  33. agentpool/agents/events/__init__.py +22 -0
  34. agentpool/agents/events/builtin_handlers.py +65 -0
  35. agentpool/agents/events/event_emitter.py +3 -0
  36. agentpool/agents/events/events.py +84 -3
  37. agentpool/agents/events/infer_info.py +145 -0
  38. agentpool/agents/events/processors.py +254 -0
  39. agentpool/agents/interactions.py +41 -6
  40. agentpool/agents/modes.py +13 -0
  41. agentpool/agents/slashed_agent.py +5 -4
  42. agentpool/agents/tool_wrapping.py +18 -6
  43. agentpool/common_types.py +35 -21
  44. agentpool/config_resources/acp_assistant.yml +2 -2
  45. agentpool/config_resources/agents.yml +3 -0
  46. agentpool/config_resources/agents_template.yml +1 -0
  47. agentpool/config_resources/claude_code_agent.yml +9 -8
  48. agentpool/config_resources/external_acp_agents.yml +2 -1
  49. agentpool/delegation/base_team.py +4 -30
  50. agentpool/delegation/pool.py +104 -265
  51. agentpool/delegation/team.py +57 -57
  52. agentpool/delegation/teamrun.py +50 -55
  53. agentpool/functional/run.py +10 -4
  54. agentpool/mcp_server/client.py +73 -38
  55. agentpool/mcp_server/conversions.py +54 -13
  56. agentpool/mcp_server/manager.py +9 -23
  57. agentpool/mcp_server/registries/official_registry_client.py +10 -1
  58. agentpool/mcp_server/tool_bridge.py +114 -79
  59. agentpool/messaging/connection_manager.py +11 -10
  60. agentpool/messaging/event_manager.py +5 -5
  61. agentpool/messaging/message_container.py +6 -30
  62. agentpool/messaging/message_history.py +87 -8
  63. agentpool/messaging/messagenode.py +52 -14
  64. agentpool/messaging/messages.py +2 -26
  65. agentpool/messaging/processing.py +10 -22
  66. agentpool/models/__init__.py +1 -1
  67. agentpool/models/acp_agents/base.py +6 -2
  68. agentpool/models/acp_agents/mcp_capable.py +124 -15
  69. agentpool/models/acp_agents/non_mcp.py +0 -23
  70. agentpool/models/agents.py +66 -66
  71. agentpool/models/agui_agents.py +1 -1
  72. agentpool/models/claude_code_agents.py +111 -17
  73. agentpool/models/file_parsing.py +0 -1
  74. agentpool/models/manifest.py +70 -50
  75. agentpool/prompts/conversion_manager.py +1 -1
  76. agentpool/prompts/prompts.py +5 -2
  77. agentpool/resource_providers/__init__.py +2 -0
  78. agentpool/resource_providers/aggregating.py +4 -2
  79. agentpool/resource_providers/base.py +13 -3
  80. agentpool/resource_providers/codemode/code_executor.py +72 -5
  81. agentpool/resource_providers/codemode/helpers.py +2 -2
  82. agentpool/resource_providers/codemode/provider.py +64 -12
  83. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  84. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  85. agentpool/resource_providers/filtering.py +3 -1
  86. agentpool/resource_providers/mcp_provider.py +66 -12
  87. agentpool/resource_providers/plan_provider.py +111 -18
  88. agentpool/resource_providers/pool.py +5 -3
  89. agentpool/resource_providers/resource_info.py +111 -0
  90. agentpool/resource_providers/static.py +2 -2
  91. agentpool/sessions/__init__.py +2 -0
  92. agentpool/sessions/manager.py +2 -3
  93. agentpool/sessions/models.py +9 -6
  94. agentpool/sessions/protocol.py +28 -0
  95. agentpool/sessions/session.py +11 -55
  96. agentpool/storage/manager.py +361 -54
  97. agentpool/talk/registry.py +4 -4
  98. agentpool/talk/talk.py +9 -10
  99. agentpool/testing.py +1 -1
  100. agentpool/tool_impls/__init__.py +6 -0
  101. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  102. agentpool/tool_impls/agent_cli/tool.py +95 -0
  103. agentpool/tool_impls/bash/__init__.py +64 -0
  104. agentpool/tool_impls/bash/helpers.py +35 -0
  105. agentpool/tool_impls/bash/tool.py +171 -0
  106. agentpool/tool_impls/delete_path/__init__.py +70 -0
  107. agentpool/tool_impls/delete_path/tool.py +142 -0
  108. agentpool/tool_impls/download_file/__init__.py +80 -0
  109. agentpool/tool_impls/download_file/tool.py +183 -0
  110. agentpool/tool_impls/execute_code/__init__.py +55 -0
  111. agentpool/tool_impls/execute_code/tool.py +163 -0
  112. agentpool/tool_impls/grep/__init__.py +80 -0
  113. agentpool/tool_impls/grep/tool.py +200 -0
  114. agentpool/tool_impls/list_directory/__init__.py +73 -0
  115. agentpool/tool_impls/list_directory/tool.py +197 -0
  116. agentpool/tool_impls/question/__init__.py +42 -0
  117. agentpool/tool_impls/question/tool.py +127 -0
  118. agentpool/tool_impls/read/__init__.py +104 -0
  119. agentpool/tool_impls/read/tool.py +305 -0
  120. agentpool/tools/__init__.py +2 -1
  121. agentpool/tools/base.py +114 -34
  122. agentpool/tools/manager.py +57 -1
  123. agentpool/ui/base.py +2 -2
  124. agentpool/ui/mock_provider.py +2 -2
  125. agentpool/ui/stdlib_provider.py +2 -2
  126. agentpool/utils/streams.py +21 -96
  127. agentpool/vfs_registry.py +7 -2
  128. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
  129. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
  130. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  131. agentpool_cli/__main__.py +20 -0
  132. agentpool_cli/create.py +1 -1
  133. agentpool_cli/serve_acp.py +59 -1
  134. agentpool_cli/serve_opencode.py +1 -1
  135. agentpool_cli/ui.py +557 -0
  136. agentpool_commands/__init__.py +12 -5
  137. agentpool_commands/agents.py +1 -1
  138. agentpool_commands/pool.py +260 -0
  139. agentpool_commands/session.py +1 -1
  140. agentpool_commands/text_sharing/__init__.py +119 -0
  141. agentpool_commands/text_sharing/base.py +123 -0
  142. agentpool_commands/text_sharing/github_gist.py +80 -0
  143. agentpool_commands/text_sharing/opencode.py +462 -0
  144. agentpool_commands/text_sharing/paste_rs.py +59 -0
  145. agentpool_commands/text_sharing/pastebin.py +116 -0
  146. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  147. agentpool_commands/utils.py +31 -32
  148. agentpool_config/__init__.py +30 -2
  149. agentpool_config/agentpool_tools.py +498 -0
  150. agentpool_config/converters.py +1 -1
  151. agentpool_config/event_handlers.py +42 -0
  152. agentpool_config/events.py +1 -1
  153. agentpool_config/forward_targets.py +1 -4
  154. agentpool_config/jinja.py +3 -3
  155. agentpool_config/mcp_server.py +1 -5
  156. agentpool_config/nodes.py +1 -1
  157. agentpool_config/observability.py +44 -0
  158. agentpool_config/session.py +0 -3
  159. agentpool_config/storage.py +38 -39
  160. agentpool_config/task.py +3 -3
  161. agentpool_config/tools.py +11 -28
  162. agentpool_config/toolsets.py +22 -90
  163. agentpool_server/a2a_server/agent_worker.py +307 -0
  164. agentpool_server/a2a_server/server.py +23 -18
  165. agentpool_server/acp_server/acp_agent.py +125 -56
  166. agentpool_server/acp_server/commands/acp_commands.py +46 -216
  167. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
  168. agentpool_server/acp_server/event_converter.py +651 -0
  169. agentpool_server/acp_server/input_provider.py +53 -10
  170. agentpool_server/acp_server/server.py +1 -11
  171. agentpool_server/acp_server/session.py +90 -410
  172. agentpool_server/acp_server/session_manager.py +8 -34
  173. agentpool_server/agui_server/server.py +3 -1
  174. agentpool_server/mcp_server/server.py +5 -2
  175. agentpool_server/opencode_server/ENDPOINTS.md +53 -14
  176. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  177. agentpool_server/opencode_server/__init__.py +0 -8
  178. agentpool_server/opencode_server/converters.py +132 -26
  179. agentpool_server/opencode_server/input_provider.py +160 -8
  180. agentpool_server/opencode_server/models/__init__.py +42 -20
  181. agentpool_server/opencode_server/models/app.py +12 -0
  182. agentpool_server/opencode_server/models/events.py +203 -29
  183. agentpool_server/opencode_server/models/mcp.py +19 -0
  184. agentpool_server/opencode_server/models/message.py +18 -1
  185. agentpool_server/opencode_server/models/parts.py +134 -1
  186. agentpool_server/opencode_server/models/question.py +56 -0
  187. agentpool_server/opencode_server/models/session.py +13 -1
  188. agentpool_server/opencode_server/routes/__init__.py +4 -0
  189. agentpool_server/opencode_server/routes/agent_routes.py +33 -2
  190. agentpool_server/opencode_server/routes/app_routes.py +66 -3
  191. agentpool_server/opencode_server/routes/config_routes.py +66 -5
  192. agentpool_server/opencode_server/routes/file_routes.py +184 -5
  193. agentpool_server/opencode_server/routes/global_routes.py +1 -1
  194. agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
  195. agentpool_server/opencode_server/routes/message_routes.py +122 -66
  196. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  197. agentpool_server/opencode_server/routes/pty_routes.py +23 -22
  198. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  199. agentpool_server/opencode_server/routes/session_routes.py +139 -68
  200. agentpool_server/opencode_server/routes/tui_routes.py +1 -1
  201. agentpool_server/opencode_server/server.py +47 -2
  202. agentpool_server/opencode_server/state.py +30 -0
  203. agentpool_storage/__init__.py +0 -4
  204. agentpool_storage/base.py +81 -2
  205. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  206. agentpool_storage/claude_provider/__init__.py +42 -0
  207. agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
  208. agentpool_storage/file_provider.py +149 -15
  209. agentpool_storage/memory_provider.py +132 -12
  210. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  211. agentpool_storage/opencode_provider/__init__.py +16 -0
  212. agentpool_storage/opencode_provider/helpers.py +414 -0
  213. agentpool_storage/opencode_provider/provider.py +895 -0
  214. agentpool_storage/session_store.py +20 -6
  215. agentpool_storage/sql_provider/sql_provider.py +135 -2
  216. agentpool_storage/sql_provider/utils.py +2 -12
  217. agentpool_storage/zed_provider/__init__.py +16 -0
  218. agentpool_storage/zed_provider/helpers.py +281 -0
  219. agentpool_storage/zed_provider/models.py +130 -0
  220. agentpool_storage/zed_provider/provider.py +442 -0
  221. agentpool_storage/zed_provider.py +803 -0
  222. agentpool_toolsets/__init__.py +0 -2
  223. agentpool_toolsets/builtin/__init__.py +2 -4
  224. agentpool_toolsets/builtin/code.py +4 -4
  225. agentpool_toolsets/builtin/debug.py +115 -40
  226. agentpool_toolsets/builtin/execution_environment.py +54 -165
  227. agentpool_toolsets/builtin/skills.py +0 -77
  228. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  229. agentpool_toolsets/builtin/workers.py +4 -2
  230. agentpool_toolsets/composio_toolset.py +2 -2
  231. agentpool_toolsets/entry_points.py +3 -1
  232. agentpool_toolsets/fsspec_toolset/grep.py +25 -5
  233. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  234. agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
  235. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  236. agentpool_toolsets/mcp_discovery/toolset.py +74 -17
  237. agentpool_toolsets/mcp_run_toolset.py +8 -11
  238. agentpool_toolsets/notifications.py +33 -33
  239. agentpool_toolsets/openapi.py +3 -1
  240. agentpool_toolsets/search_toolset.py +3 -1
  241. agentpool_config/resources.py +0 -33
  242. agentpool_server/acp_server/acp_tools.py +0 -43
  243. agentpool_server/acp_server/commands/spawn.py +0 -210
  244. agentpool_storage/opencode_provider.py +0 -730
  245. agentpool_storage/text_log_provider.py +0 -276
  246. agentpool_toolsets/builtin/chain.py +0 -288
  247. agentpool_toolsets/builtin/user_interaction.py +0 -52
  248. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  249. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  250. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,20 +49,24 @@ 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
53
62
  from agentpool.agents.modes import ModeInfo
63
+ from agentpool.common_types import (
64
+ IndividualEventHandler,
65
+ )
54
66
  from agentpool.log import get_logger
55
67
  from agentpool.messaging import ChatMessage
56
- from agentpool.messaging.processing import prepare_prompts
57
68
  from agentpool.models.acp_agents import ACPAgentConfig, MCPCapableACPAgentConfig
58
- from agentpool.utils.streams import (
59
- FileTracker,
60
- merge_queue_into_iterator,
61
- )
69
+ from agentpool.utils.streams import merge_queue_into_iterator
62
70
  from agentpool.utils.subprocess_utils import SubprocessError, monitor_process
63
71
  from agentpool.utils.token_breakdown import calculate_usage_from_parts
64
72
 
@@ -68,9 +76,9 @@ if TYPE_CHECKING:
68
76
  from types import TracebackType
69
77
 
70
78
  from anyio.abc import Process
71
- from evented.configs import EventConfig
79
+ from evented_config import EventConfig
72
80
  from exxec import ExecutionEnvironment
73
- from pydantic_ai import FinishReason
81
+ from pydantic_ai import UserContent
74
82
  from slashed import BaseCommand
75
83
  from tokonomics.model_discovery.model_info import ModelInfo
76
84
 
@@ -78,20 +86,17 @@ if TYPE_CHECKING:
78
86
  from acp.client.connection import ClientSideConnection
79
87
  from acp.client.protocol import Client
80
88
  from acp.schema import (
81
- InitializeResponse,
89
+ Implementation,
82
90
  RequestPermissionRequest,
83
91
  RequestPermissionResponse,
84
- StopReason,
85
92
  )
86
93
  from acp.schema.mcp import McpServer
87
94
  from agentpool.agents import AgentContext
95
+ from agentpool.agents.acp_agent.client_handler import ACPClientHandler
88
96
  from agentpool.agents.events import RichAgentStreamEvent
89
97
  from agentpool.agents.modes import ModeCategory
90
98
  from agentpool.common_types import (
91
99
  BuiltinEventHandlerType,
92
- IndividualEventHandler,
93
- PromptCompatible,
94
- SimpleJsonType,
95
100
  )
96
101
  from agentpool.delegation import AgentPool
97
102
  from agentpool.mcp_server.tool_bridge import ToolManagerBridge
@@ -104,14 +109,6 @@ logger = get_logger(__name__)
104
109
 
105
110
  PROTOCOL_VERSION = 1
106
111
 
107
- STOP_REASON_MAP: dict[StopReason, FinishReason] = {
108
- "end_turn": "stop",
109
- "max_tokens": "length",
110
- "max_turn_requests": "length",
111
- "refusal": "content_filter",
112
- "cancelled": "error",
113
- }
114
-
115
112
 
116
113
  class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
117
114
  """MessageNode that wraps an external ACP agent subprocess.
@@ -127,58 +124,17 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
127
124
 
128
125
  Supports both blocking `run()` and streaming `run_iter()` execution modes.
129
126
 
130
- Example with config:
127
+ Example:
131
128
  ```python
129
+ # From config
132
130
  config = ClaudeACPAgentConfig(cwd="/project", model="sonnet")
133
- agent = ACPAgent(config, agent_pool=pool)
134
- ```
131
+ agent = ACPAgent(config=config, agent_pool=pool)
135
132
 
136
- Example with kwargs:
137
- ```python
138
- agent = ACPAgent(
139
- command="claude-code-acp",
140
- cwd="/project",
141
- providers=["anthropic"],
142
- )
133
+ # From kwargs
134
+ agent = ACPAgent(command="claude-code-acp", cwd="/project")
143
135
  ```
144
136
  """
145
137
 
146
- @overload
147
- def __init__(
148
- self,
149
- *,
150
- config: BaseACPAgentConfig,
151
- input_provider: InputProvider | None = None,
152
- agent_pool: AgentPool[Any] | None = None,
153
- enable_logging: bool = True,
154
- event_configs: Sequence[EventConfig] | None = None,
155
- event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
156
- commands: Sequence[BaseCommand] | None = None,
157
- ) -> None: ...
158
-
159
- @overload
160
- def __init__(
161
- self,
162
- *,
163
- command: str,
164
- name: str | None = None,
165
- description: str | None = None,
166
- display_name: str | None = None,
167
- args: list[str] | None = None,
168
- cwd: str | None = None,
169
- env_vars: dict[str, str] | None = None,
170
- env: ExecutionEnvironment | None = None,
171
- allow_file_operations: bool = True,
172
- allow_terminal: bool = True,
173
- input_provider: InputProvider | None = None,
174
- agent_pool: AgentPool[Any] | None = None,
175
- enable_logging: bool = True,
176
- event_configs: Sequence[EventConfig] | None = None,
177
- event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
178
- tool_confirmation_mode: ToolConfirmationMode = "always",
179
- commands: Sequence[BaseCommand] | None = None,
180
- ) -> None: ...
181
-
182
138
  def __init__(
183
139
  self,
184
140
  *,
@@ -190,7 +146,6 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
190
146
  args: list[str] | None = None,
191
147
  cwd: str | None = None,
192
148
  env_vars: dict[str, str] | None = None,
193
- env: ExecutionEnvironment | None = None,
194
149
  allow_file_operations: bool = True,
195
150
  allow_terminal: bool = True,
196
151
  input_provider: InputProvider | None = None,
@@ -222,12 +177,12 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
222
177
  super().__init__(
223
178
  name=name or config.name or config.get_command(),
224
179
  description=description or config.description,
225
- display_name=display_name,
180
+ display_name=display_name or config.display_name,
226
181
  mcp_servers=config.mcp_servers,
227
182
  agent_pool=agent_pool,
228
183
  enable_logging=enable_logging,
229
184
  event_configs=event_configs or list(config.triggers),
230
- env=env or config.get_execution_environment(),
185
+ env=config.get_execution_environment(),
231
186
  input_provider=input_provider,
232
187
  tool_confirmation_mode=tool_confirmation_mode,
233
188
  event_handlers=event_handlers,
@@ -242,7 +197,7 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
242
197
  self._process: Process | None = None
243
198
  self._connection: ClientSideConnection | None = None
244
199
  self._client_handler: ACPClientHandler | None = None
245
- self._init_response: InitializeResponse | None = None
200
+ self._agent_info: Implementation | None = None
246
201
  self._session_id: str | None = None
247
202
  self._state: ACPSessionState | None = None
248
203
  self.deps_type = type(None)
@@ -254,6 +209,29 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
254
209
  # Track the prompt task for cancellation
255
210
  self._prompt_task: asyncio.Task[Any] | None = None
256
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
+
257
235
  @property
258
236
  def client_env(self) -> ExecutionEnvironment:
259
237
  """Execution environment for handling subprocess requests.
@@ -272,30 +250,31 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
272
250
 
273
251
  Args:
274
252
  data: Optional custom data to attach to the context
275
-
276
- Returns:
277
- A new AgentContext instance
278
253
  """
279
254
  from agentpool.agents.context import AgentContext
280
255
  from agentpool.models.manifest import AgentsManifest
281
256
 
282
257
  defn = self.agent_pool.manifest if self.agent_pool else AgentsManifest()
283
258
  return AgentContext(
284
- 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,
285
265
  )
286
266
 
287
267
  async def _setup_toolsets(self) -> None:
288
268
  """Initialize toolsets from config and create bridge if needed."""
289
269
  from agentpool.mcp_server.tool_bridge import BridgeConfig, ToolManagerBridge
290
270
 
291
- if not isinstance(self.config, MCPCapableACPAgentConfig) or not self.config.toolsets:
271
+ if not isinstance(self.config, MCPCapableACPAgentConfig) or not self.config.tools:
292
272
  return
293
- # Create providers from toolset configs and add to tool manager
294
- for toolset_config in self.config.toolsets:
295
- 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():
296
275
  self.tools.add_provider(provider)
297
276
  # Auto-create bridge to expose tools via MCP
298
- config = BridgeConfig(transport="sse", server_name=f"agentpool-{self.name}-tools")
277
+ config = BridgeConfig(server_name=f"agentpool-{self.name}-tools")
299
278
  self._tool_bridge = ToolManagerBridge(node=self, config=config)
300
279
  await self._tool_bridge.start()
301
280
  self._owns_bridge = True
@@ -353,10 +332,9 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
353
332
 
354
333
  async def _initialize(self) -> None:
355
334
  """Initialize the ACP connection."""
356
- from importlib.metadata import metadata
357
-
358
335
  from acp.client.connection import ClientSideConnection
359
336
  from acp.schema import InitializeRequest
337
+ from agentpool.agents.acp_agent.client_handler import ACPClientHandler
360
338
 
361
339
  if not self._process or not self._process.stdin or not self._process.stdout:
362
340
  msg = "Process not started"
@@ -383,12 +361,14 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
383
361
  read_text_file=self.config.allow_file_operations,
384
362
  write_text_file=self.config.allow_file_operations,
385
363
  )
386
- self._init_response = await self._connection.initialize(init_request)
387
- 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)
388
367
 
389
368
  async def _create_session(self) -> None:
390
369
  """Create a new ACP session with configured MCP servers."""
391
370
  from acp.schema import NewSessionRequest
371
+ from agentpool.agents.acp_agent.acp_converters import mcp_configs_to_acp
392
372
 
393
373
  if not self._connection:
394
374
  msg = "Connection not initialized"
@@ -407,6 +387,10 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
407
387
  self._session_id = response.session_id
408
388
  if self._state:
409
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
410
394
  if response.models: # Store full model info from session response
411
395
  self._state.models = response.models
412
396
  self._state.current_model_id = response.models.current_model_id
@@ -464,65 +448,30 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
464
448
  self.log.exception("Error terminating ACP process")
465
449
  self._process = None
466
450
 
467
- async def run(
468
- self,
469
- *prompts: PromptCompatible,
470
- message_id: str | None = None,
471
- input_provider: InputProvider | None = None,
472
- message_history: MessageHistory | None = None,
473
- ) -> ChatMessage[str]:
474
- """Execute prompt against ACP agent.
475
-
476
- Args:
477
- prompts: Prompts to send (will be joined with spaces)
478
- message_id: Optional message id for the returned message
479
- input_provider: Optional input provider for permission requests
480
- message_history: Optional MessageHistory to use instead of agent's own
481
-
482
- Returns:
483
- ChatMessage containing the agent's aggregated text response
484
- """
485
- # Collect all events through run_stream
486
- final_message: ChatMessage[str] | None = None
487
- async for event in self.run_stream(
488
- *prompts,
489
- message_id=message_id,
490
- input_provider=input_provider,
491
- message_history=message_history,
492
- ):
493
- if isinstance(event, StreamCompleteEvent):
494
- final_message = event.message
495
-
496
- if final_message is None:
497
- msg = "No final message received from stream"
498
- raise RuntimeError(msg)
499
-
500
- return final_message
501
-
502
- async def run_stream( # noqa: PLR0915
451
+ async def _stream_events( # noqa: PLR0915
503
452
  self,
504
- *prompts: PromptCompatible,
453
+ prompts: list[UserContent],
454
+ *,
455
+ user_msg: ChatMessage[Any],
456
+ effective_parent_id: str | None,
505
457
  message_id: str | None = None,
458
+ conversation_id: str | None = None,
459
+ parent_id: str | None = None,
506
460
  input_provider: InputProvider | None = None,
507
461
  message_history: MessageHistory | None = None,
508
462
  deps: TDeps | None = None,
509
463
  event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
464
+ wait_for_connections: bool | None = None,
465
+ store_history: bool = True,
510
466
  ) -> AsyncIterator[RichAgentStreamEvent[str]]:
511
- """Stream native events as they arrive from ACP agent.
467
+ from anyenv import MultiEventHandler
512
468
 
513
- Args:
514
- prompts: Prompts to send (will be joined with spaces)
515
- message_id: Optional message id for the final message
516
- input_provider: Optional input provider for permission requests
517
- message_history: Optional MessageHistory to use instead of agent's own
518
- deps: Optional dependencies accessible via ctx.data in tools
519
- event_handlers: Optional event handlers for this run (overrides agent's handlers)
520
-
521
- Yields:
522
- RichAgentStreamEvent instances converted from ACP session updates
523
- """
524
- from acp.schema import PromptRequest
469
+ from acp.schema import ForkSessionRequest, PromptRequest
525
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
+ )
526
475
 
527
476
  # Update input provider if provided
528
477
  if input_provider is not None:
@@ -536,21 +485,13 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
536
485
  conversation = message_history if message_history is not None else self.conversation
537
486
  # Use provided event handlers or fall back to agent's handlers
538
487
  if event_handlers is not None:
539
- from anyenv import MultiEventHandler
540
-
541
- from agentpool.agents.events import resolve_event_handlers
542
-
543
- handler: MultiEventHandler[IndividualEventHandler] = MultiEventHandler(
544
- resolve_event_handlers(event_handlers)
545
- )
488
+ handlers = resolve_event_handlers(event_handlers)
489
+ handler = MultiEventHandler[IndividualEventHandler](handlers)
546
490
  else:
547
491
  handler = self.event_handler
548
- # Prepare user message for history and convert to ACP content blocks
549
- # Get parent_id from last message in history for tree structure
550
- last_msg_id = conversation.get_last_message_id()
551
- user_msg, processed_prompts, _original_message = await prepare_prompts(
552
- *prompts, parent_id=last_msg_id
553
- )
492
+
493
+ # Prepare for ACP content block conversion
494
+ processed_prompts = prompts
554
495
  run_id = str(uuid.uuid4())
555
496
  self._state.clear() # Reset state
556
497
  # Track messages in pydantic-ai format: ModelRequest -> ModelResponse -> ...
@@ -562,6 +503,7 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
562
503
  current_response_parts: list[TextPart | ThinkingPart | ToolCallPart] = []
563
504
  text_chunks: list[str] = [] # For final content string
564
505
  file_tracker = FileTracker() # Track files modified by tool calls
506
+ assert self.conversation_id is not None # Initialized by BaseAgent.run_stream()
565
507
  run_started = RunStartedEvent(
566
508
  thread_id=self.conversation_id,
567
509
  run_id=run_id,
@@ -572,11 +514,21 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
572
514
  content_blocks = convert_to_acp_content(processed_prompts)
573
515
  pending_parts = conversation.get_pending_parts()
574
516
  final_blocks = [*to_acp_content_blocks(pending_parts), *content_blocks]
575
- prompt_request = PromptRequest(session_id=self._session_id, prompt=final_blocks)
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)
576
530
  self.log.debug("Starting streaming prompt", num_blocks=len(final_blocks))
577
- # Reset cancellation state
578
- self._cancelled = False
579
- self._current_stream_task = asyncio.current_task()
531
+ self._cancelled = False # Reset cancellation state
580
532
  # Run prompt in background
581
533
  prompt_task = asyncio.create_task(self._connection.prompt(prompt_request))
582
534
  self._prompt_task = prompt_task
@@ -606,22 +558,50 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
606
558
  yield self._state.events[last_idx]
607
559
  last_idx += 1
608
560
 
609
- # Set deps on tool bridge for access during tool invocations
561
+ # Set deps on tool bridge for access during tool invocations
562
+
610
563
  # (ContextVar doesn't work because MCP server runs in a separate task)
611
564
  if self._tool_bridge:
612
565
  self._tool_bridge.current_deps = deps
613
566
 
567
+ # Accumulate metadata events by tool_call_id (workaround for MCP stripping _meta)
568
+ tool_metadata: dict[str, dict[str, Any]] = {}
569
+
614
570
  # Merge ACP events with custom events from queue
615
571
  try:
616
572
  async with merge_queue_into_iterator(
617
573
  poll_acp_events(), self._event_queue
618
574
  ) as merged_events:
619
- async for event in file_tracker.track(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
+
620
584
  # Check for cancellation
621
585
  if self._cancelled:
622
586
  self.log.info("Stream cancelled by user")
623
587
  break
624
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
+
625
605
  # Extract content from events and build parts in arrival order
626
606
  match event:
627
607
  case PartDeltaEvent(delta=TextPartDelta(content_delta=delta)):
@@ -648,10 +628,8 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
648
628
 
649
629
  # Handle cancellation - emit partial message
650
630
  if self._cancelled:
651
- text_content = "".join(text_chunks)
652
- metadata: SimpleJsonType = file_tracker.get_metadata()
653
631
  message = ChatMessage[str](
654
- content=text_content,
632
+ content="".join(text_chunks),
655
633
  role="assistant",
656
634
  name=self.name,
657
635
  message_id=message_id or str(uuid.uuid4()),
@@ -659,19 +637,18 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
659
637
  parent_id=user_msg.message_id,
660
638
  model_name=self.model_name,
661
639
  messages=model_messages,
662
- metadata=metadata,
640
+ metadata=file_tracker.get_metadata(),
663
641
  finish_reason="stop",
664
642
  )
665
643
  complete_event = StreamCompleteEvent(message=message)
666
644
  await handler(None, complete_event)
667
645
  yield complete_event
668
- self._current_stream_task = None
669
646
  self._prompt_task = None
670
647
  return
671
648
 
672
649
  # Ensure we catch any exceptions from the prompt task
673
650
  response = await prompt_task
674
- finish_reason: FinishReason = STOP_REASON_MAP.get(response.stop_reason, "stop")
651
+ finish_reason = to_finish_reason(response.stop_reason)
675
652
  # Flush response parts to model_messages
676
653
  if current_response_parts:
677
654
  model_messages.append(
@@ -684,8 +661,6 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
684
661
  )
685
662
 
686
663
  text_content = "".join(text_chunks)
687
- metadata = file_tracker.get_metadata()
688
-
689
664
  # Calculate approximate token usage from what we can observe
690
665
  input_parts = [*processed_prompts, *pending_parts]
691
666
  usage, cost_info = await calculate_usage_from_parts(
@@ -705,77 +680,77 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
705
680
  parent_id=user_msg.message_id,
706
681
  model_name=self.model_name,
707
682
  messages=model_messages,
708
- metadata=metadata,
683
+ metadata=file_tracker.get_metadata(),
709
684
  finish_reason=finish_reason,
710
685
  usage=usage,
711
686
  cost_info=cost_info,
712
687
  )
713
688
  complete_event = StreamCompleteEvent(message=message)
714
689
  await handler(None, complete_event)
715
- yield complete_event # Emit final StreamCompleteEvent with aggregated message
716
- self.message_sent.emit(message)
717
- conversation.add_chat_messages([user_msg, message]) # Record to conversation history
718
-
719
- async def run_iter(
720
- self,
721
- *prompt_groups: Sequence[PromptCompatible],
722
- ) -> AsyncIterator[ChatMessage[str]]:
723
- """Run agent sequentially on multiple prompt groups.
724
-
725
- Args:
726
- prompt_groups: Groups of prompts to process sequentially
727
-
728
- Yields:
729
- Response messages in sequence
730
- """
731
- for prompts in prompt_groups:
732
- response = await self.run(*prompts)
733
- yield response
690
+ yield complete_event # Emit final StreamCompleteEvent - post-processing handled by base
734
691
 
735
692
  @property
736
693
  def model_name(self) -> str | None:
737
694
  """Get the model name in a consistent format."""
738
- if self._state and self._state.current_model_id:
739
- return self._state.current_model_id
740
- if self._init_response and self._init_response.agent_info:
741
- return self._init_response.agent_info.name
742
- return None
695
+ return model_id if self._state and (model_id := self._state.current_model_id) else None
743
696
 
744
697
  async def set_model(self, model: str) -> None:
745
- """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
746
703
 
747
704
  Args:
748
- model: New model name to use
705
+ model: New model ID to use
749
706
 
750
707
  Raises:
751
- ValueError: If the config doesn't have a model field
752
- 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
753
709
  """
754
- # TODO: Once ACP protocol stabilizes, use set_session_model instead of restart
755
- # from acp.schema import SetSessionModelRequest # UNSTABLE
756
- # if self._connection and self._session_id:
757
- # request = SetSessionModelRequest(session_id=self._session_id, model_id=model)
758
- # await self._connection.set_session_model(request)
759
- # if self._state:
760
- # self._state.current_model_id = model
761
- # self.log.info("Model changed via ACP protocol", model=model)
762
- # return
763
-
764
- if not hasattr(self.config, "model"):
765
- msg = f"Config type {type(self.config).__name__} doesn't support model changes"
766
- raise ValueError(msg)
767
- # Prevent changes during active processing
768
- if self._process and not self._session_id:
769
- 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"
770
714
  raise RuntimeError(msg)
771
- # Create new config with updated model
772
- new_config = self.config.model_copy(update={"model": model})
773
- if self._process: # Clean up existing process if any
774
- await self._cleanup()
775
- self.config = new_config # Update config and restart
776
- process = await self._start_process()
777
- async with monitor_process(process, context="ACP initialization"):
778
- 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)
779
754
 
780
755
  async def set_tool_confirmation_mode(self, mode: ToolConfirmationMode) -> None:
781
756
  """Set the tool confirmation mode for this agent.
@@ -862,89 +837,129 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
862
837
  result.append(toko_model)
863
838
  return result
864
839
 
865
- def get_modes(self) -> list[ModeCategory]:
840
+ async def get_modes(self) -> list[ModeCategory]:
866
841
  """Get available modes from the ACP session state.
867
842
 
868
- Passthrough from remote ACP server's mode state.
843
+ Passthrough from remote ACP server's mode and model state.
844
+ Prefers new config_options format, falls back to legacy modes/models.
869
845
 
870
846
  Returns:
871
847
  List of ModeCategory from remote server, empty if not available
872
848
  """
873
- from agentpool.agents.modes import ModeCategory, ModeInfo
849
+ from agentpool.agents.acp_agent.acp_converters import get_modes
874
850
 
875
- if not self._state or not self._state.modes:
851
+ if not self._state:
876
852
  return []
877
853
 
878
- # Convert ACP SessionModeState to our ModeCategory
879
- acp_modes = self._state.modes
880
- category_id = "remote"
881
- modes = [
882
- ModeInfo(
883
- id=m.id,
884
- name=m.name,
885
- description=m.description or "",
886
- category_id=category_id,
887
- )
888
- for m in acp_modes.available_modes
889
- ]
890
-
891
- return [
892
- ModeCategory(
893
- id=category_id,
894
- name="Mode",
895
- available_modes=modes,
896
- current_mode_id=acp_modes.current_mode_id,
897
- )
898
- ]
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
+ )
899
860
 
900
861
  async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
901
862
  """Set a mode on the remote ACP server.
902
863
 
903
- For ACPAgent, this forwards the mode change to the remote ACP server.
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.
904
867
 
905
868
  Args:
906
869
  mode: The mode to set - ModeInfo object or mode ID string
907
- category_id: Optional category ID (ignored for ACP, only one category)
870
+ category_id: Category ID (config option ID)
908
871
 
909
872
  Raises:
910
873
  RuntimeError: If not connected to ACP server
911
874
  ValueError: If mode is not available
912
875
  """
913
- from acp.schema import SetSessionModeRequest
876
+ from acp.schema import (
877
+ SetSessionConfigOptionRequest,
878
+ SetSessionModelRequest,
879
+ SetSessionModeRequest,
880
+ )
914
881
 
915
- # Extract mode_id from ModeInfo if provided
916
- mode_id = mode.id if isinstance(mode, ModeInfo) else mode
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
917
888
 
918
- if not self._connection or not self._session_id:
889
+ if not self._connection or not self._session_id or not self._state:
919
890
  msg = "Not connected to ACP server"
920
891
  raise RuntimeError(msg)
921
892
 
922
893
  # Validate mode is available
923
- available_modes = self.get_modes()
924
- if available_modes:
925
- valid_ids = {m.id for cat in available_modes for m in cat.available_modes}
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}
926
901
  if mode_id not in valid_ids:
927
- msg = f"Unknown mode: {mode_id}. Available: {valid_ids}"
902
+ msg = f"Unknown {category_id}: {mode_id}. Available: {valid_ids}"
928
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)
929
926
 
930
- # Forward mode change to remote ACP server
931
- request = SetSessionModeRequest(session_id=self._session_id, mode_id=mode_id)
932
- await self._connection.set_session_mode(request)
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)
933
940
 
934
- # Update local state
935
- if self._state and self._state.modes:
936
- self._state.modes.current_mode_id = mode_id
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)
937
944
 
938
- self.log.info("Mode changed on remote ACP server", mode_id=mode_id)
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)
939
954
 
940
955
 
941
956
  if __name__ == "__main__":
957
+ from agentpool.models.acp_agents import ACPAgentConfig
942
958
 
943
959
  async def main() -> None:
944
960
  """Demo: Basic call to an ACP agent."""
945
- args = ["run", "agentpool", "serve-acp"]
946
- cwd = str(Path.cwd())
947
- 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:
948
963
  print("Response (streaming): ", end="", flush=True)
949
964
  async for chunk in agent.run_stream("Say hello briefly."):
950
965
  print(chunk, end="", flush=True)