The headline benchmark (in README.md) is SWE-bench-Verified artifact localization at n=500. This appendix keeps the smaller repo-local benchmarks that motivated the design. They are at n=4–12 with wide confidence intervals and should be read as ablations — they hint at where state-trace helps, they do not prove anything at scale.
Label convention: all tables use state_trace as the row name because it mirrors the Python package identifier.
Every table below pulls from this glossary. If you've never benchmarked an IR system, this is the bit that makes the rest readable.
| Metric | In plain English | When you'd care |
|---|---|---|
| Success | Fraction of tasks where the agent (with this memory layer) reached a correct final state by the last step. 0.417 on 12 tasks means it finished 5 of them. |
The bottom line. "Does my agent actually close the bug?" |
| StepAcc (step accuracy) | At each step in the replay, how often did the agent pick the same action that the original human/agent trajectory took? 0.502 means about half the time. |
Proxy for "is the memory layer keeping the agent on the right track mid-trajectory?" |
| Artifact@1 | Fraction of tasks where the very first file the system recommends is the correct patch file. 1.000 means perfect; 0.417 means 5 out of 12. |
"If I act on the top suggestion, how often am I editing the right file?" |
| Artifact@5 | Same as Artifact@1 but the correct file just needs to be in the top 5 suggestions. | "If I let a small model pick from a short list, does the answer even appear?" |
| Recall@k | Fraction of tasks where any chunk mentioning the correct file appears in the top-k results. This is token-level, not artifact-level — a chunk that merely name-drops the file counts. Recall@1 = 0.75 with Artifact@1 = 0.000 means lexical search finds text about the answer but doesn't surface the actionable file node. |
"Does my retriever find text about the answer?" — weaker bar than Artifact@k. |
| MRR (mean reciprocal rank) | Average of 1 / rank_of_first_correct_result. 1.000 means the correct answer was always at rank 1. 0.625 means it averaged around rank 1.6. |
"How far do I have to read down the list on average?" |
| AvgBrief / AvgTokens | Average size of the memory payload handed to the agent per step, measured in tokens. | "How much context am I paying for every turn?" — matters for small-model / free-tier harnesses. |
| AvgLatencyMs | Milliseconds spent in the memory layer per retrieval. | In an agent loop that calls memory every action, 4,000 ms × 100 steps = 6.6 minutes of pure memory overhead. 17 ms × 100 = 1.7 seconds. |
| Capacity | Effective working-memory footprint (roughly: node count weighted by each node's size). state-trace is supposed to stay near its configured limit. | "Did my memory layer respect the bound I gave it?" |
| WithinBudget | Fraction of checkpoints where the layer stayed under the configured capacity. 1.000 means never exceeded; 0.125 means blew past it 87.5% of the time. |
"Can I run a long debugging session without memory bloat killing the agent?" |
| [lo, hi] after a number | 95% bootstrap confidence interval. If two rows' intervals overlap, you can't honestly call either one the winner on this sample. | The statistical humility check. Small N = wide intervals = tentative claims. |
The task being benchmarked almost everywhere in this file is: "an agent is debugging a real GitHub issue from SWE-agent's public trajectories. Given memory of what's been tried so far, which file should it edit next?" Unless stated otherwise, the tasks come from examples/data/agent_logs/ — real agent trajectories on real OSS repos (marshmallow, pydicom, sympy, etc.).
python3 examples/heldout_live_benchmark.pyWhat it simulates: 12 real SWE-agent debugging sessions (4 unique task groups × 3 trajectory variants each). For each session, replay the steps one at a time; at each step, the memory layer produces a brief and the agent picks the next action from a candidate list. See if the agent finishes the task and how often it picks the correct next action.
| Memory | Success | StepAcc | Artifact@1 | AvgBrief | AvgLatencyMs |
|---|---|---|---|---|---|
| no_memory | 0.000 [0.000, 0.000] | 0.059 [0.014, 0.111] | 0.417 [0.167, 0.667] | 153.2 | 1.7 |
| bm25 | 0.083 [0.000, 0.250] | 0.131 [0.017, 0.328] | 0.292 [0.042, 0.542] | 226.6 | 13.5 |
| dense_cosine | 0.000 [0.000, 0.000] | 0.119 [0.017, 0.292] | 0.271 [0.021, 0.521] | 226.3 | 19.9 |
| hybrid | 0.083 [0.000, 0.250] | 0.131 [0.017, 0.328] | 0.292 [0.042, 0.542] | 226.8 | 17.1 |
| state_trace | 0.417 [0.167, 0.667] | 0.502 [0.288, 0.718] | 0.396 [0.146, 0.708] | 228.6 | 1,257.7 |
| graphiti | 0.000 [0.000, 0.000] | 0.085 [0.000, 0.242] | 0.250 [0.000, 0.500] | 198.0 | 1,283.0 |
In real-world terms:
- state_trace finished 5 of 12 tasks. Every other memory layer (including Graphiti) finished 0 or 1. No memory: 0. BM25: 1. Graphiti: 0.
- When choosing the next action, state_trace picked the trajectory-correct move 50% of the time. Every other system was under 15%. That's the gap between "agent makes steady progress" and "agent wanders."
- All briefs fit in ~230 tokens — roughly the budget a free-tier model can digest without drowning.
- Memory overhead per step: ~1.3 seconds. For a 20-step debugging session, that's 26 seconds of memory latency total — noticeable but liveable.
Caveat: CIs are wide (only 12 trajectories). The 0.417 vs 0.083 gap is real given the CI bounds, but don't generalize this to "state-trace wins across all debugging agents."
python3 examples/offline_retrieval_eval.pyWhat it simulates: the simpler, weaker test. Ingest a full agent trajectory into memory. Ask "which file should I patch?" Measure whether the system can find the answer after already having seen the whole trajectory.
| Model | Recall@1 | Recall@3 | Recall@5 | MRR | Artifact@1 |
|---|---|---|---|---|---|
| bm25 | 0.750 | 1.000 | 1.000 | 0.875 | 0.000 |
| dense_cosine | 0.375 | 1.000 | 1.000 | 0.688 | 0.000 |
| hybrid | 0.750 | 1.000 | 1.000 | 0.875 | 0.000 |
| state_trace | 1.000 | 1.000 | 1.000 | 1.000 | 1.000 |
In real-world terms:
- The flat retrievers (BM25, dense, hybrid) score well on
Recall@1— they find text that mentions the right file. But theirArtifact@1is zero, meaning the top result is never an actionable file reference — it's an issue-text chunk that merely name-drops the file. - state_trace returns the actual file node at rank 1 on every one of the 12 tasks. If you're a coding agent about to call
edit(...), the distinction is the difference between "here's the file" and "here's a paragraph mentioning the file."
Caveat: this is "can you find the answer when you've already been told the answer." It is not cold-start. The real-world analogue is "post-hoc retrieval over a session you already ran," not "starting a fresh task."
python3 examples/graphiti_head_to_head_eval.pyWhat it simulates: same normalized agent trajectories loaded into both systems. Graphiti runs against a local Kuzu graph with BM25 + cosine + BFS reranked by RRF (no LLM entity extraction, for reproducibility without API keys). Ask each "which file to patch?"
| Model | Recall@1 | Recall@3 | Recall@5 | MRR | Artifact@1 |
|---|---|---|---|---|---|
| graphiti_kuzu | 0.500 | 0.750 | 0.750 | 0.625 | 0.500 |
| state_trace | 1.000 | 1.000 | 1.000 | 1.000 | 1.000 |
In real-world terms: on 4 tasks, Graphiti put the right file first half the time; state_trace put it first every time. Graphiti's MRR of 0.625 means the right answer averaged rank ~1.6 — often but not always at the top.
Caveat: n=4 is a toy. The headline SWE-bench-Verified n=500 in the main README is the same shape of comparison at 125× the scale — and there state_trace's lead narrows to 0.150 vs 0.098 (non-overlapping CIs, but not a 2× gap). Trust the n=500 number, not this one.
python3 examples/small_model_harness_eval.pyWhat it simulates: a small, cost-sensitive model gets a fixed token budget of context per turn. The memory layer compresses its output to fit that budget. Does the small model make the right first move given what fit?
Small budget (~220 tokens — free-tier / GPT-4o-mini territory):
| Memory | Success | AvgTokens |
|---|---|---|
| no_memory | 0.250 | 157.0 |
| state_trace | 1.000 | 218.5 |
| graphiti | 0.500 | 211.5 |
Large budget (~700 tokens):
| Memory | Success | AvgTokens |
|---|---|---|
| no_memory | 0.250 | 182.2 |
| state_trace | 1.000 | 433.5 |
| graphiti | 0.500 | 546.0 |
In real-world terms:
- With a 220-token budget, state_trace got the first move right on all 4 tasks. Graphiti got 2. No memory got 1 (lucky guess). This is the "small-model coding harness" case — the brief has to be tight and correct.
- At 700 tokens (Claude Sonnet / GPT-4 territory), the gap shape is the same: state_trace uses 433 tokens, Graphiti uses 546 tokens to reach worse performance. state-trace is denser per token, not just smaller.
Caveat: n=4. Real-world implication is directional, not conclusive.
python3 examples/live_agent_harness_eval.pyWhat it simulates: step-by-step replay with an agent policy choosing actions from a candidate list at each step. Stricter than the one-shot brief test — the agent has to keep picking right turn after turn.
| Memory | Success | StepAcc | AvgBrief |
|---|---|---|---|
| no_memory | 0.000 | 0.086 | 156.9 |
| state_trace | 1.000 | 1.000 | 225.7 |
| graphiti | 0.000 | 0.214 | 183.3 |
In real-world terms: state_trace guided the agent to pick the correct action at every step across every task. No memory and Graphiti both failed to complete any task. Graphiti's 21% step accuracy means it was picking the right move roughly one step in five — not enough momentum to finish.
Caveat: n=4, replay (not live solve), on a small curated set. This is the best-case demonstration of "state-trace keeps the agent on rails."
python3 examples/long_horizon_memory_eval.pyWhat it simulates: replay each of 12 tasks while injecting distractor steps from other trajectories in between. As the session gets longer, does your memory layer stay within its budget, and does it still find the right file?
Small mode, capacity_limit=96, noise_per_step=2:
| Memory | Artifact@1 | Capacity | WithinBudget |
|---|---|---|---|
| state_trace | 0.771 [0.646, 0.875] | 92.7 [91.7, 93.6] | 1.000 [1.000, 1.000] |
| state_trace_no_lifecycle (ablation) | 0.979 [0.938, 1.000] | 232.1 [200.8, 263.4] | 0.125 [0.042, 0.229] |
| hybrid | 0.646 [0.521, 0.771] | 14.6 [12.8, 16.4] | 1.000 [1.000, 1.000] |
In real-world terms:
- state_trace with lifecycle retention: 77% right-file-first, never exceeded the 96-unit budget.
- Same system with lifecycle disabled: 98% right-file, but 12.5% of the time it stayed within budget — i.e., 87.5% of the time it bloated past the limit you set. More recall, but only because it refused to forget anything.
- BM25-style hybrid: stayed well under budget (barely using ~15 units) but only got 65% of the files right.
This is the clearest evidence for the "bounded working memory" claim: you can crank recall up arbitrarily by never forgetting, but then you're just a vector DB with no size limit. state-trace keeps recall competitive while respecting the memory ceiling — that's the whole pitch.
python3 examples/tune_ranking_weights.py| Metric | Mean |
|---|---|
| Artifact@1 | 1.000 |
| MRR | 1.000 |
| AvgBrief | 224.0 |
In real-world terms: a small random search (--samples 6) over scoring weights currently ties the hard-coded defaults on the holdout set. The tuning infrastructure exists — if we ever see drift on a larger corpus, this is how we re-tune.
python3 examples/swebench_verified_solve_rate.py --policy codex --limit 20 --out /tmp/preds_state_trace.jsonl
python3 examples/swebench_verified_solve_rate.py --policy codex --limit 20 --no-memory --out /tmp/preds_nomem.jsonl
python3 examples/solve_rate_proxy_score.py --predictions /tmp/preds_state_trace.jsonl --predictions /tmp/preds_nomem.jsonlWhat it simulates: the first 20 SWE-bench-Verified instances, patched end-to-end by Codex CLI (no API key needed — uses the operator's logged-in Codex session). Two arms: one with the state-trace brief injected into the prompt, one with issue text only. Each arm writes a predictions.jsonl suitable for the swebench docker harness.
Proxy score (file-overlap vs golden patch files):
| arm | real patches | stubs | file_hit_rate | mean_overlap |
|---|---|---|---|---|
| state_trace | 20 | 0 | 1.000 | 1.000 |
| no_memory | 20 | 0 | 1.000 | 1.000 |
In real-world terms: both arms localize the correct file on every single instance. Codex is good enough at file-finding from issue text alone that this part of the task is already near-ceiling — state-trace's n=500 cold-start localization advantage doesn't have room to show up here. If state-trace improves real solve-rate on this workload, the contribution has to come from patch correctness, not file-finding.
Caveats:
- File-overlap is not solve-rate. A patch can hit the right file and still fail the test suite (wrong logic), or miss the file entirely and still pass (alternative implementation).
- n=20 is small — CIs ~±12%.
- Codex CLI's agentic reasoning is substantially stronger than a one-shot LLM call. The proxy saturation is partly a Codex-specific finding; results with weaker models would likely separate the arms further.
- The real solve-rate number requires the swebench docker harness to actually run the hidden tests. That step is the top of the credibility ladder (see below) and has not been run yet.
The top rung of the credibility ladder. Actual test-suite pass rate on the first 20 SWE-bench-Verified instances.
# After generating predictions with the scaffold above:
pip install swebench
python -m swebench.harness.run_evaluation \
--dataset_name princeton-nlp/SWE-bench_Verified \
--predictions_path /tmp/preds_state_trace.jsonl \
--max_workers 1 --cache_level instance --run_id st_n20
python -m swebench.harness.run_evaluation \
--dataset_name princeton-nlp/SWE-bench_Verified \
--predictions_path /tmp/preds_nomem.jsonl \
--max_workers 1 --cache_level instance --run_id nm_n20Result:
| arm | resolved | unresolved | errored (patch-apply fail) | solve-rate |
|---|---|---|---|---|
| state_trace | 7 | 3 | 10 | 7/20 = 35% |
| no_memory | 7 | 2 | 11 | 7/20 = 35% |
Which instances each arm solved:
both arms: astropy-12907, astropy-13453, astropy-14309, astropy-14995, astropy-7671
state_trace: astropy-14598, astropy-7166
no_memory: astropy-14508, astropy-7336
- Overlap: 5 instances
- state_trace-only: 2
- no_memory-only: 2
- Union (at least one arm solves): 9/20 = 45%
In real-world terms:
- Aggregate solve-rate is identical. The file-level proxy predicted this — Codex CLI already near-ceilings on cold-start file localization, so state_trace's n=500 retrieval advantage has no room to compound through to patch correctness.
- But state_trace changes Codex's behavior — the two arms resolve different subsets of 7. Net-zero in aggregate. Could be noise at n=20 or a genuine redirect-sideways. Larger N (50-100) would resolve which.
- Errors are dominated by patch-apply failures — Codex produces unified diffs whose line numbers don't match the base commit, so
git applyrejects them before tests run. Same pattern both arms. That's a downstream-model issue, not a memory-layer issue. - Routing oracle ceiling is 45%. If you could predict per-instance whether state_trace helps or hurts Codex on that instance, you'd jump 10 points. Suggests state_trace's context is meaningfully orthogonal to Codex's baseline, just not consistently in the right direction.
Caveats (important):
- n=20 is too small to be confident about direction. What we can say honestly: no big win, no big loss, identical aggregate. CIs are wide enough that a n=50 or n=100 run could land anywhere from +15% to -5%.
- The first 20 SWE-bench-Verified instances skew toward astropy. A different slice (django, sympy, sklearn) might shift things.
- Codex CLI is a substantially stronger downstream agent than a raw one-shot LLM call. Results with a smaller/weaker model would likely show a larger gap in one direction or the other, because retrieval quality matters more when the downstream model can't compensate.
- About half the instances in both arms hit patch-apply failures rather than test failures — meaning we're measuring as much "Codex's diff-generation precision" as "state-trace's memory contribution." A harness that retries on patch-apply failures or uses an edit-based rather than diff-based action format would produce a fairer signal.
Predictions for n=50 were generated successfully (50/50 real patches both arms; see /tmp/preds_state_trace_n50.jsonl and /tmp/preds_nomem_n50.jsonl). The docker harness, however, exhausted Docker Desktop's default 60GB VM disk every time it tried to push past ~5-10 instances — the swebench instance images are ~3GB each, and even --cache_level base (which removes env+instance images after each test) couldn't keep up with build-and-pull traffic, leading to a read-only filesystem state that aborts the rest of the run.
To complete n=50 on this hardware, three options:
- Raise Docker Desktop disk allocation to 200GB+ (Settings → Resources → Disk image size). Local solution.
- Use Modal — swebench has built-in cloud execution support:
pip install modal && python -m swebench.harness.run_evaluation --modal=true .... Needs a Modal account but bypasses local disk. - Run instances in batches of 5-10 with manual
docker system prune -afbetween batches.
Predictions are saved and ready; harness scoring at scale is an infrastructure step, not a code step.
-
Sample sizes are small. n=4 and n=12 benchmarks have confidence intervals that routinely overlap. Don't treat any single row as "state_trace beats X." The only externally-citable number is the SWE-bench-Verified n=500 row in the main README.
-
The benchmarks are trajectory-informed. For most of these, state_trace has seen the full SWE-agent trajectory before being asked the question. That's a realistic setup for "mid-debugging session" but an unfair advantage vs. cold-start-from-issue-text. The headline n=500 benchmark in the main README is cold-start, and state_trace's lead there narrows substantially (still beats Graphiti, ties BM25).
-
Concretely: when do these numbers matter?
- If your agent loops through 10+ steps per task → latency and capacity matter (held-out live + long-horizon pressure).
- If you're running on small/free-tier models → brief size and density matter (budgeted harness proxy).
- If you're comparing to Graphiti specifically → head-to-head + SWE-bench-Verified.
- If you need to show it won't blow memory on long sessions → long-horizon pressure.
-
Credibility ladder (weakest to strongest): offline retrieval → held-out live replay → long-horizon pressure → SWE-bench-Verified localization → solve-rate proxy (file-overlap) → live solve-rate (docker harness). The first three are what's in this file; the fourth is in the main README; the fifth is the n=20 section above (ceiling-saturated on this workload); the sixth is the real pass-rate from the swebench docker harness and is the only rung that will give a definitive "does memory change downstream behavior" answer.