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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. acp/__init__.py +13 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/bridge/README.md +15 -2
  7. acp/bridge/__init__.py +3 -2
  8. acp/bridge/__main__.py +60 -19
  9. acp/bridge/ws_server.py +173 -0
  10. acp/bridge/ws_server_cli.py +89 -0
  11. acp/client/connection.py +38 -29
  12. acp/client/implementations/default_client.py +3 -2
  13. acp/client/implementations/headless_client.py +2 -2
  14. acp/connection.py +2 -2
  15. acp/notifications.py +20 -50
  16. acp/schema/__init__.py +2 -0
  17. acp/schema/agent_responses.py +21 -0
  18. acp/schema/client_requests.py +3 -3
  19. acp/schema/session_state.py +63 -29
  20. acp/stdio.py +39 -9
  21. acp/task/supervisor.py +2 -2
  22. acp/transports.py +362 -2
  23. acp/utils.py +17 -4
  24. agentpool/__init__.py +6 -1
  25. agentpool/agents/__init__.py +2 -0
  26. agentpool/agents/acp_agent/acp_agent.py +407 -277
  27. agentpool/agents/acp_agent/acp_converters.py +196 -38
  28. agentpool/agents/acp_agent/client_handler.py +191 -26
  29. agentpool/agents/acp_agent/session_state.py +17 -6
  30. agentpool/agents/agent.py +607 -572
  31. agentpool/agents/agui_agent/__init__.py +0 -2
  32. agentpool/agents/agui_agent/agui_agent.py +176 -110
  33. agentpool/agents/agui_agent/agui_converters.py +0 -131
  34. agentpool/agents/agui_agent/helpers.py +3 -4
  35. agentpool/agents/base_agent.py +632 -17
  36. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  37. agentpool/agents/claude_code_agent/__init__.py +13 -1
  38. agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
  39. agentpool/agents/claude_code_agent/converters.py +74 -143
  40. agentpool/agents/claude_code_agent/history.py +474 -0
  41. agentpool/agents/claude_code_agent/models.py +77 -0
  42. agentpool/agents/claude_code_agent/static_info.py +100 -0
  43. agentpool/agents/claude_code_agent/usage.py +242 -0
  44. agentpool/agents/context.py +40 -0
  45. agentpool/agents/events/__init__.py +24 -0
  46. agentpool/agents/events/builtin_handlers.py +67 -1
  47. agentpool/agents/events/event_emitter.py +32 -2
  48. agentpool/agents/events/events.py +104 -3
  49. agentpool/agents/events/infer_info.py +145 -0
  50. agentpool/agents/events/processors.py +254 -0
  51. agentpool/agents/interactions.py +41 -6
  52. agentpool/agents/modes.py +67 -0
  53. agentpool/agents/slashed_agent.py +5 -4
  54. agentpool/agents/tool_call_accumulator.py +213 -0
  55. agentpool/agents/tool_wrapping.py +18 -6
  56. agentpool/common_types.py +56 -21
  57. agentpool/config_resources/__init__.py +38 -1
  58. agentpool/config_resources/acp_assistant.yml +2 -2
  59. agentpool/config_resources/agents.yml +3 -0
  60. agentpool/config_resources/agents_template.yml +1 -0
  61. agentpool/config_resources/claude_code_agent.yml +10 -6
  62. agentpool/config_resources/external_acp_agents.yml +2 -1
  63. agentpool/delegation/base_team.py +4 -30
  64. agentpool/delegation/pool.py +136 -289
  65. agentpool/delegation/team.py +58 -57
  66. agentpool/delegation/teamrun.py +51 -55
  67. agentpool/diagnostics/__init__.py +53 -0
  68. agentpool/diagnostics/lsp_manager.py +1593 -0
  69. agentpool/diagnostics/lsp_proxy.py +41 -0
  70. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  71. agentpool/diagnostics/models.py +398 -0
  72. agentpool/functional/run.py +10 -4
  73. agentpool/mcp_server/__init__.py +0 -2
  74. agentpool/mcp_server/client.py +76 -32
  75. agentpool/mcp_server/conversions.py +54 -13
  76. agentpool/mcp_server/manager.py +34 -54
  77. agentpool/mcp_server/registries/official_registry_client.py +35 -1
  78. agentpool/mcp_server/tool_bridge.py +186 -139
  79. agentpool/messaging/__init__.py +0 -2
  80. agentpool/messaging/compaction.py +72 -197
  81. agentpool/messaging/connection_manager.py +11 -10
  82. agentpool/messaging/event_manager.py +5 -5
  83. agentpool/messaging/message_container.py +6 -30
  84. agentpool/messaging/message_history.py +99 -8
  85. agentpool/messaging/messagenode.py +52 -14
  86. agentpool/messaging/messages.py +54 -35
  87. agentpool/messaging/processing.py +12 -22
  88. agentpool/models/__init__.py +1 -1
  89. agentpool/models/acp_agents/base.py +6 -24
  90. agentpool/models/acp_agents/mcp_capable.py +126 -157
  91. agentpool/models/acp_agents/non_mcp.py +129 -95
  92. agentpool/models/agents.py +98 -76
  93. agentpool/models/agui_agents.py +1 -1
  94. agentpool/models/claude_code_agents.py +144 -19
  95. agentpool/models/file_parsing.py +0 -1
  96. agentpool/models/manifest.py +113 -50
  97. agentpool/prompts/conversion_manager.py +1 -1
  98. agentpool/prompts/prompts.py +5 -2
  99. agentpool/repomap.py +1 -1
  100. agentpool/resource_providers/__init__.py +11 -1
  101. agentpool/resource_providers/aggregating.py +56 -5
  102. agentpool/resource_providers/base.py +70 -4
  103. agentpool/resource_providers/codemode/code_executor.py +72 -5
  104. agentpool/resource_providers/codemode/helpers.py +2 -2
  105. agentpool/resource_providers/codemode/provider.py +64 -12
  106. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  107. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  108. agentpool/resource_providers/filtering.py +3 -1
  109. agentpool/resource_providers/mcp_provider.py +89 -12
  110. agentpool/resource_providers/plan_provider.py +228 -46
  111. agentpool/resource_providers/pool.py +7 -3
  112. agentpool/resource_providers/resource_info.py +111 -0
  113. agentpool/resource_providers/static.py +4 -2
  114. agentpool/sessions/__init__.py +4 -1
  115. agentpool/sessions/manager.py +33 -5
  116. agentpool/sessions/models.py +59 -6
  117. agentpool/sessions/protocol.py +28 -0
  118. agentpool/sessions/session.py +11 -55
  119. agentpool/skills/registry.py +13 -8
  120. agentpool/storage/manager.py +572 -49
  121. agentpool/talk/registry.py +4 -4
  122. agentpool/talk/talk.py +9 -10
  123. agentpool/testing.py +538 -20
  124. agentpool/tool_impls/__init__.py +6 -0
  125. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  126. agentpool/tool_impls/agent_cli/tool.py +95 -0
  127. agentpool/tool_impls/bash/__init__.py +64 -0
  128. agentpool/tool_impls/bash/helpers.py +35 -0
  129. agentpool/tool_impls/bash/tool.py +171 -0
  130. agentpool/tool_impls/delete_path/__init__.py +70 -0
  131. agentpool/tool_impls/delete_path/tool.py +142 -0
  132. agentpool/tool_impls/download_file/__init__.py +80 -0
  133. agentpool/tool_impls/download_file/tool.py +183 -0
  134. agentpool/tool_impls/execute_code/__init__.py +55 -0
  135. agentpool/tool_impls/execute_code/tool.py +163 -0
  136. agentpool/tool_impls/grep/__init__.py +80 -0
  137. agentpool/tool_impls/grep/tool.py +200 -0
  138. agentpool/tool_impls/list_directory/__init__.py +73 -0
  139. agentpool/tool_impls/list_directory/tool.py +197 -0
  140. agentpool/tool_impls/question/__init__.py +42 -0
  141. agentpool/tool_impls/question/tool.py +127 -0
  142. agentpool/tool_impls/read/__init__.py +104 -0
  143. agentpool/tool_impls/read/tool.py +305 -0
  144. agentpool/tools/__init__.py +2 -1
  145. agentpool/tools/base.py +114 -34
  146. agentpool/tools/manager.py +57 -1
  147. agentpool/ui/base.py +2 -2
  148. agentpool/ui/mock_provider.py +2 -2
  149. agentpool/ui/stdlib_provider.py +2 -2
  150. agentpool/utils/file_watcher.py +269 -0
  151. agentpool/utils/identifiers.py +121 -0
  152. agentpool/utils/pydantic_ai_helpers.py +46 -0
  153. agentpool/utils/streams.py +616 -2
  154. agentpool/utils/subprocess_utils.py +155 -0
  155. agentpool/utils/token_breakdown.py +461 -0
  156. agentpool/vfs_registry.py +7 -2
  157. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
  158. agentpool-2.5.0.dist-info/RECORD +579 -0
  159. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  160. agentpool_cli/__main__.py +24 -0
  161. agentpool_cli/create.py +1 -1
  162. agentpool_cli/serve_acp.py +100 -21
  163. agentpool_cli/serve_agui.py +87 -0
  164. agentpool_cli/serve_opencode.py +119 -0
  165. agentpool_cli/ui.py +557 -0
  166. agentpool_commands/__init__.py +42 -5
  167. agentpool_commands/agents.py +75 -2
  168. agentpool_commands/history.py +62 -0
  169. agentpool_commands/mcp.py +176 -0
  170. agentpool_commands/models.py +56 -3
  171. agentpool_commands/pool.py +260 -0
  172. agentpool_commands/session.py +1 -1
  173. agentpool_commands/text_sharing/__init__.py +119 -0
  174. agentpool_commands/text_sharing/base.py +123 -0
  175. agentpool_commands/text_sharing/github_gist.py +80 -0
  176. agentpool_commands/text_sharing/opencode.py +462 -0
  177. agentpool_commands/text_sharing/paste_rs.py +59 -0
  178. agentpool_commands/text_sharing/pastebin.py +116 -0
  179. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  180. agentpool_commands/tools.py +57 -0
  181. agentpool_commands/utils.py +80 -30
  182. agentpool_config/__init__.py +30 -2
  183. agentpool_config/agentpool_tools.py +498 -0
  184. agentpool_config/builtin_tools.py +77 -22
  185. agentpool_config/commands.py +24 -1
  186. agentpool_config/compaction.py +258 -0
  187. agentpool_config/converters.py +1 -1
  188. agentpool_config/event_handlers.py +42 -0
  189. agentpool_config/events.py +1 -1
  190. agentpool_config/forward_targets.py +1 -4
  191. agentpool_config/jinja.py +3 -3
  192. agentpool_config/mcp_server.py +132 -6
  193. agentpool_config/nodes.py +1 -1
  194. agentpool_config/observability.py +44 -0
  195. agentpool_config/session.py +0 -3
  196. agentpool_config/storage.py +82 -38
  197. agentpool_config/task.py +3 -3
  198. agentpool_config/tools.py +11 -22
  199. agentpool_config/toolsets.py +109 -233
  200. agentpool_server/a2a_server/agent_worker.py +307 -0
  201. agentpool_server/a2a_server/server.py +23 -18
  202. agentpool_server/acp_server/acp_agent.py +234 -181
  203. agentpool_server/acp_server/commands/acp_commands.py +151 -156
  204. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
  205. agentpool_server/acp_server/event_converter.py +651 -0
  206. agentpool_server/acp_server/input_provider.py +53 -10
  207. agentpool_server/acp_server/server.py +24 -90
  208. agentpool_server/acp_server/session.py +173 -331
  209. agentpool_server/acp_server/session_manager.py +8 -34
  210. agentpool_server/agui_server/server.py +3 -1
  211. agentpool_server/mcp_server/server.py +5 -2
  212. agentpool_server/opencode_server/.rules +95 -0
  213. agentpool_server/opencode_server/ENDPOINTS.md +401 -0
  214. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  215. agentpool_server/opencode_server/__init__.py +19 -0
  216. agentpool_server/opencode_server/command_validation.py +172 -0
  217. agentpool_server/opencode_server/converters.py +975 -0
  218. agentpool_server/opencode_server/dependencies.py +24 -0
  219. agentpool_server/opencode_server/input_provider.py +421 -0
  220. agentpool_server/opencode_server/models/__init__.py +250 -0
  221. agentpool_server/opencode_server/models/agent.py +53 -0
  222. agentpool_server/opencode_server/models/app.py +72 -0
  223. agentpool_server/opencode_server/models/base.py +26 -0
  224. agentpool_server/opencode_server/models/common.py +23 -0
  225. agentpool_server/opencode_server/models/config.py +37 -0
  226. agentpool_server/opencode_server/models/events.py +821 -0
  227. agentpool_server/opencode_server/models/file.py +88 -0
  228. agentpool_server/opencode_server/models/mcp.py +44 -0
  229. agentpool_server/opencode_server/models/message.py +179 -0
  230. agentpool_server/opencode_server/models/parts.py +323 -0
  231. agentpool_server/opencode_server/models/provider.py +81 -0
  232. agentpool_server/opencode_server/models/pty.py +43 -0
  233. agentpool_server/opencode_server/models/question.py +56 -0
  234. agentpool_server/opencode_server/models/session.py +111 -0
  235. agentpool_server/opencode_server/routes/__init__.py +29 -0
  236. agentpool_server/opencode_server/routes/agent_routes.py +473 -0
  237. agentpool_server/opencode_server/routes/app_routes.py +202 -0
  238. agentpool_server/opencode_server/routes/config_routes.py +302 -0
  239. agentpool_server/opencode_server/routes/file_routes.py +571 -0
  240. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  241. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  242. agentpool_server/opencode_server/routes/message_routes.py +761 -0
  243. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  244. agentpool_server/opencode_server/routes/pty_routes.py +300 -0
  245. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  246. agentpool_server/opencode_server/routes/session_routes.py +1276 -0
  247. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  248. agentpool_server/opencode_server/server.py +475 -0
  249. agentpool_server/opencode_server/state.py +151 -0
  250. agentpool_server/opencode_server/time_utils.py +8 -0
  251. agentpool_storage/__init__.py +12 -0
  252. agentpool_storage/base.py +184 -2
  253. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  254. agentpool_storage/claude_provider/__init__.py +42 -0
  255. agentpool_storage/claude_provider/provider.py +1089 -0
  256. agentpool_storage/file_provider.py +278 -15
  257. agentpool_storage/memory_provider.py +193 -12
  258. agentpool_storage/models.py +3 -0
  259. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  260. agentpool_storage/opencode_provider/__init__.py +16 -0
  261. agentpool_storage/opencode_provider/helpers.py +414 -0
  262. agentpool_storage/opencode_provider/provider.py +895 -0
  263. agentpool_storage/project_store.py +325 -0
  264. agentpool_storage/session_store.py +26 -6
  265. agentpool_storage/sql_provider/__init__.py +4 -2
  266. agentpool_storage/sql_provider/models.py +48 -0
  267. agentpool_storage/sql_provider/sql_provider.py +269 -3
  268. agentpool_storage/sql_provider/utils.py +12 -13
  269. agentpool_storage/zed_provider/__init__.py +16 -0
  270. agentpool_storage/zed_provider/helpers.py +281 -0
  271. agentpool_storage/zed_provider/models.py +130 -0
  272. agentpool_storage/zed_provider/provider.py +442 -0
  273. agentpool_storage/zed_provider.py +803 -0
  274. agentpool_toolsets/__init__.py +0 -2
  275. agentpool_toolsets/builtin/__init__.py +2 -12
  276. agentpool_toolsets/builtin/code.py +96 -57
  277. agentpool_toolsets/builtin/debug.py +118 -48
  278. agentpool_toolsets/builtin/execution_environment.py +115 -230
  279. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  280. agentpool_toolsets/builtin/skills.py +9 -4
  281. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  282. agentpool_toolsets/builtin/workers.py +4 -2
  283. agentpool_toolsets/composio_toolset.py +2 -2
  284. agentpool_toolsets/entry_points.py +3 -1
  285. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  286. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  287. agentpool_toolsets/fsspec_toolset/grep.py +99 -7
  288. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  289. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  290. agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
  291. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  292. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  293. agentpool_toolsets/mcp_discovery/toolset.py +511 -0
  294. agentpool_toolsets/mcp_run_toolset.py +87 -12
  295. agentpool_toolsets/notifications.py +33 -33
  296. agentpool_toolsets/openapi.py +3 -1
  297. agentpool_toolsets/search_toolset.py +3 -1
  298. agentpool-2.1.9.dist-info/RECORD +0 -474
  299. agentpool_config/resources.py +0 -33
  300. agentpool_server/acp_server/acp_tools.py +0 -43
  301. agentpool_server/acp_server/commands/spawn.py +0 -210
  302. agentpool_storage/text_log_provider.py +0 -275
  303. agentpool_toolsets/builtin/agent_management.py +0 -239
  304. agentpool_toolsets/builtin/chain.py +0 -288
  305. agentpool_toolsets/builtin/history.py +0 -36
  306. agentpool_toolsets/builtin/integration.py +0 -85
  307. agentpool_toolsets/builtin/tool_management.py +0 -90
  308. agentpool_toolsets/builtin/user_interaction.py +0 -52
  309. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  310. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  311. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -8,17 +8,19 @@ from typing import TYPE_CHECKING, Any, Self
8
8
 
9
9
  from agentpool.log import get_logger
10
10
  from agentpool.resource_providers import ResourceProvider
11
+ from agentpool.resource_providers.resource_info import ResourceInfo
11
12
  from agentpool_config.mcp_server import BaseMCPServerConfig
12
- from agentpool_config.resources import ResourceInfo
13
13
 
14
14
 
15
15
  if TYPE_CHECKING:
16
+ from collections.abc import Sequence
16
17
  from typing import Literal
17
18
 
18
19
  from fastmcp.client.sampling import SamplingHandler
20
+ from mcp.types import ResourceTemplate
19
21
 
20
22
  from agentpool.prompts.prompts import MCPClientPrompt
21
- from agentpool.tools.base import Tool
23
+ from agentpool.tools.base import FunctionTool, Tool
22
24
  from agentpool_config.mcp_server import MCPServerConfig
23
25
 
24
26
 
@@ -28,6 +30,8 @@ logger = get_logger(__name__)
28
30
  class MCPResourceProvider(ResourceProvider):
29
31
  """Resource provider for a single MCP server."""
30
32
 
33
+ kind = "mcp"
34
+
31
35
  def __init__(
32
36
  self,
33
37
  server: MCPServerConfig | str,
@@ -43,19 +47,14 @@ class MCPResourceProvider(ResourceProvider):
43
47
  self.server = BaseMCPServerConfig.from_string(server) if isinstance(server, str) else server
44
48
  self.source = source
45
49
  self.exit_stack = AsyncExitStack()
50
+
46
51
  self._accessible_roots = accessible_roots
47
52
  self._sampling_callback = sampling_callback
48
53
 
49
- # Tool caching
50
- self._tools_cache: list[Tool] | None = None
51
54
  self._saved_enabled_states: dict[str, bool] = {}
52
-
53
- # Prompt caching
55
+ self._tools_cache: list[FunctionTool] | None = None
54
56
  self._prompts_cache: list[MCPClientPrompt] | None = None
55
-
56
- # Resource caching
57
57
  self._resources_cache: list[ResourceInfo] | None = None
58
-
59
58
  self.client = MCPClient(
60
59
  config=self.server,
61
60
  sampling_callback=self._sampling_callback,
@@ -104,23 +103,29 @@ class MCPResourceProvider(ResourceProvider):
104
103
  logger.info("MCP tool list changed, refreshing provider cache")
105
104
  self._saved_enabled_states = {t.name: t.enabled for t in self._tools_cache or []}
106
105
  self._tools_cache = None
106
+ # Notify subscribers via signal
107
+ await self.tools_changed.emit(self.create_change_event("tools"))
107
108
 
108
109
  async def _on_prompts_changed(self) -> None:
109
110
  """Callback when prompts change on the MCP server."""
110
111
  logger.info("MCP prompt list changed, refreshing provider cache")
111
112
  self._prompts_cache = None
113
+ # Notify subscribers via signal
114
+ await self.prompts_changed.emit(self.create_change_event("prompts"))
112
115
 
113
116
  async def _on_resources_changed(self) -> None:
114
117
  """Callback when resources change on the MCP server."""
115
118
  logger.info("MCP resource list changed, refreshing provider cache")
116
119
  self._resources_cache = None
120
+ # Notify subscribers via signal
121
+ await self.resources_changed.emit(self.create_change_event("resources"))
117
122
 
118
123
  async def refresh_tools_cache(self) -> None:
119
124
  """Refresh the tools cache by fetching from client."""
120
125
  try:
121
126
  # Get fresh tools from client
122
127
  mcp_tools = await self.client.list_tools()
123
- all_tools: list[Tool] = []
128
+ all_tools: list[FunctionTool] = []
124
129
 
125
130
  for tool in mcp_tools:
126
131
  try:
@@ -141,7 +146,7 @@ class MCPResourceProvider(ResourceProvider):
141
146
  logger.exception("Failed to refresh MCP tools cache")
142
147
  self._tools_cache = []
143
148
 
144
- async def get_tools(self) -> list[Tool]:
149
+ async def get_tools(self) -> Sequence[Tool]:
145
150
  """Get cached tools, refreshing if necessary."""
146
151
  if self._tools_cache is None:
147
152
  await self.refresh_tools_cache()
@@ -185,7 +190,11 @@ class MCPResourceProvider(ResourceProvider):
185
190
 
186
191
  for resource in result:
187
192
  try:
188
- converted = await ResourceInfo.from_mcp_resource(resource)
193
+ converted = await ResourceInfo.from_mcp_resource(
194
+ resource,
195
+ client_name=self.name,
196
+ reader=self.read_resource,
197
+ )
189
198
  all_resources.append(converted)
190
199
  except Exception:
191
200
  logger.exception("Failed to convert resource", name=resource.name)
@@ -204,6 +213,74 @@ class MCPResourceProvider(ResourceProvider):
204
213
 
205
214
  return self._resources_cache or []
206
215
 
216
+ async def read_resource(self, uri: str) -> list[str]:
217
+ """Read resource content by URI.
218
+
219
+ Args:
220
+ uri: URI of the resource to read
221
+
222
+ Returns:
223
+ List of text contents from the resource
224
+
225
+ Raises:
226
+ RuntimeError: If resource cannot be read
227
+ """
228
+ contents = await self.client.read_resource(uri)
229
+ result: list[str] = []
230
+ for content in contents:
231
+ if hasattr(content, "text") and content.text is not None:
232
+ result.append(str(content.text))
233
+ elif hasattr(content, "blob") and content.blob is not None:
234
+ # Binary content - return placeholder or base64
235
+ import base64
236
+
237
+ blob_data = content.blob
238
+ if isinstance(blob_data, str):
239
+ result.append(f"[Binary data: {len(blob_data)} bytes]")
240
+ elif isinstance(blob_data, bytes):
241
+ encoded = base64.b64encode(blob_data).decode("utf-8")
242
+ result.append(encoded)
243
+ else:
244
+ result.append("[Binary data: unknown format]")
245
+ return result
246
+
247
+ async def list_resource_templates(self) -> list[ResourceTemplate]:
248
+ """Get available resource templates from the MCP server.
249
+
250
+ Resource templates define URI patterns with placeholders that can be
251
+ expanded into concrete resource URIs. For example:
252
+ - Template: "file:///{path}" with path="config.json"
253
+ - Expands to: "file:///config.json"
254
+
255
+ TODO: Decide on integration strategy:
256
+ - Option 1: Templates as separate concept with expand() -> ResourceInfo
257
+ - Option 2: Unified ResourceInfo with is_template flag and read(**kwargs)
258
+ - Option 3: ResourceTemplateInfo class that produces ResourceInfo
259
+
260
+ Returns:
261
+ List of ResourceTemplate objects from the server
262
+ """
263
+ try:
264
+ return await self.client.list_resource_templates()
265
+ except Exception:
266
+ logger.exception("Failed to list resource templates")
267
+ return []
268
+
269
+ def get_status(self) -> dict[str, str]:
270
+ """Get connection status for this MCP server.
271
+
272
+ Returns:
273
+ Status dict with 'status' key and optionally 'error' key.
274
+ Status can be: 'connected', 'disabled', or 'failed'.
275
+ """
276
+ try:
277
+ if self.client.connected:
278
+ return {"status": "connected"}
279
+ except Exception as e: # noqa: BLE001
280
+ return {"status": "failed", "error": str(e)}
281
+ else:
282
+ return {"status": "disabled"}
283
+
207
284
 
208
285
  if __name__ == "__main__":
209
286
  import anyio
@@ -3,19 +3,26 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
- from typing import TYPE_CHECKING, Literal
6
+ from typing import TYPE_CHECKING, Any, Literal
7
7
 
8
8
  from agentpool.agents.context import AgentContext # noqa: TC001
9
+ from agentpool.agents.events import TextContentItem
9
10
  from agentpool.resource_providers import ResourceProvider
11
+ from agentpool.tools.base import ToolResult
12
+ from agentpool.utils.streams import TodoPriority, TodoStatus # noqa: TC001
10
13
 
11
14
 
12
15
  if TYPE_CHECKING:
16
+ from collections.abc import Sequence
17
+
13
18
  from agentpool.tools.base import Tool
19
+ from agentpool.utils.streams import TodoTracker
14
20
 
15
21
 
16
- # Plan entry types - domain models independent of ACP
22
+ # Keep PlanEntry for backward compatibility with event emitting
17
23
  PlanEntryPriority = Literal["high", "medium", "low"]
18
24
  PlanEntryStatus = Literal["pending", "in_progress", "completed"]
25
+ PlanToolMode = Literal["granular", "declarative"]
19
26
 
20
27
 
21
28
  @dataclass(kw_only=True)
@@ -50,24 +57,46 @@ class PlanProvider(ResourceProvider):
50
57
  """Provides plan-related tools for agent planning and task management.
51
58
 
52
59
  This provider creates tools for managing agent plans and tasks,
53
- emitting domain events that can be handled by protocol adapters.
60
+ delegating storage to pool.todos and emitting domain events
61
+ that can be handled by protocol adapters.
54
62
  """
55
63
 
56
- def __init__(self) -> None:
57
- """Initialize plan provider."""
64
+ kind = "tools"
65
+
66
+ def __init__(self, mode: PlanToolMode = "declarative") -> None:
67
+ """Initialize plan provider.
68
+
69
+ Args:
70
+ mode: Tool mode - 'granular' for separate tools, 'declarative' for
71
+ single set_plan tool.
72
+ """
58
73
  super().__init__(name="plan")
59
- self._current_plan: list[PlanEntry] = []
60
-
61
- async def get_tools(self) -> list[Tool]:
62
- """Get plan management tools."""
63
- return [
64
- self.create_tool(self.get_plan, category="read"),
65
- self.create_tool(self.add_plan_entry, category="other"),
66
- self.create_tool(self.update_plan_entry, category="edit"),
67
- self.create_tool(self.remove_plan_entry, category="delete"),
68
- ]
74
+ self.mode = mode
75
+
76
+ def _get_tracker(self, agent_ctx: AgentContext) -> TodoTracker | None:
77
+ """Get the TodoTracker from the pool."""
78
+ if agent_ctx.pool is not None:
79
+ return agent_ctx.pool.todos
80
+ return None
69
81
 
70
- async def get_plan(self, agent_ctx: AgentContext) -> str:
82
+ async def get_tools(self) -> Sequence[Tool]:
83
+ """Get plan management tools based on mode."""
84
+ tools: list[Tool] = [self.create_tool(self.get_plan, category="read")]
85
+
86
+ if self.mode == "declarative":
87
+ # Single bulk tool for capable models
88
+ tools.append(self.create_tool(self.set_plan, category="other"))
89
+ else:
90
+ # granular mode (default) - separate tools for simpler models
91
+ tools.extend([
92
+ self.create_tool(self.add_plan_entry, category="other"),
93
+ self.create_tool(self.update_plan_entry, category="edit"),
94
+ self.create_tool(self.remove_plan_entry, category="delete"),
95
+ ])
96
+
97
+ return tools
98
+
99
+ async def get_plan(self, agent_ctx: AgentContext) -> ToolResult:
71
100
  """Get the current plan formatted as markdown.
72
101
 
73
102
  Args:
@@ -76,8 +105,17 @@ class PlanProvider(ResourceProvider):
76
105
  Returns:
77
106
  Markdown-formatted plan with all entries and their status
78
107
  """
79
- if not self._current_plan:
80
- return "## Plan\n\n*No plan entries yet.*"
108
+ tracker = self._get_tracker(agent_ctx)
109
+ if tracker is None or not tracker.entries:
110
+ # Emit progress for empty plan
111
+ await agent_ctx.events.tool_call_progress(
112
+ title="Fetched plan (empty)",
113
+ items=[TextContentItem(text="*No tasks in plan yet.*")],
114
+ )
115
+ return ToolResult(
116
+ content="## Plan\n\n*No plan entries yet.",
117
+ metadata={"todos": []},
118
+ )
81
119
 
82
120
  lines = ["## Plan", ""]
83
121
  status_icons = {
@@ -90,18 +128,117 @@ class PlanProvider(ResourceProvider):
90
128
  "medium": "🟡",
91
129
  "low": "🟢",
92
130
  }
93
- for i, entry in enumerate(self._current_plan):
131
+ for i, entry in enumerate(tracker.entries):
94
132
  icon = status_icons.get(entry.status, "?")
95
133
  priority = priority_labels.get(entry.priority, "")
96
134
  lines.append(f"{i}. {icon} {priority} {entry.content} *({entry.status})*")
97
135
 
98
- return "\n".join(lines)
136
+ # Count completed entries for summary
137
+ completed = sum(1 for e in tracker.entries if e.status == "completed")
138
+ total = len(tracker.entries)
139
+
140
+ # Build title with summary
141
+ title = "Fetched plan with 1 task" if total == 1 else f"Fetched plan with {total} tasks"
142
+ if completed > 0:
143
+ title += f" ({completed} completed)"
144
+
145
+ # Emit progress with plan preview
146
+ plan_text = "\n".join(lines)
147
+ await agent_ctx.events.tool_call_progress(
148
+ title=title,
149
+ items=[TextContentItem(text=plan_text)],
150
+ )
151
+
152
+ # Convert to OpenCode format for metadata
153
+ todos = [{"content": e.content, "status": e.status} for e in tracker.entries]
154
+
155
+ return ToolResult(
156
+ content=plan_text,
157
+ metadata={"todos": todos},
158
+ )
159
+
160
+ async def set_plan(
161
+ self,
162
+ agent_ctx: AgentContext,
163
+ entries: list[dict[str, Any]],
164
+ ) -> ToolResult:
165
+ """Replace the entire plan with new entries (declarative/bulk update).
166
+
167
+ This is more efficient than multiple add/update calls when setting
168
+ or significantly modifying the plan.
169
+
170
+ Args:
171
+ agent_ctx: Agent execution context
172
+ entries: List of plan entries, each with:
173
+ - content (str, required): Task description
174
+ - priority (str, optional): "high", "medium", or "low" (default: "medium")
175
+ - status (str, optional): "pending", "in_progress", or "completed"
176
+ (default: "pending")
177
+
178
+ Returns:
179
+ Success message indicating plan was updated
180
+ """
181
+ tracker = self._get_tracker(agent_ctx)
182
+ if tracker is None:
183
+ return ToolResult(
184
+ content="Error: No pool available for plan tracking",
185
+ metadata={"todos": []},
186
+ )
187
+
188
+ # Clear existing entries
189
+ tracker.clear()
190
+
191
+ # Add all new entries
192
+ for entry in entries:
193
+ content = entry.get("content", "")
194
+ if not content:
195
+ continue
196
+ priority = entry.get("priority", "medium")
197
+ status = entry.get("status", "pending")
198
+ tracker.add(content, priority=priority, status=status)
199
+
200
+ await self._emit_plan_update(agent_ctx)
201
+
202
+ # Build summary for user feedback
203
+ entry_count = len(tracker.entries)
204
+ if entry_count == 0:
205
+ title = "Cleared plan"
206
+ elif entry_count == 1:
207
+ title = "Set plan with 1 task"
208
+ else:
209
+ title = f"Set plan with {entry_count} tasks"
210
+
211
+ # Format entries list for details
212
+ if tracker.entries:
213
+ lines = ["**New Plan:**"]
214
+ for i, e in enumerate(tracker.entries):
215
+ priority_emoji = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(e.priority, "")
216
+ status_emoji = {"pending": "⬚", "in_progress": "◐", "completed": "✓"}.get(
217
+ e.status, ""
218
+ )
219
+ lines.append(f"{i + 1}. {priority_emoji} {status_emoji} {e.content}")
220
+ details = "\n".join(lines)
221
+ else:
222
+ details = "*Plan is empty*"
223
+
224
+ await agent_ctx.events.tool_call_progress(
225
+ title=title,
226
+ items=[TextContentItem(text=details)],
227
+ )
228
+
229
+ # Convert to OpenCode format for metadata
230
+ todos = [{"content": e.content, "status": e.status} for e in tracker.entries]
231
+
232
+ return ToolResult(
233
+ content=f"Plan updated with {entry_count} entries",
234
+ metadata={"todos": todos},
235
+ )
99
236
 
100
237
  async def add_plan_entry(
101
238
  self,
102
239
  agent_ctx: AgentContext,
103
240
  content: str,
104
- priority: PlanEntryPriority = "medium",
241
+ priority: TodoPriority = "medium",
105
242
  index: int | None = None,
106
243
  ) -> str:
107
244
  """Add a new plan entry.
@@ -115,18 +252,24 @@ class PlanProvider(ResourceProvider):
115
252
  Returns:
116
253
  Success message indicating entry was added
117
254
  """
118
- entry = PlanEntry(content=content, priority=priority, status="pending")
119
- if index is None:
120
- self._current_plan.append(entry)
121
- entry_index = len(self._current_plan) - 1
122
- else:
123
- if index < 0 or index > len(self._current_plan):
124
- return f"Error: Index {index} out of range (0-{len(self._current_plan)})"
125
- self._current_plan.insert(index, entry)
126
- entry_index = index
255
+ tracker = self._get_tracker(agent_ctx)
256
+ if tracker is None:
257
+ return "Error: No pool available for plan tracking"
258
+
259
+ entry = tracker.add(content, priority=priority, index=index)
260
+ entry_index = tracker.entries.index(entry)
127
261
 
128
262
  await self._emit_plan_update(agent_ctx)
129
263
 
264
+ # User feedback
265
+ priority_emoji = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(priority, "")
266
+ title = f"Added task {entry_index + 1} {priority_emoji}"
267
+ details = f"**Task {entry_index + 1}**: {content}"
268
+ await agent_ctx.events.tool_call_progress(
269
+ title=title,
270
+ items=[TextContentItem(text=details)],
271
+ )
272
+
130
273
  return f"Added plan entry at index {entry_index}: {content!r} (priority={priority!r})"
131
274
 
132
275
  async def update_plan_entry(
@@ -134,8 +277,8 @@ class PlanProvider(ResourceProvider):
134
277
  agent_ctx: AgentContext,
135
278
  index: int,
136
279
  content: str | None = None,
137
- status: PlanEntryStatus | None = None,
138
- priority: PlanEntryPriority | None = None,
280
+ status: TodoStatus | None = None,
281
+ priority: TodoPriority | None = None,
139
282
  ) -> str:
140
283
  """Update an existing plan entry.
141
284
 
@@ -149,28 +292,40 @@ class PlanProvider(ResourceProvider):
149
292
  Returns:
150
293
  Success message indicating what was updated
151
294
  """
152
- if index < 0 or index >= len(self._current_plan):
153
- return f"Error: Index {index} out of range (0-{len(self._current_plan) - 1})"
295
+ tracker = self._get_tracker(agent_ctx)
296
+ if tracker is None:
297
+ return "Error: No pool available for plan tracking"
154
298
 
155
- entry = self._current_plan[index]
156
- updates = []
299
+ if index < 0 or index >= len(tracker.entries):
300
+ return f"Error: Index {index} out of range (0-{len(tracker.entries) - 1})"
157
301
 
302
+ updates = []
158
303
  if content is not None:
159
- entry.content = content
160
304
  updates.append(f"content to {content!r}")
161
-
162
305
  if status is not None:
163
- entry.status = status
164
306
  updates.append(f"status to {status!r}")
165
-
166
307
  if priority is not None:
167
- entry.priority = priority
168
308
  updates.append(f"priority to {priority!r}")
169
309
 
170
310
  if not updates:
171
311
  return "No changes specified"
172
312
 
313
+ tracker.update_by_index(index, content=content, status=status, priority=priority)
314
+
173
315
  await self._emit_plan_update(agent_ctx)
316
+
317
+ # Build title with key info
318
+ entry = tracker.entries[index]
319
+ status_emoji = {"pending": "⬚", "in_progress": "◐", "completed": "✓"}.get(entry.status, "")
320
+ title = f"Updated task {index + 1} {status_emoji}"
321
+
322
+ # Send detailed content
323
+ details = f"**Task {index + 1}**: {entry.content}\n\nChanges: {', '.join(updates)}"
324
+ await agent_ctx.events.tool_call_progress(
325
+ title=title,
326
+ items=[TextContentItem(text=details)],
327
+ )
328
+
174
329
  return f"Updated entry {index}: {', '.join(updates)}"
175
330
 
176
331
  async def remove_plan_entry(self, agent_ctx: AgentContext, index: int) -> str:
@@ -183,14 +338,41 @@ class PlanProvider(ResourceProvider):
183
338
  Returns:
184
339
  Success message indicating entry was removed
185
340
  """
186
- if index < 0 or index >= len(self._current_plan):
187
- return f"Error: Index {index} out of range (0-{len(self._current_plan) - 1})"
188
- removed_entry = self._current_plan.pop(index)
341
+ tracker = self._get_tracker(agent_ctx)
342
+ if tracker is None:
343
+ return "Error: No pool available for plan tracking"
344
+
345
+ if index < 0 or index >= len(tracker.entries):
346
+ return f"Error: Index {index} out of range (0-{len(tracker.entries) - 1})"
347
+
348
+ removed_entry = tracker.remove_by_index(index)
189
349
  await self._emit_plan_update(agent_ctx)
190
- if self._current_plan:
350
+
351
+ if removed_entry is None:
352
+ return f"Error: Could not remove entry at index {index}"
353
+
354
+ # User feedback
355
+ remaining = len(tracker.entries)
356
+ title = f"Removed task {index + 1}"
357
+ details = f"**Removed**: {removed_entry.content}\n\nRemaining tasks: {remaining}"
358
+ await agent_ctx.events.tool_call_progress(
359
+ title=title,
360
+ items=[TextContentItem(text=details)],
361
+ )
362
+
363
+ if tracker.entries:
191
364
  return f"Removed entry {index}: {removed_entry.content!r}, remaining entries reindexed"
192
365
  return f"Removed entry {index}: {removed_entry.content!r}, plan is now empty"
193
366
 
194
367
  async def _emit_plan_update(self, agent_ctx: AgentContext) -> None:
195
368
  """Emit plan update event."""
196
- await agent_ctx.events.plan_updated(self._current_plan)
369
+ tracker = self._get_tracker(agent_ctx)
370
+ if tracker is None:
371
+ return
372
+
373
+ # Convert TodoEntry to PlanEntry for event compatibility
374
+ entries = [
375
+ PlanEntry(content=e.content, priority=e.priority, status=e.status)
376
+ for e in tracker.entries
377
+ ]
378
+ await agent_ctx.events.plan_updated(entries)
@@ -9,10 +9,12 @@ 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 agentpool import AgentPool
13
15
  from agentpool.prompts.prompts import BasePrompt
14
- from agentpool.tools.base import Tool
15
- from agentpool_config.resources import ResourceInfo
16
+ from agentpool.resource_providers.resource_info import ResourceInfo
17
+ from agentpool.tools import Tool
16
18
 
17
19
  logger = get_logger(__name__)
18
20
 
@@ -20,6 +22,8 @@ logger = get_logger(__name__)
20
22
  class PoolResourceProvider(ResourceProvider):
21
23
  """Provider that exposes an AgentPool's resources."""
22
24
 
25
+ kind = "tools"
26
+
23
27
  def __init__(
24
28
  self,
25
29
  pool: AgentPool[Any],
@@ -41,7 +45,7 @@ class PoolResourceProvider(ResourceProvider):
41
45
  self.zed_mode = zed_mode
42
46
  self.include_team_members = include_team_members
43
47
 
44
- async def get_tools(self) -> list[Tool]:
48
+ async def get_tools(self) -> Sequence[Tool]:
45
49
  """Get tools from all agents in pool."""
46
50
  team_tools = [team.to_tool() for team in self.pool.teams.values()]
47
51
  agents = list(self.pool.agents.values())