speedread
Read is the wrong abstraction for coding agents.
Agent file reading should be adaptive, stateful, symbol-aware and token-budgeted instead of byte-oriented. ripgrep returns matches; speedread returns the minimum useful unit of code.
An open-source (MIT) MCP server and CLI · github.com/brennengreen/speedread · brew install brennengreen/tap/speedread
Watch one real task
Both sides are recorded eval transcripts, replayed at recorded speed. The built-in grep answers with a file name, so the agent has to ask again. speedread's search answers with the matching lines under their enclosing declaration, with line ranges.
built-in tools
Sources/ArgumentParser/Parsable Types/ParsableCommand.swift
Sources/ArgumentParser/Parsable Types/ParsableCommand.swift
16: static var configuration: CommandConfiguration { get }
20: /// This is generated from the configuration, if given, or from the type
39: configuration.commandName
43: public static var configuration: CommandConfiguration {
204: configuration.defaultSubcommand?.includesPassthroughArguments == true
212: for sub in command.configuration.subcommands {
215: guard sub.configuration.subcommands.isEmpty else { continue }
218: configurationFailure(
224: With this configuration, your asynchronous `run()` method will not be called. To fix this issue, change `\(root)`'s `ParsableCommand` conformance to `AsyncParsableCommand`.
233: for sub in command.configuration.subcommands {
235: sub.configuration.subcommands.isEmpty
255: configurationFailure(
261: With this configuration, your asynchronous `run()` method will not be called. To fix this issue, change `\(rootCommand)`'s `ParsableCommand` conformance to `AsyncParsableCommand`.
266: configurationFailure(
<shellId: 0 completed with exit code 0>speedread
2 matches in 1 file for /static var configuration/
==> Sources/ArgumentParser/Parsable Types/ParsableCommand.swift @ba951bb7 (2 matches)
[12-33] public protocol ParsableCommand: ParsableArguments
16 static var configuration: CommandConfiguration { get }
[43-45] public static var configuration: CommandConfiguration (in ParsableCommand)
43 public static var configuration: CommandConfiguration {Real agent A/B
| Task | Category | Tokens, built-in | Tokens, speedread | Δ | AIU, built-in | AIU, speedread | Model calls | Pass |
|---|---|---|---|---|---|---|---|---|
flask-shell-context | control: small symbol | 71,739 | 50,965 | −29% | 3.56 | 2.87 | 3.3 → 2.3 | 100% → 100% |
gin-go-version | control: tiny file | 42,568 | 43,219 | +2% | 2.55 | 2.53 | 2.0 → 2.0 | 100% → 100% |
ripgrep-fixed-strings | large file (8,161 lines) | 64,584 | 44,000 | −32% | 3.38 | 2.78 | 3.0 → 2.0 | 100% → 100% |
ripgrep-walk-run | large file (2,740 lines) | 78,985 | 43,424 | −45% | 3.74 | 2.62 | 3.7 → 2.0 | 100% → 100% |
zod-ip-kinds | large file (5,138 lines) | 110,832 | 52,281 | −53% | 4.90 | 3.07 | 5.0 → 2.3 | 67% → 100% |
flask-dispatch-lines | large file, several symbols | 64,331 | 43,565 | −32% | 3.32 | 2.65 | 3.0 → 2.0 | 100% → 100% |
gin-abort-callers | cross-file search | 43,141 | 43,656 | +1% | 2.78 | 2.71 | 2.0 → 2.0 | 100% → 100% |
vscode-setvalue | huge repo (19k files) | 134,957 | 81,389 | −40% | 7.55 | 6.11 | 5.0 → 3.0 | 100% → 100% |
alamofire-start-immediately | Swift, large file (1,441 lines) | 57,398 | 43,812 | −24% | 3.15 | 2.69 | 2.7 → 2.0 | 100% → 100% |
sap-configuration | Swift, protocol requirement | 86,578 | 43,528 | −50% | 4.04 | 2.63 | 4.0 → 2.0 | 100% → 100% |
Where the savings come from: every model call re-sends the system prompt, tool definitions and conversation (~21k tokens here), so an answer in one call instead of three saves two full round trips. Tool results were about the same size in both conditions.
Tool-level scenarios
The budget contract
Calibrated against a production tokenizer
Offline tokenizers are proxies. The harness logs exact input tokens per model call, so the tokens each tool result added can be recovered from consecutive calls (162 read-tool results; Theil–Sen fit, framing overhead ≈27 tokens). On claude-sonnet-5, real counts are 1.22× speedread's estimate at the median (p90 1.46×; 1.36× for speedread's own output), 1.35× the legacy Claude tokenizer and 1.54× o200k — consistent with Anthropic's note that Claude 4.7+ tokenizers produce ~30% more tokens. The content-aware estimator fixes relative density; the Claude profile (automatic for Claude clients, or SPEEDREAD_TOKENIZER=claude) scales budgets 1.4× so a response lands at ≈87% of its budget at the median.
Relationship questions
Coding tasks (SWE-style)
Injected regressions in gin (Go) and flask (Python); symptom-only bug reports; graded by each repository's full test suite with tests unmodified; every task verified to fail as injected and pass with the reference fix. Conditions: built-in tools · speedread available · speedread preferred · speedread exclusive.
| Condition | Trials | Pass | Input tokens | Model calls | Tool calls | Read-result tokens | AIU |
|---|---|---|---|---|---|---|---|
| baseline | 16 | 100% | 166,147 | 7.8 | 6.9 | 1,604 | 7.84 |
| available | 16 | 100% | 243,091 | 9.4 | 8.4 | 1,671 | 10.15 |
| preferred | 16 | 100% | 160,357 | 6.5 | 5.5 | 1,906 | 7.50 |
| exclusive | 16 | 100% | 165,307 | 7.2 | 6.2 | 1,622 | 7.37 |
The baseline already shows where agent cost lives: read and search results averaged 1,604 tokens per task, about 1% of 166,147 input tokens. The rest is the conversation being re-sent on each of 7.8 round trips, so a better reader can only save tokens by saving turns. With guidance, speedread took fewer turns on most tasks and less model time, and tokens stayed within noise. Merely installed, it was never used, and it made every task more expensive.
Did compression hide the bug? Among trials that used speedread, those where the file with the bug was first shown as a skeleton or outline, before the buggy line itself: preferred: 0 of 16 trials (never expanded 0, failed 0); exclusive: 0 of 16 trials (never expanded 0, failed 0). Agents searched first, then read exact ranges.
Claude Code caveat, quantified. Claude Code requires a native Read of a file before editing it, and MCP reads don't count. Adding a full Read of every edited file the agent hadn't viewed natively, re-sent on every later call (an upper bound), gives input tokens per task of: baseline 166,147; preferred 182,872 (+10%); exclusive 196,947 (+19%). On edit-heavy work in Claude Code, expect speedread to save turns and time rather than tokens.
How it works
budgeted, importance-weighted tree with line counts; symbols on request
ripgrep engine; hits grouped under their enclosing function or class, with its line range
callers, callees, references, implementations; syntactic and receiver-aware
batched targets and symbols; skeletons instead of blind cuts;
path@etag symbol-aware diffsMethod
Suites follow Anthropic's Demystifying evals for AI agents: explicit tasks, repeated trials, deterministic outcome graders, pass@k and pass^k, balanced tasks (with controls where speedread should not win), isolated trials, and transcripts read. Reading them found a wrong grader, an invalid task and the adoption problem. The tools follow Code execution with MCP: four tools (~1.4k tokens of definitions), filtering before results reach the model, and a CLI with JSON Lines for agents that script. Details: evals/README.md.
Research
- Anthropic: Demystifying evals for AI agents
- Anthropic: Code execution with MCP
- Anthropic: Effective context engineering for AI agents
- Anthropic: Writing effective tools for agents
- AgentDiet: trajectory reduction, 40–60% fewer input tokens without loss
- Chroma: Context Rot, accuracy falls with input length
- Lost in the Middle
- Agentless: file → skeleton → lines localization
- SWE-agent: agent-computer interfaces
- AutoCodeRover: AST search APIs
- LocAgent: code graphs for localization
- CodeCompass: 58% of trials never called a better navigation tool
- Manus: context engineering lessons (~100:1 input:output)
- Aider: tree-sitter repo map
- Apple TN3150: dataless files
- getattrlistbulk on macOS (healeycodes)
Generated by demo/build.py from evals/results/. Apple M4, macOS 15.7.9.