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 addressesthe 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)contractunderneath 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:
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.
file_access_grep's snippet andmatching_linesare anchored to the pattern, not to a requested range; "show melines 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 thewrong-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-basedinclusive range,
end_line=None→ end of file, out-of-rangeend_lineclamps.Returning line-numbered text lets the model feed numbers straight back into
file_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 changeneeded. (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_toolschildrenkeep 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 wouldremove), 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