Skip to content

fix(runtime): write edit tool replacements literally - #177

Open
kevin9327 wants to merge 1 commit into
deer-flow:mainfrom
kevin9327:fix/edit-tool-literal-replacement
Open

kevin9327 wants to merge 1 commit into
deer-flow:mainfrom
kevin9327:fix/edit-tool-literal-replacement

Conversation

@kevin9327

Copy link
Copy Markdown

Summary

When an agent uses the built-in edit tool without replace_all, any $$, $&, $` or $' in new_string is rewritten before it reaches the file. The tool still reports Replaced 1 occurrence, but the file contains different text than the model asked for. Common cases:

  • $$HOME or $$(pwd) in a Makefile becomes $HOME / $(pwd)
  • $$ (shell PID) or a $$ … $$ math block in Markdown/LaTeX loses a dollar sign
  • $& is replaced by the matched old_string, and $' / $` by the rest of the file after / before the match

The cause is that content.replace(oldString, newString) still applies replacement patterns even when the search value is a string. The replace_all path uses split/join and already writes new_string verbatim, so the same edit gives different files depending on that flag. Passing a replacer function (() => newString) makes the single-match path literal too. Matching, uniqueness checks and the result message are unchanged.

Validation

bun test packages/runtime/tests/tools/built-in/fs.test.ts -t "edit writes"

Before the fix (new test added, source unchanged):

packages\runtime\tests\tools\built-in\fs.test.ts:
52 |     await edit(allPath, "echo OLD", newString, true);
53 |     expect(await fs.readFile(allPath, "utf8")).toBe(`run:\n\t${newString}\n`);
54 | 
55 |     await write(singlePath, "run:\n\techo OLD\n");
56 |     await edit(singlePath, "echo OLD", newString);
57 |     expect(await fs.readFile(singlePath, "utf8")).toBe(
                                                       ^
error: expect(received).toBe(expected)

  "run:
- 	echo $$HOME $& $' $`
+ 	echo $HOME echo OLD 
+  run:
+ 	
  "

- Expected  - 1
+ Received  + 3

      at <anonymous> (packages\runtime\tests\tools\built-in\fs.test.ts:57:51)
(fail) filesystem built-in paths > edit writes $ sequences in new_string literally [12.93ms]

 0 pass
 34 filtered out
 1 fail

After the fix, this test passes.

  • The replace_all: true half of the test (line 53) passes both before and after, as do the existing write and edit … tests. This shows only the single-match path's output changes.
  • Whole file, bun test packages/runtime/tests/tools/built-in/fs.test.ts: 32 pass / 2 fail on main, and 33 pass / 2 fail on this branch. The same two tests fail on main: read, traversal, and search tools expand home paths and grep caps multibyte matching output. Both need ripgrep (rg), which is not installed locally; CI installs it.
  • ESLint (--max-warnings 0) on both touched files: clean on main and on this branch.
  • tsc --noEmit -p packages/runtime/tsconfig.json: clean on main and on this branch.
  • Prettier: both files already have unformatted lines on main (9 and 62); this change adds none.

🤖 Generated with Claude Code

Without replace_all, the edit tool called String.prototype.replace with
new_string as the replacement string, so `$$`, `$&`, `` $` `` and `$'`
in new_string were expanded instead of written. An edit that inserts
`$$HOME` into a Makefile or a `$$` math block into Markdown wrote
different text than the model asked for. The replace_all path already
wrote new_string verbatim. Pass a replacer function so both paths agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant