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