Euthynos / Research / M2
We benchmarked it before we believed it.
M2 is a preregistered milestone benchmark: answer keys frozen by commit before any session ran, every session through a seven-gate validity harness, recall hand-graded against the frozen keys before anyone opened a transcript. What follows is everything it showed — including the parts that didn't flatter us.
Method
Designed so we could not fool ourselves.
Preregister
Tasks, validity rules, endpoints and a session ceiling — written and frozen before spending a single session.
Freeze the keys
Required answers per task committed to git. The commit hash is the tamper-evidence.
Gate every session
Seven automated checks — pinned permissions, engine, repo state, single declared treatment — before and after each run.
Grade blind
Recall scored by hand against the frozen keys from final answers only, cross-checked by independent blind graders, before any transcript inspection.
Publish the caveats
Invalid sessions counted, unmeasured tasks named, limits stated with the same prominence as the wins.
Primary endpoint
Same graded answers. 13–31% fewer fresh tokens.
Two arms, identical prompts, three repetitions each: an agent with only its built-in file tools, and the same agent with Euthynos. Fresh input tokens are the exact API-reported figures; each bar is a per-task median over valid sessions.
Fresh input tokens per task — median over valid sessions
hono @ 26de7313 · 3 valid sessions per arm per task · lower is better
View as table
| Task | Agent alone | With Euthynos | Δ median |
|---|---|---|---|
| callers | 70,878 | 49,476 | −30% |
| similar-logic | 33,429 | 29,120 | −13% |
| blast-radius | 56,481 | 39,242 | −31% |
The part that makes the tokens mean something
Recall was identical. Perfect, in both arms.
A token saving is only real if the answers stay right. Every required item in the frozen keys — every caller, every implementation, every impact — graded present or absent, per session, before transcripts were opened.
serializeSigned as a sibling sharing an
internal helper — not a caller. That distinction was a planned false-positive class.
Secondary signals
Where the arms differed: discipline under uncertainty.
Same recall — different behaviour at the edges. False positives and unhedged “these are all” claims were graded per session against preregistered definitions.
False positives
total across valid sessions · lower is better
Including one arm-alone session resurrecting a documented historical error — claiming a symbol sits on a package surface it is not exported from.
Unhedged exhaustiveness claims
sessions making an unbounded “these are all” claim
Euthynos sessions were also the only ones that disclosed the instrument’s own boundary in their answers — “the call graph missed this site; found by text search.”
Attempted vs valid
21 of 42 sessions survived the rules. The rest are reported, not hidden.
The validity rules were frozen before session one: any profile-rule denial, turn-cap, or transport failure disqualifies a session. Mid-run, an external account session limit consumed two entire tasks — so those tasks are unmeasured, and they stay that way in every claim we make.
- Two of seven tasks were never measured. guided-edit and orientation were consumed by the session-limit wall. No number on this page includes them.
- One repository, one model, one permission environment. hono, claude-opus-5, a single pinned configuration. No generalization is claimed.
- No causal isolation of individual tools. The treatment is the whole tool surface; which specific tool drives which effect is not established.
- duplicate-audit has no arm comparison. Its Euthynos-arm sessions were lost to the wall; its agent-alone sessions ran 178k–250k fresh tokens with the false positives above.
- n = 3 per cell at best. These are medians, not significance tests.
The benchmark bit us, too
M2 found a real defect in our own engine.
Every valid blast-radius session — in both arms — found a caller our call graph missed: a class property holding an arrow function. The parser extracted method declarations but skipped property-assigned functions, so their calls never entered the graph. That is exactly the kind of failure this benchmark exists to catch.
// invisible to the graph before M2: class ClientRequestImpl { fetch = async (args) => { … cookies.push(serialize(key, value)) … } }
// the released engine, after the fix: $ callers_of serialize 9 transitive callers of serialize: d1 fetch — src/client/client.ts:58 d1 generateCookie — src/helper/cookie/… …
The fix shipped with six pinned regression tests. The frozen M2 numbers were not retroactively changed — the miss stays recorded, because rewriting history is how benchmarks stop meaning anything.
Engineering measurements
The performance envelope, stated honestly.
Validated to roughly 10,000 files, and comfortable well below about 1,500. Above 10,000 it is not validated and should not be assumed to work. Memory, not latency, is the binding constraint at the top of that range: on a 10,000-file repository process RSS rises from roughly 0.6 GB to about 1.1 GB during an edit loop.
Precise latency figures are deliberately not published in V1. Two internally consistent measurements disagreed, and the controlled experiment that would have settled which to trust could not be completed — so we publish neither, and ship the harness instead. Index reads are still neither free nor scale-invariant: find_symbol, read_function and find_references cost real time and grow with repository size. An earlier version of this page claimed ≤0.3 ms; that figure was an argument-rejection error path, not a read. Run the shipped harness for numbers that describe your own hardware.
Read the raw record, or run it yourself.
The preregistration, the results with every caveat intact, and the grading standard ship in the repository, byte-identical to the frozen originals. The answer keys and the full session ledger are not published — M2 is readable at source, but it is not re-runnable from the public repository.