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,104 @@
1
+ """Read file tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+ from agentpool.tool_impls.read.tool import ReadTool
8
+ from agentpool_config.tools import ToolHints
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from exxec import ExecutionEnvironment
13
+
14
+ from agentpool.prompts.conversion_manager import ConversionManager
15
+
16
+ __all__ = ["ReadTool", "create_read_tool"]
17
+
18
+ # Tool metadata defaults
19
+ NAME = "read"
20
+ DESCRIPTION = """Read the content of a text file, or use vision capabilities
21
+ to read images or documents.
22
+
23
+ Supports:
24
+ - Text files with optional line-based partial reads
25
+ - Binary files (images, PDFs, audio, video) returned as BinaryContent
26
+ - Automatic image resizing for better model compatibility
27
+ - Structure maps for large code files
28
+ - Multiple text encodings"""
29
+ CATEGORY: Literal["read"] = "read"
30
+ HINTS = ToolHints(read_only=True, idempotent=True)
31
+
32
+
33
+ def create_read_tool(
34
+ *,
35
+ env: ExecutionEnvironment | None = None,
36
+ converter: ConversionManager | None = None,
37
+ cwd: str | None = None,
38
+ max_file_size_kb: int = 64,
39
+ max_image_size: int | None = 2000,
40
+ max_image_bytes: int | None = None,
41
+ large_file_tokens: int = 12_000,
42
+ map_max_tokens: int = 2048,
43
+ name: str = NAME,
44
+ description: str = DESCRIPTION,
45
+ requires_confirmation: bool = False,
46
+ ) -> ReadTool:
47
+ """Create a configured ReadTool instance.
48
+
49
+ Args:
50
+ env: Execution environment to use. Falls back to agent.env if not set.
51
+ converter: Optional converter for binary files. If set, converts supported
52
+ file types to markdown instead of returning BinaryContent.
53
+ cwd: Working directory for resolving relative paths.
54
+ max_file_size_kb: Maximum file size in KB for read operations (default: 64KB).
55
+ max_image_size: Max width/height for images in pixels. Larger images are
56
+ auto-resized. Set to None to disable.
57
+ max_image_bytes: Max file size for images in bytes. Images exceeding this
58
+ are compressed. Default: None (uses 4.5MB).
59
+ large_file_tokens: Token threshold for switching to structure map (default: 12000).
60
+ map_max_tokens: Maximum tokens for structure map output (default: 2048).
61
+ name: Tool name override.
62
+ description: Tool description override.
63
+ requires_confirmation: Whether tool execution needs confirmation.
64
+
65
+ Returns:
66
+ Configured ReadTool instance.
67
+
68
+ Example:
69
+ # Basic usage
70
+ read = create_read_tool()
71
+
72
+ # With custom limits
73
+ read = create_read_tool(
74
+ max_file_size_kb=128,
75
+ max_image_size=1500,
76
+ )
77
+
78
+ # With specific environment and cwd
79
+ read = create_read_tool(
80
+ env=my_env,
81
+ cwd="/workspace/project",
82
+ )
83
+
84
+ # With converter for automatic markdown conversion
85
+ from agentpool.prompts.conversion_manager import ConversionManager
86
+ read = create_read_tool(
87
+ converter=ConversionManager(),
88
+ )
89
+ """
90
+ return ReadTool(
91
+ name=name,
92
+ description=description,
93
+ category=CATEGORY,
94
+ hints=HINTS,
95
+ env=env,
96
+ converter=converter,
97
+ cwd=cwd,
98
+ max_file_size_kb=max_file_size_kb,
99
+ max_image_size=max_image_size,
100
+ max_image_bytes=max_image_bytes,
101
+ large_file_tokens=large_file_tokens,
102
+ map_max_tokens=map_max_tokens,
103
+ requires_confirmation=requires_confirmation,
104
+ )
@@ -0,0 +1,305 @@
1
+ """Read file tool implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from pydantic_ai import BinaryContent
10
+
11
+ from agentpool.agents.context import AgentContext # noqa: TC001
12
+ from agentpool.log import get_logger
13
+ from agentpool.mime_utils import guess_type, is_binary_content, is_binary_mime
14
+ from agentpool.tools.base import Tool, ToolResult
15
+
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Awaitable, Callable
19
+
20
+ from exxec import ExecutionEnvironment
21
+ from fsspec.asyn import AsyncFileSystem
22
+
23
+ from agentpool.prompts.conversion_manager import ConversionManager
24
+
25
+
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ @dataclass
30
+ class ReadTool(Tool[ToolResult]):
31
+ """Read files from the filesystem with support for text, binary, and image content.
32
+
33
+ A standalone tool for reading files with advanced features:
34
+ - Automatic binary/text detection
35
+ - Image resizing and compression
36
+ - Large file handling with structure maps
37
+ - Line-based partial reads
38
+ - Multi-encoding support
39
+
40
+ Use create_read_tool() factory for convenient instantiation with defaults.
41
+ """
42
+
43
+ # Tool-specific configuration
44
+ env: ExecutionEnvironment | None = None
45
+ """Execution environment to use. Falls back to agent.env if not set."""
46
+
47
+ converter: ConversionManager | None = None
48
+ """Optional converter for binary files. If set and supports the file type, returns markdown."""
49
+
50
+ cwd: str | None = None
51
+ """Working directory for resolving relative paths."""
52
+
53
+ max_file_size_kb: int = 64
54
+ """Maximum file size in KB for read operations."""
55
+
56
+ max_image_size: int | None = 2000
57
+ """Max width/height for images in pixels. Images are auto-resized if larger."""
58
+
59
+ max_image_bytes: int | None = None
60
+ """Max file size for images in bytes. Images are compressed if larger."""
61
+
62
+ large_file_tokens: int = 12_000
63
+ """Token threshold for switching to structure map for large files."""
64
+
65
+ map_max_tokens: int = 2048
66
+ """Maximum tokens for structure map output."""
67
+
68
+ _repomap = None # RepoMap instance (lazy-init)
69
+
70
+ def get_callable(
71
+ self,
72
+ ) -> Callable[..., Awaitable[ToolResult]]:
73
+ """Return the read method as the callable."""
74
+ return self._read
75
+
76
+ def _get_fs(self, ctx: AgentContext) -> AsyncFileSystem:
77
+ """Get filesystem from env, falling back to agent's env if not set."""
78
+ from fsspec.asyn import AsyncFileSystem
79
+ from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
80
+
81
+ # Priority: env.get_fs() > agent.env.get_fs()
82
+ if self.env is not None:
83
+ fs = self.env.get_fs()
84
+ return fs if isinstance(fs, AsyncFileSystem) else AsyncFileSystemWrapper(fs)
85
+ fs = ctx.agent.env.get_fs()
86
+ return fs if isinstance(fs, AsyncFileSystem) else AsyncFileSystemWrapper(fs)
87
+
88
+ def _resolve_path(self, path: str, ctx: AgentContext) -> str:
89
+ """Resolve a potentially relative path to an absolute path."""
90
+ # Get cwd: explicit toolset cwd > env.cwd > agent.env.cwd
91
+ cwd: str | None = None
92
+ if self.cwd:
93
+ cwd = self.cwd
94
+ elif self.env and self.env.cwd:
95
+ cwd = self.env.cwd
96
+ elif ctx.agent.env and ctx.agent.env.cwd:
97
+ cwd = ctx.agent.env.cwd
98
+
99
+ if cwd and not (path.startswith("/") or (len(path) > 1 and path[1] == ":")):
100
+ return str(Path(cwd) / path)
101
+ return path
102
+
103
+ async def _get_file_map(self, path: str, ctx: AgentContext) -> str | None:
104
+ """Get structure map for a large file if language is supported."""
105
+ from agentpool.repomap import RepoMap, is_language_supported
106
+
107
+ if not is_language_supported(path):
108
+ return None
109
+
110
+ # Lazy init repomap - use file's directory as root
111
+ if self._repomap is None:
112
+ root = str(Path(path).parent)
113
+ fs = self._get_fs(ctx)
114
+ self._repomap = RepoMap(fs, root, max_tokens=self.map_max_tokens)
115
+
116
+ return await self._repomap.get_file_map(path, max_tokens=self.map_max_tokens)
117
+
118
+ async def _read( # noqa: PLR0911, PLR0915
119
+ self,
120
+ ctx: AgentContext,
121
+ path: str,
122
+ encoding: str = "utf-8",
123
+ line: int | None = None,
124
+ limit: int | None = None,
125
+ ) -> ToolResult:
126
+ """Read the content of a text file, or use vision capabilities to read images or documents.
127
+
128
+ Args:
129
+ ctx: Agent context for event emission and filesystem access
130
+ path: File path to read
131
+ encoding: Text encoding to use for text files (default: utf-8)
132
+ line: Optional line number to start reading from (1-based, text files only)
133
+ limit: Optional maximum number of lines to read (text files only)
134
+
135
+ Returns:
136
+ Text content for text files, BinaryContent for binary files (with optional
137
+ dimension note as list when image was resized), or error string
138
+ """
139
+ path = self._resolve_path(path, ctx)
140
+ msg = f"Reading file: {path}"
141
+ from agentpool.agents.events import LocationContentItem
142
+
143
+ # Emit progress - use 0 for line if negative (can't resolve until we read file)
144
+ # LocationContentItem/ToolCallLocation require line >= 0 per ACP spec
145
+ display_line = line if (line is not None and line > 0) else 0
146
+ await ctx.events.tool_call_progress(
147
+ title=msg,
148
+ items=[LocationContentItem(path=path, line=display_line)],
149
+ )
150
+
151
+ max_file_size = self.max_file_size_kb * 1024
152
+
153
+ try:
154
+ mime_type = guess_type(path)
155
+ # Fast path: known binary MIME types (images, audio, video, etc.)
156
+ if is_binary_mime(mime_type):
157
+ # Try converter first if available
158
+ if self.converter is not None:
159
+ try:
160
+ content = await self.converter.convert_file(path)
161
+ await ctx.events.file_operation("read", path=path, success=True)
162
+ except Exception: # noqa: BLE001
163
+ # Converter doesn't support this file type, fall back to binary
164
+ pass
165
+ else:
166
+ # Converter returned markdown
167
+ lines = content.splitlines()
168
+ preview = "\n".join(lines[:20])
169
+ return ToolResult(
170
+ content=content,
171
+ metadata={"preview": preview, "truncated": False},
172
+ )
173
+
174
+ # Fall back to native binary handling
175
+ data = await self._get_fs(ctx)._cat_file(path)
176
+ await ctx.events.file_operation("read", path=path, success=True)
177
+ mime = mime_type or "application/octet-stream"
178
+ # Resize images if needed
179
+ if self.max_image_size and mime.startswith("image/"):
180
+ from agentpool_toolsets.fsspec_toolset.image_utils import (
181
+ resize_image_if_needed,
182
+ )
183
+
184
+ data, mime, note = resize_image_if_needed(
185
+ data, mime, self.max_image_size, self.max_image_bytes
186
+ )
187
+ if note:
188
+ # Return resized image with dimension note for coordinate mapping
189
+ return ToolResult(
190
+ content=[
191
+ note,
192
+ BinaryContent(data=data, media_type=mime, identifier=path),
193
+ ],
194
+ metadata={"preview": "Image read successfully", "truncated": False},
195
+ )
196
+ return ToolResult(
197
+ content=[BinaryContent(data=data, media_type=mime, identifier=path)],
198
+ metadata={"preview": "Binary file read successfully", "truncated": False},
199
+ )
200
+
201
+ # Read content and probe for binary (git-style null byte detection)
202
+ data = await self._get_fs(ctx)._cat_file(path)
203
+ if is_binary_content(data):
204
+ # Try converter first if available
205
+ if self.converter is not None:
206
+ try:
207
+ content = await self.converter.convert_file(path)
208
+ await ctx.events.file_operation("read", path=path, success=True)
209
+ except Exception: # noqa: BLE001
210
+ # Converter doesn't support this file type, fall back to binary
211
+ pass
212
+ else:
213
+ # Converter returned markdown
214
+ lines = content.splitlines()
215
+ preview = "\n".join(lines[:20])
216
+ return ToolResult(
217
+ content=content,
218
+ metadata={"preview": preview, "truncated": False},
219
+ )
220
+
221
+ # Fall back to native binary handling
222
+ await ctx.events.file_operation("read", path=path, success=True)
223
+ mime = mime_type or "application/octet-stream"
224
+ # Resize images if needed
225
+ if self.max_image_size and mime.startswith("image/"):
226
+ from agentpool_toolsets.fsspec_toolset.image_utils import (
227
+ resize_image_if_needed,
228
+ )
229
+
230
+ data, mime, note = resize_image_if_needed(
231
+ data, mime, self.max_image_size, self.max_image_bytes
232
+ )
233
+ if note:
234
+ return ToolResult(
235
+ content=[
236
+ note,
237
+ BinaryContent(data=data, media_type=mime, identifier=path),
238
+ ],
239
+ metadata={"preview": "Image read successfully", "truncated": False},
240
+ )
241
+ return ToolResult(
242
+ content=[BinaryContent(data=data, media_type=mime, identifier=path)],
243
+ metadata={"preview": "Binary file read successfully", "truncated": False},
244
+ )
245
+
246
+ content = data.decode(encoding)
247
+
248
+ # Check if file is too large and no targeted read requested
249
+ tokens_approx = len(content) // 4
250
+ if line is None and limit is None and tokens_approx > self.large_file_tokens:
251
+ # Try structure map for supported languages
252
+ map_result = await self._get_file_map(path, ctx)
253
+ if map_result:
254
+ await ctx.events.file_operation("read", path=path, success=True)
255
+ content = map_result
256
+ else:
257
+ # Fallback: head + tail for unsupported languages
258
+ from agentpool.repomap import truncate_with_notice
259
+
260
+ content = truncate_with_notice(path, content)
261
+ await ctx.events.file_operation("read", path=path, success=True)
262
+ else:
263
+ # Normal read with optional offset/limit
264
+ from agentpool_toolsets.fsspec_toolset.helpers import truncate_lines
265
+
266
+ lines = content.splitlines()
267
+ offset = (line - 1) if line else 0
268
+ result_lines, was_truncated = truncate_lines(lines, offset, limit, max_file_size)
269
+ content = "\n".join(result_lines)
270
+ # Don't pass negative line numbers to events (ACP requires >= 0)
271
+ display_line = line if (line and line > 0) else 0
272
+ await ctx.events.file_operation("read", path=path, success=True, line=display_line)
273
+ if was_truncated:
274
+ content += f"\n\n[Content truncated at {max_file_size} bytes]"
275
+
276
+ except Exception as e: # noqa: BLE001
277
+ await ctx.events.file_operation("read", path=path, success=False, error=str(e))
278
+ error_msg = f"error: Failed to read file {path}: {e}"
279
+ return ToolResult(
280
+ content=error_msg,
281
+ metadata={"preview": "", "truncated": False},
282
+ )
283
+ else:
284
+ # Emit file content for UI display (formatted at ACP layer)
285
+ from agentpool.agents.events import FileContentItem
286
+
287
+ # Use non-negative line for display (negative lines are internal Python convention)
288
+ display_start_line = max(1, line) if line and line > 0 else None
289
+ await ctx.events.tool_call_progress(
290
+ title=f"Read: {path}",
291
+ items=[FileContentItem(content=content, path=path, start_line=display_start_line)],
292
+ replace_content=True,
293
+ )
294
+
295
+ # Prepare metadata for OpenCode UI
296
+ lines = content.splitlines()
297
+ preview = "\n".join(lines[:20])
298
+ # Check if content was truncated
299
+ was_truncated = "[Content truncated" in content
300
+
301
+ # Return result with metadata
302
+ return ToolResult(
303
+ content=content,
304
+ metadata={"preview": preview, "truncated": was_truncated},
305
+ )
@@ -2,12 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from agentpool.tools.base import Tool
5
+ from agentpool.tools.base import FunctionTool, Tool
6
6
  from agentpool.tools.manager import ToolManager, ToolError
7
7
  from agentpool.tools.tool_call_info import ToolCallInfo
8
8
  from agentpool.skills.registry import SkillsRegistry
9
9
 
10
10
  __all__ = [
11
+ "FunctionTool",
11
12
  "SkillsRegistry",
12
13
  "Tool",
13
14
  "ToolCallInfo",
agentpool/tools/base.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from abc import abstractmethod
5
6
  from dataclasses import dataclass, field
6
7
  import inspect
7
8
  from typing import TYPE_CHECKING, Any, Literal
@@ -24,12 +25,12 @@ if TYPE_CHECKING:
24
25
  from collections.abc import Awaitable, Callable
25
26
 
26
27
  from mcp.types import Tool as MCPTool
28
+ from pydantic_ai import UserContent
27
29
  from schemez import FunctionSchema, Property
28
30
 
29
31
  from agentpool.common_types import ToolSource
30
32
  from agentpool.tools.manager import ToolState
31
33
 
32
-
33
34
  logger = get_logger(__name__)
34
35
  ToolKind = Literal[
35
36
  "read",
@@ -46,11 +47,31 @@ ToolKind = Literal[
46
47
 
47
48
 
48
49
  @dataclass
49
- class Tool[TOutputType = Any]:
50
- """Information about a registered tool."""
50
+ class ToolResult:
51
+ """Structured tool result with content for LLM and metadata for UI.
52
+
53
+ This abstraction allows tools to return rich data that gets converted to
54
+ agent-specific formats (pydantic-ai ToolReturn, FastMCP ToolResult, etc.).
55
+
56
+ Attributes:
57
+ content: What the LLM sees - can be string or list of content blocks
58
+ structured_content: Machine-readable JSON data (optional)
59
+ metadata: UI/application data that is NOT sent to the LLM
60
+ """
61
+
62
+ content: str | list[UserContent]
63
+ """Content sent to the LLM (text, images, etc.)"""
51
64
 
52
- callable: Callable[..., TOutputType]
53
- """The actual tool implementation"""
65
+ structured_content: dict[str, Any] | None = None
66
+ """Structured JSON data for programmatic access (optional)"""
67
+
68
+ metadata: dict[str, Any] | None = None
69
+ """Metadata for UI/app use - NOT sent to LLM (diffs, diagnostics, etc.)."""
70
+
71
+
72
+ @dataclass
73
+ class Tool[TOutputType = Any]:
74
+ """Base class for tools. Subclass and implement get_callable() or use FunctionTool."""
54
75
 
55
76
  name: str
56
77
  """The name of the tool."""
@@ -87,14 +108,17 @@ class Tool[TOutputType = Any]:
87
108
 
88
109
  __repr__ = dataclasses_no_defaults_repr
89
110
 
111
+ @abstractmethod
112
+ def get_callable(self) -> Callable[..., TOutputType | Awaitable[TOutputType]]:
113
+ """Get the callable for this tool. Subclasses must implement."""
114
+ ...
115
+
90
116
  def to_pydantic_ai(self) -> PydanticAiTool:
91
117
  """Convert tool to Pydantic AI tool."""
92
118
  metadata = {**self.metadata, "agent_name": self.agent_name, "category": self.category}
93
119
  return PydanticAiTool(
94
- function=self.callable,
120
+ function=self.get_callable(),
95
121
  name=self.name,
96
- # takes_ctx=self.takes_ctx,
97
- # max_retries=self.max_retries,
98
122
  description=self.description,
99
123
  requires_approval=self.requires_confirmation,
100
124
  metadata=metadata,
@@ -108,7 +132,7 @@ class Tool[TOutputType = Any]:
108
132
  from agentpool.agents.context import AgentContext
109
133
 
110
134
  return schemez.create_schema(
111
- self.callable,
135
+ self.get_callable(),
112
136
  name_override=self.name,
113
137
  description_override=self.description,
114
138
  exclude_types=[AgentContext, RunContext],
@@ -165,7 +189,22 @@ class Tool[TOutputType = Any]:
165
189
  @logfire.instrument("Executing tool {self.name} with args={args}, kwargs={kwargs}")
166
190
  async def execute(self, *args: Any, **kwargs: Any) -> Any:
167
191
  """Execute tool, handling both sync and async cases."""
168
- return await execute(self.callable, *args, **kwargs, use_thread=True)
192
+ return await execute(self.get_callable(), *args, **kwargs, use_thread=True)
193
+
194
+ async def execute_and_unwrap(self, *args: Any, **kwargs: Any) -> Any:
195
+ """Execute tool and unwrap ToolResult if present.
196
+
197
+ This is a convenience method for tests and direct tool usage that want
198
+ plain content instead of ToolResult objects.
199
+
200
+ Returns:
201
+ If tool returns ToolResult, returns ToolResult.content.
202
+ Otherwise returns the raw result.
203
+ """
204
+ result = await self.execute(*args, **kwargs)
205
+ if isinstance(result, ToolResult):
206
+ return result.content
207
+ return result
169
208
 
170
209
  @classmethod
171
210
  def from_code(
@@ -173,15 +212,74 @@ class Tool[TOutputType = Any]:
173
212
  code: str,
174
213
  name: str | None = None,
175
214
  description: str | None = None,
176
- ) -> Tool[Any]:
177
- """Create a tool from a code string."""
215
+ ) -> FunctionTool[Any]:
216
+ """Create a FunctionTool from a code string."""
178
217
  namespace: dict[str, Any] = {}
179
218
  exec(code, namespace)
180
219
  func = next((v for v in namespace.values() if callable(v)), None)
181
220
  if not func:
182
221
  msg = "No callable found in provided code"
183
222
  raise ValueError(msg)
184
- return cls.from_callable(func, name_override=name, description_override=description) # pyright: ignore[reportArgumentType]
223
+ return FunctionTool.from_callable(
224
+ func, name_override=name, description_override=description
225
+ )
226
+
227
+ @classmethod
228
+ def from_callable(
229
+ cls,
230
+ fn: Callable[..., TOutputType | Awaitable[TOutputType]] | str,
231
+ *,
232
+ name_override: str | None = None,
233
+ description_override: str | None = None,
234
+ schema_override: schemez.OpenAIFunctionDefinition | None = None,
235
+ hints: ToolHints | None = None,
236
+ category: ToolKind | None = None,
237
+ enabled: bool = True,
238
+ source: ToolSource | str | None = None,
239
+ **kwargs: Any,
240
+ ) -> FunctionTool[TOutputType]:
241
+ """Create a FunctionTool from a callable or import path."""
242
+ return FunctionTool.from_callable(
243
+ fn,
244
+ name_override=name_override,
245
+ description_override=description_override,
246
+ schema_override=schema_override,
247
+ hints=hints,
248
+ category=category,
249
+ enabled=enabled,
250
+ source=source,
251
+ **kwargs,
252
+ )
253
+
254
+ def to_mcp_tool(self) -> MCPTool:
255
+ """Convert internal Tool to MCP Tool."""
256
+ schema = self.schema
257
+ from mcp.types import Tool as MCPTool, ToolAnnotations
258
+
259
+ return MCPTool(
260
+ name=schema["function"]["name"],
261
+ description=schema["function"]["description"],
262
+ inputSchema=schema["function"]["parameters"], # pyright: ignore
263
+ annotations=ToolAnnotations(
264
+ title=self.name,
265
+ readOnlyHint=self.hints.read_only if self.hints else None,
266
+ destructiveHint=self.hints.destructive if self.hints else None,
267
+ idempotentHint=self.hints.idempotent if self.hints else None,
268
+ openWorldHint=self.hints.open_world if self.hints else None,
269
+ ),
270
+ )
271
+
272
+
273
+ @dataclass
274
+ class FunctionTool[TOutputType = Any](Tool[TOutputType]):
275
+ """Tool wrapping a plain callable function."""
276
+
277
+ callable: Callable[..., TOutputType | Awaitable[TOutputType]] = field(default=lambda: None) # type: ignore[assignment]
278
+ """The actual tool implementation."""
279
+
280
+ def get_callable(self) -> Callable[..., TOutputType | Awaitable[TOutputType]]:
281
+ """Return the wrapped callable."""
282
+ return self.callable
185
283
 
186
284
  @classmethod
187
285
  def from_callable(
@@ -196,14 +294,14 @@ class Tool[TOutputType = Any]:
196
294
  enabled: bool = True,
197
295
  source: ToolSource | str | None = None,
198
296
  **kwargs: Any,
199
- ) -> Tool[TOutputType]:
297
+ ) -> FunctionTool[TOutputType]:
298
+ """Create a FunctionTool from a callable or import path string."""
200
299
  if isinstance(fn, str):
201
300
  import_path = fn
202
301
  from agentpool.utils import importing
203
302
 
204
303
  callable_obj = importing.import_callable(fn)
205
304
  name = getattr(callable_obj, "__name__", "unknown")
206
- import_path = fn
207
305
  else:
208
306
  callable_obj = fn
209
307
  module = fn.__module__
@@ -215,9 +313,9 @@ class Tool[TOutputType = Any]:
215
313
  import_path = f"{module}.{fn.__class__.__qualname__}"
216
314
 
217
315
  return cls(
218
- callable=callable_obj, # pyright: ignore[reportArgumentType]
219
316
  name=name_override or name,
220
317
  description=description_override or inspect.getdoc(callable_obj) or "",
318
+ callable=callable_obj, # pyright: ignore[reportArgumentType]
221
319
  import_path=import_path,
222
320
  schema_override=schema_override,
223
321
  category=category,
@@ -227,24 +325,6 @@ class Tool[TOutputType = Any]:
227
325
  **kwargs,
228
326
  )
229
327
 
230
- def to_mcp_tool(self) -> MCPTool:
231
- """Convert internal Tool to MCP Tool."""
232
- schema = self.schema
233
- from mcp.types import Tool as MCPTool, ToolAnnotations
234
-
235
- return MCPTool(
236
- name=schema["function"]["name"],
237
- description=schema["function"]["description"],
238
- inputSchema=schema["function"]["parameters"], # pyright: ignore
239
- annotations=ToolAnnotations(
240
- title=self.name,
241
- readOnlyHint=self.hints.read_only if self.hints else None,
242
- destructiveHint=self.hints.destructive if self.hints else None,
243
- idempotentHint=self.hints.idempotent if self.hints else None,
244
- openWorldHint=self.hints.open_world if self.hints else None,
245
- ),
246
- )
247
-
248
328
 
249
329
  @dataclass
250
330
  class ToolParameter: