mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
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:
@@ -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}"
|
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(
|
def _replace_range(
|
||||||
target: Path,
|
target: Path,
|
||||||
path: str,
|
path: str,
|
||||||
@@ -92,7 +100,11 @@ def _replace_range(
|
|||||||
new_lines = lines[: start_line - 1] + replacement + lines[end:]
|
new_lines = lines[: start_line - 1] + replacement + lines[end:]
|
||||||
_ = target.write_text("".join(new_lines), encoding="utf-8")
|
_ = target.write_text("".join(new_lines), encoding="utf-8")
|
||||||
removed = end - start_line + 1
|
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)
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
|
|||||||
@@ -18,15 +18,31 @@ def _count_non_overlapping(text: str, needle: str) -> int:
|
|||||||
start = index + len(needle)
|
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
|
remaining = found - replaced
|
||||||
unit = "occurrence" if replaced == 1 else "occurrences"
|
unit = "occurrence" if replaced == 1 else "occurrences"
|
||||||
|
change = f" ('{_excerpt(old_string)}' → '{_excerpt(new_string)}')"
|
||||||
if remaining == 0:
|
if remaining == 0:
|
||||||
if found == 1:
|
if found == 1:
|
||||||
return f"replaced 1 occurrence in {path}"
|
return f"replaced 1 occurrence in {path}{change}"
|
||||||
return f"replaced {replaced} {unit} in {path} (all {found} matches)"
|
return f"replaced {replaced} {unit} in {path}{change} (all {found} matches)"
|
||||||
return (
|
return (
|
||||||
f"replaced {replaced} {unit} in {path} "
|
f"replaced {replaced} {unit} in {path}{change} "
|
||||||
f"({replaced} of {found} matches; {remaining} remain — "
|
f"({replaced} of {found} matches; {remaining} remain — "
|
||||||
f"raise max_replaces or use a more specific old_string)"
|
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")
|
_ = target.write_text(updated, encoding="utf-8")
|
||||||
# The write changes the file's mtime, so edit_lineno's freshness check
|
# The write changes the file's mtime, so edit_lineno's freshness check
|
||||||
# rejects stale line numbers until the model re-reads with_lineno.
|
# 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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ def test_edit_lineno_replace_middle(workspace: object) -> None:
|
|||||||
_ = _read_lineno("a.txt")
|
_ = _read_lineno("a.txt")
|
||||||
out = call_sync(edit_lineno, "a.txt", 2, 3, "TWO\nTHREE\n")
|
out = call_sync(edit_lineno, "a.txt", 2, 3, "TWO\nTHREE\n")
|
||||||
assert "replaced lines 2-3" in out
|
assert "replaced lines 2-3" in out
|
||||||
|
assert "first: 'two'" in out # old-content excerpt for self-correction
|
||||||
assert call_sync(read_file, "a.txt") == "L1-4\none\nTWO\nTHREE\nfour\n"
|
assert call_sync(read_file, "a.txt") == "L1-4\none\nTWO\nTHREE\nfour\n"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ def test_edit_replace_first_only(workspace: object) -> None:
|
|||||||
assert call_sync(read_file, "t.txt") == "L1-1\nbb aa"
|
assert call_sync(read_file, "t.txt") == "L1-1\nbb aa"
|
||||||
assert "1 of 2" in result or "1 of 2 matches" in result
|
assert "1 of 2" in result or "1 of 2 matches" in result
|
||||||
assert "remain" in result
|
assert "remain" in result
|
||||||
|
assert "'aa' → 'bb'" in result # old→new excerpt for self-correction
|
||||||
|
|
||||||
|
|
||||||
def test_edit_replace_max_replaces(workspace: object) -> None:
|
def test_edit_replace_max_replaces(workspace: object) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user