From 43c32ade000028bc76025eb0b8e96b18ffcd399e Mon Sep 17 00:00:00 2001 From: FanouZeng-TT <124567600+FanouZeng-TT@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:10:27 +0800 Subject: [PATCH] fix(tenki): honour start_line and end_line in read_file (#5616) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(tenki): honour start_line and end_line in read_file The base Sandbox contract declares read_file(path, start_line, end_line), and the tools layer passes both keyword arguments on every ranged read — including the continuation path that a truncated read names with its start_line marker. TenkiSandbox.read_file accepted only path, so any ranged read through a Tenki sandbox raised TypeError and surfaced as "Unexpected error reading file"; a truncated read could not be continued. Slice the text the way the other providers do (e2b, opensandbox, boxlite): the full text when no range is given, otherwise the selected lines joined with newlines. * fix(tenki): clamp a negative start_line and pin the out-of-range read contract Mirror LocalSandbox.read_file and clamp the start line to at least 1 so a negative start_line cannot wrap around through Python's negative-index slicing. Also extend the ranged-read test with the two boundary cases the tools layer depends on: a start past EOF returns an empty string, and a negative start reads from the first line. * fix(tenki): clamp a negative end_line in read_file Mirror LocalSandbox.read_file for the symmetric range boundary: clamp a negative end_line to zero so Python's negative-index slicing cannot silently drop the last line. Pin the empty result in the existing ranged-read test. --- .../deerflow/community/tenki/sandbox.py | 18 ++++++++++++++++-- backend/tests/test_tenki_provider.py | 17 +++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/backend/packages/harness/deerflow/community/tenki/sandbox.py b/backend/packages/harness/deerflow/community/tenki/sandbox.py index e1b1a926a..4a29841ce 100644 --- a/backend/packages/harness/deerflow/community/tenki/sandbox.py +++ b/backend/packages/harness/deerflow/community/tenki/sandbox.py @@ -282,13 +282,27 @@ class TenkiSandbox(Sandbox): # ── file operations ───────────────────────────────────────────────── - def read_file(self, path: str) -> str: + def read_file( + self, + path: str, + start_line: int | None = None, + end_line: int | None = None, + ) -> str: resolved = self._resolve_path(path) try: - return self._fs_op(lambda fs: fs.read_text(resolved)) + content = self._fs_op(lambda fs: fs.read_text(resolved)) except Exception as e: logger.error("read_file %s failed: %s", resolved, e) return f"Error: {e}" + if start_line is None and end_line is None: + return content + lines = (content or "").splitlines() + # Clamp like LocalSandbox.read_file: a negative start would otherwise + # wrap around through Python's negative-index slicing instead of + # reading from the first line. + start = max(start_line or 1, 1) + end = max(end_line, 0) if end_line is not None else len(lines) + return "\n".join(lines[start - 1 : end]) def write_file(self, path: str, content: str, append: bool = False) -> None: self._write_bytes(self._resolve_path(path), content.encode("utf-8"), append=append) diff --git a/backend/tests/test_tenki_provider.py b/backend/tests/test_tenki_provider.py index 8134b894f..0ee85e0dd 100644 --- a/backend/tests/test_tenki_provider.py +++ b/backend/tests/test_tenki_provider.py @@ -514,6 +514,23 @@ def test_read_missing_file_returns_error() -> None: assert box.read_file("/mnt/user-data/workspace/nope.txt").startswith("Error:") +def test_read_file_supports_bounded_ranges() -> None: + """The tools layer passes ``start_line``/``end_line`` on every ranged read.""" + box = TenkiSandbox("sb", _FakeSandbox()) + box.write_file("/mnt/user-data/workspace/range.txt", "line 1\nline 2\nline 3\nline 4\nline 5") + assert box.read_file("/mnt/user-data/workspace/range.txt") == "line 1\nline 2\nline 3\nline 4\nline 5" + assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=2, end_line=4) == "line 2\nline 3\nline 4" + assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=4) == "line 4\nline 5" + assert box.read_file("/mnt/user-data/workspace/range.txt", end_line=2) == "line 1\nline 2" + # A start past EOF comes back empty rather than raising: the tool layer's + # "(start_line exceeds file length)" message and the truncated-read + # continuation path both depend on that contract. + assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=99) == "" + # Negative bounds clamp like LocalSandbox instead of wrapping around. + assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=-1) == ("line 1\nline 2\nline 3\nline 4\nline 5") + assert box.read_file("/mnt/user-data/workspace/range.txt", end_line=-1) == "" + + def test_download_missing_file_raises_oserror() -> None: box = TenkiSandbox("sb", _FakeSandbox()) with pytest.raises(OSError):