Skip to content

feat: add an edit tool for exact partial file edits - #19

Merged
minixalpha merged 13 commits into
mainfrom
claude/edit-tool
Aug 17, 2026
Merged

feat: add an edit tool for exact partial file edits#19
minixalpha merged 13 commits into
mainfrom
claude/edit-tool

Conversation

@minixalpha

@minixalpha minixalpha commented Aug 17, 2026

Copy link
Copy Markdown
Owner

What this does

Adds an edit tool that replaces one exact stretch of an existing UTF-8 text
file, and wires it into the agent loop alongside read, write and bash.

Changing a few lines no longer means regenerating the whole file through
write: only old_text and new_text travel, so the change costs fewer
output tokens and does not park a full copy of the file in the context. The
old_text match doubles as a precondition — when the file has moved on, the
call 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 optional replace_all
(default false). Output follows read/write — one stretch of plain text
plus an is_error flag:

[edited src/app.py: replaced 1 occurrence at line 42]
[edited src/app.py: replaced 3 occurrences, first at line 12]
[old_text matches 3 times in src/app.py; add surrounding context to make it unique, or set replace_all=true]

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 read shows the model, both are stated in the tool
description, 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:

  • The match must be unique unless replace_all is set; an empty old_text
    and old_text == new_text are both refused.
  • Invalid UTF-8, NUL bytes, directories, non-regular files (FIFOs, devices)
    and files over the 10 MB MAX_READ_BYTES cap are turned away.
  • A UTF-8 BOM is stripped before matching and restored on write.
  • Path handling, symlink behavior and error style match read/write;
    the write is a plain read-compute-write with no mtime check, CAS or atomic
    replace — the unique old_text is the precondition.
  • The terminal echoes [edit] <path> plus a folded -/+ diff of both
    sides, 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 — offer edit in TOOLS, dispatch it in
    _run_one_tool with the folded diff echo, and point the system prompt at
    edit for partial changes and write for whole-file ones.
  • tests/test_edit_tool.py (34 cases) and tests/test_agent.py (3 cases) —
    unique/replace_all matching, literal-not-regex, dedent and trailing-space
    non-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 — an edit tool_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 QA
    section on why each fuzzy fallback was rejected and why prior-Read,
    edits[], apply_patch, formatter/LSP/history/approval and a per-file
    queue are deferred.

Testing

uv run pytest -q — 113 passed.

@minixalpha
minixalpha merged commit a0e5778 into main Aug 17, 2026
2 checks passed
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