Controlled evaluation · executable graders · n=5 seeds
A controlled evaluation of an open-source, self-hostable agent-memory and retrieval layer, against no-memory controls and external memory products — every number recomputed from raw.
bento is an open-source, self-hostable agent-memory and retrieval layer (optionally run as a managed service) over four ingredients — graph retrieval (pg-raggraph), currency/supersession (stele), code-aware chunking (chunkshop), and in-process extraction (lede). This report evaluates it on seven claims against no-memory controls and two external memory products (mem0; stele as the raw engine bento wraps), with LangChain as an external framework baseline. Tasks use executable graders, not an LLM judge, except one retrieval cell noted below. Runs span three model tiers including the frontier (OpenAI gpt-5.5), which was run — it is not pending.
Every quantitative claim cites evidence/CANONICAL-NUMBERS.md, recomputed from the
committed raw seed records. Where this version disagrees with the prior draft, the raw data wins;
§"What changed" lists every correction.
Architecture
Bento is an open-source agent-memory and retrieval system — deployable
self-hosted or as a managed service — built as a single FastAPI backend-for-frontend (BFF) that
composes four retrieval/memory ingredients behind one ingredient registry. Clients talk only to the
BFF; it routes each capability to the ingredient that owns it. Three ingredients run as HTTP
services over a shared substrate (PostgreSQL + Valkey); lede runs in-process because a
network hop would dwarf its sub-millisecond work. Being open source, the system under test is itself
inspectable and clonable — the reproducibility argument extends from the harness to the product.
Clients
Backend-for-frontend
Ingredients · independent HTTP services
Substrate — PostgreSQL + Valkey · two stores that never cross-read
Composition is why "bento" ≠ vector-RAG: the test surface (C1–C7) exercises all four ingredients.
| Ingredient | Service | Role | Tested in |
|---|---|---|---|
| pg-raggraph | pg-raggraph-proxy | Graph retrieval — per-KB entity/relationship extraction + traversal over pgvector chunks | C4, C6, ask |
| stele | stele-shim | Currency/supersession — typed memory, atomic replaces, active-only recall | C1, C2, C3, C7 |
| chunkshop | chunkshop-shim | Chunking incl. code-aware symbol_aware (tree-sitter whole-symbol units) | C5, ingest |
| lede | in-process | Deterministic term/fact extraction at ingest (sub-ms) | ingest |
| RAG chunks | Stele memories | |
|---|---|---|
| Namespace | kb-{kb_id} | str(kb_id) |
| Written by | /v1/ingest, uploads, connectors | /v1/memory, session capture |
| Read by | /v1/ask (synthesis) | /v1/recall, Memory Browser |
| Search | pgvector + graph traversal | temporal / typed, active-only |
/v1/ask resolves kb-{id} and queries chunks; it has no
path into stele. Recall resolves str(kb_id) and reads stele; no path into chunks. So
the currency/accumulation claims (C1/C3/C7, stele) and the retrieval claims (C4/C6, pg-raggraph)
genuinely test different machinery — not two views of one store.
/v1/ingest → chunkshop picks a strategy (symbol_aware
for code, sentence-aware for prose) → pg-raggraph-proxy writes documents + chunks and, when graph
is on, extracts the entity graph; effective_from is stamped for point-in-time (F13)./v1/ask → resolve kb-{id} → pg-raggraph retrieval
(naive / hybrid / local graph modes) → the models service synthesizes a grounded, cited answer.POST /v1/memory writes a structured fact to stele; an optional
replaces atomically supersedes the prior value (the currency mechanism, C1).
/v1/recall returns active-only memories — superseded rows are never surfaced.Claims under test
| # | Claim | Task | Grader |
|---|---|---|---|
| C1 | Memory stays current after updates | price_book | executable exact |
| C2 | Memory reduces cost at equal accuracy | research_desk | executable graph-answer |
| C3 | Memory unlocks correctness | incident_rca | executable exact |
| C4 | Graph beats flat retrieval on multi-hop | graph-scale A/B | needle + LLM-judge |
| C5 | Code-aware chunking returns whole functions | chunking A/B | substring completeness |
| C6 | KBs stay current + point-in-time | KB-currency A/B | executable substring |
| C7 | Procedural memory cuts repeated failures | plan_replan A/B | state-oracle |
Every memory arm runs against a no-memory control on the identical task, model, and grader. The design can disprove each claim.
A mutable price book; a hot SKU's price is updated mid-stream; the agent must always answer the current price. A no-memory baseline re-reads the database (correct, but redoes work). A cache that never invalidates should go stale.
Accuracy — gpt-5.5 tierhigher is better · 0 → 1.0
Against the correct re-read baseline, bento is −0.027 at the local tier with overlapping 95% CIs — statistically indistinguishable. That is the honest meaning of "tracks the baseline": equal correctness, with a reusable cache instead of a full re-read.
| Tier | Arm | acc ± sd | vs naive | source |
|---|---|---|---|---|
| local-26B | bento | 0.923 ± 0.025 | +0.313 | seed-exp-memory-bento-* |
| local-26B | baseline | 0.950 ± 0.039 | — | seed-baseline-* |
| local-26B | naive cache | 0.610 ± 0.148 | — | seed-naive-cache-* |
| gpt-5.5 | bento | 1.000 ± 0.000 | +0.333 | results/price_book/… |
| gpt-5.5 | naive cache | 0.667 ± 0.122 | — | seed-naive-cache-* |
How we measured it · C1
Identical agent
(lookup_price / update_price / answer) over one Postgres price book, identical model; the three arms
differ only in the memory backend. K=5 data-seed-varied; executable exact
grader; gold = the price valid at that point in the update stream. Harness: agent-samples
seed_sweep --agent price_book.
Multi-hop graph QA over a static dependency graph, with recurring questions. A resolved-answer cache can collapse a repeated question's multi-hop work into one recalled answer. Static graph, so no staleness — pure cost. Both memory arms reach accuracy 1.00, so the comparison is cost at equal correctness.
Cost relative to no-memory baseline (= 100%)lower is better
mem0 loses this cell — reported, not hidden
mem0's semantic recall embeds every query (a per-query round-trip), driving 285 calls and wall time 45% above the no-memory baseline, at lower accuracy (0.94). bento and stele use exact recall — no per-query embedding — and land on the winning side.
| Arm | accuracy | wall (s) | llm_calls | tokens |
|---|---|---|---|---|
| baseline (full multi-hop) | 1.000 | 49.3 ± 1.7 | 177 | 106,045 |
| bento (recall) | 1.000 | 32.3 ± 2.9 | 108 | 61,940 |
| stele | 1.000 | 32.0 ± 5.1 | 108 | 61,627 |
| mem0 (semantic) | 0.940 | 71.7 ± 3.8 | 285 | 106,524 |
| LangChain (no memory) | 0.960 | 86.3 ± 6.1 | 166 | 103,849 |
How we measured it · C2
Same search/answer agent over a static
service-dependency graph, same model; arms differ only in whether a resolved-answer cache is
consulted. Both memory arms reach 1.00 accuracy, so cost is the comparison — wall / calls / tokens
come from the proxy call log. K=5; executable graph-answer grader. Harness:
seed_sweep --agent research_desk.
Root-cause analysis where the document corpus is unreliable and conflicting. Memory accumulates verified findings and recalls them on recurring incident classes. Bars below show accuracy with 95% confidence-interval whiskers.
Accuracy ± 95% CI — gpt-5.50 → 1.0
Two honest corrections to the prior draft
Cost is flat at the frontier, not "−55% tokens" (131k→133k): that figure came from an exploratory local run, not this clean sweep. And the single cell a competitor led decomposes to the agent loop, not the backend: in mem0's identical recall loop, bento scores 0.893 vs mem0's 0.890 — CIs overlap, statistically indistinguishable. We report this because it is what a skeptic would expect us to bury.
How we measured it · C3
Same multi-check RCA agent against a
deliberately unreliable, conflicting playbook corpus; gpt-5.5. Arms: no-memory, bento auto-inject,
bento-agent (agent-driven recall loop), and mem0-agent (the same loop, mem0 backend). K=5; executable
exact grader. bento-agent vs mem0-agent is the loop-vs-backend control. Harness:
seed_sweep --agent incident_rca.
Distinctive capabilities · C4–C7
A 6-document relational chain buried in distractors; questions paraphrased so the answer entities never appear in them and keyword/vector search has no surface to match.
Needle-in-context (isolation set)0 → 1.0
NDCG@10 ranking quality ★ customer-requested · three conditions
read this first
In pg-raggraph, naive already =
vector + BM25 (not pure vector); the graph modes layer entity/relationship traversal on top, and the
relationship-focused global mode is the strongest graph mode at scale. Below: the best
bento-graph mode vs standalone external vector and bm25, NDCG@10 graded, pure retrieval, no LLM.
| Condition | bento-graph | vector | bm25 |
|---|---|---|---|
| Single chain · no surface cue (n=6×3) | 0.78 (local) | 0.11 | 0.07 |
| Scaled 8 chains · conventional / named (n=16) | 0.72 (global) | 0.40 | 0.54 |
| Scaled 8 chains · pure-lexical codename (n=16) | 0.79 (global) | 0.67 | 0.89 |
The honest reading scaling forced — and a loss we report, not hide
bento's graph wins decisively when retrieval needs traversal without a lexical bridge (no-cue 0.78 vs ~0.1; realistic named multi-hop 0.72 vs bm25 0.54 / vector 0.40). It loses one case — a pure-lexical codename lookup — to standalone Okapi bm25 (0.79 vs 0.89), because the fusion's vector leg dilutes a near-perfect keyword signal. That is a fusion-weight config knob, not a missing-BM25 bug (BM25 is in every bento mode via the naive leg). The pattern matches pg-raggraph's own bake-off: graph modes don't beat vector+BM25 on clean lexical corpora; they earn their cost on cross-document, no-surface-bridge reasoning.
Holding the retriever fixed and varying only the answer model shows retrieval is solved and synthesis is model-bound: answer accuracy climbs 0.33 → 0.50 → 1.00 across the 26B, 31B, and frontier models on the same graph-retrieved context.
Honest boundary: on easy questions that name the chain entities, bm25 also succeeds — graph's edge is specifically the multi-hop, no-surface-overlap case.
How we measured it · C4
A 6-document relational chain is
buried among N distractors in one namespace (chain ingested with entity extraction ON, distractors
OFF). We race bento graph (local/hybrid) vs flat (naive) vs a vanilla vector RAG vs bm25 on
multi-hop questions, scoring needle-in-context and NDCG@k — qrels: gold-answer doc = 3, other chain
doc = 1, distractor = 0 — in two regimes (named / isolation). Pure retrieval, no LLM. Multi-model
synthesis holds the retriever fixed and varies the answer model. Scripts: graph_isolation_test.py,
graph_ndcg.py, graph_multimodel_synth.py.
bento's symbol-aware chunker vs LangChain's fixed-size splitter, over the same 66 files / 254 functions, same embedder. Metric: is the complete target function the top-1 hit?
Top-1 = complete function0 → 1.0
Code-aware chunking keeps 100% of functions whole (vs 49–60%) and returns the complete function as the top hit 2.4× more often. The mechanism is boundary alignment, not chunk size: a larger fixed window barely changes completeness because windows still cut across function edges.
Ranking — complete-function chunk ★ MRR / NDCG@100 → 1.0
fixed-size finds the right region reasonably (region-MRR 0.687) but the complete-function unit ranks far lower (MRR 0.384) — that 0.687→0.384 gap is the fragmentation tax in ranking terms: it locates where the function is, but no single chunk holds the whole thing.
How we measured it · C5
Both chunkers run over the same 66
source files (254 documented functions): bento's symbol_aware (tree-sitter, whole-symbol)
and LangChain RecursiveCharacterTextSplitter. Query = each function's docstring; gold =
that function. Same embedder (fastembed bge-small); we score top-1 completeness and full-ranking
MRR / NDCG@10 on whether the complete function is retrieved. Deterministic. Script:
code_chunking_ab.py.
Post-update correctness — KB docs (9 queries, 3 generations)0 → 1.0
Time-travel: version replay now works (a18)
An as_of before a document's ingest correctly excludes it (the F13 fix, verified
live). The 2026-06-29 tool sync to pg-raggraph a18 closes #90. With
evolution_tier=structural, a same-id re-ingest is append-only: it sets the old
version's effective_to, opens the new one's effective_from, and keeps
both. We re-ran the 30→90→7-day ingest. All three versions survive, and an
as_of inside each version's window returns just that version (v1=30, v2=90,
current=7) at chunk and answer level. One side effect: the old versions stay in the store and show
up down-ranked in default retrieval, so the chunk-string currency check reads 0.33. That number
measures retention, not staleness; scored at the answer or at rank-1, currency is still 1.00. See
§7.5.
/v1/recall path) —
has its own currency test: when a fact is superseded ("Postgres 14"→"Postgres 16"), does recall
return only the current one? Over 10 evolving-fact scenarios this is distill-model-dominated: the
coder model leaves both siblings active (stale 1.0); switching only the distill model to a gemma-class
temp-0 instruct model takes that to 0.1, and a consolidate sweep retires the residual to 0.0.
§7.7.How we measured it · C6
A document is re-ingested under the
same id across three value generations (30→90→7 days) and queried 9×; bento re-retrieves while the
baseline is a cache that never invalidates. Deterministic substring grader. Time-travel is probed
with as_of before / at / between versions. Script: kb_currency_test.py.
A task where some orders' ship is refused until validate_address
is called — a recovery procedure the agent discovers by reading the error. Over recurring episodes,
no-memory re-discovers the failure every time; bento stores the procedure once and recalls it.
Identical result across all three model tiers.
Refused operations over the episode streamlower is better · accuracy ties at 1.00
A 75% reduction in repeated failed operations at equal accuracy. Token cost is flat to slightly higher (the recalled hint costs tokens), so this is a win where failed operations carry real cost — side effects, money, rate limits, irreversible actions — and cost-neutral where retries are free.
python3 x.py → ModuleNotFoundError → uv run python x.py),
gates the pair (LOG / STORE / SURFACE), redacts secrets, and pushes durable corrections into an always-loaded
BENTO-LEARNED.md block so a later session skips the doomed call. A scripted W1 cold→warm
measurement (N=12, bootstrap 95% CI) puts the saving of one prevented call at 27.0 ms
(95% CI 25.6–28.3) for a net +11 context tokens — a conservative lower bound that excludes
the avoided model turn. A safety invariant drops any correction that costs more context than it prevents.
§7.8. The Tier-2 LLM analyzer is deferred.How we measured it · C7
A plan_replan task: some orders'
ship is refused until validate_address is called — a recovery the agent
discovers from the error. Over a stream of episodes, no-memory re-discovers it each time; bento
stores the procedure once (real /v1/memory via BentoStore) and recalls it thereafter.
3 model tiers, 4 episodes × 6 orders; metrics: accuracy, refused operations, calls, tokens. Script:
procedural_reuse_ab.py.
Rigor
| Comparison | delta | 95% CIs | read |
|---|---|---|---|
| C1 bento vs naive cache (local) | +0.313 | non-overlapping | significant |
| C1 bento vs correct baseline (local) | −0.027 | overlapping | indistinguishable |
| C2 bento vs baseline wall | −34.5% | non-overlapping | significant |
| C3 bento vs baseline accuracy | +0.317 | non-overlapping | significant |
| C3 bento-agent vs mem0-agent | +0.003 | overlapping | indistinguishable (the tie) |
Pre-registration shortfall — disclosed
Our pre-registration committed to K≥10 seeds; every reported cell is K=5. The headline effect sizes (+0.31, +0.32, −34%) are large enough to clear n=5 CIs, but small/null deltas (the −0.027 "within noise") are correspondingly under-powered. We state this rather than let a reader find it in the pre-registration document. Dispersions are standard deviations, not confidence intervals; CIs above are computed as mean ± t·sd/√n.
Threats to validity
infer=False; the fair mem0 comparison is the same-box, identical-loop
decomposition in C3, where the competitive claim rests.git_sha; numbers bind to the
named harness branch/commit, not a per-record SHA. Tracked for the public release.Changelog
This version re-anchors every number to recomputed raw data and removes claims the raw did not support:
turns is the case count;
replaced by model-call counts, 177→108).The full mapping is in evidence/CANONICAL-NUMBERS.md §7.
1.2 (2026-06-30): the autonomous failure-recovery loop moved from pre-implementation to
built + measured (C7; W1 net in §7.8); cross-session store currency quantified
against the distill model (C6, §7.7); a per-section "So what" reading added.
Verdict
On controlled tasks with executable graders and a no-memory control, bento delivers measurable value on the axes a hosted agent-memory layer should: it keeps answers current under updates, reduces cost on recurring work at equal accuracy, and unlocks correctness the base model cannot reach on an unreliable corpus. Its distinctive surface — graph retrieval, code-aware chunking, supersession-backed currency, procedural reuse — produces measured benefits over external comparators. The one cell a competitor led decomposes to the agent loop, not the backend; in the identical loop, bento ties.
We are equally explicit about what is not claimed: token savings at the frontier on the correctness task (cost is flat there, not reduced). The autonomous recovery loop, unproven in the v2 body, is now built and measured at Tier-1 — a prevented call saves ~27 ms at a net-positive token cost (C7, §7.8); the Tier-2 analyzer is deferred. (Two more follow-ups since the v2 body: pg-raggraph a18 delivered full version replay, §7.5; and a letta comparison ran — its agent ties bento on currency and procedural reuse, its archival backend can't supersede, and it has no graph, code-aware, or time-travel surface, §7.6.) The pre-registered seed count was not met (K=5 vs K≥10).
Appendix · deep statistics
The complete breakdown a stats-literate reviewer wants: accuracy mean ± standard
deviation, 95% CI, seed count, wall time, model calls, and tokens per cell — all recomputed from
the committed raw seed records. Source of truth: evidence/CANONICAL-NUMBERS.md;
narrative grid: evidence/full-competitive-grid.md.
| Arm | Tier | acc ± sd | 95% CI | n | wall s | calls | tokens |
|---|---|---|---|---|---|---|---|
| C1 · price_book — currency | |||||||
| baseline | local-26B | 0.950 ± 0.039 | 0.901–0.999 | 5 | — | — | — |
| bento | local-26B | 0.923 ± 0.025 | 0.892–0.954 | 5 | — | — | — |
| stele | local-26B | 0.973 ± 0.037 | 0.928–1.000 | 5 | — | — | — |
| naive cache | local-26B | 0.610 ± 0.148 | 0.426–0.794 | 5 | — | — | — |
| baseline | gpt-5.5 | 1.000 ± 0.000 | — | 5 | 187.0 | 120 | 29,553 |
| bento | gpt-5.5 | 1.000 ± 0.000 | — | 5 | 398.1 | 164 | 53,362 |
| stele | gpt-5.5 | 0.997 ± 0.007 | 0.988–1.000 | 5 | 389.1 | 161 | 52,259 |
| naive cache | gpt-5.5 | 0.667 ± 0.122 | 0.516–0.818 | 5 | 388.1 | 165 | 53,403 |
| mem0-as-naive | gpt-5.5 | 0.670 ± 0.127 | 0.512–0.828 | 5 | 418.1 | 277 | 55,116 |
| C2 · research_desk — cost-reuse (acc ties at 1.00; the win is cost) | |||||||
| baseline | local-193 | 1.000 ± 0.000 | — | 5 | 49.3 ± 1.7 | 177 | 106,045 |
| bento | local-193 | 1.000 ± 0.000 | — | 5 | 32.3 ± 2.9 | 108 | 61,940 |
| stele | local-193 | 1.000 ± 0.000 | — | 5 | 32.0 ± 5.1 | 108 | 61,627 |
| mem0 | local-193 | 0.940 ± 0.060 | — | 5 | 71.7 ± 3.8 | 285 | 106,524 |
| LangChain | local-193 | 0.960 ± 0.035 | — | 5 | 86.3 ± 6.1 | 166 | 103,849 |
| tc-bento | gpt-5.5 | 1.000 ± 0.000 | — | 5 | — | — | — |
| tc-mem0 | gpt-5.5 | 0.420 ± 0.149 | — | 5 | — | — | — |
| C3 · incident_rca — correctness-unlock | |||||||
| baseline | gpt-5.5 | 0.460 ± 0.135 | 0.292–0.628 | 5 | 610.9 | 240 | 131,148 |
| bento (auto-inject) | gpt-5.5 | 0.777 ± 0.056 | 0.707–0.847 | 5 | 580.3 | 230 | 132,723 |
| bento-agent | gpt-5.5 | 0.893 ± 0.025 | 0.862–0.924 | 5 | 530.0 | 183 | 126,350 |
| mem0-agent | gpt-5.5 | 0.890 ± 0.030 | 0.853–0.927 | 5 | 473.6 | 259 | 126,929 |
| mem0-infra | gpt-5.5 | 0.770 ± 0.079 | 0.672–0.868 | 5 | — | — | — |
| LangChain | gpt-5.5 | 0.490 ± 0.113 | 0.350–0.630 | 5 | 732.9 | 242 | 173,435 |
| Retriever | NDCG@5 (g) | NDCG@10 (g) | NDCG@10 (bin) |
|---|---|---|---|
| Single chain · no surface cue (6 q × 3, sd=0.000) — graph ≫ all | |||
| bento-local | 0.757 | 0.780 | 0.982 |
| bento-global | 0.757 | 0.733 | 0.928 |
| bento-naive (vector+BM25) | 0.051 | 0.159 | 0.231 |
| vector | 0.094 | 0.105 | 0.127 |
| bm25 | 0.061 | 0.066 | 0.115 |
| Scaled 8 chains · conventional / named (16 q) — graph (global) beats all | |||
| bento-global | 0.666 | 0.716 | 0.943 |
| bento-local / hybrid | 0.520 | 0.548 | 0.782 |
| bento-naive (vector+BM25) | 0.520 | 0.530 | 0.768 |
| bm25 | 0.482 | 0.538 | 0.741 |
| vector | 0.282 | 0.396 | 0.591 |
| Scaled 8 chains · pure-lexical codename (16 q) — bm25 wins (fusion-weight tunable) | |||
| bm25 | 0.878 | 0.886 | 1.000 |
| bento-global | 0.757 | 0.786 | 0.993 |
| bento-local / hybrid | 0.737 | 0.763 | 0.960 |
| bento-naive (vector+BM25) | 0.737 | 0.746 | 0.943 |
| vector | 0.568 | 0.674 | 0.829 |
| Answer model | needle-in-ctx | answer acc | tokens in/out |
|---|---|---|---|
| gemma-26B (local) | 1.00 | 0.333 | — |
| gemma-31B (local) | 1.00 | 0.500 | — |
| gpt-5.5 (frontier) | 1.00 | 1.000 | 1414 / 411 |
| Chunker | #chunks | intact | top-1 cmpl | MRR (cmpl) | NDCG@10 (cmpl) | MRR (region) |
|---|---|---|---|---|---|---|
| symbol_aware (bento) | 588 | 1.000 | 0.835 | 0.896 | 0.919 | 0.896 |
| fixed-size, 1200-char | 940 | 0.492 | 0.346 | 0.384 | 0.398 | 0.687 |
| fixed-size, 2000-char | 539 | 0.600 | 0.360 | — | — | — |
| C6 · KB currency (9 q, 3 generations) | post-update correct | time-travel probe | result |
|---|---|---|---|
| bento (re-retrieve) | 1.000 | as_of before ingest | excluded ✓ |
| naive cache (no invalidation) | 0.333 | as_of = now | serves current ✓ |
| as_of between versions | replay ✓ (a18 #90, §7.5) |
| C7 · procedural (4 ep × 6 orders) | arm | accuracy | refused ops | calls | tokens |
|---|---|---|---|---|---|
| gemma-26B (local) | no-memory | 1.00 | 12 | 52 | 20,944 |
| gemma-26B (local) | bento | 1.00 | 3 | 52 | 22,984 |
| gemma-31B (local) | no-memory | 1.00 | 12 | 52 | 19,384 |
| gemma-31B (local) | bento | 1.00 | 3 | 52 | 20,323 |
| gpt-5.5 (frontier) | no-memory | 1.00 | 12 | 52 | 20,082 |
| gpt-5.5 (frontier) | bento | 1.00 | 3 | 52 | 22,350 |
Notes: C4/C5/C6 are deterministic (no seed variance — the NDCG runs show sd=0.000 across 3 repeats); C7 is identical across all three model tiers (the reuse benefit is behavioral, not model-bound). C7's token cost is flat-to-slightly-higher — the win is the −75% in refused operations, not tokens. Significance reads (95% CI overlap) are in §5; threats to validity in §6.