core/tools: edit results preview the old content

Wrong-target edits (the test_run_aside pass clobber, the edit_replace
mangling) were silent: the success message reported counts only, so the
model had no signal it replaced the wrong thing until later.

edit_lineno now reports the first removed line ("first: 'pass'") and
edit_replace reports the matched pattern ('old' -> 'new'), giving the
model an immediate self-correction signal.
This commit is contained in:
2026-08-15 17:40:26 +08:00
parent 38671482f2
commit 2cf66edb60
4 changed files with 42 additions and 6 deletions
+13 -1
View File
@@ -77,6 +77,14 @@ def _append_after(target: Path, path: str, text: str, lines: list[str], new_cont
return f"appended content after line {n} in {path}"
def _excerpt(text: str, limit: int = 40) -> str:
"""Collapse newlines and truncate for a one-line message excerpt."""
one = text.replace("\r\n", " ").replace("\n", " ")
if len(one) > limit:
return one[:limit] + "…"
return one
def _replace_range(
target: Path,
path: str,
@@ -92,7 +100,11 @@ def _replace_range(
new_lines = lines[: start_line - 1] + replacement + lines[end:]
_ = target.write_text("".join(new_lines), encoding="utf-8")
removed = end - start_line + 1
return f"replaced lines {start_line}-{end} ({removed} lines) with {len(replacement)} lines in {path}"
old_first = _excerpt(lines[start_line - 1].rstrip("\r\n"))
return (
f"replaced lines {start_line}-{end} ({removed} lines; first: {old_first!r}) "
f"with {len(replacement)} lines in {path}"
)
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
+27 -5
View File
@@ -18,15 +18,31 @@ def _count_non_overlapping(text: str, needle: str) -> int:
start = index + len(needle)
def _success_message(path: str, *, replaced: int, found: int) -> str:
def _excerpt(text: str, limit: int = 40) -> str:
"""Collapse newlines and truncate for a one-line message excerpt."""
one = text.replace("\r\n", " ").replace("\n", " ")
if len(one) > limit:
return one[:limit] + "…"
return one
def _success_message(
path: str,
*,
replaced: int,
found: int,
old_string: str,
new_string: str,
) -> str:
remaining = found - replaced
unit = "occurrence" if replaced == 1 else "occurrences"
change = f" ('{_excerpt(old_string)}' → '{_excerpt(new_string)}')"
if remaining == 0:
if found == 1:
return f"replaced 1 occurrence in {path}"
return f"replaced {replaced} {unit} in {path} (all {found} matches)"
return f"replaced 1 occurrence in {path}{change}"
return f"replaced {replaced} {unit} in {path}{change} (all {found} matches)"
return (
f"replaced {replaced} {unit} in {path} "
f"replaced {replaced} {unit} in {path}{change} "
f"({replaced} of {found} matches; {remaining} remain — "
f"raise max_replaces or use a more specific old_string)"
)
@@ -56,4 +72,10 @@ async def edit_replace(path: str, old_string: str, new_string: str, max_replaces
_ = target.write_text(updated, encoding="utf-8")
# The write changes the file's mtime, so edit_lineno's freshness check
# rejects stale line numbers until the model re-reads with_lineno.
return _success_message(path, replaced=n, found=found)
return _success_message(
path,
replaced=n,
found=found,
old_string=old_string,
new_string=new_string,
)