@1mbrain/benchmarks 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +85 -0
  2. package/fixtures/1mbrain-focused-mini/1mbrain-focused-mini.json +928 -0
  3. package/fixtures/1mbrain-focused-mini/README.md +45 -0
  4. package/fixtures/adversarial-memory/dataset_claude_adversarial.json +3333 -0
  5. package/fixtures/adversarial-memory/dataset_gemini_adversarial_memory.json +2984 -0
  6. package/fixtures/balanced-mini/dataset_claude_balanced_mini.json +2077 -0
  7. package/fixtures/balanced-mini/dataset_gemini_balanced_mini.json +1995 -0
  8. package/fixtures/generate_datasets.js +1741 -0
  9. package/fixtures/graph-stress-hard/README.md +43 -0
  10. package/fixtures/graph-stress-hard/dataset_graph_stress_hard.json +4374 -0
  11. package/fixtures/graph-stress-hard/generate_graph_stress_hard.js +526 -0
  12. package/fixtures/realistic-medium/dataset_claude_realistic_medium.json +7462 -0
  13. package/fixtures/realistic-medium/dataset_gemini_realistic_medium.json +7277 -0
  14. package/fixtures/realistic-medium/gen_claude_medium.js +600 -0
  15. package/package.json +22 -0
  16. package/reports/benchmark_report.md +48 -0
  17. package/reports/benchmark_report_claude_adversarial.md +42 -0
  18. package/reports/benchmark_report_claude_adversarial_adaptive.md +42 -0
  19. package/reports/benchmark_report_claude_adversarial_adaptive2_fast.md +42 -0
  20. package/reports/benchmark_report_claude_adversarial_adaptive_fast.md +42 -0
  21. package/reports/benchmark_report_claude_adversarial_rerank.md +42 -0
  22. package/reports/benchmark_report_claude_balanced_mini.md +42 -0
  23. package/reports/benchmark_report_claude_balanced_mini_adaptive.md +42 -0
  24. package/reports/benchmark_report_claude_balanced_mini_adaptive2_fast.md +42 -0
  25. package/reports/benchmark_report_claude_balanced_mini_adaptive_fast.md +42 -0
  26. package/reports/benchmark_report_claude_balanced_mini_rerank.md +42 -0
  27. package/reports/benchmark_report_claude_realistic_medium.md +42 -0
  28. package/reports/benchmark_report_claude_realistic_medium_adaptive.md +42 -0
  29. package/reports/benchmark_report_claude_realistic_medium_adaptive2_fast.md +42 -0
  30. package/reports/benchmark_report_claude_realistic_medium_adaptive_fast.md +42 -0
  31. package/reports/benchmark_report_claude_realistic_medium_evidence_rerank_local.md +42 -0
  32. package/reports/benchmark_report_claude_realistic_medium_openai_evidence_rerank.md +41 -0
  33. package/reports/benchmark_report_claude_realistic_medium_openai_multi_signal.md +41 -0
  34. package/reports/benchmark_report_claude_realistic_medium_openai_multi_signal_scoped.md +41 -0
  35. package/reports/benchmark_report_claude_realistic_medium_openai_phase8_no_judge.md +42 -0
  36. package/reports/benchmark_report_claude_realistic_medium_openai_rankingpolicy.md +41 -0
  37. package/reports/benchmark_report_claude_realistic_medium_openai_stale_filter.md +41 -0
  38. package/reports/benchmark_report_claude_realistic_medium_openai_stale_filter_absence_fix.md +41 -0
  39. package/reports/benchmark_report_claude_realistic_medium_openai_write_time_invalidation.md +41 -0
  40. package/reports/benchmark_report_claude_realistic_medium_rerank.md +42 -0
  41. package/reports/benchmark_report_claude_realistic_medium_stale_filter_local.md +42 -0
  42. package/reports/benchmark_report_graph_stress_hard.md +42 -0
  43. package/reports/benchmark_report_graph_stress_hard_absence_fix.md +42 -0
  44. package/reports/benchmark_report_graph_stress_hard_adaptive.md +42 -0
  45. package/reports/benchmark_report_graph_stress_hard_evidence_rerank.md +42 -0
  46. package/reports/benchmark_report_graph_stress_hard_multi_signal_current_guardrail.md +42 -0
  47. package/reports/benchmark_report_graph_stress_hard_multi_signal_guardrail_fixed.md +42 -0
  48. package/reports/benchmark_report_graph_stress_hard_multi_signal_local.md +42 -0
  49. package/reports/benchmark_report_graph_stress_hard_multi_signal_scoped_guardrail.md +42 -0
  50. package/reports/benchmark_report_graph_stress_hard_multi_signal_vector_pure_guardrail.md +42 -0
  51. package/reports/benchmark_report_graph_stress_hard_phase8_sdk_guardrail.md +42 -0
  52. package/reports/benchmark_report_graph_stress_hard_rerank.md +42 -0
  53. package/reports/benchmark_report_graph_stress_hard_stale_filter.md +42 -0
  54. package/reports/benchmark_report_graph_stress_hard_write_time_invalidation.md +42 -0
  55. package/results/.gitignore +2 -0
  56. package/src/adapters/1mbrain.ts +317 -0
  57. package/src/adapters/keyword-embedding.ts +48 -0
  58. package/src/adapters/mem0.ts +124 -0
  59. package/src/adapters/qdrant.ts +214 -0
  60. package/src/adapters/unavailable.ts +49 -0
  61. package/src/adapters/vector-baseline.ts +149 -0
  62. package/src/datasets/focused-mini.ts +158 -0
  63. package/src/datasets/synthetic-agent-memory.ts +532 -0
  64. package/src/llm-evaluator.ts +262 -0
  65. package/src/metrics.ts +482 -0
  66. package/src/provider.ts +151 -0
  67. package/src/runner.ts +635 -0
  68. package/tsconfig.json +10 -0
  69. package/tsconfig.tsbuildinfo +1 -0
package/src/metrics.ts ADDED
@@ -0,0 +1,482 @@
1
+ import type {
2
+ BenchmarkCase,
3
+ BenchmarkRecallResult,
4
+ BenchmarkScenarioType,
5
+ MemoryProviderAdapter,
6
+ ProviderAvailability,
7
+ } from './provider.js';
8
+
9
+ export interface OperationTrace {
10
+ kind: string;
11
+ label?: string;
12
+ latencyMs: number;
13
+ resultIds?: string[];
14
+ success?: boolean;
15
+ details?: Record<string, unknown>;
16
+ }
17
+
18
+ export interface CaseEvaluation {
19
+ precisionAt1: number;
20
+ precisionAt3: number;
21
+ precisionAt5: number;
22
+ recallAt3: number;
23
+ recallAt5: number;
24
+ mrr: number;
25
+ evidenceAccuracy: number;
26
+ deterministicSuccess: number;
27
+ abstentionAccuracy: number | null;
28
+ temporalCorrectness: number | null;
29
+ staleMemoryErrorRate: number | null;
30
+ deletedMemoryLeakageRate: number | null;
31
+ portabilitySuccessRate: number | null;
32
+ taskContextCoverage: number | null;
33
+ rankingMovement: number | null;
34
+ answerAccuracy: number | null;
35
+ hallucinationRate: number | null;
36
+ failureTags: string[];
37
+ notes: string[];
38
+ }
39
+
40
+ export interface ProviderCaseResult {
41
+ provider: string;
42
+ providerLabel: string;
43
+ capabilities: MemoryProviderAdapter['capabilities'];
44
+ scenarioId: string;
45
+ scenarioType: BenchmarkScenarioType;
46
+ supported: boolean;
47
+ unsupportedReason?: string;
48
+ error?: string;
49
+ memoryCount: number;
50
+ ingestMs: number;
51
+ latencyMs: number;
52
+ storageSizeBytes: number | null;
53
+ results: BenchmarkRecallResult[];
54
+ operationTraces: OperationTrace[];
55
+ evaluation: CaseEvaluation;
56
+ generatedAnswer?: string;
57
+ llmError?: string;
58
+ llmEvaluation?: {
59
+ model: string;
60
+ score0To5: number;
61
+ hallucination: boolean;
62
+ rationale: string;
63
+ };
64
+ }
65
+
66
+ export interface ProviderRunResult {
67
+ provider: string;
68
+ label: string;
69
+ capabilities: MemoryProviderAdapter['capabilities'];
70
+ availability: ProviderAvailability;
71
+ caseResults: ProviderCaseResult[];
72
+ }
73
+
74
+ export interface AggregatedMetrics {
75
+ caseCount: number;
76
+ unsupportedCaseCount: number;
77
+ errorCount: number;
78
+ precisionAt1: number;
79
+ precisionAt3: number;
80
+ precisionAt5: number;
81
+ recallAt3: number;
82
+ recallAt5: number;
83
+ mrr: number;
84
+ evidenceAccuracy: number;
85
+ deterministicSuccess: number;
86
+ abstentionAccuracy: number | null;
87
+ temporalCorrectness: number | null;
88
+ staleMemoryErrorRate: number | null;
89
+ deletedMemoryLeakageRate: number | null;
90
+ portabilitySuccessRate: number | null;
91
+ taskContextCoverage: number | null;
92
+ rankingMovement: number | null;
93
+ answerAccuracy: number | null;
94
+ hallucinationRate: number | null;
95
+ p50LatencyMs: number;
96
+ p95LatencyMs: number;
97
+ averageLatencyMs: number;
98
+ p50IngestMs: number;
99
+ p95IngestMs: number;
100
+ averageIngestMs: number;
101
+ averageStorageSizeBytes: number | null;
102
+ estimatedCostPer1kQueries: number;
103
+ }
104
+
105
+ export interface ProviderSummary {
106
+ provider: string;
107
+ label: string;
108
+ availability: ProviderAvailability;
109
+ capabilities: MemoryProviderAdapter['capabilities'];
110
+ overall: AggregatedMetrics;
111
+ byScenario: Partial<Record<BenchmarkScenarioType, AggregatedMetrics>>;
112
+ failureCounts: Record<string, number>;
113
+ }
114
+
115
+ export interface ProbeResults {
116
+ [label: string]: BenchmarkRecallResult[];
117
+ }
118
+
119
+ export function emptyEvaluation(): CaseEvaluation {
120
+ return {
121
+ precisionAt1: 0,
122
+ precisionAt3: 0,
123
+ precisionAt5: 0,
124
+ recallAt3: 0,
125
+ recallAt5: 0,
126
+ mrr: 0,
127
+ evidenceAccuracy: 0,
128
+ deterministicSuccess: 0,
129
+ abstentionAccuracy: null,
130
+ temporalCorrectness: null,
131
+ staleMemoryErrorRate: null,
132
+ deletedMemoryLeakageRate: null,
133
+ portabilitySuccessRate: null,
134
+ taskContextCoverage: null,
135
+ rankingMovement: null,
136
+ answerAccuracy: null,
137
+ hallucinationRate: null,
138
+ failureTags: [],
139
+ notes: [],
140
+ };
141
+ }
142
+
143
+ export function evaluateCase(
144
+ benchmarkCase: BenchmarkCase,
145
+ results: BenchmarkRecallResult[],
146
+ operationTraces: OperationTrace[],
147
+ probes: ProbeResults,
148
+ ): CaseEvaluation {
149
+ const returnedIds = results.map((result) => result.memoryId);
150
+ const top1 = returnedIds.slice(0, 1);
151
+ const top3 = returnedIds.slice(0, 3);
152
+ const top5 = returnedIds.slice(0, 5);
153
+ const required = benchmarkCase.expectations.requiredMemoryIds;
154
+ const forbidden = new Set(benchmarkCase.expectations.forbiddenMemoryIds);
155
+ const hitsAt5 = countHits(top5, required);
156
+ const forbiddenHits = top5.filter((id) => forbidden.has(id));
157
+ const evaluation = emptyEvaluation();
158
+
159
+ evaluation.precisionAt1 = precisionAtK(top1, required);
160
+ evaluation.precisionAt3 = precisionAtK(top3, required);
161
+ evaluation.precisionAt5 = precisionAtK(top5, required);
162
+ evaluation.recallAt3 = recallAtK(top3, required);
163
+ evaluation.recallAt5 = recallAtK(top5, required);
164
+ evaluation.mrr = mrrAtK(returnedIds, required, 5);
165
+ evaluation.evidenceAccuracy =
166
+ required.length === 0
167
+ ? benchmarkCase.expectations.shouldAbstain && returnedIds.length === 0
168
+ ? 1
169
+ : 0
170
+ : forbiddenHits.length > 0
171
+ ? 0
172
+ : hitsAt5 / required.length;
173
+
174
+ if (benchmarkCase.expectations.shouldAbstain !== undefined) {
175
+ if (required.length === 0) {
176
+ const abstained = returnedIds.length === 0;
177
+ evaluation.abstentionAccuracy =
178
+ benchmarkCase.expectations.shouldAbstain === abstained ? 1 : 0;
179
+ if (evaluation.abstentionAccuracy === 0) {
180
+ evaluation.failureTags.push('abstention_failed');
181
+ }
182
+ } else {
183
+ // If there are required memories, we expect the engine to find them.
184
+ // We shouldn't fail "abstention" at the retrieval layer if it successfully retrieves the required evidence of absence.
185
+ evaluation.abstentionAccuracy = 1;
186
+ }
187
+ }
188
+
189
+ if (benchmarkCase.scenarioType === 'selective_forgetting') {
190
+ const deletedLeakage = forbiddenHits.length > 0 ? 1 : 0;
191
+ evaluation.deletedMemoryLeakageRate = deletedLeakage;
192
+ if (deletedLeakage > 0) {
193
+ evaluation.failureTags.push('deleted_memory_leakage');
194
+ }
195
+ }
196
+
197
+ if (benchmarkCase.expectations.preferredOver?.length) {
198
+ const temporalChecks = benchmarkCase.expectations.preferredOver.map((check) =>
199
+ preferredWins(returnedIds, check.preferredId, check.competingIds),
200
+ );
201
+ const passed = temporalChecks.filter(Boolean).length;
202
+ evaluation.temporalCorrectness = temporalChecks.length === 0 ? null : passed / temporalChecks.length;
203
+ evaluation.staleMemoryErrorRate =
204
+ temporalChecks.length === 0 ? null : (temporalChecks.length - passed) / temporalChecks.length;
205
+ if (evaluation.temporalCorrectness !== 1) {
206
+ evaluation.failureTags.push('stale_memory_won');
207
+ }
208
+ }
209
+
210
+ if (benchmarkCase.expectations.probeComparisons?.length) {
211
+ const movements = benchmarkCase.expectations.probeComparisons
212
+ .map((comparison) =>
213
+ compareProbeRanks(
214
+ probes[comparison.labelBefore] ?? [],
215
+ probes[comparison.labelAfter] ?? [],
216
+ comparison.memoryId,
217
+ ),
218
+ )
219
+ .filter((movement): movement is number => movement !== null);
220
+
221
+ evaluation.rankingMovement = movements.length === 0 ? null : average(movements);
222
+ if (evaluation.rankingMovement !== null && evaluation.rankingMovement <= 0) {
223
+ evaluation.failureTags.push('refresh_did_not_improve_rank');
224
+ }
225
+ }
226
+
227
+ if (benchmarkCase.scenarioType === 'portability') {
228
+ const exportImportTrace = operationTraces.find((trace) => trace.kind === 'export_import');
229
+ const success = exportImportTrace?.success === true ? 1 : 0;
230
+ evaluation.portabilitySuccessRate = success;
231
+ if (success === 0) {
232
+ evaluation.failureTags.push('import_export_failed');
233
+ }
234
+ }
235
+
236
+ if (benchmarkCase.scenarioType === 'agent_task_context') {
237
+ evaluation.taskContextCoverage =
238
+ required.length === 0 ? 1 : countHits(top5, required) / required.length;
239
+ if (evaluation.taskContextCoverage < 1) {
240
+ evaluation.failureTags.push('task_context_incomplete');
241
+ }
242
+ }
243
+
244
+ if (required.length > 0 && hitsAt5 < required.length && benchmarkCase.scenarioType !== 'agent_task_context') {
245
+ evaluation.failureTags.push('missed_required_memory');
246
+ }
247
+ if (forbiddenHits.length > 0 && benchmarkCase.scenarioType !== 'selective_forgetting') {
248
+ evaluation.failureTags.push('retrieved_forbidden_memory');
249
+ }
250
+
251
+ const scenarioPass = scenarioDeterministicPass(
252
+ benchmarkCase,
253
+ hitsAt5,
254
+ results,
255
+ evaluation,
256
+ required.length,
257
+ );
258
+ evaluation.deterministicSuccess = scenarioPass ? 1 : 0;
259
+ if (!scenarioPass && benchmarkCase.scenarioType === 'multi_hop_recall') {
260
+ evaluation.failureTags.push('could_not_connect_multi_hop_facts');
261
+ }
262
+
263
+ if (results.length > 0 && operationTraces.length === 0 && benchmarkCase.scenarioType === 'noise_resistance') {
264
+ evaluation.notes.push('Noise resistance measured from final recall only.');
265
+ }
266
+
267
+ evaluation.failureTags = Array.from(new Set(evaluation.failureTags));
268
+ return evaluation;
269
+ }
270
+
271
+ export function aggregateProviderRuns(runs: ProviderRunResult[]): ProviderSummary[] {
272
+ return runs.map((run) => ({
273
+ provider: run.provider,
274
+ label: run.label,
275
+ availability: run.availability,
276
+ capabilities: run.capabilities,
277
+ overall: aggregateCaseResults(run.caseResults),
278
+ byScenario: aggregateByScenario(run.caseResults),
279
+ failureCounts: countFailures(run.caseResults),
280
+ }));
281
+ }
282
+
283
+ function aggregateByScenario(
284
+ caseResults: ProviderCaseResult[],
285
+ ): Partial<Record<BenchmarkScenarioType, AggregatedMetrics>> {
286
+ const grouped = new Map<BenchmarkScenarioType, ProviderCaseResult[]>();
287
+ for (const caseResult of caseResults) {
288
+ const bucket = grouped.get(caseResult.scenarioType) ?? [];
289
+ bucket.push(caseResult);
290
+ grouped.set(caseResult.scenarioType, bucket);
291
+ }
292
+
293
+ const summaries: Partial<Record<BenchmarkScenarioType, AggregatedMetrics>> = {};
294
+ for (const [scenarioType, results] of grouped) {
295
+ summaries[scenarioType] = aggregateCaseResults(results);
296
+ }
297
+ return summaries;
298
+ }
299
+
300
+ function aggregateCaseResults(caseResults: ProviderCaseResult[]): AggregatedMetrics {
301
+ const supported = caseResults.filter((caseResult) => caseResult.supported && !caseResult.error);
302
+ const unsupportedCaseCount = caseResults.filter((caseResult) => !caseResult.supported).length;
303
+ const errorCount = caseResults.filter((caseResult) => Boolean(caseResult.error)).length;
304
+ const latencies = supported.map((caseResult) => caseResult.latencyMs).sort((a, b) => a - b);
305
+ const ingests = supported.map((caseResult) => caseResult.ingestMs).sort((a, b) => a - b);
306
+ const storageSizes = supported
307
+ .map((caseResult) => caseResult.storageSizeBytes)
308
+ .filter((value): value is number => value !== null);
309
+
310
+ return {
311
+ caseCount: supported.length,
312
+ unsupportedCaseCount,
313
+ errorCount,
314
+ precisionAt1: average(supported.map((caseResult) => caseResult.evaluation.precisionAt1)),
315
+ precisionAt3: average(supported.map((caseResult) => caseResult.evaluation.precisionAt3)),
316
+ precisionAt5: average(supported.map((caseResult) => caseResult.evaluation.precisionAt5)),
317
+ recallAt3: average(supported.map((caseResult) => caseResult.evaluation.recallAt3)),
318
+ recallAt5: average(supported.map((caseResult) => caseResult.evaluation.recallAt5)),
319
+ mrr: average(supported.map((caseResult) => caseResult.evaluation.mrr)),
320
+ evidenceAccuracy: average(
321
+ supported.map((caseResult) => caseResult.evaluation.evidenceAccuracy),
322
+ ),
323
+ deterministicSuccess: average(
324
+ supported.map((caseResult) => caseResult.evaluation.deterministicSuccess),
325
+ ),
326
+ abstentionAccuracy: averageNullable(
327
+ supported.map((caseResult) => caseResult.evaluation.abstentionAccuracy),
328
+ ),
329
+ temporalCorrectness: averageNullable(
330
+ supported.map((caseResult) => caseResult.evaluation.temporalCorrectness),
331
+ ),
332
+ staleMemoryErrorRate: averageNullable(
333
+ supported.map((caseResult) => caseResult.evaluation.staleMemoryErrorRate),
334
+ ),
335
+ deletedMemoryLeakageRate: averageNullable(
336
+ supported.map((caseResult) => caseResult.evaluation.deletedMemoryLeakageRate),
337
+ ),
338
+ portabilitySuccessRate: averageNullable(
339
+ supported.map((caseResult) => caseResult.evaluation.portabilitySuccessRate),
340
+ ),
341
+ taskContextCoverage: averageNullable(
342
+ supported.map((caseResult) => caseResult.evaluation.taskContextCoverage),
343
+ ),
344
+ rankingMovement: averageNullable(
345
+ supported.map((caseResult) => caseResult.evaluation.rankingMovement),
346
+ ),
347
+ answerAccuracy: averageNullable(
348
+ supported.map((caseResult) => caseResult.evaluation.answerAccuracy),
349
+ ),
350
+ hallucinationRate: averageNullable(
351
+ supported.map((caseResult) => caseResult.evaluation.hallucinationRate),
352
+ ),
353
+ p50LatencyMs: percentile(latencies, 0.5),
354
+ p95LatencyMs: percentile(latencies, 0.95),
355
+ averageLatencyMs: average(latencies),
356
+ p50IngestMs: percentile(ingests, 0.5),
357
+ p95IngestMs: percentile(ingests, 0.95),
358
+ averageIngestMs: average(ingests),
359
+ averageStorageSizeBytes: storageSizes.length > 0 ? average(storageSizes) : null,
360
+ estimatedCostPer1kQueries: 0,
361
+ };
362
+ }
363
+
364
+ function countFailures(caseResults: ProviderCaseResult[]): Record<string, number> {
365
+ const counts = new Map<string, number>();
366
+ for (const caseResult of caseResults) {
367
+ for (const tag of caseResult.evaluation.failureTags) {
368
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
369
+ }
370
+ if (caseResult.error) {
371
+ counts.set('runtime_error', (counts.get('runtime_error') ?? 0) + 1);
372
+ }
373
+ if (!caseResult.supported && caseResult.unsupportedReason) {
374
+ counts.set('unsupported_case', (counts.get('unsupported_case') ?? 0) + 1);
375
+ }
376
+ }
377
+ return Object.fromEntries([...counts.entries()].sort((a, b) => b[1] - a[1]));
378
+ }
379
+
380
+ function scenarioDeterministicPass(
381
+ benchmarkCase: BenchmarkCase,
382
+ hitsAt5: number,
383
+ results: BenchmarkRecallResult[],
384
+ evaluation: CaseEvaluation,
385
+ requiredCount: number,
386
+ ): boolean {
387
+ if (benchmarkCase.expectations.shouldAbstain) {
388
+ return evaluation.abstentionAccuracy === 1 && evaluation.deletedMemoryLeakageRate !== 1;
389
+ }
390
+
391
+ if (benchmarkCase.scenarioType === 'portability') {
392
+ return hitsAt5 === requiredCount && evaluation.portabilitySuccessRate === 1;
393
+ }
394
+
395
+ if (benchmarkCase.scenarioType === 'agent_task_context') {
396
+ return evaluation.taskContextCoverage === 1 && evaluation.evidenceAccuracy === 1;
397
+ }
398
+
399
+ if (benchmarkCase.expectations.preferredOver?.length) {
400
+ return hitsAt5 >= 1 && evaluation.temporalCorrectness === 1 && evaluation.evidenceAccuracy > 0;
401
+ }
402
+
403
+ if (benchmarkCase.scenarioType === 'multi_hop_recall') {
404
+ return hitsAt5 === requiredCount && evaluation.evidenceAccuracy === 1;
405
+ }
406
+
407
+ return requiredCount === 0 ? results.length === 0 : hitsAt5 >= requiredCount && evaluation.evidenceAccuracy === 1;
408
+ }
409
+
410
+ function preferredWins(returnedIds: string[], preferredId: string, competingIds: string[]): boolean {
411
+ const preferredRank = rankOf(returnedIds, preferredId);
412
+ if (preferredRank === null) return false;
413
+
414
+ const competingRanks = competingIds
415
+ .map((memoryId) => rankOf(returnedIds, memoryId))
416
+ .filter((rank): rank is number => rank !== null);
417
+
418
+ if (competingRanks.length === 0) return true;
419
+ return preferredRank < Math.min(...competingRanks);
420
+ }
421
+
422
+ function compareProbeRanks(
423
+ beforeResults: BenchmarkRecallResult[],
424
+ afterResults: BenchmarkRecallResult[],
425
+ memoryId: string,
426
+ ): number | null {
427
+ const beforeRank = rankOf(
428
+ beforeResults.map((result) => result.memoryId),
429
+ memoryId,
430
+ );
431
+ const afterRank = rankOf(
432
+ afterResults.map((result) => result.memoryId),
433
+ memoryId,
434
+ );
435
+
436
+ if (beforeRank === null || afterRank === null) return null;
437
+ return beforeRank - afterRank;
438
+ }
439
+
440
+ function countHits(returnedIds: string[], expectedIds: string[]): number {
441
+ const expected = new Set(expectedIds);
442
+ return returnedIds.filter((id) => expected.has(id)).length;
443
+ }
444
+
445
+ function precisionAtK(returnedIds: string[], expectedIds: string[]): number {
446
+ if (returnedIds.length === 0) return 0;
447
+ const hits = countHits(returnedIds, expectedIds);
448
+ return hits / returnedIds.length;
449
+ }
450
+
451
+ function recallAtK(returnedIds: string[], expectedIds: string[]): number {
452
+ if (expectedIds.length === 0) return 1;
453
+ const hits = countHits(returnedIds, expectedIds);
454
+ return hits / expectedIds.length;
455
+ }
456
+
457
+ function mrrAtK(returnedIds: string[], expectedIds: string[], k: number): number {
458
+ const expected = new Set(expectedIds);
459
+ const rank = returnedIds.slice(0, k).findIndex((id) => expected.has(id));
460
+ return rank === -1 ? 0 : 1 / (rank + 1);
461
+ }
462
+
463
+ function rankOf(returnedIds: string[], memoryId: string): number | null {
464
+ const index = returnedIds.indexOf(memoryId);
465
+ return index === -1 ? null : index + 1;
466
+ }
467
+
468
+ function percentile(sorted: number[], fraction: number): number {
469
+ if (sorted.length === 0) return 0;
470
+ const index = Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1);
471
+ return sorted[index];
472
+ }
473
+
474
+ function average(values: number[]): number {
475
+ if (values.length === 0) return 0;
476
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
477
+ }
478
+
479
+ function averageNullable(values: Array<number | null>): number | null {
480
+ const present = values.filter((value): value is number => value !== null);
481
+ return present.length === 0 ? null : average(present);
482
+ }
@@ -0,0 +1,151 @@
1
+ export type BenchmarkMemoryType = 'episodic' | 'semantic' | 'procedural';
2
+
3
+ export type BenchmarkScenarioType =
4
+ | 'basic_semantic_recall'
5
+ | 'multi_hop_recall'
6
+ | 'memory_update'
7
+ | 'noise_resistance'
8
+ | 'selective_forgetting'
9
+ | 'decay_refresh'
10
+ | 'portability'
11
+ | 'agent_task_context';
12
+
13
+ export interface BenchmarkMemoryRecord {
14
+ id: string;
15
+ content: string;
16
+ type: BenchmarkMemoryType;
17
+ timestamp: string;
18
+ tags: string[];
19
+ importance?: number;
20
+ metadata?: Record<string, unknown>;
21
+ associations?: Array<{
22
+ targetId: string;
23
+ strength: number;
24
+ }>;
25
+ }
26
+
27
+ export interface BenchmarkRecallRequest {
28
+ query?: string;
29
+ limit?: number;
30
+ minScore?: number;
31
+ maxHops?: number;
32
+ blendWeight?: number;
33
+ activationThreshold?: number;
34
+ }
35
+
36
+ export type BenchmarkOperation =
37
+ | {
38
+ kind: 'recall_probe';
39
+ label: string;
40
+ query: string;
41
+ repeat?: number;
42
+ options?: BenchmarkRecallRequest;
43
+ }
44
+ | {
45
+ kind: 'forget';
46
+ memoryId: string;
47
+ }
48
+ | {
49
+ kind: 'decay';
50
+ cycles: number;
51
+ decayRate: number;
52
+ minScore: number;
53
+ }
54
+ | {
55
+ kind: 'export_import';
56
+ targetAgentId: string;
57
+ };
58
+
59
+ export interface BenchmarkExpectation {
60
+ requiredMemoryIds: string[];
61
+ forbiddenMemoryIds: string[];
62
+ shouldAbstain?: boolean;
63
+ preferredOver?: Array<{
64
+ preferredId: string;
65
+ competingIds: string[];
66
+ }>;
67
+ probeComparisons?: Array<{
68
+ labelBefore: string;
69
+ labelAfter: string;
70
+ memoryId: string;
71
+ }>;
72
+ preserveAfterImportIds?: string[];
73
+ }
74
+
75
+ export interface BenchmarkCase {
76
+ scenarioId: string;
77
+ scenarioType: BenchmarkScenarioType;
78
+ title: string;
79
+ description: string;
80
+ agentId: string;
81
+ memories: BenchmarkMemoryRecord[];
82
+ operations: BenchmarkOperation[];
83
+ question: string;
84
+ expectedAnswer: string;
85
+ recallOptions: BenchmarkRecallRequest;
86
+ expectations: BenchmarkExpectation;
87
+ }
88
+
89
+ export interface BenchmarkDataset {
90
+ name: string;
91
+ generatedAt: string;
92
+ cases: BenchmarkCase[];
93
+ }
94
+
95
+ export interface BenchmarkRecallResult {
96
+ memoryId: string;
97
+ content: string;
98
+ score: number;
99
+ type?: BenchmarkMemoryType;
100
+ source?: string;
101
+ rankingTrace?: string[];
102
+ metadata?: Record<string, unknown>;
103
+ }
104
+
105
+ export interface ProviderCapabilities {
106
+ associations: boolean;
107
+ forget: boolean;
108
+ decay: boolean;
109
+ portability: boolean;
110
+ }
111
+
112
+ export interface ProviderAvailability {
113
+ status: 'available' | 'unsupported';
114
+ reason?: string;
115
+ }
116
+
117
+ export interface ProviderStats {
118
+ storageSizeBytes: number | null;
119
+ }
120
+
121
+ export interface MemoryProviderAdapter {
122
+ readonly name: string;
123
+ readonly label: string;
124
+ readonly capabilities: ProviderCapabilities;
125
+ availability(): Promise<ProviderAvailability>;
126
+ reset(agentId: string): Promise<void>;
127
+ remember(memory: BenchmarkMemoryRecord, agentId: string): Promise<void>;
128
+ associate?(sourceId: string, targetId: string, strength: number, agentId: string): Promise<void>;
129
+ recall(
130
+ request: BenchmarkRecallRequest & {
131
+ agentId: string;
132
+ },
133
+ ): Promise<BenchmarkRecallResult[]>;
134
+ forget?(memoryId: string, agentId: string): Promise<void>;
135
+ applyDecay?(decayRate: number, minScore: number): Promise<number>;
136
+ exportMemory?(agentId: string): Promise<unknown>;
137
+ importMemory?(payload: unknown, agentId: string): Promise<void>;
138
+ getStats?(): Promise<ProviderStats>;
139
+ close(): Promise<void>;
140
+ }
141
+
142
+ export class BenchmarkSkipError extends Error {
143
+ constructor(message: string) {
144
+ super(message);
145
+ this.name = 'BenchmarkSkipError';
146
+ }
147
+ }
148
+
149
+ export function isBenchmarkSkipError(error: unknown): error is BenchmarkSkipError {
150
+ return error instanceof BenchmarkSkipError;
151
+ }