Description
Problem
The harness file tools are asymmetric about lines. On the edit side they are line-precise: file_access_replace_lines targets 1-based line numbers, and file_access_grep reports matching_lines with those same numbers (the two share the \n-only, keepends split of _split_lines_keepends, so a grep number always addresses the same line the editor touches). On the read side there is no line access at all: file_access_read takes only file_name and returns the entire content (_ReadFileInput, _file_access.py), and the AgentFileStore.read(path) contract underneath is likewise all-or-nothing.
That leaves a hole in exactly the workflow the line-precise edit tools exist for. The natural point-edit loop is:
file_access_grep — find the location (line numbers),
- see the surrounding lines to write a correct edit,
file_access_replace_lines / file_access_replace — make the point edit.
Step 2 has no tool. The model's options are:
- Re-read the whole file — token-expensive on large artifacts, and self-defeating under any tool-result cap: an application that truncates or spools oversized tool results (ours caps them and spills to the store) truncates precisely the large files where partial reads matter most, so the model may never see the region it needs.
- Grep again with a broader pattern —
file_access_grep's snippet and matching_lines are anchored to the pattern, not to a requested range; "show me lines 120–160" is not expressible.
Observed effect in production (harness + AG-UI, advisor + headless sub-agents): models told to point-edit fall back to whole-file file_access_read before nearly every replace_lines, and smaller models then skip the read and guess — producing the wrong-line edits the line-precise editor was added to prevent.
Requested
Either shape works; the essential property is numbering parity with grep/replace_lines (same _split_lines_keepends semantics, including the trailing-newline final empty line):
- New tool
file_access_read_lines(file_name, start_line, end_line=None) — 1-based inclusive range, end_line=None → end of file, out-of-range end_line clamps. Returning line-numbered text lets the model feed numbers straight back into file_access_replace_lines.
- Optional params on the existing read —
file_access_read(file_name, start_line=None, end_line=None) (or offset/limit), defaulting to today's whole-file behavior.
Implementation stays inside FileAccessProvider — read the full content via the existing AgentFileStore.read, slice with _split_lines_keepends, no store-protocol change needed. (A store-level partial read would be a separate optimization; the context-window win comes from not returning the whole file to the model.)
A new tool (option 1) composes with the existing surface: disable_write_tools children keep it (it is a read), disable_readonly_tool_approval should govern it exactly like file_access_read, and read_only_tools_auto_approval_rule should match it.
Workaround (application-side, today)
A hand-rolled @tool bound to the same store the provider holds (ats.tools.read_lines.make_read_lines_tool): normalizes paths with the provider's own _normalize_relative_path, splits with _split_lines_keepends (both imported from agent_framework._harness._file_access with fallbacks — private imports this FR would remove), mirrors the provider's read-approval mode, and is wired separately into the advisor and each headless sub-agent. It works, but it reaches into two private helpers to guarantee numbering parity, and every consumer has to remember to wire it beside the provider — exactly the kind of duplication a provider-native range read would remove.
Code Sample
Language/SDK
Python
Description
Problem
The harness file tools are asymmetric about lines. On the edit side they are line-precise:
file_access_replace_linestargets 1-based line numbers, andfile_access_grepreportsmatching_lineswith those same numbers (the two share the\n-only, keepends split of_split_lines_keepends, so a grep number always addresses the same line the editor touches). On the read side there is no line access at all:file_access_readtakes onlyfile_nameand returns the entire content (_ReadFileInput,_file_access.py), and theAgentFileStore.read(path)contract underneath is likewise all-or-nothing.That leaves a hole in exactly the workflow the line-precise edit tools exist for. The natural point-edit loop is:
file_access_grep— find the location (line numbers),file_access_replace_lines/file_access_replace— make the point edit.Step 2 has no tool. The model's options are:
file_access_grep's snippet andmatching_linesare anchored to the pattern, not to a requested range; "show me lines 120–160" is not expressible.Observed effect in production (harness + AG-UI, advisor + headless sub-agents): models told to point-edit fall back to whole-file
file_access_readbefore nearly everyreplace_lines, and smaller models then skip the read and guess — producing the wrong-line edits the line-precise editor was added to prevent.Requested
Either shape works; the essential property is numbering parity with grep/replace_lines (same
_split_lines_keependssemantics, including the trailing-newline final empty line):file_access_read_lines(file_name, start_line, end_line=None)— 1-based inclusive range,end_line=None→ end of file, out-of-rangeend_lineclamps. Returning line-numbered text lets the model feed numbers straight back intofile_access_replace_lines.file_access_read(file_name, start_line=None, end_line=None)(oroffset/limit), defaulting to today's whole-file behavior.Implementation stays inside
FileAccessProvider— read the full content via the existingAgentFileStore.read, slice with_split_lines_keepends, no store-protocol change needed. (A store-level partial read would be a separate optimization; the context-window win comes from not returning the whole file to the model.)A new tool (option 1) composes with the existing surface:
disable_write_toolschildren keep it (it is a read),disable_readonly_tool_approvalshould govern it exactly likefile_access_read, andread_only_tools_auto_approval_ruleshould match it.Workaround (application-side, today)
A hand-rolled
@toolbound to the same store the provider holds (ats.tools.read_lines.make_read_lines_tool): normalizes paths with the provider's own_normalize_relative_path, splits with_split_lines_keepends(both imported fromagent_framework._harness._file_accesswith fallbacks — private imports this FR would remove), mirrors the provider's read-approval mode, and is wired separately into the advisor and each headless sub-agent. It works, but it reaches into two private helpers to guarantee numbering parity, and every consumer has to remember to wire it beside the provider — exactly the kind of duplication a provider-native range read would remove.Code Sample
Language/SDK
Python