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
@@ -4,34 +4,60 @@ from __future__ import annotations
4
4
 
5
5
  from abc import abstractmethod
6
6
  import asyncio
7
- from typing import TYPE_CHECKING, Any, Literal
7
+ from collections.abc import Callable
8
+ from contextlib import suppress
9
+ from dataclasses import dataclass, field
10
+ from typing import TYPE_CHECKING, Any, overload
8
11
 
9
- from anyenv import MultiEventHandler
10
- from exxec import LocalExecutionEnvironment
12
+ from anyenv import MultiEventHandler, method_spawner
13
+ from anyenv.signals import BoundSignal, Signal
14
+ import anyio
11
15
 
12
- from agentpool.agents.events import resolve_event_handlers
16
+ from agentpool.agents.events import StreamCompleteEvent, resolve_event_handlers
17
+ from agentpool.agents.modes import ModeInfo
13
18
  from agentpool.log import get_logger
14
- from agentpool.messaging import MessageHistory, MessageNode
19
+ from agentpool.messaging import ChatMessage, MessageHistory, MessageNode
20
+ from agentpool.prompts.convert import convert_prompts
15
21
  from agentpool.tools.manager import ToolManager
22
+ from agentpool.utils.inspection import call_with_context
23
+ from agentpool.utils.now import get_now
16
24
 
17
25
 
18
26
  if TYPE_CHECKING:
19
27
  from collections.abc import AsyncIterator, Sequence
28
+ from datetime import datetime
20
29
 
21
- from evented.configs import EventConfig
30
+ from evented_config import EventConfig
22
31
  from exxec import ExecutionEnvironment
32
+ from pydantic_ai import UserContent
33
+ from slashed import BaseCommand, CommandStore
34
+ from tokonomics.model_discovery.model_info import ModelInfo
23
35
 
36
+ from acp.schema import AvailableCommandsUpdate, ConfigOptionUpdate
37
+ from agentpool.agents.agent import Agent
24
38
  from agentpool.agents.context import AgentContext
25
39
  from agentpool.agents.events import RichAgentStreamEvent
26
- from agentpool.common_types import BuiltinEventHandlerType, IndividualEventHandler
27
- from agentpool.delegation import AgentPool
40
+ from agentpool.agents.modes import ModeCategory, ModeInfo
41
+ from agentpool.common_types import (
42
+ AgentName,
43
+ BuiltinEventHandlerType,
44
+ IndividualEventHandler,
45
+ MCPServerStatus,
46
+ ProcessorCallback,
47
+ PromptCompatible,
48
+ )
49
+ from agentpool.delegation import AgentPool, Team, TeamRun
50
+ from agentpool.messaging import ChatMessage
51
+ from agentpool.talk.stats import MessageStats
28
52
  from agentpool.ui.base import InputProvider
29
53
  from agentpool_config.mcp_server import MCPServerConfig
54
+ from agentpool_config.nodes import ToolConfirmationMode
30
55
 
56
+ # Union type for state updates emitted via state_updated signal
57
+ type StateUpdate = ModeInfo | ModelInfo | AvailableCommandsUpdate | ConfigOptionUpdate
31
58
 
32
- logger = get_logger(__name__)
33
59
 
34
- ToolConfirmationMode = Literal["always", "never", "per_tool"]
60
+ logger = get_logger(__name__)
35
61
 
36
62
 
37
63
  class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
@@ -46,8 +72,37 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
46
72
  - _input_provider: Provider for user input/confirmations
47
73
  - env: ExecutionEnvironment for running code/commands
48
74
  - context property: Returns NodeContext for the agent
75
+
76
+ Signals:
77
+ - run_failed: Emitted when agent execution fails with error details
49
78
  """
50
79
 
80
+ @dataclass(frozen=True)
81
+ class RunFailedEvent:
82
+ """Event emitted when agent execution fails."""
83
+
84
+ agent_name: str
85
+ """Name of the agent that failed."""
86
+ message: str
87
+ """Error description."""
88
+ exception: Exception
89
+ """The exception that caused the failure."""
90
+ timestamp: Any = field(default_factory=get_now) # datetime
91
+ """When the failure occurred."""
92
+
93
+ @dataclass(frozen=True)
94
+ class AgentReset:
95
+ """Emitted when agent is reset."""
96
+
97
+ agent_name: AgentName
98
+ previous_tools: dict[str, bool]
99
+ new_tools: dict[str, bool]
100
+ timestamp: datetime = field(default_factory=get_now)
101
+
102
+ agent_reset = Signal[AgentReset]()
103
+ # Signal emitted when agent execution fails
104
+ run_failed: Signal[RunFailedEvent] = Signal()
105
+
51
106
  def __init__(
52
107
  self,
53
108
  *,
@@ -64,6 +119,7 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
64
119
  output_type: type[TResult] = str, # type: ignore[assignment]
65
120
  tool_confirmation_mode: ToolConfirmationMode = "per_tool",
66
121
  event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
122
+ commands: Sequence[BaseCommand] | None = None,
67
123
  ) -> None:
68
124
  """Initialize base agent with shared infrastructure.
69
125
 
@@ -80,7 +136,13 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
80
136
  output_type: Output type for this agent
81
137
  tool_confirmation_mode: How tool execution confirmation is handled
82
138
  event_handlers: Event handlers for this agent
139
+ commands: Slash commands to register with this agent
83
140
  """
141
+ from exxec import LocalExecutionEnvironment
142
+ from slashed import CommandStore
143
+
144
+ from agentpool_commands import get_commands
145
+
84
146
  super().__init__(
85
147
  name=name,
86
148
  description=description,
@@ -90,10 +152,14 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
90
152
  enable_logging=enable_logging,
91
153
  event_configs=event_configs,
92
154
  )
155
+ self._infinite = False
156
+ self._background_task: asyncio.Task[ChatMessage[Any]] | None = None
93
157
 
94
158
  # Shared infrastructure - previously duplicated in all 4 agents
95
159
  self._event_queue: asyncio.Queue[RichAgentStreamEvent[Any]] = asyncio.Queue()
96
- self.conversation = MessageHistory()
160
+ # Use storage from agent_pool if available, otherwise memory-only
161
+ storage = agent_pool.storage if agent_pool else None
162
+ self.conversation = MessageHistory(storage=storage)
97
163
  self.env = env or LocalExecutionEnvironment()
98
164
  self._input_provider = input_provider
99
165
  self._output_type: type[TResult] = output_type
@@ -103,10 +169,97 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
103
169
  self.event_handler: MultiEventHandler[IndividualEventHandler] = MultiEventHandler(
104
170
  resolved_handlers
105
171
  )
106
-
107
- # Cancellation infrastructure
108
172
  self._cancelled = False
109
173
  self._current_stream_task: asyncio.Task[Any] | None = None
174
+ # Deferred initialization support - subclasses set True in __aenter__,
175
+ # override ensure_initialized() to do actual connection
176
+ self._connect_pending: bool = False
177
+ # State change signal - emitted when mode/model/commands change
178
+ # Uses union type for different state update kinds
179
+ self.state_updated: BoundSignal[StateUpdate] = BoundSignal()
180
+ self._command_store: CommandStore = CommandStore()
181
+ # Initialize store (registers builtin help/exit commands)
182
+ self._command_store._initialize_sync()
183
+ # Register default agent commands
184
+ for command in get_commands():
185
+ self._command_store.register_command(command)
186
+
187
+ # Register additional provided commands
188
+ if commands:
189
+ for command in commands:
190
+ self._command_store.register_command(command)
191
+
192
+ @overload
193
+ def __and__( # if other doesnt define deps, we take the agents one
194
+ self, other: ProcessorCallback[Any] | Team[TDeps] | Agent[TDeps, Any]
195
+ ) -> Team[TDeps]: ...
196
+
197
+ @overload
198
+ def __and__( # otherwise, we dont know and deps is Any
199
+ self, other: ProcessorCallback[Any] | Team[Any] | Agent[Any, Any]
200
+ ) -> Team[Any]: ...
201
+
202
+ def __and__(self, other: MessageNode[Any, Any] | ProcessorCallback[Any]) -> Team[Any]:
203
+ """Create sequential team using & operator.
204
+
205
+ Example:
206
+ group = analyzer & planner & executor # Create group of 3
207
+ group = analyzer & existing_group # Add to existing group
208
+ """
209
+ from agentpool.agents.agent import Agent
210
+ from agentpool.delegation.team import Team
211
+
212
+ match other:
213
+ case Team():
214
+ return Team([self, *other.nodes])
215
+ case Callable():
216
+ agent_2 = Agent.from_callback(other)
217
+ agent_2.agent_pool = self.agent_pool
218
+ return Team([self, agent_2])
219
+ case MessageNode():
220
+ return Team([self, other])
221
+ case _:
222
+ msg = f"Invalid agent type: {type(other)}"
223
+ raise ValueError(msg)
224
+
225
+ @overload
226
+ def __or__(self, other: MessageNode[TDeps, Any]) -> TeamRun[TDeps, Any]: ...
227
+
228
+ @overload
229
+ def __or__[TOtherDeps](self, other: MessageNode[TOtherDeps, Any]) -> TeamRun[Any, Any]: ...
230
+
231
+ @overload
232
+ def __or__(self, other: ProcessorCallback[Any]) -> TeamRun[Any, Any]: ...
233
+
234
+ def __or__(self, other: MessageNode[Any, Any] | ProcessorCallback[Any]) -> TeamRun[Any, Any]:
235
+ # Create new execution with sequential mode (for piping)
236
+ from agentpool import TeamRun
237
+ from agentpool.agents.agent import Agent
238
+
239
+ if callable(other):
240
+ other = Agent.from_callback(other)
241
+ other.agent_pool = self.agent_pool
242
+
243
+ return TeamRun([self, other])
244
+
245
+ @property
246
+ def command_store(self) -> CommandStore:
247
+ """Get the command store for slash commands."""
248
+ return self._command_store
249
+
250
+ async def reset(self) -> None:
251
+ """Reset agent state (conversation history and tool states)."""
252
+ old_tools = await self.tools.list_tools()
253
+ await self.conversation.clear()
254
+ await self.tools.reset_states()
255
+ new_tools = await self.tools.list_tools()
256
+
257
+ event = self.AgentReset(
258
+ agent_name=self.name,
259
+ previous_tools=old_tools,
260
+ new_tools=new_tools,
261
+ )
262
+ await self.agent_reset.emit(event)
110
263
 
111
264
  @abstractmethod
112
265
  def get_context(self, data: Any = None) -> AgentContext[Any]:
@@ -127,16 +280,283 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
127
280
  ...
128
281
 
129
282
  @abstractmethod
130
- def run_stream(
283
+ async def set_model(self, model: str) -> None:
284
+ """Set the model for this agent.
285
+
286
+ Args:
287
+ model: New model identifier to use
288
+ """
289
+ ...
290
+
291
+ async def run_iter(
131
292
  self,
132
- *prompt: Any,
293
+ *prompt_groups: Sequence[PromptCompatible],
294
+ store_history: bool = True,
295
+ wait_for_connections: bool | None = None,
296
+ ) -> AsyncIterator[ChatMessage[TResult]]:
297
+ """Run agent sequentially on multiple prompt groups.
298
+
299
+ Args:
300
+ prompt_groups: Groups of prompts to process sequentially
301
+ store_history: Whether to store in conversation history
302
+ wait_for_connections: Whether to wait for connected agents
303
+
304
+ Yields:
305
+ Response messages in sequence
306
+
307
+ Example:
308
+ questions = [
309
+ ["What is your name?"],
310
+ ["How old are you?", image1],
311
+ ["Describe this image", image2],
312
+ ]
313
+ async for response in agent.run_iter(*questions):
314
+ print(response.content)
315
+ """
316
+ for prompts in prompt_groups:
317
+ response = await self.run(
318
+ *prompts,
319
+ store_history=store_history,
320
+ wait_for_connections=wait_for_connections,
321
+ )
322
+ yield response # pyright: ignore
323
+
324
+ async def run_in_background(
325
+ self,
326
+ *prompt: PromptCompatible,
327
+ max_count: int | None = None,
328
+ interval: float = 1.0,
133
329
  **kwargs: Any,
330
+ ) -> asyncio.Task[ChatMessage[TResult] | None]:
331
+ """Run agent continuously in background with prompt or dynamic prompt function.
332
+
333
+ Args:
334
+ prompt: Static prompt or function that generates prompts
335
+ max_count: Maximum number of runs (None = infinite)
336
+ interval: Seconds between runs
337
+ **kwargs: Arguments passed to run()
338
+ """
339
+ self._infinite = max_count is None
340
+
341
+ async def _continuous() -> ChatMessage[Any]:
342
+ count = 0
343
+ self.log.debug("Starting continuous run", max_count=max_count, interval=interval)
344
+ latest = None
345
+ while (max_count is None or count < max_count) and not self._cancelled:
346
+ try:
347
+ agent_ctx = self.get_context()
348
+ current_prompts = [
349
+ call_with_context(p, agent_ctx, **kwargs) if callable(p) else p
350
+ for p in prompt
351
+ ]
352
+ self.log.debug("Generated prompt", iteration=count)
353
+ latest = await self.run(current_prompts, **kwargs)
354
+ self.log.debug("Run continuous result", iteration=count)
355
+
356
+ count += 1
357
+ await anyio.sleep(interval)
358
+ except asyncio.CancelledError:
359
+ self.log.debug("Continuous run cancelled")
360
+ break
361
+ except Exception:
362
+ # Check if we were cancelled (may surface as other exceptions)
363
+ if self._cancelled:
364
+ self.log.debug("Continuous run cancelled via flag")
365
+ break
366
+ count += 1
367
+ self.log.exception("Background run failed")
368
+ await anyio.sleep(interval)
369
+ self.log.debug("Continuous run completed", iterations=count)
370
+ return latest # type: ignore[return-value]
371
+
372
+ await self.stop() # Cancel any existing background task
373
+ self._cancelled = False # Reset cancellation flag for new run
374
+ task = asyncio.create_task(_continuous(), name=f"background_{self.name}")
375
+ self.log.debug("Started background task", task_name=task.get_name())
376
+ self._background_task = task
377
+ return task
378
+
379
+ async def stop(self) -> None:
380
+ """Stop continuous execution if running."""
381
+ self._cancelled = True # Signal cancellation via flag
382
+ if self._background_task and not self._background_task.done():
383
+ self._background_task.cancel()
384
+ with suppress(asyncio.CancelledError): # Expected when we cancel the task
385
+ await self._background_task
386
+ self._background_task = None
387
+
388
+ def is_busy(self) -> bool:
389
+ """Check if agent is currently processing tasks."""
390
+ return bool(self.task_manager._pending_tasks or self._background_task)
391
+
392
+ async def wait(self) -> ChatMessage[TResult]:
393
+ """Wait for background execution to complete."""
394
+ if not self._background_task:
395
+ msg = "No background task running"
396
+ raise RuntimeError(msg)
397
+ if self._infinite:
398
+ msg = "Cannot wait on infinite execution"
399
+ raise RuntimeError(msg)
400
+ try:
401
+ return await self._background_task
402
+ finally:
403
+ self._background_task = None
404
+
405
+ @method_spawner
406
+ async def run_stream(
407
+ self,
408
+ *prompts: PromptCompatible,
409
+ store_history: bool = True,
410
+ message_id: str | None = None,
411
+ conversation_id: str | None = None,
412
+ parent_id: str | None = None,
413
+ message_history: MessageHistory | None = None,
414
+ input_provider: InputProvider | None = None,
415
+ wait_for_connections: bool | None = None,
416
+ deps: TDeps | None = None,
417
+ event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
134
418
  ) -> AsyncIterator[RichAgentStreamEvent[TResult]]:
135
419
  """Run agent with streaming output.
136
420
 
421
+ This method delegates to _stream_events() which must be implemented by subclasses.
422
+ Handles prompt conversion from various formats to UserContent.
423
+
424
+ Args:
425
+ *prompts: Input prompts (various formats supported)
426
+ store_history: Whether to store in history
427
+ message_id: Optional message ID
428
+ conversation_id: Optional conversation ID
429
+ parent_id: Optional parent message ID
430
+ message_history: Optional message history
431
+ input_provider: Optional input provider
432
+ wait_for_connections: Whether to wait for connected agents
433
+ deps: Optional dependencies
434
+ event_handlers: Optional event handlers
435
+
436
+ Yields:
437
+ Stream events during execution
438
+ """
439
+ from agentpool.messaging import ChatMessage
440
+ from agentpool.utils.identifiers import generate_session_id
441
+
442
+ # Convert prompts to standard UserContent format
443
+ converted_prompts = await convert_prompts(prompts)
444
+ # Get message history (either passed or agent's own)
445
+ conversation = message_history if message_history is not None else self.conversation
446
+ # Determine effective parent_id (from param or last message in history)
447
+ effective_parent_id = parent_id if parent_id else conversation.get_last_message_id()
448
+ # Initialize or adopt conversation_id
449
+ if self.conversation_id is None:
450
+ if conversation_id:
451
+ # Adopt conversation_id (from agent chain or external session like ACP)
452
+ self.conversation_id = conversation_id
453
+ else:
454
+ # Generate new conversation_id
455
+ self.conversation_id = generate_session_id()
456
+ # Always log conversation with initial prompt for title generation
457
+ # StorageManager handles idempotent behavior (skip if already logged)
458
+ # Use last prompt to avoid staged content (staged is prepended, user prompt is last)
459
+ user_prompts = [
460
+ str(p) for p in prompts if isinstance(p, str)
461
+ ] # Filter to text prompts only
462
+ initial_prompt = user_prompts[-1] if user_prompts else None
463
+ await self.log_conversation(initial_prompt)
464
+ elif conversation_id and self.conversation_id != conversation_id:
465
+ # Adopt passed conversation_id (for routing chains)
466
+ self.conversation_id = conversation_id
467
+
468
+ user_msg = ChatMessage.user_prompt(
469
+ message=converted_prompts,
470
+ parent_id=effective_parent_id,
471
+ conversation_id=self.conversation_id,
472
+ )
473
+
474
+ # Stream events from implementation
475
+ final_message = None
476
+ self._current_stream_task = asyncio.current_task()
477
+ try:
478
+ async for event in self._stream_events(
479
+ converted_prompts,
480
+ user_msg=user_msg,
481
+ effective_parent_id=effective_parent_id,
482
+ store_history=store_history,
483
+ message_id=message_id,
484
+ conversation_id=conversation_id,
485
+ parent_id=parent_id,
486
+ message_history=message_history,
487
+ input_provider=input_provider,
488
+ wait_for_connections=wait_for_connections,
489
+ deps=deps,
490
+ event_handlers=event_handlers,
491
+ ):
492
+ yield event
493
+ # Capture final message from StreamCompleteEvent
494
+ if isinstance(event, StreamCompleteEvent):
495
+ final_message = event.message
496
+ except Exception as e:
497
+ self.log.exception("Agent stream failed")
498
+ failed_event = BaseAgent.RunFailedEvent(
499
+ agent_name=self.name,
500
+ message="Agent stream failed",
501
+ exception=e,
502
+ )
503
+ await self.run_failed.emit(failed_event)
504
+ raise
505
+ finally:
506
+ self._current_stream_task = None
507
+
508
+ # Post-processing after stream completes
509
+ if final_message is not None:
510
+ # Emit signal (always - for event handlers)
511
+ await self.message_sent.emit(final_message)
512
+ # Conditional persistence based on store_history
513
+ # TODO: Verify store_history semantics across all use cases:
514
+ # - Should subagent tool calls set store_history=False?
515
+ # - Should forked/ephemeral runs always skip persistence?
516
+ # - Should signals still fire when store_history=False?
517
+ # Current behavior: store_history controls both DB logging AND conversation context
518
+ if store_history:
519
+ # Log to persistent storage and add to conversation context
520
+ await self.log_message(final_message)
521
+ conversation.add_chat_messages([user_msg, final_message])
522
+ # Route to connected agents (always - they decide what to do with it)
523
+ await self.connections.route_message(final_message, wait=wait_for_connections)
524
+
525
+ @abstractmethod
526
+ def _stream_events(
527
+ self,
528
+ prompts: list[UserContent],
529
+ *,
530
+ user_msg: Any, # ChatMessage but imported in run_stream
531
+ effective_parent_id: str | None,
532
+ message_id: str | None = None,
533
+ conversation_id: str | None = None,
534
+ parent_id: str | None = None,
535
+ input_provider: InputProvider | None = None,
536
+ message_history: MessageHistory | None = None,
537
+ deps: TDeps | None = None,
538
+ event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
539
+ wait_for_connections: bool | None = None,
540
+ store_history: bool = True,
541
+ ) -> AsyncIterator[RichAgentStreamEvent[TResult]]:
542
+ """Agent-specific streaming implementation.
543
+
544
+ Subclasses must implement this to provide their streaming logic.
545
+ Prompts are pre-converted to UserContent format by run_stream().
546
+
137
547
  Args:
138
- *prompt: Input prompts
139
- **kwargs: Additional arguments
548
+ prompts: Converted prompts in UserContent format
549
+ user_msg: Pre-created user ChatMessage (from base class)
550
+ effective_parent_id: Resolved parent message ID for threading
551
+ message_id: Optional message ID
552
+ conversation_id: Optional conversation ID
553
+ parent_id: Optional parent message ID
554
+ input_provider: Optional input provider
555
+ message_history: Optional message history
556
+ deps: Optional dependencies
557
+ event_handlers: Optional event handlers
558
+ wait_for_connections: Whether to wait for connected agents
559
+ store_history: Whether to store in history
140
560
 
141
561
  Yields:
142
562
  Stream events during execution
@@ -151,6 +571,25 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
151
571
  """
152
572
  self.tool_confirmation_mode = mode
153
573
 
574
+ def is_initializing(self) -> bool:
575
+ """Check if agent is still initializing.
576
+
577
+ Returns:
578
+ True if deferred initialization is pending
579
+ """
580
+ return self._connect_pending
581
+
582
+ async def ensure_initialized(self) -> None:
583
+ """Wait for deferred initialization to complete.
584
+
585
+ Subclasses that use deferred init should:
586
+ 1. Set `self._connect_pending = True` in `__aenter__`
587
+ 2. Override this method to do actual connection work
588
+ 3. Set `self._connect_pending = False` when done
589
+
590
+ The base implementation is a no-op for agents without deferred init.
591
+ """
592
+
154
593
  def is_cancelled(self) -> bool:
155
594
  """Check if the agent has been cancelled.
156
595
 
@@ -175,3 +614,179 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
175
614
  if self._current_stream_task and not self._current_stream_task.done():
176
615
  self._current_stream_task.cancel()
177
616
  logger.info("Interrupted agent stream", agent=self.name)
617
+
618
+ async def get_stats(self) -> MessageStats:
619
+ """Get message statistics."""
620
+ from agentpool.talk.stats import MessageStats
621
+
622
+ return MessageStats(messages=list(self.conversation.chat_messages))
623
+
624
+ def get_mcp_server_info(self) -> dict[str, MCPServerStatus]:
625
+ """Get information about configured MCP servers.
626
+
627
+ Returns a dict mapping server names to their status info. Used by
628
+ the OpenCode /mcp endpoint to display MCP servers in the UI.
629
+
630
+ The default implementation checks external_providers on the tool manager.
631
+ Subclasses may override to provide agent-specific MCP server info
632
+ (e.g., ClaudeCodeAgent has its own MCP server handling).
633
+
634
+ Returns:
635
+ Dict mapping server name to MCPServerStatus
636
+ """
637
+ from agentpool.common_types import MCPServerStatus
638
+ from agentpool.mcp_server.manager import MCPManager
639
+ from agentpool.resource_providers import AggregatingResourceProvider
640
+ from agentpool.resource_providers.mcp_provider import MCPResourceProvider
641
+
642
+ def add_status(provider: MCPResourceProvider, result: dict[str, MCPServerStatus]) -> None:
643
+ status_dict = provider.get_status()
644
+ status_type = status_dict.get("status", "disabled")
645
+ if status_type == "connected":
646
+ result[provider.name] = MCPServerStatus(
647
+ name=provider.name, status="connected", server_type="stdio"
648
+ )
649
+ elif status_type == "failed":
650
+ error = status_dict.get("error", "Unknown error")
651
+ result[provider.name] = MCPServerStatus(
652
+ name=provider.name, status="error", error=error
653
+ )
654
+ else:
655
+ result[provider.name] = MCPServerStatus(name=provider.name, status="disconnected")
656
+
657
+ result: dict[str, MCPServerStatus] = {}
658
+ try:
659
+ for provider in self.tools.external_providers:
660
+ if isinstance(provider, MCPResourceProvider):
661
+ add_status(provider, result)
662
+ elif isinstance(provider, AggregatingResourceProvider):
663
+ for nested in provider.providers:
664
+ if isinstance(nested, MCPResourceProvider):
665
+ add_status(nested, result)
666
+ elif isinstance(provider, MCPManager):
667
+ for mcp_provider in provider.get_mcp_providers():
668
+ add_status(mcp_provider, result)
669
+ except Exception: # noqa: BLE001
670
+ pass
671
+
672
+ return result
673
+
674
+ @method_spawner
675
+ async def run(
676
+ self,
677
+ *prompts: PromptCompatible | ChatMessage[Any],
678
+ store_history: bool = True,
679
+ message_id: str | None = None,
680
+ conversation_id: str | None = None,
681
+ parent_id: str | None = None,
682
+ message_history: MessageHistory | None = None,
683
+ deps: TDeps | None = None,
684
+ input_provider: InputProvider | None = None,
685
+ event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
686
+ wait_for_connections: bool | None = None,
687
+ ) -> ChatMessage[TResult]:
688
+ """Run agent with prompt and get response.
689
+
690
+ This is the standard synchronous run method shared by all agent types.
691
+ It collects all streaming events from run_stream() and returns the final message.
692
+
693
+ Args:
694
+ prompts: User query or instruction
695
+ store_history: Whether the message exchange should be added to the
696
+ context window
697
+ message_id: Optional message id for the returned message.
698
+ Automatically generated if not provided.
699
+ conversation_id: Optional conversation id for the returned message.
700
+ parent_id: Parent message id
701
+ message_history: Optional MessageHistory object to
702
+ use instead of agent's own conversation
703
+ deps: Optional dependencies for the agent
704
+ input_provider: Optional input provider for the agent
705
+ event_handlers: Optional event handlers for this run (overrides agent's handlers)
706
+ wait_for_connections: Whether to wait for connected agents to complete
707
+
708
+ Returns:
709
+ ChatMessage containing response and run information
710
+
711
+ Raises:
712
+ RuntimeError: If no final message received from stream
713
+ UnexpectedModelBehavior: If the model fails or behaves unexpectedly
714
+ """
715
+ # Collect all events through run_stream
716
+ final_message: ChatMessage[TResult] | None = None
717
+ async for event in self.run_stream(
718
+ *prompts,
719
+ store_history=store_history,
720
+ message_id=message_id,
721
+ conversation_id=conversation_id,
722
+ parent_id=parent_id,
723
+ message_history=message_history,
724
+ deps=deps,
725
+ input_provider=input_provider,
726
+ event_handlers=event_handlers,
727
+ wait_for_connections=wait_for_connections,
728
+ ):
729
+ if isinstance(event, StreamCompleteEvent):
730
+ final_message = event.message
731
+
732
+ if final_message is None:
733
+ msg = "No final message received from stream"
734
+ raise RuntimeError(msg)
735
+
736
+ return final_message
737
+
738
+ @abstractmethod
739
+ async def get_available_models(self) -> list[ModelInfo] | None:
740
+ """Get available models for this agent.
741
+
742
+ Returns a list of models that can be used with this agent, or None
743
+ if model discovery is not supported for this agent type.
744
+
745
+ Uses tokonomics.ModelInfo which includes pricing, capabilities,
746
+ and limits. Can be converted to protocol-specific formats (OpenCode, ACP).
747
+
748
+ Returns:
749
+ List of tokonomics ModelInfo, or None if not supported
750
+ """
751
+ ...
752
+
753
+ @abstractmethod
754
+ async def get_modes(self) -> list[ModeCategory]:
755
+ """Get available mode categories for this agent.
756
+
757
+ Returns a list of mode categories that can be switched. Each category
758
+ represents a group of mutually exclusive modes (e.g., permissions,
759
+ models, behavior presets).
760
+
761
+ Different agent types expose different modes:
762
+ - Native Agent: permissions + model selection
763
+ - ClaudeCodeAgent: permissions + model selection
764
+ - ACPAgent: Passthrough from remote server
765
+ - AGUIAgent: model selection (if applicable)
766
+
767
+ Returns:
768
+ List of ModeCategory, empty list if no modes supported
769
+ """
770
+ ...
771
+
772
+ @abstractmethod
773
+ async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
774
+ """Set a mode within a category.
775
+
776
+ Each agent type handles mode switching according to its own semantics:
777
+ - Native Agent: Maps to tool confirmation mode
778
+ - ClaudeCodeAgent: Maps to SDK permission mode
779
+ - ACPAgent: Forwards to remote server
780
+ - AGUIAgent: No-op (no modes supported)
781
+
782
+ Args:
783
+ mode: The mode to activate - either a ModeInfo object or mode ID string.
784
+ If ModeInfo, category_id is extracted from it (unless overridden).
785
+ category_id: Optional category ID. If None and mode is a string,
786
+ uses the first category. If None and mode is ModeInfo,
787
+ uses the mode's category_id.
788
+
789
+ Raises:
790
+ ValueError: If mode_id or category_id is invalid
791
+ """
792
+ ...