agentpool 2.1.9__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.
Potentially problematic release.
This version of agentpool might be problematic. Click here for more details.
- acp/README.md +64 -0
- acp/__init__.py +172 -0
- acp/__main__.py +10 -0
- acp/acp_requests.py +285 -0
- acp/agent/__init__.py +6 -0
- acp/agent/connection.py +256 -0
- acp/agent/implementations/__init__.py +6 -0
- acp/agent/implementations/debug_server/__init__.py +1 -0
- acp/agent/implementations/debug_server/cli.py +79 -0
- acp/agent/implementations/debug_server/debug.html +234 -0
- acp/agent/implementations/debug_server/debug_server.py +496 -0
- acp/agent/implementations/testing.py +91 -0
- acp/agent/protocol.py +65 -0
- acp/bridge/README.md +162 -0
- acp/bridge/__init__.py +6 -0
- acp/bridge/__main__.py +91 -0
- acp/bridge/bridge.py +246 -0
- acp/bridge/py.typed +0 -0
- acp/bridge/settings.py +15 -0
- acp/client/__init__.py +7 -0
- acp/client/connection.py +251 -0
- acp/client/implementations/__init__.py +7 -0
- acp/client/implementations/default_client.py +185 -0
- acp/client/implementations/headless_client.py +266 -0
- acp/client/implementations/noop_client.py +110 -0
- acp/client/protocol.py +61 -0
- acp/connection.py +280 -0
- acp/exceptions.py +46 -0
- acp/filesystem.py +524 -0
- acp/notifications.py +832 -0
- acp/py.typed +0 -0
- acp/schema/__init__.py +265 -0
- acp/schema/agent_plan.py +30 -0
- acp/schema/agent_requests.py +126 -0
- acp/schema/agent_responses.py +256 -0
- acp/schema/base.py +39 -0
- acp/schema/capabilities.py +230 -0
- acp/schema/client_requests.py +247 -0
- acp/schema/client_responses.py +96 -0
- acp/schema/common.py +81 -0
- acp/schema/content_blocks.py +188 -0
- acp/schema/mcp.py +82 -0
- acp/schema/messages.py +171 -0
- acp/schema/notifications.py +82 -0
- acp/schema/protocol_stuff.md +3 -0
- acp/schema/session_state.py +160 -0
- acp/schema/session_updates.py +419 -0
- acp/schema/slash_commands.py +51 -0
- acp/schema/terminal.py +15 -0
- acp/schema/tool_call.py +347 -0
- acp/stdio.py +250 -0
- acp/task/__init__.py +53 -0
- acp/task/debug.py +197 -0
- acp/task/dispatcher.py +93 -0
- acp/task/queue.py +69 -0
- acp/task/sender.py +82 -0
- acp/task/state.py +87 -0
- acp/task/supervisor.py +93 -0
- acp/terminal_handle.py +30 -0
- acp/tool_call_reporter.py +199 -0
- acp/tool_call_state.py +178 -0
- acp/transports.py +104 -0
- acp/utils.py +240 -0
- agentpool/__init__.py +63 -0
- agentpool/__main__.py +7 -0
- agentpool/agents/__init__.py +30 -0
- agentpool/agents/acp_agent/__init__.py +5 -0
- agentpool/agents/acp_agent/acp_agent.py +837 -0
- agentpool/agents/acp_agent/acp_converters.py +294 -0
- agentpool/agents/acp_agent/client_handler.py +317 -0
- agentpool/agents/acp_agent/session_state.py +44 -0
- agentpool/agents/agent.py +1264 -0
- agentpool/agents/agui_agent/__init__.py +19 -0
- agentpool/agents/agui_agent/agui_agent.py +677 -0
- agentpool/agents/agui_agent/agui_converters.py +423 -0
- agentpool/agents/agui_agent/chunk_transformer.py +204 -0
- agentpool/agents/agui_agent/event_types.py +83 -0
- agentpool/agents/agui_agent/helpers.py +192 -0
- agentpool/agents/architect.py +71 -0
- agentpool/agents/base_agent.py +177 -0
- agentpool/agents/claude_code_agent/__init__.py +11 -0
- agentpool/agents/claude_code_agent/claude_code_agent.py +1021 -0
- agentpool/agents/claude_code_agent/converters.py +243 -0
- agentpool/agents/context.py +105 -0
- agentpool/agents/events/__init__.py +61 -0
- agentpool/agents/events/builtin_handlers.py +129 -0
- agentpool/agents/events/event_emitter.py +320 -0
- agentpool/agents/events/events.py +561 -0
- agentpool/agents/events/tts_handlers.py +186 -0
- agentpool/agents/interactions.py +419 -0
- agentpool/agents/slashed_agent.py +244 -0
- agentpool/agents/sys_prompts.py +178 -0
- agentpool/agents/tool_wrapping.py +184 -0
- agentpool/base_provider.py +28 -0
- agentpool/common_types.py +226 -0
- agentpool/config_resources/__init__.py +16 -0
- agentpool/config_resources/acp_assistant.yml +24 -0
- agentpool/config_resources/agents.yml +109 -0
- agentpool/config_resources/agents_template.yml +18 -0
- agentpool/config_resources/agui_test.yml +18 -0
- agentpool/config_resources/claude_code_agent.yml +16 -0
- agentpool/config_resources/claude_style_subagent.md +30 -0
- agentpool/config_resources/external_acp_agents.yml +77 -0
- agentpool/config_resources/opencode_style_subagent.md +19 -0
- agentpool/config_resources/tts_test_agents.yml +78 -0
- agentpool/delegation/__init__.py +8 -0
- agentpool/delegation/base_team.py +504 -0
- agentpool/delegation/message_flow_tracker.py +39 -0
- agentpool/delegation/pool.py +1129 -0
- agentpool/delegation/team.py +325 -0
- agentpool/delegation/teamrun.py +343 -0
- agentpool/docs/__init__.py +5 -0
- agentpool/docs/gen_examples.py +42 -0
- agentpool/docs/utils.py +370 -0
- agentpool/functional/__init__.py +20 -0
- agentpool/functional/py.typed +0 -0
- agentpool/functional/run.py +80 -0
- agentpool/functional/structure.py +136 -0
- agentpool/hooks/__init__.py +20 -0
- agentpool/hooks/agent_hooks.py +247 -0
- agentpool/hooks/base.py +119 -0
- agentpool/hooks/callable.py +140 -0
- agentpool/hooks/command.py +180 -0
- agentpool/hooks/prompt.py +122 -0
- agentpool/jinja_filters.py +132 -0
- agentpool/log.py +224 -0
- agentpool/mcp_server/__init__.py +17 -0
- agentpool/mcp_server/client.py +429 -0
- agentpool/mcp_server/constants.py +32 -0
- agentpool/mcp_server/conversions.py +172 -0
- agentpool/mcp_server/helpers.py +47 -0
- agentpool/mcp_server/manager.py +232 -0
- agentpool/mcp_server/message_handler.py +164 -0
- agentpool/mcp_server/registries/__init__.py +1 -0
- agentpool/mcp_server/registries/official_registry_client.py +345 -0
- agentpool/mcp_server/registries/pulsemcp_client.py +88 -0
- agentpool/mcp_server/tool_bridge.py +548 -0
- agentpool/messaging/__init__.py +58 -0
- agentpool/messaging/compaction.py +928 -0
- agentpool/messaging/connection_manager.py +319 -0
- agentpool/messaging/context.py +66 -0
- agentpool/messaging/event_manager.py +426 -0
- agentpool/messaging/events.py +39 -0
- agentpool/messaging/message_container.py +209 -0
- agentpool/messaging/message_history.py +491 -0
- agentpool/messaging/messagenode.py +377 -0
- agentpool/messaging/messages.py +655 -0
- agentpool/messaging/processing.py +76 -0
- agentpool/mime_utils.py +95 -0
- agentpool/models/__init__.py +21 -0
- agentpool/models/acp_agents/__init__.py +22 -0
- agentpool/models/acp_agents/base.py +308 -0
- agentpool/models/acp_agents/mcp_capable.py +790 -0
- agentpool/models/acp_agents/non_mcp.py +842 -0
- agentpool/models/agents.py +450 -0
- agentpool/models/agui_agents.py +89 -0
- agentpool/models/claude_code_agents.py +238 -0
- agentpool/models/file_agents.py +116 -0
- agentpool/models/file_parsing.py +367 -0
- agentpool/models/manifest.py +658 -0
- agentpool/observability/__init__.py +9 -0
- agentpool/observability/observability_registry.py +97 -0
- agentpool/prompts/__init__.py +1 -0
- agentpool/prompts/base.py +27 -0
- agentpool/prompts/builtin_provider.py +75 -0
- agentpool/prompts/conversion_manager.py +95 -0
- agentpool/prompts/convert.py +96 -0
- agentpool/prompts/manager.py +204 -0
- agentpool/prompts/parts/zed.md +33 -0
- agentpool/prompts/prompts.py +581 -0
- agentpool/py.typed +0 -0
- agentpool/queries/tree-sitter-language-pack/README.md +7 -0
- agentpool/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- agentpool/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- agentpool/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- agentpool/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
- agentpool/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- agentpool/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- agentpool/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- agentpool/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- agentpool/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- agentpool/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- agentpool/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- agentpool/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- agentpool/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- agentpool/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- agentpool/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- agentpool/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- agentpool/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- agentpool/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- agentpool/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- agentpool/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- agentpool/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- agentpool/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- agentpool/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- agentpool/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- agentpool/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- agentpool/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- agentpool/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- agentpool/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- agentpool/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- agentpool/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- agentpool/queries/tree-sitter-languages/README.md +24 -0
- agentpool/queries/tree-sitter-languages/c-tags.scm +9 -0
- agentpool/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- agentpool/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- agentpool/queries/tree-sitter-languages/dart-tags.scm +91 -0
- agentpool/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- agentpool/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- agentpool/queries/tree-sitter-languages/elm-tags.scm +19 -0
- agentpool/queries/tree-sitter-languages/fortran-tags.scm +15 -0
- agentpool/queries/tree-sitter-languages/go-tags.scm +30 -0
- agentpool/queries/tree-sitter-languages/haskell-tags.scm +3 -0
- agentpool/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- agentpool/queries/tree-sitter-languages/java-tags.scm +20 -0
- agentpool/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- agentpool/queries/tree-sitter-languages/julia-tags.scm +60 -0
- agentpool/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- agentpool/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- agentpool/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- agentpool/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- agentpool/queries/tree-sitter-languages/php-tags.scm +26 -0
- agentpool/queries/tree-sitter-languages/python-tags.scm +12 -0
- agentpool/queries/tree-sitter-languages/ql-tags.scm +26 -0
- agentpool/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- agentpool/queries/tree-sitter-languages/rust-tags.scm +60 -0
- agentpool/queries/tree-sitter-languages/scala-tags.scm +65 -0
- agentpool/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- agentpool/queries/tree-sitter-languages/zig-tags.scm +3 -0
- agentpool/repomap.py +1231 -0
- agentpool/resource_providers/__init__.py +17 -0
- agentpool/resource_providers/aggregating.py +54 -0
- agentpool/resource_providers/base.py +172 -0
- agentpool/resource_providers/codemode/__init__.py +9 -0
- agentpool/resource_providers/codemode/code_executor.py +215 -0
- agentpool/resource_providers/codemode/default_prompt.py +19 -0
- agentpool/resource_providers/codemode/helpers.py +83 -0
- agentpool/resource_providers/codemode/progress_executor.py +212 -0
- agentpool/resource_providers/codemode/provider.py +150 -0
- agentpool/resource_providers/codemode/remote_mcp_execution.py +143 -0
- agentpool/resource_providers/codemode/remote_provider.py +171 -0
- agentpool/resource_providers/filtering.py +42 -0
- agentpool/resource_providers/mcp_provider.py +246 -0
- agentpool/resource_providers/plan_provider.py +196 -0
- agentpool/resource_providers/pool.py +69 -0
- agentpool/resource_providers/static.py +289 -0
- agentpool/running/__init__.py +20 -0
- agentpool/running/decorators.py +56 -0
- agentpool/running/discovery.py +101 -0
- agentpool/running/executor.py +284 -0
- agentpool/running/injection.py +111 -0
- agentpool/running/py.typed +0 -0
- agentpool/running/run_nodes.py +87 -0
- agentpool/server.py +122 -0
- agentpool/sessions/__init__.py +13 -0
- agentpool/sessions/manager.py +302 -0
- agentpool/sessions/models.py +71 -0
- agentpool/sessions/session.py +239 -0
- agentpool/sessions/store.py +163 -0
- agentpool/skills/__init__.py +5 -0
- agentpool/skills/manager.py +120 -0
- agentpool/skills/registry.py +210 -0
- agentpool/skills/skill.py +36 -0
- agentpool/storage/__init__.py +17 -0
- agentpool/storage/manager.py +419 -0
- agentpool/storage/serialization.py +136 -0
- agentpool/talk/__init__.py +13 -0
- agentpool/talk/registry.py +128 -0
- agentpool/talk/stats.py +159 -0
- agentpool/talk/talk.py +604 -0
- agentpool/tasks/__init__.py +20 -0
- agentpool/tasks/exceptions.py +25 -0
- agentpool/tasks/registry.py +33 -0
- agentpool/testing.py +129 -0
- agentpool/text_templates/__init__.py +39 -0
- agentpool/text_templates/system_prompt.jinja +30 -0
- agentpool/text_templates/tool_call_default.jinja +13 -0
- agentpool/text_templates/tool_call_markdown.jinja +25 -0
- agentpool/text_templates/tool_call_simple.jinja +5 -0
- agentpool/tools/__init__.py +16 -0
- agentpool/tools/base.py +269 -0
- agentpool/tools/exceptions.py +9 -0
- agentpool/tools/manager.py +255 -0
- agentpool/tools/tool_call_info.py +87 -0
- agentpool/ui/__init__.py +2 -0
- agentpool/ui/base.py +89 -0
- agentpool/ui/mock_provider.py +81 -0
- agentpool/ui/stdlib_provider.py +150 -0
- agentpool/utils/__init__.py +44 -0
- agentpool/utils/baseregistry.py +185 -0
- agentpool/utils/count_tokens.py +62 -0
- agentpool/utils/dag.py +184 -0
- agentpool/utils/importing.py +206 -0
- agentpool/utils/inspection.py +334 -0
- agentpool/utils/model_capabilities.py +25 -0
- agentpool/utils/network.py +28 -0
- agentpool/utils/now.py +22 -0
- agentpool/utils/parse_time.py +87 -0
- agentpool/utils/result_utils.py +35 -0
- agentpool/utils/signatures.py +305 -0
- agentpool/utils/streams.py +112 -0
- agentpool/utils/tasks.py +186 -0
- agentpool/vfs_registry.py +250 -0
- agentpool-2.1.9.dist-info/METADATA +336 -0
- agentpool-2.1.9.dist-info/RECORD +474 -0
- agentpool-2.1.9.dist-info/WHEEL +4 -0
- agentpool-2.1.9.dist-info/entry_points.txt +14 -0
- agentpool-2.1.9.dist-info/licenses/LICENSE +22 -0
- agentpool_cli/__init__.py +34 -0
- agentpool_cli/__main__.py +66 -0
- agentpool_cli/agent.py +175 -0
- agentpool_cli/cli_types.py +23 -0
- agentpool_cli/common.py +163 -0
- agentpool_cli/create.py +175 -0
- agentpool_cli/history.py +217 -0
- agentpool_cli/log.py +78 -0
- agentpool_cli/py.typed +0 -0
- agentpool_cli/run.py +84 -0
- agentpool_cli/serve_acp.py +177 -0
- agentpool_cli/serve_api.py +69 -0
- agentpool_cli/serve_mcp.py +74 -0
- agentpool_cli/serve_vercel.py +233 -0
- agentpool_cli/store.py +171 -0
- agentpool_cli/task.py +84 -0
- agentpool_cli/utils.py +104 -0
- agentpool_cli/watch.py +54 -0
- agentpool_commands/__init__.py +180 -0
- agentpool_commands/agents.py +199 -0
- agentpool_commands/base.py +45 -0
- agentpool_commands/commands.py +58 -0
- agentpool_commands/completers.py +110 -0
- agentpool_commands/connections.py +175 -0
- agentpool_commands/markdown_utils.py +31 -0
- agentpool_commands/models.py +62 -0
- agentpool_commands/prompts.py +78 -0
- agentpool_commands/py.typed +0 -0
- agentpool_commands/read.py +77 -0
- agentpool_commands/resources.py +210 -0
- agentpool_commands/session.py +48 -0
- agentpool_commands/tools.py +269 -0
- agentpool_commands/utils.py +189 -0
- agentpool_commands/workers.py +163 -0
- agentpool_config/__init__.py +53 -0
- agentpool_config/builtin_tools.py +265 -0
- agentpool_config/commands.py +237 -0
- agentpool_config/conditions.py +301 -0
- agentpool_config/converters.py +30 -0
- agentpool_config/durable.py +331 -0
- agentpool_config/event_handlers.py +600 -0
- agentpool_config/events.py +153 -0
- agentpool_config/forward_targets.py +251 -0
- agentpool_config/hook_conditions.py +331 -0
- agentpool_config/hooks.py +241 -0
- agentpool_config/jinja.py +206 -0
- agentpool_config/knowledge.py +41 -0
- agentpool_config/loaders.py +350 -0
- agentpool_config/mcp_server.py +243 -0
- agentpool_config/nodes.py +202 -0
- agentpool_config/observability.py +191 -0
- agentpool_config/output_types.py +55 -0
- agentpool_config/pool_server.py +267 -0
- agentpool_config/prompt_hubs.py +105 -0
- agentpool_config/prompts.py +185 -0
- agentpool_config/py.typed +0 -0
- agentpool_config/resources.py +33 -0
- agentpool_config/session.py +119 -0
- agentpool_config/skills.py +17 -0
- agentpool_config/storage.py +288 -0
- agentpool_config/system_prompts.py +190 -0
- agentpool_config/task.py +162 -0
- agentpool_config/teams.py +52 -0
- agentpool_config/tools.py +112 -0
- agentpool_config/toolsets.py +1033 -0
- agentpool_config/workers.py +86 -0
- agentpool_prompts/__init__.py +1 -0
- agentpool_prompts/braintrust_hub.py +235 -0
- agentpool_prompts/fabric.py +75 -0
- agentpool_prompts/langfuse_hub.py +79 -0
- agentpool_prompts/promptlayer_provider.py +59 -0
- agentpool_prompts/py.typed +0 -0
- agentpool_server/__init__.py +9 -0
- agentpool_server/a2a_server/__init__.py +5 -0
- agentpool_server/a2a_server/a2a_types.py +41 -0
- agentpool_server/a2a_server/server.py +190 -0
- agentpool_server/a2a_server/storage.py +81 -0
- agentpool_server/acp_server/__init__.py +22 -0
- agentpool_server/acp_server/acp_agent.py +786 -0
- agentpool_server/acp_server/acp_tools.py +43 -0
- agentpool_server/acp_server/commands/__init__.py +18 -0
- agentpool_server/acp_server/commands/acp_commands.py +594 -0
- agentpool_server/acp_server/commands/debug_commands.py +376 -0
- agentpool_server/acp_server/commands/docs_commands/__init__.py +39 -0
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +169 -0
- agentpool_server/acp_server/commands/docs_commands/get_schema.py +176 -0
- agentpool_server/acp_server/commands/docs_commands/get_source.py +110 -0
- agentpool_server/acp_server/commands/docs_commands/git_diff.py +111 -0
- agentpool_server/acp_server/commands/docs_commands/helpers.py +33 -0
- agentpool_server/acp_server/commands/docs_commands/url_to_markdown.py +90 -0
- agentpool_server/acp_server/commands/spawn.py +210 -0
- agentpool_server/acp_server/converters.py +235 -0
- agentpool_server/acp_server/input_provider.py +338 -0
- agentpool_server/acp_server/server.py +288 -0
- agentpool_server/acp_server/session.py +969 -0
- agentpool_server/acp_server/session_manager.py +313 -0
- agentpool_server/acp_server/syntax_detection.py +250 -0
- agentpool_server/acp_server/zed_tools.md +90 -0
- agentpool_server/aggregating_server.py +309 -0
- agentpool_server/agui_server/__init__.py +11 -0
- agentpool_server/agui_server/server.py +128 -0
- agentpool_server/base.py +189 -0
- agentpool_server/http_server.py +164 -0
- agentpool_server/mcp_server/__init__.py +6 -0
- agentpool_server/mcp_server/server.py +314 -0
- agentpool_server/mcp_server/zed_wrapper.py +110 -0
- agentpool_server/openai_api_server/__init__.py +5 -0
- agentpool_server/openai_api_server/completions/__init__.py +1 -0
- agentpool_server/openai_api_server/completions/helpers.py +81 -0
- agentpool_server/openai_api_server/completions/models.py +98 -0
- agentpool_server/openai_api_server/responses/__init__.py +1 -0
- agentpool_server/openai_api_server/responses/helpers.py +74 -0
- agentpool_server/openai_api_server/responses/models.py +96 -0
- agentpool_server/openai_api_server/server.py +242 -0
- agentpool_server/py.typed +0 -0
- agentpool_storage/__init__.py +9 -0
- agentpool_storage/base.py +310 -0
- agentpool_storage/file_provider.py +378 -0
- agentpool_storage/formatters.py +129 -0
- agentpool_storage/memory_provider.py +396 -0
- agentpool_storage/models.py +108 -0
- agentpool_storage/py.typed +0 -0
- agentpool_storage/session_store.py +262 -0
- agentpool_storage/sql_provider/__init__.py +21 -0
- agentpool_storage/sql_provider/cli.py +146 -0
- agentpool_storage/sql_provider/models.py +249 -0
- agentpool_storage/sql_provider/queries.py +15 -0
- agentpool_storage/sql_provider/sql_provider.py +444 -0
- agentpool_storage/sql_provider/utils.py +234 -0
- agentpool_storage/text_log_provider.py +275 -0
- agentpool_toolsets/__init__.py +15 -0
- agentpool_toolsets/builtin/__init__.py +33 -0
- agentpool_toolsets/builtin/agent_management.py +239 -0
- agentpool_toolsets/builtin/chain.py +288 -0
- agentpool_toolsets/builtin/code.py +398 -0
- agentpool_toolsets/builtin/debug.py +291 -0
- agentpool_toolsets/builtin/execution_environment.py +381 -0
- agentpool_toolsets/builtin/file_edit/__init__.py +11 -0
- agentpool_toolsets/builtin/file_edit/file_edit.py +747 -0
- agentpool_toolsets/builtin/file_edit/fuzzy_matcher/__init__.py +5 -0
- agentpool_toolsets/builtin/file_edit/fuzzy_matcher/example_usage.py +311 -0
- agentpool_toolsets/builtin/file_edit/fuzzy_matcher/streaming_fuzzy_matcher.py +443 -0
- agentpool_toolsets/builtin/history.py +36 -0
- agentpool_toolsets/builtin/integration.py +85 -0
- agentpool_toolsets/builtin/skills.py +77 -0
- agentpool_toolsets/builtin/subagent_tools.py +324 -0
- agentpool_toolsets/builtin/tool_management.py +90 -0
- agentpool_toolsets/builtin/user_interaction.py +52 -0
- agentpool_toolsets/builtin/workers.py +128 -0
- agentpool_toolsets/composio_toolset.py +96 -0
- agentpool_toolsets/config_creation.py +192 -0
- agentpool_toolsets/entry_points.py +47 -0
- agentpool_toolsets/fsspec_toolset/__init__.py +7 -0
- agentpool_toolsets/fsspec_toolset/diagnostics.py +115 -0
- agentpool_toolsets/fsspec_toolset/grep.py +450 -0
- agentpool_toolsets/fsspec_toolset/helpers.py +631 -0
- agentpool_toolsets/fsspec_toolset/streaming_diff_parser.py +249 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +1384 -0
- agentpool_toolsets/mcp_run_toolset.py +61 -0
- agentpool_toolsets/notifications.py +146 -0
- agentpool_toolsets/openapi.py +118 -0
- agentpool_toolsets/py.typed +0 -0
- agentpool_toolsets/search_toolset.py +202 -0
- agentpool_toolsets/semantic_memory_toolset.py +536 -0
- agentpool_toolsets/streaming_tools.py +265 -0
- agentpool_toolsets/vfs_toolset.py +124 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
"""Sophisticated file editing tool with multiple replacement strategies.
|
|
2
|
+
|
|
3
|
+
This module implements a robust file editing tool that uses multiple fallback
|
|
4
|
+
strategies for finding and replacing text, similar to OpenCode's edit tool.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import difflib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import re
|
|
12
|
+
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field, field_validator
|
|
15
|
+
|
|
16
|
+
from agentpool.tools.base import Tool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Generator
|
|
21
|
+
|
|
22
|
+
from agentpool.agents import AgentContext
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FuzzyMatch(NamedTuple):
|
|
26
|
+
"""Result of fuzzy text matching."""
|
|
27
|
+
|
|
28
|
+
similarity: float
|
|
29
|
+
start_line: int
|
|
30
|
+
end_line: int
|
|
31
|
+
text: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EditParams(BaseModel):
|
|
35
|
+
"""Parameters for the edit tool."""
|
|
36
|
+
|
|
37
|
+
file_path: str = Field(description="The path to the file to modify")
|
|
38
|
+
old_string: str = Field(description="The text to replace")
|
|
39
|
+
new_string: str = Field(description="The text to replace it with")
|
|
40
|
+
replace_all: bool = Field(
|
|
41
|
+
default=False, description="Replace all occurrences of old_string (default false)"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@field_validator("file_path")
|
|
45
|
+
@classmethod
|
|
46
|
+
def validate_file_path(cls, v: str) -> str:
|
|
47
|
+
"""Validate file path is not empty."""
|
|
48
|
+
if not v.strip():
|
|
49
|
+
msg = "file_path cannot be empty"
|
|
50
|
+
raise ValueError(msg)
|
|
51
|
+
return v
|
|
52
|
+
|
|
53
|
+
def model_post_init(self, __context: Any, /) -> None:
|
|
54
|
+
"""Post-initialization validation."""
|
|
55
|
+
if self.old_string == self.new_string:
|
|
56
|
+
msg = "old_string and new_string must be different"
|
|
57
|
+
raise ValueError(msg)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _levenshtein_distance(a: str, b: str, /) -> int:
|
|
61
|
+
"""Calculate Levenshtein distance between two strings."""
|
|
62
|
+
if not a:
|
|
63
|
+
return len(b)
|
|
64
|
+
if not b:
|
|
65
|
+
return len(a)
|
|
66
|
+
|
|
67
|
+
matrix = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
|
|
68
|
+
|
|
69
|
+
for i in range(len(a) + 1):
|
|
70
|
+
matrix[i][0] = i
|
|
71
|
+
for j in range(len(b) + 1):
|
|
72
|
+
matrix[0][j] = j
|
|
73
|
+
|
|
74
|
+
for i in range(1, len(a) + 1):
|
|
75
|
+
for j in range(1, len(b) + 1):
|
|
76
|
+
cost = 0 if a[i - 1] == b[j - 1] else 1
|
|
77
|
+
matrix[i][j] = min(
|
|
78
|
+
matrix[i - 1][j] + 1, # deletion
|
|
79
|
+
matrix[i][j - 1] + 1, # insertion
|
|
80
|
+
matrix[i - 1][j - 1] + cost, # substitution
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return matrix[len(a)][len(b)]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _simple_replacer(content: str, find: str) -> Generator[str]:
|
|
87
|
+
"""Direct string matching replacer."""
|
|
88
|
+
if find in content:
|
|
89
|
+
yield find
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _line_trimmed_replacer(content: str, find: str) -> Generator[str]:
|
|
93
|
+
"""Line-by-line matching with trimmed whitespace."""
|
|
94
|
+
original_lines = content.split("\n")
|
|
95
|
+
search_lines = find.split("\n")
|
|
96
|
+
|
|
97
|
+
# Remove trailing empty line if present
|
|
98
|
+
if search_lines and search_lines[-1] == "":
|
|
99
|
+
search_lines.pop()
|
|
100
|
+
|
|
101
|
+
for i in range(len(original_lines) - len(search_lines) + 1):
|
|
102
|
+
matches = True
|
|
103
|
+
|
|
104
|
+
for j in range(len(search_lines)):
|
|
105
|
+
if i + j >= len(original_lines):
|
|
106
|
+
matches = False
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
original_trimmed = original_lines[i + j].strip()
|
|
110
|
+
search_trimmed = search_lines[j].strip()
|
|
111
|
+
|
|
112
|
+
if original_trimmed != search_trimmed:
|
|
113
|
+
matches = False
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
if matches:
|
|
117
|
+
# Calculate the actual substring in the original content
|
|
118
|
+
start_index = sum(len(original_lines[k]) + 1 for k in range(i))
|
|
119
|
+
end_index = start_index
|
|
120
|
+
|
|
121
|
+
for k in range(len(search_lines)):
|
|
122
|
+
end_index += len(original_lines[i + k])
|
|
123
|
+
if k < len(search_lines) - 1:
|
|
124
|
+
end_index += 1 # newline
|
|
125
|
+
|
|
126
|
+
yield content[start_index:end_index]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _calculate_block_similarity(
|
|
130
|
+
original_lines: list[str],
|
|
131
|
+
search_lines: list[str],
|
|
132
|
+
start_line: int,
|
|
133
|
+
end_line: int,
|
|
134
|
+
search_block_size: int,
|
|
135
|
+
) -> float:
|
|
136
|
+
"""Calculate similarity between search block and candidate block."""
|
|
137
|
+
actual_block_size = end_line - start_line + 1
|
|
138
|
+
lines_to_check = min(search_block_size - 2, actual_block_size - 2)
|
|
139
|
+
|
|
140
|
+
if lines_to_check <= 0:
|
|
141
|
+
return 1.0
|
|
142
|
+
|
|
143
|
+
similarity = 0.0
|
|
144
|
+
for j in range(1, min(search_block_size - 1, actual_block_size - 1)):
|
|
145
|
+
original_line = original_lines[start_line + j].strip()
|
|
146
|
+
search_line = search_lines[j].strip()
|
|
147
|
+
max_len = max(len(original_line), len(search_line))
|
|
148
|
+
|
|
149
|
+
if max_len == 0:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
distance = _levenshtein_distance(original_line, search_line)
|
|
153
|
+
similarity += (1 - distance / max_len) / lines_to_check
|
|
154
|
+
|
|
155
|
+
return similarity
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _block_anchor_replacer(content: str, find: str) -> Generator[str]:
|
|
159
|
+
"""Multi-line block matching using first/last line anchors with similarity scoring."""
|
|
160
|
+
single_candidate_threshold = 0.0
|
|
161
|
+
multiple_candidates_threshold = 0.3
|
|
162
|
+
min_lines_for_block = 3
|
|
163
|
+
|
|
164
|
+
original_lines = content.split("\n")
|
|
165
|
+
search_lines = find.split("\n")
|
|
166
|
+
|
|
167
|
+
if len(search_lines) < min_lines_for_block:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Remove trailing empty line if present
|
|
171
|
+
if search_lines and search_lines[-1] == "":
|
|
172
|
+
search_lines.pop()
|
|
173
|
+
|
|
174
|
+
first_line_search = search_lines[0].strip()
|
|
175
|
+
last_line_search = search_lines[-1].strip()
|
|
176
|
+
search_block_size = len(search_lines)
|
|
177
|
+
|
|
178
|
+
# Find all candidate positions
|
|
179
|
+
candidates: list[tuple[int, int]] = []
|
|
180
|
+
for i in range(len(original_lines)):
|
|
181
|
+
if original_lines[i].strip() != first_line_search:
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
# Look for matching last line
|
|
185
|
+
for j in range(i + 2, len(original_lines)):
|
|
186
|
+
if original_lines[j].strip() == last_line_search:
|
|
187
|
+
candidates.append((i, j))
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
if not candidates:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
if len(candidates) == 1:
|
|
194
|
+
start_line, end_line = candidates[0]
|
|
195
|
+
similarity = _calculate_block_similarity(
|
|
196
|
+
original_lines, search_lines, start_line, end_line, search_block_size
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if similarity >= single_candidate_threshold:
|
|
200
|
+
start_index = sum(len(original_lines[k]) + 1 for k in range(start_line))
|
|
201
|
+
end_index = start_index + sum(
|
|
202
|
+
len(original_lines[k]) + (1 if k < end_line else 0)
|
|
203
|
+
for k in range(start_line, end_line + 1)
|
|
204
|
+
)
|
|
205
|
+
yield content[start_index:end_index]
|
|
206
|
+
else:
|
|
207
|
+
# Multiple candidates - find best match
|
|
208
|
+
best_match = None
|
|
209
|
+
max_similarity = -1.0
|
|
210
|
+
|
|
211
|
+
for start_line, end_line in candidates:
|
|
212
|
+
similarity = _calculate_block_similarity(
|
|
213
|
+
original_lines, search_lines, start_line, end_line, search_block_size
|
|
214
|
+
)
|
|
215
|
+
if similarity > max_similarity:
|
|
216
|
+
max_similarity = similarity
|
|
217
|
+
best_match = (start_line, end_line)
|
|
218
|
+
|
|
219
|
+
if max_similarity >= multiple_candidates_threshold and best_match:
|
|
220
|
+
start_line, end_line = best_match
|
|
221
|
+
start_index = sum(len(original_lines[k]) + 1 for k in range(start_line))
|
|
222
|
+
end_index = start_index + sum(
|
|
223
|
+
len(original_lines[k]) + (1 if k < end_line else 0)
|
|
224
|
+
for k in range(start_line, end_line + 1)
|
|
225
|
+
)
|
|
226
|
+
yield content[start_index:end_index]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _whitespace_normalized_replacer(content: str, find: str) -> Generator[str]:
|
|
230
|
+
"""Whitespace-normalized matching replacer."""
|
|
231
|
+
|
|
232
|
+
def normalize_whitespace(text: str) -> str:
|
|
233
|
+
# Normalize multiple whitespace to single space
|
|
234
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
235
|
+
# Normalize spacing around common punctuation
|
|
236
|
+
# Normalize spacing around common punctuation
|
|
237
|
+
replacements = [
|
|
238
|
+
(r"\s*:\s*", ":"),
|
|
239
|
+
(r"\s*;\s*", ";"),
|
|
240
|
+
(r"\s*,\s*", ","),
|
|
241
|
+
(r"\s*\(\s*", "("),
|
|
242
|
+
(r"\s*\)\s*", ")"),
|
|
243
|
+
(r"\s*\[\s*", "["),
|
|
244
|
+
(r"\s*\]\s*", "]"),
|
|
245
|
+
(r"\s*\{\s*", "{"),
|
|
246
|
+
(r"\s*\}\s*", "}"),
|
|
247
|
+
]
|
|
248
|
+
for pattern, replacement in replacements:
|
|
249
|
+
text = re.sub(pattern, replacement, text)
|
|
250
|
+
return text
|
|
251
|
+
|
|
252
|
+
normalized_find = normalize_whitespace(find)
|
|
253
|
+
found_matches = set() # Track matches to avoid duplicates
|
|
254
|
+
|
|
255
|
+
# Try to match the entire content first
|
|
256
|
+
if normalize_whitespace(content) == normalized_find and content not in found_matches:
|
|
257
|
+
found_matches.add(content)
|
|
258
|
+
yield content
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
lines = content.split("\n")
|
|
262
|
+
find_lines = find.split("\n")
|
|
263
|
+
|
|
264
|
+
# Multi-line matches
|
|
265
|
+
if len(find_lines) > 1:
|
|
266
|
+
for i in range(len(lines) - len(find_lines) + 1):
|
|
267
|
+
block = lines[i : i + len(find_lines)]
|
|
268
|
+
block_content = "\n".join(block)
|
|
269
|
+
if (
|
|
270
|
+
normalize_whitespace(block_content) == normalized_find
|
|
271
|
+
and block_content not in found_matches
|
|
272
|
+
):
|
|
273
|
+
found_matches.add(block_content)
|
|
274
|
+
yield block_content
|
|
275
|
+
else:
|
|
276
|
+
# Single line matches
|
|
277
|
+
for line in lines:
|
|
278
|
+
if normalize_whitespace(line) == normalized_find and line not in found_matches:
|
|
279
|
+
found_matches.add(line)
|
|
280
|
+
yield line
|
|
281
|
+
else:
|
|
282
|
+
# Check for substring matches
|
|
283
|
+
normalized_line = normalize_whitespace(line)
|
|
284
|
+
if normalized_find in normalized_line:
|
|
285
|
+
# Find actual substring using regex
|
|
286
|
+
words = find.strip().split()
|
|
287
|
+
if words:
|
|
288
|
+
pattern = r"\s+".join(re.escape(word) for word in words)
|
|
289
|
+
match = re.search(pattern, line)
|
|
290
|
+
if match and match.group(0) not in found_matches:
|
|
291
|
+
found_matches.add(match.group(0))
|
|
292
|
+
yield match.group(0)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _indentation_flexible_replacer(content: str, find: str) -> Generator[str]:
|
|
296
|
+
"""Indentation-flexible matching replacer."""
|
|
297
|
+
|
|
298
|
+
def remove_common_indentation(text: str) -> str:
|
|
299
|
+
lines = text.split("\n")
|
|
300
|
+
non_empty_lines = [line for line in lines if line.strip()]
|
|
301
|
+
|
|
302
|
+
if not non_empty_lines:
|
|
303
|
+
return text
|
|
304
|
+
|
|
305
|
+
min_indent = min(len(line) - len(line.lstrip()) for line in non_empty_lines)
|
|
306
|
+
|
|
307
|
+
return "\n".join(line[min_indent:] if line.strip() else line for line in lines)
|
|
308
|
+
|
|
309
|
+
normalized_find = remove_common_indentation(find)
|
|
310
|
+
content_lines = content.split("\n")
|
|
311
|
+
find_lines = find.split("\n")
|
|
312
|
+
|
|
313
|
+
for i in range(len(content_lines) - len(find_lines) + 1):
|
|
314
|
+
block = "\n".join(content_lines[i : i + len(find_lines)])
|
|
315
|
+
if remove_common_indentation(block) == normalized_find:
|
|
316
|
+
yield block
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _escape_normalized_replacer(content: str, find: str) -> Generator[str]:
|
|
320
|
+
"""Escape sequence normalized matching replacer."""
|
|
321
|
+
|
|
322
|
+
def unescape_string(text: str) -> str:
|
|
323
|
+
# Handle common escape sequences
|
|
324
|
+
# Handle common escape sequences
|
|
325
|
+
replacements = [
|
|
326
|
+
("\\n", "\n"),
|
|
327
|
+
("\\t", "\t"),
|
|
328
|
+
("\\r", "\r"),
|
|
329
|
+
("\\'", "'"),
|
|
330
|
+
('\\"', '"'),
|
|
331
|
+
("\\`", "`"),
|
|
332
|
+
("\\\\", "\\"),
|
|
333
|
+
("\\$", "$"),
|
|
334
|
+
]
|
|
335
|
+
for escaped, unescaped in replacements:
|
|
336
|
+
text = text.replace(escaped, unescaped)
|
|
337
|
+
return text
|
|
338
|
+
|
|
339
|
+
unescaped_find = unescape_string(find)
|
|
340
|
+
|
|
341
|
+
# Try direct match with unescaped find
|
|
342
|
+
if unescaped_find in content:
|
|
343
|
+
yield unescaped_find
|
|
344
|
+
|
|
345
|
+
# Look for content that when unescaped matches our find string
|
|
346
|
+
# This handles the case where content has escaped chars but find doesn't
|
|
347
|
+
for i in range(len(content) - len(find) + 1):
|
|
348
|
+
for length in range(len(find), min(len(content) - i + 1, len(find) * 3)):
|
|
349
|
+
substring = content[i : i + length]
|
|
350
|
+
if unescape_string(substring) == unescaped_find:
|
|
351
|
+
yield substring
|
|
352
|
+
break
|
|
353
|
+
|
|
354
|
+
# Try finding escaped versions in content line by line
|
|
355
|
+
lines = content.split("\n")
|
|
356
|
+
find_lines = unescaped_find.split("\n")
|
|
357
|
+
|
|
358
|
+
for i in range(len(lines) - len(find_lines) + 1):
|
|
359
|
+
block = "\n".join(lines[i : i + len(find_lines)])
|
|
360
|
+
if unescape_string(block) == unescaped_find:
|
|
361
|
+
yield block
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _trimmed_boundary_replacer(content: str, find: str) -> Generator[str]:
|
|
365
|
+
"""Trimmed boundary matching replacer."""
|
|
366
|
+
trimmed_find = find.strip()
|
|
367
|
+
|
|
368
|
+
if trimmed_find == find:
|
|
369
|
+
return # Already trimmed
|
|
370
|
+
|
|
371
|
+
# Try trimmed version
|
|
372
|
+
if trimmed_find in content:
|
|
373
|
+
yield trimmed_find
|
|
374
|
+
|
|
375
|
+
# Try finding blocks where trimmed content matches
|
|
376
|
+
lines = content.split("\n")
|
|
377
|
+
find_lines = find.split("\n")
|
|
378
|
+
|
|
379
|
+
for i in range(len(lines) - len(find_lines) + 1):
|
|
380
|
+
block = "\n".join(lines[i : i + len(find_lines)])
|
|
381
|
+
if block.strip() == trimmed_find:
|
|
382
|
+
yield block
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _context_aware_replacer(content: str, find: str) -> Generator[str]:
|
|
386
|
+
"""Context-aware matching using anchor lines."""
|
|
387
|
+
find_lines = find.split("\n")
|
|
388
|
+
min_context_lines = 3
|
|
389
|
+
if len(find_lines) < min_context_lines:
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
# Remove trailing empty line if present
|
|
393
|
+
if find_lines and find_lines[-1] == "":
|
|
394
|
+
find_lines.pop()
|
|
395
|
+
|
|
396
|
+
content_lines = content.split("\n")
|
|
397
|
+
first_line = find_lines[0].strip()
|
|
398
|
+
last_line = find_lines[-1].strip()
|
|
399
|
+
|
|
400
|
+
# Find blocks with matching first and last lines
|
|
401
|
+
for i in range(len(content_lines)):
|
|
402
|
+
if content_lines[i].strip() != first_line:
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
for j in range(i + 2, len(content_lines)):
|
|
406
|
+
if content_lines[j].strip() == last_line:
|
|
407
|
+
block_lines = content_lines[i : j + 1]
|
|
408
|
+
|
|
409
|
+
# Check similarity of middle content
|
|
410
|
+
if len(block_lines) == len(find_lines):
|
|
411
|
+
matching_lines = 0
|
|
412
|
+
total_non_empty = 0
|
|
413
|
+
|
|
414
|
+
for k in range(1, len(block_lines) - 1):
|
|
415
|
+
block_line = block_lines[k].strip()
|
|
416
|
+
find_line = find_lines[k].strip()
|
|
417
|
+
|
|
418
|
+
if block_line or find_line:
|
|
419
|
+
total_non_empty += 1
|
|
420
|
+
if block_line == find_line:
|
|
421
|
+
matching_lines += 1
|
|
422
|
+
|
|
423
|
+
# Require at least 50% similarity
|
|
424
|
+
min_similarity_ratio = 0.5
|
|
425
|
+
if (
|
|
426
|
+
total_non_empty == 0
|
|
427
|
+
or matching_lines / total_non_empty >= min_similarity_ratio
|
|
428
|
+
):
|
|
429
|
+
yield "\n".join(block_lines)
|
|
430
|
+
break
|
|
431
|
+
break
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _multi_occurrence_replacer(content: str, find: str) -> Generator[str]:
|
|
435
|
+
"""Multi-occurrence replacer that yields all exact matches."""
|
|
436
|
+
start_index = 0
|
|
437
|
+
while True:
|
|
438
|
+
index = content.find(find, start_index)
|
|
439
|
+
if index == -1:
|
|
440
|
+
break
|
|
441
|
+
yield find
|
|
442
|
+
start_index = index + len(find)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _trim_diff(diff_text: str) -> str:
|
|
446
|
+
"""Trim common indentation from diff output."""
|
|
447
|
+
lines = diff_text.split("\n")
|
|
448
|
+
content_lines = [
|
|
449
|
+
line
|
|
450
|
+
for line in lines
|
|
451
|
+
if line.startswith(("+", "-", " ")) and not line.startswith(("---", "+++"))
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
if not content_lines:
|
|
455
|
+
return diff_text
|
|
456
|
+
|
|
457
|
+
# Find minimum indentation
|
|
458
|
+
min_indent = float("inf")
|
|
459
|
+
for line in content_lines:
|
|
460
|
+
content = line[1:] # Remove +/- prefix
|
|
461
|
+
if content.strip():
|
|
462
|
+
indent = len(content) - len(content.lstrip())
|
|
463
|
+
min_indent = min(min_indent, indent)
|
|
464
|
+
|
|
465
|
+
if min_indent == float("inf") or min_indent == 0:
|
|
466
|
+
return diff_text
|
|
467
|
+
|
|
468
|
+
# Trim indentation
|
|
469
|
+
trimmed_lines = []
|
|
470
|
+
for line in lines:
|
|
471
|
+
if line.startswith(("+", "-", " ")) and not line.startswith(("---", "+++")):
|
|
472
|
+
prefix = line[0]
|
|
473
|
+
content = line[1:]
|
|
474
|
+
trimmed_lines.append(prefix + content[int(min_indent) :])
|
|
475
|
+
else:
|
|
476
|
+
trimmed_lines.append(line)
|
|
477
|
+
|
|
478
|
+
return "\n".join(trimmed_lines)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def replace_content(
|
|
482
|
+
content: str, old_string: str, new_string: str, replace_all: bool = False
|
|
483
|
+
) -> str:
|
|
484
|
+
"""Replace content using multiple fallback strategies with detailed error messages."""
|
|
485
|
+
if old_string == new_string:
|
|
486
|
+
msg = "old_string and new_string must be different"
|
|
487
|
+
raise ValueError(msg)
|
|
488
|
+
|
|
489
|
+
replacers = [
|
|
490
|
+
_simple_replacer,
|
|
491
|
+
_line_trimmed_replacer,
|
|
492
|
+
_block_anchor_replacer,
|
|
493
|
+
_whitespace_normalized_replacer,
|
|
494
|
+
_indentation_flexible_replacer,
|
|
495
|
+
_escape_normalized_replacer,
|
|
496
|
+
_trimmed_boundary_replacer,
|
|
497
|
+
_context_aware_replacer,
|
|
498
|
+
_multi_occurrence_replacer,
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
found_matches = False
|
|
502
|
+
|
|
503
|
+
for replacer in replacers:
|
|
504
|
+
matches = list(replacer(content, old_string))
|
|
505
|
+
if not matches:
|
|
506
|
+
continue
|
|
507
|
+
|
|
508
|
+
found_matches = True
|
|
509
|
+
|
|
510
|
+
for search_text in matches:
|
|
511
|
+
index = content.find(search_text)
|
|
512
|
+
if index == -1:
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
if replace_all:
|
|
516
|
+
return content.replace(search_text, new_string)
|
|
517
|
+
|
|
518
|
+
# Check if there are multiple occurrences
|
|
519
|
+
last_index = content.rfind(search_text)
|
|
520
|
+
if index != last_index:
|
|
521
|
+
continue # Multiple occurrences, need more context
|
|
522
|
+
|
|
523
|
+
# Single occurrence - replace it
|
|
524
|
+
return content[:index] + new_string + content[index + len(search_text) :]
|
|
525
|
+
|
|
526
|
+
if not found_matches:
|
|
527
|
+
# Provide helpful error with fuzzy match context
|
|
528
|
+
error_msg = _build_not_found_error(content, old_string)
|
|
529
|
+
raise ValueError(error_msg)
|
|
530
|
+
|
|
531
|
+
msg = (
|
|
532
|
+
"old_string found multiple times and requires more code context "
|
|
533
|
+
"to uniquely identify the intended match"
|
|
534
|
+
)
|
|
535
|
+
raise ValueError(msg)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _find_best_fuzzy_match(
|
|
539
|
+
content: str, search_text: str, threshold: float = 0.8
|
|
540
|
+
) -> FuzzyMatch | None:
|
|
541
|
+
"""Find the best fuzzy match for search text in content."""
|
|
542
|
+
content_lines = content.split("\n")
|
|
543
|
+
search_lines = search_text.split("\n")
|
|
544
|
+
window_size = len(search_lines)
|
|
545
|
+
|
|
546
|
+
if window_size == 0:
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
# Find non-empty lines as anchors
|
|
550
|
+
non_empty_search = [line for line in search_lines if line.strip()]
|
|
551
|
+
if not non_empty_search:
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
first_anchor = non_empty_search[0]
|
|
555
|
+
|
|
556
|
+
# Find candidate starting positions
|
|
557
|
+
candidate_starts = set()
|
|
558
|
+
spread = 5
|
|
559
|
+
|
|
560
|
+
for i, line in enumerate(content_lines):
|
|
561
|
+
if first_anchor in line:
|
|
562
|
+
start_min = max(0, i - spread)
|
|
563
|
+
start_max = min(len(content_lines) - window_size + 1, i + spread + 1)
|
|
564
|
+
for s in range(start_min, start_max):
|
|
565
|
+
candidate_starts.add(s)
|
|
566
|
+
|
|
567
|
+
if not candidate_starts:
|
|
568
|
+
# Sample first 100 positions
|
|
569
|
+
max_positions = min(len(content_lines) - window_size + 1, 100)
|
|
570
|
+
candidate_starts = set(range(max_positions))
|
|
571
|
+
|
|
572
|
+
best_match = None
|
|
573
|
+
best_similarity = 0.0
|
|
574
|
+
|
|
575
|
+
for start in candidate_starts:
|
|
576
|
+
end = start + window_size
|
|
577
|
+
window_text = "\n".join(content_lines[start:end])
|
|
578
|
+
|
|
579
|
+
matcher = difflib.SequenceMatcher(None, search_text, window_text)
|
|
580
|
+
similarity = matcher.ratio()
|
|
581
|
+
|
|
582
|
+
if similarity >= threshold and similarity > best_similarity:
|
|
583
|
+
best_similarity = similarity
|
|
584
|
+
best_match = FuzzyMatch(
|
|
585
|
+
similarity=similarity,
|
|
586
|
+
start_line=start + 1, # 1-based
|
|
587
|
+
end_line=end,
|
|
588
|
+
text=window_text,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
return best_match
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _create_unified_diff(text1: str, text2: str) -> str:
|
|
595
|
+
"""Create a unified diff between two texts."""
|
|
596
|
+
lines1 = text1.splitlines(keepends=True)
|
|
597
|
+
lines2 = text2.splitlines(keepends=True)
|
|
598
|
+
|
|
599
|
+
# Ensure lines end with newline
|
|
600
|
+
lines1 = [line if line.endswith("\n") else line + "\n" for line in lines1]
|
|
601
|
+
lines2 = [line if line.endswith("\n") else line + "\n" for line in lines2]
|
|
602
|
+
|
|
603
|
+
diff = difflib.unified_diff(
|
|
604
|
+
lines1, lines2, fromfile="SEARCH", tofile="CLOSEST MATCH", lineterm="", n=3
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
result = "".join(diff)
|
|
608
|
+
|
|
609
|
+
# Truncate if too long
|
|
610
|
+
max_chars = 2000
|
|
611
|
+
if len(result) > max_chars:
|
|
612
|
+
result = result[:max_chars] + "\n...(diff truncated)"
|
|
613
|
+
|
|
614
|
+
return result.rstrip()
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _build_not_found_error(content: str, old_string: str) -> str:
|
|
618
|
+
"""Build a helpful error message when old_string is not found."""
|
|
619
|
+
lines = content.split("\n")
|
|
620
|
+
search_lines = old_string.split("\n")
|
|
621
|
+
|
|
622
|
+
error_parts = ["Search text not found in file."]
|
|
623
|
+
|
|
624
|
+
# Add first line context
|
|
625
|
+
if search_lines and search_lines[0].strip():
|
|
626
|
+
first_search_line = search_lines[0].strip()
|
|
627
|
+
matches = [i for i, line in enumerate(lines) if first_search_line in line]
|
|
628
|
+
|
|
629
|
+
if matches:
|
|
630
|
+
error_parts.append(
|
|
631
|
+
f"\nFirst search line '{first_search_line}' appears at line(s): "
|
|
632
|
+
f"{', '.join(str(i + 1) for i in matches[:3])}"
|
|
633
|
+
)
|
|
634
|
+
else:
|
|
635
|
+
error_parts.append(
|
|
636
|
+
f"\nFirst search line '{first_search_line}' not found anywhere in file"
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
# Try fuzzy matching
|
|
640
|
+
fuzzy_match = _find_best_fuzzy_match(content, old_string, threshold=0.8)
|
|
641
|
+
if fuzzy_match:
|
|
642
|
+
similarity_pct = fuzzy_match.similarity * 100
|
|
643
|
+
error_parts.append(
|
|
644
|
+
f"\nClosest fuzzy match (similarity {similarity_pct:.1f}%) "
|
|
645
|
+
f"at lines {fuzzy_match.start_line}–{fuzzy_match.end_line}:" # noqa: RUF001
|
|
646
|
+
)
|
|
647
|
+
diff = _create_unified_diff(old_string, fuzzy_match.text)
|
|
648
|
+
if diff:
|
|
649
|
+
error_parts.append(f"\n{diff}")
|
|
650
|
+
|
|
651
|
+
# Add debugging tips
|
|
652
|
+
error_parts.append(
|
|
653
|
+
"\n\nDebugging tips:"
|
|
654
|
+
"\n1. Check for exact whitespace/indentation match"
|
|
655
|
+
"\n2. Verify line endings match the file (\\r\\n vs \\n)"
|
|
656
|
+
"\n3. Ensure the search text hasn't been modified"
|
|
657
|
+
"\n4. Try reading the file section first to get exact text"
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
return "".join(error_parts)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
async def edit_file_tool(
|
|
664
|
+
file_path: str,
|
|
665
|
+
old_string: str,
|
|
666
|
+
new_string: str,
|
|
667
|
+
replace_all: bool = False,
|
|
668
|
+
context: AgentContext | None = None,
|
|
669
|
+
) -> dict[str, Any]:
|
|
670
|
+
"""Perform exact string replacements in files using sophisticated matching strategies.
|
|
671
|
+
|
|
672
|
+
Supports multiple fallback approaches including whitespace normalization,
|
|
673
|
+
indentation flexibility, and context-aware matching.
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
file_path: Path to the file to modify
|
|
677
|
+
old_string: Text to replace
|
|
678
|
+
new_string: Text to replace it with
|
|
679
|
+
replace_all: Whether to replace all occurrences
|
|
680
|
+
context: Agent execution context
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
Dict with operation results including diff and any errors
|
|
684
|
+
"""
|
|
685
|
+
if old_string == new_string:
|
|
686
|
+
msg = "old_string and new_string must be different"
|
|
687
|
+
raise ValueError(msg)
|
|
688
|
+
|
|
689
|
+
# Resolve file path
|
|
690
|
+
path = Path(file_path)
|
|
691
|
+
if not path.is_absolute():
|
|
692
|
+
# Make relative to current working directory
|
|
693
|
+
path = Path.cwd() / path
|
|
694
|
+
|
|
695
|
+
# Validate file exists and is a file
|
|
696
|
+
if not path.exists():
|
|
697
|
+
msg = f"File not found: {path}"
|
|
698
|
+
raise FileNotFoundError(msg)
|
|
699
|
+
|
|
700
|
+
if not path.is_file():
|
|
701
|
+
msg = f"Path is a directory, not a file: {path}"
|
|
702
|
+
raise ValueError(msg)
|
|
703
|
+
|
|
704
|
+
# Read current content
|
|
705
|
+
try:
|
|
706
|
+
original_content = path.read_text(encoding="utf-8")
|
|
707
|
+
except UnicodeDecodeError:
|
|
708
|
+
# Try with different encoding
|
|
709
|
+
original_content = path.read_text(encoding="latin-1")
|
|
710
|
+
|
|
711
|
+
# Handle empty file case
|
|
712
|
+
if old_string == "" and original_content == "":
|
|
713
|
+
new_content = new_string
|
|
714
|
+
else:
|
|
715
|
+
new_content = replace_content(original_content, old_string, new_string, replace_all)
|
|
716
|
+
|
|
717
|
+
# Generate diff
|
|
718
|
+
diff_lines = list(
|
|
719
|
+
difflib.unified_diff(
|
|
720
|
+
original_content.splitlines(keepends=True),
|
|
721
|
+
new_content.splitlines(keepends=True),
|
|
722
|
+
fromfile=str(path),
|
|
723
|
+
tofile=str(path),
|
|
724
|
+
lineterm="",
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
diff_text = "".join(diff_lines)
|
|
728
|
+
trimmed_diff = _trim_diff(diff_text) if diff_text else ""
|
|
729
|
+
|
|
730
|
+
# Write new content
|
|
731
|
+
try:
|
|
732
|
+
path.write_text(new_content, encoding="utf-8")
|
|
733
|
+
except Exception as e:
|
|
734
|
+
msg = f"Failed to write file: {e}"
|
|
735
|
+
raise RuntimeError(msg) from e
|
|
736
|
+
|
|
737
|
+
return {
|
|
738
|
+
"success": True,
|
|
739
|
+
"file_path": str(path),
|
|
740
|
+
"diff": trimmed_diff,
|
|
741
|
+
"message": f"Successfully edited {path.name}",
|
|
742
|
+
"lines_changed": len([line for line in diff_lines if line.startswith(("+", "-"))]),
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
# Create the tool instance
|
|
747
|
+
edit_tool = Tool.from_callable(edit_file_tool, name_override="edit_file")
|