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
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
20
20
  from agentpool.prompts.prompts import MCPClientPrompt
21
21
  from agentpool.resource_providers import ResourceProvider
22
22
  from agentpool.resource_providers.codemode.provider import CodeModeResourceProvider
23
+ from agentpool.resource_providers.resource_info import ResourceInfo
23
24
 
24
25
 
25
26
  logger = get_logger(__name__)
@@ -186,6 +187,8 @@ class ToolManager:
186
187
  async def list_prompts(self) -> list[MCPClientPrompt]:
187
188
  """Get all prompts from all providers."""
188
189
  from agentpool.mcp_server.manager import MCPManager
190
+ from agentpool.prompts.prompts import MCPClientPrompt as MCPPrompt
191
+ from agentpool.resource_providers import AggregatingResourceProvider
189
192
 
190
193
  all_prompts: list[MCPClientPrompt] = []
191
194
  # Get prompts from all external providers (check if they're MCP providers)
@@ -195,12 +198,65 @@ class ToolManager:
195
198
  # Get prompts from MCP providers via the aggregating provider
196
199
  agg_provider = provider.get_aggregating_provider()
197
200
  prompts = await agg_provider.get_prompts()
198
- all_prompts.extend(prompts)
201
+ # Filter to only MCPClientPrompt instances
202
+ mcp_prompts = [p for p in prompts if isinstance(p, MCPPrompt)]
203
+ all_prompts.extend(mcp_prompts)
204
+ except Exception:
205
+ logger.exception("Failed to get prompts from provider", provider=provider)
206
+ elif isinstance(provider, AggregatingResourceProvider):
207
+ try:
208
+ # AggregatingResourceProvider can directly provide prompts
209
+ prompts = await provider.get_prompts()
210
+ # Filter to only MCPClientPrompt instances
211
+ mcp_prompts = [p for p in prompts if isinstance(p, MCPPrompt)]
212
+ all_prompts.extend(mcp_prompts)
199
213
  except Exception:
200
214
  logger.exception("Failed to get prompts from provider", provider=provider)
201
215
 
202
216
  return all_prompts
203
217
 
218
+ async def list_resources(self) -> list[ResourceInfo]:
219
+ """Get all resources from all providers.
220
+
221
+ Returns:
222
+ List of ResourceInfo objects from all providers
223
+ """
224
+ all_resources: list[ResourceInfo] = []
225
+ # Get resources from all providers concurrently
226
+ provider_coroutines = [provider.get_resources() for provider in self.providers]
227
+ results = await asyncio.gather(*provider_coroutines, return_exceptions=True)
228
+
229
+ for provider, result in zip(self.providers, results, strict=False):
230
+ if isinstance(result, BaseException):
231
+ logger.warning(
232
+ "Failed to get resources from provider",
233
+ provider=provider.name,
234
+ error=str(result),
235
+ )
236
+ continue
237
+ all_resources.extend(result)
238
+
239
+ return all_resources
240
+
241
+ async def get_resource(self, name: str) -> ResourceInfo:
242
+ """Get a specific resource by name.
243
+
244
+ Args:
245
+ name: Name of the resource to find
246
+
247
+ Returns:
248
+ ResourceInfo for the requested resource
249
+
250
+ Raises:
251
+ ToolError: If resource not found
252
+ """
253
+ resources = await self.list_resources()
254
+ resource: ResourceInfo | None = next((r for r in resources if r.name == name), None)
255
+ if not resource:
256
+ msg = f"Resource not found: {name}"
257
+ raise ToolError(msg)
258
+ return resource
259
+
204
260
  @asynccontextmanager
205
261
  async def temporary_tools(
206
262
  self,
agentpool/ui/base.py CHANGED
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
12
12
  from mcp import types
13
13
  from pydantic import BaseModel
14
14
 
15
- from agentpool.agents.context import ConfirmationResult
15
+ from agentpool.agents.context import AgentContext, ConfirmationResult
16
16
  from agentpool.messaging import ChatMessage
17
17
  from agentpool.messaging.context import NodeContext
18
18
  from agentpool.tools.base import Tool
@@ -62,7 +62,7 @@ class InputProvider(ABC):
62
62
  @abstractmethod
63
63
  def get_tool_confirmation(
64
64
  self,
65
- context: NodeContext[Any],
65
+ context: AgentContext[Any],
66
66
  tool: Tool,
67
67
  args: dict[str, Any],
68
68
  message_history: list[ChatMessage[Any]] | None = None,
@@ -11,7 +11,7 @@ from agentpool.ui.base import InputProvider
11
11
  if TYPE_CHECKING:
12
12
  from mcp import types
13
13
 
14
- from agentpool.agents.context import ConfirmationResult
14
+ from agentpool.agents.context import AgentContext, ConfirmationResult
15
15
  from agentpool.messaging import ChatMessage
16
16
  from agentpool.messaging.context import NodeContext
17
17
  from agentpool.tools.base import Tool
@@ -58,7 +58,7 @@ class MockInputProvider(InputProvider):
58
58
 
59
59
  async def get_tool_confirmation(
60
60
  self,
61
- context: NodeContext,
61
+ context: AgentContext[Any],
62
62
  tool: Tool,
63
63
  args: dict[str, Any],
64
64
  message_history: list[ChatMessage[Any]] | None = None,
@@ -17,7 +17,7 @@ from agentpool.ui.base import InputProvider
17
17
  if TYPE_CHECKING:
18
18
  from pydantic import BaseModel
19
19
 
20
- from agentpool.agents.context import ConfirmationResult
20
+ from agentpool.agents.context import AgentContext, ConfirmationResult
21
21
  from agentpool.messaging import ChatMessage
22
22
  from agentpool.messaging.context import NodeContext
23
23
  from agentpool.tools.base import Tool
@@ -60,7 +60,7 @@ class StdlibInputProvider(InputProvider):
60
60
 
61
61
  async def get_tool_confirmation(
62
62
  self,
63
- context: NodeContext,
63
+ context: AgentContext[Any],
64
64
  tool: Tool,
65
65
  args: dict[str, Any],
66
66
  message_history: list[ChatMessage[Any]] | None = None,
@@ -0,0 +1,269 @@
1
+ """File watcher utilities using watchfiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Awaitable, Callable, Set as AbstractSet
7
+ import contextlib
8
+ from dataclasses import dataclass, field
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Self
12
+
13
+ from watchfiles import Change, awatch
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ # Callback type for file change notifications
20
+ FileChangeCallback = Callable[[AbstractSet[tuple[Change, str]]], Awaitable[None]]
21
+
22
+
23
+ @dataclass
24
+ class FileWatcher:
25
+ """Async file watcher using watchfiles.
26
+
27
+ Watches specified paths for changes and calls a callback when changes occur.
28
+
29
+ Example:
30
+ ```python
31
+ async def on_change(changes):
32
+ for change_type, path in changes:
33
+ print(f"{change_type}: {path}")
34
+
35
+ watcher = FileWatcher(
36
+ paths=["/path/to/.git/HEAD"],
37
+ callback=on_change,
38
+ )
39
+
40
+ async with watcher:
41
+ # Watcher is running
42
+ await asyncio.sleep(60)
43
+ # Watcher stopped
44
+ ```
45
+ """
46
+
47
+ paths: list[str | Path]
48
+ """Paths to watch (files or directories)."""
49
+
50
+ callback: FileChangeCallback
51
+ """Async callback invoked when changes are detected."""
52
+
53
+ debounce: int = 100
54
+ """Debounce time in milliseconds."""
55
+
56
+ _task: asyncio.Task[None] | None = field(default=None, repr=False)
57
+ """Background watch task."""
58
+
59
+ _stop_event: asyncio.Event = field(default_factory=asyncio.Event, repr=False)
60
+ """Event to signal stop."""
61
+
62
+ async def start(self) -> None:
63
+ """Start watching for file changes."""
64
+ if self._task is not None:
65
+ return # Already running
66
+
67
+ self._stop_event.clear()
68
+ self._task = asyncio.create_task(self._watch_loop())
69
+
70
+ async def stop(self) -> None:
71
+ """Stop watching for file changes."""
72
+ if self._task is None:
73
+ return
74
+
75
+ self._stop_event.set()
76
+ self._task.cancel()
77
+ with contextlib.suppress(asyncio.CancelledError):
78
+ await self._task
79
+ self._task = None
80
+
81
+ async def _watch_loop(self) -> None:
82
+ """Internal watch loop."""
83
+ str_paths = [str(p) for p in self.paths]
84
+
85
+ # Filter to only existing paths
86
+ existing_paths = [p for p in str_paths if Path(p).exists()]
87
+ if not existing_paths:
88
+ logger.warning("FileWatcher: no existing paths to watch from %s", str_paths)
89
+ return
90
+
91
+ logger.info("FileWatcher: starting watch loop for %s", existing_paths)
92
+ try:
93
+ async for changes in awatch(
94
+ *existing_paths,
95
+ debounce=self.debounce,
96
+ stop_event=self._stop_event,
97
+ ):
98
+ logger.info("FileWatcher detected changes: %s", changes)
99
+ # Don't let callback errors kill the watcher
100
+ try:
101
+ await self.callback(changes)
102
+ except Exception:
103
+ logger.exception("Error in file watcher callback")
104
+ except Exception:
105
+ logger.exception("FileWatcher watch loop failed")
106
+
107
+ async def __aenter__(self) -> Self:
108
+ """Start watcher on context enter."""
109
+ await self.start()
110
+ return self
111
+
112
+ async def __aexit__(self, *args: object) -> None:
113
+ """Stop watcher on context exit."""
114
+ await self.stop()
115
+
116
+
117
+ async def get_git_branch(repo_path: str | Path) -> str | None:
118
+ """Get the current git branch name.
119
+
120
+ Args:
121
+ repo_path: Path to the git repository
122
+
123
+ Returns:
124
+ Branch name or None if not a git repo or on detached HEAD
125
+
126
+ TODO: For remote/ACP support, this should accept an optional ExecutionEnvironment
127
+ and use env.execute_command() instead of subprocess. This would allow git commands
128
+ to run on the client side where the repository lives.
129
+ """
130
+ try:
131
+ proc = await asyncio.create_subprocess_exec(
132
+ "git",
133
+ "rev-parse",
134
+ "--abbrev-ref",
135
+ "HEAD",
136
+ cwd=str(repo_path),
137
+ stdout=asyncio.subprocess.PIPE,
138
+ stderr=asyncio.subprocess.PIPE,
139
+ )
140
+ stdout, _ = await proc.communicate()
141
+ if proc.returncode != 0:
142
+ return None
143
+ except OSError:
144
+ return None
145
+ else:
146
+ branch = stdout.decode().strip()
147
+ return branch if branch != "HEAD" else None
148
+
149
+
150
+ @dataclass
151
+ class GitBranchWatcher:
152
+ """Watches for git branch changes using polling.
153
+
154
+ Polls the current git branch periodically and calls a callback when it changes.
155
+ Uses polling instead of file watching because git uses atomic renames which
156
+ are not reliably detected by inotify/watchfiles.
157
+
158
+ Example:
159
+ ```python
160
+ async def on_branch_change(branch: str | None):
161
+ print(f"Branch changed to: {branch}")
162
+
163
+ watcher = GitBranchWatcher(
164
+ repo_path="/path/to/repo",
165
+ callback=on_branch_change,
166
+ )
167
+
168
+ async with watcher:
169
+ await asyncio.sleep(60)
170
+ ```
171
+
172
+ TODO: For remote/ACP support, this should accept an ExecutionEnvironment
173
+ and run git commands through env.execute_command(). The polling would still
174
+ happen server-side, but the git commands would execute on the client.
175
+ """
176
+
177
+ repo_path: str | Path
178
+ """Path to the git repository."""
179
+
180
+ callback: Callable[[str | None], Awaitable[None]]
181
+ """Async callback invoked with new branch name when branch changes."""
182
+
183
+ poll_interval: float = 1.0
184
+ """Polling interval in seconds."""
185
+
186
+ _current_branch: str | None = field(default=None, repr=False)
187
+ """Cached current branch."""
188
+
189
+ _task: asyncio.Task[None] | None = field(default=None, repr=False)
190
+ """Background polling task."""
191
+
192
+ _stop_event: asyncio.Event = field(default_factory=asyncio.Event, repr=False)
193
+ """Event to signal stop."""
194
+
195
+ async def start(self) -> None:
196
+ """Start watching for branch changes."""
197
+ if self._task is not None:
198
+ return # Already running
199
+
200
+ repo = Path(self.repo_path)
201
+ git_dir = repo / ".git"
202
+
203
+ # Handle git worktrees - .git might be a file pointing to the real git dir
204
+ if git_dir.is_file():
205
+ content = git_dir.read_text().strip()
206
+ if content.startswith("gitdir:"):
207
+ git_dir = Path(content[7:].strip())
208
+
209
+ if not git_dir.exists():
210
+ logger.warning("Git directory not found: %s", git_dir)
211
+ return
212
+
213
+ # Get initial branch
214
+ self._current_branch = await get_git_branch(self.repo_path)
215
+ logger.info(
216
+ "GitBranchWatcher started (polling), repo: %s, initial branch: %s",
217
+ self.repo_path,
218
+ self._current_branch,
219
+ )
220
+
221
+ self._stop_event.clear()
222
+ self._task = asyncio.create_task(self._poll_loop())
223
+
224
+ async def _poll_loop(self) -> None:
225
+ """Internal polling loop."""
226
+ while not self._stop_event.is_set():
227
+ try:
228
+ await asyncio.wait_for(
229
+ self._stop_event.wait(),
230
+ timeout=self.poll_interval,
231
+ )
232
+ break # Stop event was set
233
+ except TimeoutError:
234
+ # Poll interval elapsed, check for changes
235
+ pass
236
+
237
+ try:
238
+ new_branch = await get_git_branch(self.repo_path)
239
+ if new_branch != self._current_branch:
240
+ logger.info("Branch changed: %s -> %s", self._current_branch, new_branch)
241
+ self._current_branch = new_branch
242
+ await self.callback(new_branch)
243
+ except Exception:
244
+ logger.exception("Error polling git branch")
245
+
246
+ async def stop(self) -> None:
247
+ """Stop watching for branch changes."""
248
+ if self._task is None:
249
+ return
250
+
251
+ self._stop_event.set()
252
+ self._task.cancel()
253
+ with contextlib.suppress(asyncio.CancelledError):
254
+ await self._task
255
+ self._task = None
256
+
257
+ @property
258
+ def current_branch(self) -> str | None:
259
+ """Get the current cached branch name."""
260
+ return self._current_branch
261
+
262
+ async def __aenter__(self) -> Self:
263
+ """Start watcher on context enter."""
264
+ await self.start()
265
+ return self
266
+
267
+ async def __aexit__(self, *args: object) -> None:
268
+ """Stop watcher on context exit."""
269
+ await self.stop()
@@ -0,0 +1,121 @@
1
+ """Identifier generation utilities.
2
+
3
+ Generates IDs that are lexicographically sortable by creation time.
4
+ Format: {prefix}_{hex_timestamp}{random_base62}
5
+
6
+ Compatible with OpenCode's identifier format.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import secrets
12
+ import time
13
+ from typing import Literal
14
+
15
+
16
+ PrefixType = Literal["session", "message", "permission", "user", "part", "pty"]
17
+
18
+ PREFIXES: dict[PrefixType, str] = {
19
+ "session": "ses",
20
+ "message": "msg",
21
+ "permission": "per",
22
+ "user": "usr",
23
+ "part": "prt",
24
+ "pty": "pty",
25
+ }
26
+
27
+ BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
28
+ ID_LENGTH = 26 # Characters after prefix (12 hex + 14 base62)
29
+
30
+ # State for monotonic ID generation
31
+ _last_timestamp = 0
32
+ _counter = 0
33
+
34
+
35
+ def _random_base62(length: int) -> str:
36
+ """Generate random base62 string."""
37
+ return "".join(secrets.choice(BASE62_CHARS) for _ in range(length))
38
+
39
+
40
+ def ascending(prefix: PrefixType, given: str | None = None) -> str:
41
+ """Generate an ascending (chronologically sortable) ID.
42
+
43
+ Args:
44
+ prefix: The type prefix for the ID
45
+ given: If provided, validate and return this ID instead of generating
46
+
47
+ Returns:
48
+ A sortable ID with the format {prefix}_{hex_timestamp}{random}
49
+
50
+ Raises:
51
+ ValueError: If given ID doesn't start with expected prefix
52
+ """
53
+ if given is not None:
54
+ expected_prefix = PREFIXES[prefix]
55
+ if not given.startswith(expected_prefix):
56
+ msg = f"ID {given} does not start with {expected_prefix}"
57
+ raise ValueError(msg)
58
+ return given
59
+
60
+ return _create(prefix, descending=False)
61
+
62
+
63
+ def descending(prefix: PrefixType) -> str:
64
+ """Generate a descending (reverse chronologically sortable) ID.
65
+
66
+ Args:
67
+ prefix: The type prefix for the ID
68
+
69
+ Returns:
70
+ A reverse-sortable ID
71
+ """
72
+ return _create(prefix, descending=True)
73
+
74
+
75
+ def _create(prefix: PrefixType, *, descending: bool = False) -> str:
76
+ """Create a new ID with timestamp encoding.
77
+
78
+ Args:
79
+ prefix: The type prefix
80
+ descending: If True, invert the timestamp for reverse sorting
81
+
82
+ Returns:
83
+ A new ID string
84
+ """
85
+ global _last_timestamp, _counter # noqa: PLW0603
86
+
87
+ current_timestamp = int(time.time() * 1000) # milliseconds
88
+
89
+ if current_timestamp != _last_timestamp:
90
+ _last_timestamp = current_timestamp
91
+ _counter = 0
92
+ _counter += 1
93
+
94
+ # Combine timestamp and counter
95
+ now = current_timestamp * 0x1000 + _counter
96
+
97
+ if descending:
98
+ now = ~now & 0xFFFFFFFFFFFF # Invert for descending order (48 bits)
99
+
100
+ # Encode as 6 bytes (48 bits), big-endian
101
+ time_bytes = bytearray(6)
102
+ for i in range(6):
103
+ time_bytes[i] = (now >> (40 - 8 * i)) & 0xFF
104
+
105
+ time_hex = time_bytes.hex()
106
+
107
+ # Add random suffix (14 chars for 26 total after prefix)
108
+ random_suffix = _random_base62(ID_LENGTH - 12)
109
+
110
+ return f"{PREFIXES[prefix]}_{time_hex}{random_suffix}"
111
+
112
+
113
+ def generate_session_id() -> str:
114
+ """Generate a unique, chronologically sortable session ID.
115
+
116
+ Convenience function for the common case.
117
+
118
+ Returns:
119
+ A session ID like 'ses_b71310fdf001ZHcn6VSpkaBcHi'
120
+ """
121
+ return ascending("session")
@@ -0,0 +1,46 @@
1
+ """Helper utilities for working with pydantic-ai message types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from pydantic_ai.messages import BaseToolCallPart
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from pydantic_ai.messages import ToolCallPartDelta
12
+
13
+
14
+ def safe_args_as_dict(
15
+ part: BaseToolCallPart | ToolCallPartDelta,
16
+ *,
17
+ default: dict[str, Any] | None = None,
18
+ ) -> dict[str, Any]:
19
+ """Safely extract args as dict from a tool call part.
20
+
21
+ Models can return malformed JSON for tool arguments, especially during
22
+ streaming when args are still being assembled. This helper catches parse
23
+ errors and returns a fallback value.
24
+
25
+ Args:
26
+ part: A tool call part (complete or delta) with args to extract
27
+ default: Value to return on parse failure. If None, returns {"_raw_args": ...}
28
+ with the original unparsed args.
29
+
30
+ Returns:
31
+ The parsed arguments dict, or a fallback on parse failure.
32
+ """
33
+ if not isinstance(part, BaseToolCallPart):
34
+ # ToolCallPartDelta doesn't have args_as_dict
35
+ if default is not None:
36
+ return default
37
+ raw = getattr(part, "args", None)
38
+ return {"_raw_args": raw} if raw else {}
39
+ try:
40
+ return part.args_as_dict()
41
+ except ValueError:
42
+ # Model returned malformed JSON for tool args
43
+ if default is not None:
44
+ return default
45
+ # Preserve raw args for debugging/inspection
46
+ return {"_raw_args": part.args} if part.args else {}