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
@@ -0,0 +1,200 @@
1
+ """Grep search tool implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from agentpool.agents.context import AgentContext # noqa: TC001
10
+ from agentpool.log import get_logger
11
+ from agentpool.tools.base import Tool, ToolResult
12
+
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Awaitable, Callable
16
+
17
+ from exxec import ExecutionEnvironment
18
+ from fsspec.asyn import AsyncFileSystem
19
+
20
+ from agentpool_toolsets.fsspec_toolset.grep import GrepBackend
21
+
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ @dataclass
27
+ class GrepTool(Tool[ToolResult]):
28
+ """Search file contents for patterns using grep.
29
+
30
+ A standalone tool for searching file contents with:
31
+ - Regex pattern matching
32
+ - File filtering with glob patterns
33
+ - Context lines before/after matches
34
+ - Subprocess grep (ripgrep/grep) or pure Python fallback
35
+ - Configurable output limits
36
+
37
+ Use create_grep_tool() factory for convenient instantiation.
38
+ """
39
+
40
+ # Tool-specific configuration
41
+ env: ExecutionEnvironment | None = None
42
+ """Execution environment to use. Falls back to agent.env if not set."""
43
+
44
+ cwd: str | None = None
45
+ """Working directory for resolving relative paths."""
46
+
47
+ max_output_kb: int = 64
48
+ """Maximum output size in KB."""
49
+
50
+ use_subprocess_grep: bool = True
51
+ """Use ripgrep/grep subprocess if available (faster for large codebases)."""
52
+
53
+ _grep_backend: GrepBackend | None = field(default=None, init=False)
54
+ """Cached grep backend detection."""
55
+
56
+ def get_callable(self) -> Callable[..., Awaitable[ToolResult]]:
57
+ """Return the grep method as the callable."""
58
+ return self._grep
59
+
60
+ def _get_fs(self, ctx: AgentContext) -> AsyncFileSystem:
61
+ """Get filesystem from env, falling back to agent's env if not set."""
62
+ from fsspec.asyn import AsyncFileSystem
63
+ from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
64
+
65
+ if self.env is not None:
66
+ fs = self.env.get_fs()
67
+ return fs if isinstance(fs, AsyncFileSystem) else AsyncFileSystemWrapper(fs)
68
+ fs = ctx.agent.env.get_fs()
69
+ return fs if isinstance(fs, AsyncFileSystem) else AsyncFileSystemWrapper(fs)
70
+
71
+ def _resolve_path(self, path: str, ctx: AgentContext) -> str:
72
+ """Resolve a potentially relative path to an absolute path."""
73
+ cwd: str | None = None
74
+ if self.cwd:
75
+ cwd = self.cwd
76
+ elif self.env and self.env.cwd:
77
+ cwd = self.env.cwd
78
+ elif ctx.agent.env and ctx.agent.env.cwd:
79
+ cwd = ctx.agent.env.cwd
80
+
81
+ if cwd and not (path.startswith("/") or (len(path) > 1 and path[1] == ":")):
82
+ return str(Path(cwd) / path)
83
+ return path
84
+
85
+ async def _grep(
86
+ self,
87
+ ctx: AgentContext,
88
+ pattern: str,
89
+ path: str,
90
+ *,
91
+ file_pattern: str = "**/*",
92
+ case_sensitive: bool = False,
93
+ max_matches: int = 100,
94
+ context_lines: int = 0,
95
+ ) -> ToolResult:
96
+ """Search file contents for a pattern.
97
+
98
+ Args:
99
+ ctx: Agent context for event emission and filesystem access
100
+ pattern: Regex pattern to search for
101
+ path: Base directory to search in
102
+ file_pattern: Glob pattern to filter files (e.g. "**/*.py")
103
+ case_sensitive: Whether search is case-sensitive
104
+ max_matches: Maximum number of matches to return
105
+ context_lines: Number of context lines before/after match
106
+
107
+ Returns:
108
+ Grep results as formatted text
109
+ """
110
+ from agentpool_toolsets.fsspec_toolset.grep import (
111
+ DEFAULT_EXCLUDE_PATTERNS,
112
+ GrepBackend,
113
+ detect_grep_backend,
114
+ grep_with_fsspec,
115
+ grep_with_subprocess,
116
+ )
117
+
118
+ resolved_path = self._resolve_path(path, ctx)
119
+ msg = f"Searching for {pattern!r} in {resolved_path}"
120
+ await ctx.events.tool_call_start(title=msg, kind="search", locations=[resolved_path])
121
+
122
+ max_output_bytes = self.max_output_kb * 1024
123
+ result: dict[str, Any] | None = None
124
+
125
+ try:
126
+ # Try subprocess grep if configured and available
127
+ if self.use_subprocess_grep:
128
+ # Get execution environment for running grep command
129
+ env = self.env or ctx.agent.env
130
+ if env is not None:
131
+ # Detect and cache grep backend
132
+ if self._grep_backend is None:
133
+ self._grep_backend = await detect_grep_backend(env)
134
+ # Only use subprocess if we have a real grep backend
135
+ if self._grep_backend != GrepBackend.PYTHON:
136
+ result = await grep_with_subprocess(
137
+ env=env,
138
+ pattern=pattern,
139
+ path=resolved_path,
140
+ backend=self._grep_backend,
141
+ case_sensitive=case_sensitive,
142
+ max_matches=max_matches,
143
+ max_output_bytes=max_output_bytes,
144
+ exclude_patterns=DEFAULT_EXCLUDE_PATTERNS,
145
+ use_gitignore=True,
146
+ context_lines=context_lines,
147
+ )
148
+
149
+ # Fallback to fsspec grep if subprocess didn't work
150
+ if result is None or "error" in result:
151
+ fs = self._get_fs(ctx)
152
+ result = await grep_with_fsspec(
153
+ fs=fs,
154
+ pattern=pattern,
155
+ path=resolved_path,
156
+ file_pattern=file_pattern,
157
+ case_sensitive=case_sensitive,
158
+ max_matches=max_matches,
159
+ max_output_bytes=max_output_bytes,
160
+ context_lines=context_lines,
161
+ )
162
+
163
+ if "error" in result:
164
+ error_msg = f"Error: {result['error']}"
165
+ return ToolResult(
166
+ content=error_msg,
167
+ metadata={"matches": 0, "truncated": False},
168
+ )
169
+
170
+ # Format output
171
+ matches = result.get("matches", "")
172
+ match_count = result.get("match_count", 0)
173
+ was_truncated = result.get("was_truncated", False)
174
+
175
+ if not matches:
176
+ output = f"No matches found for pattern '{pattern}'"
177
+ else:
178
+ output = f"Found {match_count} matches:\n\n{matches}"
179
+ if was_truncated:
180
+ output += "\n\n[Results truncated]"
181
+
182
+ # Emit formatted content for UI display
183
+ from agentpool.agents.events import TextContentItem
184
+
185
+ await ctx.events.tool_call_progress(
186
+ title=f"Found {match_count} matches",
187
+ items=[TextContentItem(text=output)],
188
+ replace_content=True,
189
+ )
190
+ except Exception as e: # noqa: BLE001
191
+ error_msg = f"Error: Grep failed: {e}"
192
+ return ToolResult(
193
+ content=error_msg,
194
+ metadata={"matches": 0, "truncated": False},
195
+ )
196
+ else:
197
+ return ToolResult(
198
+ content=output,
199
+ metadata={"matches": match_count, "truncated": was_truncated},
200
+ )
@@ -0,0 +1,73 @@
1
+ """List directory tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+ from agentpool.tool_impls.list_directory.tool import ListDirectoryTool
8
+ from agentpool_config.tools import ToolHints
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from exxec import ExecutionEnvironment
13
+
14
+ __all__ = ["ListDirectoryTool", "create_list_directory_tool"]
15
+
16
+ # Tool metadata defaults
17
+ NAME = "list_directory"
18
+ DESCRIPTION = """List files in a directory with filtering support.
19
+
20
+ Supports:
21
+ - Glob pattern matching (*, **, *.py, etc.)
22
+ - Exclude patterns for filtering
23
+ - Recursive directory traversal with depth control
24
+ - Safety limits to prevent overwhelming output"""
25
+ CATEGORY: Literal["read"] = "read"
26
+ HINTS = ToolHints(read_only=True, idempotent=True)
27
+
28
+
29
+ def create_list_directory_tool(
30
+ *,
31
+ env: ExecutionEnvironment | None = None,
32
+ cwd: str | None = None,
33
+ max_items: int = 500,
34
+ name: str = NAME,
35
+ description: str = DESCRIPTION,
36
+ requires_confirmation: bool = False,
37
+ ) -> ListDirectoryTool:
38
+ """Create a configured ListDirectoryTool instance.
39
+
40
+ Args:
41
+ env: Execution environment to use. Falls back to agent.env if not set.
42
+ cwd: Working directory for resolving relative paths.
43
+ max_items: Maximum number of items to return (safety limit, default: 500).
44
+ name: Tool name override.
45
+ description: Tool description override.
46
+ requires_confirmation: Whether tool execution needs confirmation.
47
+
48
+ Returns:
49
+ Configured ListDirectoryTool instance.
50
+
51
+ Example:
52
+ # Basic usage
53
+ ls = create_list_directory_tool()
54
+
55
+ # With custom limits
56
+ ls = create_list_directory_tool(max_items=1000)
57
+
58
+ # With specific environment
59
+ ls = create_list_directory_tool(
60
+ env=my_env,
61
+ cwd="/workspace",
62
+ )
63
+ """
64
+ return ListDirectoryTool(
65
+ name=name,
66
+ description=description,
67
+ category=CATEGORY,
68
+ hints=HINTS,
69
+ env=env,
70
+ cwd=cwd,
71
+ max_items=max_items,
72
+ requires_confirmation=requires_confirmation,
73
+ )
@@ -0,0 +1,197 @@
1
+ """List directory tool implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from fnmatch import fnmatch
7
+ import os
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from upathtools import is_directory
12
+
13
+ from agentpool.agents.context import AgentContext # noqa: TC001
14
+ from agentpool.log import get_logger
15
+ from agentpool.tools.base import Tool, ToolResult
16
+
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Awaitable, Callable
20
+
21
+ from exxec import ExecutionEnvironment
22
+ from fsspec.asyn import AsyncFileSystem
23
+
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ @dataclass
29
+ class ListDirectoryTool(Tool[ToolResult]):
30
+ """List files in a directory with filtering support.
31
+
32
+ A standalone tool for listing directory contents with:
33
+ - Glob pattern matching (*, **, *.py, etc.)
34
+ - Exclude patterns for filtering
35
+ - Recursive directory traversal with depth control
36
+ - Formatted markdown output
37
+
38
+ Use create_list_directory_tool() factory for convenient instantiation.
39
+ """
40
+
41
+ # Tool-specific configuration
42
+ env: ExecutionEnvironment | None = None
43
+ """Execution environment to use. Falls back to agent.env if not set."""
44
+
45
+ cwd: str | None = None
46
+ """Working directory for resolving relative paths."""
47
+
48
+ max_items: int = 500
49
+ """Maximum number of items to return (safety limit)."""
50
+
51
+ def get_callable(self) -> Callable[..., Awaitable[ToolResult]]:
52
+ """Return the list_directory method as the callable."""
53
+ return self._list_directory
54
+
55
+ def _get_fs(self, ctx: AgentContext) -> AsyncFileSystem:
56
+ """Get filesystem from env, falling back to agent's env if not set."""
57
+ from fsspec.asyn import AsyncFileSystem
58
+ from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
59
+
60
+ if self.env is not None:
61
+ fs = self.env.get_fs()
62
+ return fs if isinstance(fs, AsyncFileSystem) else AsyncFileSystemWrapper(fs)
63
+ fs = ctx.agent.env.get_fs()
64
+ return fs if isinstance(fs, AsyncFileSystem) else AsyncFileSystemWrapper(fs)
65
+
66
+ def _resolve_path(self, path: str, ctx: AgentContext) -> str:
67
+ """Resolve a potentially relative path to an absolute path."""
68
+ cwd: str | None = None
69
+ if self.cwd:
70
+ cwd = self.cwd
71
+ elif self.env and self.env.cwd:
72
+ cwd = self.env.cwd
73
+ elif ctx.agent.env and ctx.agent.env.cwd:
74
+ cwd = ctx.agent.env.cwd
75
+
76
+ if cwd and not (path.startswith("/") or (len(path) > 1 and path[1] == ":")):
77
+ return str(Path(cwd) / path)
78
+ return path
79
+
80
+ async def _list_directory(
81
+ self,
82
+ ctx: AgentContext,
83
+ path: str,
84
+ *,
85
+ pattern: str = "*",
86
+ exclude: list[str] | None = None,
87
+ max_depth: int = 1,
88
+ ) -> ToolResult:
89
+ """List files in a directory with filtering support.
90
+
91
+ Args:
92
+ ctx: Agent context for event emission and filesystem access
93
+ path: Base directory to list
94
+ pattern: Glob pattern to match files against. Use "*.py" to match Python
95
+ files in current directory only, or "**/*.py" to match recursively.
96
+ The max_depth parameter limits how deep "**" patterns search.
97
+ exclude: List of patterns to exclude (uses fnmatch against relative paths)
98
+ max_depth: Maximum directory depth to search (default: 1 = current dir only).
99
+ Only affects recursive "**" patterns.
100
+
101
+ Returns:
102
+ Markdown-formatted directory listing
103
+ """
104
+ from agentpool_toolsets.fsspec_toolset.helpers import format_directory_listing
105
+
106
+ path = self._resolve_path(path, ctx)
107
+ msg = f"Listing directory: {path}"
108
+ await ctx.events.tool_call_start(title=msg, kind="read", locations=[path])
109
+
110
+ try:
111
+ fs = self._get_fs(ctx)
112
+ # Check if path exists
113
+ if not await fs._exists(path):
114
+ error_msg = f"Path does not exist: {path}"
115
+ await ctx.events.file_operation("list", path=path, success=False, error=error_msg)
116
+ return ToolResult(
117
+ content=f"Error: {error_msg}",
118
+ metadata={"count": 0, "truncated": False},
119
+ )
120
+
121
+ # Build glob path
122
+ glob_pattern = f"{path.rstrip('/')}/{pattern}"
123
+ paths = await fs._glob(glob_pattern, maxdepth=max_depth, detail=True)
124
+
125
+ files: list[dict[str, Any]] = []
126
+ dirs: list[dict[str, Any]] = []
127
+
128
+ # Safety check - prevent returning too many items
129
+ total_found = len(paths)
130
+ if total_found > self.max_items:
131
+ suggestions = []
132
+ if pattern == "*":
133
+ suggestions.append("Use a more specific pattern like '*.py', '*.txt', etc.")
134
+ if max_depth > 1:
135
+ suggestions.append(f"Reduce max_depth from {max_depth} to 1 or 2.")
136
+ if not exclude:
137
+ suggestions.append("Use exclude parameter to filter out unwanted directories.")
138
+
139
+ suggestion_text = " ".join(suggestions) if suggestions else ""
140
+ error_msg = f"Error: Too many items ({total_found:,}). {suggestion_text}"
141
+ return ToolResult(
142
+ content=error_msg,
143
+ metadata={"count": total_found, "truncated": True},
144
+ )
145
+
146
+ for file_path, file_info in paths.items(): # pyright: ignore[reportAttributeAccessIssue]
147
+ rel_path = os.path.relpath(str(file_path), path)
148
+
149
+ # Skip excluded patterns
150
+ if exclude and any(fnmatch(rel_path, pat) for pat in exclude):
151
+ continue
152
+
153
+ # Use type from glob detail info, falling back to isdir only if needed
154
+ is_dir = await is_directory(fs, file_path, entry_type=file_info.get("type")) # pyright: ignore[reportArgumentType]
155
+
156
+ item_info = {
157
+ "name": Path(file_path).name, # pyright: ignore[reportArgumentType]
158
+ "path": file_path,
159
+ "relative_path": rel_path,
160
+ "size": file_info.get("size", 0),
161
+ "type": "directory" if is_dir else "file",
162
+ }
163
+ if "mtime" in file_info:
164
+ item_info["modified"] = file_info["mtime"]
165
+
166
+ if is_dir:
167
+ dirs.append(item_info)
168
+ else:
169
+ files.append(item_info)
170
+
171
+ await ctx.events.file_operation("list", path=path, success=True)
172
+ result = format_directory_listing(path, dirs, files, pattern)
173
+ # Emit formatted content for UI display
174
+ from agentpool.agents.events import TextContentItem
175
+
176
+ await ctx.events.tool_call_progress(
177
+ title=f"Listed: {path}",
178
+ items=[TextContentItem(text=result)],
179
+ replace_content=True,
180
+ )
181
+ except (OSError, ValueError, FileNotFoundError) as e:
182
+ await ctx.events.file_operation("list", path=path, success=False, error=str(e))
183
+ error_msg = (
184
+ f"Error: Could not list directory: {path}. Ensure path is absolute and exists."
185
+ )
186
+ return ToolResult(
187
+ content=error_msg,
188
+ metadata={"count": 0, "truncated": False},
189
+ )
190
+ else:
191
+ total_items = len(files) + len(dirs)
192
+ # Check if we hit the limit (truncation)
193
+ was_truncated = total_items >= self.max_items
194
+ return ToolResult(
195
+ content=result,
196
+ metadata={"count": total_items, "truncated": was_truncated},
197
+ )
@@ -0,0 +1,42 @@
1
+ """User interaction tool for asking clarifying questions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from agentpool.tool_impls.question.tool import QuestionTool
8
+ from agentpool_config.tools import ToolHints
9
+
10
+
11
+ __all__ = ["QuestionTool", "create_question_tool"]
12
+
13
+ # Tool metadata defaults
14
+ NAME = "question"
15
+ DESCRIPTION = "Ask the user a clarifying question during processing."
16
+ CATEGORY: Literal["other"] = "other"
17
+ HINTS = ToolHints(open_world=True)
18
+
19
+
20
+ def create_question_tool(
21
+ *,
22
+ name: str = NAME,
23
+ description: str = DESCRIPTION,
24
+ requires_confirmation: bool = False,
25
+ ) -> QuestionTool:
26
+ """Create a configured QuestionTool instance.
27
+
28
+ Args:
29
+ name: Tool name override.
30
+ description: Tool description override.
31
+ requires_confirmation: Whether tool execution needs confirmation.
32
+
33
+ Returns:
34
+ Configured QuestionTool instance.
35
+ """
36
+ return QuestionTool(
37
+ name=name,
38
+ description=description,
39
+ category=CATEGORY,
40
+ hints=HINTS,
41
+ requires_confirmation=requires_confirmation,
42
+ )
@@ -0,0 +1,127 @@
1
+ """AskUser tool implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any, assert_never
7
+
8
+ from agentpool.agents.context import AgentContext # noqa: TC001
9
+ from agentpool.tools.base import Tool, ToolResult
10
+
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Awaitable, Callable
14
+
15
+
16
+ @dataclass
17
+ class QuestionOption:
18
+ """Option for a question in OpenCode format.
19
+
20
+ Represents a single choice that can be presented to the user.
21
+ """
22
+
23
+ label: str
24
+ """Display text (1-5 words, concise)."""
25
+
26
+ description: str
27
+ """Explanation of the choice."""
28
+
29
+
30
+ @dataclass
31
+ class OpenCodeQuestionInfo:
32
+ """Question information in OpenCode format.
33
+
34
+ This matches OpenCode's QuestionInfo schema used by the TUI.
35
+ """
36
+
37
+ question: str
38
+ """Complete question text."""
39
+
40
+ header: str
41
+ """Very short label (max 12 chars) - used for tab headers in the TUI."""
42
+
43
+ options: list[QuestionOption]
44
+ """Available choices for the user to select from."""
45
+
46
+ multiple: bool = False
47
+ """Allow selecting multiple choices."""
48
+
49
+
50
+ @dataclass
51
+ class QuestionTool(Tool[ToolResult]):
52
+ """Tool for asking the user clarifying questions.
53
+
54
+ Enables agents to ask users for additional information or clarification
55
+ when needed to complete a task effectively.
56
+ """
57
+
58
+ def get_callable(self) -> Callable[..., Awaitable[ToolResult]]:
59
+ """Get the tool callable."""
60
+ return self._execute
61
+
62
+ async def _execute(
63
+ self,
64
+ ctx: AgentContext,
65
+ prompt: str,
66
+ response_schema: dict[str, Any] | None = None,
67
+ ) -> ToolResult:
68
+ """Ask the user a clarifying question.
69
+
70
+ Args:
71
+ ctx: Agent execution context.
72
+ prompt: Question to ask the user.
73
+ response_schema: Optional JSON schema for structured response.
74
+
75
+ Returns:
76
+ The user's response as a string.
77
+ """
78
+ from mcp.types import ElicitRequestFormParams, ElicitResult, ErrorData
79
+
80
+ schema = response_schema or {"type": "string"}
81
+ params = ElicitRequestFormParams(message=prompt, requestedSchema=schema)
82
+ result = await ctx.handle_elicitation(params)
83
+
84
+ match result:
85
+ case ElicitResult(action="accept", content=content):
86
+ # Content is a dict with "value" key per MCP spec
87
+ if isinstance(content, dict) and "value" in content:
88
+ value = content["value"]
89
+ # Handle list responses (multi-select)
90
+ if isinstance(value, list):
91
+ answer_str = ", ".join(str(v) for v in value)
92
+ # OpenCode expects array of arrays (one per question)
93
+ return ToolResult(
94
+ content=answer_str,
95
+ metadata={
96
+ "answers": [value]
97
+ }, # Single question, array of selected values
98
+ )
99
+ # Single answer
100
+ answer_str = str(value)
101
+ return ToolResult(
102
+ content=answer_str,
103
+ metadata={"answers": [[answer_str]]}, # Single question, single answer
104
+ )
105
+ # Fallback for plain content
106
+ answer_str = str(content)
107
+ return ToolResult(
108
+ content=answer_str,
109
+ metadata={"answers": [[answer_str]]},
110
+ )
111
+ case ElicitResult(action="cancel"):
112
+ return ToolResult(
113
+ content="User cancelled the request",
114
+ metadata={"answers": []},
115
+ )
116
+ case ElicitResult():
117
+ return ToolResult(
118
+ content="User declined to answer",
119
+ metadata={"answers": []},
120
+ )
121
+ case ErrorData(message=message):
122
+ return ToolResult(
123
+ content=f"Error: {message}",
124
+ metadata={"answers": []},
125
+ )
126
+ case _ as unreachable:
127
+ assert_never(unreachable)