From 38671482f2d5114192084188672074c2c8374de0 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Sat, 15 Aug 2026 17:38:58 +0800 Subject: [PATCH] core/tools: edit_lineno unread error names the read window The boundary overguard (read offset=50 shows lines 51+, then editing line 50 fails) forced blind re-reads. The error now reports the read window ("this turn read lines 51-86") and advises reading with a margin around the target range; read_file's description says the same. --- src/plyngent/tools/file/edit_lineno.py | 11 +++++++++-- src/plyngent/tools/file/read.py | 2 ++ tests/test_tools/test_edit_lineno.py | 12 ++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/plyngent/tools/file/edit_lineno.py b/src/plyngent/tools/file/edit_lineno.py index eecb7ab..d8f25a7 100644 --- a/src/plyngent/tools/file/edit_lineno.py +++ b/src/plyngent/tools/file/edit_lineno.py @@ -111,13 +111,20 @@ async def edit_lineno(path: str, start_line: int, end_line: int, new_content: st if err is not None: return err - unread = _unread_lines_in_range(lineno_read_lines(str(target)), start_line, end_line, len(lines)) + read_lines = lineno_read_lines(str(target)) + unread = _unread_lines_in_range(read_lines, start_line, end_line, len(lines)) if unread: preview = unread[:_UNREAD_PREVIEW] head = ", ".join(str(i) for i in preview) if len(unread) > _UNREAD_PREVIEW: head += f", … ({len(unread)} total)" - return f"error: lines {head} not read; call read_file(path, with_lineno=true) first to see current line numbers" + window = "" + if read_lines: + window = f" (this turn read lines {min(read_lines)}-{max(read_lines)})" + return ( + f"error: lines {head} not read{window}; call read_file(path, with_lineno=true) " + "first — read with a small margin around the lines you plan to edit" + ) recorded_mtime = lineno_read_mtime(str(target)) if recorded_mtime is not None: diff --git a/src/plyngent/tools/file/read.py b/src/plyngent/tools/file/read.py index edf2695..f78cb75 100644 --- a/src/plyngent/tools/file/read.py +++ b/src/plyngent/tools/file/read.py @@ -64,6 +64,8 @@ async def read_file( (None = rest of file). When ``with_lineno`` is true, each line is prefixed with its 1-based file line number (`` N|…``), matching ``edit_lineno`` numbering, and those lines are marked readable for ``edit_lineno`` this turn. + Only the displayed lines become editable, so read with a small margin around + any range you plan to edit with ``edit_lineno``. ``max_chars`` caps the returned slice (default: the agent's tool-result cap, ``DEFAULT_TOOL_RESULT_MAX_CHARS``); a ``truncate_token`` is appended when cut diff --git a/tests/test_tools/test_edit_lineno.py b/tests/test_tools/test_edit_lineno.py index f716bdb..e83f2b0 100644 --- a/tests/test_tools/test_edit_lineno.py +++ b/tests/test_tools/test_edit_lineno.py @@ -38,6 +38,18 @@ def test_edit_lineno_read_truncated_marks_only_displayed_lines(workspace: object assert "not read" in out and "90" in out +def test_edit_lineno_error_shows_read_window(workspace: object) -> None: + """The unread error names the read window so the model can re-read precisely.""" + del workspace + _ = call_sync(write_file, "j.txt", "a\nb\nc\nd\n") + _ = reset_lineno_tracker() + _ = call_sync(read_file, "j.txt", offset=1, limit=2, with_lineno=True) # lines 2-3 + out = call_sync(edit_lineno, "j.txt", 1, 1, "X\n") + assert "not read" in out + assert "read lines 2-3" in out + assert "margin" in out + + def test_edit_lineno_requires_all_lines_read(workspace: object) -> None: del workspace _ = call_sync(write_file, "r.txt", "a\nb\nc\nd\ne\n")