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,12 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  from time import perf_counter
7
- from typing import TYPE_CHECKING, Any
7
+ from typing import TYPE_CHECKING, Any, Literal
8
8
  from uuid import uuid4
9
9
 
10
10
  from anyenv.async_run import as_generated
11
11
  import anyio
12
- from toprompt import to_prompt
13
12
 
14
13
  from agentpool.common_types import SupportsRunStream
15
14
  from agentpool.delegation.base_team import BaseTeam
@@ -33,36 +32,6 @@ if TYPE_CHECKING:
33
32
  from agentpool_config.task import Job
34
33
 
35
34
 
36
- async def normalize_stream_for_teams(
37
- node: MessageNode[Any, Any],
38
- *args: Any,
39
- **kwargs: Any,
40
- ) -> AsyncIterator[tuple[MessageNode[Any, Any], RichAgentStreamEvent[Any]]]:
41
- """Normalize any streaming node to yield (node, event) tuples for team composition.
42
-
43
- Args:
44
- node: The streaming node (Agent, Team, etc.)
45
- *args: Arguments to pass to run_stream
46
- **kwargs: Keyword arguments to pass to run_stream
47
-
48
- Yields:
49
- Tuples of (node, event) where node is the MessageNode instance
50
- and event is the streaming event from that node.
51
- """
52
- if not isinstance(node, SupportsRunStream):
53
- msg = f"Node {node.name} does not support streaming"
54
- raise TypeError(msg)
55
-
56
- stream = node.run_stream(*args, **kwargs)
57
- async for item in stream:
58
- if isinstance(item, tuple):
59
- # Already normalized (from Team or other composite node)
60
- yield item
61
- else:
62
- # Raw event (from Agent) - wrap it with the source node
63
- yield (node, item)
64
-
65
-
66
35
  class Team[TDeps = None](BaseTeam[TDeps, Any]):
67
36
  """Group of agents that can execute together."""
68
37
 
@@ -77,8 +46,7 @@ class Team[TDeps = None](BaseTeam[TDeps, Any]):
77
46
  final_prompt = list(prompts)
78
47
  if self.shared_prompt:
79
48
  final_prompt.insert(0, self.shared_prompt)
80
- combined_prompt = "\n".join([await to_prompt(p) for p in final_prompt])
81
- all_nodes = list(await self.pick_agents(combined_prompt))
49
+ all_nodes = list(self.nodes)
82
50
  # Create Talk connections for monitoring this execution
83
51
  execution_talks: list[Talk[Any]] = []
84
52
  for node in all_nodes:
@@ -130,8 +98,7 @@ class Team[TDeps = None](BaseTeam[TDeps, Any]):
130
98
  await queue.put(None)
131
99
 
132
100
  # Get nodes to run
133
- combined_prompt = "\n".join([await to_prompt(p) for p in prompts])
134
- all_nodes = list(await self.pick_agents(combined_prompt))
101
+ all_nodes = list(self.nodes)
135
102
 
136
103
  # Start all agents
137
104
  tasks = [asyncio.create_task(_run(n), name=f"run_{n.name}") for n in all_nodes]
@@ -163,8 +130,8 @@ class Team[TDeps = None](BaseTeam[TDeps, Any]):
163
130
  ) -> ChatMessage[list[Any]]:
164
131
  """Run all agents in parallel and return combined message."""
165
132
  # Prepare prompts and create user message
166
- user_msg, processed_prompts, original_message = await prepare_prompts(*prompts)
167
- self.message_received.emit(user_msg)
133
+ user_msg, processed_prompts = await prepare_prompts(*prompts)
134
+ await self.message_received.emit(user_msg)
168
135
 
169
136
  # Execute team logic
170
137
  result = await self.execute(*processed_prompts, **kwargs)
@@ -195,7 +162,6 @@ class Team[TDeps = None](BaseTeam[TDeps, Any]):
195
162
  user_msg,
196
163
  self,
197
164
  self.connections,
198
- original_message,
199
165
  wait_for_connections,
200
166
  )
201
167
 
@@ -203,7 +169,7 @@ class Team[TDeps = None](BaseTeam[TDeps, Any]):
203
169
  self,
204
170
  *prompts: PromptCompatible,
205
171
  **kwargs: Any,
206
- ) -> AsyncIterator[tuple[MessageNode[Any, Any], RichAgentStreamEvent[Any]]]:
172
+ ) -> AsyncIterator[RichAgentStreamEvent[Any]]:
207
173
  """Stream responses from all team members in parallel.
208
174
 
209
175
  Args:
@@ -211,23 +177,53 @@ class Team[TDeps = None](BaseTeam[TDeps, Any]):
211
177
  kwargs: Additional arguments passed to each agent
212
178
 
213
179
  Yields:
214
- Tuples of (agent, event) where agent is the Agent instance
215
- and event is the streaming event from that agent.
180
+ RichAgentStreamEvent, with member events wrapped in SubAgentEvent
216
181
  """
217
- # Get nodes to run
218
- combined_prompt = "\n".join([await to_prompt(p) for p in prompts])
219
- all_nodes = list(await self.pick_agents(combined_prompt))
182
+ from agentpool.agents.events import SubAgentEvent
220
183
 
221
- # Create list of streams that yield (agent, event) tuples
222
- agent_streams = [
223
- normalize_stream_for_teams(agent, *prompts, **kwargs)
224
- for agent in all_nodes
225
- if isinstance(agent, SupportsRunStream)
226
- ]
227
-
228
- # Merge all agent streams
229
- async for agent_event_tuple in as_generated(agent_streams):
230
- yield agent_event_tuple
184
+ # Get nodes to run
185
+ all_nodes = list(self.nodes)
186
+
187
+ # Create list of streams
188
+ async def wrap_stream(
189
+ node: MessageNode[Any, Any],
190
+ ) -> AsyncIterator[RichAgentStreamEvent[Any]]:
191
+ """Wrap a node's stream events in SubAgentEvent."""
192
+ if not isinstance(node, SupportsRunStream):
193
+ return
194
+ async for event in node.run_stream(*prompts, **kwargs):
195
+ # Handle already-wrapped SubAgentEvents (nested teams)
196
+ if isinstance(event, SubAgentEvent):
197
+ yield SubAgentEvent(
198
+ source_name=event.source_name,
199
+ source_type=event.source_type,
200
+ event=event.event,
201
+ depth=event.depth + 1,
202
+ )
203
+ else:
204
+ # Determine source type based on node type
205
+ from agentpool.delegation.teamrun import TeamRun
206
+
207
+ if isinstance(node, TeamRun):
208
+ source_type: Literal["team_parallel", "team_sequential", "agent"] = (
209
+ "team_sequential"
210
+ )
211
+ elif isinstance(node, BaseTeam):
212
+ source_type = "team_parallel"
213
+ else:
214
+ source_type = "agent"
215
+
216
+ yield SubAgentEvent(
217
+ source_name=node.name,
218
+ source_type=source_type,
219
+ event=event,
220
+ depth=1,
221
+ )
222
+
223
+ streams = [wrap_stream(node) for node in all_nodes]
224
+ # Merge all streams
225
+ async for event in as_generated(streams):
226
+ yield event
231
227
 
232
228
  async def run_job[TJobResult](
233
229
  self,
@@ -311,6 +307,7 @@ if __name__ == "__main__":
311
307
 
312
308
  async def main() -> None:
313
309
  from agentpool import Agent, TeamRun
310
+ from agentpool.agents.events import SubAgentEvent
314
311
 
315
312
  agent_a = Agent(name="A", model="test")
316
313
  agent_b = Agent(name="B", model="test")
@@ -320,7 +317,10 @@ if __name__ == "__main__":
320
317
  outer_team = Team([inner_run, agent_c], name="Parallel")
321
318
 
322
319
  print("Testing Team containing TeamRun...")
323
- async for node, event in outer_team.run_stream("test"):
324
- print(f"{node.name}: {type(event).__name__}")
320
+ async for event in outer_team.run_stream("test"):
321
+ if isinstance(event, SubAgentEvent):
322
+ print(f"[depth={event.depth}] {event.source_name}: {type(event.event).__name__}")
323
+ else:
324
+ print(f"Event: {type(event).__name__}")
325
325
 
326
326
  anyio.run(main)
@@ -9,11 +9,9 @@ from typing import TYPE_CHECKING, Any, Literal, overload
9
9
  from uuid import uuid4
10
10
 
11
11
  import anyio
12
- from pydantic_ai import PartDeltaEvent, TextPartDelta
13
12
 
14
13
  from agentpool.common_types import SupportsRunStream
15
14
  from agentpool.delegation.base_team import BaseTeam
16
- from agentpool.delegation.team import normalize_stream_for_teams
17
15
  from agentpool.log import get_logger
18
16
  from agentpool.messaging import AgentResponse, ChatMessage, TeamResponse
19
17
  from agentpool.messaging.processing import finalize_message, prepare_prompts
@@ -27,7 +25,7 @@ if TYPE_CHECKING:
27
25
 
28
26
  from agentpool import MessageNode
29
27
  from agentpool.agents.events import RichAgentStreamEvent
30
- from agentpool.common_types import PromptCompatible, SupportsStructuredOutput
28
+ from agentpool.common_types import PromptCompatible
31
29
  from agentpool.delegation import AgentPool
32
30
 
33
31
 
@@ -65,9 +63,6 @@ class TeamRun[TDeps, TResult](BaseTeam[TDeps, TResult]):
65
63
  display_name: str | None = None,
66
64
  shared_prompt: str | None = None,
67
65
  validator: MessageNode[Any, TResult],
68
- picker: SupportsStructuredOutput | None = None,
69
- num_picks: int | None = None,
70
- pick_prompt: str | None = None,
71
66
  agent_pool: AgentPool | None = None,
72
67
  ) -> None: ...
73
68
 
@@ -81,9 +76,6 @@ class TeamRun[TDeps, TResult](BaseTeam[TDeps, TResult]):
81
76
  display_name: str | None = None,
82
77
  shared_prompt: str | None = None,
83
78
  validator: None = None,
84
- picker: SupportsStructuredOutput | None = None,
85
- num_picks: int | None = None,
86
- pick_prompt: str | None = None,
87
79
  agent_pool: AgentPool | None = None,
88
80
  ) -> None: ...
89
81
 
@@ -97,9 +89,6 @@ class TeamRun[TDeps, TResult](BaseTeam[TDeps, TResult]):
97
89
  display_name: str | None = None,
98
90
  shared_prompt: str | None = None,
99
91
  validator: MessageNode[Any, TResult] | None = None,
100
- picker: SupportsStructuredOutput | None = None,
101
- num_picks: int | None = None,
102
- pick_prompt: str | None = None,
103
92
  agent_pool: AgentPool | None = None,
104
93
  ) -> None: ...
105
94
 
@@ -112,9 +101,6 @@ class TeamRun[TDeps, TResult](BaseTeam[TDeps, TResult]):
112
101
  display_name: str | None = None,
113
102
  shared_prompt: str | None = None,
114
103
  validator: MessageNode[Any, TResult] | None = None,
115
- picker: SupportsStructuredOutput | None = None,
116
- num_picks: int | None = None,
117
- pick_prompt: str | None = None,
118
104
  agent_pool: AgentPool | None = None,
119
105
  # result_mode: ResultMode = "last",
120
106
  ) -> None:
@@ -124,9 +110,6 @@ class TeamRun[TDeps, TResult](BaseTeam[TDeps, TResult]):
124
110
  description=description,
125
111
  display_name=display_name,
126
112
  shared_prompt=shared_prompt,
127
- picker=picker,
128
- num_picks=num_picks,
129
- pick_prompt=pick_prompt,
130
113
  agent_pool=agent_pool,
131
114
  )
132
115
  self.validator = validator
@@ -147,8 +130,8 @@ class TeamRun[TDeps, TResult](BaseTeam[TDeps, TResult]):
147
130
  ) -> ChatMessage[TResult]:
148
131
  """Run agents sequentially and return combined message."""
149
132
  # Prepare prompts and create user message
150
- user_msg, processed_prompts, original_message = await prepare_prompts(*prompts)
151
- self.message_received.emit(user_msg)
133
+ user_msg, processed_prompts = await prepare_prompts(*prompts)
134
+ await self.message_received.emit(user_msg)
152
135
  # Execute sequential logic
153
136
  message_id = str(uuid4()) # Always generate unique response ID
154
137
  result = await self.execute(*processed_prompts, **kwargs)
@@ -187,7 +170,6 @@ class TeamRun[TDeps, TResult](BaseTeam[TDeps, TResult]):
187
170
  user_msg,
188
171
  self,
189
172
  self.connections,
190
- original_message,
191
173
  wait_for_connections,
192
174
  )
193
175
 
@@ -224,12 +206,9 @@ class TeamRun[TDeps, TResult](BaseTeam[TDeps, TResult]):
224
206
  *prompt: PromptCompatible,
225
207
  **kwargs: Any,
226
208
  ) -> AsyncIterator[Talk[Any] | AgentResponse[Any]]:
227
- from toprompt import to_prompt
228
-
229
209
  connections: list[Talk[Any]] = []
230
210
  try:
231
- combined_prompt = "\n".join([await to_prompt(p) for p in prompt])
232
- all_nodes = list(await self.pick_agents(combined_prompt))
211
+ all_nodes = list(self.nodes)
233
212
  if self.validator:
234
213
  all_nodes.append(self.validator)
235
214
  first = all_nodes[0]
@@ -274,7 +253,7 @@ class TeamRun[TDeps, TResult](BaseTeam[TDeps, TResult]):
274
253
  *prompts: PromptCompatible,
275
254
  require_all: bool = True,
276
255
  **kwargs: Any,
277
- ) -> AsyncIterator[tuple[MessageNode[Any, Any], RichAgentStreamEvent[Any]]]:
256
+ ) -> AsyncIterator[RichAgentStreamEvent[Any]]:
278
257
  """Stream responses through the chain of team members.
279
258
 
280
259
  Args:
@@ -284,50 +263,61 @@ class TeamRun[TDeps, TResult](BaseTeam[TDeps, TResult]):
284
263
  kwargs: Additional arguments passed to each agent
285
264
 
286
265
  Yields:
287
- Tuples of (agent, event) where agent is the Agent instance
288
- and event is the streaming event.
266
+ RichAgentStreamEvent, with member events wrapped in SubAgentEvent
289
267
  """
290
- from agentpool.agents.events import StreamCompleteEvent
268
+ from agentpool.agents.events import StreamCompleteEvent, SubAgentEvent
269
+ from agentpool.delegation.team import Team
291
270
 
292
271
  current_message = prompts
293
- collected_content = []
294
- for agent in self.nodes:
272
+ for node in self.nodes:
295
273
  try:
296
- agent_content = []
297
-
298
- # Use wrapper to normalize all streaming nodes to (agent, event) tuples
299
- if not isinstance(agent, SupportsRunStream):
300
- msg = f"Agent {agent.name} does not support streaming"
274
+ if not isinstance(node, SupportsRunStream):
275
+ msg = f"Node {node.name} does not support streaming"
301
276
  raise TypeError(msg) # noqa: TRY301
302
277
 
303
- stream = normalize_stream_for_teams(agent, *current_message, **kwargs)
304
-
305
- async for agent_event_tuple in stream:
306
- actual_agent, event = agent_event_tuple
307
- match event:
308
- case PartDeltaEvent(delta=TextPartDelta(content_delta=delta)):
309
- agent_content.append(delta)
310
- collected_content.append(delta)
311
- yield (actual_agent, event) # Yield tuple with agent context
312
- case StreamCompleteEvent(message=message):
313
- # Use complete response as input for next agent
314
- current_message = (message.content,)
315
- yield (actual_agent, event) # Yield tuple with agent context
316
- case _:
317
- yield (actual_agent, event) # Yield tuple with agent context
278
+ async for event in node.run_stream(*current_message, **kwargs):
279
+ # Handle already-wrapped SubAgentEvents (nested teams)
280
+ if isinstance(event, SubAgentEvent):
281
+ yield SubAgentEvent(
282
+ source_name=event.source_name,
283
+ source_type=event.source_type,
284
+ event=event.event,
285
+ depth=event.depth + 1,
286
+ )
287
+ else:
288
+ # Determine source type based on node type
289
+ if isinstance(node, Team):
290
+ source_type: Literal["team_parallel", "team_sequential", "agent"] = (
291
+ "team_parallel"
292
+ )
293
+ elif isinstance(node, BaseTeam):
294
+ source_type = "team_sequential"
295
+ else:
296
+ source_type = "agent"
297
+
298
+ yield SubAgentEvent(
299
+ source_name=node.name,
300
+ source_type=source_type,
301
+ event=event,
302
+ )
303
+
304
+ # Extract content for next agent in chain
305
+ if isinstance(event, StreamCompleteEvent):
306
+ current_message = (event.message.content,)
318
307
 
319
308
  except Exception as e:
320
309
  if require_all:
321
- msg = f"Chain broken at {agent.name}: {e}"
310
+ msg = f"Chain broken at {node.name}: {e}"
322
311
  logger.exception(msg)
323
312
  raise ValueError(msg) from e
324
- logger.warning("Chain handler failed", name=agent.name, error=e)
313
+ logger.warning("Chain handler failed", name=node.name, error=e)
325
314
 
326
315
 
327
316
  if __name__ == "__main__":
328
317
 
329
318
  async def main() -> None:
330
319
  from agentpool import Agent, Team
320
+ from agentpool.agents.events import SubAgentEvent
331
321
 
332
322
  agent1 = Agent(name="Agent1", model="test")
333
323
  agent2 = Agent(name="Agent2", model="test")
@@ -336,8 +326,13 @@ if __name__ == "__main__":
336
326
  outer_run = TeamRun([inner_team, agent3], name="Sequential")
337
327
  print("Testing TeamRun containing Team...")
338
328
  try:
339
- async for node, event in outer_run.run_stream("test"):
340
- print(f"{node.name}: {type(event).__name__}")
329
+ async for event in outer_run.run_stream("test"):
330
+ if isinstance(event, SubAgentEvent):
331
+ print(
332
+ f"[depth={event.depth}] {event.source_name}: {type(event.event).__name__}"
333
+ )
334
+ else:
335
+ print(f"Event: {type(event).__name__}")
341
336
  except Exception as e: # noqa: BLE001
342
337
  print(f"Error: {e}")
343
338
 
@@ -42,11 +42,14 @@ async def run_agent(
42
42
  ) -> Any:
43
43
  """Run prompt through agent and return result."""
44
44
  async with Agent[Any, str](**kwargs) as agent:
45
+ # Convert to structured output agent if output_type specified
46
+ final = agent.to_structured(output_type) if output_type is not None else agent
47
+
45
48
  if image_url:
46
49
  image = ImageUrl(url=image_url)
47
- result = await agent.run(prompt, image, output_type=output_type)
50
+ result = await final.run(prompt, image)
48
51
  else:
49
- result = await agent.run(prompt, output_type=output_type)
52
+ result = await final.run(prompt)
50
53
  return result.content
51
54
 
52
55
 
@@ -76,5 +79,8 @@ def run_agent_sync(
76
79
  **kwargs: Unpack[AgentKwargs],
77
80
  ) -> Any:
78
81
  """Sync wrapper for run_agent."""
79
- coro = run_agent(prompt, image_url, output_type=output_type, **kwargs) # type: ignore
80
- return run_sync(coro)
82
+
83
+ async def _run() -> Any:
84
+ return await run_agent(prompt, image_url, output_type=output_type, **kwargs) # type: ignore
85
+
86
+ return run_sync(_run())
@@ -28,7 +28,7 @@ from agentpool.log import get_logger
28
28
  from agentpool.mcp_server.constants import MCP_TO_LOGGING
29
29
  from agentpool.mcp_server.helpers import extract_text_content, mcp_tool_to_fn_schema
30
30
  from agentpool.mcp_server.message_handler import MCPMessageHandler
31
- from agentpool.tools.base import Tool
31
+ from agentpool.tools.base import FunctionTool
32
32
  from agentpool.utils.signatures import create_modified_signature
33
33
  from agentpool_config.mcp_server import (
34
34
  SSEMCPServerConfig,
@@ -56,6 +56,7 @@ if TYPE_CHECKING:
56
56
  Implementation,
57
57
  Prompt as MCPPrompt,
58
58
  Resource as MCPResource,
59
+ ResourceTemplate,
59
60
  TextResourceContents,
60
61
  Tool as MCPTool,
61
62
  )
@@ -106,12 +107,17 @@ class MCPClient:
106
107
  """Check if client is connected by examining session state."""
107
108
  return self._client.is_connected()
108
109
 
110
+ def _ensure_connected(self) -> None:
111
+ """Ensure client is connected, raise RuntimeError if not."""
112
+ if not self.connected:
113
+ msg = "Not connected to MCP server"
114
+ raise RuntimeError(msg)
115
+
109
116
  async def __aenter__(self) -> Self:
110
117
  """Enter context manager."""
111
118
  try:
112
119
  # First attempt with configured auth
113
120
  await self._client.__aenter__() # type: ignore[no-untyped-call]
114
-
115
121
  except Exception as first_error:
116
122
  # OAuth fallback for HTTP/SSE if not already using OAuth
117
123
  if not isinstance(self.config, StdioMCPServerConfig) and not self.config.auth.oauth:
@@ -241,31 +247,20 @@ class MCPClient:
241
247
 
242
248
  Tools are filtered based on the server config's enabled_tools/disabled_tools settings.
243
249
  """
244
- if not self.connected:
245
- msg = "Not connected to MCP server"
246
- raise RuntimeError(msg)
247
-
250
+ self._ensure_connected()
248
251
  try:
249
252
  tools = await self._client.list_tools()
250
- # Filter tools based on config
251
- filtered_tools = [t for t in tools if self.config.is_tool_allowed(t.name)]
252
- logger.debug(
253
- "Listed tools from MCP server",
254
- total_tools=len(tools),
255
- filtered_tools=len(filtered_tools),
256
- )
253
+ filtered = [t for t in tools if self.config.is_tool_allowed(t.name)]
254
+ logger.debug("Listed tools from MCP server", total=len(tools), filtered=len(filtered))
257
255
  except Exception as e: # noqa: BLE001
258
256
  logger.warning("Failed to list tools", error=e)
259
257
  return []
260
258
  else:
261
- return filtered_tools
259
+ return filtered
262
260
 
263
261
  async def list_prompts(self) -> list[MCPPrompt]:
264
262
  """Get available prompts from the server."""
265
- if not self.connected:
266
- msg = "Not connected to MCP server"
267
- raise RuntimeError(msg)
268
-
263
+ self._ensure_connected()
269
264
  try:
270
265
  return await self._client.list_prompts()
271
266
  except Exception as e: # noqa: BLE001
@@ -274,31 +269,69 @@ class MCPClient:
274
269
 
275
270
  async def list_resources(self) -> list[MCPResource]:
276
271
  """Get available resources from the server."""
277
- if not self.connected:
278
- msg = "Not connected to MCP server"
279
- raise RuntimeError(msg)
280
-
272
+ self._ensure_connected()
281
273
  try:
282
274
  return await self._client.list_resources()
283
275
  except Exception as e:
284
276
  msg = f"Failed to list resources: {e}"
285
277
  raise RuntimeError(msg) from e
286
278
 
279
+ async def list_resource_templates(self) -> list[ResourceTemplate]:
280
+ """Get available resource templates from the server.
281
+
282
+ Resource templates are URI patterns with placeholders that can be
283
+ expanded to create concrete resource URIs.
284
+
285
+ Example template: "file:///{path}" -> expand with path="config.json"
286
+ -> "file:///config.json" which can then be read.
287
+
288
+ TODO: Integrate resource templates into the ResourceInfo system.
289
+ Currently templates are separate from resources - we need to decide:
290
+ - Should templates appear in list_resources() with a flag?
291
+ - Should ResourceInfo.read() accept kwargs for template expansion?
292
+ - Should templates have their own ResourceTemplateInfo class?
293
+
294
+ Returns:
295
+ List of resource templates from the server
296
+ """
297
+ self._ensure_connected()
298
+ try:
299
+ return await self._client.list_resource_templates()
300
+ except Exception as e:
301
+ msg = f"Failed to list resource templates: {e}"
302
+ raise RuntimeError(msg) from e
303
+
304
+ async def read_resource(self, uri: str) -> list[TextResourceContents | BlobResourceContents]:
305
+ """Read resource content by URI.
306
+
307
+ Args:
308
+ uri: URI of the resource to read
309
+
310
+ Returns:
311
+ List of resource contents (text or blob)
312
+
313
+ Raises:
314
+ RuntimeError: If not connected or read fails
315
+ """
316
+ self._ensure_connected()
317
+ try:
318
+ return await self._client.read_resource(uri)
319
+ except Exception as e:
320
+ msg = f"Failed to read resource {uri!r}: {e}"
321
+ raise RuntimeError(msg) from e
322
+
287
323
  async def get_prompt(
288
324
  self, name: str, arguments: dict[str, str] | None = None
289
325
  ) -> GetPromptResult:
290
326
  """Get a specific prompt's content."""
291
- if not self.connected:
292
- msg = "Not connected to MCP server"
293
- raise RuntimeError(msg)
294
-
327
+ self._ensure_connected()
295
328
  try:
296
329
  return await self._client.get_prompt_mcp(name, arguments)
297
330
  except Exception as e:
298
331
  msg = f"Failed to get prompt {name!r}: {e}"
299
332
  raise RuntimeError(msg) from e
300
333
 
301
- def convert_tool(self, tool: MCPTool) -> Tool:
334
+ def convert_tool(self, tool: MCPTool) -> FunctionTool:
302
335
  """Create a properly typed callable from MCP tool schema."""
303
336
 
304
337
  async def tool_callable(
@@ -319,7 +352,6 @@ class MCPClient:
319
352
  schema = mcp_tool_to_fn_schema(tool)
320
353
  fn_schema = FunctionSchema.from_dict(schema)
321
354
  sig = fn_schema.to_python_signature()
322
-
323
355
  tool_callable.__signature__ = create_modified_signature( # type: ignore[attr-defined]
324
356
  sig, inject={"ctx": RunContext, "agent_ctx": AgentContext}
325
357
  )
@@ -331,7 +363,7 @@ class MCPClient:
331
363
  tool_callable.__annotations__ = annotations
332
364
  tool_callable.__name__ = tool.name
333
365
  tool_callable.__doc__ = tool.description or "No description provided."
334
- return Tool.from_callable(tool_callable, source="mcp")
366
+ return FunctionTool.from_callable(tool_callable, source="mcp")
335
367
 
336
368
  async def call_tool(
337
369
  self,
@@ -341,10 +373,7 @@ class MCPClient:
341
373
  agent_ctx: AgentContext[Any] | None = None,
342
374
  ) -> ToolReturn | str | Any:
343
375
  """Call an MCP tool with full PydanticAI return type support."""
344
- if not self.connected:
345
- msg = "Not connected to MCP server"
346
- raise RuntimeError(msg)
347
-
376
+ self._ensure_connected()
348
377
  # Create progress handler that bridges to AgentContext if available
349
378
  progress_handler = None
350
379
  if agent_ctx:
@@ -385,9 +414,18 @@ class MCPClient:
385
414
 
386
415
  self._current_elicitation_handler = elicitation_handler
387
416
 
417
+ # Prepare metadata to pass tool_call_id to the MCP server
418
+ meta = None
419
+ if agent_ctx and agent_ctx.tool_call_id:
420
+ # Use the same key that tool_bridge expects: "claudecode/toolUseId"
421
+ # Ensure it's a string (handles both real values and mocks)
422
+ tool_call_id = str(agent_ctx.tool_call_id) if agent_ctx.tool_call_id else None
423
+ if tool_call_id:
424
+ meta = {"claudecode/toolUseId": tool_call_id}
425
+
388
426
  try:
389
427
  result = await self._client.call_tool(
390
- name, arguments, progress_handler=progress_handler
428
+ name, arguments, progress_handler=progress_handler, meta=meta
391
429
  )
392
430
  content = await self._from_mcp_content(result.content)
393
431
  # Decision logic for return type
@@ -424,10 +462,7 @@ class MCPClient:
424
462
  if __name__ == "__main__":
425
463
  path = "/home/phil65/dev/oss/agentpool/tests/mcp_server/server.py"
426
464
  # path = Path(__file__).parent / "test_mcp_server.py"
427
- config = StdioMCPServerConfig(
428
- command="uv",
429
- args=["run", str(path)],
430
- )
465
+ config = StdioMCPServerConfig(command="uv", args=["run", str(path)])
431
466
 
432
467
  async def main() -> None:
433
468
  async with MCPClient(config=config) as mcp_client: