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
@@ -4,39 +4,54 @@ 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 anyenv.signals import BoundSignal
11
- from exxec import LocalExecutionEnvironment
12
+ from anyenv import MultiEventHandler, method_spawner
13
+ from anyenv.signals import BoundSignal, Signal
14
+ import anyio
12
15
 
13
- 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
14
18
  from agentpool.log import get_logger
15
- from agentpool.messaging import MessageHistory, MessageNode
19
+ from agentpool.messaging import ChatMessage, MessageHistory, MessageNode
20
+ from agentpool.prompts.convert import convert_prompts
16
21
  from agentpool.tools.manager import ToolManager
22
+ from agentpool.utils.inspection import call_with_context
23
+ from agentpool.utils.now import get_now
17
24
 
18
25
 
19
26
  if TYPE_CHECKING:
20
27
  from collections.abc import AsyncIterator, Sequence
28
+ from datetime import datetime
21
29
 
22
- from evented.configs import EventConfig
30
+ from evented_config import EventConfig
23
31
  from exxec import ExecutionEnvironment
32
+ from pydantic_ai import UserContent
24
33
  from slashed import BaseCommand, CommandStore
25
34
  from tokonomics.model_discovery.model_info import ModelInfo
26
35
 
27
36
  from acp.schema import AvailableCommandsUpdate, ConfigOptionUpdate
37
+ from agentpool.agents.agent import Agent
28
38
  from agentpool.agents.context import AgentContext
29
39
  from agentpool.agents.events import RichAgentStreamEvent
30
40
  from agentpool.agents.modes import ModeCategory, ModeInfo
31
41
  from agentpool.common_types import (
42
+ AgentName,
32
43
  BuiltinEventHandlerType,
33
44
  IndividualEventHandler,
34
45
  MCPServerStatus,
46
+ ProcessorCallback,
47
+ PromptCompatible,
35
48
  )
36
- from agentpool.delegation import AgentPool
49
+ from agentpool.delegation import AgentPool, Team, TeamRun
50
+ from agentpool.messaging import ChatMessage
37
51
  from agentpool.talk.stats import MessageStats
38
52
  from agentpool.ui.base import InputProvider
39
53
  from agentpool_config.mcp_server import MCPServerConfig
54
+ from agentpool_config.nodes import ToolConfirmationMode
40
55
 
41
56
  # Union type for state updates emitted via state_updated signal
42
57
  type StateUpdate = ModeInfo | ModelInfo | AvailableCommandsUpdate | ConfigOptionUpdate
@@ -45,9 +60,6 @@ if TYPE_CHECKING:
45
60
  logger = get_logger(__name__)
46
61
 
47
62
 
48
- ToolConfirmationMode = Literal["always", "never", "per_tool"]
49
-
50
-
51
63
  class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
52
64
  """Base class for Agent, ACPAgent, AGUIAgent, and ClaudeCodeAgent.
53
65
 
@@ -60,8 +72,37 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
60
72
  - _input_provider: Provider for user input/confirmations
61
73
  - env: ExecutionEnvironment for running code/commands
62
74
  - context property: Returns NodeContext for the agent
75
+
76
+ Signals:
77
+ - run_failed: Emitted when agent execution fails with error details
63
78
  """
64
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
+
65
106
  def __init__(
66
107
  self,
67
108
  *,
@@ -97,6 +138,11 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
97
138
  event_handlers: Event handlers for this agent
98
139
  commands: Slash commands to register with this agent
99
140
  """
141
+ from exxec import LocalExecutionEnvironment
142
+ from slashed import CommandStore
143
+
144
+ from agentpool_commands import get_commands
145
+
100
146
  super().__init__(
101
147
  name=name,
102
148
  description=description,
@@ -106,10 +152,14 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
106
152
  enable_logging=enable_logging,
107
153
  event_configs=event_configs,
108
154
  )
155
+ self._infinite = False
156
+ self._background_task: asyncio.Task[ChatMessage[Any]] | None = None
109
157
 
110
158
  # Shared infrastructure - previously duplicated in all 4 agents
111
159
  self._event_queue: asyncio.Queue[RichAgentStreamEvent[Any]] = asyncio.Queue()
112
- 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)
113
163
  self.env = env or LocalExecutionEnvironment()
114
164
  self._input_provider = input_provider
115
165
  self._output_type: type[TResult] = output_type
@@ -119,22 +169,17 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
119
169
  self.event_handler: MultiEventHandler[IndividualEventHandler] = MultiEventHandler(
120
170
  resolved_handlers
121
171
  )
122
-
123
- # Cancellation infrastructure
124
172
  self._cancelled = False
125
173
  self._current_stream_task: asyncio.Task[Any] | None = None
126
-
174
+ # Deferred initialization support - subclasses set True in __aenter__,
175
+ # override ensure_initialized() to do actual connection
176
+ self._connect_pending: bool = False
127
177
  # State change signal - emitted when mode/model/commands change
128
178
  # Uses union type for different state update kinds
129
179
  self.state_updated: BoundSignal[StateUpdate] = BoundSignal()
130
-
131
- # Command store for slash commands
132
- from slashed import CommandStore
133
-
134
- from agentpool_commands import get_commands
135
-
136
180
  self._command_store: CommandStore = CommandStore()
137
-
181
+ # Initialize store (registers builtin help/exit commands)
182
+ self._command_store._initialize_sync()
138
183
  # Register default agent commands
139
184
  for command in get_commands():
140
185
  self._command_store.register_command(command)
@@ -144,11 +189,78 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
144
189
  for command in commands:
145
190
  self._command_store.register_command(command)
146
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
+
147
245
  @property
148
246
  def command_store(self) -> CommandStore:
149
247
  """Get the command store for slash commands."""
150
248
  return self._command_store
151
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)
263
+
152
264
  @abstractmethod
153
265
  def get_context(self, data: Any = None) -> AgentContext[Any]:
154
266
  """Create a new context for this agent.
@@ -176,17 +288,275 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
176
288
  """
177
289
  ...
178
290
 
179
- @abstractmethod
180
- def run_stream(
291
+ async def run_iter(
292
+ self,
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(
181
325
  self,
182
- *prompt: Any,
326
+ *prompt: PromptCompatible,
327
+ max_count: int | None = None,
328
+ interval: float = 1.0,
183
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,
184
418
  ) -> AsyncIterator[RichAgentStreamEvent[TResult]]:
185
419
  """Run agent with streaming output.
186
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
+
187
547
  Args:
188
- *prompt: Input prompts
189
- **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
190
560
 
191
561
  Yields:
192
562
  Stream events during execution
@@ -201,6 +571,25 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
201
571
  """
202
572
  self.tool_confirmation_mode = mode
203
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
+
204
593
  def is_cancelled(self) -> bool:
205
594
  """Check if the agent has been cancelled.
206
595
 
@@ -282,6 +671,70 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
282
671
 
283
672
  return result
284
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
+
285
738
  @abstractmethod
286
739
  async def get_available_models(self) -> list[ModelInfo] | None:
287
740
  """Get available models for this agent.
@@ -298,18 +751,18 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
298
751
  ...
299
752
 
300
753
  @abstractmethod
301
- def get_modes(self) -> list[ModeCategory]:
754
+ async def get_modes(self) -> list[ModeCategory]:
302
755
  """Get available mode categories for this agent.
303
756
 
304
757
  Returns a list of mode categories that can be switched. Each category
305
758
  represents a group of mutually exclusive modes (e.g., permissions,
306
- behavior presets).
759
+ models, behavior presets).
307
760
 
308
761
  Different agent types expose different modes:
309
- - Native Agent: Tool confirmation modes (default, acceptEdits)
310
- - ClaudeCodeAgent: Claude Code SDK modes (plan, code, etc.)
762
+ - Native Agent: permissions + model selection
763
+ - ClaudeCodeAgent: permissions + model selection
311
764
  - ACPAgent: Passthrough from remote server
312
- - AGUIAgent: Empty list (no modes)
765
+ - AGUIAgent: model selection (if applicable)
313
766
 
314
767
  Returns:
315
768
  List of ModeCategory, empty list if no modes supported