feat: prioritize batched reads for turn efficiency
ci / check (macos-latest) (push) Canceled after 0s
ci / check (ubuntu-latest) (push) Canceled after 0s
ci / check (windows-latest) (push) Canceled after 0s

Move read_many_files to core tool set so it is always offered. Strengthen
system-prompt guidance: read_file now redirects to read_many_files for
multiple files, read_many_files is framed as the primary reading tool with
an explicit batch range (2-20). Add a 'How to work' rule on read
efficiency, and a read-batching instruction in the deep agent variant.
861 tests pass; build clean.
This commit is contained in:
asepharyana
2026-09-11 19:06:04 +07:00
parent 7d77408a41
commit b0da909454
7 changed files with 61 additions and 6 deletions
+39
View File
@@ -0,0 +1,39 @@
# Prioritize batched reads (read_many_files) for turn efficiency
## Problem
The model frequently calls `read_file` once per file when investigating, costing one
round trip each. `read_many_files` already exists and batches 2-20 files in one call,
but the guidance is weak: it is in `edit-plus` (not `core`), and the system prompt does
not tell the model to prefer batching. Turns burn more steps than needed.
## Scope
- Move `read_many_files` into the `core` tool set so it is always offered (even with
minimal tool sets or a read-only variant).
- Strengthen the per-tool guidance in `src/prompt.ts` TOOL_DOCS: `read_file` says
"prefer read_many_files when you need several files"; `read_many_files` becomes the
primary reading instruction with an explicit batch hint (2-20).
- Add one "How to work" rule about reading efficiently (batch, grep/outline first,
never read a file twice).
- `deep` agent variant: add a read-batching instruction to its appendix.
- Tests: assert `toolSetOf('read_many_files') === 'core'`; assert TOOL_DOCS contains
the batching guidance; adjust any test asserting `read_many_files` is not core.
- Docs: mention read_many_files as the default reading tool in docs/tools.md.
## Files touched
- `src/tools.ts` — readManyFilesTool meta set: `'edit-plus'` → `'core'`.
- `src/prompt.ts` — TOOL_DOCS lines + a workflow rule.
- `src/agents.ts` — deep appendix line.
- `test/session-features.test.ts` or `test/tools.test.ts` — set assertion.
- `test/prompt.test.ts` — presence of batching guidance.
- `docs/tools.md` — reading guidance.
## Verification
- `bun run typecheck` clean.
- `bun test` — full suite green (tool sets + prompt + deep agent tests).
- `bun run build` compiles.
## Decision
Batched reads save one round trip per extra file — the single highest-leverage
efficiency win for investigation-heavy turns. Keep `read_file` (single-file reads,
offset/limit, still needed for one file) but steer batching as the default once the
file set is known.
+2 -2
View File
@@ -66,8 +66,8 @@ Sets let you switch off what a project does not need:
| Set | Tools | Cost | | Set | Tools | Cost |
|---|---|---| |---|---|---|
| `core` | `read_file` `write_file` `edit_file` `glob` `grep` `bash` `bash_status` `bash_stop` | ~3,200 B | | `core` | `read_file` `read_many_files` `write_file` `edit_file` `glob` `grep` `bash` `bash_status` `bash_stop` | ~3,200 B |
| `edit-plus` | `multi_edit` `list_dir` `read_many_files` `apply_patch` `move_file` `delete_file` | patch and file ops | | `edit-plus` | `multi_edit` `list_dir` `apply_patch` `move_file` `delete_file` | patch and file ops |
| `nav` | `find_symbol` `json_query` | navigation and structured reads | | `nav` | `find_symbol` `json_query` | navigation and structured reads |
| `extra` | 20 tools: line edits, fs inspect, git extensions, code/env reads | on by default | | `extra` | 20 tools: line edits, fs inspect, git extensions, code/env reads | on by default |
| `git` | `git_status` `git_diff` `git_log` `git_show` `git_blame` `git_branch` `git_commit_message` | ~2,180 B + message | | `git` | `git_status` `git_diff` `git_log` `git_show` `git_blame` `git_branch` `git_commit_message` | ~2,180 B + message |
+2 -1
View File
@@ -70,7 +70,8 @@ export const VARIANTS: AgentVariant[] = [
maxSteps: 80, maxSteps: 80,
appendix: appendix:
'This task is hard or its cause is unclear. Form more than one hypothesis before you act and say which one ' + 'This task is hard or its cause is unclear. Form more than one hypothesis before you act and say which one ' +
'you are testing. Read enough of the code to be sure rather than guessing. Record findings with remember ' + 'you are testing. Read enough of the code to be sure rather than guessing — batch related files into a single ' +
'read_many_files call so the investigation stays cheap. Record findings with remember ' +
'so they survive compaction. Report what you verified and what you could not.', 'so they survive compaction. Report what you verified and what you could not.',
}, },
{ {
+3 -2
View File
@@ -36,10 +36,10 @@ type ToolDoc = { name: string; line: string };
* a withheld tool teaches the model to attempt calls that cannot succeed. * a withheld tool teaches the model to attempt calls that cannot succeed.
*/ */
const TOOL_DOCS: ToolDoc[] = [ const TOOL_DOCS: ToolDoc[] = [
{ name: 'read_file', line: 'read before you edit. Never describe code you have not opened.' }, { name: 'read_file', line: 'read one file before you edit. Never describe code you have not opened. When you need several files, batch them in one read_many_files call instead of N round trips.' },
{ {
name: 'read_many_files', name: 'read_many_files',
line: 'read several files in one round trip once you know which ones you need. An unreadable path is reported in place, not fatal.', line: 'the primary reading tool: batch 2-20 files in one round trip once you know which you need. Per-file offset/limit; an unreadable path is reported in place, not fatal. Prefer this over repeated read_file calls.',
}, },
{ {
name: 'glob', name: 'glob',
@@ -186,6 +186,7 @@ export function systemPrompt(parts: PromptParts): string {
const workflow = [ const workflow = [
'- Read before you write. Ground every claim about the code in something you actually opened. Never describe code you have not read.', '- Read before you write. Ground every claim about the code in something you actually opened. Never describe code you have not read.',
'- Read efficiently: batch the files you need in one read_many_files call, use grep or outline before opening a large file, and never read the same file twice.',
'- Make the smallest change that solves the task. A bugfix diff contains only the bug; a feature diff contains only the feature.', '- Make the smallest change that solves the task. A bugfix diff contains only the bug; a feature diff contains only the feature.',
'- Match the existing style, libraries, and conventions. Sample a neighbouring file before inventing a pattern.', '- Match the existing style, libraries, and conventions. Sample a neighbouring file before inventing a pattern.',
approvalTools.length > 0 approvalTools.length > 0
+1 -1
View File
@@ -56,7 +56,7 @@ export const readFileTool = withMeta({ set: 'core', mutating: false }, tool({
const MAX_BATCH_FILES = 20; const MAX_BATCH_FILES = 20;
export const readManyFilesTool = withMeta({ set: 'edit-plus', mutating: false }, tool({ export const readManyFilesTool = withMeta({ set: 'core', mutating: false }, tool({
description: description:
'Read several text files in one call. Use it when you already know which files you need — one round trip ' + 'Read several text files in one call. Use it when you already know which files you need — one round trip ' +
'instead of one per file. Each file may set its own offset and limit. A path that cannot be read is reported ' + 'instead of one per file. Each file may set its own offset and limit. A path that cannot be read is reported ' +
+12
View File
@@ -10,6 +10,18 @@ test('every documented tool has usable guidance', () => {
} }
}); });
test('read guidance steers the model to batch reads for efficiency', () => {
const readMany = TOOL_DOCS.find((d) => d.name === 'read_many_files');
const readOne = TOOL_DOCS.find((d) => d.name === 'read_file');
expect(readMany?.line).toContain('batch');
expect(readMany?.line).toContain('read_file');
expect(readOne?.line).toContain('read_many_files');
// The "How to work" block also instructs batched reading.
const prompt = systemPrompt({ cwd: '/repo', availableTools: ['read_file', 'read_many_files'] });
expect(prompt).toContain('Read efficiently');
expect(prompt).toContain('read_many_files');
});
test('only the offered tools are described', () => { test('only the offered tools are described', () => {
const rendered = renderTools(['read_file', 'grep']); const rendered = renderTools(['read_file', 'grep']);
expect(rendered).toContain('read_file'); expect(rendered).toContain('read_file');
+2
View File
@@ -307,6 +307,8 @@ test('session tools survive tool-set gating, since they are not part of that bud
test('toolSetOf names the set a tool came from, and nothing for a session tool', () => { test('toolSetOf names the set a tool came from, and nothing for a session tool', () => {
expect(toolSetOf('read_file')).toBe('core'); expect(toolSetOf('read_file')).toBe('core');
// Batched reading is core so it is always offered — efficiency, not an extra set.
expect(toolSetOf('read_many_files')).toBe('core');
expect(toolSetOf('multi_edit')).toBe('edit-plus'); expect(toolSetOf('multi_edit')).toBe('edit-plus');
expect(toolSetOf('git_log')).toBe('git'); expect(toolSetOf('git_log')).toBe('git');
expect(toolSetOf('todo_write')).toBeUndefined(); expect(toolSetOf('todo_write')).toBeUndefined();