.maestro/playbooks/Wizard-2026-02-22/2026-02-22-LoCoMo-Eval/LOCOMO-EVAL-04.md
This phase implements two complementary scoring methodologies. Token-level F1 follows the original LoCoMo paper (ACL 2024) for comparison with historical baselines. LLM-as-a-Judge (J-score) follows the Mem0 paper (arXiv 2504.19413, ECAI accepted) for comparison with modern memory systems — Mem0 (66.88%), Mem0g (68.44%), Zep (65.99%), full-context (72.90%). The J-scorer uses Claude Sonnet 4.6 as judge (different model from the Opus 4.6 answerer to avoid self-judging bias), with 10 independent runs per question for statistical significance, matching Mem0's methodology.
Build F1 scoring module:
evals/locomo/src/scoring/f1.tsnormalizeAnswer(text: string) — apply the standard normalization pipeline used in extractive QA benchmarks:
porterStem(word: string) — implement a minimal Porter stemmer (Step 1a/1b/1c at minimum: plurals, -ed, -ing, -ness). Alternatively, check if the LoCoMo repo contains their scoring implementation at data/locomo-repo/ — if they provide a Python scoring script, match their exact normalization pipeline for fair comparisontokenize(normalizedText: string) — split on whitespace, apply Porter stemming to each tokencomputeTokenF1(predicted: string, groundTruth: string) — the core scoring function:
common = intersection of predicted tokens and ground truth tokens (use multiset intersection — count each token occurrence)Build LLM-as-a-Judge scoring module:
evals/locomo/src/scoring/judge.tsbuildJudgePrompt(question: string, groundTruth: string, predictedAnswer: string, category: string):
{"score": <0-100>, "explanation": "<1-2 sentence rationale>"}judgeAnswer(question: string, groundTruth: string, predictedAnswer: string, category: string) — single judge call:
claude-sonnet-4-6JudgeResult (score 0-100, explanation string)judgeAnswerMultipleRuns(question: string, groundTruth: string, predictedAnswer: string, category: string, numRuns: number) — run judgeAnswer N times (default: 10) and aggregate:
JudgeAggregation ({ mean_score, std_dev, run_count, individual_scores })Build results aggregation and reporting module with dual metrics:
evals/locomo/src/scoring/reporter.tsevals/locomo/src/types.tsscoreResultsF1(results: Array<{predicted_answer: string, ground_truth: string, category: string}>) — apply computeTokenF1 to each result, return array of QAResult with f1_score filled inaggregateF1ByCategory(results: QAResult[]) — group by category, compute mean F1 per category, return map of category to {mean_f1, count, min_f1, max_f1}computeOverallF1(results: QAResult[]) — macro average: sum of all F1 scores / total questionsaggregateJudgeByCategory(results: QAResult[]) — group by category, compute mean J-score per category (averaging the per-question mean_scores), return map of category to {mean_j, pooled_std_dev, count}computeOverallJudge(results: QAResult[]) — macro average of per-question mean J-scores, with pooled standard deviationF1_BASELINES = {
"Human": { overall: 87.9 },
"Mem0": { overall: null, single_hop: 38.72, multi_hop: 28.64, temporal: 48.93, open_domain: 47.65 },
"Mem0g": { overall: null, single_hop: 38.09, multi_hop: 24.32, temporal: 51.55, open_domain: 49.27 },
"Zep": { overall: null, single_hop: 35.74, multi_hop: 19.37, temporal: 42.00, open_domain: 49.56 },
"LangMem": { overall: null, single_hop: 35.51, multi_hop: 26.04, temporal: 30.75, open_domain: 40.91 },
"OpenAI Memory": { overall: null, single_hop: 34.30, multi_hop: 20.09, temporal: 14.04, open_domain: 39.31 },
"A-Mem": { overall: null, single_hop: 20.76, multi_hop: 9.22, temporal: 35.40, open_domain: 33.34 },
"GPT-3.5-turbo-16K": { overall: 37.8 },
"RAG-observations (original paper)": { overall: 41.4 },
"GPT-4-turbo": { overall: 32.1 }
}
J_BASELINES = {
"Full-context": { overall: 72.90 },
"Mem0g": { overall: 68.44, single_hop: 65.71, multi_hop: 47.19, temporal: 58.13, open_domain: 75.71 },
"Mem0": { overall: 66.88, single_hop: 67.13, multi_hop: 51.15, temporal: 55.51, open_domain: 72.93 },
"Zep": { overall: 65.99, single_hop: 61.70, multi_hop: 41.35, temporal: 49.31, open_domain: 76.60 },
"RAG (best, k=2 256-tok)": { overall: 60.97 },
"LangMem": { overall: 58.10, single_hop: 62.23, multi_hop: 47.92, temporal: 23.43, open_domain: 71.12 },
"OpenAI Memory": { overall: 52.90, single_hop: 63.79, multi_hop: 42.92, temporal: 21.71, open_domain: 62.29 },
"A-Mem": { overall: 48.38, single_hop: 39.79, multi_hop: 18.85, temporal: 49.91, open_domain: 54.05 }
}
formatF1ComparisonTable(evalResults: QAResult[], f1Baselines) — markdown table comparing claude-mem F1 against all F1 baselines, per-category and overallformatJudgeComparisonTable(evalResults: QAResult[], jBaselines) — markdown table comparing claude-mem J-scores against all J baselines, per-category and overall, with ± stddevformatLatencyComparisonTable(latencyStats: LatencyStats) — markdown table comparing claude-mem latency/tokens against Mem0's published numbers (search p50: 0.148s, search p95: 0.200s, total p50: 0.708s, total p95: 1.440s, tokens: 1,764)formatFullReport(evalResults: QAResult[]) — generate complete markdown report string combining all three comparison tables plus per-category breakdown, top-5/bottom-5 questions for error analysis, and latency/token summaryWrite scoring tests (F1 + Judge):
evals/locomo/tests/f1-scoring.test.tsevals/locomo/tests/judge-scoring.test.tsbuildJudgePrompt includes question, ground truth, predicted answer, and categoryjudgeAnswer — verify it calls with model claude-sonnet-4-6, temperature 0.5, max_tokens 256judgeAnswerMultipleRuns aggregation: mock 10 runs returning scores [70, 72, 68, 71, 73, 69, 70, 72, 71, 70] → verify mean ≈ 70.6, stddev ≈ ~1.5Run all scoring tests and fix failures:
bun test evals/locomo/tests/f1-scoring.test.tsbun test evals/locomo/tests/judge-scoring.test.tsScore the Phase 03 prototype results with both methods:
evals/locomo/scripts/score-prototype.tsevals/locomo/results/qa-prototype-results.jsonscoreResultsF1 to get F1 scores for each question. Print per-category F1 breakdown and overall F1.judgeAnswerMultipleRuns with 10 runs. This will make 10 × N judge API calls — for the ~20 prototype questions, that's ~200 calls. Add a 200ms delay between batches.bun evals/locomo/scripts/score-prototype.ts