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):