speedread logo

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

−35%
input tokens · code questions
real agent, 30 trials per arm
−47%
model time · code questions
median, same model and harness
−57%
input tokens · relationship questions
trace chosen in 7 of 8 trials
64/64
bug fixes pass the full test suite
compression hid the bug in 0 of 32
0%
adoption when merely installed
100% with one sentence of guidance
0%
reads over budget on hostile content
was 9.5% · 525 reads

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.

Same question. Same model. Same harness. claude-sonnet-5 via GitHub Copilot CLI · swift-argument-parser · real transcripts replayed at recorded speed (all 3 trials of each condition were identical) “In ParsableCommand.swift, what is the type of the ParsableCommand protocol's static `configuration` requirement, and on which line is that requirement declared?” built-in tools view · grep · glob · bash ● model call 121,296 tokens in grep "static var configuration" ParsableCommand.swift -n → Sources/ArgumentParser/Parsable Types/ParsableCommand.swift a file name: no lines, no line numbers ● model call 221,461 tokens in grep "configuration" ParsableCommand.swift -n → Sources/ArgumentParser/Parsable Types/ParsableCommand.swift a file name: no lines, no line numbers ● model call 321,646 tokens in bash grep -n "configuration" ParsableCommand.swift → 16: static var configuration: CommandConfiguration { get } 20: /// This is generated from the configuration, if given, or from … 39: configuration.commandName … 12 more lines ● model call 422,226 tokens in ✓ The configuration requirement's type is CommandConfiguratio… 1 model call · 0 tool calls · 21,296 tokens · 2.13 AIU 2 model calls · 1 tool call · 42,757 tokens · 2.77 AIU 3 model calls · 2 tool calls · 64,403 tokens · 3.45 AIU 4 model calls · 3 tool calls · 86,629 tokens · 4.10 AIU done in 9.4 s · 4 model calls · 3 tool calls · 86,629 tokens · 4.10 AIU speedread read · search · trace · map ● model call 121,618 tokens in search "static var configuration" ParsableCommand.swift → 2 matches in 1 file for /static var configuration/ ==> Sources/ArgumentParser/Parsable Types/ParsableCommand.swift @ba95… [12-33] public protocol ParsableCommand: ParsableArguments 16 static var configuration: CommandConfiguration { get } [43-45] public static var configuration: CommandConfiguration (in Par… … 1 more lines ● model call 221,896 tokens in ✓ static var configuration: CommandConfiguration { get } — de… 1 model call · 0 tool calls · 21,618 tokens · 2.08 AIU 2 model calls · 1 tool call · 43,514 tokens · 2.63 AIU done in 3.2 s · 2 model calls · 1 tool call · 43,514 tokens · 2.63 AIU speedread: 2 model calls instead of 4 · −50% input tokens · −36% cost · 3.0× less model time

built-in tools

model call 1 · 21,296 tokens in
grep "static var configuration" ParsableCommand.swift -n
Sources/ArgumentParser/Parsable Types/ParsableCommand.swift
model call 2 · 21,461 tokens in
grep "configuration" ParsableCommand.swift -n
Sources/ArgumentParser/Parsable Types/ParsableCommand.swift
model call 3 · 21,646 tokens in
bash grep -n "configuration" 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>
model call 4 · 22,226 tokens in
✓ The `configuration` requirement's type is `CommandConfiguration`, declared on line 16 (`static var configuration: CommandConfiguration { get }`).

speedread

model call 1 · 21,618 tokens in
search "static var configuration" ParsableCommand.swift
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 {
model call 2 · 21,896 tokens in
✓ `static var configuration: CommandConfiguration { get }` — declared on line 16.

Real agent A/B

Real agent, same model: speedread vs built-in tools 10 code questions × 3 trials per condition · claude-sonnet-5 via GitHub Copilot CLI · exact tokens from the harness's usage log · pass@1 97% → 100% built-in toolsspeedread Input tokens per task 75,511 48,984 −35% Model time per task (median) 6.7 s 3.5 s −47% Model calls (round trips) 3.37 2.17 −36% Tool calls 2.37 1.20 −49% Cost per task (AI units) 3.90 3.07 −21% Correct on every trial (pass^3) 90% 100% 100% Merely installed next to the built-in tools, speedread was used in 0% of 10 trials. Adoption is part of the product: make it the reader (see README).
TaskCategoryTokens, built-inTokens, speedreadΔAIU, built-inAIU, speedreadModel callsPass
flask-shell-contextcontrol: small symbol71,73950,965−29%3.562.873.3 → 2.3100% → 100%
gin-go-versioncontrol: tiny file42,56843,219+2%2.552.532.0 → 2.0100% → 100%
ripgrep-fixed-stringslarge file (8,161 lines)64,58444,000−32%3.382.783.0 → 2.0100% → 100%
ripgrep-walk-runlarge file (2,740 lines)78,98543,424−45%3.742.623.7 → 2.0100% → 100%
zod-ip-kindslarge file (5,138 lines)110,83252,281−53%4.903.075.0 → 2.367% → 100%
flask-dispatch-lineslarge file, several symbols64,33143,565−32%3.322.653.0 → 2.0100% → 100%
gin-abort-callerscross-file search43,14143,656+1%2.782.712.0 → 2.0100% → 100%
vscode-setvaluehuge repo (19k files)134,95781,389−40%7.556.115.0 → 3.0100% → 100%
alamofire-start-immediatelySwift, large file (1,441 lines)57,39843,812−24%3.152.692.7 → 2.0100% → 100%
sap-configurationSwift, protocol requirement86,57843,528−50%4.042.634.0 → 2.0100% → 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

Tool-level scenarios: 954,734 → 67,079 tokens (−93%) 35 scenarios on 6 real repos, each graded for information sufficiency (35/35 pass) · 104 → 35 tool calls · o200k tokens the model sees built-in toolsspeedread Understand a large file 274,994 tokens · 13 calls 25,144 tokens · 6 calls −91% Read one function 182,112 tokens · 9 calls 4,483 tokens · 5 calls −98% Read one function (vs best-case grep + window) 8,362 tokens · 10 calls 4,483 tokens · 5 calls −46% Re-read a file after an edit 233,511 tokens · 11 calls 488 tokens · 4 calls −100% Find usages + read the callers 244,412 tokens · 17 calls 17,522 tokens · 4 calls −93% Follow a growing log 3,969 tokens · 1 calls 703 tokens · 1 call −82% Small files (control) 5,145 tokens · 5 calls 4,318 tokens · 5 calls −16% Orient in a repo 2,229 tokens · 38 calls 9,938 tokens · 5 calls −87% calls Orient costs more tokens on purpose: one budgeted map replaces 5–9 ls calls. Small files: identical content; only the header differs.

The budget contract

Budget contract on hostile content: reads over budget read responses at budgets of 1k, 4k and 8k · o200k, cl100k and legacy Claude tokenizers fixed 2.6 bytes/tokencontent-aware estimator SVG 44% over · worst 1.55× 0% over · worst 0.94× 0% JSON 39% over · worst 1.41× 0% over · worst 0.99× 0% Lockfiles 11% over · worst 0.96× 0% over · worst 0.83× 0% Go 6% over · worst 0.94× 0% over · worst 0.83× 0% CJK docs & locales 3% over · worst 0.88× 0% over · worst 0.84× 0% Emoji-heavy text 100% over · worst 1.00× 0% over · worst 0.95× 0% Base64 100% over · worst 1.74× 0% over · worst 0.84× 0% Hex dump 100% over · worst 1.73× 0% over · worst 0.88× 0% Numeric CSV 100% over · worst 1.24× 0% over · worst 0.90× 0% Unicode math symbols 100% over · worst 1.31× 0% over · worst 0.85× 0% All 525 reads 9.5% over · worst 1.81× 0.0% over · worst 0.99× 0% Offline tokenizers. Real claude-sonnet-5 counts run higher (1.22× the estimate at the median); speedread's Claude profile scales budgets 1.4×.

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

Relationship questions: where trace earns its keep callers two hops out, resolved callees, interface and trait implementations · 8 trials per condition · both pass 100% · claude-sonnet-5 via GitHub Copilot CLI built-in toolsspeedread Go interface implementations (structural) 46,464 tokens · 1.5 tool calls 39,882 tokens · 1.0 tool calls −14% Rust trait implementations 47,231 tokens · 1.5 tool calls 38,763 tokens · 1.0 tool calls −18% Multi-hop callers 231,472 tokens · 10.5 tool calls 61,350 tokens · 2.0 tool calls −73% Resolved callees 94,022 tokens · 4.0 tool calls 38,548 tokens · 1.0 tool calls −59% All relationship questions 104,797 tokens · 4.4 tool calls 44,636 tokens · 1.2 tool calls −57% Unprompted, the agent chose trace in 7 of 8 speedread trials. Two of the four are answerable with one good grep (the controls); there the gap is small.

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.

Real bug fixes, same model: four ways to give an agent speedread 8 injected regressions in gin (Go) and flask (Python) × 2 trials per condition · pass = full test suite, tests untouched · claude-sonnet-5 via GitHub Copilot CLI built-in tools speedread available speedread preferred speedread exclusive Pass rate (pass@1) 100% 100% 100% 100% Input tokens per task 166,147 243,091 (+46%) 160,357 (−3%) 165,307 (−1%) Model time per task (median) 20.5 s 21.5 s (+5%) 15.6 s (−24%) 14.9 s (−27%) Model calls (round trips) 7.8 9.4 (+21%) 6.5 (−16%) 7.2 (−6%) Cost per task (AI units) 7.84 10.15 (+30%) 7.50 (−4%) 7.37 (−6%) Reads done with speedread 0% 0% 78% 83% The buggy line was first hidden inside a skeleton or outline in 0 of the 32 trials that used speedread. Unused (available), it was chosen in 0% of trials.
ConditionTrialsPassInput tokensModel callsTool callsRead-result tokensAIU
baseline16100%166,1477.86.91,6047.84
available16100%243,0919.48.41,67110.15
preferred16100%160,3576.55.51,9067.50
exclusive16100%165,3077.26.21,6227.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

map: locate structure
budgeted, importance-weighted tree with line counts; symbols on request
search: locate text
ripgrep engine; hits grouped under their enclosing function or class, with its line range
trace: locate relationships
callers, callees, references, implementations; syntactic and receiver-aware
read: exact evidence
batched targets and symbols; skeletons instead of blind cuts; path@etag symbol-aware diffs

Method

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

Generated by demo/build.py from evals/results/. Apple M4, macOS 15.7.9.