From b0da909454660f89b78e27f670b834f87780fab8 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 11 Sep 2026 19:06:04 +0700 Subject: [PATCH] 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. --- .hermes/plans/read-batch-priority.md | 39 ++++++++++++++++++++++++++++ docs/tools.md | 4 +-- src/agents.ts | 3 ++- src/prompt.ts | 5 ++-- src/tools.ts | 2 +- test/prompt.test.ts | 12 +++++++++ test/session-features.test.ts | 2 ++ 7 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 .hermes/plans/read-batch-priority.md diff --git a/.hermes/plans/read-batch-priority.md b/.hermes/plans/read-batch-priority.md new file mode 100644 index 0000000..644b54e --- /dev/null +++ b/.hermes/plans/read-batch-priority.md @@ -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. \ No newline at end of file diff --git a/docs/tools.md b/docs/tools.md index 81b57b1..7998587 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -66,8 +66,8 @@ Sets let you switch off what a project does not need: | Set | Tools | Cost | |---|---|---| -| `core` | `read_file` `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 | +| `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` `apply_patch` `move_file` `delete_file` | patch and file ops | | `nav` | `find_symbol` `json_query` | navigation and structured reads | | `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 | diff --git a/src/agents.ts b/src/agents.ts index b66d628..dd13d47 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -70,7 +70,8 @@ export const VARIANTS: AgentVariant[] = [ maxSteps: 80, appendix: '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.', }, { diff --git a/src/prompt.ts b/src/prompt.ts index 11a0d06..88f33aa 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -36,10 +36,10 @@ type ToolDoc = { name: string; line: string }; * a withheld tool teaches the model to attempt calls that cannot succeed. */ 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', - 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', @@ -186,6 +186,7 @@ export function systemPrompt(parts: PromptParts): string { 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 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.', '- Match the existing style, libraries, and conventions. Sample a neighbouring file before inventing a pattern.', approvalTools.length > 0 diff --git a/src/tools.ts b/src/tools.ts index b9e6407..17a15c6 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -56,7 +56,7 @@ export const readFileTool = withMeta({ set: 'core', mutating: false }, tool({ 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: '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 ' + diff --git a/test/prompt.test.ts b/test/prompt.test.ts index 4de8a5a..8e6774d 100644 --- a/test/prompt.test.ts +++ b/test/prompt.test.ts @@ -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', () => { const rendered = renderTools(['read_file', 'grep']); expect(rendered).toContain('read_file'); diff --git a/test/session-features.test.ts b/test/session-features.test.ts index 40b90cd..6c7a667 100644 --- a/test/session-features.test.ts +++ b/test/session-features.test.ts @@ -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', () => { 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('git_log')).toBe('git'); expect(toolSetOf('todo_write')).toBeUndefined();