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.

Files changed (474) hide show
  1. acp/README.md +64 -0
  2. acp/__init__.py +172 -0
  3. acp/__main__.py +10 -0
  4. acp/acp_requests.py +285 -0
  5. acp/agent/__init__.py +6 -0
  6. acp/agent/connection.py +256 -0
  7. acp/agent/implementations/__init__.py +6 -0
  8. acp/agent/implementations/debug_server/__init__.py +1 -0
  9. acp/agent/implementations/debug_server/cli.py +79 -0
  10. acp/agent/implementations/debug_server/debug.html +234 -0
  11. acp/agent/implementations/debug_server/debug_server.py +496 -0
  12. acp/agent/implementations/testing.py +91 -0
  13. acp/agent/protocol.py +65 -0
  14. acp/bridge/README.md +162 -0
  15. acp/bridge/__init__.py +6 -0
  16. acp/bridge/__main__.py +91 -0
  17. acp/bridge/bridge.py +246 -0
  18. acp/bridge/py.typed +0 -0
  19. acp/bridge/settings.py +15 -0
  20. acp/client/__init__.py +7 -0
  21. acp/client/connection.py +251 -0
  22. acp/client/implementations/__init__.py +7 -0
  23. acp/client/implementations/default_client.py +185 -0
  24. acp/client/implementations/headless_client.py +266 -0
  25. acp/client/implementations/noop_client.py +110 -0
  26. acp/client/protocol.py +61 -0
  27. acp/connection.py +280 -0
  28. acp/exceptions.py +46 -0
  29. acp/filesystem.py +524 -0
  30. acp/notifications.py +832 -0
  31. acp/py.typed +0 -0
  32. acp/schema/__init__.py +265 -0
  33. acp/schema/agent_plan.py +30 -0
  34. acp/schema/agent_requests.py +126 -0
  35. acp/schema/agent_responses.py +256 -0
  36. acp/schema/base.py +39 -0
  37. acp/schema/capabilities.py +230 -0
  38. acp/schema/client_requests.py +247 -0
  39. acp/schema/client_responses.py +96 -0
  40. acp/schema/common.py +81 -0
  41. acp/schema/content_blocks.py +188 -0
  42. acp/schema/mcp.py +82 -0
  43. acp/schema/messages.py +171 -0
  44. acp/schema/notifications.py +82 -0
  45. acp/schema/protocol_stuff.md +3 -0
  46. acp/schema/session_state.py +160 -0
  47. acp/schema/session_updates.py +419 -0
  48. acp/schema/slash_commands.py +51 -0
  49. acp/schema/terminal.py +15 -0
  50. acp/schema/tool_call.py +347 -0
  51. acp/stdio.py +250 -0
  52. acp/task/__init__.py +53 -0
  53. acp/task/debug.py +197 -0
  54. acp/task/dispatcher.py +93 -0
  55. acp/task/queue.py +69 -0
  56. acp/task/sender.py +82 -0
  57. acp/task/state.py +87 -0
  58. acp/task/supervisor.py +93 -0
  59. acp/terminal_handle.py +30 -0
  60. acp/tool_call_reporter.py +199 -0
  61. acp/tool_call_state.py +178 -0
  62. acp/transports.py +104 -0
  63. acp/utils.py +240 -0
  64. agentpool/__init__.py +63 -0
  65. agentpool/__main__.py +7 -0
  66. agentpool/agents/__init__.py +30 -0
  67. agentpool/agents/acp_agent/__init__.py +5 -0
  68. agentpool/agents/acp_agent/acp_agent.py +837 -0
  69. agentpool/agents/acp_agent/acp_converters.py +294 -0
  70. agentpool/agents/acp_agent/client_handler.py +317 -0
  71. agentpool/agents/acp_agent/session_state.py +44 -0
  72. agentpool/agents/agent.py +1264 -0
  73. agentpool/agents/agui_agent/__init__.py +19 -0
  74. agentpool/agents/agui_agent/agui_agent.py +677 -0
  75. agentpool/agents/agui_agent/agui_converters.py +423 -0
  76. agentpool/agents/agui_agent/chunk_transformer.py +204 -0
  77. agentpool/agents/agui_agent/event_types.py +83 -0
  78. agentpool/agents/agui_agent/helpers.py +192 -0
  79. agentpool/agents/architect.py +71 -0
  80. agentpool/agents/base_agent.py +177 -0
  81. agentpool/agents/claude_code_agent/__init__.py +11 -0
  82. agentpool/agents/claude_code_agent/claude_code_agent.py +1021 -0
  83. agentpool/agents/claude_code_agent/converters.py +243 -0
  84. agentpool/agents/context.py +105 -0
  85. agentpool/agents/events/__init__.py +61 -0
  86. agentpool/agents/events/builtin_handlers.py +129 -0
  87. agentpool/agents/events/event_emitter.py +320 -0
  88. agentpool/agents/events/events.py +561 -0
  89. agentpool/agents/events/tts_handlers.py +186 -0
  90. agentpool/agents/interactions.py +419 -0
  91. agentpool/agents/slashed_agent.py +244 -0
  92. agentpool/agents/sys_prompts.py +178 -0
  93. agentpool/agents/tool_wrapping.py +184 -0
  94. agentpool/base_provider.py +28 -0
  95. agentpool/common_types.py +226 -0
  96. agentpool/config_resources/__init__.py +16 -0
  97. agentpool/config_resources/acp_assistant.yml +24 -0
  98. agentpool/config_resources/agents.yml +109 -0
  99. agentpool/config_resources/agents_template.yml +18 -0
  100. agentpool/config_resources/agui_test.yml +18 -0
  101. agentpool/config_resources/claude_code_agent.yml +16 -0
  102. agentpool/config_resources/claude_style_subagent.md +30 -0
  103. agentpool/config_resources/external_acp_agents.yml +77 -0
  104. agentpool/config_resources/opencode_style_subagent.md +19 -0
  105. agentpool/config_resources/tts_test_agents.yml +78 -0
  106. agentpool/delegation/__init__.py +8 -0
  107. agentpool/delegation/base_team.py +504 -0
  108. agentpool/delegation/message_flow_tracker.py +39 -0
  109. agentpool/delegation/pool.py +1129 -0
  110. agentpool/delegation/team.py +325 -0
  111. agentpool/delegation/teamrun.py +343 -0
  112. agentpool/docs/__init__.py +5 -0
  113. agentpool/docs/gen_examples.py +42 -0
  114. agentpool/docs/utils.py +370 -0
  115. agentpool/functional/__init__.py +20 -0
  116. agentpool/functional/py.typed +0 -0
  117. agentpool/functional/run.py +80 -0
  118. agentpool/functional/structure.py +136 -0
  119. agentpool/hooks/__init__.py +20 -0
  120. agentpool/hooks/agent_hooks.py +247 -0
  121. agentpool/hooks/base.py +119 -0
  122. agentpool/hooks/callable.py +140 -0
  123. agentpool/hooks/command.py +180 -0
  124. agentpool/hooks/prompt.py +122 -0
  125. agentpool/jinja_filters.py +132 -0
  126. agentpool/log.py +224 -0
  127. agentpool/mcp_server/__init__.py +17 -0
  128. agentpool/mcp_server/client.py +429 -0
  129. agentpool/mcp_server/constants.py +32 -0
  130. agentpool/mcp_server/conversions.py +172 -0
  131. agentpool/mcp_server/helpers.py +47 -0
  132. agentpool/mcp_server/manager.py +232 -0
  133. agentpool/mcp_server/message_handler.py +164 -0
  134. agentpool/mcp_server/registries/__init__.py +1 -0
  135. agentpool/mcp_server/registries/official_registry_client.py +345 -0
  136. agentpool/mcp_server/registries/pulsemcp_client.py +88 -0
  137. agentpool/mcp_server/tool_bridge.py +548 -0
  138. agentpool/messaging/__init__.py +58 -0
  139. agentpool/messaging/compaction.py +928 -0
  140. agentpool/messaging/connection_manager.py +319 -0
  141. agentpool/messaging/context.py +66 -0
  142. agentpool/messaging/event_manager.py +426 -0
  143. agentpool/messaging/events.py +39 -0
  144. agentpool/messaging/message_container.py +209 -0
  145. agentpool/messaging/message_history.py +491 -0
  146. agentpool/messaging/messagenode.py +377 -0
  147. agentpool/messaging/messages.py +655 -0
  148. agentpool/messaging/processing.py +76 -0
  149. agentpool/mime_utils.py +95 -0
  150. agentpool/models/__init__.py +21 -0
  151. agentpool/models/acp_agents/__init__.py +22 -0
  152. agentpool/models/acp_agents/base.py +308 -0
  153. agentpool/models/acp_agents/mcp_capable.py +790 -0
  154. agentpool/models/acp_agents/non_mcp.py +842 -0
  155. agentpool/models/agents.py +450 -0
  156. agentpool/models/agui_agents.py +89 -0
  157. agentpool/models/claude_code_agents.py +238 -0
  158. agentpool/models/file_agents.py +116 -0
  159. agentpool/models/file_parsing.py +367 -0
  160. agentpool/models/manifest.py +658 -0
  161. agentpool/observability/__init__.py +9 -0
  162. agentpool/observability/observability_registry.py +97 -0
  163. agentpool/prompts/__init__.py +1 -0
  164. agentpool/prompts/base.py +27 -0
  165. agentpool/prompts/builtin_provider.py +75 -0
  166. agentpool/prompts/conversion_manager.py +95 -0
  167. agentpool/prompts/convert.py +96 -0
  168. agentpool/prompts/manager.py +204 -0
  169. agentpool/prompts/parts/zed.md +33 -0
  170. agentpool/prompts/prompts.py +581 -0
  171. agentpool/py.typed +0 -0
  172. agentpool/queries/tree-sitter-language-pack/README.md +7 -0
  173. agentpool/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  174. agentpool/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  175. agentpool/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  176. agentpool/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  177. agentpool/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  178. agentpool/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  179. agentpool/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  180. agentpool/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  181. agentpool/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  182. agentpool/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  183. agentpool/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  184. agentpool/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  185. agentpool/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  186. agentpool/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  187. agentpool/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  188. agentpool/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  189. agentpool/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  190. agentpool/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  191. agentpool/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  192. agentpool/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  193. agentpool/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  194. agentpool/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  195. agentpool/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  196. agentpool/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  197. agentpool/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  198. agentpool/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  199. agentpool/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  200. agentpool/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  201. agentpool/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  202. agentpool/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  203. agentpool/queries/tree-sitter-languages/README.md +24 -0
  204. agentpool/queries/tree-sitter-languages/c-tags.scm +9 -0
  205. agentpool/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  206. agentpool/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  207. agentpool/queries/tree-sitter-languages/dart-tags.scm +91 -0
  208. agentpool/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  209. agentpool/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  210. agentpool/queries/tree-sitter-languages/elm-tags.scm +19 -0
  211. agentpool/queries/tree-sitter-languages/fortran-tags.scm +15 -0
  212. agentpool/queries/tree-sitter-languages/go-tags.scm +30 -0
  213. agentpool/queries/tree-sitter-languages/haskell-tags.scm +3 -0
  214. agentpool/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  215. agentpool/queries/tree-sitter-languages/java-tags.scm +20 -0
  216. agentpool/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  217. agentpool/queries/tree-sitter-languages/julia-tags.scm +60 -0
  218. agentpool/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  219. agentpool/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  220. agentpool/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  221. agentpool/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  222. agentpool/queries/tree-sitter-languages/php-tags.scm +26 -0
  223. agentpool/queries/tree-sitter-languages/python-tags.scm +12 -0
  224. agentpool/queries/tree-sitter-languages/ql-tags.scm +26 -0
  225. agentpool/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  226. agentpool/queries/tree-sitter-languages/rust-tags.scm +60 -0
  227. agentpool/queries/tree-sitter-languages/scala-tags.scm +65 -0
  228. agentpool/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  229. agentpool/queries/tree-sitter-languages/zig-tags.scm +3 -0
  230. agentpool/repomap.py +1231 -0
  231. agentpool/resource_providers/__init__.py +17 -0
  232. agentpool/resource_providers/aggregating.py +54 -0
  233. agentpool/resource_providers/base.py +172 -0
  234. agentpool/resource_providers/codemode/__init__.py +9 -0
  235. agentpool/resource_providers/codemode/code_executor.py +215 -0
  236. agentpool/resource_providers/codemode/default_prompt.py +19 -0
  237. agentpool/resource_providers/codemode/helpers.py +83 -0
  238. agentpool/resource_providers/codemode/progress_executor.py +212 -0
  239. agentpool/resource_providers/codemode/provider.py +150 -0
  240. agentpool/resource_providers/codemode/remote_mcp_execution.py +143 -0
  241. agentpool/resource_providers/codemode/remote_provider.py +171 -0
  242. agentpool/resource_providers/filtering.py +42 -0
  243. agentpool/resource_providers/mcp_provider.py +246 -0
  244. agentpool/resource_providers/plan_provider.py +196 -0
  245. agentpool/resource_providers/pool.py +69 -0
  246. agentpool/resource_providers/static.py +289 -0
  247. agentpool/running/__init__.py +20 -0
  248. agentpool/running/decorators.py +56 -0
  249. agentpool/running/discovery.py +101 -0
  250. agentpool/running/executor.py +284 -0
  251. agentpool/running/injection.py +111 -0
  252. agentpool/running/py.typed +0 -0
  253. agentpool/running/run_nodes.py +87 -0
  254. agentpool/server.py +122 -0
  255. agentpool/sessions/__init__.py +13 -0
  256. agentpool/sessions/manager.py +302 -0
  257. agentpool/sessions/models.py +71 -0
  258. agentpool/sessions/session.py +239 -0
  259. agentpool/sessions/store.py +163 -0
  260. agentpool/skills/__init__.py +5 -0
  261. agentpool/skills/manager.py +120 -0
  262. agentpool/skills/registry.py +210 -0
  263. agentpool/skills/skill.py +36 -0
  264. agentpool/storage/__init__.py +17 -0
  265. agentpool/storage/manager.py +419 -0
  266. agentpool/storage/serialization.py +136 -0
  267. agentpool/talk/__init__.py +13 -0
  268. agentpool/talk/registry.py +128 -0
  269. agentpool/talk/stats.py +159 -0
  270. agentpool/talk/talk.py +604 -0
  271. agentpool/tasks/__init__.py +20 -0
  272. agentpool/tasks/exceptions.py +25 -0
  273. agentpool/tasks/registry.py +33 -0
  274. agentpool/testing.py +129 -0
  275. agentpool/text_templates/__init__.py +39 -0
  276. agentpool/text_templates/system_prompt.jinja +30 -0
  277. agentpool/text_templates/tool_call_default.jinja +13 -0
  278. agentpool/text_templates/tool_call_markdown.jinja +25 -0
  279. agentpool/text_templates/tool_call_simple.jinja +5 -0
  280. agentpool/tools/__init__.py +16 -0
  281. agentpool/tools/base.py +269 -0
  282. agentpool/tools/exceptions.py +9 -0
  283. agentpool/tools/manager.py +255 -0
  284. agentpool/tools/tool_call_info.py +87 -0
  285. agentpool/ui/__init__.py +2 -0
  286. agentpool/ui/base.py +89 -0
  287. agentpool/ui/mock_provider.py +81 -0
  288. agentpool/ui/stdlib_provider.py +150 -0
  289. agentpool/utils/__init__.py +44 -0
  290. agentpool/utils/baseregistry.py +185 -0
  291. agentpool/utils/count_tokens.py +62 -0
  292. agentpool/utils/dag.py +184 -0
  293. agentpool/utils/importing.py +206 -0
  294. agentpool/utils/inspection.py +334 -0
  295. agentpool/utils/model_capabilities.py +25 -0
  296. agentpool/utils/network.py +28 -0
  297. agentpool/utils/now.py +22 -0
  298. agentpool/utils/parse_time.py +87 -0
  299. agentpool/utils/result_utils.py +35 -0
  300. agentpool/utils/signatures.py +305 -0
  301. agentpool/utils/streams.py +112 -0
  302. agentpool/utils/tasks.py +186 -0
  303. agentpool/vfs_registry.py +250 -0
  304. agentpool-2.1.9.dist-info/METADATA +336 -0
  305. agentpool-2.1.9.dist-info/RECORD +474 -0
  306. agentpool-2.1.9.dist-info/WHEEL +4 -0
  307. agentpool-2.1.9.dist-info/entry_points.txt +14 -0
  308. agentpool-2.1.9.dist-info/licenses/LICENSE +22 -0
  309. agentpool_cli/__init__.py +34 -0
  310. agentpool_cli/__main__.py +66 -0
  311. agentpool_cli/agent.py +175 -0
  312. agentpool_cli/cli_types.py +23 -0
  313. agentpool_cli/common.py +163 -0
  314. agentpool_cli/create.py +175 -0
  315. agentpool_cli/history.py +217 -0
  316. agentpool_cli/log.py +78 -0
  317. agentpool_cli/py.typed +0 -0
  318. agentpool_cli/run.py +84 -0
  319. agentpool_cli/serve_acp.py +177 -0
  320. agentpool_cli/serve_api.py +69 -0
  321. agentpool_cli/serve_mcp.py +74 -0
  322. agentpool_cli/serve_vercel.py +233 -0
  323. agentpool_cli/store.py +171 -0
  324. agentpool_cli/task.py +84 -0
  325. agentpool_cli/utils.py +104 -0
  326. agentpool_cli/watch.py +54 -0
  327. agentpool_commands/__init__.py +180 -0
  328. agentpool_commands/agents.py +199 -0
  329. agentpool_commands/base.py +45 -0
  330. agentpool_commands/commands.py +58 -0
  331. agentpool_commands/completers.py +110 -0
  332. agentpool_commands/connections.py +175 -0
  333. agentpool_commands/markdown_utils.py +31 -0
  334. agentpool_commands/models.py +62 -0
  335. agentpool_commands/prompts.py +78 -0
  336. agentpool_commands/py.typed +0 -0
  337. agentpool_commands/read.py +77 -0
  338. agentpool_commands/resources.py +210 -0
  339. agentpool_commands/session.py +48 -0
  340. agentpool_commands/tools.py +269 -0
  341. agentpool_commands/utils.py +189 -0
  342. agentpool_commands/workers.py +163 -0
  343. agentpool_config/__init__.py +53 -0
  344. agentpool_config/builtin_tools.py +265 -0
  345. agentpool_config/commands.py +237 -0
  346. agentpool_config/conditions.py +301 -0
  347. agentpool_config/converters.py +30 -0
  348. agentpool_config/durable.py +331 -0
  349. agentpool_config/event_handlers.py +600 -0
  350. agentpool_config/events.py +153 -0
  351. agentpool_config/forward_targets.py +251 -0
  352. agentpool_config/hook_conditions.py +331 -0
  353. agentpool_config/hooks.py +241 -0
  354. agentpool_config/jinja.py +206 -0
  355. agentpool_config/knowledge.py +41 -0
  356. agentpool_config/loaders.py +350 -0
  357. agentpool_config/mcp_server.py +243 -0
  358. agentpool_config/nodes.py +202 -0
  359. agentpool_config/observability.py +191 -0
  360. agentpool_config/output_types.py +55 -0
  361. agentpool_config/pool_server.py +267 -0
  362. agentpool_config/prompt_hubs.py +105 -0
  363. agentpool_config/prompts.py +185 -0
  364. agentpool_config/py.typed +0 -0
  365. agentpool_config/resources.py +33 -0
  366. agentpool_config/session.py +119 -0
  367. agentpool_config/skills.py +17 -0
  368. agentpool_config/storage.py +288 -0
  369. agentpool_config/system_prompts.py +190 -0
  370. agentpool_config/task.py +162 -0
  371. agentpool_config/teams.py +52 -0
  372. agentpool_config/tools.py +112 -0
  373. agentpool_config/toolsets.py +1033 -0
  374. agentpool_config/workers.py +86 -0
  375. agentpool_prompts/__init__.py +1 -0
  376. agentpool_prompts/braintrust_hub.py +235 -0
  377. agentpool_prompts/fabric.py +75 -0
  378. agentpool_prompts/langfuse_hub.py +79 -0
  379. agentpool_prompts/promptlayer_provider.py +59 -0
  380. agentpool_prompts/py.typed +0 -0
  381. agentpool_server/__init__.py +9 -0
  382. agentpool_server/a2a_server/__init__.py +5 -0
  383. agentpool_server/a2a_server/a2a_types.py +41 -0
  384. agentpool_server/a2a_server/server.py +190 -0
  385. agentpool_server/a2a_server/storage.py +81 -0
  386. agentpool_server/acp_server/__init__.py +22 -0
  387. agentpool_server/acp_server/acp_agent.py +786 -0
  388. agentpool_server/acp_server/acp_tools.py +43 -0
  389. agentpool_server/acp_server/commands/__init__.py +18 -0
  390. agentpool_server/acp_server/commands/acp_commands.py +594 -0
  391. agentpool_server/acp_server/commands/debug_commands.py +376 -0
  392. agentpool_server/acp_server/commands/docs_commands/__init__.py +39 -0
  393. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +169 -0
  394. agentpool_server/acp_server/commands/docs_commands/get_schema.py +176 -0
  395. agentpool_server/acp_server/commands/docs_commands/get_source.py +110 -0
  396. agentpool_server/acp_server/commands/docs_commands/git_diff.py +111 -0
  397. agentpool_server/acp_server/commands/docs_commands/helpers.py +33 -0
  398. agentpool_server/acp_server/commands/docs_commands/url_to_markdown.py +90 -0
  399. agentpool_server/acp_server/commands/spawn.py +210 -0
  400. agentpool_server/acp_server/converters.py +235 -0
  401. agentpool_server/acp_server/input_provider.py +338 -0
  402. agentpool_server/acp_server/server.py +288 -0
  403. agentpool_server/acp_server/session.py +969 -0
  404. agentpool_server/acp_server/session_manager.py +313 -0
  405. agentpool_server/acp_server/syntax_detection.py +250 -0
  406. agentpool_server/acp_server/zed_tools.md +90 -0
  407. agentpool_server/aggregating_server.py +309 -0
  408. agentpool_server/agui_server/__init__.py +11 -0
  409. agentpool_server/agui_server/server.py +128 -0
  410. agentpool_server/base.py +189 -0
  411. agentpool_server/http_server.py +164 -0
  412. agentpool_server/mcp_server/__init__.py +6 -0
  413. agentpool_server/mcp_server/server.py +314 -0
  414. agentpool_server/mcp_server/zed_wrapper.py +110 -0
  415. agentpool_server/openai_api_server/__init__.py +5 -0
  416. agentpool_server/openai_api_server/completions/__init__.py +1 -0
  417. agentpool_server/openai_api_server/completions/helpers.py +81 -0
  418. agentpool_server/openai_api_server/completions/models.py +98 -0
  419. agentpool_server/openai_api_server/responses/__init__.py +1 -0
  420. agentpool_server/openai_api_server/responses/helpers.py +74 -0
  421. agentpool_server/openai_api_server/responses/models.py +96 -0
  422. agentpool_server/openai_api_server/server.py +242 -0
  423. agentpool_server/py.typed +0 -0
  424. agentpool_storage/__init__.py +9 -0
  425. agentpool_storage/base.py +310 -0
  426. agentpool_storage/file_provider.py +378 -0
  427. agentpool_storage/formatters.py +129 -0
  428. agentpool_storage/memory_provider.py +396 -0
  429. agentpool_storage/models.py +108 -0
  430. agentpool_storage/py.typed +0 -0
  431. agentpool_storage/session_store.py +262 -0
  432. agentpool_storage/sql_provider/__init__.py +21 -0
  433. agentpool_storage/sql_provider/cli.py +146 -0
  434. agentpool_storage/sql_provider/models.py +249 -0
  435. agentpool_storage/sql_provider/queries.py +15 -0
  436. agentpool_storage/sql_provider/sql_provider.py +444 -0
  437. agentpool_storage/sql_provider/utils.py +234 -0
  438. agentpool_storage/text_log_provider.py +275 -0
  439. agentpool_toolsets/__init__.py +15 -0
  440. agentpool_toolsets/builtin/__init__.py +33 -0
  441. agentpool_toolsets/builtin/agent_management.py +239 -0
  442. agentpool_toolsets/builtin/chain.py +288 -0
  443. agentpool_toolsets/builtin/code.py +398 -0
  444. agentpool_toolsets/builtin/debug.py +291 -0
  445. agentpool_toolsets/builtin/execution_environment.py +381 -0
  446. agentpool_toolsets/builtin/file_edit/__init__.py +11 -0
  447. agentpool_toolsets/builtin/file_edit/file_edit.py +747 -0
  448. agentpool_toolsets/builtin/file_edit/fuzzy_matcher/__init__.py +5 -0
  449. agentpool_toolsets/builtin/file_edit/fuzzy_matcher/example_usage.py +311 -0
  450. agentpool_toolsets/builtin/file_edit/fuzzy_matcher/streaming_fuzzy_matcher.py +443 -0
  451. agentpool_toolsets/builtin/history.py +36 -0
  452. agentpool_toolsets/builtin/integration.py +85 -0
  453. agentpool_toolsets/builtin/skills.py +77 -0
  454. agentpool_toolsets/builtin/subagent_tools.py +324 -0
  455. agentpool_toolsets/builtin/tool_management.py +90 -0
  456. agentpool_toolsets/builtin/user_interaction.py +52 -0
  457. agentpool_toolsets/builtin/workers.py +128 -0
  458. agentpool_toolsets/composio_toolset.py +96 -0
  459. agentpool_toolsets/config_creation.py +192 -0
  460. agentpool_toolsets/entry_points.py +47 -0
  461. agentpool_toolsets/fsspec_toolset/__init__.py +7 -0
  462. agentpool_toolsets/fsspec_toolset/diagnostics.py +115 -0
  463. agentpool_toolsets/fsspec_toolset/grep.py +450 -0
  464. agentpool_toolsets/fsspec_toolset/helpers.py +631 -0
  465. agentpool_toolsets/fsspec_toolset/streaming_diff_parser.py +249 -0
  466. agentpool_toolsets/fsspec_toolset/toolset.py +1384 -0
  467. agentpool_toolsets/mcp_run_toolset.py +61 -0
  468. agentpool_toolsets/notifications.py +146 -0
  469. agentpool_toolsets/openapi.py +118 -0
  470. agentpool_toolsets/py.typed +0 -0
  471. agentpool_toolsets/search_toolset.py +202 -0
  472. agentpool_toolsets/semantic_memory_toolset.py +536 -0
  473. agentpool_toolsets/streaming_tools.py +265 -0
  474. 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")