diff --git a/src/fastcontext/agent/tool/read.py b/src/fastcontext/agent/tool/read.py index a0caa76..fa32e26 100644 --- a/src/fastcontext/agent/tool/read.py +++ b/src/fastcontext/agent/tool/read.py @@ -21,7 +21,7 @@ class ReadTool(Tool): }, "offset": { "type": "integer", - "description": "The line number to start reading from. Positive values are 1-indexed from the start of the file. Only provide if the file is too large to read at once.", + "description": "The line number to start reading from. Positive values are 1-indexed from the start of the file. Negative values count backwards from the end of the file (e.g. -1 starts at the last line). Only provide if the file is too large to read at once.", }, "limit": { "type": "integer", @@ -47,8 +47,8 @@ async def call(self, parameters: str, **kwargs) -> str: if not Path(file_path).exists(): return f"Error: {file_path} does not exist" - if not isinstance(offset, int) or offset <= 0: - return "Error: offset must be a positive integer" + if not isinstance(offset, int) or isinstance(offset, bool) or offset == 0: + return "Error: offset must be a non-zero integer" if limit is not None and (not isinstance(limit, int) or limit <= 0): return "Error: limit must be a positive integer" @@ -59,6 +59,11 @@ async def call(self, parameters: str, **kwargs) -> str: if len(raw_lines) == 0: return "File is empty." + # Negative offsets count backwards from the end of the file; clamp to the + # start so an over-long negative offset still reads from line 1. + if offset < 0: + offset = max(1, len(raw_lines) + offset + 1) + end_line = -1 if limit is not None: end_line = offset + limit - 1 diff --git a/tests/test_read_negative_offset.py b/tests/test_read_negative_offset.py new file mode 100644 index 0000000..c101d25 --- /dev/null +++ b/tests/test_read_negative_offset.py @@ -0,0 +1,37 @@ +import asyncio +import json +import tempfile +from pathlib import Path + +from fastcontext.agent.tool.read import ReadTool + + +def _read(read, cwd, params): + return asyncio.run(read.call(json.dumps(params), cwd=cwd)) + + +def test_read_negative_offset_counts_from_end(): + """A negative offset must count backwards from the end of the file + (FastContext paper, Appendix E, p. 19: 'Negative values count backwards + from the end').""" + read = ReadTool() + with tempfile.TemporaryDirectory() as cwd: + p = Path(cwd) / "f.txt" + p.write_text("".join(f"line{i}\n" for i in range(1, 11)), encoding="utf-8") # 10 lines + + # -2 -> start at line 9, read to end. + out = _read(read, cwd, {"path": str(p), "offset": -2}) + assert "9|line9" in out and "10|line10" in out + assert "8|line8" not in out + + # -1 with limit 1 -> only the last line. + out = _read(read, cwd, {"path": str(p), "offset": -1, "limit": 1}) + assert "10|line10" in out and "9|line9" not in out + + # Over-long negative offset clamps to the start of the file. + out = _read(read, cwd, {"path": str(p), "offset": -999}) + assert "1|line1" in out + + # Zero is still rejected. + out = _read(read, cwd, {"path": str(p), "offset": 0}) + assert "Error" in out and "non-zero" in out