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,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:
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
20
20
  from agentpool.prompts.prompts import MCPClientPrompt
21
21
  from agentpool.resource_providers import ResourceProvider
22
22
  from agentpool.resource_providers.codemode.provider import CodeModeResourceProvider
23
+ from agentpool.resource_providers.resource_info import ResourceInfo
23
24
 
24
25
 
25
26
  logger = get_logger(__name__)
@@ -186,6 +187,8 @@ class ToolManager:
186
187
  async def list_prompts(self) -> list[MCPClientPrompt]:
187
188
  """Get all prompts from all providers."""
188
189
  from agentpool.mcp_server.manager import MCPManager
190
+ from agentpool.prompts.prompts import MCPClientPrompt as MCPPrompt
191
+ from agentpool.resource_providers import AggregatingResourceProvider
189
192
 
190
193
  all_prompts: list[MCPClientPrompt] = []
191
194
  # Get prompts from all external providers (check if they're MCP providers)
@@ -195,12 +198,65 @@ class ToolManager:
195
198
  # Get prompts from MCP providers via the aggregating provider
196
199
  agg_provider = provider.get_aggregating_provider()
197
200
  prompts = await agg_provider.get_prompts()
198
- all_prompts.extend(prompts)
201
+ # Filter to only MCPClientPrompt instances
202
+ mcp_prompts = [p for p in prompts if isinstance(p, MCPPrompt)]
203
+ all_prompts.extend(mcp_prompts)
204
+ except Exception:
205
+ logger.exception("Failed to get prompts from provider", provider=provider)
206
+ elif isinstance(provider, AggregatingResourceProvider):
207
+ try:
208
+ # AggregatingResourceProvider can directly provide prompts
209
+ prompts = await provider.get_prompts()
210
+ # Filter to only MCPClientPrompt instances
211
+ mcp_prompts = [p for p in prompts if isinstance(p, MCPPrompt)]
212
+ all_prompts.extend(mcp_prompts)
199
213
  except Exception:
200
214
  logger.exception("Failed to get prompts from provider", provider=provider)
201
215
 
202
216
  return all_prompts
203
217
 
218
+ async def list_resources(self) -> list[ResourceInfo]:
219
+ """Get all resources from all providers.
220
+
221
+ Returns:
222
+ List of ResourceInfo objects from all providers
223
+ """
224
+ all_resources: list[ResourceInfo] = []
225
+ # Get resources from all providers concurrently
226
+ provider_coroutines = [provider.get_resources() for provider in self.providers]
227
+ results = await asyncio.gather(*provider_coroutines, return_exceptions=True)
228
+
229
+ for provider, result in zip(self.providers, results, strict=False):
230
+ if isinstance(result, BaseException):
231
+ logger.warning(
232
+ "Failed to get resources from provider",
233
+ provider=provider.name,
234
+ error=str(result),
235
+ )
236
+ continue
237
+ all_resources.extend(result)
238
+
239
+ return all_resources
240
+
241
+ async def get_resource(self, name: str) -> ResourceInfo:
242
+ """Get a specific resource by name.
243
+
244
+ Args:
245
+ name: Name of the resource to find
246
+
247
+ Returns:
248
+ ResourceInfo for the requested resource
249
+
250
+ Raises:
251
+ ToolError: If resource not found
252
+ """
253
+ resources = await self.list_resources()
254
+ resource: ResourceInfo | None = next((r for r in resources if r.name == name), None)
255
+ if not resource:
256
+ msg = f"Resource not found: {name}"
257
+ raise ToolError(msg)
258
+ return resource
259
+
204
260
  @asynccontextmanager
205
261
  async def temporary_tools(
206
262
  self,
agentpool/ui/base.py CHANGED
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
12
12
  from mcp import types
13
13
  from pydantic import BaseModel
14
14
 
15
- from agentpool.agents.context import ConfirmationResult
15
+ from agentpool.agents.context import AgentContext, ConfirmationResult
16
16
  from agentpool.messaging import ChatMessage
17
17
  from agentpool.messaging.context import NodeContext
18
18
  from agentpool.tools.base import Tool
@@ -62,7 +62,7 @@ class InputProvider(ABC):
62
62
  @abstractmethod
63
63
  def get_tool_confirmation(
64
64
  self,
65
- context: NodeContext[Any],
65
+ context: AgentContext[Any],
66
66
  tool: Tool,
67
67
  args: dict[str, Any],
68
68
  message_history: list[ChatMessage[Any]] | None = None,
@@ -11,7 +11,7 @@ from agentpool.ui.base import InputProvider
11
11
  if TYPE_CHECKING:
12
12
  from mcp import types
13
13
 
14
- from agentpool.agents.context import ConfirmationResult
14
+ from agentpool.agents.context import AgentContext, ConfirmationResult
15
15
  from agentpool.messaging import ChatMessage
16
16
  from agentpool.messaging.context import NodeContext
17
17
  from agentpool.tools.base import Tool
@@ -58,7 +58,7 @@ class MockInputProvider(InputProvider):
58
58
 
59
59
  async def get_tool_confirmation(
60
60
  self,
61
- context: NodeContext,
61
+ context: AgentContext[Any],
62
62
  tool: Tool,
63
63
  args: dict[str, Any],
64
64
  message_history: list[ChatMessage[Any]] | None = None,