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
@@ -17,7 +17,9 @@ from pathlib import Path
17
17
  from typing import TYPE_CHECKING, Any
18
18
 
19
19
  from pydantic import HttpUrl
20
+ from pydantic_ai import RunContext # noqa: TC002
20
21
 
22
+ from agentpool.agents.context import AgentContext # noqa: TC001
21
23
  from agentpool.log import get_logger
22
24
  from agentpool.mcp_server.client import MCPClient
23
25
  from agentpool.mcp_server.registries.official_registry_client import (
@@ -28,7 +30,10 @@ from agentpool.resource_providers import ResourceProvider
28
30
 
29
31
 
30
32
  if TYPE_CHECKING:
31
- from agentpool.agents.context import AgentContext
33
+ from collections.abc import Sequence
34
+
35
+ from fastmcp.client.sampling import SamplingHandler
36
+
32
37
  from agentpool.mcp_server.registries.official_registry_client import RegistryServer
33
38
  from agentpool.tools.base import Tool
34
39
  from agentpool_config.mcp_server import MCPServerConfig
@@ -58,6 +63,7 @@ class MCPDiscoveryToolset(ResourceProvider):
58
63
  registry_url: str = "https://registry.modelcontextprotocol.io",
59
64
  allowed_servers: list[str] | None = None,
60
65
  blocked_servers: list[str] | None = None,
66
+ sampling_callback: SamplingHandler[Any, Any] | None = None,
61
67
  ) -> None:
62
68
  """Initialize the MCP Discovery toolset.
63
69
 
@@ -66,6 +72,7 @@ class MCPDiscoveryToolset(ResourceProvider):
66
72
  registry_url: Base URL for the MCP registry API
67
73
  allowed_servers: If set, only these server names can be used
68
74
  blocked_servers: Server names that cannot be used
75
+ sampling_callback: Callback for MCP sampling requests
69
76
  """
70
77
  super().__init__(name=name)
71
78
  self._registry_url = registry_url
@@ -75,6 +82,7 @@ class MCPDiscoveryToolset(ResourceProvider):
75
82
  self._tools_cache: dict[str, list[dict[str, Any]]] = {}
76
83
  self._allowed_servers = set(allowed_servers) if allowed_servers else None
77
84
  self._blocked_servers = set(blocked_servers) if blocked_servers else set()
85
+ self._sampling_callback = sampling_callback
78
86
  self._tools: list[Tool] | None = None
79
87
  # Lazy-loaded semantic search components
80
88
  self._db: Any = None
@@ -191,7 +199,7 @@ class MCPDiscoveryToolset(ResourceProvider):
191
199
  del self._connections[server_name]
192
200
 
193
201
  config = await self._get_server_config(server_name)
194
- client = MCPClient(config=config)
202
+ client = MCPClient(config=config, sampling_callback=self._sampling_callback)
195
203
  await client.__aenter__()
196
204
  self._connections[server_name] = client
197
205
  logger.info("Connected to MCP server", server=server_name)
@@ -207,7 +215,7 @@ class MCPDiscoveryToolset(ResourceProvider):
207
215
  logger.warning("Error closing connection", server=name, error=e)
208
216
  self._connections.clear()
209
217
 
210
- async def get_tools(self) -> list[Tool]:
218
+ async def get_tools(self) -> Sequence[Tool]:
211
219
  """Get the discovery tools."""
212
220
  if self._tools is not None:
213
221
  return self._tools
@@ -387,6 +395,7 @@ class MCPDiscoveryToolset(ResourceProvider):
387
395
 
388
396
  async def call_mcp_tool( # noqa: D417
389
397
  self,
398
+ ctx: RunContext,
390
399
  agent_ctx: AgentContext,
391
400
  server_name: str,
392
401
  tool_name: str,
@@ -397,6 +406,9 @@ class MCPDiscoveryToolset(ResourceProvider):
397
406
  Use this to execute a specific tool on an MCP server. The server
398
407
  connection is reused if already established.
399
408
 
409
+ This properly supports progress reporting, elicitation, and sampling
410
+ through the AgentContext integration.
411
+
400
412
  Args:
401
413
  server_name: Name of the MCP server (e.g., "com.github/github")
402
414
  tool_name: Name of the tool to call
@@ -416,21 +428,15 @@ class MCPDiscoveryToolset(ResourceProvider):
416
428
  try:
417
429
  client = await self._get_connection(server_name)
418
430
 
419
- # Extract text content from result
420
- from mcp.types import TextContent
421
-
422
- result = await client._client.call_tool(tool_name, arguments or {})
423
-
424
- # Process result content
425
- text_parts = [
426
- content.text for content in result.content if isinstance(content, TextContent)
427
- ]
428
-
429
- if text_parts:
430
- return "\n".join(text_parts)
431
+ # Use MCPClient.call_tool which handles progress, elicitation, and sampling
432
+ return await client.call_tool(
433
+ name=tool_name,
434
+ run_context=ctx,
435
+ arguments=arguments or {},
436
+ agent_ctx=agent_ctx,
437
+ )
431
438
 
432
- # Return raw result if no text content
433
- return str(result)
439
+ # Result is already processed by MCPClient (ToolReturn, str, or structured data)
434
440
 
435
441
  except MCPRegistryError as e:
436
442
  return f"Error: {e}"
@@ -452,3 +458,54 @@ class MCPDiscoveryToolset(ResourceProvider):
452
458
  self._tmpdir = None
453
459
  self._db = None
454
460
  self._table = None
461
+
462
+
463
+ if __name__ == "__main__":
464
+ import asyncio
465
+
466
+ from agentpool.agents.agent import Agent
467
+
468
+ async def main() -> None:
469
+ """End-to-end example: Add MCP discovery toolset to an agent and call a tool."""
470
+ # Create the discovery toolset
471
+ toolset = MCPDiscoveryToolset(
472
+ name="mcp_discovery",
473
+ # Optionally restrict to specific servers for safety
474
+ # allowed_servers=["@modelcontextprotocol/server-everything"],
475
+ )
476
+
477
+ # Create an AgentPool agent with the toolset
478
+ agent = Agent(
479
+ model="openai:gpt-4o",
480
+ system_prompt="""
481
+ You are a helpful assistant with access to MCP servers.
482
+ Use the MCP discovery tools to search for and use tools from the MCP ecosystem.
483
+ """,
484
+ toolsets=[toolset],
485
+ )
486
+
487
+ async with agent:
488
+ # Example 1: Search for servers
489
+ print("\n=== Example 1: Search for file system servers ===")
490
+ result = await agent.run(
491
+ "Search for MCP servers related to file systems and show me the top 3 results"
492
+ )
493
+ print(result.data)
494
+
495
+ # Example 2: List tools from a specific server
496
+ print("\n=== Example 2: List tools from a server ===")
497
+ result = await agent.run(
498
+ "List the tools available on the '@modelcontextprotocol/server-everything' server"
499
+ )
500
+ print(result.data)
501
+
502
+ # Example 3: Call a tool (using a safe, read-only tool for demo)
503
+ print("\n=== Example 3: Call a tool ===")
504
+ result = await agent.run(
505
+ """Use the MCP discovery to call the 'echo' tool from
506
+ '@modelcontextprotocol/server-everything' with the argument
507
+ message='Hello from MCP Discovery!'"""
508
+ )
509
+ print(result.data)
510
+
511
+ asyncio.run(main())
@@ -12,6 +12,8 @@ from agentpool.resource_providers import ResourceProvider
12
12
 
13
13
 
14
14
  if TYPE_CHECKING:
15
+ from collections.abc import Sequence
16
+ from contextlib import AbstractAsyncContextManager
15
17
  from types import TracebackType
16
18
 
17
19
  from mcp import ClientSession
@@ -44,15 +46,14 @@ class McpRunTools(ResourceProvider):
44
46
  self.client = Client(session_id=id_, config=config)
45
47
  self._tools: list[Tool] | None = None
46
48
  self._session: ClientSession | None = None
47
- self._mcp_client_ctx: Any = None # Context manager for persistent connection
49
+ # Context manager for persistent connection
50
+ self._mcp_client_ctx: AbstractAsyncContextManager[ClientSession] | None = None
48
51
 
49
52
  async def __aenter__(self) -> Self:
50
53
  """Start persistent SSE connection."""
51
- # Create MCPClient from mcp_run
52
54
  mcp_client = self.client.mcp_sse()
53
55
  self._mcp_client_ctx = mcp_client.connect()
54
56
  self._session = await self._mcp_client_ctx.__aenter__()
55
-
56
57
  # Set up notification handler for tool changes
57
58
  # The MCP ClientSession dispatches notifications via _received_notification
58
59
  # We monkey-patch it to intercept ToolListChangedNotification
@@ -64,7 +65,6 @@ class McpRunTools(ResourceProvider):
64
65
  if isinstance(notification.root, ToolListChangedNotification):
65
66
  logger.info("MCP.run tool list changed notification received")
66
67
  await self._on_tools_changed()
67
- # Call original handler
68
68
  await original_handler(notification)
69
69
 
70
70
  self._session._received_notification = notification_handler # type: ignore[method-assign]
@@ -89,7 +89,7 @@ class McpRunTools(ResourceProvider):
89
89
  self._tools = None
90
90
  await self.tools_changed.emit(self.create_change_event("tools"))
91
91
 
92
- async def get_tools(self) -> list[Tool]:
92
+ async def get_tools(self) -> Sequence[Tool]:
93
93
  """Get tools from MCP.run."""
94
94
  # Return cached tools if available
95
95
  if self._tools is not None:
@@ -97,8 +97,7 @@ class McpRunTools(ResourceProvider):
97
97
 
98
98
  self._tools = []
99
99
  for name, tool in self.client.tools.items():
100
- # Capture session for use in tool calls
101
- session = self._session
100
+ session = self._session # Capture session for use in tool calls
102
101
 
103
102
  async def run(
104
103
  tool_name: str = name,
@@ -113,11 +112,9 @@ class McpRunTools(ResourceProvider):
113
112
  return await new_session.call_tool(tool_name, arguments=input_dict) # type: ignore[no-any-return]
114
113
 
115
114
  run.__name__ = name
116
- wrapped_tool = self.create_tool(
117
- run, schema_override=cast(OpenAIFunctionDefinition, tool.input_schema)
118
- )
115
+ schema = cast(OpenAIFunctionDefinition, tool.input_schema)
116
+ wrapped_tool = self.create_tool(run, schema_override=schema)
119
117
  self._tools.append(wrapped_tool)
120
-
121
118
  return self._tools
122
119
 
123
120
  async def refresh_tools(self) -> None:
@@ -11,7 +11,7 @@ from agentpool.resource_providers import ResourceProvider
11
11
 
12
12
 
13
13
  if TYPE_CHECKING:
14
- from collections.abc import Mapping
14
+ from collections.abc import Mapping, Sequence
15
15
 
16
16
  from agentpool.tools.base import Tool
17
17
 
@@ -19,6 +19,35 @@ if TYPE_CHECKING:
19
19
  logger = get_logger(__name__)
20
20
 
21
21
 
22
+ def get_schema(channel_names: list[str] | None) -> OpenAIFunctionDefinition:
23
+ properties: dict[str, Any] = {
24
+ "message": {"type": "string", "description": "The notification message body"},
25
+ "title": {"type": "string", "description": "Optional notification title"},
26
+ }
27
+
28
+ if channel_names:
29
+ properties["channel"] = {
30
+ "type": "string",
31
+ "enum": channel_names,
32
+ "description": "Send to a specific channel. If not specified, sends to all channels.",
33
+ }
34
+ else:
35
+ properties["channel"] = {
36
+ "type": "string",
37
+ "description": "Send to a specific channel. If not specified, sends to all channels.",
38
+ }
39
+
40
+ return OpenAIFunctionDefinition(
41
+ name="send_notification",
42
+ description=(
43
+ "Send a notification via configured channels. "
44
+ "Specify a channel name to send to that channel only, "
45
+ "or omit to broadcast to all channels."
46
+ ),
47
+ parameters={"type": "object", "properties": properties, "required": ["message"]},
48
+ )
49
+
50
+
22
51
  class NotificationsTools(ResourceProvider):
23
52
  """Provider for Apprise-based notification tools.
24
53
 
@@ -53,44 +82,15 @@ class NotificationsTools(ResourceProvider):
53
82
  self._apprise.add(url, tag=channel_name)
54
83
  logger.debug("Added notification URL", channel=channel_name, url=url[:30] + "...")
55
84
 
56
- async def get_tools(self) -> list[Tool]:
85
+ async def get_tools(self) -> Sequence[Tool]:
57
86
  """Get notification tools with dynamic schema based on configured channels."""
58
87
  if self._tools is not None:
59
88
  return self._tools
60
89
 
61
90
  channel_names = sorted(self.channels.keys())
62
- # Build schema with enum for available channels
63
- properties: dict[str, Any] = {
64
- "message": {"type": "string", "description": "The notification message body"},
65
- "title": {"type": "string", "description": "Optional notification title"},
66
- }
67
-
68
- if channel_names:
69
- properties["channel"] = {
70
- "type": "string",
71
- "enum": channel_names,
72
- "description": "Send to a specific channel. If not specified, sends to all channels.", # noqa: E501
73
- }
74
- else:
75
- properties["channel"] = {
76
- "type": "string",
77
- "description": "Send to a specific channel. If not specified, sends to all channels.", # noqa: E501
78
- }
79
-
80
- schema_override = OpenAIFunctionDefinition(
81
- name="send_notification",
82
- description=(
83
- "Send a notification via configured channels. "
84
- "Specify a channel name to send to that channel only, "
85
- "or omit to broadcast to all channels."
86
- ),
87
- parameters={"type": "object", "properties": properties, "required": ["message"]},
88
- )
89
-
91
+ schema = get_schema(channel_names)
90
92
  self._tools = [
91
- self.create_tool(
92
- self.send_notification, schema_override=schema_override, open_world=True
93
- )
93
+ self.create_tool(self.send_notification, schema_override=schema, open_world=True)
94
94
  ]
95
95
  return self._tools
96
96
 
@@ -13,6 +13,8 @@ from agentpool.resource_providers import ResourceProvider
13
13
 
14
14
 
15
15
  if TYPE_CHECKING:
16
+ from collections.abc import Sequence
17
+
16
18
  import httpx
17
19
  from upathtools import JoinablePathLike
18
20
 
@@ -41,7 +43,7 @@ class OpenAPITools(ResourceProvider):
41
43
  self._operations: dict[str, Any] = {}
42
44
  self._factory: OpenAPICallableFactory | None = None
43
45
 
44
- async def get_tools(self) -> list[Tool]:
46
+ async def get_tools(self) -> Sequence[Tool]:
45
47
  """Get all API operations as tools."""
46
48
  if not self._spec:
47
49
  await self._load_spec()
@@ -9,6 +9,8 @@ from agentpool.resource_providers import ResourceProvider
9
9
 
10
10
 
11
11
  if TYPE_CHECKING:
12
+ from collections.abc import Sequence
13
+
12
14
  from searchly.base import (
13
15
  CountryCode,
14
16
  LanguageCode,
@@ -178,7 +180,7 @@ class SearchTools(ResourceProvider):
178
180
 
179
181
  return formatted
180
182
 
181
- async def get_tools(self) -> list[Tool]:
183
+ async def get_tools(self) -> Sequence[Tool]:
182
184
  """Get search tools from configured providers."""
183
185
  tools: list[Tool] = []
184
186
  if self._web_provider:
@@ -1,33 +0,0 @@
1
- """Models for resource information."""
2
-
3
- from __future__ import annotations
4
-
5
- from dataclasses import dataclass
6
- from typing import TYPE_CHECKING, Self
7
-
8
-
9
- if TYPE_CHECKING:
10
- from mcp.types import Resource as MCPResource
11
-
12
-
13
- @dataclass
14
- class ResourceInfo:
15
- """Information about an available resource.
16
-
17
- This class provides essential information about a resource that can be loaded.
18
- Use the resource name with load_resource() to access the actual content.
19
- """
20
-
21
- name: str
22
- """Name of the resource, use this with load_resource()"""
23
-
24
- uri: str
25
- """URI identifying the resource location"""
26
-
27
- description: str | None = None
28
- """Optional description of the resource's content or purpose"""
29
-
30
- @classmethod
31
- async def from_mcp_resource(cls, resource: MCPResource) -> Self:
32
- """Create ResourceInfo from MCP resource."""
33
- return cls(name=resource.name, uri=str(resource.uri), description=resource.description)
@@ -1,43 +0,0 @@
1
- """ACP resource providers."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import TYPE_CHECKING
6
-
7
- from exxec.acp_provider import ACPExecutionEnvironment
8
-
9
- from agentpool.resource_providers import PlanProvider
10
- from agentpool_toolsets.builtin import CodeTools, ExecutionEnvironmentTools
11
- from agentpool_toolsets.fsspec_toolset import FSSpecTools
12
-
13
-
14
- if TYPE_CHECKING:
15
- from agentpool.resource_providers.aggregating import AggregatingResourceProvider
16
- from agentpool_server.acp_server.session import ACPSession
17
-
18
-
19
- def get_acp_provider(session: ACPSession) -> AggregatingResourceProvider:
20
- """Create aggregated resource provider with ACP-specific toolsets.
21
-
22
- Args:
23
- session: The ACP session to create providers for
24
-
25
- Returns:
26
- AggregatingResourceProvider with execution, filesystem, and code tools
27
- """
28
- from agentpool.resource_providers.aggregating import AggregatingResourceProvider
29
-
30
- execution_env = ACPExecutionEnvironment(
31
- fs=session.fs, requests=session.requests, cwd=session.cwd
32
- )
33
-
34
- providers = [
35
- PlanProvider(),
36
- ExecutionEnvironmentTools(env=execution_env, name=f"acp_execution_{session.session_id}"),
37
- FSSpecTools(execution_env, name=f"acp_fs_{session.session_id}", cwd=session.cwd),
38
- CodeTools(execution_env, name=f"acp_code_{session.session_id}", cwd=session.cwd),
39
- ]
40
- return AggregatingResourceProvider(providers=providers, name=f"acp_{session.session_id}")
41
-
42
-
43
- __all__ = ["get_acp_provider"]
@@ -1,210 +0,0 @@
1
- """Spawn subagent slash command."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import TYPE_CHECKING, Any
6
- import uuid
7
-
8
- from pydantic_ai import (
9
- FinalResultEvent,
10
- FunctionToolCallEvent,
11
- FunctionToolResultEvent,
12
- PartDeltaEvent,
13
- PartStartEvent,
14
- RetryPromptPart,
15
- TextPart,
16
- TextPartDelta,
17
- ThinkingPart,
18
- ThinkingPartDelta,
19
- ToolCallPartDelta,
20
- ToolReturnPart,
21
- )
22
- from slashed import CommandContext, CommandError # noqa: TC002
23
-
24
- from agentpool.agents.events import StreamCompleteEvent, ToolCallProgressEvent
25
- from agentpool.log import get_logger
26
- from agentpool.messaging.context import NodeContext # noqa: TC001
27
- from agentpool_commands.base import NodeCommand
28
- from agentpool_server.acp_server.session import ACPSession # noqa: TC001
29
-
30
-
31
- if TYPE_CHECKING:
32
- from agentpool.agents.events import RichAgentStreamEvent
33
-
34
-
35
- logger = get_logger(__name__)
36
-
37
-
38
- class SpawnSubagentCommand(NodeCommand):
39
- """Spawn a subagent to execute a specific task.
40
-
41
- The subagent runs concurrently and reports progress in a dedicated tool call box.
42
-
43
- Usage:
44
- /spawn "agent-name" "prompt for the subagent"
45
- /spawn "code-reviewer" "Review the main.py file for potential bugs"
46
- """
47
-
48
- name = "spawn"
49
- category = "agents"
50
-
51
- async def execute_command(
52
- self,
53
- ctx: CommandContext[NodeContext[ACPSession]],
54
- agent_name: str,
55
- task_prompt: str,
56
- ) -> None:
57
- """Spawn a subagent to execute a task.
58
-
59
- Args:
60
- ctx: Command context with ACP session
61
- agent_name: Name of the agent to spawn
62
- task_prompt: Task prompt for the subagent
63
- """
64
- session = ctx.context.data
65
- assert session, "ACP session required for spawn command"
66
- # Generate unique tool call ID
67
- tool_call_id = f"spawn-{agent_name}-{uuid.uuid4().hex[:8]}"
68
- try:
69
- # Check if agent exists in pool
70
- if not session.agent_pool or agent_name not in session.agent_pool.agents:
71
- available = list(session.agent_pool.agents.keys())
72
- error_msg = f"Agent {agent_name!r} not found. Available agents: {available}"
73
- await ctx.print(f"❌ {error_msg}")
74
- return
75
-
76
- target_agent = session.agent_pool.get_agent(agent_name)
77
- await session.notifications.tool_call_start(
78
- tool_call_id=tool_call_id,
79
- title=f"Spawning agent: {agent_name}",
80
- kind="execute",
81
- raw_input={
82
- "agent_name": agent_name,
83
- "task_prompt": task_prompt,
84
- },
85
- )
86
-
87
- aggregated_content: list[str] = [] # Aggregate output as we stream
88
- try:
89
- # Run the subagent and handle events
90
- async for event in target_agent.run_stream(task_prompt):
91
- await _handle_subagent_event(event, tool_call_id, aggregated_content, session)
92
-
93
- final_content = "".join(aggregated_content).strip()
94
- await session.notifications.tool_call_progress(
95
- tool_call_id=tool_call_id,
96
- status="completed",
97
- content=[final_content] if final_content else None,
98
- )
99
- except Exception as e:
100
- error_msg = f"Subagent execution failed: {e}"
101
- logger.exception("Subagent execution error", error=str(e))
102
- await session.notifications.tool_call_progress(
103
- tool_call_id=tool_call_id,
104
- status="failed",
105
- raw_output=error_msg,
106
- )
107
-
108
- except Exception as e:
109
- error_msg = f"Failed to spawn agent '{agent_name}': {e}"
110
- logger.exception("Spawn command error", error=str(e))
111
- raise CommandError(error_msg) from e
112
-
113
-
114
- async def _handle_subagent_event(
115
- event: RichAgentStreamEvent[Any],
116
- tool_call_id: str,
117
- aggregated_content: list[str],
118
- session: ACPSession,
119
- ) -> None:
120
- """Handle events from spawned subagent and convert to tool_call_progress.
121
-
122
- Args:
123
- event: Event from the subagent stream
124
- tool_call_id: ID of the tool call box
125
- aggregated_content: List to accumulate content for final display
126
- session: ACP session for notifications
127
- """
128
- match event:
129
- case (
130
- PartStartEvent(part=TextPart(content=delta))
131
- | PartDeltaEvent(delta=TextPartDelta(content_delta=delta))
132
- ):
133
- # Subagent text output → accumulate and update progress
134
- aggregated_content.append(delta)
135
- await session.notifications.tool_call_progress(
136
- tool_call_id=tool_call_id,
137
- status="in_progress",
138
- content=["".join(aggregated_content)],
139
- )
140
-
141
- case (
142
- PartStartEvent(part=ThinkingPart(content=delta))
143
- | PartDeltaEvent(delta=ThinkingPartDelta(content_delta=delta))
144
- ):
145
- # Subagent thinking → show thinking indicator
146
- if delta:
147
- thinking_text = f"💭 {delta}"
148
- aggregated_content.append(thinking_text)
149
- await session.notifications.tool_call_progress(
150
- tool_call_id=tool_call_id,
151
- status="in_progress",
152
- content=["".join(aggregated_content)],
153
- )
154
-
155
- case FunctionToolCallEvent(part=part):
156
- # Subagent calls a tool → show nested tool call
157
- tool_text = f"\n🔧 Using tool: {part.tool_name}\n"
158
- aggregated_content.append(tool_text)
159
- await session.notifications.tool_call_progress(
160
- tool_call_id=tool_call_id,
161
- status="in_progress",
162
- content=["".join(aggregated_content)],
163
- )
164
-
165
- case FunctionToolResultEvent(
166
- result=ToolReturnPart(content=content, tool_name=tool_name),
167
- ):
168
- # Subagent tool completes → show tool result
169
- result_text = f"✅ {tool_name}: {content}\n"
170
- aggregated_content.append(result_text)
171
- await session.notifications.tool_call_progress(
172
- tool_call_id=tool_call_id,
173
- status="in_progress",
174
- content=["".join(aggregated_content)],
175
- )
176
-
177
- case FunctionToolResultEvent(
178
- result=RetryPromptPart(tool_name=tool_name) as result,
179
- ):
180
- # Tool call failed and needs retry
181
- error_message = result.model_response()
182
- error_text = f"❌ {tool_name or 'unknown'}: Error: {error_message}\n"
183
- aggregated_content.append(error_text)
184
- await session.notifications.tool_call_progress(
185
- tool_call_id=tool_call_id,
186
- status="in_progress",
187
- content=["".join(aggregated_content)],
188
- )
189
-
190
- case ToolCallProgressEvent(message=message, tool_name=tool_name):
191
- # Progress event from tools
192
- if message:
193
- progress_text = f"🔄 {tool_name}: {message}\n"
194
- aggregated_content.append(progress_text)
195
- await session.notifications.tool_call_progress(
196
- tool_call_id=tool_call_id,
197
- status="in_progress",
198
- content=["".join(aggregated_content)],
199
- )
200
-
201
- case (
202
- PartStartEvent()
203
- | PartDeltaEvent(delta=ToolCallPartDelta())
204
- | FinalResultEvent()
205
- | StreamCompleteEvent()
206
- ):
207
- pass # These events don't need special handling
208
-
209
- case _:
210
- logger.debug("Unhandled subagent event", event_type=type(event).__name__)