Documentation
¶
Overview ¶
Package bench is a retrieval benchmark harness: it ingests a dataset of memories and scores each question's gold retrieval (Recall@K, MRR) and latency. Runs offline on the committed sample with a deterministic local embedder, or against a real endpoint and a converted LongMemEval/LoCoMo set.
Index ¶
- Variables
- func AnswerAndJudge(ctx context.Context, svc *service.Service, judge llm.Completer, q Question, ...) (correct bool, answer string, err error)
- func IngestQAUpsert(ctx context.Context, st store.Store, e embed.Embedder, items []Item) error
- func IngestQAWrite(ctx context.Context, st store.Store, e embed.Embedder, items []Item, ...) error
- func JudgeSystemFor(category string) string
- func LoadCodingAgent(path string) (*Dataset, *CodingAgentMeta, error)
- func Markdown(results []Result) string
- func McNemarExact(b, c int) float64
- func NamespaceOf(group string) string
- func RerankGateMarkdown(rows []RerankGateResult, k int) string
- func RerankMarkdown(results []RerankResult, k int) string
- func VecGateMarkdown(rows []VecGateResult, k int) string
- type ChatStats
- type CodingAgentMeta
- type CountingChat
- type CountingDistiller
- type Dataset
- func LoadFile(path string) (*Dataset, error)
- func LoadLoCoMo(path string) (*Dataset, error)
- func LoadLoCoMoSessions(path string) (*Dataset, error)
- func LoadLongMemEval(path string, mode DocMode) (*Dataset, error)
- func Poison(ds *Dataset, perGroup int, filler string) *Dataset
- func Sample() (*Dataset, error)
- func SplitHoldout(ds *Dataset, mode string) (*Dataset, error)
- type DistillStats
- type DocMode
- type IngestMode
- type Item
- type Question
- type RecallHit
- type RerankGateResult
- type RerankResult
- type Result
- type System
- type SystemOpts
- type VecGateResult
Constants ¶
This section is empty.
Variables ¶
var CodingAgentCategories = map[string]bool{ "decision": true, "convention": true, "rationale": true, "current-state": true, "synthesis": true, "temporal-update": true, "abstention": true, }
CodingAgentCategories is the fixed question-category vocabulary for the coding-agent-memory suite. The gold audit rejects any other value.
Functions ¶
func AnswerAndJudge ¶ added in v0.5.10
func AnswerAndJudge( ctx context.Context, svc *service.Service, judge llm.Completer, q Question, k int, level service.ReasoningLevel, ) (correct bool, answer string, err error)
AnswerAndJudge runs the production answer path (recall + service.Answer's reader prompt; the agentic tool loop when a reasoning level is set) and grades the reply against the reference. It returns the verdict and the raw answer text so a paired comparison can list the discordant cases for inspection.
func IngestQAUpsert ¶ added in v0.5.10
IngestQAUpsert loads items directly into the store (retrieval-only baseline): semantic tier, dated at the item time so temporal targeting can aim.
func IngestQAWrite ¶ added in v0.5.10
func IngestQAWrite(ctx context.Context, st store.Store, e embed.Embedder, items []Item, distiller llm.Distiller) error
IngestQAWrite feeds items through service.Remember sequentially in dataset order (write-path corroborate/contradict is order-sensitive), clocked at each item's time so temporal targeting and valid_to invalidation see the real chronology. TTL is forced to never-expire (question dates can fall long after a session, and the bench measures answer quality, not retention). Item.Session rides along as session_id metadata so the session-echo guard and distill batching see realistic keys. distiller, when non-nil, wires LLM distill-on-write (superseding the heuristic extractor, as in production) — one completion per capture.
func JudgeSystemFor ¶ added in v0.5.10
JudgeSystemFor returns the per-category judge rubric. The coding-agent suite's "temporal-update" reuses the knowledge-update rubric (the answer must reflect the latest value) and "abstention" the decline rubric; LongMemEval's knowledge-update / temporal-reasoning / *_abs categories keep their existing mappings.
func LoadCodingAgent ¶ added in v0.5.10
func LoadCodingAgent(path string) (*Dataset, *CodingAgentMeta, error)
LoadCodingAgent reads the coding-agent dataset. Unlike the LongMemEval/LoCoMo loaders, every item MUST carry a valid RFC3339 time and every question a valid now — the suite is temporally ordered, so a missing or malformed timestamp is an error, not a silent zero. Items are returned sorted by (time, id) so write-mode ingest replays the real chronology deterministically. The returned meta carries the audit-only fields (kind, superseded_by).
func McNemarExact ¶ added in v0.5.10
McNemarExact returns the two-sided exact-binomial p-value for a paired comparison with discordant counts b and c: b questions where arm A is correct and arm B is wrong, c where B is correct and A is wrong (concordant pairs carry no information about which arm is better). Under H0 each discordant pair is a fair coin, so the count is Binomial(n=b+c, p=0.5); the two-sided p-value is the total probability of a split at least as lopsided as observed. This exact form is used instead of the chi-square approximation because the pilot's discordant counts are small (n≈45 questions), where the approximation is unreliable.
func NamespaceOf ¶ added in v0.5.10
NamespaceOf exposes nsOf to the external bench_test package (the synthesis spike needs the store namespace a question group maps to).
func RerankGateMarkdown ¶ added in v0.4.13
func RerankGateMarkdown(rows []RerankGateResult, k int) string
RerankGateMarkdown renders the sweep, lowest threshold first.
func RerankMarkdown ¶
func RerankMarkdown(results []RerankResult, k int) string
RerankMarkdown renders the RRF-vs-composite comparison, grouped by category.
func VecGateMarkdown ¶ added in v0.4.13
func VecGateMarkdown(rows []VecGateResult, k int) string
VecGateMarkdown renders the sweep, lowest threshold first.
Types ¶
type ChatStats ¶ added in v0.5.10
type ChatStats struct {
Completes int64 `json:"completes"`
ToolRounds int64 `json:"tool_rounds"`
InTokens int64 `json:"in_tokens_est"`
OutTokens int64 `json:"out_tokens_est"`
LatencyMS int64 `json:"latency_ms"`
}
ChatStats is a snapshot of a CountingChat's counters. LatencyMS is the wall clock spent inside the wrapped LLM calls (not recall or judging).
type CodingAgentMeta ¶ added in v0.5.10
CodingAgentMeta holds the per-item fields the harness audits but the retrieval types (Item) do not carry: item kind and the supersession pointer. Keyed by item ID.
type CountingChat ¶ added in v0.5.10
type CountingChat struct {
// contains filtered or unexported fields
}
CountingChat wraps an llm.Client with per-direction token counters so an answer arm can report what the reader (and, in the agentic loop, each tool round) spent — the answer-path analogue of CountingDistiller. It implements both llm.Completer (single-shot reader + judge) and llm.ToolChat (the agentic loop): service.Answer type-asserts the answerer to ToolChat, so the wrapper must be passed as the WithAnswerer value or the loop silently degrades to single-shot. Token counts use the same ~4 bytes/token estimate as the retrieval metrics and cover the prompt/response text only.
func NewCountingChat ¶ added in v0.5.10
func NewCountingChat(c llm.Client) *CountingChat
NewCountingChat wraps c; if c implements llm.ToolChat the agentic loop is supported, otherwise ChatTools reports an error the loop treats as a fallback.
func (*CountingChat) ChatTools ¶ added in v0.5.10
func (c *CountingChat) ChatTools( ctx context.Context, system string, turns []llm.ChatTurn, tools []llm.Tool, choice llm.ToolChoice, ) (llm.ChatResult, error)
ChatTools forwards one agentic round, counting the round and payload tokens.
func (*CountingChat) Complete ¶ added in v0.5.10
Complete forwards to the wrapped completer, counting the call and payload tokens in each direction.
func (*CountingChat) Stats ¶ added in v0.5.10
func (c *CountingChat) Stats() ChatStats
Stats returns the current counter values.
type CountingDistiller ¶ added in v0.5.9
type CountingDistiller struct {
// contains filtered or unexported fields
}
CountingDistiller wraps a Distiller with cost/compression counters so a write-mode run can report what distill-on-write spent at ingest. Token counts use the same ~4 bytes/token estimate as the retrieval metrics and cover the JSON payloads only; the fixed distill prompt template is per-call overhead on top.
func NewCountingDistiller ¶ added in v0.5.9
func NewCountingDistiller(inner llm.Distiller) *CountingDistiller
NewCountingDistiller wraps inner with usage counters.
func (*CountingDistiller) Distill ¶ added in v0.5.9
func (c *CountingDistiller) Distill(ctx context.Context, in llm.DistillInput) ([]llm.Fact, error)
Distill forwards to the wrapped distiller, counting episodes consumed, facts produced, and estimated payload tokens in each direction.
func (*CountingDistiller) Stats ¶ added in v0.5.9
func (c *CountingDistiller) Stats() DistillStats
Stats returns the current counter values.
type Dataset ¶
type Dataset struct {
Name string `json:"name"`
Items []Item `json:"items"`
Questions []Question `json:"questions"`
}
Dataset is a normalized retrieval benchmark.
func LoadLoCoMo ¶
LoadLoCoMo converts the published LoCoMo file into the normalized Dataset. Each conversation is its own group/namespace (dialogue ids repeat across conversations); each dialogue turn is an item, and each QA's evidence ids are its gold set. Questions without evidence (e.g. adversarial) are skipped.
func LoadLoCoMoSessions ¶ added in v0.0.4
LoadLoCoMoSessions loads LoCoMo at SESSION granularity: each conversation session becomes one document (its turns concatenated), and a question's gold set is the session(s) holding its evidence turns. This matches how session-level memory systems (e.g. MemPalace) score LoCoMo, enabling an apples-to-apples comparison; LoadLoCoMo scores the harder turn granularity.
func Poison ¶ added in v0.0.11
Poison returns a copy of ds with perGroup debris items added to every group that has questions — simulating a low-quality bulk import (e.g. a mem0 export of restatements) collapsed into the namespace. The debris shares one content template so a dedup pass clusters and collapses it, modelling the realistic "exports are full of near-duplicates" case. Use it to measure the Recall@K delta a poisoned store suffers, and that dedup/curation recover it.
func SplitHoldout ¶ added in v0.5.9
SplitHoldout filters longmemeval questions into a deterministic tune/held split: every 10th question by load order is "held" (50 of 500), the rest are "tune" (450). "all" (or empty) returns the dataset unchanged. Items are pruned to the surviving questions' groups, and the name is suffixed so results files don't collide across splits.
type DistillStats ¶ added in v0.5.9
type DistillStats struct {
Calls int64 `json:"calls"`
Errors int64 `json:"errors"`
Episodes int64 `json:"episodes"`
Facts int64 `json:"facts"`
InTokens int64 `json:"in_tokens_est"`
OutTokens int64 `json:"out_tokens_est"`
}
DistillStats is a snapshot of a CountingDistiller's counters.
type DocMode ¶ added in v0.0.4
type DocMode string
LoadLongMemEval converts a LongMemEval file: each haystack session becomes an item, each question's answer_session_ids becomes its gold set. DocMode selects how a LongMemEval haystack session is rendered into one embedded item, for the vector-leg document-construction experiment.
const ( // DocFull renders "role: content\n" for every turn (the production shape). DocFull DocMode = "full" // DocUserOnly renders only user turns, with no role prefixes (MemPalace's // raw mode: assistant turns dilute the vector leg on user-question recall). DocUserOnly DocMode = "user-only" // DocDated prefixes the full session with its date, so temporal questions // have a textual anchor the embedder can see. DocDated DocMode = "dated" )
type IngestMode ¶ added in v0.5.9
type IngestMode string
IngestMode selects how the corpus enters the store.
const ( // IngestUpsert writes items directly via store.Upsert (the historical // default): pure retrieval measurement, write-path features inert. IngestUpsert IngestMode = "upsert" // IngestWrite routes items through service.Remember, exercising the shipped // write path: tier classification, gates, fingerprint/write dedup, // corroboration, and contradiction invalidation all participate. IngestWrite IngestMode = "write" )
type Item ¶
type Item struct {
ID string `json:"id"`
Content string `json:"content"`
Group string `json:"group,omitempty"`
Time time.Time `json:"-"`
// Session is a dev-session key (coding-agent suite): passed as session_id
// metadata on write-mode ingest. Source is a provenance pointer to the
// primary source the item was mined from (git hash, note file, plan). Both
// are zero for the LongMemEval/LoCoMo loaders.
Session string `json:"session,omitempty"`
Source string `json:"source,omitempty"`
}
Item is one memory to ingest; Group scopes it to a namespace, empty falls back to a shared default. Time, when set, is the memory's source timestamp (used to ground recency in the recency-aware re-ranking comparison).
type Question ¶
type Question struct {
Query string `json:"query"`
Gold []string `json:"gold"`
Group string `json:"group,omitempty"`
Answer string `json:"answer,omitempty"`
Category string `json:"category,omitempty"`
Now time.Time `json:"-"`
// GoldAll is the full evidence set for synthesis questions (answering
// requires combining every id); recall Gold credits any-hit, GoldAll drives
// coverage@k. Empty falls back to Gold. Provenance points at the primary
// source the gold answer was verified against. Both are used only by the
// coding-agent suite.
GoldAll []string `json:"gold_all,omitempty"`
Provenance string `json:"provenance,omitempty"`
}
Question is a query plus the gold memory IDs it should retrieve. Group must match its items; Answer/Category are populated for QA evaluation where available. Now, when set, is the query's reference time (e.g. the question date) — the "now" against which recency is measured.
type RecallHit ¶ added in v0.5.9
RecallHit is one retrieved memory. IDs usually holds a single dataset item ID; write-mode ingest can merge several items into one stored memory (fingerprint/write dedup), in which case the hit carries every item ID that landed on it. Rows the write path derived itself (extract-on-write) map to no item and keep their memory ID, which never matches gold — write-mode recall is conservative by construction.
type RerankGateResult ¶ added in v0.4.13
type RerankGateResult struct {
Threshold float64 `json:"threshold"`
PosRecallAtK float64 `json:"pos_recall_at_k"`
NegInjectionRate float64 `json:"neg_injection_rate"`
}
RerankGateResult is one cross-encoder relevance-score threshold's effect under a per-query gate: if a query's best rerank score (over its recall pool) is below the threshold, nothing relevant exists and recall returns empty. Positive = own namespace (recall must survive); negative = a foreign namespace (injection must collapse). Cross-encoders emit calibrated absolute relevance, unlike bi-encoder cosine — this measures whether that separation is real.
func RerankGateSweep ¶ added in v0.4.13
func RerankGateSweep( ctx context.Context, st store.Store, e embed.Embedder, ce *rerank.CrossEncoder, ds *Dataset, k, pool int, thresholds []float64, queryPrefix string, ) ([]RerankGateResult, error)
RerankGateSweep ingests once, then for every question reranks its recall pool (hybrid fusion + composite, top `pool`) against the query in its own namespace (positive) and in a foreign namespace (negative), recording the top rerank score and whether the gold lands in the reranked top-k. It reports the top rerank-score distribution and, per threshold, positive recall@k vs negative injection. Negatives pair each question with the next question's namespace.
type RerankResult ¶
type RerankResult struct {
System string
Category string
Questions int
RecallAt1 float64
RecallAtK float64
MRR float64
}
RerankResult is one ranking strategy's score over a question set.
func LLMRerankCompare ¶ added in v0.0.4
func LLMRerankCompare( ctx context.Context, st store.Store, e embed.Embedder, rr rerank.Reranker, ds *Dataset, k, fetch int, queryPrefix string, ) ([]RerankResult, error)
LLMRerankCompare measures the with-LLM read-side rerank lift on pure retrieval. For each question it builds the production candidate order (hybrid score fusion -> composite re-rank), then re-orders the top `fetch` with an LLM reranker, and scores recall@1/@k and MRR for both. The LLM tier is slow (one chat call per question), so drive it over a subset with cmd/bench -limit.
func RerankCompare ¶
func RerankCompare( ctx context.Context, st store.Store, e embed.Embedder, ds *Dataset, cats []string, k int, queryPrefix string, ) ([]RerankResult, error)
RerankCompare isolates the effect of recency-aware re-ranking: it ingests ds (items carry source timestamps), then for each selected question scores the SAME fused candidate set two ways — pure RRF order vs the composite re-ranker using the question's reference time. Reports recall@1, recall@K, and MRR per category and overall, for both strategies. cats empty means all categories.
type Result ¶
type Result struct {
System string `json:"system"`
Dataset string `json:"dataset"`
K int `json:"k"`
Questions int `json:"questions"`
RecallAtK float64 `json:"recall_at_k"`
MRR float64 `json:"mrr"`
P50Millis float64 `json:"p50_ms"`
P95Millis float64 `json:"p95_ms"`
IngestMs float64 `json:"ingest_ms"`
// TokensInjectedMean is the mean estimated token count of the top-K
// retrieved contents per question — what recall would inject into a
// consumer's context. TokenEfficiency divides it by the corpus's total
// estimated tokens: the cost axis of answering from memory vs full context.
TokensInjectedMean float64 `json:"tokens_injected_mean"`
TokenEfficiency float64 `json:"token_efficiency"`
PerCategory map[string]float64 `json:"per_category,omitempty"`
}
Result is one system's score on a dataset at a given K.
type System ¶
type System interface {
Name() string
Ingest(ctx context.Context, items []Item) error
Recall(ctx context.Context, group, query string, k int) ([]RecallHit, error)
}
System is a memory system under test.
func MeminiSystems ¶
func MeminiSystems( st store.Store, e embed.Embedder, concurrency int, queryPrefix string, fusionAlpha float64, poolFactor, poolFloor int, mode IngestMode, distiller llm.Distiller, ) []System
MeminiSystems returns the hybrid, vector-only, and keyword-only retrieval strategies sharing one ingested store. queryPrefix, when non-empty, is prepended to query embeddings (hybrid and vector legs), matching MEMINI_EMBED_QUERY_PREFIX in production. fusionAlpha < 0 uses RRF; >= 0 uses convex-combination score fusion with that vector weight. poolFactor/poolFloor override hybrid recall's per-leg pool sizing (non-positive keeps defaults). mode selects direct upserts (historical default) or the production write path. distiller, non-nil with write mode, enables LLM distill-on-write (nil keeps the heuristic extractor).
func MeminiSystemsOpts ¶ added in v0.5.10
MeminiSystemsOpts is MeminiSystems with the full option set, including dated ingest (SystemOpts.Dated) for temporally-ordered corpora.
type SystemOpts ¶ added in v0.5.10
type SystemOpts struct {
Concurrency int
QueryPrefix string
FusionAlpha float64 // < 0 uses RRF; >= 0 uses convex score fusion
PoolFactor int
PoolFloor int
Mode IngestMode
Distiller llm.Distiller
// Dated honors Item.Time instead of the fixed benchClock: upsert rows are
// stamped and dated at Item.Time; write-mode ingest advances a per-item clock
// (with ValidFrom, never-TTL, and session_id metadata), so contradiction and
// temporal recall see the real chronology. RecallNow is the clock recall runs
// under once ingest completes (zero = benchClock). Ignored when Dated is false.
Dated bool
RecallNow time.Time
}
SystemOpts configures MeminiSystemsOpts. The zero value reproduces MeminiSystems' historical defaults (fixed benchClock, undated ingest).
type VecGateResult ¶ added in v0.4.13
type VecGateResult struct {
Threshold float64 `json:"threshold"`
PosRecallAtK float64 `json:"pos_recall_at_k"`
NegInjectionRate float64 `json:"neg_injection_rate"`
}
VecGateResult is one absolute-vector-score threshold's effect under a per-query semantic-relevance gate: if a query's best raw vector score (1/(1+L2)) is below the threshold, nothing relevant exists and recall returns empty. Positive = each query against its own namespace (recall must survive); negative = the same query against a foreign namespace (injection must collapse). The right default is the knee: highest threshold where PosRecallAtK is ~unchanged but NegInjectionRate has dropped.
func VecGateSweep ¶ added in v0.4.13
func VecGateSweep( ctx context.Context, st store.Store, e embed.Embedder, ds *Dataset, k int, thresholds []float64, concurrency int, queryPrefix string, fusionAlpha float64, ) ([]VecGateResult, error)
VecGateSweep ingests once, then for every question measures the top raw vector score in its own namespace (positive) and in a foreign namespace (negative), plus whether the real fused recall already retrieves the gold. It reports, per threshold, the per-query gate's effect: positive recall@k (lost only when the own-namespace top vector score falls below the gate) and negative injection rate (a foreign query passes the gate when its top vector score clears it). Negatives pair each question with the next question's namespace; group ids are unique per question, so the paired namespace never holds the answer.