Benchmarks¶
How good is Particles as an agent memory? This page is the publication
surface for the agent-memory benchmark evaluation:
LongMemEval (Wu et al., ICLR 2025) run against the pipeline the
agent-memory wedge actually ships — deposit each haystack chat session as
a CONVERSATION corpus entry, standard extraction + §6.6 reconciliation,
top-k query retrieval — under default configuration and default
thresholds. Not a benchmark-tuned lab build.
Results¶
SUBSET run — 150 of 500 questions. Inaugural run, 2026-08-16. Selection tuple: LongMemEval
s(cleaned) · dataset revision98d7416c24c778c2fee6e6f3006e7a073259d48f·sample_seed=13· strata = all six question types ·limit=150·top_k=10· answer / judge / extraction modelanthropic:claude-sonnet-5· embedding modelall-MiniLM-L6-v2· thresholdsextraction.similarity_threshold=0.8,confidence.uncalibrated_cap.enabled=false. Raw report JSON, the artifact of record:benchmarks/longmemeval-s150-2026-08-16.json. A later full-500 run replaces this table outright — the two are never averaged or mixed.
Retrieval stage — 143 questions scored; 7 abstention questions
excluded (6aeb4375_abs, f685340e_abs, 80ec1f4f_abs, 88432d0a_abs,
0862e8bf_abs, gpt4_70e84552_abs, gpt4_93159ced_abs), see below.
| Question type | n | Recall@10 | Precision@10 |
|---|---|---|---|
| All | 143 | 0.940 | 0.668 |
| knowledge-update | 21 | 0.976 | 0.710 |
| multi-session | 38 | 0.984 | 0.771 |
| single-session-assistant | 17 | 0.882 | 0.753 |
| single-session-preference | 9 | 0.889 | 0.311 |
| single-session-user | 20 | 0.950 | 0.620 |
| temporal-reasoning | 38 | 0.908 | 0.613 |
End-to-end QA — 150 questions, one answering model
(anthropic:claude-sonnet-5) across all three conditions:
| Question type | n | qa_particles |
qa_full_context (baseline) |
qa_no_memory (floor) |
|---|---|---|---|---|
| All | 150 | 0.733 | 0.793 | 0.080 |
| knowledge-update | 23 | 0.739 | 0.826 | 0.087 |
| multi-session | 40 | 0.800 | 0.750 | 0.050 |
| single-session-assistant | 17 | 0.647 | 0.941 | 0.235 |
| single-session-preference | 9 | 0.333 | 0.667 | 0.000 |
| single-session-user | 21 | 0.762 | 0.905 | 0.048 |
| temporal-reasoning | 40 | 0.775 | 0.725 | 0.075 |
Read plainly: the store's top-10 particles cover the labeled evidence
session(s) 94 % of the time, and an answering model given only those ten
particles answers correctly 73 % of the time — against 79 % when the same
model is handed the entire haystack, and 8 % with no memory at all. The
full-context baseline wins overall, by 6 points; it wins large on the
three single-session types (where the whole answer sits in one session the
haystack contains verbatim) and loses to the particle path on
multi-session and temporal-reasoning (where the answer is assembled
across sessions). The gap between 94 % retrieval recall and 73 % answer
accuracy is the claim-granularity cost: the right session is retrieved
but the ten particles do not always carry the specific fact — the
single-session-preference row (0.31 precision, 0.33 accuracy, n=9) is the
sharpest instance. Cost of the run at Sonnet 5 introductory pricing, with
extraction routed through the Message Batches API: ≈ US$2.70 per question.
Run notes (disclosed so the numbers can be read correctly):
- The run was executed in five checkpointed segments — the
per-question checkpoint — spanning two harness fixes that
landed on
mainmid-run. The first 12 questions were extracted with live Wikidata subject resolution on for every conversational subject name; the remaining 138 with it off (subjects.skip_live_authorities_source_types, 1.129.8). Retrieval and QA scoring never read subject identity, so the effect on the table is near zero — but §6.6 reconciliation is subject-gated, so it is not provably zero. - Five questions were re-run after a budget fix. Sonnet 5's adaptive
thinking spends from the same
max_tokensas the answer, and five calls (threeqa_full_contextanswers at the old 1024 cap, two abstention-judge verdicts at the old 16 cap) returned no text block and were scored incorrect. The caps were raised (4096 / 1024) and those five questions were dropped from the checkpoint and re-run end to end. A higher cap never changes a reply that finished under the lower one, so the other 145 questions are unaffected. The final report carries zero failed answer or judge calls. - Extraction ran pooled —
--pooled, the fan-in to the batch API — atextraction.max_tokens=16384(the default 8192 truncated ~45 % of sessions in a smoke test). Even so, ≈ 437 of ≈ 6,900 extraction calls (~6 %) hit the 16k cap and lost the tail of that session's candidate list; the retrieval and QA numbers include that loss. - Sonnet 5 rejects the
temperatureparameter, so the answer and judge calls ran at the model's default sampling: a re-run will not reproduce these numbers exactly, only within sampling noise. - The judge is Sonnet 5 following the dataset's per-type autoeval protocol (see § Judge deviation): comparable within this table, not to the paper's leaderboard.
Comparator memories — the same run with a different memory¶
The four conditions above anchor Particles against no memory and the
whole haystack. They say nothing about how it compares with the memory an
agent harness already gives you. So the same 150 questions were re-run with
the particle store swapped for two comparator memories
(particles benchmark memory --memory chunks|notes --no-baselines): the same
selection tuple, the same answer scaffold and answering model, the same
judge, the same session-granularity retrieval scoring; only the memory
differs. The qa_full_context and qa_no_memory columns are the particles
run's — under an identical tuple they are the same calls — and are marked
reused. Reports of record:
chunks,
notes.
chunks— raw-transcript RAG. Every session cut into turn-aligned chunks of ≤ 1,500 characters, embedded with the same MiniLM model the store uses, top-10 chunks by cosine handed to the answerer. No write-time LLM call. Asks: does claim extraction add anything over retrieving the transcript itself?notes— LLM-written session notes, the harness-memory pattern (a "summarise each conversation into a notes file" agent). Every session summarised once by the same Sonnet 5 that wrote the particles (median note 2.4k characters), notes embedded, top-10 notes handed to the answerer. Asks: does Particles' epistemic layer beat plain distillation?
Retrieval stage (143 questions; same 7 abstention questions excluded):
| Memory | Recall@10 | Precision@10 | Items per session |
|---|---|---|---|
particles |
0.940 | 0.668 | many (one per claim) |
chunks |
0.912 | 0.652 | several (one per ≤1.5k chars) |
notes |
0.950 | 0.171 † | exactly one |
† Precision@10 is not comparable across memories of different item
granularity: a notes memory has one item per session, so with one or
two labeled evidence sessions at most one or two of ten retrieved items can
ever be hits — 0.17 is close to that structural ceiling, not a defect. Read
recall across the three; read precision only within a memory.
End-to-end QA (150 questions, one answering model, claude-sonnet-5):
| Question type | n | qa_particles |
qa_chunks |
qa_notes |
qa_full_context (reused) |
qa_no_memory (reused) |
|---|---|---|---|---|---|---|
| All | 150 | 0.733 | 0.693 | 0.813 | 0.793 | 0.080 |
| knowledge-update | 23 | 0.739 | 0.783 | 0.870 | 0.826 | 0.087 |
| multi-session | 40 | 0.800 | 0.450 | 0.775 | 0.750 | 0.050 |
| single-session-assistant | 17 | 0.647 | 1.000 | 0.882 | 0.941 | 0.235 |
| single-session-preference | 9 | 0.333 | 0.889 | 0.778 | 0.667 | 0.000 |
| single-session-user | 21 | 0.762 | 0.810 | 0.810 | 0.905 | 0.048 |
| temporal-reasoning | 40 | 0.775 | 0.650 | 0.800 | 0.725 | 0.075 |
Read plainly, and published as-is:
- Particles beats raw-transcript RAG by 4 points overall — decisively
where the answer is assembled across sessions (
multi-session0.80 vs 0.45,temporal-reasoning0.78 vs 0.65) and it loses on every single-session type, where a verbatim chunk carries the exact wording a claim paraphrases away. Claim extraction earns its keep for cross-session synthesis, not for lookup. - LLM-written session notes beat Particles by 8 points — and beat the
full-context baseline by 2. Distillation of the whole session into
notes, retrieved at session granularity, is the strongest memory in this
table on this benchmark, and it is the memory pattern an agent harness
already ships. On recall-style question answering, Particles' epistemic
layer (claim granularity, provenance, confidence, reconciliation) does
not show up as answer accuracy; only
multi-session(0.80 vs 0.78) is a Particles edge, and it is inside sampling noise at n=40. - The read-time context is not equal, and that is part of the story: the ten retrieved particles are ~1.3–2k characters (mean claim ~90 characters plus date and subjects), ten chunks up to 15k, ten notes ~24k (median). The notes memory hands the answerer roughly ten times the context Particles does — still ~5 % of the haystack, which is why it beats full context — so the table above compares memories as configured. The budget-matched arm is below.
- Cost: chunks ≈ US$2 (150 answer + judge calls, no write-time calls); notes ≈ US$70 (7,123 session notes: 3,088 through the Batches API, ~4,000 at full price after the batch queue stalled — every batched note is cached beside the checkpoints, so a re-run pays nothing). 84 notes (1.2 %) were truncated at the 2,048-token cap and 4 (0.06 %) failed to write (those sessions were absent from that question's memory — disclosed in the report's quality notes).
Budget-matched arm — the same comparators at Particles' context size¶
The two comparators were re-run with the answerer's context clamped to
500 tokens (~2,000 characters) — the same clamp the particles path
exposes (--context-budget): items are appended in rank order
until the next would exceed the budget, the first always kept, so a notes
memory keeps its top note (median 2.4k chars — slightly more than the
particles context), a chunks memory one or two chunks. Retrieval and
top_k are untouched (recall is identical to the unclamped runs);
selection.context_budget_tokens=500 marks the reports. Reports of record:
chunks @500,
notes @500.
Cost ≈ US$3 (answer + judge only; every note came from the cache).
| Question type | n | qa_particles (~1.3–2k chars) |
qa_chunks @ 2k |
qa_notes @ 2k |
qa_chunks unclamped |
qa_notes unclamped |
|---|---|---|---|---|---|---|
| All | 150 | 0.733 | 0.293 | 0.393 | 0.693 | 0.813 |
| knowledge-update | 23 | 0.739 | 0.391 | 0.391 | 0.783 | 0.870 |
| multi-session | 40 | 0.800 | 0.200 | 0.125 | 0.450 | 0.775 |
| single-session-assistant | 17 | 0.647 | 0.412 | 0.882 | 1.000 | 0.882 |
| single-session-preference | 9 | 0.333 | 0.333 | 0.556 | 0.889 | 0.778 |
| single-session-user | 21 | 0.762 | 0.429 | 0.714 | 0.810 | 0.810 |
| temporal-reasoning | 40 | 0.775 | 0.200 | 0.250 | 0.650 | 0.800 |
Read plainly: at equal read-time context, Particles wins by a wide
margin — 0.733 against 0.393 for session notes and 0.293 for transcript
chunks, and the clamp is if anything generous to the comparators (a single
note is longer than the whole particles context). The two comparators lose
almost everything on the cross-session types (multi-session 0.20 / 0.125,
temporal-reasoning 0.20 / 0.25 against 0.80 / 0.78): one session's note
or chunk cannot carry an answer assembled from several sessions, while ten
claims from up to ten sessions can. Notes hold up on the single-session
types (0.88 / 0.71 on assistant / user), where the top note is the
answer's session and its ~2.4k characters carry the fact verbatim.
So the two tables together say one thing precisely — and it is the claim
this page supports: at a fixed read-time context budget of ~2k
characters, Particles answers 1.9× as many LongMemEval questions correctly
as LLM-written session notes (0.733 vs 0.393) and 2.5× as many as RAG
over the raw transcript (0.733 vs 0.293). Its edge is information
density — the most answer per read-time token — and its deficit is
coverage: given a much larger budget, whole-session distillation
recovers what claim extraction paraphrased away and then some. Which memory
is "better" depends on the read-time budget the agent can afford; on this
benchmark the crossover lies somewhere between ~2k and ~24k characters of
context, and pinning it (a sweep of --context-budget on the notes path)
is the natural next run.
Two measurement families — never merged¶
Every run measures four conditions and reports them in two separately labeled families. Conflating them is the endemic dishonesty mode of the memory-benchmark space ("our memory retrieves the right session 85% of the time" quoted as "answers correctly 85% of the time"), so the separation is structural: the report model has no aggregate score field, and the renderer has no way to merge the sections.
Retrieval stage — a property of the particle store and its ranker, saying nothing about answer accuracy:
| Condition | What it measures |
|---|---|
retrieval |
Evidence-session Recall@k and Precision@k of the store's top-k query result, scored by mapping each retrieved particle through its provenance chain (particle → corpus entry → URI-R → haystack session) against the dataset's labeled evidence sessions |
Abstention questions are excluded here, with a disclosed count. An
abstention variant (*_abs) has no evidence session by protocol — the
right answer is "you never told me" — so retrieval is unscoreable for it,
and blending it in would inflate mean recall (a vacuous 1.0 with nothing to
miss) and deflate mean precision (every retrieved particle a "false
positive" against an empty set). The exclusion keys off the protocol flag,
not the label shape: the cleaned dataset labels each abstention question
with its near-miss session (the one that mentions a similar-but-different
fact), and that label is deliberately not treated as evidence. They
contribute to no retrieval aggregate; the table discloses the excluded count
and lists each excluded question as n/a (abstention). They remain fully in the QA
family below, where the dataset's protocol scores them (credit for declining
to answer).
End-to-end QA — an answering LLM on top of (or instead of) the memory, which can recover from bad retrieval or fumble good retrieval:
| Condition | What it measures |
|---|---|
qa_particles |
Accuracy of the answering model given the question + the top-k retrieved particles (claim text, subjects, dates) |
qa_full_context |
The baseline that must not be buried: the same model, same prompt scaffold, with the entire concatenated haystack instead of retrieved particles. Published either way — including if it wins |
qa_no_memory |
The same model with the question only — the parametric-guessing / abstention floor |
Conditions ii–iv use one answering model pinned to one resolved model
id; the runner refuses to run a QA condition set whose resolved models
differ. If a baseline condition was skipped, its row renders as not run —
there is no flag to omit it, so a partial comparison is always visibly
partial.
A call that produced no verdict is excluded from the accuracy denominator,
with a disclosed count. If the answer or the judge call yields no usable
reply, that question is not scored wrong for that condition — it is scored
not at all, counted, and named in the table. The reason is asymmetry:
qa_full_context sends ~115k tokens per call and qa_no_memory sends three
lines, so any shared failure rate would land almost entirely on the baseline,
weakening it with transport noise rather than with anything about the memory.
The exclusion is disclosed per condition and split by cause, because the
two mean opposite things to a reader:
- output-budget — the reply carried no text within
max_tokens. An extended-thinking model spends its thinking from the same budget, so this is a configuration error on our side; the table says so, and the fix is to raise the cap and re-run those questions. It is deliberately not retried, since an identical call at an identical cap reproduces it. - infra — the call still failed after its retries.
The table also states, before any of this, whether the full-context baseline
fits the answering model's context window on the variant being run. A run
whose haystack would overflow is refused rather than reported: an overflowing
baseline is not a weaker baseline, it is a destroyed one, while the
question-only condition sails through untouched. This is the standing
precondition on the larger m variant.
Both rules were added in v1.137.1 and bind future runs. No number on this page changed: the run below carries zero failed answer or judge calls (see its run notes), so there was nothing to restate.
Judge deviation — read before comparing¶
Answers are scored by an LLM judge following the dataset's
per-question-type autoeval protocol, ported to an Anthropic judge
(routed through the llm.benchmark purpose) rather than the paper's OpenAI
judge. Abstention-variant questions score per the dataset protocol (credit
for declining to answer). Consequence: numbers on this page are
comparable within the table — same judge, same protocol, same selection —
not across leaderboards. An OpenAI-judge protocol-fidelity option is
deferred until cross-leaderboard comparability becomes a requirement.
Subset labeling discipline¶
Any run over fewer than all questions is a subset run, and the table
header must say so — including the full selection tuple that makes it
reproducible: dataset revision, variant, sample seed, strata (question
types), limit, resolved answer/judge model ids, the resolved extraction
model id and embedding model id (the store's contents are a function
of the first and the ranking of the second — two runs that differ on either
are different pipelines, not comparable), top_k, and a snapshot of the
pipeline thresholds in effect. The answer, extraction, and judge model
resolutions are each pinned mid-run by refusal — a drift aborts the run
rather than silently mixing pipelines. Two runs with the same recorded
tuple are comparable; anything else is disclosed drift. Subset and full-run
numbers are never mixed in one table.
Reproducing¶
# Cost preview only — no LLM call is made
particles benchmark memory --estimate
# Dev loop (defaults: 10 questions, s variant, seeded stratified selection)
particles benchmark memory
# The publishable runs (operator-invoked, never inside a test suite)
particles benchmark memory --limit 150 --variant s --format json --output report.json
particles benchmark memory --all --variant s --format json --output report.json
# The comparator memories over the same selection (reuse the particles run's
# qa_full_context / qa_no_memory columns — same tuple, same calls)
particles benchmark memory --limit 150 --variant s --memory chunks --no-baselines --format json --output chunks.json
particles benchmark memory --limit 150 --variant s --memory notes --no-baselines --format json --output notes.json
The dataset (LongMemEval v1 cleaned, MIT-licensed, ~3 GB) is downloaded on
demand from HuggingFace at a pinned revision with SHA-256 verification and
cached under ~/.particles/benchmark/longmemeval/ — never vendored into
the repository. Answering routes through the llm.benchmark_answer
config purpose; the judge through llm.benchmark. Each question runs in an
ephemeral scratch store, so a benchmark run never touches a user store.
Relationship to the extractor benchmarks¶
The particles extractor benchmark* verbs — including the modality and
polarity variants — measure a single extractor's output against gold
particles. This page's benchmark measures the whole pipeline against
gold answers — a different system under test, reported under its own
particles benchmark verb group.
particles extractor benchmark additionally persists each run's report as
a JSON file under benchmark.runs_dir (default ~/.particles/benchmark/runs/),
stamped with the resolved extraction provider:model pairing — the durable
raw series behind provider comparisons and calibration-drift analysis
. Pass --no-save for a throwaway run.
Extraction is a sampling process, so a single run is a single sample:
--runs N repeats each suite N times and reports each metric's mean, range
and standard deviation rather than one point estimate — the error bars a
provider comparison needs before it calls a gap real. Each pass still
persists its own report file. The cost is N× the LLM calls, so the repeat
path prints its projection first and asks before spending above
benchmark.confirm_call_threshold (--estimate prints and exits; --yes
pre-confirms). --fail-on is evaluated against the mean across runs.
A single run (the default) is unchanged and never gated.