feat: add an edit tool for exact partial file edits - #19
Merged
Conversation
Replace an exact stretch of an existing UTF-8 text file instead of resending it whole through write. The match is literal and unique by default; the only conversions are the UTF-8 BOM and a single LF->CRLF retry for CRLF files, both of which read's view forces. Everything else fails closed with a message that says how to recover.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Adds an
edittool that replaces one exact stretch of an existing UTF-8 textfile, and wires it into the agent loop alongside
read,writeandbash.Changing a few lines no longer means regenerating the whole file through
write: onlyold_textandnew_texttravel, so the change costs feweroutput tokens and does not park a full copy of the file in the context. The
old_textmatch doubles as a precondition — when the file has moved on, thecall fails and says how to recover, instead of silently burying the change
under a whole-file overwrite.
Contract
Input:
path,old_text,new_text, and an optionalreplace_all(default
false). Output followsread/write— one stretch of plain textplus an
is_errorflag:Matching is literal — no regex, and no trim, indentation, similarity or
Unicode-normalization fallbacks. The only conversions applied to the model's
input are the UTF-8 BOM and, for a CRLF file, a single LF→CRLF retry; both are
forced by what
readshows the model, both are stated in the tooldescription, and neither widens the match beyond the literal it was given.
With no approval step between the match and the write, a fuzzy hit would let
the tool change more than the model asked for and report success — so the tool
fails closed instead, and every error says what to do next.
Other behavior worth calling out:
replace_allis set; an emptyold_textand
old_text == new_textare both refused.and files over the 10 MB
MAX_READ_BYTEScap are turned away.read/write;the write is a plain read-compute-write with no mtime check, CAS or atomic
replace — the unique
old_textis the precondition.[edit] <path>plus a folded-/+diff of bothsides, since the full strings are already in the model's tool input.
Changes
src/nanopycodeagent/edit_tool.py— new tool definition and execution.src/nanopycodeagent/agent.py— offereditinTOOLS, dispatch it in_run_one_toolwith the folded diff echo, and point the system prompt ateditfor partial changes andwritefor whole-file ones.tests/test_edit_tool.py(34 cases) andtests/test_agent.py(3 cases) —unique/
replace_allmatching, literal-not-regex, dedent and trailing-spacenon-matches, CRLF and mixed-ending passes, BOM, invalid UTF-8 and NUL,
missing/directory/FIFO/over-cap targets,
~and relative paths, symlinks,preview folding, and edits within one reply applying in order.
tests/helpers.py— anedittool_use block fake.docs/changelogs/0.7.x.md—[Unreleased]entry.docs/dev_notes/{zh-CN,en}/0.7.x.md— the design rationale, including a QAsection on why each fuzzy fallback was rejected and why prior-Read,
edits[],apply_patch, formatter/LSP/history/approval and a per-filequeue are deferred.
Testing
uv run pytest -q— 113 passed.