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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. acp/__init__.py +0 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/client/connection.py +38 -29
  7. acp/client/implementations/default_client.py +3 -2
  8. acp/client/implementations/headless_client.py +2 -2
  9. acp/connection.py +2 -2
  10. acp/notifications.py +18 -49
  11. acp/schema/__init__.py +2 -0
  12. acp/schema/agent_responses.py +21 -0
  13. acp/schema/client_requests.py +3 -3
  14. acp/schema/session_state.py +63 -29
  15. acp/task/supervisor.py +2 -2
  16. acp/utils.py +2 -2
  17. agentpool/__init__.py +2 -0
  18. agentpool/agents/acp_agent/acp_agent.py +278 -263
  19. agentpool/agents/acp_agent/acp_converters.py +150 -17
  20. agentpool/agents/acp_agent/client_handler.py +35 -24
  21. agentpool/agents/acp_agent/session_state.py +14 -6
  22. agentpool/agents/agent.py +471 -643
  23. agentpool/agents/agui_agent/agui_agent.py +104 -107
  24. agentpool/agents/agui_agent/helpers.py +3 -4
  25. agentpool/agents/base_agent.py +485 -32
  26. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  27. agentpool/agents/claude_code_agent/__init__.py +13 -1
  28. agentpool/agents/claude_code_agent/claude_code_agent.py +654 -334
  29. agentpool/agents/claude_code_agent/converters.py +4 -141
  30. agentpool/agents/claude_code_agent/models.py +77 -0
  31. agentpool/agents/claude_code_agent/static_info.py +100 -0
  32. agentpool/agents/claude_code_agent/usage.py +242 -0
  33. agentpool/agents/events/__init__.py +22 -0
  34. agentpool/agents/events/builtin_handlers.py +65 -0
  35. agentpool/agents/events/event_emitter.py +3 -0
  36. agentpool/agents/events/events.py +84 -3
  37. agentpool/agents/events/infer_info.py +145 -0
  38. agentpool/agents/events/processors.py +254 -0
  39. agentpool/agents/interactions.py +41 -6
  40. agentpool/agents/modes.py +13 -0
  41. agentpool/agents/slashed_agent.py +5 -4
  42. agentpool/agents/tool_wrapping.py +18 -6
  43. agentpool/common_types.py +35 -21
  44. agentpool/config_resources/acp_assistant.yml +2 -2
  45. agentpool/config_resources/agents.yml +3 -0
  46. agentpool/config_resources/agents_template.yml +1 -0
  47. agentpool/config_resources/claude_code_agent.yml +9 -8
  48. agentpool/config_resources/external_acp_agents.yml +2 -1
  49. agentpool/delegation/base_team.py +4 -30
  50. agentpool/delegation/pool.py +104 -265
  51. agentpool/delegation/team.py +57 -57
  52. agentpool/delegation/teamrun.py +50 -55
  53. agentpool/functional/run.py +10 -4
  54. agentpool/mcp_server/client.py +73 -38
  55. agentpool/mcp_server/conversions.py +54 -13
  56. agentpool/mcp_server/manager.py +9 -23
  57. agentpool/mcp_server/registries/official_registry_client.py +10 -1
  58. agentpool/mcp_server/tool_bridge.py +114 -79
  59. agentpool/messaging/connection_manager.py +11 -10
  60. agentpool/messaging/event_manager.py +5 -5
  61. agentpool/messaging/message_container.py +6 -30
  62. agentpool/messaging/message_history.py +87 -8
  63. agentpool/messaging/messagenode.py +52 -14
  64. agentpool/messaging/messages.py +2 -26
  65. agentpool/messaging/processing.py +10 -22
  66. agentpool/models/__init__.py +1 -1
  67. agentpool/models/acp_agents/base.py +6 -2
  68. agentpool/models/acp_agents/mcp_capable.py +124 -15
  69. agentpool/models/acp_agents/non_mcp.py +0 -23
  70. agentpool/models/agents.py +66 -66
  71. agentpool/models/agui_agents.py +1 -1
  72. agentpool/models/claude_code_agents.py +111 -17
  73. agentpool/models/file_parsing.py +0 -1
  74. agentpool/models/manifest.py +70 -50
  75. agentpool/prompts/conversion_manager.py +1 -1
  76. agentpool/prompts/prompts.py +5 -2
  77. agentpool/resource_providers/__init__.py +2 -0
  78. agentpool/resource_providers/aggregating.py +4 -2
  79. agentpool/resource_providers/base.py +13 -3
  80. agentpool/resource_providers/codemode/code_executor.py +72 -5
  81. agentpool/resource_providers/codemode/helpers.py +2 -2
  82. agentpool/resource_providers/codemode/provider.py +64 -12
  83. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  84. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  85. agentpool/resource_providers/filtering.py +3 -1
  86. agentpool/resource_providers/mcp_provider.py +66 -12
  87. agentpool/resource_providers/plan_provider.py +111 -18
  88. agentpool/resource_providers/pool.py +5 -3
  89. agentpool/resource_providers/resource_info.py +111 -0
  90. agentpool/resource_providers/static.py +2 -2
  91. agentpool/sessions/__init__.py +2 -0
  92. agentpool/sessions/manager.py +2 -3
  93. agentpool/sessions/models.py +9 -6
  94. agentpool/sessions/protocol.py +28 -0
  95. agentpool/sessions/session.py +11 -55
  96. agentpool/storage/manager.py +361 -54
  97. agentpool/talk/registry.py +4 -4
  98. agentpool/talk/talk.py +9 -10
  99. agentpool/testing.py +1 -1
  100. agentpool/tool_impls/__init__.py +6 -0
  101. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  102. agentpool/tool_impls/agent_cli/tool.py +95 -0
  103. agentpool/tool_impls/bash/__init__.py +64 -0
  104. agentpool/tool_impls/bash/helpers.py +35 -0
  105. agentpool/tool_impls/bash/tool.py +171 -0
  106. agentpool/tool_impls/delete_path/__init__.py +70 -0
  107. agentpool/tool_impls/delete_path/tool.py +142 -0
  108. agentpool/tool_impls/download_file/__init__.py +80 -0
  109. agentpool/tool_impls/download_file/tool.py +183 -0
  110. agentpool/tool_impls/execute_code/__init__.py +55 -0
  111. agentpool/tool_impls/execute_code/tool.py +163 -0
  112. agentpool/tool_impls/grep/__init__.py +80 -0
  113. agentpool/tool_impls/grep/tool.py +200 -0
  114. agentpool/tool_impls/list_directory/__init__.py +73 -0
  115. agentpool/tool_impls/list_directory/tool.py +197 -0
  116. agentpool/tool_impls/question/__init__.py +42 -0
  117. agentpool/tool_impls/question/tool.py +127 -0
  118. agentpool/tool_impls/read/__init__.py +104 -0
  119. agentpool/tool_impls/read/tool.py +305 -0
  120. agentpool/tools/__init__.py +2 -1
  121. agentpool/tools/base.py +114 -34
  122. agentpool/tools/manager.py +57 -1
  123. agentpool/ui/base.py +2 -2
  124. agentpool/ui/mock_provider.py +2 -2
  125. agentpool/ui/stdlib_provider.py +2 -2
  126. agentpool/utils/streams.py +21 -96
  127. agentpool/vfs_registry.py +7 -2
  128. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
  129. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
  130. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  131. agentpool_cli/__main__.py +20 -0
  132. agentpool_cli/create.py +1 -1
  133. agentpool_cli/serve_acp.py +59 -1
  134. agentpool_cli/serve_opencode.py +1 -1
  135. agentpool_cli/ui.py +557 -0
  136. agentpool_commands/__init__.py +12 -5
  137. agentpool_commands/agents.py +1 -1
  138. agentpool_commands/pool.py +260 -0
  139. agentpool_commands/session.py +1 -1
  140. agentpool_commands/text_sharing/__init__.py +119 -0
  141. agentpool_commands/text_sharing/base.py +123 -0
  142. agentpool_commands/text_sharing/github_gist.py +80 -0
  143. agentpool_commands/text_sharing/opencode.py +462 -0
  144. agentpool_commands/text_sharing/paste_rs.py +59 -0
  145. agentpool_commands/text_sharing/pastebin.py +116 -0
  146. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  147. agentpool_commands/utils.py +31 -32
  148. agentpool_config/__init__.py +30 -2
  149. agentpool_config/agentpool_tools.py +498 -0
  150. agentpool_config/converters.py +1 -1
  151. agentpool_config/event_handlers.py +42 -0
  152. agentpool_config/events.py +1 -1
  153. agentpool_config/forward_targets.py +1 -4
  154. agentpool_config/jinja.py +3 -3
  155. agentpool_config/mcp_server.py +1 -5
  156. agentpool_config/nodes.py +1 -1
  157. agentpool_config/observability.py +44 -0
  158. agentpool_config/session.py +0 -3
  159. agentpool_config/storage.py +38 -39
  160. agentpool_config/task.py +3 -3
  161. agentpool_config/tools.py +11 -28
  162. agentpool_config/toolsets.py +22 -90
  163. agentpool_server/a2a_server/agent_worker.py +307 -0
  164. agentpool_server/a2a_server/server.py +23 -18
  165. agentpool_server/acp_server/acp_agent.py +125 -56
  166. agentpool_server/acp_server/commands/acp_commands.py +46 -216
  167. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
  168. agentpool_server/acp_server/event_converter.py +651 -0
  169. agentpool_server/acp_server/input_provider.py +53 -10
  170. agentpool_server/acp_server/server.py +1 -11
  171. agentpool_server/acp_server/session.py +90 -410
  172. agentpool_server/acp_server/session_manager.py +8 -34
  173. agentpool_server/agui_server/server.py +3 -1
  174. agentpool_server/mcp_server/server.py +5 -2
  175. agentpool_server/opencode_server/ENDPOINTS.md +53 -14
  176. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  177. agentpool_server/opencode_server/__init__.py +0 -8
  178. agentpool_server/opencode_server/converters.py +132 -26
  179. agentpool_server/opencode_server/input_provider.py +160 -8
  180. agentpool_server/opencode_server/models/__init__.py +42 -20
  181. agentpool_server/opencode_server/models/app.py +12 -0
  182. agentpool_server/opencode_server/models/events.py +203 -29
  183. agentpool_server/opencode_server/models/mcp.py +19 -0
  184. agentpool_server/opencode_server/models/message.py +18 -1
  185. agentpool_server/opencode_server/models/parts.py +134 -1
  186. agentpool_server/opencode_server/models/question.py +56 -0
  187. agentpool_server/opencode_server/models/session.py +13 -1
  188. agentpool_server/opencode_server/routes/__init__.py +4 -0
  189. agentpool_server/opencode_server/routes/agent_routes.py +33 -2
  190. agentpool_server/opencode_server/routes/app_routes.py +66 -3
  191. agentpool_server/opencode_server/routes/config_routes.py +66 -5
  192. agentpool_server/opencode_server/routes/file_routes.py +184 -5
  193. agentpool_server/opencode_server/routes/global_routes.py +1 -1
  194. agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
  195. agentpool_server/opencode_server/routes/message_routes.py +122 -66
  196. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  197. agentpool_server/opencode_server/routes/pty_routes.py +23 -22
  198. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  199. agentpool_server/opencode_server/routes/session_routes.py +139 -68
  200. agentpool_server/opencode_server/routes/tui_routes.py +1 -1
  201. agentpool_server/opencode_server/server.py +47 -2
  202. agentpool_server/opencode_server/state.py +30 -0
  203. agentpool_storage/__init__.py +0 -4
  204. agentpool_storage/base.py +81 -2
  205. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  206. agentpool_storage/claude_provider/__init__.py +42 -0
  207. agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
  208. agentpool_storage/file_provider.py +149 -15
  209. agentpool_storage/memory_provider.py +132 -12
  210. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  211. agentpool_storage/opencode_provider/__init__.py +16 -0
  212. agentpool_storage/opencode_provider/helpers.py +414 -0
  213. agentpool_storage/opencode_provider/provider.py +895 -0
  214. agentpool_storage/session_store.py +20 -6
  215. agentpool_storage/sql_provider/sql_provider.py +135 -2
  216. agentpool_storage/sql_provider/utils.py +2 -12
  217. agentpool_storage/zed_provider/__init__.py +16 -0
  218. agentpool_storage/zed_provider/helpers.py +281 -0
  219. agentpool_storage/zed_provider/models.py +130 -0
  220. agentpool_storage/zed_provider/provider.py +442 -0
  221. agentpool_storage/zed_provider.py +803 -0
  222. agentpool_toolsets/__init__.py +0 -2
  223. agentpool_toolsets/builtin/__init__.py +2 -4
  224. agentpool_toolsets/builtin/code.py +4 -4
  225. agentpool_toolsets/builtin/debug.py +115 -40
  226. agentpool_toolsets/builtin/execution_environment.py +54 -165
  227. agentpool_toolsets/builtin/skills.py +0 -77
  228. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  229. agentpool_toolsets/builtin/workers.py +4 -2
  230. agentpool_toolsets/composio_toolset.py +2 -2
  231. agentpool_toolsets/entry_points.py +3 -1
  232. agentpool_toolsets/fsspec_toolset/grep.py +25 -5
  233. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  234. agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
  235. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  236. agentpool_toolsets/mcp_discovery/toolset.py +74 -17
  237. agentpool_toolsets/mcp_run_toolset.py +8 -11
  238. agentpool_toolsets/notifications.py +33 -33
  239. agentpool_toolsets/openapi.py +3 -1
  240. agentpool_toolsets/search_toolset.py +3 -1
  241. agentpool_config/resources.py +0 -33
  242. agentpool_server/acp_server/acp_tools.py +0 -43
  243. agentpool_server/acp_server/commands/spawn.py +0 -210
  244. agentpool_storage/opencode_provider.py +0 -730
  245. agentpool_storage/text_log_provider.py +0 -276
  246. agentpool_toolsets/builtin/chain.py +0 -288
  247. agentpool_toolsets/builtin/user_interaction.py +0 -52
  248. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  249. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  250. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,183 @@
1
+ """Download file tool implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ import time
8
+ from typing import TYPE_CHECKING, Any
9
+ from urllib.parse import urlparse
10
+
11
+ import anyio
12
+
13
+ from agentpool.agents.context import AgentContext # noqa: TC001
14
+ from agentpool.log import get_logger
15
+ from agentpool.tools.base import Tool
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 DownloadFileTool(Tool[dict[str, Any]]):
30
+ """Download files from URLs to the filesystem.
31
+
32
+ A standalone tool for downloading files with:
33
+ - HTTP/HTTPS URL downloads
34
+ - Progress tracking
35
+ - Configurable chunk size
36
+ - Speed monitoring
37
+ - Automatic directory creation
38
+
39
+ Use create_download_file_tool() factory for convenient instantiation.
40
+ """
41
+
42
+ # Tool-specific configuration
43
+ env: ExecutionEnvironment | None = None
44
+ """Execution environment to use. Falls back to agent.env if not set."""
45
+
46
+ cwd: str | None = None
47
+ """Working directory for resolving relative paths."""
48
+
49
+ chunk_size: int = 8192
50
+ """Size of chunks to download (bytes)."""
51
+
52
+ timeout: float = 30.0
53
+ """Request timeout in seconds."""
54
+
55
+ def get_callable(self) -> Callable[..., Awaitable[dict[str, Any]]]:
56
+ """Return the download_file method as the callable."""
57
+ return self._download_file
58
+
59
+ def _get_fs(self, ctx: AgentContext) -> AsyncFileSystem:
60
+ """Get filesystem from env, falling back to agent's env if not set."""
61
+ from fsspec.asyn import AsyncFileSystem
62
+ from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
63
+
64
+ if self.env is not None:
65
+ fs = self.env.get_fs()
66
+ return fs if isinstance(fs, AsyncFileSystem) else AsyncFileSystemWrapper(fs)
67
+ fs = ctx.agent.env.get_fs()
68
+ return fs if isinstance(fs, AsyncFileSystem) else AsyncFileSystemWrapper(fs)
69
+
70
+ def _resolve_path(self, path: str, ctx: AgentContext) -> str:
71
+ """Resolve a potentially relative path to an absolute path."""
72
+ cwd: str | None = None
73
+ if self.cwd:
74
+ cwd = self.cwd
75
+ elif self.env and self.env.cwd:
76
+ cwd = self.env.cwd
77
+ elif ctx.agent.env and ctx.agent.env.cwd:
78
+ cwd = ctx.agent.env.cwd
79
+
80
+ if cwd and not (path.startswith("/") or (len(path) > 1 and path[1] == ":")):
81
+ return str(Path(cwd) / path)
82
+ return path
83
+
84
+ async def _write(self, ctx: AgentContext, path: str, content: bytes) -> None:
85
+ """Write bytes to a file."""
86
+ await self._get_fs(ctx)._pipe_file(path, content)
87
+
88
+ async def _download_file(
89
+ self,
90
+ ctx: AgentContext,
91
+ url: str,
92
+ target_dir: str = "downloads",
93
+ chunk_size: int | None = None,
94
+ ) -> dict[str, Any]:
95
+ """Download a file from URL to the filesystem.
96
+
97
+ Args:
98
+ ctx: Agent context for event emission and filesystem access
99
+ url: URL to download from
100
+ target_dir: Directory to save the file (relative to cwd if set)
101
+ chunk_size: Size of chunks to download (overrides default)
102
+
103
+ Returns:
104
+ Status information about the download
105
+ """
106
+ import httpx
107
+
108
+ effective_chunk_size = chunk_size or self.chunk_size
109
+ start_time = time.time()
110
+
111
+ # Resolve target directory
112
+ target_dir = self._resolve_path(target_dir, ctx)
113
+
114
+ msg = f"Downloading: {url}"
115
+ await ctx.events.tool_call_start(title=msg, kind="read", locations=[url])
116
+
117
+ # Extract filename from URL
118
+ filename = Path(urlparse(url).path).name or "downloaded_file"
119
+ full_path = f"{target_dir.rstrip('/')}/{filename}"
120
+
121
+ try:
122
+ fs = self._get_fs(ctx)
123
+ # Ensure target directory exists
124
+ await fs._makedirs(target_dir, exist_ok=True)
125
+
126
+ async with (
127
+ httpx.AsyncClient(verify=False) as client,
128
+ client.stream("GET", url, timeout=self.timeout) as response,
129
+ ):
130
+ response.raise_for_status()
131
+
132
+ total = (
133
+ int(response.headers["Content-Length"])
134
+ if "Content-Length" in response.headers
135
+ else None
136
+ )
137
+
138
+ # Collect all data
139
+ data = bytearray()
140
+ async for chunk in response.aiter_bytes(effective_chunk_size):
141
+ data.extend(chunk)
142
+ size = len(data)
143
+
144
+ if total and (size % (effective_chunk_size * 100) == 0 or size == total):
145
+ progress = size / total * 100
146
+ speed_mbps = (size / 1_048_576) / (time.time() - start_time)
147
+ progress_msg = f"\r{filename}: {progress:.1f}% ({speed_mbps:.1f} MB/s)"
148
+ await ctx.events.progress(progress, 100, progress_msg)
149
+ await anyio.sleep(0)
150
+
151
+ # Write to filesystem
152
+ await self._write(ctx, full_path, bytes(data))
153
+
154
+ duration = time.time() - start_time
155
+ size_mb = len(data) / 1_048_576
156
+
157
+ await ctx.events.file_operation("read", path=full_path, success=True)
158
+
159
+ return {
160
+ "path": full_path,
161
+ "filename": filename,
162
+ "size_bytes": len(data),
163
+ "size_mb": round(size_mb, 2),
164
+ "duration_seconds": round(duration, 2),
165
+ "speed_mbps": round(size_mb / duration, 2) if duration > 0 else 0,
166
+ }
167
+
168
+ except httpx.ConnectError as e:
169
+ error_msg = f"Connection error downloading {url}: {e}"
170
+ await ctx.events.file_operation("read", path=url, success=False, error=error_msg)
171
+ return {"error": error_msg}
172
+ except httpx.TimeoutException:
173
+ error_msg = f"Timeout downloading {url}"
174
+ await ctx.events.file_operation("read", path=url, success=False, error=error_msg)
175
+ return {"error": error_msg}
176
+ except httpx.HTTPStatusError as e:
177
+ error_msg = f"HTTP error {e.response.status_code} downloading {url}"
178
+ await ctx.events.file_operation("read", path=url, success=False, error=error_msg)
179
+ return {"error": error_msg}
180
+ except Exception as e: # noqa: BLE001
181
+ error_msg = f"Error downloading {url}: {e!s}"
182
+ await ctx.events.file_operation("read", path=url, success=False, error=error_msg)
183
+ return {"error": error_msg}
@@ -0,0 +1,55 @@
1
+ """Python code execution tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+ from agentpool.tool_impls.execute_code.tool import ExecuteCodeTool
8
+ from agentpool_config.tools import ToolHints
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from exxec import ExecutionEnvironment
13
+
14
+ __all__ = ["ExecuteCodeTool", "create_execute_code_tool"]
15
+
16
+ # Tool metadata defaults
17
+ NAME = "execute_code"
18
+ DESCRIPTION = "Execute Python code and return the result."
19
+ CATEGORY: Literal["execute"] = "execute"
20
+ HINTS = ToolHints()
21
+
22
+
23
+ def create_execute_code_tool(
24
+ *,
25
+ env: ExecutionEnvironment | None = None,
26
+ name: str = NAME,
27
+ description: str = DESCRIPTION,
28
+ requires_confirmation: bool = False,
29
+ ) -> ExecuteCodeTool:
30
+ """Create a configured ExecuteCodeTool instance.
31
+
32
+ Args:
33
+ env: Execution environment to use. Falls back to agent.env if not set.
34
+ name: Tool name override.
35
+ description: Tool description override.
36
+ requires_confirmation: Whether tool execution needs confirmation.
37
+
38
+ Returns:
39
+ Configured ExecuteCodeTool instance.
40
+
41
+ Example:
42
+ # Basic
43
+ exec_code = create_execute_code_tool()
44
+
45
+ # Require confirmation
46
+ exec_code = create_execute_code_tool(requires_confirmation=True)
47
+ """
48
+ return ExecuteCodeTool(
49
+ name=name,
50
+ description=description,
51
+ category=CATEGORY,
52
+ hints=HINTS,
53
+ env=env,
54
+ requires_confirmation=requires_confirmation,
55
+ )
@@ -0,0 +1,163 @@
1
+ """Python code execution tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import UTC, datetime
7
+ import json
8
+ from typing import TYPE_CHECKING
9
+ import uuid
10
+
11
+ from exxec.events import (
12
+ OutputEvent,
13
+ ProcessCompletedEvent,
14
+ ProcessErrorEvent,
15
+ ProcessStartedEvent,
16
+ )
17
+
18
+ from agentpool.agents.context import AgentContext # noqa: TC001
19
+ from agentpool.log import get_logger
20
+ from agentpool.tools.base import Tool
21
+
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Awaitable, Callable
25
+
26
+ from exxec import ExecutionEnvironment
27
+ from fsspec.asyn import AsyncFileSystem
28
+
29
+
30
+ logger = get_logger(__name__)
31
+
32
+
33
+ @dataclass
34
+ class ExecuteCodeTool(Tool[str]):
35
+ """Execute Python code and return the result.
36
+
37
+ A standalone tool for executing Python code in a sandboxed environment.
38
+
39
+ Use create_execute_code_tool() factory for convenient instantiation with defaults.
40
+ """
41
+
42
+ # Tool-specific configuration
43
+ env: ExecutionEnvironment | None = None
44
+ """Execution environment to use. Falls back to agent.env if not set."""
45
+
46
+ def __post_init__(self) -> None:
47
+ """Initialize filesystem for script history after dataclass init."""
48
+ from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
49
+ from fsspec.implementations.memory import MemoryFileSystem
50
+
51
+ self._memory_fs = MemoryFileSystem()
52
+ self._fs = AsyncFileSystemWrapper(self._memory_fs)
53
+
54
+ def get_callable(self) -> Callable[..., Awaitable[str]]:
55
+ """Return the execute method as the callable."""
56
+ return self._execute
57
+
58
+ def _get_env(self, ctx: AgentContext) -> ExecutionEnvironment:
59
+ """Get execution environment, falling back to agent's env."""
60
+ if self.env is not None:
61
+ return self.env
62
+ return ctx.agent.env
63
+
64
+ def get_fs(self) -> AsyncFileSystem:
65
+ """Get filesystem view of script history.
66
+
67
+ Returns:
68
+ AsyncFileSystem containing:
69
+ - scripts/{timestamp}_{title}.py - Executed scripts
70
+ - scripts/{timestamp}_{title}.json - Execution metadata
71
+ """
72
+ return self._fs
73
+
74
+ async def _execute(
75
+ self,
76
+ ctx: AgentContext,
77
+ code: str,
78
+ title: str,
79
+ ) -> str:
80
+ """Execute Python code and return the result.
81
+
82
+ Args:
83
+ ctx: Agent context for event emission and environment access
84
+ code: Python code to execute
85
+ title: Short descriptive title for this script (3-4 words)
86
+ """
87
+ process_id: str | None = None
88
+ output_parts: list[str] = []
89
+ exit_code: int | None = None
90
+ error_msg: str | None = None
91
+
92
+ # Check if we're running in ACP - terminal streams client-side
93
+ from exxec.acp_provider import ACPExecutionEnvironment
94
+
95
+ env = self._get_env(ctx)
96
+ is_acp = isinstance(env, ACPExecutionEnvironment)
97
+
98
+ try:
99
+ async for event in env.stream_code(code):
100
+ match event:
101
+ case ProcessStartedEvent(process_id=pid, command=cmd):
102
+ process_id = pid
103
+ await ctx.events.process_started(pid, cmd, success=True)
104
+
105
+ case OutputEvent(data=data):
106
+ output_parts.append(data)
107
+ # Skip progress events for ACP - terminal streams client-side
108
+ if process_id and not is_acp:
109
+ await ctx.events.process_output(process_id, data)
110
+
111
+ case ProcessCompletedEvent(exit_code=code_):
112
+ exit_code = code_
113
+ # Skip exit event for ACP - completion handled by FunctionToolResultEvent
114
+ if process_id and not is_acp:
115
+ out = "".join(output_parts)
116
+ await ctx.events.process_exit(process_id, exit_code, final_output=out)
117
+
118
+ case ProcessErrorEvent(error=err, exit_code=code_):
119
+ error_msg = err
120
+ exit_code = code_
121
+ # Skip exit event for ACP - completion handled by FunctionToolResultEvent
122
+ if process_id and not is_acp:
123
+ await ctx.events.process_exit(
124
+ process_id, exit_code or 1, final_output=err
125
+ )
126
+
127
+ combined_output = "".join(output_parts)
128
+
129
+ # Format error response
130
+ if error_msg:
131
+ result_str = f"{combined_output}\n\nError: {error_msg}\nExit code: {exit_code}"
132
+ elif exit_code and exit_code != 0:
133
+ result_str = f"{combined_output}\n\nExit code: {exit_code}"
134
+ else:
135
+ result_str = combined_output
136
+
137
+ except Exception as e: # noqa: BLE001
138
+ error_id = process_id or f"code_{uuid.uuid4().hex[:8]}"
139
+ await ctx.events.process_started(error_id, "execute_code", success=False, error=str(e))
140
+ exit_code = 1
141
+ error_msg = str(e)
142
+ result_str = f"Error executing code: {e}"
143
+ finally:
144
+ # Save to filesystem
145
+ end_time = datetime.now(UTC)
146
+ timestamp = end_time.strftime("%Y%m%d_%H%M%S")
147
+
148
+ # Write script file
149
+ script_path = f"scripts/{timestamp}_{title}.py"
150
+ self._memory_fs.pipe(script_path, code.encode("utf-8"))
151
+
152
+ # Write metadata file
153
+ metadata = {
154
+ "title": title,
155
+ "timestamp": end_time.isoformat(),
156
+ "exit_code": exit_code or 0,
157
+ "result": result_str,
158
+ "error": error_msg,
159
+ }
160
+ metadata_path = f"scripts/{timestamp}_{title}.json"
161
+ self._memory_fs.pipe(metadata_path, json.dumps(metadata, indent=2).encode("utf-8"))
162
+
163
+ return result_str
@@ -0,0 +1,80 @@
1
+ """Grep search tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+ from agentpool.tool_impls.grep.tool import GrepTool
8
+ from agentpool_config.tools import ToolHints
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from exxec import ExecutionEnvironment
13
+
14
+ __all__ = ["GrepTool", "create_grep_tool"]
15
+
16
+ # Tool metadata defaults
17
+ NAME = "grep"
18
+ DESCRIPTION = """Search file contents for patterns using grep.
19
+
20
+ Supports:
21
+ - Regex pattern matching
22
+ - File filtering with glob patterns
23
+ - Context lines before/after matches
24
+ - Fast subprocess grep (ripgrep/grep) with Python fallback
25
+ - Case-sensitive and case-insensitive search"""
26
+ CATEGORY: Literal["search"] = "search"
27
+ HINTS = ToolHints(read_only=True, idempotent=True)
28
+
29
+
30
+ def create_grep_tool(
31
+ *,
32
+ env: ExecutionEnvironment | None = None,
33
+ cwd: str | None = None,
34
+ max_output_kb: int = 64,
35
+ use_subprocess_grep: bool = True,
36
+ name: str = NAME,
37
+ description: str = DESCRIPTION,
38
+ requires_confirmation: bool = False,
39
+ ) -> GrepTool:
40
+ """Create a configured GrepTool instance.
41
+
42
+ Args:
43
+ env: Execution environment to use. Falls back to agent.env if not set.
44
+ cwd: Working directory for resolving relative paths.
45
+ max_output_kb: Maximum output size in KB (default: 64KB).
46
+ use_subprocess_grep: Use ripgrep/grep subprocess if available (default: True).
47
+ name: Tool name override.
48
+ description: Tool description override.
49
+ requires_confirmation: Whether tool execution needs confirmation.
50
+
51
+ Returns:
52
+ Configured GrepTool instance.
53
+
54
+ Example:
55
+ # Basic usage
56
+ grep = create_grep_tool()
57
+
58
+ # With custom limits
59
+ grep = create_grep_tool(
60
+ max_output_kb=128,
61
+ use_subprocess_grep=False, # Force Python implementation
62
+ )
63
+
64
+ # With specific environment
65
+ grep = create_grep_tool(
66
+ env=my_env,
67
+ cwd="/workspace",
68
+ )
69
+ """
70
+ return GrepTool(
71
+ name=name,
72
+ description=description,
73
+ category=CATEGORY,
74
+ hints=HINTS,
75
+ env=env,
76
+ cwd=cwd,
77
+ max_output_kb=max_output_kb,
78
+ use_subprocess_grep=use_subprocess_grep,
79
+ requires_confirmation=requires_confirmation,
80
+ )
@@ -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
+ )