Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/fastcontext/agent/tool/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -47,8 +47,8 @@ async def call(self, parameters: str, **kwargs) -> str:
if not Path(file_path).exists():
return f"<system-reminder>Error: {file_path} does not exist</system-reminder>"

if not isinstance(offset, int) or offset <= 0:
return "<system-reminder>Error: offset must be a positive integer</system-reminder>"
if not isinstance(offset, int) or isinstance(offset, bool) or offset == 0:
return "<system-reminder>Error: offset must be a non-zero integer</system-reminder>"

if limit is not None and (not isinstance(limit, int) or limit <= 0):
return "<system-reminder>Error: limit must be a positive integer</system-reminder>"
Expand All @@ -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
Expand Down
37 changes: 37 additions & 0 deletions tests/test_read_negative_offset.py
Original file line number Diff line number Diff line change
@@ -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 "<system-reminder>Error" in out and "non-zero" in out