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.
- acp/__init__.py +13 -4
- acp/acp_requests.py +20 -77
- acp/agent/connection.py +8 -0
- acp/agent/implementations/debug_server/debug_server.py +6 -2
- acp/agent/protocol.py +6 -0
- acp/bridge/README.md +15 -2
- acp/bridge/__init__.py +3 -2
- acp/bridge/__main__.py +60 -19
- acp/bridge/ws_server.py +173 -0
- acp/bridge/ws_server_cli.py +89 -0
- acp/client/connection.py +38 -29
- acp/client/implementations/default_client.py +3 -2
- acp/client/implementations/headless_client.py +2 -2
- acp/connection.py +2 -2
- acp/notifications.py +20 -50
- acp/schema/__init__.py +2 -0
- acp/schema/agent_responses.py +21 -0
- acp/schema/client_requests.py +3 -3
- acp/schema/session_state.py +63 -29
- acp/stdio.py +39 -9
- acp/task/supervisor.py +2 -2
- acp/transports.py +362 -2
- acp/utils.py +17 -4
- agentpool/__init__.py +6 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +407 -277
- agentpool/agents/acp_agent/acp_converters.py +196 -38
- agentpool/agents/acp_agent/client_handler.py +191 -26
- agentpool/agents/acp_agent/session_state.py +17 -6
- agentpool/agents/agent.py +607 -572
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +176 -110
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +632 -17
- agentpool/agents/claude_code_agent/FORKING.md +191 -0
- agentpool/agents/claude_code_agent/__init__.py +13 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
- agentpool/agents/claude_code_agent/converters.py +74 -143
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/claude_code_agent/models.py +77 -0
- agentpool/agents/claude_code_agent/static_info.py +100 -0
- agentpool/agents/claude_code_agent/usage.py +242 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +24 -0
- agentpool/agents/events/builtin_handlers.py +67 -1
- agentpool/agents/events/event_emitter.py +32 -2
- agentpool/agents/events/events.py +104 -3
- agentpool/agents/events/infer_info.py +145 -0
- agentpool/agents/events/processors.py +254 -0
- agentpool/agents/interactions.py +41 -6
- agentpool/agents/modes.py +67 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +56 -21
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/acp_assistant.yml +2 -2
- agentpool/config_resources/agents.yml +3 -0
- agentpool/config_resources/agents_template.yml +1 -0
- agentpool/config_resources/claude_code_agent.yml +10 -6
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +136 -289
- agentpool/delegation/team.py +58 -57
- agentpool/delegation/teamrun.py +51 -55
- agentpool/diagnostics/__init__.py +53 -0
- agentpool/diagnostics/lsp_manager.py +1593 -0
- agentpool/diagnostics/lsp_proxy.py +41 -0
- agentpool/diagnostics/lsp_proxy_script.py +229 -0
- agentpool/diagnostics/models.py +398 -0
- agentpool/functional/run.py +10 -4
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +76 -32
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +34 -54
- agentpool/mcp_server/registries/official_registry_client.py +35 -1
- agentpool/mcp_server/tool_bridge.py +186 -139
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/connection_manager.py +11 -10
- agentpool/messaging/event_manager.py +5 -5
- agentpool/messaging/message_container.py +6 -30
- agentpool/messaging/message_history.py +99 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +54 -35
- agentpool/messaging/processing.py +12 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -24
- agentpool/models/acp_agents/mcp_capable.py +126 -157
- agentpool/models/acp_agents/non_mcp.py +129 -95
- agentpool/models/agents.py +98 -76
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +144 -19
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +113 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +11 -1
- agentpool/resource_providers/aggregating.py +56 -5
- agentpool/resource_providers/base.py +70 -4
- agentpool/resource_providers/codemode/code_executor.py +72 -5
- agentpool/resource_providers/codemode/helpers.py +2 -2
- agentpool/resource_providers/codemode/provider.py +64 -12
- agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
- agentpool/resource_providers/codemode/remote_provider.py +9 -12
- agentpool/resource_providers/filtering.py +3 -1
- agentpool/resource_providers/mcp_provider.py +89 -12
- agentpool/resource_providers/plan_provider.py +228 -46
- agentpool/resource_providers/pool.py +7 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +4 -2
- agentpool/sessions/__init__.py +4 -1
- agentpool/sessions/manager.py +33 -5
- agentpool/sessions/models.py +59 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +572 -49
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +538 -20
- agentpool/tool_impls/__init__.py +6 -0
- agentpool/tool_impls/agent_cli/__init__.py +42 -0
- agentpool/tool_impls/agent_cli/tool.py +95 -0
- agentpool/tool_impls/bash/__init__.py +64 -0
- agentpool/tool_impls/bash/helpers.py +35 -0
- agentpool/tool_impls/bash/tool.py +171 -0
- agentpool/tool_impls/delete_path/__init__.py +70 -0
- agentpool/tool_impls/delete_path/tool.py +142 -0
- agentpool/tool_impls/download_file/__init__.py +80 -0
- agentpool/tool_impls/download_file/tool.py +183 -0
- agentpool/tool_impls/execute_code/__init__.py +55 -0
- agentpool/tool_impls/execute_code/tool.py +163 -0
- agentpool/tool_impls/grep/__init__.py +80 -0
- agentpool/tool_impls/grep/tool.py +200 -0
- agentpool/tool_impls/list_directory/__init__.py +73 -0
- agentpool/tool_impls/list_directory/tool.py +197 -0
- agentpool/tool_impls/question/__init__.py +42 -0
- agentpool/tool_impls/question/tool.py +127 -0
- agentpool/tool_impls/read/__init__.py +104 -0
- agentpool/tool_impls/read/tool.py +305 -0
- agentpool/tools/__init__.py +2 -1
- agentpool/tools/base.py +114 -34
- agentpool/tools/manager.py +57 -1
- agentpool/ui/base.py +2 -2
- agentpool/ui/mock_provider.py +2 -2
- agentpool/ui/stdlib_provider.py +2 -2
- agentpool/utils/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +616 -2
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
- agentpool-2.5.0.dist-info/RECORD +579 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +24 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +100 -21
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +42 -5
- agentpool_commands/agents.py +75 -2
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/pool.py +260 -0
- agentpool_commands/session.py +1 -1
- agentpool_commands/text_sharing/__init__.py +119 -0
- agentpool_commands/text_sharing/base.py +123 -0
- agentpool_commands/text_sharing/github_gist.py +80 -0
- agentpool_commands/text_sharing/opencode.py +462 -0
- agentpool_commands/text_sharing/paste_rs.py +59 -0
- agentpool_commands/text_sharing/pastebin.py +116 -0
- agentpool_commands/text_sharing/shittycodingagent.py +112 -0
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +80 -30
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/converters.py +1 -1
- agentpool_config/event_handlers.py +42 -0
- agentpool_config/events.py +1 -1
- agentpool_config/forward_targets.py +1 -4
- agentpool_config/jinja.py +3 -3
- agentpool_config/mcp_server.py +132 -6
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +82 -38
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -22
- agentpool_config/toolsets.py +109 -233
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +234 -181
- agentpool_server/acp_server/commands/acp_commands.py +151 -156
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +24 -90
- agentpool_server/acp_server/session.py +173 -331
- agentpool_server/acp_server/session_manager.py +8 -34
- agentpool_server/agui_server/server.py +3 -1
- agentpool_server/mcp_server/server.py +5 -2
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +401 -0
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +19 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +975 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +421 -0
- agentpool_server/opencode_server/models/__init__.py +250 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +72 -0
- agentpool_server/opencode_server/models/base.py +26 -0
- agentpool_server/opencode_server/models/common.py +23 -0
- agentpool_server/opencode_server/models/config.py +37 -0
- agentpool_server/opencode_server/models/events.py +821 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +44 -0
- agentpool_server/opencode_server/models/message.py +179 -0
- agentpool_server/opencode_server/models/parts.py +323 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +111 -0
- agentpool_server/opencode_server/routes/__init__.py +29 -0
- agentpool_server/opencode_server/routes/agent_routes.py +473 -0
- agentpool_server/opencode_server/routes/app_routes.py +202 -0
- agentpool_server/opencode_server/routes/config_routes.py +302 -0
- agentpool_server/opencode_server/routes/file_routes.py +571 -0
- agentpool_server/opencode_server/routes/global_routes.py +94 -0
- agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
- agentpool_server/opencode_server/routes/message_routes.py +761 -0
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +300 -0
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +1276 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +475 -0
- agentpool_server/opencode_server/state.py +151 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +12 -0
- agentpool_storage/base.py +184 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/claude_provider/provider.py +1089 -0
- agentpool_storage/file_provider.py +278 -15
- agentpool_storage/memory_provider.py +193 -12
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
- agentpool_storage/opencode_provider/__init__.py +16 -0
- agentpool_storage/opencode_provider/helpers.py +414 -0
- agentpool_storage/opencode_provider/provider.py +895 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +26 -6
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +269 -3
- agentpool_storage/sql_provider/utils.py +12 -13
- agentpool_storage/zed_provider/__init__.py +16 -0
- agentpool_storage/zed_provider/helpers.py +281 -0
- agentpool_storage/zed_provider/models.py +130 -0
- agentpool_storage/zed_provider/provider.py +442 -0
- agentpool_storage/zed_provider.py +803 -0
- agentpool_toolsets/__init__.py +0 -2
- agentpool_toolsets/builtin/__init__.py +2 -12
- agentpool_toolsets/builtin/code.py +96 -57
- agentpool_toolsets/builtin/debug.py +118 -48
- agentpool_toolsets/builtin/execution_environment.py +115 -230
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +9 -4
- agentpool_toolsets/builtin/subagent_tools.py +64 -51
- agentpool_toolsets/builtin/workers.py +4 -2
- agentpool_toolsets/composio_toolset.py +2 -2
- agentpool_toolsets/entry_points.py +3 -1
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +99 -7
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +511 -0
- agentpool_toolsets/mcp_run_toolset.py +87 -12
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool-2.1.9.dist-info/RECORD +0 -474
- agentpool_config/resources.py +0 -33
- agentpool_server/acp_server/acp_tools.py +0 -43
- agentpool_server/acp_server/commands/spawn.py +0 -210
- agentpool_storage/text_log_provider.py +0 -275
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,18 +7,20 @@ from fnmatch import fnmatch
|
|
|
7
7
|
import os
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
import time
|
|
10
|
-
from typing import TYPE_CHECKING, Any
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
11
11
|
from urllib.parse import urlparse
|
|
12
12
|
|
|
13
13
|
import anyio
|
|
14
14
|
from exxec.base import ExecutionEnvironment
|
|
15
15
|
from pydantic_ai import (
|
|
16
16
|
BinaryContent,
|
|
17
|
+
ModelResponse,
|
|
17
18
|
PartDeltaEvent,
|
|
18
19
|
PartStartEvent,
|
|
19
20
|
RunContext, # noqa: TC002
|
|
20
21
|
TextPart,
|
|
21
22
|
TextPartDelta,
|
|
23
|
+
ToolCallPart,
|
|
22
24
|
)
|
|
23
25
|
from upathtools import is_directory
|
|
24
26
|
|
|
@@ -26,9 +28,19 @@ from agentpool.agents.context import AgentContext # noqa: TC001
|
|
|
26
28
|
from agentpool.log import get_logger
|
|
27
29
|
from agentpool.mime_utils import guess_type, is_binary_content, is_binary_mime
|
|
28
30
|
from agentpool.resource_providers import ResourceProvider
|
|
31
|
+
from agentpool.tool_impls.delete_path import create_delete_path_tool
|
|
32
|
+
from agentpool.tool_impls.download_file import create_download_file_tool
|
|
33
|
+
from agentpool.tool_impls.grep import create_grep_tool
|
|
34
|
+
from agentpool.tool_impls.list_directory import create_list_directory_tool
|
|
35
|
+
from agentpool.tool_impls.read import create_read_tool
|
|
36
|
+
from agentpool.tools.base import ToolResult # noqa: TC001
|
|
29
37
|
from agentpool_toolsets.builtin.file_edit import replace_content
|
|
30
38
|
from agentpool_toolsets.builtin.file_edit.fuzzy_matcher import StreamingFuzzyMatcher
|
|
31
|
-
from agentpool_toolsets.fsspec_toolset.diagnostics import
|
|
39
|
+
from agentpool_toolsets.fsspec_toolset.diagnostics import (
|
|
40
|
+
DiagnosticsConfig,
|
|
41
|
+
DiagnosticsManager,
|
|
42
|
+
format_diagnostics_table,
|
|
43
|
+
)
|
|
32
44
|
from agentpool_toolsets.fsspec_toolset.grep import GrepBackend
|
|
33
45
|
from agentpool_toolsets.fsspec_toolset.helpers import (
|
|
34
46
|
format_directory_listing,
|
|
@@ -43,9 +55,11 @@ from agentpool_toolsets.fsspec_toolset.streaming_diff_parser import (
|
|
|
43
55
|
|
|
44
56
|
|
|
45
57
|
if TYPE_CHECKING:
|
|
58
|
+
from collections.abc import Sequence
|
|
59
|
+
|
|
46
60
|
import fsspec
|
|
47
61
|
from fsspec.asyn import AsyncFileSystem
|
|
48
|
-
from pydantic_ai
|
|
62
|
+
from pydantic_ai import ModelRequest
|
|
49
63
|
|
|
50
64
|
from agentpool.agents.base_agent import BaseAgent
|
|
51
65
|
from agentpool.common_types import ModelType
|
|
@@ -78,6 +92,9 @@ class FSSpecTools(ResourceProvider):
|
|
|
78
92
|
enable_diagnostics: bool = False,
|
|
79
93
|
large_file_tokens: int = 12_000,
|
|
80
94
|
map_max_tokens: int = 2048,
|
|
95
|
+
edit_tool: Literal["simple", "batch", "agentic"] = "simple",
|
|
96
|
+
max_image_size: int | None = 2000,
|
|
97
|
+
max_image_bytes: int | None = None,
|
|
81
98
|
) -> None:
|
|
82
99
|
"""Initialize with an fsspec filesystem or execution environment.
|
|
83
100
|
|
|
@@ -94,6 +111,12 @@ class FSSpecTools(ResourceProvider):
|
|
|
94
111
|
enable_diagnostics: Run LSP CLI diagnostics after file writes (default: False)
|
|
95
112
|
large_file_tokens: Token threshold for switching to structure map (default: 12000)
|
|
96
113
|
map_max_tokens: Maximum tokens for structure map output (default: 2048)
|
|
114
|
+
edit_tool: Which edit variant to expose ("simple" or "batch")
|
|
115
|
+
max_image_size: Max width/height for images in pixels. Larger images are
|
|
116
|
+
auto-resized for better model compatibility. Set to None to disable.
|
|
117
|
+
max_image_bytes: Max file size for images in bytes. Images exceeding this
|
|
118
|
+
are compressed using progressive quality/dimension reduction.
|
|
119
|
+
Default: 4.5MB (below Anthropic's 5MB limit).
|
|
97
120
|
"""
|
|
98
121
|
from fsspec.asyn import AsyncFileSystem
|
|
99
122
|
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
|
|
@@ -124,8 +147,11 @@ class FSSpecTools(ResourceProvider):
|
|
|
124
147
|
self._large_file_tokens = large_file_tokens
|
|
125
148
|
self._map_max_tokens = map_max_tokens
|
|
126
149
|
self._repomap: RepoMap | None = None
|
|
150
|
+
self._edit_tool = edit_tool
|
|
151
|
+
self._max_image_size = max_image_size
|
|
152
|
+
self._max_image_bytes = max_image_bytes
|
|
127
153
|
|
|
128
|
-
def
|
|
154
|
+
def _get_fs(self, agent_ctx: AgentContext) -> AsyncFileSystem:
|
|
129
155
|
"""Get filesystem, falling back to agent's env if not set.
|
|
130
156
|
|
|
131
157
|
Args:
|
|
@@ -139,11 +165,15 @@ class FSSpecTools(ResourceProvider):
|
|
|
139
165
|
fs = agent_ctx.agent.env.get_fs()
|
|
140
166
|
return fs if isinstance(fs, AsyncFileSystem) else AsyncFileSystemWrapper(fs)
|
|
141
167
|
|
|
142
|
-
def _get_diagnostics_manager(self, agent_ctx: AgentContext) -> DiagnosticsManager:
|
|
168
|
+
def _get_diagnostics_manager(self, agent_ctx: AgentContext) -> DiagnosticsManager | None:
|
|
143
169
|
"""Get or create the diagnostics manager."""
|
|
170
|
+
if not self._enable_diagnostics:
|
|
171
|
+
return None
|
|
144
172
|
if self._diagnostics is None:
|
|
145
173
|
env = self.execution_env or agent_ctx.agent.env
|
|
146
|
-
|
|
174
|
+
# Default to rust-only for fast feedback after edits
|
|
175
|
+
config = DiagnosticsConfig(rust_only=True, max_servers_per_language=1)
|
|
176
|
+
self._diagnostics = DiagnosticsManager(env, config=config)
|
|
147
177
|
return self._diagnostics
|
|
148
178
|
|
|
149
179
|
async def _run_diagnostics(self, agent_ctx: AgentContext, path: str) -> str | None:
|
|
@@ -151,12 +181,12 @@ class FSSpecTools(ResourceProvider):
|
|
|
151
181
|
|
|
152
182
|
Returns formatted diagnostics string if issues found, None otherwise.
|
|
153
183
|
"""
|
|
154
|
-
if not self._enable_diagnostics:
|
|
155
|
-
return None
|
|
156
184
|
mgr = self._get_diagnostics_manager(agent_ctx)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
185
|
+
if mgr is None:
|
|
186
|
+
return None
|
|
187
|
+
result = await mgr.run_for_file(path)
|
|
188
|
+
if result.diagnostics:
|
|
189
|
+
return format_diagnostics_table(result.diagnostics)
|
|
160
190
|
return None
|
|
161
191
|
|
|
162
192
|
async def _get_file_map(self, path: str, agent_ctx: AgentContext) -> str | None:
|
|
@@ -177,7 +207,7 @@ class FSSpecTools(ResourceProvider):
|
|
|
177
207
|
# Lazy init repomap - use file's directory as root
|
|
178
208
|
if self._repomap is None:
|
|
179
209
|
root = str(Path(path).parent)
|
|
180
|
-
fs = self.
|
|
210
|
+
fs = self._get_fs(agent_ctx)
|
|
181
211
|
self._repomap = RepoMap(fs, root, max_tokens=self._map_max_tokens)
|
|
182
212
|
|
|
183
213
|
return await self._repomap.get_file_map(path, max_tokens=self._map_max_tokens)
|
|
@@ -202,31 +232,66 @@ class FSSpecTools(ResourceProvider):
|
|
|
202
232
|
return str(Path(cwd) / path)
|
|
203
233
|
return path
|
|
204
234
|
|
|
205
|
-
async def get_tools(self) ->
|
|
235
|
+
async def get_tools(self) -> Sequence[Tool]:
|
|
206
236
|
"""Get filesystem tools."""
|
|
207
237
|
if self._tools is not None:
|
|
208
238
|
return self._tools
|
|
209
239
|
|
|
240
|
+
# Create standalone tools with toolset's configuration
|
|
241
|
+
list_dir_tool = create_list_directory_tool(
|
|
242
|
+
env=self.execution_env,
|
|
243
|
+
cwd=self.cwd,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
read_tool = create_read_tool(
|
|
247
|
+
env=self.execution_env,
|
|
248
|
+
converter=self.converter, # Pass converter for automatic markdown conversion
|
|
249
|
+
cwd=self.cwd,
|
|
250
|
+
max_file_size_kb=self.max_file_size // 1024,
|
|
251
|
+
max_image_size=self._max_image_size,
|
|
252
|
+
max_image_bytes=self._max_image_bytes,
|
|
253
|
+
large_file_tokens=self._large_file_tokens,
|
|
254
|
+
map_max_tokens=self._map_max_tokens,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
grep_tool = create_grep_tool(
|
|
258
|
+
env=self.execution_env,
|
|
259
|
+
cwd=self.cwd,
|
|
260
|
+
max_output_kb=self.max_grep_output // 1024,
|
|
261
|
+
use_subprocess_grep=self.use_subprocess_grep,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
delete_tool = create_delete_path_tool(
|
|
265
|
+
env=self.execution_env,
|
|
266
|
+
cwd=self.cwd,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
download_tool = create_download_file_tool(
|
|
270
|
+
env=self.execution_env,
|
|
271
|
+
cwd=self.cwd,
|
|
272
|
+
)
|
|
273
|
+
|
|
210
274
|
self._tools = [
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
self.create_tool(self.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
self.create_tool(self.agentic_edit, category="edit"),
|
|
218
|
-
self.create_tool(self.download_file, category="read", open_world=True),
|
|
275
|
+
list_dir_tool,
|
|
276
|
+
read_tool,
|
|
277
|
+
grep_tool,
|
|
278
|
+
self.create_tool(self.write, category="edit"),
|
|
279
|
+
delete_tool,
|
|
280
|
+
download_tool,
|
|
219
281
|
]
|
|
220
282
|
|
|
221
|
-
|
|
283
|
+
# Add edit tool based on config - mutually exclusive
|
|
284
|
+
if self._edit_tool == "agentic":
|
|
285
|
+
self._tools.append(self.create_tool(self.agentic_edit, category="edit"))
|
|
286
|
+
elif self._edit_tool == "batch":
|
|
222
287
|
self._tools.append(
|
|
223
|
-
self.create_tool(
|
|
224
|
-
self.read_as_markdown,
|
|
225
|
-
category="read",
|
|
226
|
-
read_only=True,
|
|
227
|
-
idempotent=True,
|
|
228
|
-
)
|
|
288
|
+
self.create_tool(self.edit_batch, category="edit", name_override="edit")
|
|
229
289
|
)
|
|
290
|
+
else: # simple
|
|
291
|
+
self._tools.append(self.create_tool(self.edit, category="edit"))
|
|
292
|
+
|
|
293
|
+
# Add regex line editing tool
|
|
294
|
+
self._tools.append(self.create_tool(self.regex_replace_lines, category="edit"))
|
|
230
295
|
|
|
231
296
|
return self._tools
|
|
232
297
|
|
|
@@ -258,7 +323,7 @@ class FSSpecTools(ResourceProvider):
|
|
|
258
323
|
await agent_ctx.events.tool_call_start(title=msg, kind="read", locations=[path])
|
|
259
324
|
|
|
260
325
|
try:
|
|
261
|
-
fs = self.
|
|
326
|
+
fs = self._get_fs(agent_ctx)
|
|
262
327
|
# Check if path exists
|
|
263
328
|
if not await fs._exists(path):
|
|
264
329
|
error_msg = f"Path does not exist: {path}"
|
|
@@ -329,14 +394,14 @@ class FSSpecTools(ResourceProvider):
|
|
|
329
394
|
else:
|
|
330
395
|
return result
|
|
331
396
|
|
|
332
|
-
async def
|
|
397
|
+
async def read( # noqa: D417
|
|
333
398
|
self,
|
|
334
399
|
agent_ctx: AgentContext,
|
|
335
400
|
path: str,
|
|
336
401
|
encoding: str = "utf-8",
|
|
337
402
|
line: int | None = None,
|
|
338
403
|
limit: int | None = None,
|
|
339
|
-
) -> str | BinaryContent:
|
|
404
|
+
) -> str | BinaryContent | list[str | BinaryContent]:
|
|
340
405
|
"""Read the context of a text file, or use vision capabilites to read images or documents.
|
|
341
406
|
|
|
342
407
|
Args:
|
|
@@ -346,30 +411,57 @@ class FSSpecTools(ResourceProvider):
|
|
|
346
411
|
limit: Optional maximum number of lines to read (text files only)
|
|
347
412
|
|
|
348
413
|
Returns:
|
|
349
|
-
Text content for text files, BinaryContent for binary files
|
|
414
|
+
Text content for text files, BinaryContent for binary files (with optional
|
|
415
|
+
dimension note as list when image was resized), or dict with error
|
|
350
416
|
"""
|
|
351
417
|
path = self._resolve_path(path, agent_ctx)
|
|
352
418
|
msg = f"Reading file: {path}"
|
|
353
419
|
from agentpool.agents.events import LocationContentItem
|
|
354
420
|
|
|
421
|
+
# Emit progress - use 0 for line if negative (can't resolve until we read file)
|
|
422
|
+
# LocationContentItem/ToolCallLocation require line >= 0 per ACP spec
|
|
423
|
+
display_line = line if (line is not None and line > 0) else 0
|
|
355
424
|
await agent_ctx.events.tool_call_progress(
|
|
356
425
|
title=msg,
|
|
357
|
-
items=[LocationContentItem(path=path)],
|
|
426
|
+
items=[LocationContentItem(path=path, line=display_line)],
|
|
358
427
|
)
|
|
359
428
|
try:
|
|
360
429
|
mime_type = guess_type(path)
|
|
361
430
|
# Fast path: known binary MIME types (images, audio, video, etc.)
|
|
362
431
|
if is_binary_mime(mime_type):
|
|
363
|
-
data = await self.
|
|
432
|
+
data = await self._get_fs(agent_ctx)._cat_file(path)
|
|
364
433
|
await agent_ctx.events.file_operation("read", path=path, success=True)
|
|
365
434
|
mime = mime_type or "application/octet-stream"
|
|
435
|
+
# Resize images if needed
|
|
436
|
+
if self._max_image_size and mime.startswith("image/"):
|
|
437
|
+
from agentpool_toolsets.fsspec_toolset.image_utils import (
|
|
438
|
+
resize_image_if_needed,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
data, mime, note = resize_image_if_needed(
|
|
442
|
+
data, mime, self._max_image_size, self._max_image_bytes
|
|
443
|
+
)
|
|
444
|
+
if note:
|
|
445
|
+
# Return resized image with dimension note for coordinate mapping
|
|
446
|
+
return [note, BinaryContent(data=data, media_type=mime, identifier=path)]
|
|
366
447
|
return BinaryContent(data=data, media_type=mime, identifier=path)
|
|
367
448
|
# Read content and probe for binary (git-style null byte detection)
|
|
368
|
-
data = await self.
|
|
449
|
+
data = await self._get_fs(agent_ctx)._cat_file(path)
|
|
369
450
|
if is_binary_content(data):
|
|
370
451
|
# Binary file - return as BinaryContent for native model handling
|
|
371
452
|
await agent_ctx.events.file_operation("read", path=path, success=True)
|
|
372
453
|
mime = mime_type or "application/octet-stream"
|
|
454
|
+
# Resize images if needed
|
|
455
|
+
if self._max_image_size and mime.startswith("image/"):
|
|
456
|
+
from agentpool_toolsets.fsspec_toolset.image_utils import (
|
|
457
|
+
resize_image_if_needed,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
data, mime, note = resize_image_if_needed(
|
|
461
|
+
data, mime, self._max_image_size, self._max_image_bytes
|
|
462
|
+
)
|
|
463
|
+
if note:
|
|
464
|
+
return [note, BinaryContent(data=data, media_type=mime, identifier=path)]
|
|
373
465
|
return BinaryContent(data=data, media_type=mime, identifier=path)
|
|
374
466
|
content = data.decode(encoding)
|
|
375
467
|
|
|
@@ -395,7 +487,11 @@ class FSSpecTools(ResourceProvider):
|
|
|
395
487
|
lines, offset, limit, self.max_file_size
|
|
396
488
|
)
|
|
397
489
|
content = "\n".join(result_lines)
|
|
398
|
-
|
|
490
|
+
# Don't pass negative line numbers to events (ACP requires >= 0)
|
|
491
|
+
display_line = line if (line and line > 0) else 0
|
|
492
|
+
await agent_ctx.events.file_operation(
|
|
493
|
+
"read", path=path, success=True, line=display_line
|
|
494
|
+
)
|
|
399
495
|
if was_truncated:
|
|
400
496
|
content += f"\n\n[Content truncated at {self.max_file_size} bytes]"
|
|
401
497
|
|
|
@@ -406,9 +502,11 @@ class FSSpecTools(ResourceProvider):
|
|
|
406
502
|
# Emit file content for UI display (formatted at ACP layer)
|
|
407
503
|
from agentpool.agents.events import FileContentItem
|
|
408
504
|
|
|
505
|
+
# Use non-negative line for display (negative lines are internal Python convention)
|
|
506
|
+
display_start_line = max(1, line) if line and line > 0 else None
|
|
409
507
|
await agent_ctx.events.tool_call_progress(
|
|
410
508
|
title=f"Read: {path}",
|
|
411
|
-
items=[FileContentItem(content=content, path=path)],
|
|
509
|
+
items=[FileContentItem(content=content, path=path, start_line=display_start_line)],
|
|
412
510
|
replace_content=True,
|
|
413
511
|
)
|
|
414
512
|
# Return raw content for agent
|
|
@@ -445,14 +543,14 @@ class FSSpecTools(ResourceProvider):
|
|
|
445
543
|
else:
|
|
446
544
|
return content
|
|
447
545
|
|
|
448
|
-
async def
|
|
546
|
+
async def write( # noqa: D417
|
|
449
547
|
self,
|
|
450
548
|
agent_ctx: AgentContext,
|
|
451
549
|
path: str,
|
|
452
550
|
content: str,
|
|
453
551
|
mode: str = "w",
|
|
454
552
|
overwrite: bool = False,
|
|
455
|
-
) ->
|
|
553
|
+
) -> str | ToolResult:
|
|
456
554
|
"""Write content to a file.
|
|
457
555
|
|
|
458
556
|
Args:
|
|
@@ -462,8 +560,11 @@ class FSSpecTools(ResourceProvider):
|
|
|
462
560
|
overwrite: Must be True to overwrite existing files (safety check)
|
|
463
561
|
|
|
464
562
|
Returns:
|
|
465
|
-
|
|
563
|
+
Success message or ToolResult with metadata
|
|
466
564
|
"""
|
|
565
|
+
from agentpool.agents.events import DiffContentItem
|
|
566
|
+
from agentpool.tools.base import ToolResult
|
|
567
|
+
|
|
467
568
|
path = self._resolve_path(path, agent_ctx)
|
|
468
569
|
msg = f"Writing file: {path}"
|
|
469
570
|
await agent_ctx.events.tool_call_start(title=msg, kind="edit", locations=[path])
|
|
@@ -474,7 +575,7 @@ class FSSpecTools(ResourceProvider):
|
|
|
474
575
|
if mode not in ("w", "a"):
|
|
475
576
|
msg = f"Invalid mode '{mode}'. Use 'w' (write) or 'a' (append)"
|
|
476
577
|
await agent_ctx.events.file_operation("write", path=path, success=False, error=msg)
|
|
477
|
-
return
|
|
578
|
+
return f"Error: {msg}"
|
|
478
579
|
|
|
479
580
|
# Check size limit
|
|
480
581
|
if content_bytes > self.max_file_size:
|
|
@@ -483,10 +584,10 @@ class FSSpecTools(ResourceProvider):
|
|
|
483
584
|
f"({self.max_file_size} bytes)"
|
|
484
585
|
)
|
|
485
586
|
await agent_ctx.events.file_operation("write", path=path, success=False, error=msg)
|
|
486
|
-
return
|
|
587
|
+
return f"Error: {msg}"
|
|
487
588
|
|
|
488
589
|
# Check if file exists and overwrite protection
|
|
489
|
-
fs = self.
|
|
590
|
+
fs = self._get_fs(agent_ctx)
|
|
490
591
|
file_exists = await fs._exists(path)
|
|
491
592
|
|
|
492
593
|
if file_exists and mode == "w" and not overwrite:
|
|
@@ -495,7 +596,7 @@ class FSSpecTools(ResourceProvider):
|
|
|
495
596
|
f"This is a safety measure to prevent accidental data loss."
|
|
496
597
|
)
|
|
497
598
|
await agent_ctx.events.file_operation("write", path=path, success=False, error=msg)
|
|
498
|
-
return
|
|
599
|
+
return f"Error: {msg}"
|
|
499
600
|
|
|
500
601
|
# Handle append mode: read existing content and prepend it
|
|
501
602
|
if mode == "a" and file_exists:
|
|
@@ -508,30 +609,47 @@ class FSSpecTools(ResourceProvider):
|
|
|
508
609
|
pass # If we can't read, just write new content
|
|
509
610
|
|
|
510
611
|
await self._write(agent_ctx, path, content)
|
|
612
|
+
await agent_ctx.events.tool_call_progress(
|
|
613
|
+
title=f"Wrote: {path}",
|
|
614
|
+
items=[
|
|
615
|
+
DiffContentItem(path=path, old_text="", new_text=content),
|
|
616
|
+
],
|
|
617
|
+
)
|
|
511
618
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
size = info.get("size", content_bytes)
|
|
515
|
-
except (OSError, KeyError):
|
|
516
|
-
size = content_bytes
|
|
517
|
-
|
|
518
|
-
result: dict[str, Any] = {
|
|
519
|
-
"path": path,
|
|
520
|
-
"size": size,
|
|
521
|
-
"mode": mode,
|
|
522
|
-
"file_existed": file_exists,
|
|
523
|
-
"bytes_written": content_bytes,
|
|
524
|
-
}
|
|
525
|
-
await agent_ctx.events.file_operation("write", path=path, success=True)
|
|
526
|
-
|
|
527
|
-
# Run diagnostics if enabled
|
|
619
|
+
# Run diagnostics if enabled (include in message for agent)
|
|
620
|
+
diagnostics_msg = ""
|
|
528
621
|
if diagnostics_output := await self._run_diagnostics(agent_ctx, path):
|
|
529
|
-
|
|
622
|
+
diagnostics_msg = f"\n\nDiagnostics:\n{diagnostics_output}"
|
|
623
|
+
|
|
624
|
+
action = "Appended to" if mode == "a" and file_exists else "Wrote"
|
|
625
|
+
success_msg = f"{action} {path} ({content_bytes} bytes){diagnostics_msg}"
|
|
626
|
+
|
|
627
|
+
# TODO: Include diagnostics in metadata for UI display
|
|
628
|
+
# Expected metadata shape:
|
|
629
|
+
# {
|
|
630
|
+
# "diagnostics": {
|
|
631
|
+
# "<file_path>": [
|
|
632
|
+
# {
|
|
633
|
+
# "range": {"start": {"line": 0, "character": 0}, "end": {...}},
|
|
634
|
+
# "message": "...",
|
|
635
|
+
# "severity": 1 # 1=error, 2=warning, 3=info, 4=hint
|
|
636
|
+
# }
|
|
637
|
+
# ]
|
|
638
|
+
# }
|
|
639
|
+
# }
|
|
640
|
+
|
|
641
|
+
return ToolResult(
|
|
642
|
+
content=success_msg, # Agent sees this (includes diagnostics text)
|
|
643
|
+
metadata={
|
|
644
|
+
# Include file content for UI display (used by OpenCode TUI)
|
|
645
|
+
"filePath": str(Path(path).absolute()),
|
|
646
|
+
"content": content,
|
|
647
|
+
# TODO: Add structured diagnostics here for UI
|
|
648
|
+
},
|
|
649
|
+
)
|
|
530
650
|
except Exception as e: # noqa: BLE001
|
|
531
651
|
await agent_ctx.events.file_operation("write", path=path, success=False, error=str(e))
|
|
532
|
-
return
|
|
533
|
-
else:
|
|
534
|
-
return result
|
|
652
|
+
return f"Error: Failed to write file {path}: {e}"
|
|
535
653
|
|
|
536
654
|
async def delete_path( # noqa: D417
|
|
537
655
|
self, agent_ctx: AgentContext, path: str, recursive: bool = False
|
|
@@ -550,7 +668,7 @@ class FSSpecTools(ResourceProvider):
|
|
|
550
668
|
await agent_ctx.events.tool_call_start(title=msg, kind="delete", locations=[path])
|
|
551
669
|
try:
|
|
552
670
|
# Check if path exists and get its type
|
|
553
|
-
fs = self.
|
|
671
|
+
fs = self._get_fs(agent_ctx)
|
|
554
672
|
try:
|
|
555
673
|
info = await fs._info(path)
|
|
556
674
|
path_type = info.get("type", "unknown")
|
|
@@ -599,7 +717,7 @@ class FSSpecTools(ResourceProvider):
|
|
|
599
717
|
await agent_ctx.events.file_operation("delete", path=path, success=True)
|
|
600
718
|
return result
|
|
601
719
|
|
|
602
|
-
async def
|
|
720
|
+
async def edit( # noqa: D417
|
|
603
721
|
self,
|
|
604
722
|
agent_ctx: AgentContext,
|
|
605
723
|
path: str,
|
|
@@ -607,7 +725,8 @@ class FSSpecTools(ResourceProvider):
|
|
|
607
725
|
new_string: str,
|
|
608
726
|
description: str,
|
|
609
727
|
replace_all: bool = False,
|
|
610
|
-
|
|
728
|
+
line_hint: int | None = None,
|
|
729
|
+
) -> str | ToolResult:
|
|
611
730
|
r"""Edit a file by replacing specific content with smart matching.
|
|
612
731
|
|
|
613
732
|
Uses sophisticated matching strategies to handle whitespace, indentation,
|
|
@@ -619,32 +738,95 @@ class FSSpecTools(ResourceProvider):
|
|
|
619
738
|
new_string: Text content to replace it with
|
|
620
739
|
description: Human-readable description of what the edit accomplishes
|
|
621
740
|
replace_all: Whether to replace all occurrences (default: False)
|
|
741
|
+
line_hint: Line number hint to disambiguate when multiple matches exist.
|
|
742
|
+
If the pattern matches multiple locations, the match closest to this
|
|
743
|
+
line will be used. Useful after getting a "multiple matches" error.
|
|
622
744
|
|
|
623
745
|
Returns:
|
|
624
746
|
Success message with edit summary
|
|
625
747
|
"""
|
|
748
|
+
return await self.edit_batch(
|
|
749
|
+
agent_ctx,
|
|
750
|
+
path,
|
|
751
|
+
replacements=[(old_string, new_string)],
|
|
752
|
+
description=description,
|
|
753
|
+
replace_all=replace_all,
|
|
754
|
+
line_hint=line_hint,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
async def edit_batch( # noqa: D417
|
|
758
|
+
self,
|
|
759
|
+
agent_ctx: AgentContext,
|
|
760
|
+
path: str,
|
|
761
|
+
replacements: list[tuple[str, str]],
|
|
762
|
+
description: str,
|
|
763
|
+
replace_all: bool = False,
|
|
764
|
+
line_hint: int | None = None,
|
|
765
|
+
) -> str | ToolResult:
|
|
766
|
+
r"""Edit a file by applying multiple replacements in one operation.
|
|
767
|
+
|
|
768
|
+
Uses sophisticated matching strategies to handle whitespace, indentation,
|
|
769
|
+
and other variations. Shows the changes as a diff in the UI.
|
|
770
|
+
|
|
771
|
+
Replacements are applied sequentially, so later replacements see the result
|
|
772
|
+
of earlier ones. Each old_string must uniquely match one location (unless
|
|
773
|
+
replace_all=True). If a pattern matches multiple locations, include more
|
|
774
|
+
surrounding context to disambiguate.
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
path: File path (absolute or relative to session cwd)
|
|
778
|
+
replacements: List of (old_string, new_string) tuples to apply sequentially.
|
|
779
|
+
IMPORTANT: Must be a list of pairs, like:
|
|
780
|
+
[("old text", "new text"), ("another old", "another new")]
|
|
781
|
+
|
|
782
|
+
Each old_string should include enough context to uniquely identify
|
|
783
|
+
the target location. For multi-line edits, include the full block.
|
|
784
|
+
description: Human-readable description of what the edit accomplishes
|
|
785
|
+
replace_all: Whether to replace all occurrences of each pattern (default: False)
|
|
786
|
+
line_hint: Line number hint to disambiguate when multiple matches exist.
|
|
787
|
+
Only applies when there is a single replacement. If the pattern matches
|
|
788
|
+
multiple locations, the match closest to this line will be used.
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
Success message with edit summary
|
|
792
|
+
|
|
793
|
+
Example:
|
|
794
|
+
replacements=[
|
|
795
|
+
("def old_name(", "def new_name("),
|
|
796
|
+
("old_name()", "new_name()"), # Update call sites
|
|
797
|
+
]
|
|
798
|
+
"""
|
|
626
799
|
path = self._resolve_path(path, agent_ctx)
|
|
627
800
|
msg = f"Editing file: {path}"
|
|
628
801
|
await agent_ctx.events.tool_call_start(title=msg, kind="edit", locations=[path])
|
|
629
|
-
if old_string == new_string:
|
|
630
|
-
return "Error: old_string and new_string must be different"
|
|
631
802
|
|
|
632
|
-
|
|
633
|
-
|
|
803
|
+
if not replacements:
|
|
804
|
+
return "Error: replacements list cannot be empty"
|
|
805
|
+
|
|
806
|
+
for old_str, new_str in replacements:
|
|
807
|
+
if old_str == new_str:
|
|
808
|
+
return f"Error: old_string and new_string must be different: {old_str!r}"
|
|
634
809
|
|
|
635
810
|
try: # Read current file content
|
|
636
811
|
original_content = await self._read(agent_ctx, path)
|
|
637
812
|
if isinstance(original_content, bytes):
|
|
638
813
|
original_content = original_content.decode("utf-8")
|
|
639
814
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
815
|
+
# Apply all replacements sequentially
|
|
816
|
+
new_content = original_content
|
|
817
|
+
# line_hint only makes sense for single replacements
|
|
818
|
+
hint = line_hint if len(replacements) == 1 else None
|
|
819
|
+
for old_str, new_str in replacements:
|
|
820
|
+
try:
|
|
821
|
+
new_content = replace_content(
|
|
822
|
+
new_content, old_str, new_str, replace_all, line_hint=hint
|
|
823
|
+
)
|
|
824
|
+
except ValueError as e:
|
|
825
|
+
error_msg = f"Edit failed on replacement {old_str!r}: {e}"
|
|
826
|
+
await agent_ctx.events.file_operation(
|
|
827
|
+
"edit", path=path, success=False, error=error_msg
|
|
828
|
+
)
|
|
829
|
+
return error_msg
|
|
648
830
|
|
|
649
831
|
await self._write(agent_ctx, path, new_content)
|
|
650
832
|
success_msg = f"Successfully edited {Path(path).name}: {description}"
|
|
@@ -666,9 +848,238 @@ class FSSpecTools(ResourceProvider):
|
|
|
666
848
|
error_msg = f"Error editing file: {e}"
|
|
667
849
|
await agent_ctx.events.file_operation("edit", path=path, success=False, error=error_msg)
|
|
668
850
|
return error_msg
|
|
851
|
+
else:
|
|
852
|
+
# Generate unified diff for OpenCode UI
|
|
853
|
+
from difflib import unified_diff
|
|
854
|
+
|
|
855
|
+
from agentpool.tools.base import ToolResult
|
|
856
|
+
|
|
857
|
+
# Ensure content ends with newline for proper diff formatting
|
|
858
|
+
original_for_diff = (
|
|
859
|
+
original_content if original_content.endswith("\n") else original_content + "\n"
|
|
860
|
+
)
|
|
861
|
+
new_for_diff = new_content if new_content.endswith("\n") else new_content + "\n"
|
|
862
|
+
|
|
863
|
+
diff_lines = unified_diff(
|
|
864
|
+
original_for_diff.splitlines(keepends=True),
|
|
865
|
+
new_for_diff.splitlines(keepends=True),
|
|
866
|
+
fromfile=f"a/{Path(path).name}",
|
|
867
|
+
tofile=f"b/{Path(path).name}",
|
|
868
|
+
)
|
|
869
|
+
diff = "".join(diff_lines)
|
|
870
|
+
|
|
871
|
+
# Count additions and deletions
|
|
872
|
+
original_lines = set(original_content.splitlines())
|
|
873
|
+
new_lines = set(new_content.splitlines())
|
|
874
|
+
additions = len(new_lines - original_lines)
|
|
875
|
+
deletions = len(original_lines - new_lines)
|
|
876
|
+
|
|
877
|
+
return ToolResult(
|
|
878
|
+
content=success_msg,
|
|
879
|
+
metadata={
|
|
880
|
+
"diff": diff,
|
|
881
|
+
"filediff": {
|
|
882
|
+
"file": str(Path(path).absolute()),
|
|
883
|
+
"before": original_content,
|
|
884
|
+
"after": new_content,
|
|
885
|
+
"additions": additions,
|
|
886
|
+
"deletions": deletions,
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
async def regex_replace_lines( # noqa: PLR0915
|
|
892
|
+
self,
|
|
893
|
+
agent_ctx: AgentContext,
|
|
894
|
+
path: str,
|
|
895
|
+
start: int | str,
|
|
896
|
+
end: int | str,
|
|
897
|
+
pattern: str,
|
|
898
|
+
replacement: str,
|
|
899
|
+
*,
|
|
900
|
+
count: int = 0,
|
|
901
|
+
) -> str:
|
|
902
|
+
r"""Apply regex replacement to a line range specified by line numbers or text markers.
|
|
903
|
+
|
|
904
|
+
Useful for systematic edits:
|
|
905
|
+
- Remove/add indentation
|
|
906
|
+
- Comment/uncomment blocks
|
|
907
|
+
- Rename variables within scope
|
|
908
|
+
- Delete line ranges
|
|
909
|
+
|
|
910
|
+
Args:
|
|
911
|
+
agent_ctx: Agent execution context
|
|
912
|
+
path: File path to edit
|
|
913
|
+
start: Start of range - int (1-based line number) or str (unique text marker)
|
|
914
|
+
end: End of range - int (1-based line number) or str (first occurrence after start)
|
|
915
|
+
pattern: Regex pattern to search for within the range
|
|
916
|
+
replacement: Replacement string (supports \1, \2 capture groups; empty removes)
|
|
917
|
+
count: Max replacements per line (0 = unlimited)
|
|
918
|
+
|
|
919
|
+
Returns:
|
|
920
|
+
Success message with statistics
|
|
921
|
+
|
|
922
|
+
Examples:
|
|
923
|
+
# Remove a function
|
|
924
|
+
regex_replace_lines(ctx, "file.py", "def old_func(", " return", r".*\n", "")
|
|
925
|
+
|
|
926
|
+
# Indent by line numbers
|
|
927
|
+
regex_replace_lines(ctx, "file.py", 10, 20, r"^", " ")
|
|
928
|
+
|
|
929
|
+
# Uncomment a section
|
|
930
|
+
regex_replace_lines(ctx, "file.py", "# START", "# END", r"^# ", "")
|
|
931
|
+
"""
|
|
932
|
+
import re
|
|
933
|
+
|
|
934
|
+
path = self._resolve_path(path, agent_ctx)
|
|
935
|
+
msg = f"Regex editing file: {path}"
|
|
936
|
+
await agent_ctx.events.tool_call_start(title=msg, kind="edit", locations=[path])
|
|
937
|
+
|
|
938
|
+
try:
|
|
939
|
+
# Read original content
|
|
940
|
+
original_content = await self._read(agent_ctx, path)
|
|
941
|
+
if isinstance(original_content, bytes):
|
|
942
|
+
original_content = original_content.decode("utf-8")
|
|
943
|
+
|
|
944
|
+
lines = original_content.splitlines(keepends=True)
|
|
945
|
+
total_lines = len(lines)
|
|
946
|
+
|
|
947
|
+
# Resolve start position
|
|
948
|
+
if isinstance(start, int):
|
|
949
|
+
if start < 1:
|
|
950
|
+
msg = f"start line must be >= 1, got {start}"
|
|
951
|
+
raise ValueError(msg) # noqa: TRY301
|
|
952
|
+
start_line = start
|
|
953
|
+
else:
|
|
954
|
+
# Find unique occurrence of start string (raises ValueError if not found/unique)
|
|
955
|
+
start_line = self._find_unique_line(lines, start, "start")
|
|
956
|
+
|
|
957
|
+
# Resolve end position
|
|
958
|
+
if isinstance(end, int):
|
|
959
|
+
if end < start_line:
|
|
960
|
+
msg = f"end line {end} must be >= start line {start_line}"
|
|
961
|
+
raise ValueError(msg) # noqa: TRY301
|
|
962
|
+
end_line = end
|
|
963
|
+
else:
|
|
964
|
+
# Find first occurrence of end string after start (raises ValueError if not found)
|
|
965
|
+
end_line = self._find_first_after(lines, end, start_line, "end")
|
|
966
|
+
|
|
967
|
+
# Validate range
|
|
968
|
+
if end_line > total_lines:
|
|
969
|
+
msg = f"end_line {end_line} exceeds file length {total_lines}"
|
|
970
|
+
raise ValueError(msg) # noqa: TRY301
|
|
971
|
+
|
|
972
|
+
# Convert to 0-based indexing for array access
|
|
973
|
+
start_idx = start_line - 1
|
|
974
|
+
end_idx = end_line # end_line is inclusive, but list slice is exclusive
|
|
975
|
+
|
|
976
|
+
# Compile regex pattern
|
|
977
|
+
regex = re.compile(pattern)
|
|
978
|
+
|
|
979
|
+
# Apply replacements to the specified line range
|
|
980
|
+
modified_count = 0
|
|
981
|
+
replacement_count = 0
|
|
982
|
+
|
|
983
|
+
for i in range(start_idx, end_idx):
|
|
984
|
+
original = lines[i]
|
|
985
|
+
modified, num_subs = regex.subn(replacement, original, count=count)
|
|
986
|
+
if num_subs > 0:
|
|
987
|
+
lines[i] = modified
|
|
988
|
+
modified_count += 1
|
|
989
|
+
replacement_count += num_subs
|
|
990
|
+
|
|
991
|
+
# Build new content
|
|
992
|
+
new_content = "".join(lines)
|
|
993
|
+
|
|
994
|
+
# Write back
|
|
995
|
+
await self._write(agent_ctx, path, new_content)
|
|
996
|
+
|
|
997
|
+
# Build success message
|
|
998
|
+
success_msg = (
|
|
999
|
+
f"Successfully applied regex to lines {start_line}-{end_line} in {Path(path).name}"
|
|
1000
|
+
)
|
|
1001
|
+
if modified_count > 0:
|
|
1002
|
+
success_msg += (
|
|
1003
|
+
f" ({modified_count} lines modified, {replacement_count} replacements)"
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
# Emit file edit event for diff display
|
|
1007
|
+
await agent_ctx.events.file_edit_progress(
|
|
1008
|
+
path=path,
|
|
1009
|
+
old_text=original_content,
|
|
1010
|
+
new_text=new_content,
|
|
1011
|
+
status="completed",
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
# Run diagnostics if enabled
|
|
1015
|
+
if diagnostics_output := await self._run_diagnostics(agent_ctx, path):
|
|
1016
|
+
success_msg += f"\n\nDiagnostics:\n{diagnostics_output}"
|
|
1017
|
+
except Exception as e: # noqa: BLE001
|
|
1018
|
+
error_msg = f"Error applying regex to file: {e}"
|
|
1019
|
+
await agent_ctx.events.file_operation("edit", path=path, success=False, error=error_msg)
|
|
1020
|
+
return error_msg
|
|
669
1021
|
else:
|
|
670
1022
|
return success_msg
|
|
671
1023
|
|
|
1024
|
+
@staticmethod
|
|
1025
|
+
def _find_unique_line(lines: list[str], search_text: str, param_name: str) -> int:
|
|
1026
|
+
"""Find unique occurrence of text in lines.
|
|
1027
|
+
|
|
1028
|
+
Args:
|
|
1029
|
+
lines: File lines
|
|
1030
|
+
search_text: Text to search for
|
|
1031
|
+
param_name: Parameter name for error messages
|
|
1032
|
+
|
|
1033
|
+
Returns:
|
|
1034
|
+
Line number (1-based)
|
|
1035
|
+
|
|
1036
|
+
Raises:
|
|
1037
|
+
ValueError: If text not found or matches multiple lines
|
|
1038
|
+
"""
|
|
1039
|
+
matches = []
|
|
1040
|
+
for i, line in enumerate(lines, start=1):
|
|
1041
|
+
if search_text in line:
|
|
1042
|
+
matches.append(i)
|
|
1043
|
+
|
|
1044
|
+
if not matches:
|
|
1045
|
+
msg = f"{param_name} text not found: {search_text!r}"
|
|
1046
|
+
raise ValueError(msg)
|
|
1047
|
+
if len(matches) > 1:
|
|
1048
|
+
match_lines = ", ".join(str(m) for m in matches[:5])
|
|
1049
|
+
more = f" and {len(matches) - 5} more" if len(matches) > 5 else "" # noqa: PLR2004
|
|
1050
|
+
msg = (
|
|
1051
|
+
f"{param_name} text matches multiple lines ({match_lines}{more}). "
|
|
1052
|
+
f"Include more context to make it unique."
|
|
1053
|
+
)
|
|
1054
|
+
raise ValueError(msg)
|
|
1055
|
+
|
|
1056
|
+
return matches[0]
|
|
1057
|
+
|
|
1058
|
+
@staticmethod
|
|
1059
|
+
def _find_first_after(
|
|
1060
|
+
lines: list[str], search_text: str, after_line: int, param_name: str
|
|
1061
|
+
) -> int:
|
|
1062
|
+
"""Find first occurrence of text after a given line.
|
|
1063
|
+
|
|
1064
|
+
Args:
|
|
1065
|
+
lines: File lines
|
|
1066
|
+
search_text: Text to search for
|
|
1067
|
+
after_line: Line number to search after (1-based)
|
|
1068
|
+
param_name: Parameter name for error messages
|
|
1069
|
+
|
|
1070
|
+
Returns:
|
|
1071
|
+
Line number (1-based)
|
|
1072
|
+
|
|
1073
|
+
Raises:
|
|
1074
|
+
ValueError: If text not found after the specified line
|
|
1075
|
+
"""
|
|
1076
|
+
for i in range(after_line - 1, len(lines)):
|
|
1077
|
+
if search_text in lines[i]:
|
|
1078
|
+
return i + 1
|
|
1079
|
+
|
|
1080
|
+
msg = f"{param_name} text not found after line {after_line}: {search_text!r}"
|
|
1081
|
+
raise ValueError(msg)
|
|
1082
|
+
|
|
672
1083
|
async def grep( # noqa: D417
|
|
673
1084
|
self,
|
|
674
1085
|
agent_ctx: AgentContext,
|
|
@@ -731,7 +1142,7 @@ class FSSpecTools(ResourceProvider):
|
|
|
731
1142
|
|
|
732
1143
|
# Fallback to fsspec grep if subprocess didn't work
|
|
733
1144
|
if result is None or "error" in result:
|
|
734
|
-
fs = self.
|
|
1145
|
+
fs = self._get_fs(agent_ctx)
|
|
735
1146
|
result = await grep_with_fsspec(
|
|
736
1147
|
fs=fs,
|
|
737
1148
|
pattern=pattern,
|
|
@@ -774,12 +1185,12 @@ class FSSpecTools(ResourceProvider):
|
|
|
774
1185
|
async def _read(self, agent_ctx: AgentContext, path: str, encoding: str = "utf-8") -> str:
|
|
775
1186
|
# with self.fs.open(path, "r", encoding="utf-8") as f:
|
|
776
1187
|
# return f.read()
|
|
777
|
-
return await self.
|
|
1188
|
+
return await self._get_fs(agent_ctx)._cat(path) # type: ignore[no-any-return]
|
|
778
1189
|
|
|
779
1190
|
async def _write(self, agent_ctx: AgentContext, path: str, content: str | bytes) -> None:
|
|
780
1191
|
if isinstance(content, str):
|
|
781
1192
|
content = content.encode()
|
|
782
|
-
await self.
|
|
1193
|
+
await self._get_fs(agent_ctx)._pipe_file(path, content)
|
|
783
1194
|
|
|
784
1195
|
async def download_file( # noqa: D417
|
|
785
1196
|
self,
|
|
@@ -813,7 +1224,7 @@ class FSSpecTools(ResourceProvider):
|
|
|
813
1224
|
full_path = f"{target_dir.rstrip('/')}/{filename}"
|
|
814
1225
|
|
|
815
1226
|
try:
|
|
816
|
-
fs = self.
|
|
1227
|
+
fs = self._get_fs(agent_ctx)
|
|
817
1228
|
# Ensure target directory exists
|
|
818
1229
|
await fs._makedirs(target_dir, exist_ok=True)
|
|
819
1230
|
|
|
@@ -876,7 +1287,7 @@ class FSSpecTools(ResourceProvider):
|
|
|
876
1287
|
await agent_ctx.events.file_operation("read", path=url, success=False, error=error_msg)
|
|
877
1288
|
return {"error": error_msg}
|
|
878
1289
|
|
|
879
|
-
async def agentic_edit( # noqa: D417
|
|
1290
|
+
async def agentic_edit( # noqa: D417
|
|
880
1291
|
self,
|
|
881
1292
|
run_ctx: RunContext,
|
|
882
1293
|
agent_ctx: AgentContext,
|
|
@@ -906,8 +1317,6 @@ class FSSpecTools(ResourceProvider):
|
|
|
906
1317
|
Returns:
|
|
907
1318
|
Success message with edit summary
|
|
908
1319
|
"""
|
|
909
|
-
from pydantic_ai.messages import CachePoint, ModelRequest
|
|
910
|
-
|
|
911
1320
|
from agentpool.messaging import ChatMessage, MessageHistory
|
|
912
1321
|
|
|
913
1322
|
path = self._resolve_path(path, agent_ctx)
|
|
@@ -941,10 +1350,8 @@ class FSSpecTools(ResourceProvider):
|
|
|
941
1350
|
# 1. Stored history (previous runs) from agent.conversation
|
|
942
1351
|
# 2. Current run messages from run_ctx.messages (not yet stored)
|
|
943
1352
|
stored_history = agent.conversation.get_history()
|
|
944
|
-
|
|
945
1353
|
# Build complete message list
|
|
946
1354
|
all_messages: list[ModelRequest | ModelResponse] = []
|
|
947
|
-
|
|
948
1355
|
# Add stored history from previous runs
|
|
949
1356
|
for chat_msg in stored_history:
|
|
950
1357
|
all_messages.extend(chat_msg.to_pydantic_ai())
|
|
@@ -952,7 +1359,6 @@ class FSSpecTools(ResourceProvider):
|
|
|
952
1359
|
# Add current run's messages (not yet in stored history)
|
|
953
1360
|
# But exclude the last message if it contains the current agentic_edit tool call
|
|
954
1361
|
# to avoid the sub-agent seeing "I'm calling agentic_edit" in its context
|
|
955
|
-
from pydantic_ai.messages import ModelResponse, ToolCallPart
|
|
956
1362
|
|
|
957
1363
|
for msg in run_ctx.messages:
|
|
958
1364
|
if isinstance(msg, ModelResponse):
|
|
@@ -967,17 +1373,16 @@ class FSSpecTools(ResourceProvider):
|
|
|
967
1373
|
else:
|
|
968
1374
|
all_messages.append(msg)
|
|
969
1375
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
cache_request: ModelRequest = ModelRequest(parts=[CachePoint()])
|
|
973
|
-
all_messages.append(cache_request)
|
|
1376
|
+
# Inject CachePoint to cache everything up to this point
|
|
1377
|
+
# if all_messages:
|
|
1378
|
+
# cache_request: ModelRequest = ModelRequest(parts=[CachePoint()])
|
|
1379
|
+
# all_messages.append(cache_request)
|
|
974
1380
|
|
|
975
1381
|
# Wrap in a single ChatMessage for the forked history
|
|
976
1382
|
fork_history = MessageHistory(
|
|
977
1383
|
messages=[ChatMessage(messages=all_messages, role="user", content="")]
|
|
978
1384
|
)
|
|
979
|
-
|
|
980
|
-
fork_history = MessageHistory()
|
|
1385
|
+
fork_history = MessageHistory()
|
|
981
1386
|
|
|
982
1387
|
# Stream the edit using the same agent but with forked history
|
|
983
1388
|
if mode == "edit" and matcher == "zed":
|