feat: prioritize batched reads for turn efficiency
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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 ' +
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user