mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 14:06:18 +00:00
fix(tenki): honour start_line and end_line in read_file (#5616)
* 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.
This commit is contained in:
parent
6db7e8091f
commit
43c32ade00
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user