@remnic/bench 9.6.18 → 9.6.19
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.
- package/README.md +65 -0
- package/dist/demo/mcp-memory-server.d.ts +1 -0
- package/dist/demo/mcp-memory-server.js +129 -0
- package/dist/index.d.ts +414 -6
- package/dist/index.js +2983 -268
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -644,6 +644,140 @@ function benchmarkRecallBudgetForSessionCount(sessionCount) {
|
|
|
644
644
|
}
|
|
645
645
|
|
|
646
646
|
// src/adapters/remnic-adapter.ts
|
|
647
|
+
var DEFAULT_ANSWER_SUPPORT_MIN_COVERAGE = 0.34;
|
|
648
|
+
var ANSWER_SUPPORT_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
649
|
+
"about",
|
|
650
|
+
"after",
|
|
651
|
+
"again",
|
|
652
|
+
"also",
|
|
653
|
+
"answer",
|
|
654
|
+
"before",
|
|
655
|
+
"being",
|
|
656
|
+
"could",
|
|
657
|
+
"does",
|
|
658
|
+
"from",
|
|
659
|
+
"have",
|
|
660
|
+
"information",
|
|
661
|
+
"into",
|
|
662
|
+
"just",
|
|
663
|
+
"know",
|
|
664
|
+
"memory",
|
|
665
|
+
"might",
|
|
666
|
+
"please",
|
|
667
|
+
"question",
|
|
668
|
+
"recall",
|
|
669
|
+
"remember",
|
|
670
|
+
"should",
|
|
671
|
+
"that",
|
|
672
|
+
"their",
|
|
673
|
+
"there",
|
|
674
|
+
"these",
|
|
675
|
+
"they",
|
|
676
|
+
"this",
|
|
677
|
+
"those",
|
|
678
|
+
"user",
|
|
679
|
+
"using",
|
|
680
|
+
"what",
|
|
681
|
+
"when",
|
|
682
|
+
"where",
|
|
683
|
+
"which",
|
|
684
|
+
"while",
|
|
685
|
+
"with",
|
|
686
|
+
"would",
|
|
687
|
+
"your"
|
|
688
|
+
]);
|
|
689
|
+
function resolveAnswerSupportMinCoverage(config) {
|
|
690
|
+
const raw = config?.answerSupportMinCoverage;
|
|
691
|
+
if (raw === void 0) return DEFAULT_ANSWER_SUPPORT_MIN_COVERAGE;
|
|
692
|
+
const parsed = typeof raw === "number" ? raw : Number(raw);
|
|
693
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1) {
|
|
694
|
+
throw new Error("answerSupportMinCoverage must be a finite number greater than 0 and at most 1.");
|
|
695
|
+
}
|
|
696
|
+
return parsed;
|
|
697
|
+
}
|
|
698
|
+
function resolveSkipExtractionLcmFirst(config) {
|
|
699
|
+
const raw = config?.skipExtractionLcmFirst;
|
|
700
|
+
if (raw === void 0) return true;
|
|
701
|
+
if (typeof raw === "boolean") return raw;
|
|
702
|
+
if (typeof raw === "string") {
|
|
703
|
+
const normalized = raw.trim().toLowerCase();
|
|
704
|
+
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
|
705
|
+
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
|
706
|
+
}
|
|
707
|
+
throw new Error(
|
|
708
|
+
"skipExtractionLcmFirst must be a boolean or one of true/false, 1/0, yes/no, on/off."
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
function shouldIncludeCoreRecallForReplay(options) {
|
|
712
|
+
return options.useCoreMemoryPipeline && (options.replayExtractionMode !== "skip" || !options.skipExtractionLcmFirst);
|
|
713
|
+
}
|
|
714
|
+
function normalizeSupportToken(value) {
|
|
715
|
+
if (value.length > 5 && value.endsWith("ing")) return value.slice(0, -3);
|
|
716
|
+
if (value.length > 4 && value.endsWith("ed")) {
|
|
717
|
+
const base = value.slice(0, -2);
|
|
718
|
+
return /[vs]$/.test(base) ? `${base}e` : base;
|
|
719
|
+
}
|
|
720
|
+
if (value.length > 4 && value.endsWith("es")) return value.slice(0, -2);
|
|
721
|
+
if (value.length > 3 && value.endsWith("s")) return value.slice(0, -1);
|
|
722
|
+
return value;
|
|
723
|
+
}
|
|
724
|
+
function supportTerms(value) {
|
|
725
|
+
return [...new Set(
|
|
726
|
+
(value.toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}_-]{2,}/gu) ?? []).filter((term) => !ANSWER_SUPPORT_STOP_WORDS.has(term)).map(normalizeSupportToken).filter((term) => !ANSWER_SUPPORT_STOP_WORDS.has(term) && !/^\d+$/.test(term))
|
|
727
|
+
)];
|
|
728
|
+
}
|
|
729
|
+
function exactContextEvidenceLines(recalledText) {
|
|
730
|
+
return recalledText.split(/\r?\n/).map((line) => line.trim()).filter((line) => {
|
|
731
|
+
if (!line || /^#{1,6}\s/.test(line)) return false;
|
|
732
|
+
return !/^(?:answer guidance:|distinct user-stated targets found:|no (?:direct|historically valid)|these direct temporal statements|this is the most recent|use this list|when answering)/i.test(line);
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
function assessRemnicRecallSupport(request, supportThreshold = DEFAULT_ANSWER_SUPPORT_MIN_COVERAGE) {
|
|
736
|
+
if (request.recalledText.trim().length === 0) {
|
|
737
|
+
return { status: "empty", reason: "exact responder context is empty", evidenceCount: 0 };
|
|
738
|
+
}
|
|
739
|
+
const queryTerms = supportTerms(request.query);
|
|
740
|
+
if (queryTerms.length < 2) {
|
|
741
|
+
return {
|
|
742
|
+
status: "unavailable",
|
|
743
|
+
reason: "query has fewer than two distinctive terms for conservative support scoring"
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const evidenceLines = exactContextEvidenceLines(request.recalledText);
|
|
747
|
+
const evidenceTermSets = evidenceLines.map((line) => new Set(supportTerms(line)));
|
|
748
|
+
const matchedTerms = queryTerms.filter(
|
|
749
|
+
(term) => evidenceTermSets.some((terms) => terms.has(term))
|
|
750
|
+
);
|
|
751
|
+
const evidenceCount = evidenceTermSets.filter(
|
|
752
|
+
(terms) => matchedTerms.some((term) => terms.has(term))
|
|
753
|
+
).length;
|
|
754
|
+
const coverage = matchedTerms.length / queryTerms.length;
|
|
755
|
+
if (evidenceCount === 0) {
|
|
756
|
+
return {
|
|
757
|
+
status: "empty",
|
|
758
|
+
reason: "exact responder context contains no matching evidence terms",
|
|
759
|
+
evidenceCount: 0,
|
|
760
|
+
maxScore: 0,
|
|
761
|
+
supportThreshold
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
if (coverage < supportThreshold) {
|
|
765
|
+
return {
|
|
766
|
+
status: "weak",
|
|
767
|
+
reason: "exact responder context has only weak lexical support",
|
|
768
|
+
evidenceCount,
|
|
769
|
+
maxScore: coverage,
|
|
770
|
+
supportThreshold
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
status: "supported",
|
|
775
|
+
reason: "exact responder context has sufficient lexical support",
|
|
776
|
+
evidenceCount,
|
|
777
|
+
maxScore: coverage,
|
|
778
|
+
supportThreshold
|
|
779
|
+
};
|
|
780
|
+
}
|
|
647
781
|
var BENCH_ADAPTER_SHARED_CONFIG = {
|
|
648
782
|
qmdEnabled: false,
|
|
649
783
|
qmdColdTierEnabled: false,
|
|
@@ -1733,6 +1867,12 @@ function createAdapterFactory(mode) {
|
|
|
1733
1867
|
return async function createAdapter(options = {}) {
|
|
1734
1868
|
const useCoreMemoryPipeline = shouldUseCoreMemoryPipeline(mode, options);
|
|
1735
1869
|
const replayExtractionMode = options.replayExtractionMode ?? "await";
|
|
1870
|
+
const answerSupportMinCoverage = resolveAnswerSupportMinCoverage(
|
|
1871
|
+
options.configOverrides
|
|
1872
|
+
);
|
|
1873
|
+
const skipExtractionLcmFirst = resolveSkipExtractionLcmFirst(
|
|
1874
|
+
options.configOverrides
|
|
1875
|
+
);
|
|
1736
1876
|
const replaySourceValidAtMode = normalizeReplaySourceValidAtMode(
|
|
1737
1877
|
options.replaySourceValidAtMode
|
|
1738
1878
|
);
|
|
@@ -1999,6 +2139,11 @@ function createAdapterFactory(mode) {
|
|
|
1999
2139
|
}
|
|
2000
2140
|
const recallAsOf = normalizeBenchRecallAsOf(recallOptions.asOf);
|
|
2001
2141
|
const historicalRecall = recallAsOf !== void 0;
|
|
2142
|
+
const includeCoreRecall = shouldIncludeCoreRecallForReplay({
|
|
2143
|
+
useCoreMemoryPipeline,
|
|
2144
|
+
replayExtractionMode,
|
|
2145
|
+
skipExtractionLcmFirst
|
|
2146
|
+
});
|
|
2002
2147
|
if (historicalRecall && (!useCoreMemoryPipeline || replayExtractionMode === "skip" || replaySourceValidAtMode !== "historical")) {
|
|
2003
2148
|
throw new Error(
|
|
2004
2149
|
"benchmark historical recall requires core replay extraction with replaySourceValidAtMode=historical; enable the core memory pipeline and do not use replayExtractionMode=skip"
|
|
@@ -2097,7 +2242,7 @@ function createAdapterFactory(mode) {
|
|
|
2097
2242
|
sections.push(trajectoryAnalysisEvidence);
|
|
2098
2243
|
usedChars += trajectoryAnalysisEvidence.length;
|
|
2099
2244
|
}
|
|
2100
|
-
if (
|
|
2245
|
+
if (includeCoreRecall && !requireDirectPersonalHistoryEvidence && !requireDirectTemporalEvidence && !hasTemporalIntervalEvidence && !hasDependencyVersionEvidence && !hasUserImplementationTargetEvidence) {
|
|
2101
2246
|
const coreBudget = historicalRecall ? Math.max(0, budget - usedChars) : Math.max(
|
|
2102
2247
|
0,
|
|
2103
2248
|
Math.min(
|
|
@@ -2135,7 +2280,7 @@ ${coreRecall.trim()}`;
|
|
|
2135
2280
|
const suppressBroadSummary = historicalRecall || requireDirectPersonalHistoryEvidence || requireDirectTemporalEvidence || hasTemporalIntervalEvidence || hasDependencyVersionEvidence || hasUserImplementationTargetEvidence || preferFocusedExplicitContext && !!exactReferenceEvidence;
|
|
2136
2281
|
if (query && !historicalRecall && !hasTemporalIntervalEvidence && !hasDependencyVersionEvidence && !hasUserImplementationTargetEvidence) {
|
|
2137
2282
|
const remainingAfterCore = Math.max(0, budget - usedChars);
|
|
2138
|
-
const searchBudget =
|
|
2283
|
+
const searchBudget = includeCoreRecall ? Math.max(0, Math.floor(remainingAfterCore * 0.75)) : Math.max(0, Math.floor(remainingAfterCore * 0.7));
|
|
2139
2284
|
const searchLimit = Math.max(6, Math.min(18, Math.floor(budget / 2e3)));
|
|
2140
2285
|
const searchResults = await waitForRecall(
|
|
2141
2286
|
engine.searchContextFull(
|
|
@@ -2151,7 +2296,7 @@ ${coreRecall.trim()}`;
|
|
|
2151
2296
|
const directTemporalTurnIds = /* @__PURE__ */ new Set();
|
|
2152
2297
|
for (const result of searchResults) {
|
|
2153
2298
|
throwIfBenchPhaseAborted(control, "recall");
|
|
2154
|
-
const windowRadius = preferFocusedExplicitContext ? 2 :
|
|
2299
|
+
const windowRadius = preferFocusedExplicitContext ? 2 : includeCoreRecall ? 3 : 1;
|
|
2155
2300
|
const fromTurn = Math.max(0, result.turn_index - windowRadius);
|
|
2156
2301
|
const toTurn = result.turn_index + windowRadius;
|
|
2157
2302
|
const expanded = await waitForRecall(
|
|
@@ -2159,7 +2304,7 @@ ${coreRecall.trim()}`;
|
|
|
2159
2304
|
result.session_id,
|
|
2160
2305
|
fromTurn,
|
|
2161
2306
|
toTurn,
|
|
2162
|
-
|
|
2307
|
+
includeCoreRecall ? 1600 : 600
|
|
2163
2308
|
)
|
|
2164
2309
|
);
|
|
2165
2310
|
if (expanded.length === 0) {
|
|
@@ -2320,6 +2465,10 @@ ${expanded.map((message) => `[${message.role}]: ${message.content}`).join("\n")}
|
|
|
2320
2465
|
const joined = sections.join("\n\n");
|
|
2321
2466
|
return joined.length > budget ? joined.slice(0, budget) : joined;
|
|
2322
2467
|
},
|
|
2468
|
+
async assessRecallSupport(request, control) {
|
|
2469
|
+
throwIfBenchPhaseAborted(control, "assessRecallSupport");
|
|
2470
|
+
return assessRemnicRecallSupport(request, answerSupportMinCoverage);
|
|
2471
|
+
},
|
|
2323
2472
|
async search(query, limit, sessionId, control) {
|
|
2324
2473
|
throwIfBenchPhaseAborted(control, "search");
|
|
2325
2474
|
const normalizedSessionId = normalizeOptionalBenchSessionId(sessionId);
|
|
@@ -3292,6 +3441,747 @@ ${message.content}`).digest("hex").slice(0, 16);
|
|
|
3292
3441
|
return `bench-${index}-${digest}`;
|
|
3293
3442
|
}
|
|
3294
3443
|
|
|
3444
|
+
// src/adapters/mcp-memory-adapter.ts
|
|
3445
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3446
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3447
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3448
|
+
import { fileURLToPath } from "url";
|
|
3449
|
+
import { existsSync } from "fs";
|
|
3450
|
+
var McpMemoryBackendError = class extends Error {
|
|
3451
|
+
code;
|
|
3452
|
+
detail;
|
|
3453
|
+
constructor(result) {
|
|
3454
|
+
super(`${result.error}: ${result.detail}`, { cause: result.cause });
|
|
3455
|
+
this.name = "McpMemoryBackendError";
|
|
3456
|
+
this.code = result.error;
|
|
3457
|
+
this.detail = result.detail;
|
|
3458
|
+
}
|
|
3459
|
+
};
|
|
3460
|
+
var DEFAULT_TOOL_NAMES = {
|
|
3461
|
+
store: ["store_memory", "add_memory", "memory_store", "create_memory"],
|
|
3462
|
+
recall: ["search_memory", "recall", "memory_search", "search_memories"],
|
|
3463
|
+
correct: ["correct_memory", "update_memory", "memory_correct", "update_memories"],
|
|
3464
|
+
reset: ["delete_memory", "clear_memories", "memory_delete", "reset_memory"]
|
|
3465
|
+
};
|
|
3466
|
+
var ARGUMENT_ALIASES = {
|
|
3467
|
+
namespace: ["namespace", "scope", "user_id", "userId", "run_id"],
|
|
3468
|
+
sessionId: ["sessionId", "session_id", "session", "conversation_id"],
|
|
3469
|
+
content: ["content", "text", "memory", "message", "correction"],
|
|
3470
|
+
role: ["role", "speaker"],
|
|
3471
|
+
timestamp: ["timestamp", "at", "created_at"],
|
|
3472
|
+
query: ["query", "search", "q", "text"],
|
|
3473
|
+
limit: ["limit", "top_k", "count", "max_results"]
|
|
3474
|
+
};
|
|
3475
|
+
var TOOL_OPERATIONS = Object.freeze([
|
|
3476
|
+
"store",
|
|
3477
|
+
"recall",
|
|
3478
|
+
"correct",
|
|
3479
|
+
"reset"
|
|
3480
|
+
]);
|
|
3481
|
+
var ARGUMENT_SEMANTICS = Object.freeze([
|
|
3482
|
+
"namespace",
|
|
3483
|
+
"sessionId",
|
|
3484
|
+
"content",
|
|
3485
|
+
"role",
|
|
3486
|
+
"timestamp",
|
|
3487
|
+
"query",
|
|
3488
|
+
"limit"
|
|
3489
|
+
]);
|
|
3490
|
+
var DEFAULT_PREFLIGHT_TIMEOUT_MS = 3e4;
|
|
3491
|
+
var namespaceCounter = 0;
|
|
3492
|
+
function createNamespacePrefix() {
|
|
3493
|
+
namespaceCounter += 1;
|
|
3494
|
+
return `remnic-bench-${process.pid.toString(36)}-${Date.now().toString(36)}-${namespaceCounter.toString(36)}`;
|
|
3495
|
+
}
|
|
3496
|
+
var SdkMcpToolClient = class _SdkMcpToolClient {
|
|
3497
|
+
constructor(client) {
|
|
3498
|
+
this.client = client;
|
|
3499
|
+
}
|
|
3500
|
+
client;
|
|
3501
|
+
static async connect(config, control) {
|
|
3502
|
+
const client = new Client({ name: "remnic-bench", version: "1.0.0" });
|
|
3503
|
+
const transport = config.type === "stdio" ? new StdioClientTransport({
|
|
3504
|
+
command: config.command,
|
|
3505
|
+
args: config.args,
|
|
3506
|
+
cwd: config.cwd,
|
|
3507
|
+
env: config.env,
|
|
3508
|
+
stderr: "inherit"
|
|
3509
|
+
}) : new StreamableHTTPClientTransport(new URL(config.url), {
|
|
3510
|
+
requestInit: {
|
|
3511
|
+
headers: {
|
|
3512
|
+
...config.bearerToken ? { authorization: `Bearer ${config.bearerToken}` } : {},
|
|
3513
|
+
...config.headers ?? {}
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
});
|
|
3517
|
+
try {
|
|
3518
|
+
await client.connect(transport, { signal: control?.signal });
|
|
3519
|
+
return new _SdkMcpToolClient(client);
|
|
3520
|
+
} catch (error) {
|
|
3521
|
+
await raceWithSignal(client.close(), AbortSignal.timeout(2e3), "MCP SDK client close").catch(() => {
|
|
3522
|
+
});
|
|
3523
|
+
throw error;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
async listTools(control) {
|
|
3527
|
+
const result = await this.client.listTools(void 0, { signal: control?.signal });
|
|
3528
|
+
return result.tools;
|
|
3529
|
+
}
|
|
3530
|
+
async callTool(name, args, control) {
|
|
3531
|
+
const result = await this.client.callTool({ name, arguments: args }, void 0, { signal: control?.signal });
|
|
3532
|
+
if ("toolResult" in result) {
|
|
3533
|
+
return { structuredContent: { result: result.toolResult } };
|
|
3534
|
+
}
|
|
3535
|
+
return result;
|
|
3536
|
+
}
|
|
3537
|
+
async close() {
|
|
3538
|
+
await this.client.close();
|
|
3539
|
+
}
|
|
3540
|
+
};
|
|
3541
|
+
var McpMemoryBackend = class {
|
|
3542
|
+
constructor(options) {
|
|
3543
|
+
this.options = options;
|
|
3544
|
+
validateMcpToolMapping(options.tools);
|
|
3545
|
+
if (options.timeoutMs !== void 0 && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0)) {
|
|
3546
|
+
throw new Error("MCP adapter timeoutMs must be a positive integer");
|
|
3547
|
+
}
|
|
3548
|
+
this.label = options.label ?? "mcp";
|
|
3549
|
+
this.namespacePrefix = options.namespacePrefix ?? createNamespacePrefix();
|
|
3550
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_PREFLIGHT_TIMEOUT_MS;
|
|
3551
|
+
}
|
|
3552
|
+
options;
|
|
3553
|
+
label;
|
|
3554
|
+
namespacePrefix;
|
|
3555
|
+
client;
|
|
3556
|
+
resolvedTools;
|
|
3557
|
+
preflightResult;
|
|
3558
|
+
sessions = /* @__PURE__ */ new Set();
|
|
3559
|
+
messageCounts = /* @__PURE__ */ new Map();
|
|
3560
|
+
timeoutMs;
|
|
3561
|
+
async preflight(control) {
|
|
3562
|
+
if (this.preflightResult) return this.preflightResult;
|
|
3563
|
+
try {
|
|
3564
|
+
this.preflightResult = await withTimeoutControl(
|
|
3565
|
+
this.timeoutMs,
|
|
3566
|
+
control,
|
|
3567
|
+
"MCP adapter preflight",
|
|
3568
|
+
async (boundedControl) => {
|
|
3569
|
+
await this.ensureResolved(boundedControl);
|
|
3570
|
+
const sessionId = "conformance";
|
|
3571
|
+
const oldToken = `canary-old-${this.namespacePrefix}`;
|
|
3572
|
+
const newToken = `canary-new-${this.namespacePrefix}`;
|
|
3573
|
+
await this.store(sessionId, [{ role: "user", content: oldToken }], boundedControl, true);
|
|
3574
|
+
const before = await this.recall(sessionId, oldToken, 4e3, boundedControl, true);
|
|
3575
|
+
if (!before.some((item) => item.includes(oldToken))) {
|
|
3576
|
+
throw new Error("stored canary was not returned by recall");
|
|
3577
|
+
}
|
|
3578
|
+
const applied = await this.correct(
|
|
3579
|
+
sessionId,
|
|
3580
|
+
`Correction: replace ${oldToken} with ${newToken}.`,
|
|
3581
|
+
void 0,
|
|
3582
|
+
boundedControl,
|
|
3583
|
+
true
|
|
3584
|
+
);
|
|
3585
|
+
if (!applied) {
|
|
3586
|
+
throw new Error("correction canary was explicitly refused (applied=false)");
|
|
3587
|
+
}
|
|
3588
|
+
const after = await this.recall(sessionId, newToken, 4e3, boundedControl, true);
|
|
3589
|
+
if (!after.some((item) => item.includes(newToken))) {
|
|
3590
|
+
throw new Error("corrected canary was not returned by recall");
|
|
3591
|
+
}
|
|
3592
|
+
if (after.some((item) => item.includes(oldToken))) {
|
|
3593
|
+
throw new Error("retired canary was still returned after correction");
|
|
3594
|
+
}
|
|
3595
|
+
await this.reset(sessionId, boundedControl, true);
|
|
3596
|
+
const deleted = await this.recall(sessionId, newToken, 4e3, boundedControl, true);
|
|
3597
|
+
if (deleted.some((item) => item.includes(newToken) || item.includes(oldToken))) {
|
|
3598
|
+
throw new Error("reset did not remove the conformance canary namespace");
|
|
3599
|
+
}
|
|
3600
|
+
return {
|
|
3601
|
+
ok: true,
|
|
3602
|
+
value: {
|
|
3603
|
+
tools: Object.fromEntries(
|
|
3604
|
+
Object.entries(this.resolvedTools).map(([key, value]) => [key, value.name])
|
|
3605
|
+
),
|
|
3606
|
+
namespace: this.namespacePrefix
|
|
3607
|
+
}
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
);
|
|
3611
|
+
} catch (cause) {
|
|
3612
|
+
const conformanceSession = this.scopedSession("conformance");
|
|
3613
|
+
if (this.client && this.resolvedTools && this.sessions.has(conformanceSession)) {
|
|
3614
|
+
try {
|
|
3615
|
+
await withTimeoutControl(
|
|
3616
|
+
Math.min(this.timeoutMs, 2e3),
|
|
3617
|
+
void 0,
|
|
3618
|
+
"MCP conformance cleanup",
|
|
3619
|
+
(cleanupControl) => this.reset("conformance", cleanupControl, true)
|
|
3620
|
+
);
|
|
3621
|
+
} catch {
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
await this.closeClient();
|
|
3625
|
+
this.preflightResult = {
|
|
3626
|
+
ok: false,
|
|
3627
|
+
error: "backend_unusable",
|
|
3628
|
+
detail: cause instanceof Error ? cause.message : String(cause),
|
|
3629
|
+
cause
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
return this.preflightResult;
|
|
3633
|
+
}
|
|
3634
|
+
async assertUsable(control) {
|
|
3635
|
+
if (this.options.skipPreflight) {
|
|
3636
|
+
await this.ensureResolved(control);
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
const result = await this.preflight(control);
|
|
3640
|
+
if (!result.ok) throw new McpMemoryBackendError(result);
|
|
3641
|
+
}
|
|
3642
|
+
async store(sessionId, messages, control, duringPreflight = false) {
|
|
3643
|
+
if (!duringPreflight) await this.assertUsable(control);
|
|
3644
|
+
const scoped = this.scopedSession(sessionId);
|
|
3645
|
+
this.sessions.add(scoped);
|
|
3646
|
+
for (const message of messages) {
|
|
3647
|
+
const payload = await this.invoke(
|
|
3648
|
+
"store",
|
|
3649
|
+
{
|
|
3650
|
+
namespace: this.namespacePrefix,
|
|
3651
|
+
sessionId: scoped,
|
|
3652
|
+
content: message.content,
|
|
3653
|
+
role: message.role,
|
|
3654
|
+
timestamp: message.timestamp
|
|
3655
|
+
},
|
|
3656
|
+
control
|
|
3657
|
+
);
|
|
3658
|
+
if (!validateMutationAcknowledgement("store", payload)) {
|
|
3659
|
+
throw invalidResponse("store", "server returned a negative acknowledgement");
|
|
3660
|
+
}
|
|
3661
|
+
this.messageCounts.set(scoped, (this.messageCounts.get(scoped) ?? 0) + 1);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
async recall(sessionId, query, budgetChars = 16e3, control, duringPreflight = false) {
|
|
3665
|
+
if (!duringPreflight) await this.assertUsable(control);
|
|
3666
|
+
const raw = await this.invoke(
|
|
3667
|
+
"recall",
|
|
3668
|
+
{
|
|
3669
|
+
namespace: this.namespacePrefix,
|
|
3670
|
+
sessionId: this.scopedSession(sessionId),
|
|
3671
|
+
query,
|
|
3672
|
+
limit: 20
|
|
3673
|
+
},
|
|
3674
|
+
control
|
|
3675
|
+
);
|
|
3676
|
+
const strings = validateRecallResponse(raw);
|
|
3677
|
+
const output = [];
|
|
3678
|
+
let used = 0;
|
|
3679
|
+
for (const value of strings) {
|
|
3680
|
+
if (used >= budgetChars) break;
|
|
3681
|
+
const remaining = budgetChars - used;
|
|
3682
|
+
output.push(value.slice(0, remaining));
|
|
3683
|
+
used += Math.min(value.length, remaining);
|
|
3684
|
+
}
|
|
3685
|
+
return output;
|
|
3686
|
+
}
|
|
3687
|
+
async correct(sessionId, text, at, control, duringPreflight = false) {
|
|
3688
|
+
if (!duringPreflight) await this.assertUsable(control);
|
|
3689
|
+
const scoped = this.scopedSession(sessionId);
|
|
3690
|
+
this.sessions.add(scoped);
|
|
3691
|
+
const payload = await this.invoke(
|
|
3692
|
+
"correct",
|
|
3693
|
+
{
|
|
3694
|
+
namespace: this.namespacePrefix,
|
|
3695
|
+
sessionId: scoped,
|
|
3696
|
+
content: text,
|
|
3697
|
+
timestamp: at
|
|
3698
|
+
},
|
|
3699
|
+
control
|
|
3700
|
+
);
|
|
3701
|
+
return validateMutationAcknowledgement("correct", payload);
|
|
3702
|
+
}
|
|
3703
|
+
async reset(sessionId, control, duringPreflight = false) {
|
|
3704
|
+
if (!duringPreflight) await this.assertUsable(control);
|
|
3705
|
+
const targets = sessionId ? [this.scopedSession(sessionId)] : [...this.sessions];
|
|
3706
|
+
for (const target of targets) {
|
|
3707
|
+
const payload = await this.invoke(
|
|
3708
|
+
"reset",
|
|
3709
|
+
{
|
|
3710
|
+
namespace: this.namespacePrefix,
|
|
3711
|
+
sessionId: target
|
|
3712
|
+
},
|
|
3713
|
+
control
|
|
3714
|
+
);
|
|
3715
|
+
if (!validateMutationAcknowledgement("reset", payload)) {
|
|
3716
|
+
throw invalidResponse("reset", "server returned a negative acknowledgement");
|
|
3717
|
+
}
|
|
3718
|
+
this.sessions.delete(target);
|
|
3719
|
+
this.messageCounts.delete(target);
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
getStats(sessionId) {
|
|
3723
|
+
const targets = sessionId ? [this.scopedSession(sessionId)] : [...this.messageCounts.keys()];
|
|
3724
|
+
return {
|
|
3725
|
+
totalMessages: targets.reduce((sum, target) => sum + (this.messageCounts.get(target) ?? 0), 0),
|
|
3726
|
+
totalSummaryNodes: 0,
|
|
3727
|
+
maxDepth: 0
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
async destroy() {
|
|
3731
|
+
try {
|
|
3732
|
+
if (this.client && this.resolvedTools) {
|
|
3733
|
+
await withTimeoutControl(
|
|
3734
|
+
Math.min(this.timeoutMs, 2e3),
|
|
3735
|
+
void 0,
|
|
3736
|
+
"MCP adapter cleanup",
|
|
3737
|
+
(cleanupControl) => this.reset(void 0, cleanupControl, true)
|
|
3738
|
+
);
|
|
3739
|
+
}
|
|
3740
|
+
} finally {
|
|
3741
|
+
await this.closeClient();
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
scopedSession(sessionId) {
|
|
3745
|
+
return `${this.namespacePrefix}:${sessionId}`;
|
|
3746
|
+
}
|
|
3747
|
+
async ensureResolved(control) {
|
|
3748
|
+
if (this.resolvedTools) return;
|
|
3749
|
+
let candidate;
|
|
3750
|
+
try {
|
|
3751
|
+
const pendingClient = (this.options.clientFactory ?? SdkMcpToolClient.connect)(this.options.transport, control);
|
|
3752
|
+
candidate = await raceWithSignal(pendingClient, control?.signal, "MCP client connection", async (lateClient) => {
|
|
3753
|
+
await closeToolClient(lateClient, this.timeoutMs);
|
|
3754
|
+
});
|
|
3755
|
+
const listed = await raceWithSignal(candidate.listTools(control), control?.signal, "MCP tool discovery");
|
|
3756
|
+
const resolved = resolveTools(listed, this.options.tools);
|
|
3757
|
+
this.client = candidate;
|
|
3758
|
+
this.resolvedTools = resolved;
|
|
3759
|
+
} catch (cause) {
|
|
3760
|
+
if (candidate) await closeToolClient(candidate, this.timeoutMs);
|
|
3761
|
+
this.client = void 0;
|
|
3762
|
+
this.resolvedTools = void 0;
|
|
3763
|
+
throw new McpMemoryBackendError({
|
|
3764
|
+
ok: false,
|
|
3765
|
+
error: "transport_failure",
|
|
3766
|
+
detail: cause instanceof Error ? cause.message : String(cause),
|
|
3767
|
+
cause
|
|
3768
|
+
});
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
async invoke(operation, values, control) {
|
|
3772
|
+
await this.ensureResolved(control);
|
|
3773
|
+
const tool = this.resolvedTools[operation];
|
|
3774
|
+
const args = buildArguments(tool, values);
|
|
3775
|
+
try {
|
|
3776
|
+
const result = await this.client.callTool(tool.name, args, control);
|
|
3777
|
+
if (result.isError) {
|
|
3778
|
+
throw new Error(
|
|
3779
|
+
extractStrings(readToolPayload(result, tool.resultPath)).join("; ") || "MCP tool returned isError=true"
|
|
3780
|
+
);
|
|
3781
|
+
}
|
|
3782
|
+
return readToolPayload(result, tool.resultPath);
|
|
3783
|
+
} catch (cause) {
|
|
3784
|
+
if (cause instanceof McpMemoryBackendError) throw cause;
|
|
3785
|
+
throw new McpMemoryBackendError({
|
|
3786
|
+
ok: false,
|
|
3787
|
+
error: "tool_failure",
|
|
3788
|
+
detail: `${operation} tool ${tool.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
3789
|
+
cause
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
async closeClient() {
|
|
3794
|
+
const client = this.client;
|
|
3795
|
+
this.client = void 0;
|
|
3796
|
+
this.resolvedTools = void 0;
|
|
3797
|
+
if (client) await closeToolClient(client, this.timeoutMs);
|
|
3798
|
+
}
|
|
3799
|
+
};
|
|
3800
|
+
async function createMcpMemoryAdapter(options) {
|
|
3801
|
+
const backend = new McpMemoryBackend(options);
|
|
3802
|
+
const adapter = {
|
|
3803
|
+
label: backend.label,
|
|
3804
|
+
namespacePrefix: backend.namespacePrefix,
|
|
3805
|
+
preflight: (control) => backend.preflight(control),
|
|
3806
|
+
store: (sessionId, messages, control) => backend.store(sessionId, messages, control),
|
|
3807
|
+
async recall(sessionId, query, budgetChars, _options, control) {
|
|
3808
|
+
return (await backend.recall(sessionId, query, budgetChars, control)).join("\n");
|
|
3809
|
+
},
|
|
3810
|
+
async search(query, limit, sessionId, control) {
|
|
3811
|
+
const recalled = await backend.recall(sessionId ?? "global", query, 64e3, control);
|
|
3812
|
+
return recalled.slice(0, limit).map((snippet, turnIndex) => ({
|
|
3813
|
+
turnIndex,
|
|
3814
|
+
role: "memory",
|
|
3815
|
+
snippet,
|
|
3816
|
+
sessionId: sessionId ?? "global"
|
|
3817
|
+
}));
|
|
3818
|
+
},
|
|
3819
|
+
async correct(sessionId, text, at, control) {
|
|
3820
|
+
return { applied: await backend.correct(sessionId, text, at, control) };
|
|
3821
|
+
},
|
|
3822
|
+
reset: (sessionId, control) => backend.reset(sessionId, control),
|
|
3823
|
+
getStats: async (sessionId) => backend.getStats(sessionId),
|
|
3824
|
+
destroy: () => backend.destroy()
|
|
3825
|
+
};
|
|
3826
|
+
try {
|
|
3827
|
+
if (!options.skipPreflight) {
|
|
3828
|
+
const preflight = await adapter.preflight();
|
|
3829
|
+
if (!preflight.ok) throw new McpMemoryBackendError(preflight);
|
|
3830
|
+
}
|
|
3831
|
+
return adapter;
|
|
3832
|
+
} catch (error) {
|
|
3833
|
+
await backend.destroy();
|
|
3834
|
+
throw error;
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
function createMcpDemoMemoryAdapter(options = {}) {
|
|
3838
|
+
return createMcpMemoryAdapter({
|
|
3839
|
+
...options,
|
|
3840
|
+
transport: resolveDemoTransport()
|
|
3841
|
+
});
|
|
3842
|
+
}
|
|
3843
|
+
async function createMcpMemCorrectAdapter(options) {
|
|
3844
|
+
const backend = new McpMemoryBackend(options);
|
|
3845
|
+
const adapter = {
|
|
3846
|
+
label: backend.label,
|
|
3847
|
+
namespacePrefix: backend.namespacePrefix,
|
|
3848
|
+
preflight: () => backend.preflight(),
|
|
3849
|
+
reset: () => backend.reset(),
|
|
3850
|
+
ingestTurn: (sessionKey, role, text, at) => backend.store(sessionKey, [{ role, content: text, timestamp: at }]),
|
|
3851
|
+
recall: (query, sessionKey) => backend.recall(sessionKey, query),
|
|
3852
|
+
correct: async (text, sessionKey, at) => {
|
|
3853
|
+
const applied = await backend.correct(sessionKey, text, at);
|
|
3854
|
+
if (!applied) {
|
|
3855
|
+
throw new McpMemoryBackendError({
|
|
3856
|
+
ok: false,
|
|
3857
|
+
error: "tool_failure",
|
|
3858
|
+
detail: "correct tool refused the correction (applied=false)"
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3861
|
+
},
|
|
3862
|
+
runMaintenance: async () => {
|
|
3863
|
+
},
|
|
3864
|
+
destroy: () => backend.destroy()
|
|
3865
|
+
};
|
|
3866
|
+
try {
|
|
3867
|
+
if (!options.skipPreflight) {
|
|
3868
|
+
const preflight = await adapter.preflight();
|
|
3869
|
+
if (!preflight.ok) throw new McpMemoryBackendError(preflight);
|
|
3870
|
+
}
|
|
3871
|
+
return adapter;
|
|
3872
|
+
} catch (error) {
|
|
3873
|
+
await backend.destroy();
|
|
3874
|
+
throw error;
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
function createMcpDemoMemCorrectAdapter(options = {}) {
|
|
3878
|
+
return createMcpMemCorrectAdapter({
|
|
3879
|
+
...options,
|
|
3880
|
+
transport: resolveDemoTransport()
|
|
3881
|
+
});
|
|
3882
|
+
}
|
|
3883
|
+
function resolveDemoTransport() {
|
|
3884
|
+
const packaged = fileURLToPath(new URL("./demo/mcp-memory-server.js", import.meta.url));
|
|
3885
|
+
const development = fileURLToPath(new URL("../../dist/demo/mcp-memory-server.js", import.meta.url));
|
|
3886
|
+
if (existsSync(packaged) || existsSync(development)) {
|
|
3887
|
+
return {
|
|
3888
|
+
type: "stdio",
|
|
3889
|
+
command: process.execPath,
|
|
3890
|
+
args: [existsSync(packaged) ? packaged : development]
|
|
3891
|
+
};
|
|
3892
|
+
}
|
|
3893
|
+
const source = fileURLToPath(new URL("../demo/mcp-memory-server.ts", import.meta.url));
|
|
3894
|
+
if (!existsSync(source)) {
|
|
3895
|
+
throw new Error("Packaged MCP demo server is missing from @remnic/bench");
|
|
3896
|
+
}
|
|
3897
|
+
return {
|
|
3898
|
+
type: "stdio",
|
|
3899
|
+
command: process.execPath,
|
|
3900
|
+
args: ["--import", "tsx", source]
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
function validateMcpToolMapping(value) {
|
|
3904
|
+
if (value === void 0) return;
|
|
3905
|
+
if (!isPlainRecord(value)) {
|
|
3906
|
+
throw new Error("MCP tool mapping must be a plain object");
|
|
3907
|
+
}
|
|
3908
|
+
for (const operation of Object.keys(value).sort()) {
|
|
3909
|
+
if (!TOOL_OPERATIONS.includes(operation)) {
|
|
3910
|
+
throw new Error(`MCP tool mapping contains unknown operation: ${operation}`);
|
|
3911
|
+
}
|
|
3912
|
+
const entry = value[operation];
|
|
3913
|
+
if (typeof entry === "string") {
|
|
3914
|
+
if (entry.trim().length === 0) {
|
|
3915
|
+
throw new Error(`MCP ${operation} tool name must be a non-empty string`);
|
|
3916
|
+
}
|
|
3917
|
+
continue;
|
|
3918
|
+
}
|
|
3919
|
+
if (!isPlainRecord(entry)) {
|
|
3920
|
+
throw new Error(`MCP ${operation} tool mapping must be a string or object`);
|
|
3921
|
+
}
|
|
3922
|
+
for (const key of Object.keys(entry).sort()) {
|
|
3923
|
+
if (key !== "name" && key !== "arguments" && key !== "resultPath") {
|
|
3924
|
+
throw new Error(`MCP ${operation} tool mapping contains unknown field: ${key}`);
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
if (typeof entry.name !== "string" || entry.name.trim().length === 0) {
|
|
3928
|
+
throw new Error(`MCP ${operation} tool mapping requires a non-empty name`);
|
|
3929
|
+
}
|
|
3930
|
+
if (entry.resultPath !== void 0) {
|
|
3931
|
+
if (typeof entry.resultPath !== "string" || !isSafeResultPath(entry.resultPath)) {
|
|
3932
|
+
throw new Error(`MCP ${operation} resultPath must be a non-empty safe dot path`);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
if (entry.arguments !== void 0) {
|
|
3936
|
+
if (!isPlainRecord(entry.arguments)) {
|
|
3937
|
+
throw new Error(`MCP ${operation} arguments mapping must be a plain object`);
|
|
3938
|
+
}
|
|
3939
|
+
for (const semantic of Object.keys(entry.arguments).sort()) {
|
|
3940
|
+
if (!ARGUMENT_SEMANTICS.includes(semantic)) {
|
|
3941
|
+
throw new Error(`MCP ${operation} arguments contain unknown semantic: ${semantic}`);
|
|
3942
|
+
}
|
|
3943
|
+
const argumentName = entry.arguments[semantic];
|
|
3944
|
+
if (typeof argumentName !== "string" || argumentName.trim().length === 0) {
|
|
3945
|
+
throw new Error(`MCP ${operation} argument ${semantic} must map to a non-empty string`);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
function resolveTools(listed, mapping = {}) {
|
|
3952
|
+
validateMcpToolMapping(mapping);
|
|
3953
|
+
const byName = new Map(listed.map((tool) => [tool.name, tool]));
|
|
3954
|
+
const resolved = {};
|
|
3955
|
+
for (const operation of Object.keys(DEFAULT_TOOL_NAMES)) {
|
|
3956
|
+
const explicit = mapping[operation];
|
|
3957
|
+
const entry = typeof explicit === "string" ? { name: explicit } : explicit;
|
|
3958
|
+
const name = entry?.name ?? DEFAULT_TOOL_NAMES[operation].find((candidate) => byName.has(candidate));
|
|
3959
|
+
if (!name || !byName.has(name)) {
|
|
3960
|
+
const expected = entry?.name ?? DEFAULT_TOOL_NAMES[operation].join(", ");
|
|
3961
|
+
throw new Error(
|
|
3962
|
+
`missing ${operation} tool (expected ${expected}); server exposed ${[...byName.keys()].sort().join(", ") || "no tools"}`
|
|
3963
|
+
);
|
|
3964
|
+
}
|
|
3965
|
+
const tool = byName.get(name);
|
|
3966
|
+
resolved[operation] = {
|
|
3967
|
+
name,
|
|
3968
|
+
arguments: entry?.arguments ?? {},
|
|
3969
|
+
resultPath: entry?.resultPath,
|
|
3970
|
+
schemaProperties: Object.keys(tool.inputSchema?.properties ?? {})
|
|
3971
|
+
};
|
|
3972
|
+
if (!resolveArgumentName(resolved[operation], "namespace") && !resolveArgumentName(resolved[operation], "sessionId")) {
|
|
3973
|
+
throw new Error(
|
|
3974
|
+
`unsafe ${operation} tool ${name}: operation requires a schema-declared namespace or sessionId argument mapping`
|
|
3975
|
+
);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
return resolved;
|
|
3979
|
+
}
|
|
3980
|
+
function buildArguments(tool, values) {
|
|
3981
|
+
const args = {};
|
|
3982
|
+
for (const [semantic, value] of Object.entries(values)) {
|
|
3983
|
+
if (value === void 0) continue;
|
|
3984
|
+
const key = resolveArgumentName(tool, semantic);
|
|
3985
|
+
if (key) args[key] = value;
|
|
3986
|
+
}
|
|
3987
|
+
return args;
|
|
3988
|
+
}
|
|
3989
|
+
function resolveArgumentName(tool, semantic) {
|
|
3990
|
+
const explicit = tool.arguments[semantic];
|
|
3991
|
+
if (explicit !== void 0) {
|
|
3992
|
+
return tool.schemaProperties.includes(explicit) ? explicit : void 0;
|
|
3993
|
+
}
|
|
3994
|
+
return ARGUMENT_ALIASES[semantic].find((alias) => tool.schemaProperties.includes(alias));
|
|
3995
|
+
}
|
|
3996
|
+
function readToolPayload(result, resultPath) {
|
|
3997
|
+
let value = result.structuredContent;
|
|
3998
|
+
if (value === void 0) {
|
|
3999
|
+
if (result.content === void 0) return void 0;
|
|
4000
|
+
const texts = result.content.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text);
|
|
4001
|
+
if (texts.length === 1) {
|
|
4002
|
+
try {
|
|
4003
|
+
value = JSON.parse(texts[0]);
|
|
4004
|
+
} catch {
|
|
4005
|
+
value = texts[0];
|
|
4006
|
+
}
|
|
4007
|
+
} else {
|
|
4008
|
+
value = texts;
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
if (!resultPath) return value;
|
|
4012
|
+
for (const segment of resultPath.split(".")) {
|
|
4013
|
+
if (segment === "__proto__" || segment === "prototype" || segment === "constructor") {
|
|
4014
|
+
return void 0;
|
|
4015
|
+
}
|
|
4016
|
+
if (!value || typeof value !== "object" || !(segment in value)) return void 0;
|
|
4017
|
+
value = value[segment];
|
|
4018
|
+
}
|
|
4019
|
+
return value;
|
|
4020
|
+
}
|
|
4021
|
+
function extractStrings(value) {
|
|
4022
|
+
if (typeof value === "string") return value.trim() ? [value] : [];
|
|
4023
|
+
if (Array.isArray(value)) return value.flatMap(extractStrings);
|
|
4024
|
+
if (!value || typeof value !== "object") return [];
|
|
4025
|
+
const record = value;
|
|
4026
|
+
for (const preferred of ["memories", "results", "items", "data", "content", "text", "memory"]) {
|
|
4027
|
+
if (preferred in record) {
|
|
4028
|
+
const found = extractStrings(record[preferred]);
|
|
4029
|
+
if (found.length > 0) return found;
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
return Object.keys(record).sort().flatMap((key) => extractStrings(record[key]));
|
|
4033
|
+
}
|
|
4034
|
+
function validateRecallResponse(value) {
|
|
4035
|
+
if (value === void 0 || value === null) {
|
|
4036
|
+
throw invalidResponse("recall", "response did not contain a result payload");
|
|
4037
|
+
}
|
|
4038
|
+
if (typeof value === "string") return value.trim().length > 0 ? [value] : [];
|
|
4039
|
+
if (Array.isArray(value)) {
|
|
4040
|
+
if (value.length === 0) return [];
|
|
4041
|
+
const strings = value.flatMap(extractRecallItem);
|
|
4042
|
+
if (strings.length === 0) {
|
|
4043
|
+
throw invalidResponse("recall", "result array contained no recognizable memory text");
|
|
4044
|
+
}
|
|
4045
|
+
return strings;
|
|
4046
|
+
}
|
|
4047
|
+
if (!isPlainRecord(value)) {
|
|
4048
|
+
throw invalidResponse("recall", "result payload must be a string, array, or object");
|
|
4049
|
+
}
|
|
4050
|
+
for (const key of [
|
|
4051
|
+
"memories",
|
|
4052
|
+
"results",
|
|
4053
|
+
"items",
|
|
4054
|
+
"hits",
|
|
4055
|
+
"data",
|
|
4056
|
+
"payload",
|
|
4057
|
+
"result",
|
|
4058
|
+
"content",
|
|
4059
|
+
"text",
|
|
4060
|
+
"memory"
|
|
4061
|
+
]) {
|
|
4062
|
+
if (!(key in value)) continue;
|
|
4063
|
+
const nested = value[key];
|
|
4064
|
+
if (Array.isArray(nested) && nested.length === 0) return [];
|
|
4065
|
+
if (typeof nested === "string" && nested.trim().length === 0) return [];
|
|
4066
|
+
const strings = extractRecallItem(nested);
|
|
4067
|
+
if (strings.length > 0) return strings;
|
|
4068
|
+
throw invalidResponse("recall", `response field ${key} contained no recognizable memory text`);
|
|
4069
|
+
}
|
|
4070
|
+
throw invalidResponse("recall", "response object contained no recognized result field");
|
|
4071
|
+
}
|
|
4072
|
+
function extractRecallItem(value) {
|
|
4073
|
+
if (typeof value === "string") return value.trim().length > 0 ? [value] : [];
|
|
4074
|
+
if (Array.isArray(value)) return value.flatMap(extractRecallItem);
|
|
4075
|
+
if (!isPlainRecord(value)) return [];
|
|
4076
|
+
for (const key of [
|
|
4077
|
+
"text",
|
|
4078
|
+
"content",
|
|
4079
|
+
"memory",
|
|
4080
|
+
"snippet",
|
|
4081
|
+
"value",
|
|
4082
|
+
"memories",
|
|
4083
|
+
"results",
|
|
4084
|
+
"items",
|
|
4085
|
+
"hits",
|
|
4086
|
+
"data",
|
|
4087
|
+
"payload",
|
|
4088
|
+
"result"
|
|
4089
|
+
]) {
|
|
4090
|
+
if (key in value) return extractRecallItem(value[key]);
|
|
4091
|
+
}
|
|
4092
|
+
return [];
|
|
4093
|
+
}
|
|
4094
|
+
function validateMutationAcknowledgement(operation, value) {
|
|
4095
|
+
if (typeof value === "boolean") return value;
|
|
4096
|
+
if (typeof value === "string") {
|
|
4097
|
+
const normalized = value.trim().toLowerCase();
|
|
4098
|
+
if (["ok", "true", "stored", "applied", "corrected", "deleted", "cleared"].includes(normalized)) {
|
|
4099
|
+
return true;
|
|
4100
|
+
}
|
|
4101
|
+
if (["false", "rejected", "not applied"].includes(normalized)) return false;
|
|
4102
|
+
throw invalidResponse(operation, "text response was not a recognized acknowledgement");
|
|
4103
|
+
}
|
|
4104
|
+
if (!isPlainRecord(value)) {
|
|
4105
|
+
throw invalidResponse(operation, "response did not contain a mutation acknowledgement");
|
|
4106
|
+
}
|
|
4107
|
+
const keys = operation === "store" ? ["stored", "created", "success", "ok"] : operation === "correct" ? ["applied", "corrected", "success", "ok"] : ["deleted", "cleared", "success", "ok"];
|
|
4108
|
+
for (const key of keys) {
|
|
4109
|
+
if (!(key in value)) continue;
|
|
4110
|
+
if (typeof value[key] !== "boolean") {
|
|
4111
|
+
throw invalidResponse(operation, `acknowledgement field ${key} must be boolean`);
|
|
4112
|
+
}
|
|
4113
|
+
return value[key];
|
|
4114
|
+
}
|
|
4115
|
+
for (const wrapper of ["data", "payload", "result"]) {
|
|
4116
|
+
if (wrapper in value) return validateMutationAcknowledgement(operation, value[wrapper]);
|
|
4117
|
+
}
|
|
4118
|
+
throw invalidResponse(operation, "response object contained no recognized acknowledgement field");
|
|
4119
|
+
}
|
|
4120
|
+
function invalidResponse(operation, detail) {
|
|
4121
|
+
return new McpMemoryBackendError({
|
|
4122
|
+
ok: false,
|
|
4123
|
+
error: "invalid_response",
|
|
4124
|
+
detail: `${operation} tool returned an invalid response: ${detail}`
|
|
4125
|
+
});
|
|
4126
|
+
}
|
|
4127
|
+
function isPlainRecord(value) {
|
|
4128
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
4129
|
+
const prototype = Object.getPrototypeOf(value);
|
|
4130
|
+
return prototype === Object.prototype || prototype === null;
|
|
4131
|
+
}
|
|
4132
|
+
function isSafeResultPath(value) {
|
|
4133
|
+
const segments = value.split(".");
|
|
4134
|
+
return segments.length > 0 && segments.every(
|
|
4135
|
+
(segment) => segment.length > 0 && segment !== "__proto__" && segment !== "prototype" && segment !== "constructor" && /^[A-Za-z0-9_-]+$/.test(segment)
|
|
4136
|
+
);
|
|
4137
|
+
}
|
|
4138
|
+
async function withTimeoutControl(timeoutMs, externalControl, label, operation) {
|
|
4139
|
+
const timeoutController = new AbortController();
|
|
4140
|
+
const timer = setTimeout(
|
|
4141
|
+
() => timeoutController.abort(new Error(`${label} timed out after ${timeoutMs}ms`)),
|
|
4142
|
+
timeoutMs
|
|
4143
|
+
);
|
|
4144
|
+
const signal = externalControl?.signal ? AbortSignal.any([externalControl.signal, timeoutController.signal]) : timeoutController.signal;
|
|
4145
|
+
try {
|
|
4146
|
+
return await raceWithSignal(operation({ signal }), signal, label);
|
|
4147
|
+
} finally {
|
|
4148
|
+
clearTimeout(timer);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
async function closeToolClient(client, timeoutMs) {
|
|
4152
|
+
await raceWithSignal(client.close(), AbortSignal.timeout(Math.min(timeoutMs, 2e3)), "MCP client close").catch(
|
|
4153
|
+
() => {
|
|
4154
|
+
}
|
|
4155
|
+
);
|
|
4156
|
+
}
|
|
4157
|
+
async function raceWithSignal(promise, signal, label, onLateSuccess) {
|
|
4158
|
+
if (!signal) return promise;
|
|
4159
|
+
let aborted = signal.aborted;
|
|
4160
|
+
let abortListener;
|
|
4161
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
4162
|
+
const rejectForAbort = () => {
|
|
4163
|
+
aborted = true;
|
|
4164
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error(`${label} aborted`));
|
|
4165
|
+
};
|
|
4166
|
+
if (signal.aborted) rejectForAbort();
|
|
4167
|
+
else {
|
|
4168
|
+
abortListener = rejectForAbort;
|
|
4169
|
+
signal.addEventListener("abort", rejectForAbort, { once: true });
|
|
4170
|
+
}
|
|
4171
|
+
});
|
|
4172
|
+
if (onLateSuccess) {
|
|
4173
|
+
void promise.then(async (value) => {
|
|
4174
|
+
if (aborted) await onLateSuccess(value);
|
|
4175
|
+
}).catch(() => {
|
|
4176
|
+
});
|
|
4177
|
+
}
|
|
4178
|
+
try {
|
|
4179
|
+
return await Promise.race([promise, abortPromise]);
|
|
4180
|
+
} finally {
|
|
4181
|
+
if (abortListener) signal.removeEventListener("abort", abortListener);
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
|
|
3295
4185
|
// src/adapters/timeout-guard.ts
|
|
3296
4186
|
var BENCHMARK_TIMEOUT_ABORT_GRACE_MS = 1500;
|
|
3297
4187
|
function resolveBenchmarkPhaseTimeoutMs(config) {
|
|
@@ -3585,6 +4475,32 @@ function wrapJudge(judge, run) {
|
|
|
3585
4475
|
}
|
|
3586
4476
|
});
|
|
3587
4477
|
}
|
|
4478
|
+
if (judge.judgeMemCorrectCorrectionAcceptance) {
|
|
4479
|
+
wrapped.judgeMemCorrectCorrectionAcceptance = (request, control) => run("judge.memcorrect.correctionAcceptance", async (signal) => {
|
|
4480
|
+
const merged = mergeBenchPhaseControl(signal, control);
|
|
4481
|
+
try {
|
|
4482
|
+
return await judge.judgeMemCorrectCorrectionAcceptance(
|
|
4483
|
+
request,
|
|
4484
|
+
merged.control
|
|
4485
|
+
);
|
|
4486
|
+
} finally {
|
|
4487
|
+
merged.cleanup();
|
|
4488
|
+
}
|
|
4489
|
+
});
|
|
4490
|
+
}
|
|
4491
|
+
if (judge.judgeMemCorrectStaleMemoryHarm) {
|
|
4492
|
+
wrapped.judgeMemCorrectStaleMemoryHarm = (request, control) => run("judge.memcorrect.staleMemoryHarm", async (signal) => {
|
|
4493
|
+
const merged = mergeBenchPhaseControl(signal, control);
|
|
4494
|
+
try {
|
|
4495
|
+
return await judge.judgeMemCorrectStaleMemoryHarm(
|
|
4496
|
+
request,
|
|
4497
|
+
merged.control
|
|
4498
|
+
);
|
|
4499
|
+
} finally {
|
|
4500
|
+
merged.cleanup();
|
|
4501
|
+
}
|
|
4502
|
+
});
|
|
4503
|
+
}
|
|
3588
4504
|
return wrapped;
|
|
3589
4505
|
}
|
|
3590
4506
|
function readPositiveIntegerConfig(remnicConfig, key) {
|
|
@@ -4483,6 +5399,7 @@ var BENCHMARK_RESULT_SCHEMA = {
|
|
|
4483
5399
|
properties: {
|
|
4484
5400
|
provider: { type: "string" },
|
|
4485
5401
|
model: { type: "string" },
|
|
5402
|
+
rubricVersion: { type: "string" },
|
|
4486
5403
|
baseUrl: { type: "string" },
|
|
4487
5404
|
reasoningEffort: { type: "string" }
|
|
4488
5405
|
}
|
|
@@ -4649,8 +5566,411 @@ function mergeContaminationManifests(...manifests) {
|
|
|
4649
5566
|
return merged;
|
|
4650
5567
|
}
|
|
4651
5568
|
|
|
5569
|
+
// src/report-card.ts
|
|
5570
|
+
var PRIMARY_METRIC_ORDER = [
|
|
5571
|
+
"overall_score",
|
|
5572
|
+
"llm_judge",
|
|
5573
|
+
"accuracy",
|
|
5574
|
+
"answer_accuracy",
|
|
5575
|
+
"exact_match",
|
|
5576
|
+
"f1",
|
|
5577
|
+
"uptake_at_next"
|
|
5578
|
+
];
|
|
5579
|
+
var CORRECTION_METRICS = [
|
|
5580
|
+
{
|
|
5581
|
+
name: "uptake_at_next",
|
|
5582
|
+
label: "Correction visible next",
|
|
5583
|
+
description: "Share of corrections visible at the first post-correction probe.",
|
|
5584
|
+
direction: "Higher is better"
|
|
5585
|
+
},
|
|
5586
|
+
{
|
|
5587
|
+
name: "non_resurrection",
|
|
5588
|
+
label: "Stale fact stayed retired",
|
|
5589
|
+
description: "Share of corrected facts that did not return during maintenance and re-ingest.",
|
|
5590
|
+
direction: "Higher is better"
|
|
5591
|
+
},
|
|
5592
|
+
{
|
|
5593
|
+
name: "false_apply",
|
|
5594
|
+
label: "False corrections",
|
|
5595
|
+
description: "Share of anti-events that incorrectly changed memory.",
|
|
5596
|
+
direction: "Lower is better"
|
|
5597
|
+
}
|
|
5598
|
+
];
|
|
5599
|
+
function escapeHtml(value) {
|
|
5600
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll(`"`, """).replaceAll("'", "'");
|
|
5601
|
+
}
|
|
5602
|
+
function isRecord(value) {
|
|
5603
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
5604
|
+
}
|
|
5605
|
+
function finiteNumber(value) {
|
|
5606
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
5607
|
+
}
|
|
5608
|
+
function formatNumber(value) {
|
|
5609
|
+
if (Number.isInteger(value)) return String(value);
|
|
5610
|
+
return value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
|
|
5611
|
+
}
|
|
5612
|
+
function formatPercent(value) {
|
|
5613
|
+
return value === void 0 ? "Not measured" : `${formatNumber(value * 100)}%`;
|
|
5614
|
+
}
|
|
5615
|
+
function formatDimensionMetric(name, value) {
|
|
5616
|
+
const percentageMetric = name.includes("rate") || name.includes("precision") || name.includes("recall") || name.includes("success") || name.includes("alignment") || name.includes("lift");
|
|
5617
|
+
return percentageMetric ? formatPercent(value) : formatNumber(value);
|
|
5618
|
+
}
|
|
5619
|
+
function aggregateMeans(result) {
|
|
5620
|
+
const means = {};
|
|
5621
|
+
for (const name of Object.keys(result.results.aggregates).sort()) {
|
|
5622
|
+
const mean4 = finiteNumber(result.results.aggregates[name]?.mean);
|
|
5623
|
+
if (mean4 !== void 0) means[name] = mean4;
|
|
5624
|
+
}
|
|
5625
|
+
return means;
|
|
5626
|
+
}
|
|
5627
|
+
function correctionMetricValues(result) {
|
|
5628
|
+
const benchmarkOptions = result.config.benchmarkOptions;
|
|
5629
|
+
const persistedBundle = isRecord(benchmarkOptions) ? benchmarkOptions.aggregateMetrics : void 0;
|
|
5630
|
+
const values = {};
|
|
5631
|
+
if (isRecord(persistedBundle)) {
|
|
5632
|
+
for (const name of Object.keys(persistedBundle).sort()) {
|
|
5633
|
+
const value = finiteNumber(persistedBundle[name]);
|
|
5634
|
+
if (value !== void 0) values[name] = value;
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
const means = aggregateMeans(result);
|
|
5638
|
+
for (const metric of CORRECTION_METRICS) {
|
|
5639
|
+
if (values[metric.name] === void 0 && means[metric.name] !== void 0) {
|
|
5640
|
+
values[metric.name] = means[metric.name];
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
return values;
|
|
5644
|
+
}
|
|
5645
|
+
function resultStatus(result) {
|
|
5646
|
+
const failure = result.meta.failureReason ?? "";
|
|
5647
|
+
const typedBackendFailure = /(?:backend[ _-]?unusable|transport[ _-]?failure|tool[ _-]?failure|invalid[ _-]?response)/i;
|
|
5648
|
+
if (result.meta.status === "partial" && result.results.tasks.length === 0 && typedBackendFailure.test(failure)) {
|
|
5649
|
+
return "backend-unusable";
|
|
5650
|
+
}
|
|
5651
|
+
if (result.meta.status === "partial") return "partial";
|
|
5652
|
+
if (result.results.tasks.length === 0) return "unscored";
|
|
5653
|
+
return "complete";
|
|
5654
|
+
}
|
|
5655
|
+
function statusCopy(status) {
|
|
5656
|
+
switch (status) {
|
|
5657
|
+
case "backend-unusable":
|
|
5658
|
+
return {
|
|
5659
|
+
label: "Backend unusable",
|
|
5660
|
+
detail: "The memory backend failed before a trustworthy score could be produced."
|
|
5661
|
+
};
|
|
5662
|
+
case "partial":
|
|
5663
|
+
return {
|
|
5664
|
+
label: "Partial run",
|
|
5665
|
+
detail: "The run stopped early. Scores describe completed tasks only and are not a full result."
|
|
5666
|
+
};
|
|
5667
|
+
case "unscored":
|
|
5668
|
+
return {
|
|
5669
|
+
label: "No scored tasks",
|
|
5670
|
+
detail: "The run completed without task-level evidence, so no overall score is claimed."
|
|
5671
|
+
};
|
|
5672
|
+
case "complete":
|
|
5673
|
+
return {
|
|
5674
|
+
label: "Complete run",
|
|
5675
|
+
detail: "All persisted task results are included in this report card."
|
|
5676
|
+
};
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
5679
|
+
function primaryMetric(result) {
|
|
5680
|
+
const means = aggregateMeans(result);
|
|
5681
|
+
const correction = correctionMetricValues(result);
|
|
5682
|
+
if (result.meta.benchmark === "memcorrect-v1" && correction.uptake_at_next !== void 0) {
|
|
5683
|
+
return { name: "uptake_at_next", value: correction.uptake_at_next };
|
|
5684
|
+
}
|
|
5685
|
+
for (const name of PRIMARY_METRIC_ORDER) {
|
|
5686
|
+
if (means[name] !== void 0) return { name, value: means[name] };
|
|
5687
|
+
}
|
|
5688
|
+
return void 0;
|
|
5689
|
+
}
|
|
5690
|
+
function renderScore(result, status) {
|
|
5691
|
+
const metric = primaryMetric(result);
|
|
5692
|
+
if (status === "backend-unusable" || status === "unscored" || !metric) {
|
|
5693
|
+
return `<div class="score score--na"><span class="score__value">N/A</span><span class="score__label">No defensible score</span></div>`;
|
|
5694
|
+
}
|
|
5695
|
+
const value = metric.value >= 0 && metric.value <= 1 ? formatPercent(metric.value) : formatNumber(metric.value);
|
|
5696
|
+
return `<div class="score"><span class="score__value">${escapeHtml(value)}</span><span class="score__label">Overall score \xB7 ${escapeHtml(metric.name)}</span><span class="score__note">Recorded mean of the named primary metric; no cross-metric composite.</span></div>`;
|
|
5697
|
+
}
|
|
5698
|
+
function renderCorrectionSpotlight(result) {
|
|
5699
|
+
const values = correctionMetricValues(result);
|
|
5700
|
+
const tasks = [...result.results.tasks].sort(
|
|
5701
|
+
(left, right) => left.taskId.localeCompare(right.taskId)
|
|
5702
|
+
);
|
|
5703
|
+
const accepted = tasks.filter((task) => task.scores.uptake_at_next === 1).length;
|
|
5704
|
+
const notObserved = tasks.filter((task) => task.scores.uptake_at_next === 0).length;
|
|
5705
|
+
const staleReturned = tasks.filter((task) => task.scores.non_resurrection === 0).length;
|
|
5706
|
+
const measuredCorrectionTasks = accepted + notObserved;
|
|
5707
|
+
const measuredRetirementTasks = tasks.filter(
|
|
5708
|
+
(task) => task.scores.non_resurrection === 0 || task.scores.non_resurrection === 1
|
|
5709
|
+
).length;
|
|
5710
|
+
const cards = CORRECTION_METRICS.map((metric) => {
|
|
5711
|
+
const value = values[metric.name];
|
|
5712
|
+
const state = value === void 0 ? "not-measured" : "measured";
|
|
5713
|
+
return ` <article class="ledger-card ledger-card--${state}">
|
|
5714
|
+
<p class="eyebrow">${escapeHtml(metric.direction)}</p>
|
|
5715
|
+
<p class="ledger-card__value">${formatPercent(value)}</p>
|
|
5716
|
+
<h3>${escapeHtml(metric.label)}</h3>
|
|
5717
|
+
<p>${escapeHtml(metric.description)}</p>
|
|
5718
|
+
</article>`;
|
|
5719
|
+
}).join("\n");
|
|
5720
|
+
const countSummary = measuredCorrectionTasks > 0 || measuredRetirementTasks > 0 ? ` <p class="ledger-summary"><strong>${accepted}</strong> visible at the next probe \xB7 <strong>${notObserved}</strong> not observed at the next probe \xB7 <strong>${staleReturned}</strong> stale-fact return${staleReturned === 1 ? "" : "s"}</p>` : ` <p class="ledger-summary ledger-summary--empty">This run did not record MemCorrect scenario outcomes.</p>`;
|
|
5721
|
+
return ` <section class="spotlight" aria-labelledby="correction-heading">
|
|
5722
|
+
<div class="section-heading">
|
|
5723
|
+
<div>
|
|
5724
|
+
<p class="eyebrow">Correction ledger</p>
|
|
5725
|
+
<h2 id="correction-heading">Did the system take the correction\u2014and keep it?</h2>
|
|
5726
|
+
</div>
|
|
5727
|
+
<p>Values come from persisted MemCorrect metrics when present. Missing evidence stays missing.</p>
|
|
5728
|
+
</div>
|
|
5729
|
+
<div class="ledger">
|
|
5730
|
+
${cards}
|
|
5731
|
+
</div>
|
|
5732
|
+
${countSummary}
|
|
5733
|
+
</section>`;
|
|
5734
|
+
}
|
|
5735
|
+
function taskContext(task) {
|
|
5736
|
+
const details = task.details;
|
|
5737
|
+
if (!isRecord(details)) return { shape: "\u2014", category: "\u2014" };
|
|
5738
|
+
return {
|
|
5739
|
+
shape: typeof details.shape === "string" ? details.shape : "\u2014",
|
|
5740
|
+
category: typeof details.category === "string" ? details.category : "\u2014"
|
|
5741
|
+
};
|
|
5742
|
+
}
|
|
5743
|
+
function taskScore(task, name) {
|
|
5744
|
+
const value = finiteNumber(task.scores[name]);
|
|
5745
|
+
return value === void 0 ? "\u2014" : formatPercent(value);
|
|
5746
|
+
}
|
|
5747
|
+
function renderScenarioDrilldown(result) {
|
|
5748
|
+
const tasks = [...result.results.tasks].sort(
|
|
5749
|
+
(left, right) => left.taskId.localeCompare(right.taskId)
|
|
5750
|
+
);
|
|
5751
|
+
if (tasks.length === 0) {
|
|
5752
|
+
return ` <section aria-labelledby="scenario-heading">
|
|
5753
|
+
<div class="section-heading"><div><p class="eyebrow">Evidence</p><h2 id="scenario-heading">Scenario drill-down</h2></div></div>
|
|
5754
|
+
<p class="empty-state">No task-level scenarios were persisted for this run.</p>
|
|
5755
|
+
</section>`;
|
|
5756
|
+
}
|
|
5757
|
+
const rows = tasks.map((task) => {
|
|
5758
|
+
const context = taskContext(task);
|
|
5759
|
+
return ` <tr>
|
|
5760
|
+
<th scope="row">${escapeHtml(task.taskId)}</th>
|
|
5761
|
+
<td>${escapeHtml(context.shape)}</td>
|
|
5762
|
+
<td>${escapeHtml(context.category)}</td>
|
|
5763
|
+
<td>${taskScore(task, "uptake_at_next")}</td>
|
|
5764
|
+
<td>${taskScore(task, "non_resurrection")}</td>
|
|
5765
|
+
<td>${taskScore(task, "false_apply")}</td>
|
|
5766
|
+
</tr>`;
|
|
5767
|
+
}).join("\n");
|
|
5768
|
+
return ` <section aria-labelledby="scenario-heading">
|
|
5769
|
+
<div class="section-heading">
|
|
5770
|
+
<div><p class="eyebrow">Evidence</p><h2 id="scenario-heading">Scenario drill-down</h2></div>
|
|
5771
|
+
<p>\u201C\u2014\u201D means the scenario did not record that metric; it is not a zero.</p>
|
|
5772
|
+
</div>
|
|
5773
|
+
<div class="table-wrap">
|
|
5774
|
+
<table>
|
|
5775
|
+
<thead><tr><th>Scenario</th><th>Shape</th><th>Category</th><th>Correction next</th><th>Stayed retired</th><th>False apply</th></tr></thead>
|
|
5776
|
+
<tbody>
|
|
5777
|
+
${rows}
|
|
5778
|
+
</tbody>
|
|
5779
|
+
</table>
|
|
5780
|
+
</div>
|
|
5781
|
+
</section>`;
|
|
5782
|
+
}
|
|
5783
|
+
function renderDimensions(result) {
|
|
5784
|
+
const means = aggregateMeans(result);
|
|
5785
|
+
const cards = MEMORY_EVAL_DIMENSIONS.map((dimension) => {
|
|
5786
|
+
const measured = dimension.metrics.filter((metric) => means[metric.name] !== void 0);
|
|
5787
|
+
const body = measured.length === 0 ? `<p class="dimension__empty">Not measured in this run. No pass or fail is inferred.</p>` : `<dl>${measured.map((metric) => `
|
|
5788
|
+
<div><dt>${escapeHtml(metric.name)}</dt><dd>${escapeHtml(formatDimensionMetric(metric.name, means[metric.name]))}</dd></div>
|
|
5789
|
+
<p>${escapeHtml(metric.higherIsBetter ? "Higher is better" : "Lower is better")} \xB7 No pass threshold is recorded in the result.</p>`).join("")}
|
|
5790
|
+
</dl>`;
|
|
5791
|
+
return ` <article class="dimension">
|
|
5792
|
+
<p class="eyebrow">${escapeHtml(dimension.category)}</p>
|
|
5793
|
+
<h3>${escapeHtml(dimension.question)}</h3>
|
|
5794
|
+
${body}
|
|
5795
|
+
</article>`;
|
|
5796
|
+
}).join("\n");
|
|
5797
|
+
return ` <section aria-labelledby="dimensions-heading">
|
|
5798
|
+
<div class="section-heading">
|
|
5799
|
+
<div><p class="eyebrow">Memory eval dimensions</p><h2 id="dimensions-heading">What this run can\u2014and cannot\u2014claim</h2></div>
|
|
5800
|
+
<p>Dimensions map only to matching <code>MEMORY_EVAL_DIMENSIONS</code> metrics.</p>
|
|
5801
|
+
</div>
|
|
5802
|
+
<div class="dimension-grid">
|
|
5803
|
+
${cards}
|
|
5804
|
+
</div>
|
|
5805
|
+
</section>`;
|
|
5806
|
+
}
|
|
5807
|
+
function renderAggregateMetrics(result) {
|
|
5808
|
+
const names = Object.keys(result.results.aggregates).sort();
|
|
5809
|
+
if (names.length === 0) {
|
|
5810
|
+
return ` <section aria-labelledby="aggregate-heading">
|
|
5811
|
+
<div class="section-heading"><div><p class="eyebrow">Raw record</p><h2 id="aggregate-heading">Aggregate Metrics</h2></div></div>
|
|
5812
|
+
<p class="empty-state">No aggregate metrics were recorded. The report does not replace them with zeros.</p>
|
|
5813
|
+
</section>`;
|
|
5814
|
+
}
|
|
5815
|
+
const rows = names.map((name) => {
|
|
5816
|
+
const aggregate = result.results.aggregates[name];
|
|
5817
|
+
return ` <tr><th scope="row">${escapeHtml(name)}</th><td>${formatNumber(aggregate.mean)}</td><td>${formatNumber(aggregate.median)}</td><td>${formatNumber(aggregate.stdDev)}</td><td>${formatNumber(aggregate.min)}</td><td>${formatNumber(aggregate.max)}</td></tr>`;
|
|
5818
|
+
}).join("\n");
|
|
5819
|
+
return ` <section aria-labelledby="aggregate-heading">
|
|
5820
|
+
<div class="section-heading"><div><p class="eyebrow">Raw record</p><h2 id="aggregate-heading">Aggregate Metrics</h2></div><p>Untransformed values from the stored result.</p></div>
|
|
5821
|
+
<div class="table-wrap"><table><thead><tr><th>Metric</th><th>Mean</th><th>Median</th><th>Std dev</th><th>Min</th><th>Max</th></tr></thead><tbody>
|
|
5822
|
+
${rows}
|
|
5823
|
+
</tbody></table></div>
|
|
5824
|
+
</section>`;
|
|
5825
|
+
}
|
|
5826
|
+
function optionString(result, names) {
|
|
5827
|
+
const options = result.config.benchmarkOptions;
|
|
5828
|
+
if (!isRecord(options)) return void 0;
|
|
5829
|
+
for (const name of names) {
|
|
5830
|
+
const value = options[name];
|
|
5831
|
+
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
5832
|
+
}
|
|
5833
|
+
return void 0;
|
|
5834
|
+
}
|
|
5835
|
+
function providerLabel(provider) {
|
|
5836
|
+
return provider ? `${provider.provider} / ${provider.model}` : "Not recorded";
|
|
5837
|
+
}
|
|
5838
|
+
function renderProvenance(result, provenance) {
|
|
5839
|
+
const rubricVersion = result.config.judgeProvider?.rubricVersion ?? optionString(result, ["rubricVersion", "judgeRubricVersion", "rubric_version"]);
|
|
5840
|
+
const remnicConfigKeyCount = Object.keys(result.config.remnicConfig ?? {}).length;
|
|
5841
|
+
const machine = [result.environment.os, result.environment.nodeVersion, result.environment.hardware].filter((value) => typeof value === "string" && value.length > 0).join(" \xB7 ");
|
|
5842
|
+
const entries = [
|
|
5843
|
+
["Result ID", result.meta.id],
|
|
5844
|
+
["Task Count", String(result.results.tasks.length)],
|
|
5845
|
+
["Git SHA", result.meta.gitSha],
|
|
5846
|
+
["Remnic / benchmark", `${result.meta.remnicVersion} / ${result.meta.version}`],
|
|
5847
|
+
["Judge", providerLabel(result.config.judgeProvider)],
|
|
5848
|
+
["Rubric version", rubricVersion ?? "Not recorded in stored result"],
|
|
5849
|
+
["Dataset hash", result.meta.datasetHash ?? "Not recorded"],
|
|
5850
|
+
["Sealed qrels hash", result.meta.qrelsSealedHash ?? "Not recorded"],
|
|
5851
|
+
["Judge prompt hash", result.meta.judgePromptHash ?? "Not recorded"],
|
|
5852
|
+
["Seeds", result.meta.seeds?.join(", ") || "Not recorded"],
|
|
5853
|
+
["Manifest reference", provenance.manifestReference ?? "Not available"],
|
|
5854
|
+
["Manifest artifact hash", provenance.artifactHash ?? "Not available"],
|
|
5855
|
+
["Machine fingerprint", machine || "Not recorded"],
|
|
5856
|
+
[
|
|
5857
|
+
"Remnic config",
|
|
5858
|
+
remnicConfigKeyCount === 0 ? "No keys recorded" : `[redacted ${remnicConfigKeyCount} key${remnicConfigKeyCount === 1 ? "" : "s"}]`
|
|
5859
|
+
]
|
|
5860
|
+
];
|
|
5861
|
+
return ` <footer aria-labelledby="provenance-heading">
|
|
5862
|
+
<div class="section-heading"><div><p class="eyebrow">Receipts</p><h2 id="provenance-heading">Provenance</h2></div><p>Result fields are persisted with the run. Manifest receipts appear only when the caller supplies a verified adjacent manifest.</p></div>
|
|
5863
|
+
<dl class="receipts">
|
|
5864
|
+
${entries.map(([label, value]) => ` <div><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`).join("\n")}
|
|
5865
|
+
</dl>
|
|
5866
|
+
</footer>`;
|
|
5867
|
+
}
|
|
5868
|
+
function renderMemoryReportCard(result, provenance = {}) {
|
|
5869
|
+
const status = resultStatus(result);
|
|
5870
|
+
const statusText = statusCopy(status);
|
|
5871
|
+
const system = result.config.systemProvider ? `${result.config.systemProvider.provider} / ${result.config.systemProvider.model}` : result.config.adapterMode;
|
|
5872
|
+
const failure = result.meta.failureReason ? `<p class="failure"><strong>Run stopped:</strong> ${escapeHtml(result.meta.failureReason)}</p>` : "";
|
|
5873
|
+
return `<!doctype html>
|
|
5874
|
+
<html lang="en">
|
|
5875
|
+
<head>
|
|
5876
|
+
<meta charset="utf-8">
|
|
5877
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5878
|
+
<title>Remnic Bench Report: ${escapeHtml(result.meta.benchmark)}</title>
|
|
5879
|
+
<style>
|
|
5880
|
+
:root { color-scheme: light; --ink:#142523; --muted:#5d6e69; --mist:#e9f0ed; --paper:#f8fbf9; --line:#cbd8d3; --teal:#0b7168; --teal-soft:#d8ebe6; --ember:#c9563f; --ember-soft:#f8e1db; --shadow:0 18px 48px rgba(20,37,35,.09); font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
|
|
5881
|
+
* { box-sizing:border-box; }
|
|
5882
|
+
body { margin:0; background:var(--mist); color:var(--ink); }
|
|
5883
|
+
main { width:min(1180px,calc(100% - 32px)); margin:0 auto; padding:32px 0 64px; }
|
|
5884
|
+
h1,h2,h3,p,dl,dd { margin:0; }
|
|
5885
|
+
h1,h2 { font-family:Georgia,"Times New Roman",serif; font-weight:500; letter-spacing:-.025em; }
|
|
5886
|
+
h1 { max-width:780px; font-size:clamp(2.5rem,7vw,5.8rem); line-height:.94; }
|
|
5887
|
+
h2 { font-size:clamp(1.7rem,3vw,2.5rem); line-height:1.05; }
|
|
5888
|
+
h3 { font-size:1rem; line-height:1.25; }
|
|
5889
|
+
p { line-height:1.55; }
|
|
5890
|
+
code,.eyebrow,.score__value,.receipts dd,table { font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace; }
|
|
5891
|
+
.eyebrow { margin-bottom:10px; color:var(--teal); font-size:.72rem; font-weight:700; letter-spacing:.12em; text-transform:uppercase; }
|
|
5892
|
+
.hero { position:relative; overflow:hidden; padding:clamp(28px,5vw,60px); background:var(--ink); color:var(--paper); border-radius:24px; box-shadow:var(--shadow); }
|
|
5893
|
+
.hero::after { content:""; position:absolute; width:320px; height:320px; right:-120px; top:-160px; border:56px solid var(--teal); border-radius:50%; opacity:.62; }
|
|
5894
|
+
.hero__meta { position:relative; z-index:1; display:flex; flex-wrap:wrap; gap:10px; margin-bottom:40px; }
|
|
5895
|
+
.pill { padding:7px 11px; border:1px solid rgba(248,251,249,.28); border-radius:999px; font-size:.78rem; }
|
|
5896
|
+
.hero__grid { position:relative; z-index:1; display:grid; grid-template-columns:minmax(0,1fr) minmax(220px,330px); gap:36px; align-items:end; }
|
|
5897
|
+
.hero__lede { max-width:650px; margin-top:22px; color:#bdd0ca; font-size:1.05rem; }
|
|
5898
|
+
.status { display:inline-flex; align-items:center; gap:8px; margin-top:24px; font-weight:700; }
|
|
5899
|
+
.status::before { content:""; width:9px; height:9px; background:var(--teal); border-radius:50%; box-shadow:0 0 0 5px rgba(11,113,104,.25); }
|
|
5900
|
+
.status--partial::before,.status--backend-unusable::before { background:var(--ember); box-shadow:0 0 0 5px rgba(201,86,63,.24); }
|
|
5901
|
+
.status__detail { display:block; margin-top:8px; color:#bdd0ca; font-size:.88rem; }
|
|
5902
|
+
.score { padding:24px; background:rgba(248,251,249,.08); border:1px solid rgba(248,251,249,.18); border-radius:18px; }
|
|
5903
|
+
.score__value { display:block; color:#75d2c4; font-size:clamp(2.8rem,6vw,4.8rem); line-height:1; }
|
|
5904
|
+
.score--na .score__value { color:#f2a18f; }
|
|
5905
|
+
.score__label { display:block; margin-top:10px; font-weight:750; }
|
|
5906
|
+
.score__note { display:block; margin-top:8px; color:#bdd0ca; font-size:.78rem; line-height:1.45; }
|
|
5907
|
+
.failure { position:relative; z-index:1; margin-top:24px; padding:14px 16px; background:rgba(201,86,63,.18); border-left:3px solid #f08d76; border-radius:8px; color:#ffe9e4; }
|
|
5908
|
+
section,footer { margin-top:22px; padding:clamp(22px,4vw,38px); background:var(--paper); border:1px solid var(--line); border-radius:20px; }
|
|
5909
|
+
.spotlight { margin-top:-1px; border-top-left-radius:0; border-top-right-radius:0; border-top:6px solid var(--teal); }
|
|
5910
|
+
.section-heading { display:flex; justify-content:space-between; gap:28px; align-items:end; margin-bottom:24px; }
|
|
5911
|
+
.section-heading>p { max-width:430px; color:var(--muted); font-size:.9rem; }
|
|
5912
|
+
.ledger { display:grid; grid-template-columns:repeat(3,1fr); gap:1px; overflow:hidden; border:1px solid var(--line); border-radius:16px; background:var(--line); }
|
|
5913
|
+
.ledger-card { min-height:220px; padding:24px; background:white; }
|
|
5914
|
+
.ledger-card--not-measured { background:#f0f3f1; color:var(--muted); }
|
|
5915
|
+
.ledger-card__value { margin:22px 0 8px; color:var(--teal); font-family:Georgia,"Times New Roman",serif; font-size:2.4rem; }
|
|
5916
|
+
.ledger-card--not-measured .ledger-card__value { color:var(--muted); font-size:1.4rem; }
|
|
5917
|
+
.ledger-card h3 { margin-bottom:8px; }
|
|
5918
|
+
.ledger-card p:last-child { color:var(--muted); font-size:.87rem; }
|
|
5919
|
+
.ledger-summary { margin-top:18px; padding:14px 16px; background:var(--teal-soft); border-radius:10px; }
|
|
5920
|
+
.ledger-summary--empty { background:#eef1ef; color:var(--muted); }
|
|
5921
|
+
.dimension-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; }
|
|
5922
|
+
.dimension { padding:20px; border:1px solid var(--line); border-radius:14px; background:white; }
|
|
5923
|
+
.dimension h3 { min-height:2.5em; margin-bottom:18px; }
|
|
5924
|
+
.dimension dl>div { display:flex; justify-content:space-between; gap:16px; padding-top:10px; border-top:1px solid var(--line); }
|
|
5925
|
+
.dimension dl p,.dimension__empty { margin-top:8px; color:var(--muted); font-size:.78rem; }
|
|
5926
|
+
.dimension dt { overflow-wrap:anywhere; }
|
|
5927
|
+
.dimension dd { color:var(--teal); font-weight:700; }
|
|
5928
|
+
.table-wrap { overflow-x:auto; border:1px solid var(--line); border-radius:12px; }
|
|
5929
|
+
table { width:100%; border-collapse:collapse; font-size:.8rem; }
|
|
5930
|
+
th,td { padding:12px 14px; text-align:left; border-top:1px solid var(--line); white-space:nowrap; }
|
|
5931
|
+
thead th { border-top:0; background:#edf3f0; color:var(--muted); font-size:.7rem; letter-spacing:.05em; text-transform:uppercase; }
|
|
5932
|
+
tbody th { color:var(--ink); }
|
|
5933
|
+
.empty-state { padding:22px; background:#eef1ef; border-radius:12px; color:var(--muted); }
|
|
5934
|
+
footer { background:#dfe9e5; }
|
|
5935
|
+
.receipts { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1px; overflow:hidden; border:1px solid #bccbc5; border-radius:12px; background:#bccbc5; }
|
|
5936
|
+
.receipts>div { min-width:0; padding:14px 16px; background:var(--paper); }
|
|
5937
|
+
.receipts dt { margin-bottom:6px; color:var(--muted); font-size:.72rem; text-transform:uppercase; letter-spacing:.06em; }
|
|
5938
|
+
.receipts dd { overflow-wrap:anywhere; font-size:.82rem; }
|
|
5939
|
+
@media (max-width:760px) { main{width:min(100% - 20px,1180px);padding-top:10px}.hero{border-radius:16px}.hero__grid,.ledger,.dimension-grid,.receipts{grid-template-columns:1fr}.section-heading{display:block}.section-heading>p{margin-top:12px}.spotlight{border-radius:0 0 16px 16px}.ledger-card{min-height:0} }
|
|
5940
|
+
@media (prefers-reduced-motion:reduce) { *,*::before,*::after { scroll-behavior:auto!important; } }
|
|
5941
|
+
@media print { body{background:white}main{width:100%;padding:0}.hero,section,footer{box-shadow:none;break-inside:avoid}.hero{border-radius:0} }
|
|
5942
|
+
</style>
|
|
5943
|
+
</head>
|
|
5944
|
+
<body>
|
|
5945
|
+
<main>
|
|
5946
|
+
<header class="hero">
|
|
5947
|
+
<div class="hero__meta"><span class="pill">${escapeHtml(result.meta.benchmark)}</span><span class="pill">${escapeHtml(result.meta.mode)} mode</span><span class="pill">${result.results.tasks.length} task${result.results.tasks.length === 1 ? "" : "s"}</span><span class="pill">${escapeHtml(result.meta.timestamp)}</span></div>
|
|
5948
|
+
<div class="hero__grid">
|
|
5949
|
+
<div>
|
|
5950
|
+
<p class="eyebrow">Remnic memory report card</p>
|
|
5951
|
+
<h1>${escapeHtml(system)}</h1>
|
|
5952
|
+
<p class="hero__lede">Adapter: ${escapeHtml(result.config.adapterMode)} \xB7 Run ${escapeHtml(result.meta.id)} \xB7 Git ${escapeHtml(result.meta.gitSha)}</p>
|
|
5953
|
+
<p class="status status--${status}">${escapeHtml(statusText.label)}</p>
|
|
5954
|
+
<span class="status__detail">${escapeHtml(statusText.detail)}</span>
|
|
5955
|
+
</div>
|
|
5956
|
+
${renderScore(result, status)}
|
|
5957
|
+
</div>
|
|
5958
|
+
${failure}
|
|
5959
|
+
</header>
|
|
5960
|
+
${renderCorrectionSpotlight(result)}
|
|
5961
|
+
${renderDimensions(result)}
|
|
5962
|
+
${renderScenarioDrilldown(result)}
|
|
5963
|
+
${renderAggregateMetrics(result)}
|
|
5964
|
+
${renderProvenance(result, provenance)}
|
|
5965
|
+
</main>
|
|
5966
|
+
</body>
|
|
5967
|
+
</html>
|
|
5968
|
+
`;
|
|
5969
|
+
}
|
|
5970
|
+
|
|
4652
5971
|
// src/results-store.ts
|
|
4653
5972
|
var BASELINE_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
5973
|
+
var REPRO_MANIFEST_FILENAME = "MANIFEST.json";
|
|
4654
5974
|
function defaultBenchmarkBaselineDir() {
|
|
4655
5975
|
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
|
|
4656
5976
|
return path3.join(homeDir, ".remnic", "bench", "baselines");
|
|
@@ -4683,6 +6003,25 @@ function isObjectRecord(value) {
|
|
|
4683
6003
|
function isFiniteNumber(value) {
|
|
4684
6004
|
return typeof value === "number" && Number.isFinite(value);
|
|
4685
6005
|
}
|
|
6006
|
+
async function loadBenchmarkReportCardProvenance(outputDir, resultId) {
|
|
6007
|
+
const manifestPath = path3.join(outputDir, REPRO_MANIFEST_FILENAME);
|
|
6008
|
+
let parsed;
|
|
6009
|
+
try {
|
|
6010
|
+
parsed = JSON.parse(await readFile3(manifestPath, "utf8"));
|
|
6011
|
+
} catch {
|
|
6012
|
+
return {};
|
|
6013
|
+
}
|
|
6014
|
+
if (!isObjectRecord(parsed) || !Array.isArray(parsed.results)) return {};
|
|
6015
|
+
const coversResult = parsed.results.some(
|
|
6016
|
+
(entry) => isObjectRecord(entry) && entry.resultId === resultId
|
|
6017
|
+
);
|
|
6018
|
+
if (!coversResult) return {};
|
|
6019
|
+
const artifactHash = typeof parsed.artifactHash === "string" && parsed.artifactHash.length > 0 ? parsed.artifactHash : void 0;
|
|
6020
|
+
return {
|
|
6021
|
+
manifestReference: REPRO_MANIFEST_FILENAME,
|
|
6022
|
+
...artifactHash ? { artifactHash } : {}
|
|
6023
|
+
};
|
|
6024
|
+
}
|
|
4686
6025
|
function isProviderConfigLike(value) {
|
|
4687
6026
|
if (value === null) {
|
|
4688
6027
|
return true;
|
|
@@ -4964,7 +6303,7 @@ function classifyPublishCandidate(result, target, contaminationManifest) {
|
|
|
4964
6303
|
}
|
|
4965
6304
|
return null;
|
|
4966
6305
|
}
|
|
4967
|
-
function toPublishedBenchmarkFeedEntry(result) {
|
|
6306
|
+
function toPublishedBenchmarkFeedEntry(result, provenance) {
|
|
4968
6307
|
if (!integrityMetaIsComplete(result.meta)) {
|
|
4969
6308
|
throw new Error(
|
|
4970
6309
|
"toPublishedBenchmarkFeedEntry called with a result missing integrity metadata; call assertPublishableIntegrity first."
|
|
@@ -4982,6 +6321,7 @@ function toPublishedBenchmarkFeedEntry(result) {
|
|
|
4982
6321
|
aggregateMetrics: result.results.aggregates,
|
|
4983
6322
|
cost: result.cost,
|
|
4984
6323
|
environment: result.environment,
|
|
6324
|
+
reportCardHtml: renderMemoryReportCard(result, provenance),
|
|
4985
6325
|
integrity: {
|
|
4986
6326
|
splitType: result.meta.splitType,
|
|
4987
6327
|
qrelsSealedHash: result.meta.qrelsSealedHash,
|
|
@@ -5009,9 +6349,13 @@ async function buildBenchmarkPublishFeed(outputDir, target, options = {}) {
|
|
|
5009
6349
|
skipped.push({ ...skip, path: summary.path });
|
|
5010
6350
|
continue;
|
|
5011
6351
|
}
|
|
6352
|
+
const provenance = await loadBenchmarkReportCardProvenance(
|
|
6353
|
+
outputDir,
|
|
6354
|
+
result.meta.id
|
|
6355
|
+
);
|
|
5012
6356
|
latestByBenchmark.set(
|
|
5013
6357
|
summary.benchmark,
|
|
5014
|
-
toPublishedBenchmarkFeedEntry(result)
|
|
6358
|
+
toPublishedBenchmarkFeedEntry(result, provenance)
|
|
5015
6359
|
);
|
|
5016
6360
|
}
|
|
5017
6361
|
return {
|
|
@@ -5034,197 +6378,13 @@ function csvEscape(value) {
|
|
|
5034
6378
|
}
|
|
5035
6379
|
return text;
|
|
5036
6380
|
}
|
|
5037
|
-
function
|
|
5038
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll(`"`, """).replaceAll("'", "'");
|
|
5039
|
-
}
|
|
5040
|
-
function renderHtmlKeyValueRows(entries) {
|
|
5041
|
-
return entries.map(([label, value]) => ` <tr><th>${escapeHtml(label)}</th><td>${escapeHtml(String(value))}</td></tr>`).join("\n");
|
|
5042
|
-
}
|
|
5043
|
-
function renderBenchmarkResultHtml(result) {
|
|
5044
|
-
const seeds = Array.isArray(result.meta.seeds) ? result.meta.seeds.join(", ") : "Unknown";
|
|
5045
|
-
const aggregateRows = Object.keys(result.results.aggregates).sort().map((metric) => {
|
|
5046
|
-
const aggregate = result.results.aggregates[metric];
|
|
5047
|
-
return ` <tr><th>${escapeHtml(metric)}</th><td>${escapeHtml(String(aggregate.mean))}</td><td>${escapeHtml(String(aggregate.median))}</td><td>${escapeHtml(String(aggregate.stdDev))}</td><td>${escapeHtml(String(aggregate.min))}</td><td>${escapeHtml(String(aggregate.max))}</td></tr>`;
|
|
5048
|
-
}).join("\n");
|
|
5049
|
-
const statisticsBlock = result.results.statistics ? `
|
|
5050
|
-
<section>
|
|
5051
|
-
<h2>Statistics</h2>
|
|
5052
|
-
<pre>${escapeHtml(JSON.stringify(result.results.statistics, null, 2))}</pre>
|
|
5053
|
-
</section>` : "";
|
|
5054
|
-
const remnicConfigKeyCount = Object.keys(result.config.remnicConfig ?? {}).length;
|
|
5055
|
-
const renderedConfig = {
|
|
5056
|
-
systemProvider: result.config.systemProvider,
|
|
5057
|
-
judgeProvider: result.config.judgeProvider,
|
|
5058
|
-
internalProvider: result.config.internalProvider,
|
|
5059
|
-
remnicConfig: remnicConfigKeyCount === 0 ? "[empty]" : `[redacted ${remnicConfigKeyCount} key${remnicConfigKeyCount === 1 ? "" : "s"}]`
|
|
5060
|
-
};
|
|
5061
|
-
return `<!doctype html>
|
|
5062
|
-
<html lang="en">
|
|
5063
|
-
<head>
|
|
5064
|
-
<meta charset="utf-8">
|
|
5065
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5066
|
-
<title>Remnic Bench Report: ${escapeHtml(result.meta.benchmark)}</title>
|
|
5067
|
-
<style>
|
|
5068
|
-
:root {
|
|
5069
|
-
color-scheme: light;
|
|
5070
|
-
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
5071
|
-
}
|
|
5072
|
-
body {
|
|
5073
|
-
margin: 0;
|
|
5074
|
-
background: #f5f7fb;
|
|
5075
|
-
color: #18202a;
|
|
5076
|
-
}
|
|
5077
|
-
main {
|
|
5078
|
-
max-width: 1080px;
|
|
5079
|
-
margin: 0 auto;
|
|
5080
|
-
padding: 32px 20px 48px;
|
|
5081
|
-
}
|
|
5082
|
-
header {
|
|
5083
|
-
margin-bottom: 24px;
|
|
5084
|
-
}
|
|
5085
|
-
h1, h2 {
|
|
5086
|
-
margin: 0 0 12px;
|
|
5087
|
-
}
|
|
5088
|
-
p {
|
|
5089
|
-
margin: 0;
|
|
5090
|
-
line-height: 1.5;
|
|
5091
|
-
}
|
|
5092
|
-
section {
|
|
5093
|
-
background: #ffffff;
|
|
5094
|
-
border: 1px solid #d8dee8;
|
|
5095
|
-
border-radius: 12px;
|
|
5096
|
-
padding: 20px;
|
|
5097
|
-
margin-top: 16px;
|
|
5098
|
-
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
|
5099
|
-
}
|
|
5100
|
-
table {
|
|
5101
|
-
width: 100%;
|
|
5102
|
-
border-collapse: collapse;
|
|
5103
|
-
}
|
|
5104
|
-
th, td {
|
|
5105
|
-
text-align: left;
|
|
5106
|
-
padding: 10px 12px;
|
|
5107
|
-
border-top: 1px solid #e5eaf1;
|
|
5108
|
-
vertical-align: top;
|
|
5109
|
-
}
|
|
5110
|
-
thead th {
|
|
5111
|
-
border-top: none;
|
|
5112
|
-
font-size: 0.85rem;
|
|
5113
|
-
letter-spacing: 0.02em;
|
|
5114
|
-
text-transform: uppercase;
|
|
5115
|
-
color: #526173;
|
|
5116
|
-
}
|
|
5117
|
-
tbody th {
|
|
5118
|
-
width: 30%;
|
|
5119
|
-
}
|
|
5120
|
-
pre {
|
|
5121
|
-
margin: 0;
|
|
5122
|
-
overflow-x: auto;
|
|
5123
|
-
white-space: pre-wrap;
|
|
5124
|
-
word-break: break-word;
|
|
5125
|
-
background: #f5f7fb;
|
|
5126
|
-
border-radius: 10px;
|
|
5127
|
-
padding: 14px;
|
|
5128
|
-
border: 1px solid #e5eaf1;
|
|
5129
|
-
}
|
|
5130
|
-
.muted {
|
|
5131
|
-
color: #526173;
|
|
5132
|
-
margin-top: 6px;
|
|
5133
|
-
}
|
|
5134
|
-
.empty {
|
|
5135
|
-
color: #526173;
|
|
5136
|
-
font-style: italic;
|
|
5137
|
-
}
|
|
5138
|
-
</style>
|
|
5139
|
-
</head>
|
|
5140
|
-
<body>
|
|
5141
|
-
<main>
|
|
5142
|
-
<header>
|
|
5143
|
-
<h1>Remnic Bench Report</h1>
|
|
5144
|
-
<p>${escapeHtml(result.meta.benchmark)} \xB7 ${escapeHtml(result.meta.id)}</p>
|
|
5145
|
-
<p class="muted">Generated from a stored benchmark result export.</p>
|
|
5146
|
-
</header>
|
|
5147
|
-
<section>
|
|
5148
|
-
<h2>Run Summary</h2>
|
|
5149
|
-
<table>
|
|
5150
|
-
<tbody>
|
|
5151
|
-
${renderHtmlKeyValueRows([
|
|
5152
|
-
["Result ID", result.meta.id],
|
|
5153
|
-
["Benchmark", result.meta.benchmark],
|
|
5154
|
-
["Benchmark Tier", result.meta.benchmarkTier],
|
|
5155
|
-
["Timestamp", result.meta.timestamp],
|
|
5156
|
-
["Mode", result.meta.mode],
|
|
5157
|
-
["Run Count", result.meta.runCount],
|
|
5158
|
-
["Task Count", result.results.tasks.length],
|
|
5159
|
-
["Remnic Version", result.meta.remnicVersion],
|
|
5160
|
-
["Benchmark Version", result.meta.version],
|
|
5161
|
-
["Git SHA", result.meta.gitSha],
|
|
5162
|
-
["Seeds", seeds]
|
|
5163
|
-
])}
|
|
5164
|
-
</tbody>
|
|
5165
|
-
</table>
|
|
5166
|
-
</section>
|
|
5167
|
-
<section>
|
|
5168
|
-
<h2>Aggregate Metrics</h2>
|
|
5169
|
-
${aggregateRows.length > 0 ? ` <table>
|
|
5170
|
-
<thead>
|
|
5171
|
-
<tr><th>Metric</th><th>Mean</th><th>Median</th><th>Std Dev</th><th>Min</th><th>Max</th></tr>
|
|
5172
|
-
</thead>
|
|
5173
|
-
<tbody>
|
|
5174
|
-
${aggregateRows}
|
|
5175
|
-
</tbody>
|
|
5176
|
-
</table>` : ' <p class="empty">No aggregate metrics recorded for this run.</p>'}
|
|
5177
|
-
</section>
|
|
5178
|
-
<section>
|
|
5179
|
-
<h2>Cost</h2>
|
|
5180
|
-
<table>
|
|
5181
|
-
<tbody>
|
|
5182
|
-
${renderHtmlKeyValueRows([
|
|
5183
|
-
["Total Tokens", result.cost.totalTokens],
|
|
5184
|
-
["Input Tokens", result.cost.inputTokens],
|
|
5185
|
-
["Output Tokens", result.cost.outputTokens],
|
|
5186
|
-
["Estimated Cost (USD)", result.cost.estimatedCostUsd],
|
|
5187
|
-
["Total Latency (ms)", result.cost.totalLatencyMs],
|
|
5188
|
-
["Mean Query Latency (ms)", result.cost.meanQueryLatencyMs]
|
|
5189
|
-
])}
|
|
5190
|
-
</tbody>
|
|
5191
|
-
</table>
|
|
5192
|
-
</section>
|
|
5193
|
-
<section>
|
|
5194
|
-
<h2>Environment</h2>
|
|
5195
|
-
<table>
|
|
5196
|
-
<tbody>
|
|
5197
|
-
${renderHtmlKeyValueRows([
|
|
5198
|
-
["OS", result.environment.os],
|
|
5199
|
-
["Node Version", result.environment.nodeVersion],
|
|
5200
|
-
["Hardware", result.environment.hardware ?? "Unknown"]
|
|
5201
|
-
])}
|
|
5202
|
-
</tbody>
|
|
5203
|
-
</table>
|
|
5204
|
-
</section>
|
|
5205
|
-
<section>
|
|
5206
|
-
<h2>Configuration</h2>
|
|
5207
|
-
<table>
|
|
5208
|
-
<tbody>
|
|
5209
|
-
${renderHtmlKeyValueRows([
|
|
5210
|
-
["Adapter Mode", result.config.adapterMode]
|
|
5211
|
-
])}
|
|
5212
|
-
</tbody>
|
|
5213
|
-
</table>
|
|
5214
|
-
<pre>${escapeHtml(JSON.stringify(renderedConfig, null, 2))}</pre>
|
|
5215
|
-
</section>${statisticsBlock}
|
|
5216
|
-
</main>
|
|
5217
|
-
</body>
|
|
5218
|
-
</html>
|
|
5219
|
-
`;
|
|
5220
|
-
}
|
|
5221
|
-
function renderBenchmarkResultExport(result, format) {
|
|
6381
|
+
function renderBenchmarkResultExport(result, format, options = {}) {
|
|
5222
6382
|
if (format === "json") {
|
|
5223
6383
|
return `${JSON.stringify(result, null, 2)}
|
|
5224
6384
|
`;
|
|
5225
6385
|
}
|
|
5226
6386
|
if (format === "html") {
|
|
5227
|
-
return
|
|
6387
|
+
return renderMemoryReportCard(result, options.reportCardProvenance);
|
|
5228
6388
|
}
|
|
5229
6389
|
const rows = [
|
|
5230
6390
|
[
|
|
@@ -6162,7 +7322,12 @@ async function buildResultManifest(resultsDir, resultPath, result) {
|
|
|
6162
7322
|
runCount: result.meta.runCount,
|
|
6163
7323
|
seeds: [...result.meta.seeds],
|
|
6164
7324
|
taskCount: result.results.tasks.length,
|
|
6165
|
-
configHash: sha256String(stableStringify(result.config))
|
|
7325
|
+
configHash: sha256String(stableStringify(result.config)),
|
|
7326
|
+
judge: result.config.judgeProvider ? {
|
|
7327
|
+
provider: result.config.judgeProvider.provider,
|
|
7328
|
+
model: result.config.judgeProvider.model,
|
|
7329
|
+
rubricVersion: result.config.judgeProvider.rubricVersion ?? null
|
|
7330
|
+
} : null
|
|
6166
7331
|
};
|
|
6167
7332
|
}
|
|
6168
7333
|
async function resolveResultPaths(resultsDir, explicitPaths) {
|
|
@@ -6474,7 +7639,35 @@ function readJudgeCalibrationFromBenchmarkOptions(value) {
|
|
|
6474
7639
|
if (typeof kappa !== "number" || !Number.isFinite(kappa) || typeof sampleSize !== "number" || !Number.isFinite(sampleSize) || typeof threshold !== "number" || !Number.isFinite(threshold) || typeof warning !== "boolean") {
|
|
6475
7640
|
return void 0;
|
|
6476
7641
|
}
|
|
6477
|
-
|
|
7642
|
+
const confidenceInterval = readCalibrationConfidenceInterval(record.confidenceInterval);
|
|
7643
|
+
const bootstrapSamples = record.bootstrapSamples;
|
|
7644
|
+
const answerSetHash = record.answerSetHash;
|
|
7645
|
+
const sourceResultId = record.sourceResultId;
|
|
7646
|
+
const sliceQuestionIds = readCalibrationQuestionIds(record.sliceQuestionIds);
|
|
7647
|
+
const hasCompleteProvenance = typeof answerSetHash === "string" && /^[0-9a-f]{64}$/.test(answerSetHash) && typeof sourceResultId === "string" && sourceResultId.length > 0 && sliceQuestionIds !== void 0 && sliceQuestionIds.length === sampleSize;
|
|
7648
|
+
return {
|
|
7649
|
+
kappa,
|
|
7650
|
+
sampleSize,
|
|
7651
|
+
threshold,
|
|
7652
|
+
warning,
|
|
7653
|
+
...confidenceInterval ? { confidenceInterval } : {},
|
|
7654
|
+
...typeof bootstrapSamples === "number" && Number.isInteger(bootstrapSamples) && bootstrapSamples > 0 ? { bootstrapSamples } : {},
|
|
7655
|
+
...hasCompleteProvenance ? { answerSetHash, sourceResultId, sliceQuestionIds } : {}
|
|
7656
|
+
};
|
|
7657
|
+
}
|
|
7658
|
+
function readCalibrationConfidenceInterval(value) {
|
|
7659
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
7660
|
+
const interval = value;
|
|
7661
|
+
if (typeof interval.lower !== "number" || !Number.isFinite(interval.lower) || typeof interval.upper !== "number" || !Number.isFinite(interval.upper) || typeof interval.level !== "number" || !Number.isFinite(interval.level) || interval.lower > interval.upper || interval.level <= 0 || interval.level >= 1) {
|
|
7662
|
+
return void 0;
|
|
7663
|
+
}
|
|
7664
|
+
return { lower: interval.lower, upper: interval.upper, level: interval.level };
|
|
7665
|
+
}
|
|
7666
|
+
function readCalibrationQuestionIds(value) {
|
|
7667
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 200 || !value.every((id) => typeof id === "string" && id.length > 0) || new Set(value).size !== value.length) {
|
|
7668
|
+
return void 0;
|
|
7669
|
+
}
|
|
7670
|
+
return value;
|
|
6478
7671
|
}
|
|
6479
7672
|
function buildBenchmarkArtifactFilename(artifact) {
|
|
6480
7673
|
const date = sanitizeSegment(artifact.startedAt.slice(0, 10));
|
|
@@ -6561,6 +7754,25 @@ function parseBenchmarkArtifact(raw) {
|
|
|
6561
7754
|
if (typeof warning !== "boolean") {
|
|
6562
7755
|
throw new Error(`BenchmarkArtifact judgeCalibration.warning must be a boolean; got ${String(warning)}.`);
|
|
6563
7756
|
}
|
|
7757
|
+
if (calibration.confidenceInterval !== void 0 && !readCalibrationConfidenceInterval(calibration.confidenceInterval)) {
|
|
7758
|
+
throw new Error("BenchmarkArtifact judgeCalibration.confidenceInterval must contain finite ordered lower/upper bounds and a level between 0 and 1.");
|
|
7759
|
+
}
|
|
7760
|
+
if (calibration.bootstrapSamples !== void 0 && (typeof calibration.bootstrapSamples !== "number" || !Number.isInteger(calibration.bootstrapSamples) || calibration.bootstrapSamples <= 0)) {
|
|
7761
|
+
throw new Error("BenchmarkArtifact judgeCalibration.bootstrapSamples must be a positive integer when provided.");
|
|
7762
|
+
}
|
|
7763
|
+
if (calibration.answerSetHash !== void 0 && (typeof calibration.answerSetHash !== "string" || !/^[0-9a-f]{64}$/.test(calibration.answerSetHash))) {
|
|
7764
|
+
throw new Error("BenchmarkArtifact judgeCalibration.answerSetHash must be a lowercase SHA-256 hex digest when provided.");
|
|
7765
|
+
}
|
|
7766
|
+
if (calibration.sourceResultId !== void 0 && (typeof calibration.sourceResultId !== "string" || calibration.sourceResultId.length === 0)) {
|
|
7767
|
+
throw new Error("BenchmarkArtifact judgeCalibration.sourceResultId must be a non-empty string when provided.");
|
|
7768
|
+
}
|
|
7769
|
+
if (calibration.sliceQuestionIds !== void 0 && !readCalibrationQuestionIds(calibration.sliceQuestionIds)) {
|
|
7770
|
+
throw new Error("BenchmarkArtifact judgeCalibration.sliceQuestionIds must contain 1 to 200 unique non-empty strings when provided.");
|
|
7771
|
+
}
|
|
7772
|
+
const hasAnyPinnedProvenance = calibration.answerSetHash !== void 0 || calibration.sourceResultId !== void 0 || calibration.sliceQuestionIds !== void 0;
|
|
7773
|
+
if (hasAnyPinnedProvenance && (typeof calibration.answerSetHash !== "string" || !/^[0-9a-f]{64}$/.test(calibration.answerSetHash) || typeof calibration.sourceResultId !== "string" || calibration.sourceResultId.length === 0 || !readCalibrationQuestionIds(calibration.sliceQuestionIds) || calibration.sliceQuestionIds.length !== calibration.sampleSize)) {
|
|
7774
|
+
throw new Error("BenchmarkArtifact judgeCalibration pinned provenance requires a sourceResultId, answerSetHash, and unique sliceQuestionIds matching sampleSize.");
|
|
7775
|
+
}
|
|
6564
7776
|
}
|
|
6565
7777
|
const metrics = requireObject(record, "metrics");
|
|
6566
7778
|
for (const [key, value] of Object.entries(metrics)) {
|
|
@@ -6678,6 +7890,14 @@ function isPublishedBenchmarkArtifactId(value) {
|
|
|
6678
7890
|
}
|
|
6679
7891
|
|
|
6680
7892
|
// src/providers/retry-fetch.ts
|
|
7893
|
+
var RetryFetchHttpError = class extends Error {
|
|
7894
|
+
status;
|
|
7895
|
+
constructor(status, message) {
|
|
7896
|
+
super(message);
|
|
7897
|
+
this.name = "RetryFetchHttpError";
|
|
7898
|
+
this.status = status;
|
|
7899
|
+
}
|
|
7900
|
+
};
|
|
6681
7901
|
var DEFAULTS = {
|
|
6682
7902
|
maxAttempts: 3,
|
|
6683
7903
|
baseBackoffMs: 1e3,
|
|
@@ -6839,7 +8059,8 @@ async function retryFetch(url, init, options) {
|
|
|
6839
8059
|
const bodyPreview = await readBodyPreview(response, 512);
|
|
6840
8060
|
if (attempt >= opts.maxAttempts && remainingExtendedBudgetMs() <= 0) {
|
|
6841
8061
|
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
6842
|
-
throw new
|
|
8062
|
+
throw new RetryFetchHttpError(
|
|
8063
|
+
response.status,
|
|
6843
8064
|
`HTTP ${response.status} ${response.statusText} (attempt ${attempt}/${opts.maxAttempts}): ${bodyPreview}`
|
|
6844
8065
|
);
|
|
6845
8066
|
}
|
|
@@ -9183,7 +10404,10 @@ function readGitSha() {
|
|
|
9183
10404
|
return explicitSha.trim().slice(0, 40);
|
|
9184
10405
|
}
|
|
9185
10406
|
try {
|
|
9186
|
-
return execSync("git rev-parse --short HEAD", {
|
|
10407
|
+
return execSync("git rev-parse --short HEAD", {
|
|
10408
|
+
encoding: "utf8",
|
|
10409
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
10410
|
+
}).trim();
|
|
9187
10411
|
} catch {
|
|
9188
10412
|
return "unknown";
|
|
9189
10413
|
}
|
|
@@ -9766,11 +10990,12 @@ async function answerBenchmarkQuestion(options) {
|
|
|
9766
10990
|
}
|
|
9767
10991
|
const answerMode = options.answerMode ?? "default";
|
|
9768
10992
|
const answerFormat = options.answerFormat === "auto" || options.answerFormat === void 0 ? inferAnswerFormat(options.question) : options.answerFormat;
|
|
9769
|
-
const
|
|
10993
|
+
const baseQuestion = answerMode === "strict" ? buildStrictBenchmarkQuestion(options.question, answerFormat) : answerMode === "agentic-memory" ? buildAgenticMemoryBenchmarkQuestion(
|
|
9770
10994
|
options.question,
|
|
9771
10995
|
answerFormat,
|
|
9772
10996
|
options.questionContext
|
|
9773
10997
|
) : options.question;
|
|
10998
|
+
const question = shouldInstructRecallAbstention(options.recallSupport) ? buildRecallAbstentionQuestion(baseQuestion, options.recallSupport) : baseQuestion;
|
|
9774
10999
|
const response = await options.responder.respond(
|
|
9775
11000
|
question,
|
|
9776
11001
|
options.recalledText
|
|
@@ -9793,6 +11018,19 @@ async function answerBenchmarkQuestion(options) {
|
|
|
9793
11018
|
model: finalResponse.model ?? response.model
|
|
9794
11019
|
};
|
|
9795
11020
|
}
|
|
11021
|
+
function shouldInstructRecallAbstention(support) {
|
|
11022
|
+
return support?.status === "empty" || support?.status === "weak";
|
|
11023
|
+
}
|
|
11024
|
+
function buildRecallAbstentionQuestion(question, support) {
|
|
11025
|
+
const description = support.status === "empty" ? "the successful recall returned no evidence" : "the recall adapter explicitly classified the supplied evidence as weak";
|
|
11026
|
+
return [
|
|
11027
|
+
question,
|
|
11028
|
+
"",
|
|
11029
|
+
"Recall support gate:",
|
|
11030
|
+
`- ${description}.`,
|
|
11031
|
+
'- Do not infer, guess, or synthesize an answer. Answer exactly "unknown".'
|
|
11032
|
+
].join("\n");
|
|
11033
|
+
}
|
|
9796
11034
|
function buildAgenticMemoryBenchmarkQuestion(question, answerFormat = "auto", context) {
|
|
9797
11035
|
const prompt = buildStrictBenchmarkQuestion(question, answerFormat);
|
|
9798
11036
|
return [
|
|
@@ -10402,6 +11640,592 @@ function asStringArray(value) {
|
|
|
10402
11640
|
|
|
10403
11641
|
// src/responders.ts
|
|
10404
11642
|
import { FallbackLlmClient } from "@remnic/core";
|
|
11643
|
+
|
|
11644
|
+
// src/judges/memcorrect-rubrics.ts
|
|
11645
|
+
var OPENAI_RESPONSES_JUDGE_RUBRIC_VERSION = "openai-responses-bench-v1";
|
|
11646
|
+
var MEMCORRECT_CORRECTION_ACCEPTANCE_RUBRIC_VERSION = "memcorrect-correction-acceptance-v1";
|
|
11647
|
+
var MEMCORRECT_STALE_HARM_RUBRIC_VERSION = "memcorrect-stale-memory-harm-v1";
|
|
11648
|
+
var GENERAL_ANSWER_JUDGE_RUBRIC = [
|
|
11649
|
+
"Grade the predicted benchmark answer against the reference answer.",
|
|
11650
|
+
"A score of 1 means fully correct, 0 means fully incorrect, and values between 0 and 1 mean partially correct.",
|
|
11651
|
+
"Do not reward unsupported claims. Ignore harmless wording and formatting differences."
|
|
11652
|
+
].join(" ");
|
|
11653
|
+
var MEMCORRECT_CORRECTION_ACCEPTANCE_RUBRIC = [
|
|
11654
|
+
"Decide whether the memory system accepted the user's correction.",
|
|
11655
|
+
"Pass only when the corrected fact is reflected and the retired fact is not asserted as current.",
|
|
11656
|
+
"A quotation or correction-history record that clearly marks the retired fact as obsolete is not stale recall.",
|
|
11657
|
+
"Use a score of 1 for accepted, 0 for not accepted, and a fractional score only when the evidence is genuinely ambiguous."
|
|
11658
|
+
].join(" ");
|
|
11659
|
+
var MEMCORRECT_STALE_HARM_RUBRIC = [
|
|
11660
|
+
"Decide whether the memory system avoided stale-memory harm after a correction.",
|
|
11661
|
+
"Pass only when the answer does not present the retired fact as current and does not act on it.",
|
|
11662
|
+
"Correction provenance may mention the retired fact only when it is explicitly identified as obsolete.",
|
|
11663
|
+
"Use a score of 1 when no stale harm is present, 0 when stale information affected the answer, and a fractional score only for genuinely ambiguous evidence."
|
|
11664
|
+
].join(" ");
|
|
11665
|
+
|
|
11666
|
+
// src/providers/openai-responses.ts
|
|
11667
|
+
var DEFAULT_OPENAI_RESPONSES_JUDGE_MODEL = "gpt-5.6";
|
|
11668
|
+
var VERDICT_JSON_SCHEMA = {
|
|
11669
|
+
type: "object",
|
|
11670
|
+
additionalProperties: false,
|
|
11671
|
+
required: ["score", "decision", "reason"],
|
|
11672
|
+
properties: {
|
|
11673
|
+
score: { type: "number", minimum: 0, maximum: 1 },
|
|
11674
|
+
decision: { type: "string", enum: ["pass", "partial", "fail"] },
|
|
11675
|
+
reason: { type: "string" }
|
|
11676
|
+
}
|
|
11677
|
+
};
|
|
11678
|
+
var ASSISTANT_RUBRIC_JSON_SCHEMA = {
|
|
11679
|
+
type: "object",
|
|
11680
|
+
additionalProperties: false,
|
|
11681
|
+
required: ["identity_accuracy", "stance_coherence", "novelty", "calibration", "notes"],
|
|
11682
|
+
properties: {
|
|
11683
|
+
identity_accuracy: { type: "number", minimum: 0, maximum: 5 },
|
|
11684
|
+
stance_coherence: { type: "number", minimum: 0, maximum: 5 },
|
|
11685
|
+
novelty: { type: "number", minimum: 0, maximum: 5 },
|
|
11686
|
+
calibration: { type: "number", minimum: 0, maximum: 5 },
|
|
11687
|
+
notes: { type: "string" }
|
|
11688
|
+
}
|
|
11689
|
+
};
|
|
11690
|
+
var OpenAiResponsesJudgeError = class extends Error {
|
|
11691
|
+
code;
|
|
11692
|
+
retryable;
|
|
11693
|
+
httpStatus;
|
|
11694
|
+
telemetry;
|
|
11695
|
+
constructor(failure) {
|
|
11696
|
+
super(failure.error.message);
|
|
11697
|
+
this.name = "OpenAiResponsesJudgeError";
|
|
11698
|
+
this.code = failure.error.code;
|
|
11699
|
+
this.retryable = failure.error.retryable;
|
|
11700
|
+
this.httpStatus = failure.error.httpStatus;
|
|
11701
|
+
this.telemetry = failure.telemetry;
|
|
11702
|
+
}
|
|
11703
|
+
};
|
|
11704
|
+
var OpenAiResponsesProvider = class {
|
|
11705
|
+
provider = "openai";
|
|
11706
|
+
id;
|
|
11707
|
+
name;
|
|
11708
|
+
rubricVersion;
|
|
11709
|
+
config;
|
|
11710
|
+
usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
11711
|
+
telemetryEvents = [];
|
|
11712
|
+
constructor(config = {}) {
|
|
11713
|
+
const model = normalizeModel(config.model);
|
|
11714
|
+
this.config = { ...config, model };
|
|
11715
|
+
this.id = `openai-responses:${model}`;
|
|
11716
|
+
this.name = model;
|
|
11717
|
+
this.rubricVersion = config.rubricVersion ?? OPENAI_RESPONSES_JUDGE_RUBRIC_VERSION;
|
|
11718
|
+
}
|
|
11719
|
+
async complete(prompt, opts = {}) {
|
|
11720
|
+
const startedAt = performance.now();
|
|
11721
|
+
let response;
|
|
11722
|
+
try {
|
|
11723
|
+
response = await retryFetch(
|
|
11724
|
+
this.responsesUrl(),
|
|
11725
|
+
{
|
|
11726
|
+
method: "POST",
|
|
11727
|
+
headers: this.headers(opts.headers),
|
|
11728
|
+
signal: opts.signal,
|
|
11729
|
+
body: JSON.stringify({
|
|
11730
|
+
model: this.config.model,
|
|
11731
|
+
...opts.systemPrompt ? { instructions: opts.systemPrompt } : {},
|
|
11732
|
+
input: prompt,
|
|
11733
|
+
temperature: opts.temperature ?? this.config.temperature,
|
|
11734
|
+
...this.config.seed !== void 0 ? { seed: this.config.seed } : {},
|
|
11735
|
+
...opts.maxTokens !== void 0 ? { max_output_tokens: opts.maxTokens } : {},
|
|
11736
|
+
store: false
|
|
11737
|
+
})
|
|
11738
|
+
},
|
|
11739
|
+
this.config.retryOptions
|
|
11740
|
+
);
|
|
11741
|
+
} catch (error) {
|
|
11742
|
+
throw this.asTransportError(error, startedAt, this.rubricVersion);
|
|
11743
|
+
}
|
|
11744
|
+
const parsed = await this.parseResponse(response, startedAt, this.rubricVersion);
|
|
11745
|
+
this.recordUsage(parsed.telemetry.inputTokens, parsed.telemetry.outputTokens);
|
|
11746
|
+
if (!parsed.ok) {
|
|
11747
|
+
this.recordTelemetry(parsed.telemetry);
|
|
11748
|
+
throw new OpenAiResponsesJudgeError(parsed);
|
|
11749
|
+
}
|
|
11750
|
+
const text = parsed.text;
|
|
11751
|
+
if (text === null) {
|
|
11752
|
+
const failure = this.failure(
|
|
11753
|
+
"malformed_response",
|
|
11754
|
+
"OpenAI Responses API returned no text output.",
|
|
11755
|
+
startedAt,
|
|
11756
|
+
{
|
|
11757
|
+
response,
|
|
11758
|
+
payload: parsed.payload,
|
|
11759
|
+
rubricVersion: this.rubricVersion
|
|
11760
|
+
}
|
|
11761
|
+
);
|
|
11762
|
+
this.recordTelemetry(failure.telemetry);
|
|
11763
|
+
throw new OpenAiResponsesJudgeError(failure);
|
|
11764
|
+
}
|
|
11765
|
+
this.recordTelemetry(parsed.telemetry);
|
|
11766
|
+
return {
|
|
11767
|
+
text,
|
|
11768
|
+
tokens: {
|
|
11769
|
+
input: parsed.telemetry.inputTokens,
|
|
11770
|
+
output: parsed.telemetry.outputTokens
|
|
11771
|
+
},
|
|
11772
|
+
latencyMs: parsed.telemetry.latencyMs,
|
|
11773
|
+
model: parsed.telemetry.model
|
|
11774
|
+
};
|
|
11775
|
+
}
|
|
11776
|
+
async judge(request) {
|
|
11777
|
+
const startedAt = performance.now();
|
|
11778
|
+
let response;
|
|
11779
|
+
try {
|
|
11780
|
+
response = await retryFetch(
|
|
11781
|
+
this.responsesUrl(),
|
|
11782
|
+
{
|
|
11783
|
+
method: "POST",
|
|
11784
|
+
headers: this.headers(),
|
|
11785
|
+
signal: request.signal,
|
|
11786
|
+
body: JSON.stringify({
|
|
11787
|
+
model: this.config.model,
|
|
11788
|
+
instructions: request.rubric,
|
|
11789
|
+
input: request.input,
|
|
11790
|
+
temperature: this.config.temperature,
|
|
11791
|
+
...this.config.seed !== void 0 ? { seed: this.config.seed } : {},
|
|
11792
|
+
text: {
|
|
11793
|
+
format: {
|
|
11794
|
+
type: "json_schema",
|
|
11795
|
+
name: "benchmark_verdict",
|
|
11796
|
+
description: "A normalized benchmark grading verdict.",
|
|
11797
|
+
strict: true,
|
|
11798
|
+
schema: VERDICT_JSON_SCHEMA
|
|
11799
|
+
}
|
|
11800
|
+
},
|
|
11801
|
+
max_output_tokens: request.maxTokens ?? 256,
|
|
11802
|
+
store: false
|
|
11803
|
+
})
|
|
11804
|
+
},
|
|
11805
|
+
this.config.retryOptions
|
|
11806
|
+
);
|
|
11807
|
+
} catch (error) {
|
|
11808
|
+
const failure = this.transportFailure(error, startedAt, request.rubricVersion);
|
|
11809
|
+
this.recordTelemetry(failure.telemetry);
|
|
11810
|
+
return failure;
|
|
11811
|
+
}
|
|
11812
|
+
const parsed = await this.parseResponse(response, startedAt, request.rubricVersion);
|
|
11813
|
+
this.recordUsage(parsed.telemetry.inputTokens, parsed.telemetry.outputTokens);
|
|
11814
|
+
if (!parsed.ok) {
|
|
11815
|
+
this.recordTelemetry(parsed.telemetry);
|
|
11816
|
+
return parsed;
|
|
11817
|
+
}
|
|
11818
|
+
if (parsed.text === null) {
|
|
11819
|
+
const failure = this.failure(
|
|
11820
|
+
"malformed_response",
|
|
11821
|
+
"OpenAI Responses API returned no structured verdict text.",
|
|
11822
|
+
startedAt,
|
|
11823
|
+
{ response, payload: parsed.payload, rubricVersion: request.rubricVersion }
|
|
11824
|
+
);
|
|
11825
|
+
this.recordTelemetry(failure.telemetry);
|
|
11826
|
+
return failure;
|
|
11827
|
+
}
|
|
11828
|
+
const verdict = parseVerdict(parsed.text);
|
|
11829
|
+
if (!verdict) {
|
|
11830
|
+
const failure = this.failure(
|
|
11831
|
+
"malformed_verdict",
|
|
11832
|
+
"OpenAI Responses API returned a verdict that failed schema validation.",
|
|
11833
|
+
startedAt,
|
|
11834
|
+
{ response, payload: parsed.payload, rubricVersion: request.rubricVersion }
|
|
11835
|
+
);
|
|
11836
|
+
this.recordTelemetry(failure.telemetry);
|
|
11837
|
+
return failure;
|
|
11838
|
+
}
|
|
11839
|
+
this.recordTelemetry(parsed.telemetry);
|
|
11840
|
+
return { ok: true, verdict, telemetry: parsed.telemetry };
|
|
11841
|
+
}
|
|
11842
|
+
async evaluateAssistantRubric(request) {
|
|
11843
|
+
const rubricVersion = `sealed:${request.rubricId}`;
|
|
11844
|
+
const startedAt = performance.now();
|
|
11845
|
+
let response;
|
|
11846
|
+
try {
|
|
11847
|
+
response = await retryFetch(
|
|
11848
|
+
this.responsesUrl(),
|
|
11849
|
+
{
|
|
11850
|
+
method: "POST",
|
|
11851
|
+
headers: this.headers(),
|
|
11852
|
+
body: JSON.stringify({
|
|
11853
|
+
model: this.config.model,
|
|
11854
|
+
instructions: request.system,
|
|
11855
|
+
input: request.user,
|
|
11856
|
+
temperature: this.config.temperature,
|
|
11857
|
+
...this.config.seed !== void 0 ? { seed: this.config.seed } : {},
|
|
11858
|
+
text: {
|
|
11859
|
+
format: {
|
|
11860
|
+
type: "json_schema",
|
|
11861
|
+
name: "sealed_assistant_rubric",
|
|
11862
|
+
description: "Scores for every sealed assistant-rubric dimension.",
|
|
11863
|
+
strict: true,
|
|
11864
|
+
schema: ASSISTANT_RUBRIC_JSON_SCHEMA
|
|
11865
|
+
}
|
|
11866
|
+
},
|
|
11867
|
+
max_output_tokens: 512,
|
|
11868
|
+
store: false
|
|
11869
|
+
})
|
|
11870
|
+
},
|
|
11871
|
+
this.config.retryOptions
|
|
11872
|
+
);
|
|
11873
|
+
} catch (error) {
|
|
11874
|
+
throw this.asTransportError(error, startedAt, rubricVersion);
|
|
11875
|
+
}
|
|
11876
|
+
const parsed = await this.parseResponse(response, startedAt, rubricVersion);
|
|
11877
|
+
this.recordUsage(parsed.telemetry.inputTokens, parsed.telemetry.outputTokens);
|
|
11878
|
+
if (!parsed.ok) {
|
|
11879
|
+
this.recordTelemetry(parsed.telemetry);
|
|
11880
|
+
throw new OpenAiResponsesJudgeError(parsed);
|
|
11881
|
+
}
|
|
11882
|
+
if (parsed.text === null || !parseAssistantRubric(parsed.text)) {
|
|
11883
|
+
const failure = this.failure(
|
|
11884
|
+
"malformed_verdict",
|
|
11885
|
+
"OpenAI Responses API returned an invalid sealed assistant-rubric verdict.",
|
|
11886
|
+
startedAt,
|
|
11887
|
+
{ response, payload: parsed.payload, rubricVersion }
|
|
11888
|
+
);
|
|
11889
|
+
this.recordTelemetry(failure.telemetry);
|
|
11890
|
+
throw new OpenAiResponsesJudgeError(failure);
|
|
11891
|
+
}
|
|
11892
|
+
this.recordTelemetry(parsed.telemetry);
|
|
11893
|
+
return parsed.text;
|
|
11894
|
+
}
|
|
11895
|
+
getUsage() {
|
|
11896
|
+
return { ...this.usage };
|
|
11897
|
+
}
|
|
11898
|
+
resetUsage() {
|
|
11899
|
+
this.usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
11900
|
+
}
|
|
11901
|
+
getTelemetryEvents() {
|
|
11902
|
+
return this.telemetryEvents.map((event) => ({ ...event }));
|
|
11903
|
+
}
|
|
11904
|
+
async parseResponse(response, startedAt, rubricVersion) {
|
|
11905
|
+
let payload;
|
|
11906
|
+
try {
|
|
11907
|
+
payload = await response.json();
|
|
11908
|
+
} catch {
|
|
11909
|
+
if (!response.ok) {
|
|
11910
|
+
return this.failure(
|
|
11911
|
+
response.status === 429 ? "rate_limited" : "api_error",
|
|
11912
|
+
`OpenAI Responses API request failed with HTTP ${response.status}.`,
|
|
11913
|
+
startedAt,
|
|
11914
|
+
{ response, rubricVersion }
|
|
11915
|
+
);
|
|
11916
|
+
}
|
|
11917
|
+
return this.failure(
|
|
11918
|
+
"malformed_response",
|
|
11919
|
+
"OpenAI Responses API returned non-JSON data.",
|
|
11920
|
+
startedAt,
|
|
11921
|
+
{ response, rubricVersion }
|
|
11922
|
+
);
|
|
11923
|
+
}
|
|
11924
|
+
if (!response.ok) {
|
|
11925
|
+
const code = response.status === 429 ? "rate_limited" : "api_error";
|
|
11926
|
+
return this.failure(
|
|
11927
|
+
code,
|
|
11928
|
+
`OpenAI Responses API request failed with HTTP ${response.status}.`,
|
|
11929
|
+
startedAt,
|
|
11930
|
+
{ response, payload, rubricVersion }
|
|
11931
|
+
);
|
|
11932
|
+
}
|
|
11933
|
+
if (payload.error || payload.status === "failed" || payload.status === "cancelled") {
|
|
11934
|
+
return this.failure(
|
|
11935
|
+
"api_error",
|
|
11936
|
+
`OpenAI Responses API reported ${payload.error?.code ?? payload.status ?? "an error"}.`,
|
|
11937
|
+
startedAt,
|
|
11938
|
+
{ response, payload, rubricVersion }
|
|
11939
|
+
);
|
|
11940
|
+
}
|
|
11941
|
+
if (payload.status === "incomplete") {
|
|
11942
|
+
return this.failure(
|
|
11943
|
+
"incomplete_response",
|
|
11944
|
+
`OpenAI Responses API response was incomplete (${payload.incomplete_details?.reason ?? "unknown reason"}).`,
|
|
11945
|
+
startedAt,
|
|
11946
|
+
{ response, payload, rubricVersion }
|
|
11947
|
+
);
|
|
11948
|
+
}
|
|
11949
|
+
const refusal = readRefusal(payload);
|
|
11950
|
+
if (refusal !== null) {
|
|
11951
|
+
return this.failure(
|
|
11952
|
+
"refusal",
|
|
11953
|
+
"OpenAI Responses API refused the benchmark grading request.",
|
|
11954
|
+
startedAt,
|
|
11955
|
+
{ response, payload, rubricVersion }
|
|
11956
|
+
);
|
|
11957
|
+
}
|
|
11958
|
+
return {
|
|
11959
|
+
ok: true,
|
|
11960
|
+
payload,
|
|
11961
|
+
text: readOutputText(payload),
|
|
11962
|
+
telemetry: this.telemetry(payload, startedAt, rubricVersion)
|
|
11963
|
+
};
|
|
11964
|
+
}
|
|
11965
|
+
failure(code, message, startedAt, context) {
|
|
11966
|
+
const telemetry = this.telemetry(
|
|
11967
|
+
context.payload,
|
|
11968
|
+
startedAt,
|
|
11969
|
+
context.rubricVersion,
|
|
11970
|
+
code,
|
|
11971
|
+
context.response?.status
|
|
11972
|
+
);
|
|
11973
|
+
return {
|
|
11974
|
+
ok: false,
|
|
11975
|
+
error: {
|
|
11976
|
+
code,
|
|
11977
|
+
message,
|
|
11978
|
+
retryable: code === "rate_limited" || code === "transport_error",
|
|
11979
|
+
...context.response ? { httpStatus: context.response.status } : {}
|
|
11980
|
+
},
|
|
11981
|
+
telemetry
|
|
11982
|
+
};
|
|
11983
|
+
}
|
|
11984
|
+
transportFailure(error, startedAt, rubricVersion) {
|
|
11985
|
+
if (isAbortError2(error)) {
|
|
11986
|
+
return this.failure(
|
|
11987
|
+
"aborted",
|
|
11988
|
+
"OpenAI Responses API request was aborted by the caller.",
|
|
11989
|
+
startedAt,
|
|
11990
|
+
{ rubricVersion }
|
|
11991
|
+
);
|
|
11992
|
+
}
|
|
11993
|
+
if (error instanceof RetryFetchHttpError) {
|
|
11994
|
+
return this.failure(
|
|
11995
|
+
error.status === 429 ? "rate_limited" : "api_error",
|
|
11996
|
+
`OpenAI Responses API request failed with HTTP ${error.status} after retries.`,
|
|
11997
|
+
startedAt,
|
|
11998
|
+
{
|
|
11999
|
+
response: new Response(null, { status: error.status }),
|
|
12000
|
+
rubricVersion
|
|
12001
|
+
}
|
|
12002
|
+
);
|
|
12003
|
+
}
|
|
12004
|
+
return this.failure(
|
|
12005
|
+
"transport_error",
|
|
12006
|
+
`OpenAI Responses API transport failed (${safeErrorName(error)}).`,
|
|
12007
|
+
startedAt,
|
|
12008
|
+
{ rubricVersion }
|
|
12009
|
+
);
|
|
12010
|
+
}
|
|
12011
|
+
asTransportError(error, startedAt, rubricVersion) {
|
|
12012
|
+
const failure = this.transportFailure(error, startedAt, rubricVersion);
|
|
12013
|
+
this.recordTelemetry(failure.telemetry);
|
|
12014
|
+
return new OpenAiResponsesJudgeError(failure);
|
|
12015
|
+
}
|
|
12016
|
+
telemetry(payload, startedAt, rubricVersion, errorCode, httpStatus) {
|
|
12017
|
+
const tokens = readTokens(payload);
|
|
12018
|
+
return {
|
|
12019
|
+
model: payload?.model ?? this.config.model,
|
|
12020
|
+
rubricVersion,
|
|
12021
|
+
inputTokens: tokens.input,
|
|
12022
|
+
outputTokens: tokens.output,
|
|
12023
|
+
latencyMs: Math.round(performance.now() - startedAt),
|
|
12024
|
+
...errorCode ? { errorCode } : {},
|
|
12025
|
+
...httpStatus !== void 0 ? { httpStatus } : {}
|
|
12026
|
+
};
|
|
12027
|
+
}
|
|
12028
|
+
recordTelemetry(event) {
|
|
12029
|
+
this.telemetryEvents.push({ ...event });
|
|
12030
|
+
}
|
|
12031
|
+
recordUsage(input, output) {
|
|
12032
|
+
this.usage = {
|
|
12033
|
+
inputTokens: this.usage.inputTokens + input,
|
|
12034
|
+
outputTokens: this.usage.outputTokens + output,
|
|
12035
|
+
totalTokens: this.usage.totalTokens + input + output
|
|
12036
|
+
};
|
|
12037
|
+
}
|
|
12038
|
+
responsesUrl() {
|
|
12039
|
+
const baseUrl = trimTrailingSlashes(this.config.baseUrl ?? "https://api.openai.com/v1");
|
|
12040
|
+
return `${baseUrl}/responses`;
|
|
12041
|
+
}
|
|
12042
|
+
headers(extra = {}) {
|
|
12043
|
+
return {
|
|
12044
|
+
"content-type": "application/json",
|
|
12045
|
+
...this.config.apiKey ? { authorization: `Bearer ${this.config.apiKey}` } : {},
|
|
12046
|
+
...this.config.headers ?? {},
|
|
12047
|
+
...extra
|
|
12048
|
+
};
|
|
12049
|
+
}
|
|
12050
|
+
};
|
|
12051
|
+
function createOpenAiResponsesProvider(config = {}) {
|
|
12052
|
+
return new OpenAiResponsesProvider(config);
|
|
12053
|
+
}
|
|
12054
|
+
function createOpenAiResponsesBenchJudge(config = {}, provider = createOpenAiResponsesProvider(config)) {
|
|
12055
|
+
const scoreWithMetrics = async (question, predicted, expected, control) => {
|
|
12056
|
+
const result = await provider.judge({
|
|
12057
|
+
rubric: GENERAL_ANSWER_JUDGE_RUBRIC,
|
|
12058
|
+
rubricVersion: config.rubricVersion ?? OPENAI_RESPONSES_JUDGE_RUBRIC_VERSION,
|
|
12059
|
+
input: [
|
|
12060
|
+
`QUESTION: ${question}`,
|
|
12061
|
+
`REFERENCE_ANSWER: ${expected}`,
|
|
12062
|
+
`PREDICTED_ANSWER: ${predicted}`
|
|
12063
|
+
].join("\n\n"),
|
|
12064
|
+
signal: control?.signal
|
|
12065
|
+
});
|
|
12066
|
+
if (!result.ok) throw new OpenAiResponsesJudgeError(result);
|
|
12067
|
+
return toBenchJudgeResult(result);
|
|
12068
|
+
};
|
|
12069
|
+
const scoreBinaryPrompt = async (prompt, control) => {
|
|
12070
|
+
const result = await provider.judge({
|
|
12071
|
+
rubric: `${GENERAL_ANSWER_JUDGE_RUBRIC} This evaluator is binary: score must be exactly 0 or 1.`,
|
|
12072
|
+
rubricVersion: config.rubricVersion ?? OPENAI_RESPONSES_JUDGE_RUBRIC_VERSION,
|
|
12073
|
+
input: prompt,
|
|
12074
|
+
signal: control?.signal
|
|
12075
|
+
});
|
|
12076
|
+
if (!result.ok) throw new OpenAiResponsesJudgeError(result);
|
|
12077
|
+
if (result.verdict.score !== 0 && result.verdict.score !== 1) {
|
|
12078
|
+
const failure = {
|
|
12079
|
+
ok: false,
|
|
12080
|
+
error: {
|
|
12081
|
+
code: "malformed_verdict",
|
|
12082
|
+
message: "OpenAI Responses API returned a non-binary verdict for a binary rubric.",
|
|
12083
|
+
retryable: false
|
|
12084
|
+
},
|
|
12085
|
+
telemetry: { ...result.telemetry, errorCode: "malformed_verdict" }
|
|
12086
|
+
};
|
|
12087
|
+
throw new OpenAiResponsesJudgeError(failure);
|
|
12088
|
+
}
|
|
12089
|
+
return toBenchJudgeResult(result);
|
|
12090
|
+
};
|
|
12091
|
+
const judgeSpecialized = async (request, rubric, control) => {
|
|
12092
|
+
const result = rubric === "correction" ? await judgeMemCorrectCorrectionAcceptance(provider, serializeMemCorrectJudgeRequest(request), control?.signal) : await judgeMemCorrectStaleMemoryHarm(provider, serializeMemCorrectJudgeRequest(request), control?.signal);
|
|
12093
|
+
if (!result.ok) throw new OpenAiResponsesJudgeError(result);
|
|
12094
|
+
return {
|
|
12095
|
+
...toBenchJudgeResult(result),
|
|
12096
|
+
decision: result.verdict.decision,
|
|
12097
|
+
reason: result.verdict.reason,
|
|
12098
|
+
rubricVersion: result.telemetry.rubricVersion
|
|
12099
|
+
};
|
|
12100
|
+
};
|
|
12101
|
+
return {
|
|
12102
|
+
async score(question, predicted, expected, control) {
|
|
12103
|
+
return (await scoreWithMetrics(question, predicted, expected, control)).score;
|
|
12104
|
+
},
|
|
12105
|
+
scoreWithMetrics,
|
|
12106
|
+
scoreBinaryPrompt,
|
|
12107
|
+
judgeMemCorrectCorrectionAcceptance: (request, control) => judgeSpecialized(request, "correction", control),
|
|
12108
|
+
judgeMemCorrectStaleMemoryHarm: (request, control) => judgeSpecialized(request, "stale_harm", control)
|
|
12109
|
+
};
|
|
12110
|
+
}
|
|
12111
|
+
async function judgeMemCorrectCorrectionAcceptance(provider, input, signal) {
|
|
12112
|
+
return provider.judge({
|
|
12113
|
+
rubric: MEMCORRECT_CORRECTION_ACCEPTANCE_RUBRIC,
|
|
12114
|
+
rubricVersion: MEMCORRECT_CORRECTION_ACCEPTANCE_RUBRIC_VERSION,
|
|
12115
|
+
input,
|
|
12116
|
+
signal
|
|
12117
|
+
});
|
|
12118
|
+
}
|
|
12119
|
+
async function judgeMemCorrectStaleMemoryHarm(provider, input, signal) {
|
|
12120
|
+
return provider.judge({
|
|
12121
|
+
rubric: MEMCORRECT_STALE_HARM_RUBRIC,
|
|
12122
|
+
rubricVersion: MEMCORRECT_STALE_HARM_RUBRIC_VERSION,
|
|
12123
|
+
input,
|
|
12124
|
+
signal
|
|
12125
|
+
});
|
|
12126
|
+
}
|
|
12127
|
+
function toBenchJudgeResult(result) {
|
|
12128
|
+
return {
|
|
12129
|
+
score: result.verdict.score,
|
|
12130
|
+
tokens: {
|
|
12131
|
+
input: result.telemetry.inputTokens,
|
|
12132
|
+
output: result.telemetry.outputTokens
|
|
12133
|
+
},
|
|
12134
|
+
latencyMs: result.telemetry.latencyMs,
|
|
12135
|
+
model: result.telemetry.model
|
|
12136
|
+
};
|
|
12137
|
+
}
|
|
12138
|
+
function normalizeModel(model) {
|
|
12139
|
+
if (model === void 0) return DEFAULT_OPENAI_RESPONSES_JUDGE_MODEL;
|
|
12140
|
+
const trimmed = model.trim();
|
|
12141
|
+
if (trimmed.length === 0) {
|
|
12142
|
+
throw new Error("OpenAI Responses judge model must be a non-empty string");
|
|
12143
|
+
}
|
|
12144
|
+
return trimmed;
|
|
12145
|
+
}
|
|
12146
|
+
function parseVerdict(text) {
|
|
12147
|
+
let parsed;
|
|
12148
|
+
try {
|
|
12149
|
+
parsed = JSON.parse(text);
|
|
12150
|
+
} catch {
|
|
12151
|
+
return null;
|
|
12152
|
+
}
|
|
12153
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
12154
|
+
const candidate = parsed;
|
|
12155
|
+
const keys = Object.keys(candidate).sort();
|
|
12156
|
+
if (keys.join(",") !== "decision,reason,score") return null;
|
|
12157
|
+
if (typeof candidate.score !== "number" || !Number.isFinite(candidate.score) || candidate.score < 0 || candidate.score > 1 || candidate.decision !== "pass" && candidate.decision !== "partial" && candidate.decision !== "fail" || typeof candidate.reason !== "string" || candidate.reason.trim().length === 0) {
|
|
12158
|
+
return null;
|
|
12159
|
+
}
|
|
12160
|
+
return {
|
|
12161
|
+
score: candidate.score,
|
|
12162
|
+
decision: candidate.decision,
|
|
12163
|
+
reason: candidate.reason.trim()
|
|
12164
|
+
};
|
|
12165
|
+
}
|
|
12166
|
+
function parseAssistantRubric(text) {
|
|
12167
|
+
let parsed;
|
|
12168
|
+
try {
|
|
12169
|
+
parsed = JSON.parse(text);
|
|
12170
|
+
} catch {
|
|
12171
|
+
return false;
|
|
12172
|
+
}
|
|
12173
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
|
|
12174
|
+
const candidate = parsed;
|
|
12175
|
+
const keys = Object.keys(candidate).sort();
|
|
12176
|
+
if (keys.join(",") !== "calibration,identity_accuracy,notes,novelty,stance_coherence") return false;
|
|
12177
|
+
return ["identity_accuracy", "stance_coherence", "novelty", "calibration"].every(
|
|
12178
|
+
(key) => typeof candidate[key] === "number" && Number.isFinite(candidate[key]) && candidate[key] >= 0 && candidate[key] <= 5
|
|
12179
|
+
) && typeof candidate.notes === "string";
|
|
12180
|
+
}
|
|
12181
|
+
function serializeMemCorrectJudgeRequest(request) {
|
|
12182
|
+
return JSON.stringify({
|
|
12183
|
+
taskId: request.taskId,
|
|
12184
|
+
query: request.query,
|
|
12185
|
+
retiredContent: request.retiredContent,
|
|
12186
|
+
correctedContent: request.correctedContent,
|
|
12187
|
+
evidence: {
|
|
12188
|
+
postCorrectionRecall: request.postCorrectionRecall,
|
|
12189
|
+
postMaintenanceRecall: request.postMaintenanceRecall,
|
|
12190
|
+
postReingestRecall: request.postReingestRecall
|
|
12191
|
+
}
|
|
12192
|
+
});
|
|
12193
|
+
}
|
|
12194
|
+
function readOutputText(payload) {
|
|
12195
|
+
const text = (payload.output ?? []).flatMap((item) => item.type === "message" ? item.content ?? [] : []).filter((part) => part.type === "output_text").map((part) => part.text ?? "").join("").trim();
|
|
12196
|
+
return text.length > 0 ? text : null;
|
|
12197
|
+
}
|
|
12198
|
+
function readRefusal(payload) {
|
|
12199
|
+
for (const item of payload.output ?? []) {
|
|
12200
|
+
if (item.type !== "message") continue;
|
|
12201
|
+
for (const part of item.content ?? []) {
|
|
12202
|
+
if (part.type === "refusal") return part.refusal ?? "refused";
|
|
12203
|
+
}
|
|
12204
|
+
}
|
|
12205
|
+
return null;
|
|
12206
|
+
}
|
|
12207
|
+
function readTokens(payload) {
|
|
12208
|
+
return {
|
|
12209
|
+
input: finiteNonNegative(payload?.usage?.input_tokens),
|
|
12210
|
+
output: finiteNonNegative(payload?.usage?.output_tokens)
|
|
12211
|
+
};
|
|
12212
|
+
}
|
|
12213
|
+
function finiteNonNegative(value) {
|
|
12214
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
12215
|
+
}
|
|
12216
|
+
function safeErrorName(error) {
|
|
12217
|
+
return error instanceof Error && error.name.trim().length > 0 ? error.name : "Error";
|
|
12218
|
+
}
|
|
12219
|
+
function isAbortError2(error) {
|
|
12220
|
+
return error instanceof Error && error.name === "AbortError";
|
|
12221
|
+
}
|
|
12222
|
+
function trimTrailingSlashes(value) {
|
|
12223
|
+
let end = value.length;
|
|
12224
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
12225
|
+
return end === value.length ? value : value.slice(0, end);
|
|
12226
|
+
}
|
|
12227
|
+
|
|
12228
|
+
// src/responders.ts
|
|
10405
12229
|
var DEFAULT_RESPONDER_SYSTEM_PROMPT = [
|
|
10406
12230
|
"You answer benchmark questions using only the supplied Remnic memory context.",
|
|
10407
12231
|
"If the context does not contain enough information, say that the answer is unknown.",
|
|
@@ -10891,6 +12715,9 @@ function createJudgeFromProvider(provider) {
|
|
|
10891
12715
|
}
|
|
10892
12716
|
function createProviderBackedJudge(config, providerInstance) {
|
|
10893
12717
|
validateProviderConfig(config, "judge");
|
|
12718
|
+
if (config.provider === "openai" && providerInstance === void 0) {
|
|
12719
|
+
return createOpenAiResponsesBenchJudge({ ...config, provider: "openai" });
|
|
12720
|
+
}
|
|
10894
12721
|
return createJudgeFromProvider(providerInstance ?? createProvider(config));
|
|
10895
12722
|
}
|
|
10896
12723
|
function createAmaBenchRecommendedJudgeFromProvider(provider) {
|
|
@@ -10944,7 +12771,15 @@ function createStructuredJudgeFromProvider(provider) {
|
|
|
10944
12771
|
}
|
|
10945
12772
|
function createProviderBackedStructuredJudge(config, providerInstance) {
|
|
10946
12773
|
validateProviderConfig(config, "judge");
|
|
10947
|
-
|
|
12774
|
+
if (config.provider === "openai" && providerInstance === void 0) {
|
|
12775
|
+
const provider = createOpenAiResponsesProvider({ ...config, provider: "openai" });
|
|
12776
|
+
return {
|
|
12777
|
+
evaluate: (request) => provider.evaluateAssistantRubric(request)
|
|
12778
|
+
};
|
|
12779
|
+
}
|
|
12780
|
+
return createStructuredJudgeFromProvider(
|
|
12781
|
+
providerInstance ?? createProvider(config)
|
|
12782
|
+
);
|
|
10948
12783
|
}
|
|
10949
12784
|
function createGatewayResponder(options) {
|
|
10950
12785
|
if (!options.gatewayConfig) {
|
|
@@ -11769,10 +13604,10 @@ function manifestProviderKindToBuiltIn(kind) {
|
|
|
11769
13604
|
}
|
|
11770
13605
|
|
|
11771
13606
|
// src/local-lab/preflight.ts
|
|
11772
|
-
var
|
|
13607
|
+
var DEFAULT_PREFLIGHT_TIMEOUT_MS2 = 5e3;
|
|
11773
13608
|
async function preflightLocalLabRole(input, options = {}) {
|
|
11774
13609
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
11775
|
-
const timeoutMs = options.timeoutMs ??
|
|
13610
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PREFLIGHT_TIMEOUT_MS2;
|
|
11776
13611
|
const endpoint = discoveryEndpointFor(input.provider, input.baseUrl);
|
|
11777
13612
|
const controller = new AbortController();
|
|
11778
13613
|
const timer = setTimeout(() => controller.abort(new Error("preflight timeout")), timeoutMs);
|
|
@@ -12109,9 +13944,8 @@ async function resolveBenchRuntimeProfile(options) {
|
|
|
12109
13944
|
registerCodexCliFallbackRunnerIfNeeded(internalProvider);
|
|
12110
13945
|
const responderFactoryConfig = systemProvider ? asProviderFactoryConfig(systemProvider) : void 0;
|
|
12111
13946
|
const judgeFactoryConfig = judgeProvider ? asProviderFactoryConfig(judgeProvider) : void 0;
|
|
12112
|
-
const
|
|
12113
|
-
const
|
|
12114
|
-
const structuredJudge = judgeFactoryConfig ? createProviderBackedStructuredJudge(judgeFactoryConfig, judgeProviderInstance) : void 0;
|
|
13947
|
+
const judge = judgeFactoryConfig ? createProviderBackedJudge(judgeFactoryConfig) : void 0;
|
|
13948
|
+
const structuredJudge = judgeFactoryConfig ? createProviderBackedStructuredJudge(judgeFactoryConfig) : void 0;
|
|
12115
13949
|
if (profile === "baseline") {
|
|
12116
13950
|
const responder = responderFactoryConfig ? createProviderBackedResponder(responderFactoryConfig) : void 0;
|
|
12117
13951
|
const baselineConfig = buildBenchBaselineRemnicConfig();
|
|
@@ -12136,7 +13970,7 @@ async function resolveBenchRuntimeProfile(options) {
|
|
|
12136
13970
|
...drainTimeoutMs ? { drainTimeoutMs } : {}
|
|
12137
13971
|
},
|
|
12138
13972
|
systemProvider,
|
|
12139
|
-
judgeProvider,
|
|
13973
|
+
judgeProvider: judgeProvider ? sanitizeProviderConfig(judgeProvider) : null,
|
|
12140
13974
|
internalProvider: internalProvider ? sanitizeProviderConfig(internalProvider) : null
|
|
12141
13975
|
};
|
|
12142
13976
|
}
|
|
@@ -12145,6 +13979,10 @@ async function resolveBenchRuntimeProfile(options) {
|
|
|
12145
13979
|
const fileConfig = options.remnicConfigPath ? await loadRemnicConfigFile(options.remnicConfigPath) : {};
|
|
12146
13980
|
const realProfileOverrides = {
|
|
12147
13981
|
lcmEnabled: true,
|
|
13982
|
+
// Read-time faithfulness is on by default for the full-feature profile.
|
|
13983
|
+
// An explicit config value (including false-like strings) remains
|
|
13984
|
+
// authoritative so benchmark operators can perform reversible ablations.
|
|
13985
|
+
...fileConfig.answerSupportGate === void 0 ? { answerSupportGate: true } : {},
|
|
12148
13986
|
...options.modelSource ? { modelSource: options.modelSource } : {},
|
|
12149
13987
|
...options.gatewayAgentId ? { gatewayAgentId: options.gatewayAgentId } : {},
|
|
12150
13988
|
...options.fastGatewayAgentId ? { fastGatewayAgentId: options.fastGatewayAgentId } : {},
|
|
@@ -12175,7 +14013,7 @@ async function resolveBenchRuntimeProfile(options) {
|
|
|
12175
14013
|
...drainTimeoutMs ? { drainTimeoutMs } : {}
|
|
12176
14014
|
},
|
|
12177
14015
|
systemProvider,
|
|
12178
|
-
judgeProvider,
|
|
14016
|
+
judgeProvider: judgeProvider ? sanitizeProviderConfig(judgeProvider) : null,
|
|
12179
14017
|
internalProvider: internalProvider ? sanitizeProviderConfig(internalProvider) : null
|
|
12180
14018
|
};
|
|
12181
14019
|
}
|
|
@@ -12226,7 +14064,7 @@ async function resolveBenchRuntimeProfile(options) {
|
|
|
12226
14064
|
...drainTimeoutMs ? { drainTimeoutMs } : {}
|
|
12227
14065
|
},
|
|
12228
14066
|
systemProvider: null,
|
|
12229
|
-
judgeProvider,
|
|
14067
|
+
judgeProvider: judgeProvider ? sanitizeProviderConfig(judgeProvider) : null,
|
|
12230
14068
|
internalProvider: internalProvider ? sanitizeProviderConfig(internalProvider) : null
|
|
12231
14069
|
};
|
|
12232
14070
|
}
|
|
@@ -12284,7 +14122,9 @@ async function loadJsonObject(filePath, label) {
|
|
|
12284
14122
|
}
|
|
12285
14123
|
function resolveProviderConfig(kind, provider, model, baseUrl, requestTimeout, disableThinking, apiKey, max429WaitMs, reasoningEffort, responderContextBudgetChars, responderPromptBudgetChars) {
|
|
12286
14124
|
const hasProvider = typeof provider === "string";
|
|
12287
|
-
const
|
|
14125
|
+
const defaultOpenAiJudgeModel = kind === "judge" && provider === "openai" && model === void 0 ? DEFAULT_OPENAI_RESPONSES_JUDGE_MODEL : void 0;
|
|
14126
|
+
const resolvedModel = defaultOpenAiJudgeModel ?? model;
|
|
14127
|
+
const hasModel = typeof resolvedModel === "string" && resolvedModel.trim().length > 0;
|
|
12288
14128
|
const hasBaseUrl = typeof baseUrl === "string" && baseUrl.trim().length > 0;
|
|
12289
14129
|
const hasApiKey = typeof apiKey === "string" && apiKey.trim().length > 0;
|
|
12290
14130
|
const hasReasoningEffort = reasoningEffort !== void 0;
|
|
@@ -12313,7 +14153,8 @@ function resolveProviderConfig(kind, provider, model, baseUrl, requestTimeout, d
|
|
|
12313
14153
|
}
|
|
12314
14154
|
return {
|
|
12315
14155
|
provider,
|
|
12316
|
-
model:
|
|
14156
|
+
model: resolvedModel.trim(),
|
|
14157
|
+
...kind === "judge" && provider === "openai" ? { rubricVersion: OPENAI_RESPONSES_JUDGE_RUBRIC_VERSION } : {},
|
|
12317
14158
|
...hasBaseUrl ? { baseUrl: baseUrl.trim() } : {},
|
|
12318
14159
|
...hasApiKey ? { apiKey: apiKey.trim() } : {},
|
|
12319
14160
|
...requestTimeout != null || max429WaitMs != null ? { retryOptions: {
|
|
@@ -12588,7 +14429,8 @@ function asProviderFactoryConfig(config) {
|
|
|
12588
14429
|
...config.temperature !== void 0 ? { temperature: config.temperature } : {},
|
|
12589
14430
|
...config.seed !== void 0 ? { seed: config.seed } : {},
|
|
12590
14431
|
...config.responderContextBudgetChars !== void 0 ? { responderContextBudgetChars: config.responderContextBudgetChars } : {},
|
|
12591
|
-
...config.responderPromptBudgetChars !== void 0 ? { responderPromptBudgetChars: config.responderPromptBudgetChars } : {}
|
|
14432
|
+
...config.responderPromptBudgetChars !== void 0 ? { responderPromptBudgetChars: config.responderPromptBudgetChars } : {},
|
|
14433
|
+
...config.rubricVersion !== void 0 ? { rubricVersion: config.rubricVersion } : {}
|
|
12592
14434
|
};
|
|
12593
14435
|
}
|
|
12594
14436
|
function asNonEmptyString(value) {
|
|
@@ -12642,9 +14484,8 @@ async function resolveLocalLabRuntimeProfile(options) {
|
|
|
12642
14484
|
options
|
|
12643
14485
|
);
|
|
12644
14486
|
const judgeFactoryConfig = judgeProvider ? asProviderFactoryConfig(judgeProvider) : void 0;
|
|
12645
|
-
const
|
|
12646
|
-
const
|
|
12647
|
-
const structuredJudge = judgeFactoryConfig ? createProviderBackedStructuredJudge(judgeFactoryConfig, judgeProviderInstance) : void 0;
|
|
14487
|
+
const judge = judgeFactoryConfig ? createProviderBackedJudge(judgeFactoryConfig) : void 0;
|
|
14488
|
+
const structuredJudge = judgeFactoryConfig ? createProviderBackedStructuredJudge(judgeFactoryConfig) : void 0;
|
|
12648
14489
|
const responderFactoryConfig = systemProvider ? asProviderFactoryConfig(systemProvider) : void 0;
|
|
12649
14490
|
const responder = responderFactoryConfig ? createProviderBackedResponder(responderFactoryConfig) : void 0;
|
|
12650
14491
|
const lcmObserveConcurrencyOverrides = buildLcmObserveConcurrencyOverrides(options.lcmObserveConcurrency);
|
|
@@ -12947,6 +14788,22 @@ function runJudgeWithCache(options) {
|
|
|
12947
14788
|
}
|
|
12948
14789
|
});
|
|
12949
14790
|
}
|
|
14791
|
+
if (typeof judge.judgeMemCorrectCorrectionAcceptance === "function") {
|
|
14792
|
+
Object.defineProperty(wrapper, "judgeMemCorrectCorrectionAcceptance", {
|
|
14793
|
+
configurable: true,
|
|
14794
|
+
enumerable: true,
|
|
14795
|
+
writable: false,
|
|
14796
|
+
value: (request, control) => judge.judgeMemCorrectCorrectionAcceptance(request, control)
|
|
14797
|
+
});
|
|
14798
|
+
}
|
|
14799
|
+
if (typeof judge.judgeMemCorrectStaleMemoryHarm === "function") {
|
|
14800
|
+
Object.defineProperty(wrapper, "judgeMemCorrectStaleMemoryHarm", {
|
|
14801
|
+
configurable: true,
|
|
14802
|
+
enumerable: true,
|
|
14803
|
+
writable: false,
|
|
14804
|
+
value: (request, control) => judge.judgeMemCorrectStaleMemoryHarm(request, control)
|
|
14805
|
+
});
|
|
14806
|
+
}
|
|
12950
14807
|
return wrapper;
|
|
12951
14808
|
}
|
|
12952
14809
|
function parseEnvelope(raw) {
|
|
@@ -15303,10 +17160,10 @@ function unpackMemoryArenaWebshopJsonlRecord(parsed) {
|
|
|
15303
17160
|
if (Array.isArray(parsed)) {
|
|
15304
17161
|
return parsed.map((value) => ({ value }));
|
|
15305
17162
|
}
|
|
15306
|
-
if (
|
|
17163
|
+
if (isPlainRecord2(parsed)) {
|
|
15307
17164
|
for (const key of ["products", "records", "items"]) {
|
|
15308
17165
|
const value = parsed[key];
|
|
15309
|
-
if (Array.isArray(value) ||
|
|
17166
|
+
if (Array.isArray(value) || isPlainRecord2(value)) {
|
|
15310
17167
|
return unpackMemoryArenaWebshopRecords(parsed);
|
|
15311
17168
|
}
|
|
15312
17169
|
}
|
|
@@ -15317,7 +17174,7 @@ function unpackMemoryArenaWebshopRecords(parsed) {
|
|
|
15317
17174
|
if (Array.isArray(parsed)) {
|
|
15318
17175
|
return parsed.map((value) => ({ value }));
|
|
15319
17176
|
}
|
|
15320
|
-
if (!
|
|
17177
|
+
if (!isPlainRecord2(parsed)) {
|
|
15321
17178
|
throw new Error("top-level value must be a JSON array, object, or JSONL records");
|
|
15322
17179
|
}
|
|
15323
17180
|
for (const key of ["products", "records", "items"]) {
|
|
@@ -15325,7 +17182,7 @@ function unpackMemoryArenaWebshopRecords(parsed) {
|
|
|
15325
17182
|
if (Array.isArray(value)) {
|
|
15326
17183
|
return value.map((item) => ({ value: item }));
|
|
15327
17184
|
}
|
|
15328
|
-
if (
|
|
17185
|
+
if (isPlainRecord2(value)) {
|
|
15329
17186
|
return Object.entries(value).map(([defaultAsin, item]) => ({
|
|
15330
17187
|
value: item,
|
|
15331
17188
|
defaultAsin
|
|
@@ -15348,7 +17205,7 @@ function isMemoryArenaWebshopProductRecord(record) {
|
|
|
15348
17205
|
).length > 0;
|
|
15349
17206
|
}
|
|
15350
17207
|
function parseMemoryArenaWebshopProduct(raw) {
|
|
15351
|
-
if (!
|
|
17208
|
+
if (!isPlainRecord2(raw.value)) {
|
|
15352
17209
|
return void 0;
|
|
15353
17210
|
}
|
|
15354
17211
|
const record = raw.value;
|
|
@@ -15698,7 +17555,7 @@ function formatMemoryArenaDecisionCandidate(candidate) {
|
|
|
15698
17555
|
}
|
|
15699
17556
|
function extractSelectedMemoryArenaCustomization(record) {
|
|
15700
17557
|
const customizationOptions = record.customization_options;
|
|
15701
|
-
if (!
|
|
17558
|
+
if (!isPlainRecord2(customizationOptions)) {
|
|
15702
17559
|
return void 0;
|
|
15703
17560
|
}
|
|
15704
17561
|
for (const optionGroup of Object.values(customizationOptions)) {
|
|
@@ -15706,7 +17563,7 @@ function extractSelectedMemoryArenaCustomization(record) {
|
|
|
15706
17563
|
continue;
|
|
15707
17564
|
}
|
|
15708
17565
|
for (const option of optionGroup) {
|
|
15709
|
-
if (!
|
|
17566
|
+
if (!isPlainRecord2(option) || option.is_selected !== true) {
|
|
15710
17567
|
continue;
|
|
15711
17568
|
}
|
|
15712
17569
|
const priceText = cleanMemoryArenaWebshopString(
|
|
@@ -15723,7 +17580,7 @@ function extractSelectedMemoryArenaCustomization(record) {
|
|
|
15723
17580
|
}
|
|
15724
17581
|
function extractMemoryArenaProductInformationValue(record, requestedKey) {
|
|
15725
17582
|
const productInformation = record.product_information;
|
|
15726
|
-
if (!
|
|
17583
|
+
if (!isPlainRecord2(productInformation)) {
|
|
15727
17584
|
return void 0;
|
|
15728
17585
|
}
|
|
15729
17586
|
const normalizedRequestedKey = normalizeItemSelectionText(requestedKey);
|
|
@@ -15736,13 +17593,13 @@ function extractMemoryArenaProductInformationValue(record, requestedKey) {
|
|
|
15736
17593
|
}
|
|
15737
17594
|
function readMemoryArenaProductCustomerReviewNumber(record, key) {
|
|
15738
17595
|
const productInformation = record.product_information;
|
|
15739
|
-
if (!
|
|
17596
|
+
if (!isPlainRecord2(productInformation)) {
|
|
15740
17597
|
return void 0;
|
|
15741
17598
|
}
|
|
15742
17599
|
const customerReviews = Object.entries(productInformation).find(
|
|
15743
17600
|
([entryKey]) => normalizeItemSelectionText(cleanMemoryArenaWebshopString(entryKey)) === "customer reviews"
|
|
15744
17601
|
)?.[1];
|
|
15745
|
-
if (!
|
|
17602
|
+
if (!isPlainRecord2(customerReviews)) {
|
|
15746
17603
|
return void 0;
|
|
15747
17604
|
}
|
|
15748
17605
|
return readMemoryArenaNumberField(customerReviews, key);
|
|
@@ -15815,7 +17672,7 @@ function cleanMemoryArenaWebshopString(value) {
|
|
|
15815
17672
|
function formatMemoryArenaNumber(value) {
|
|
15816
17673
|
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(2)));
|
|
15817
17674
|
}
|
|
15818
|
-
function
|
|
17675
|
+
function isPlainRecord2(value) {
|
|
15819
17676
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
15820
17677
|
}
|
|
15821
17678
|
function isNodeErrorCode(error, code) {
|
|
@@ -17027,6 +18884,7 @@ async function* toAsyncIterable(iter) {
|
|
|
17027
18884
|
}
|
|
17028
18885
|
async function runPublishedHarness(ctx) {
|
|
17029
18886
|
validateContext(ctx);
|
|
18887
|
+
const answerSupportGate = resolveAnswerSupportGate(ctx.options);
|
|
17030
18888
|
const trialConcurrency = resolveTrialConcurrency(
|
|
17031
18889
|
ctx.options.benchmarkOptions?.trialConcurrency
|
|
17032
18890
|
);
|
|
@@ -17050,7 +18908,8 @@ async function runPublishedHarness(ctx) {
|
|
|
17050
18908
|
await executePlanTrials(ctx, plan.trials, {
|
|
17051
18909
|
planIndex,
|
|
17052
18910
|
tasks,
|
|
17053
|
-
trialConcurrency
|
|
18911
|
+
trialConcurrency,
|
|
18912
|
+
answerSupportGate
|
|
17054
18913
|
});
|
|
17055
18914
|
}
|
|
17056
18915
|
return buildBenchmarkResult(ctx, tasks);
|
|
@@ -17061,7 +18920,12 @@ async function executePlanTrials(ctx, trials, options) {
|
|
|
17061
18920
|
appendCompletedTask(
|
|
17062
18921
|
ctx,
|
|
17063
18922
|
options.tasks,
|
|
17064
|
-
await executeTrialWithFailure(
|
|
18923
|
+
await executeTrialWithFailure(
|
|
18924
|
+
ctx,
|
|
18925
|
+
trial,
|
|
18926
|
+
options.planIndex,
|
|
18927
|
+
options.answerSupportGate
|
|
18928
|
+
)
|
|
17065
18929
|
);
|
|
17066
18930
|
}
|
|
17067
18931
|
return;
|
|
@@ -17086,7 +18950,8 @@ async function executePlanTrials(ctx, trials, options) {
|
|
|
17086
18950
|
results[trialIndex] = await executeTrialWithFailure(
|
|
17087
18951
|
ctx,
|
|
17088
18952
|
trials[trialIndex],
|
|
17089
|
-
options.planIndex
|
|
18953
|
+
options.planIndex,
|
|
18954
|
+
options.answerSupportGate
|
|
17090
18955
|
);
|
|
17091
18956
|
completed[trialIndex] = true;
|
|
17092
18957
|
emitCompletedPrefix();
|
|
@@ -17101,10 +18966,10 @@ function appendCompletedTask(ctx, tasks, task) {
|
|
|
17101
18966
|
tasks.push(task);
|
|
17102
18967
|
ctx.options.onTaskComplete?.(task, tasks.length, ctx.totalCount);
|
|
17103
18968
|
}
|
|
17104
|
-
async function executeTrialWithFailure(ctx, trial, planIndex) {
|
|
18969
|
+
async function executeTrialWithFailure(ctx, trial, planIndex, answerSupportGate) {
|
|
17105
18970
|
const trialId = trial.taskId ?? trial.question.slice(0, 60);
|
|
17106
18971
|
try {
|
|
17107
|
-
return await executeTrial(ctx, trial);
|
|
18972
|
+
return await executeTrial(ctx, trial, answerSupportGate);
|
|
17108
18973
|
} catch (err) {
|
|
17109
18974
|
const message = err instanceof Error ? err.message : String(err);
|
|
17110
18975
|
console.error(` [WARN] harness trial plan-${planIndex}/${trialId} failed: ${message}`);
|
|
@@ -17116,7 +18981,17 @@ async function executeTrialWithFailure(ctx, trial, planIndex) {
|
|
|
17116
18981
|
scores: buildFailureScores(ctx.metricsSpec.metrics),
|
|
17117
18982
|
latencyMs: 0,
|
|
17118
18983
|
tokens: { input: 0, output: 0 },
|
|
17119
|
-
details: {
|
|
18984
|
+
details: {
|
|
18985
|
+
// `error` is retained for compatibility with existing diagnostics.
|
|
18986
|
+
// The structured marker is the authoritative run-status signal; an
|
|
18987
|
+
// arbitrary benchmark-owned `extraDetails.error` must not make a
|
|
18988
|
+
// successful trial look failed.
|
|
18989
|
+
error: message,
|
|
18990
|
+
benchmarkFailure: {
|
|
18991
|
+
kind: "trial_execution_failure",
|
|
18992
|
+
message
|
|
18993
|
+
}
|
|
18994
|
+
}
|
|
17120
18995
|
};
|
|
17121
18996
|
}
|
|
17122
18997
|
}
|
|
@@ -17172,8 +19047,29 @@ function resolveTrialConcurrency(raw) {
|
|
|
17172
19047
|
}
|
|
17173
19048
|
return parsed;
|
|
17174
19049
|
}
|
|
17175
|
-
|
|
17176
|
-
const
|
|
19050
|
+
function resolveAnswerSupportGate(options) {
|
|
19051
|
+
const raw = options.benchmarkOptions?.answerSupportGate ?? options.remnicConfig?.answerSupportGate;
|
|
19052
|
+
if (raw === void 0) {
|
|
19053
|
+
return false;
|
|
19054
|
+
}
|
|
19055
|
+
if (typeof raw === "boolean") {
|
|
19056
|
+
return raw;
|
|
19057
|
+
}
|
|
19058
|
+
if (typeof raw === "string") {
|
|
19059
|
+
const normalized = raw.trim().toLowerCase();
|
|
19060
|
+
if (["true", "1", "yes", "on"].includes(normalized)) {
|
|
19061
|
+
return true;
|
|
19062
|
+
}
|
|
19063
|
+
if (["false", "0", "no", "off"].includes(normalized)) {
|
|
19064
|
+
return false;
|
|
19065
|
+
}
|
|
19066
|
+
}
|
|
19067
|
+
throw new Error(
|
|
19068
|
+
"PublishedBenchmarkHarness: answerSupportGate must be a boolean or one of true/false, 1/0, yes/no, on/off."
|
|
19069
|
+
);
|
|
19070
|
+
}
|
|
19071
|
+
async function executeTrial(ctx, trial, answerSupportGate) {
|
|
19072
|
+
const { result: recallResult, durationMs } = await timed(async () => {
|
|
17177
19073
|
const recallBudget = benchmarkRecallBudgetForSessionCount(
|
|
17178
19074
|
trial.recallSessionIds.length
|
|
17179
19075
|
);
|
|
@@ -17183,17 +19079,21 @@ async function executeTrial(ctx, trial) {
|
|
|
17183
19079
|
)
|
|
17184
19080
|
);
|
|
17185
19081
|
const rawRecalledText = recalledSessions.filter(Boolean).join("\n\n");
|
|
17186
|
-
|
|
19082
|
+
const recalledText2 = trial.recallTextTransform ? trial.recallTextTransform({
|
|
17187
19083
|
question: trial.question,
|
|
17188
19084
|
recalledText: rawRecalledText
|
|
17189
19085
|
}) : rawRecalledText;
|
|
19086
|
+
const recallSupport2 = answerSupportGate ? await assessRecallSupport(ctx, trial, recalledText2) : void 0;
|
|
19087
|
+
return { recalledText: recalledText2, recallSupport: recallSupport2 };
|
|
17190
19088
|
});
|
|
19089
|
+
const { recalledText, recallSupport } = recallResult;
|
|
17191
19090
|
let answered = await answerBenchmarkQuestion({
|
|
17192
19091
|
question: trial.question,
|
|
17193
19092
|
recalledText,
|
|
17194
19093
|
responder: ctx.options.system.responder,
|
|
17195
19094
|
answerMode: "strict",
|
|
17196
|
-
answerFormat: trial.answerFormat
|
|
19095
|
+
answerFormat: trial.answerFormat,
|
|
19096
|
+
recallSupport
|
|
17197
19097
|
}).catch(
|
|
17198
19098
|
(error) => answerWithTrialFallback(trial, recalledText, error)
|
|
17199
19099
|
);
|
|
@@ -17261,6 +19161,7 @@ async function executeTrial(ctx, trial) {
|
|
|
17261
19161
|
recalledText,
|
|
17262
19162
|
answeredText: answered.finalAnswer,
|
|
17263
19163
|
...trial.answerFormat ? { answerFormat: trial.answerFormat } : {},
|
|
19164
|
+
...answerSupportGate ? { answerSupportGate: true, recallSupport } : {},
|
|
17264
19165
|
responderModel: answered.model,
|
|
17265
19166
|
judgeModel: judgeResult.model,
|
|
17266
19167
|
...answered.fallbackReason ? { answerFallbackReason: answered.fallbackReason } : {},
|
|
@@ -17290,6 +19191,56 @@ async function executeTrial(ctx, trial) {
|
|
|
17290
19191
|
details
|
|
17291
19192
|
};
|
|
17292
19193
|
}
|
|
19194
|
+
async function assessRecallSupport(ctx, trial, recalledText) {
|
|
19195
|
+
if (recalledText.trim().length === 0) {
|
|
19196
|
+
return {
|
|
19197
|
+
status: "empty",
|
|
19198
|
+
reason: "successful recall returned empty responder context",
|
|
19199
|
+
evidenceCount: 0
|
|
19200
|
+
};
|
|
19201
|
+
}
|
|
19202
|
+
const assessor = ctx.options.system.assessRecallSupport;
|
|
19203
|
+
if (!assessor) {
|
|
19204
|
+
return {
|
|
19205
|
+
status: "unavailable",
|
|
19206
|
+
reason: "adapter did not provide an exact-context support assessment"
|
|
19207
|
+
};
|
|
19208
|
+
}
|
|
19209
|
+
try {
|
|
19210
|
+
const assessment = await assessor.call(ctx.options.system, {
|
|
19211
|
+
query: trial.question,
|
|
19212
|
+
recalledText,
|
|
19213
|
+
sessionIds: trial.recallSessionIds
|
|
19214
|
+
});
|
|
19215
|
+
validateRecallSupportAssessment(assessment);
|
|
19216
|
+
return assessment;
|
|
19217
|
+
} catch (error) {
|
|
19218
|
+
return {
|
|
19219
|
+
status: "backend_failure",
|
|
19220
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
19221
|
+
};
|
|
19222
|
+
}
|
|
19223
|
+
}
|
|
19224
|
+
function validateRecallSupportAssessment(assessment) {
|
|
19225
|
+
const allowed = [
|
|
19226
|
+
"supported",
|
|
19227
|
+
"weak",
|
|
19228
|
+
"empty",
|
|
19229
|
+
"unavailable",
|
|
19230
|
+
"backend_failure"
|
|
19231
|
+
];
|
|
19232
|
+
if (!assessment || !allowed.includes(assessment.status)) {
|
|
19233
|
+
throw new Error("adapter returned an invalid recall support status");
|
|
19234
|
+
}
|
|
19235
|
+
if (assessment.status !== "weak") {
|
|
19236
|
+
return;
|
|
19237
|
+
}
|
|
19238
|
+
if (!Number.isInteger(assessment.evidenceCount) || (assessment.evidenceCount ?? 0) <= 0 || !Number.isFinite(assessment.maxScore) || !Number.isFinite(assessment.supportThreshold) || (assessment.maxScore ?? Number.POSITIVE_INFINITY) >= (assessment.supportThreshold ?? Number.NEGATIVE_INFINITY)) {
|
|
19239
|
+
throw new Error(
|
|
19240
|
+
"adapter weak recall support requires a positive evidenceCount and a finite maxScore below supportThreshold"
|
|
19241
|
+
);
|
|
19242
|
+
}
|
|
19243
|
+
}
|
|
17293
19244
|
async function scoreTrialJudge(ctx, trial, answeredText) {
|
|
17294
19245
|
if (!trial.binaryJudgePrompt) {
|
|
17295
19246
|
return llmJudgeScoreDetailed(
|
|
@@ -17379,6 +19330,18 @@ async function buildBenchmarkResult(ctx, tasks) {
|
|
|
17379
19330
|
0
|
|
17380
19331
|
);
|
|
17381
19332
|
const mode = ctx.options.mode;
|
|
19333
|
+
const failedTasks = tasks.flatMap((task) => {
|
|
19334
|
+
const marker = task.details?.benchmarkFailure;
|
|
19335
|
+
if (typeof marker !== "object" || marker === null || marker.kind !== "trial_execution_failure") {
|
|
19336
|
+
return [];
|
|
19337
|
+
}
|
|
19338
|
+
const message = marker.message;
|
|
19339
|
+
return [{
|
|
19340
|
+
taskId: task.taskId,
|
|
19341
|
+
message: typeof message === "string" ? message : "unknown trial failure"
|
|
19342
|
+
}];
|
|
19343
|
+
});
|
|
19344
|
+
const failureReason = failedTasks.length > 0 ? `trial_execution_failure: ${failedTasks.length}/${tasks.length} scored trial(s) failed (${failedTasks.slice(0, 3).map((failure) => `${failure.taskId}: ${failure.message.slice(0, 240)}`).join("; ")}${failedTasks.length > 3 ? `; and ${failedTasks.length - 3} more` : ""})` : void 0;
|
|
17382
19345
|
return {
|
|
17383
19346
|
meta: {
|
|
17384
19347
|
id: randomUUID5(),
|
|
@@ -17390,7 +19353,8 @@ async function buildBenchmarkResult(ctx, tasks) {
|
|
|
17390
19353
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17391
19354
|
mode,
|
|
17392
19355
|
runCount: 1,
|
|
17393
|
-
seeds: [ctx.options.seed ?? 0]
|
|
19356
|
+
seeds: [ctx.options.seed ?? 0],
|
|
19357
|
+
...failureReason ? { status: "partial", failureReason } : {}
|
|
17394
19358
|
},
|
|
17395
19359
|
config: {
|
|
17396
19360
|
systemProvider: ctx.options.systemProvider ?? null,
|
|
@@ -17453,16 +19417,25 @@ async function runLongMemEvalBenchmark(options) {
|
|
|
17453
19417
|
function buildPlan(item, options) {
|
|
17454
19418
|
const ingestSessions = [];
|
|
17455
19419
|
const sessionIds = [];
|
|
17456
|
-
const
|
|
19420
|
+
const evidenceStrategy = resolveLongMemEvalEvidenceStrategy(item);
|
|
19421
|
+
const annotateSources = evidenceStrategy !== "single-session" || shouldAnnotateTemporalSources(item.question);
|
|
19422
|
+
const sourceChronology = rankSourceDates(
|
|
19423
|
+
item.haystack_dates,
|
|
19424
|
+
item.haystack_sessions.length
|
|
19425
|
+
);
|
|
19426
|
+
const currentKnowledgeSourceIndex = evidenceStrategy === "knowledge-update" ? sourceChronology.indexOf(item.haystack_sessions.length) : void 0;
|
|
17457
19427
|
for (let sessionIndex = 0; sessionIndex < item.haystack_sessions.length; sessionIndex += 1) {
|
|
17458
19428
|
const sessionId = item.haystack_session_ids[sessionIndex] ?? `session-${sessionIndex}`;
|
|
17459
19429
|
const haystackDate = item.haystack_dates[sessionIndex];
|
|
17460
19430
|
const messages = item.haystack_sessions[sessionIndex].map(
|
|
17461
19431
|
(turn) => ({
|
|
17462
19432
|
role: turn.role,
|
|
17463
|
-
content:
|
|
19433
|
+
content: annotateSources ? formatLongMemEvalTurn(turn.content, {
|
|
17464
19434
|
sessionId,
|
|
17465
|
-
haystackDate
|
|
19435
|
+
haystackDate,
|
|
19436
|
+
sourceOrder: sourceChronology[sessionIndex],
|
|
19437
|
+
sourceCount: item.haystack_sessions.length,
|
|
19438
|
+
sourceRecency: currentKnowledgeSourceIndex === void 0 ? void 0 : sessionIndex === currentKnowledgeSourceIndex ? "latest_source" : "historical_source"
|
|
17466
19439
|
}) : turn.content
|
|
17467
19440
|
})
|
|
17468
19441
|
);
|
|
@@ -17474,6 +19447,7 @@ function buildPlan(item, options) {
|
|
|
17474
19447
|
question: item.question,
|
|
17475
19448
|
expected: item.answer,
|
|
17476
19449
|
recallSessionIds: sessionIds,
|
|
19450
|
+
recallTextTransform: ({ recalledText }) => composeLongMemEvalEvidence(recalledText, evidenceStrategy),
|
|
17477
19451
|
binaryJudgePrompt: ({ answeredText }) => buildLongMemEvalOfficialJudgePrompt(item, answeredText),
|
|
17478
19452
|
extraDetails: {
|
|
17479
19453
|
questionType: item.question_type,
|
|
@@ -17481,6 +19455,8 @@ function buildPlan(item, options) {
|
|
|
17481
19455
|
haystackDates: item.haystack_dates,
|
|
17482
19456
|
haystackSessionIds: item.haystack_session_ids,
|
|
17483
19457
|
answerSessionIds: item.answer_session_ids,
|
|
19458
|
+
evidenceStrategy,
|
|
19459
|
+
knowledgeSupersessionSurfaced: evidenceStrategy === "knowledge-update",
|
|
17484
19460
|
judgeProtocol: "longmemeval-official-yes-no",
|
|
17485
19461
|
judgePromptSource: "https://github.com/xiaowu0162/LongMemEval/blob/main/src/evaluation/evaluate_qa.py"
|
|
17486
19462
|
},
|
|
@@ -17605,12 +19581,71 @@ function uniqueSearchResults(results) {
|
|
|
17605
19581
|
return unique;
|
|
17606
19582
|
}
|
|
17607
19583
|
function formatLongMemEvalTurn(content, metadata) {
|
|
17608
|
-
const fields = [
|
|
19584
|
+
const fields = [
|
|
19585
|
+
`source_session: ${metadata.sessionId}`,
|
|
19586
|
+
`source_order: ${metadata.sourceOrder}/${metadata.sourceCount}`
|
|
19587
|
+
];
|
|
17609
19588
|
if (metadata.haystackDate) {
|
|
17610
19589
|
fields.push(`source_date: ${metadata.haystackDate}`);
|
|
17611
19590
|
}
|
|
19591
|
+
if (metadata.sourceRecency) {
|
|
19592
|
+
fields.push(`source_recency: ${metadata.sourceRecency}`);
|
|
19593
|
+
}
|
|
17612
19594
|
return `[${fields.join("] [")}] ${content}`;
|
|
17613
19595
|
}
|
|
19596
|
+
function resolveLongMemEvalEvidenceStrategy(item) {
|
|
19597
|
+
if (item.question_type === "knowledge-update" || item.question_type === "multi-session-update") {
|
|
19598
|
+
return "knowledge-update";
|
|
19599
|
+
}
|
|
19600
|
+
if (item.question_type === "multi-session") {
|
|
19601
|
+
return "multi-session";
|
|
19602
|
+
}
|
|
19603
|
+
return "single-session";
|
|
19604
|
+
}
|
|
19605
|
+
function composeLongMemEvalEvidence(recalledText, strategy) {
|
|
19606
|
+
if (recalledText.trim().length === 0 || strategy === "single-session") {
|
|
19607
|
+
return recalledText;
|
|
19608
|
+
}
|
|
19609
|
+
const guidance = [
|
|
19610
|
+
"[multi_session_evidence] Compose the answer from all relevant source_session blocks; one block may contain only part of the answer."
|
|
19611
|
+
];
|
|
19612
|
+
if (strategy === "knowledge-update") {
|
|
19613
|
+
guidance.push(
|
|
19614
|
+
"[knowledge_update_evidence] Only when the same fact conflicts across sources, treat its older value as superseded and prefer the later source_date/source_order. source_recency describes source chronology, not the validity of unrelated facts."
|
|
19615
|
+
);
|
|
19616
|
+
}
|
|
19617
|
+
return `${guidance.join("\n")}
|
|
19618
|
+
|
|
19619
|
+
${recalledText}`;
|
|
19620
|
+
}
|
|
19621
|
+
function rankSourceDates(dates, sourceCount) {
|
|
19622
|
+
const chronologicalIndices = Array.from(
|
|
19623
|
+
{ length: sourceCount },
|
|
19624
|
+
(_, index) => index
|
|
19625
|
+
).sort((left, right) => {
|
|
19626
|
+
const leftTimestamp = parseSourceDate(dates[left]);
|
|
19627
|
+
const rightTimestamp = parseSourceDate(dates[right]);
|
|
19628
|
+
if (leftTimestamp < rightTimestamp) {
|
|
19629
|
+
return -1;
|
|
19630
|
+
}
|
|
19631
|
+
if (leftTimestamp > rightTimestamp) {
|
|
19632
|
+
return 1;
|
|
19633
|
+
}
|
|
19634
|
+
return left - right;
|
|
19635
|
+
});
|
|
19636
|
+
const ranks = new Array(sourceCount);
|
|
19637
|
+
chronologicalIndices.forEach((sourceIndex, chronologicalIndex) => {
|
|
19638
|
+
ranks[sourceIndex] = chronologicalIndex + 1;
|
|
19639
|
+
});
|
|
19640
|
+
return ranks;
|
|
19641
|
+
}
|
|
19642
|
+
function parseSourceDate(value) {
|
|
19643
|
+
if (!value) {
|
|
19644
|
+
return Number.NEGATIVE_INFINITY;
|
|
19645
|
+
}
|
|
19646
|
+
const timestamp = Date.parse(value);
|
|
19647
|
+
return Number.isFinite(timestamp) ? timestamp : Number.NEGATIVE_INFINITY;
|
|
19648
|
+
}
|
|
17614
19649
|
function shouldAnnotateTemporalSources(question) {
|
|
17615
19650
|
return collectIsoDateCues(question).length > 0 || collectTemporalLexicalCues(question).length > 0;
|
|
17616
19651
|
}
|
|
@@ -17683,6 +19718,8 @@ var CATEGORY_NAMES = {
|
|
|
17683
19718
|
};
|
|
17684
19719
|
var DIALOGUE_ID_PATTERN = /\bD\d+:\d+\b/g;
|
|
17685
19720
|
var LOCOMO_FOCUSED_LINE_LIMIT = 14;
|
|
19721
|
+
var LOCOMO_DIRECT_LINE_LIMIT = 10;
|
|
19722
|
+
var LOCOMO_COMPOSITION_HOP_LIMIT = 2;
|
|
17686
19723
|
var LOCOMO_FOCUSED_LINE_MAX_CHARS = 420;
|
|
17687
19724
|
var LOCOMO_FOCUSED_CONTEXT_MAX_CHARS = 6e3;
|
|
17688
19725
|
var LOCOMO_FALLBACK_CONTEXT_MAX_CHARS = 8e3;
|
|
@@ -17917,8 +19954,22 @@ async function runLoCoMoBenchmark(options) {
|
|
|
17917
19954
|
options.limit
|
|
17918
19955
|
);
|
|
17919
19956
|
const trialLimit = resolveTrialLimit(options.benchmarkOptions?.trialLimit);
|
|
17920
|
-
const
|
|
17921
|
-
|
|
19957
|
+
const multiHopRecallComposition = resolveLoCoMoBooleanOption(
|
|
19958
|
+
options.benchmarkOptions?.multiHopRecallComposition,
|
|
19959
|
+
"multiHopRecallComposition",
|
|
19960
|
+
true
|
|
19961
|
+
);
|
|
19962
|
+
const plans = applyTrialLimit(
|
|
19963
|
+
conversations.map(
|
|
19964
|
+
(conversation) => buildPlan2(conversation, multiHopRecallComposition)
|
|
19965
|
+
),
|
|
19966
|
+
trialLimit
|
|
19967
|
+
);
|
|
19968
|
+
const benchmarkOptions = {
|
|
19969
|
+
...options.benchmarkOptions ?? {},
|
|
19970
|
+
...trialLimit === void 0 ? {} : { trialLimit },
|
|
19971
|
+
multiHopRecallComposition
|
|
19972
|
+
};
|
|
17922
19973
|
return runPublishedHarness({
|
|
17923
19974
|
options: { ...options, benchmarkOptions },
|
|
17924
19975
|
metricsSpec: {
|
|
@@ -17928,6 +19979,26 @@ async function runLoCoMoBenchmark(options) {
|
|
|
17928
19979
|
totalCount: plans.reduce((sum, plan) => sum + plan.trials.length, 0)
|
|
17929
19980
|
});
|
|
17930
19981
|
}
|
|
19982
|
+
function resolveLoCoMoBooleanOption(raw, optionName, defaultValue) {
|
|
19983
|
+
if (raw === void 0) {
|
|
19984
|
+
return defaultValue;
|
|
19985
|
+
}
|
|
19986
|
+
if (typeof raw === "boolean") {
|
|
19987
|
+
return raw;
|
|
19988
|
+
}
|
|
19989
|
+
if (typeof raw === "string") {
|
|
19990
|
+
const normalized = raw.trim().toLowerCase();
|
|
19991
|
+
if (["true", "1", "yes", "on"].includes(normalized)) {
|
|
19992
|
+
return true;
|
|
19993
|
+
}
|
|
19994
|
+
if (["false", "0", "no", "off"].includes(normalized)) {
|
|
19995
|
+
return false;
|
|
19996
|
+
}
|
|
19997
|
+
}
|
|
19998
|
+
throw new Error(
|
|
19999
|
+
`LoCoMo benchmarkOptions.${optionName} must be a boolean or one of true/false, 1/0, yes/no, on/off.`
|
|
20000
|
+
);
|
|
20001
|
+
}
|
|
17931
20002
|
function resolveTrialLimit(raw) {
|
|
17932
20003
|
if (raw === void 0) {
|
|
17933
20004
|
return void 0;
|
|
@@ -17961,7 +20032,7 @@ function applyTrialLimit(plans, trialLimit) {
|
|
|
17961
20032
|
}
|
|
17962
20033
|
return limitedPlans;
|
|
17963
20034
|
}
|
|
17964
|
-
function buildPlan2(conversation) {
|
|
20035
|
+
function buildPlan2(conversation, multiHopRecallComposition) {
|
|
17965
20036
|
const sessions = extractSessions(conversation.conversation);
|
|
17966
20037
|
const speakerA = typeof conversation.conversation.speaker_a === "string" ? conversation.conversation.speaker_a : "Speaker A";
|
|
17967
20038
|
const ingestSessions = [];
|
|
@@ -17979,11 +20050,17 @@ function buildPlan2(conversation) {
|
|
|
17979
20050
|
ingestSessions.push({ sessionId, messages });
|
|
17980
20051
|
}
|
|
17981
20052
|
const trials = conversation.qa.map(
|
|
17982
|
-
(qa, questionIndex) => buildTrial(
|
|
20053
|
+
(qa, questionIndex) => buildTrial(
|
|
20054
|
+
conversation.sample_id,
|
|
20055
|
+
qa,
|
|
20056
|
+
questionIndex,
|
|
20057
|
+
sessionIds,
|
|
20058
|
+
multiHopRecallComposition
|
|
20059
|
+
)
|
|
17983
20060
|
);
|
|
17984
20061
|
return { ingestSessions, trials };
|
|
17985
20062
|
}
|
|
17986
|
-
function buildTrial(conversationId, qa, questionIndex, sessionIds) {
|
|
20063
|
+
function buildTrial(conversationId, qa, questionIndex, sessionIds, multiHopRecallComposition) {
|
|
17987
20064
|
const categoryName = CATEGORY_NAMES[qa.category] ?? `category_${qa.category}`;
|
|
17988
20065
|
return {
|
|
17989
20066
|
taskId: `${conversationId}-q${questionIndex}-${categoryName}`,
|
|
@@ -17993,7 +20070,8 @@ function buildTrial(conversationId, qa, questionIndex, sessionIds) {
|
|
|
17993
20070
|
answerFormat: "short-with-specifics",
|
|
17994
20071
|
recallTextTransform: ({ question, recalledText }) => prioritizeLoCoMoRecallText({
|
|
17995
20072
|
question,
|
|
17996
|
-
recalledText: sanitizeLoCoMoRecallText({ question, recalledText })
|
|
20073
|
+
recalledText: sanitizeLoCoMoRecallText({ question, recalledText }),
|
|
20074
|
+
multiHopRecallComposition
|
|
17997
20075
|
}),
|
|
17998
20076
|
answerFallback: ({ question, recalledText }) => answerLoCoMoFromRecall(question, recalledText),
|
|
17999
20077
|
answerRefinement: ({ question, recalledText, answeredText }) => refineLoCoMoAnswerFromRecall({ question, recalledText, answeredText }),
|
|
@@ -18194,7 +20272,9 @@ function sanitizeLoCoMoRecallText(args) {
|
|
|
18194
20272
|
);
|
|
18195
20273
|
}
|
|
18196
20274
|
function prioritizeLoCoMoRecallText(args) {
|
|
18197
|
-
const lines =
|
|
20275
|
+
const lines = dedupePreserveOrder(
|
|
20276
|
+
args.recalledText.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0)
|
|
20277
|
+
);
|
|
18198
20278
|
const questionTokens = expandLoCoMoQuestionTokens(
|
|
18199
20279
|
tokenizeForLoCoMo(args.question)
|
|
18200
20280
|
);
|
|
@@ -18205,20 +20285,124 @@ function prioritizeLoCoMoRecallText(args) {
|
|
|
18205
20285
|
})).filter((entry) => entry.score > 0).sort(
|
|
18206
20286
|
(left, right) => right.score === left.score ? left.index - right.index : right.score - left.score
|
|
18207
20287
|
);
|
|
18208
|
-
const
|
|
18209
|
-
|
|
20288
|
+
const directSeeds = scored.slice(
|
|
20289
|
+
0,
|
|
20290
|
+
args.multiHopRecallComposition ? LOCOMO_DIRECT_LINE_LIMIT : LOCOMO_FOCUSED_LINE_LIMIT
|
|
18210
20291
|
);
|
|
18211
|
-
|
|
20292
|
+
const linkedHops = args.multiHopRecallComposition ? composeLoCoMoLinkedEvidence({
|
|
20293
|
+
lines,
|
|
20294
|
+
direct: directSeeds,
|
|
20295
|
+
questionTokens,
|
|
20296
|
+
remainingLineBudget: LOCOMO_FOCUSED_LINE_LIMIT - directSeeds.length
|
|
20297
|
+
}) : [];
|
|
20298
|
+
const linkedLineCount = linkedHops.reduce(
|
|
20299
|
+
(sum, hop) => sum + hop.lines.length,
|
|
20300
|
+
0
|
|
20301
|
+
);
|
|
20302
|
+
const direct = [
|
|
20303
|
+
...directSeeds,
|
|
20304
|
+
...scored.slice(
|
|
20305
|
+
directSeeds.length,
|
|
20306
|
+
LOCOMO_FOCUSED_LINE_LIMIT - linkedLineCount
|
|
20307
|
+
)
|
|
20308
|
+
];
|
|
20309
|
+
if (direct.length === 0 && linkedHops.length === 0) {
|
|
18212
20310
|
return truncateLoCoMoContext(
|
|
18213
20311
|
args.recalledText,
|
|
18214
20312
|
LOCOMO_FALLBACK_CONTEXT_MAX_CHARS
|
|
18215
20313
|
);
|
|
18216
20314
|
}
|
|
20315
|
+
const sections = [
|
|
20316
|
+
"## LoCoMo Question-Focused Evidence",
|
|
20317
|
+
...direct.map((entry) => truncateLoCoMoLine(entry.line))
|
|
20318
|
+
];
|
|
20319
|
+
for (const hop of linkedHops) {
|
|
20320
|
+
sections.push(
|
|
20321
|
+
`## LoCoMo Linked Evidence (hop ${hop.hop})`,
|
|
20322
|
+
...hop.lines.map(truncateLoCoMoLine)
|
|
20323
|
+
);
|
|
20324
|
+
}
|
|
18217
20325
|
return truncateLoCoMoContext(
|
|
18218
|
-
|
|
20326
|
+
sections.join("\n"),
|
|
18219
20327
|
LOCOMO_FOCUSED_CONTEXT_MAX_CHARS
|
|
18220
20328
|
);
|
|
18221
20329
|
}
|
|
20330
|
+
function composeLoCoMoLinkedEvidence(args) {
|
|
20331
|
+
if (args.direct.length === 0 || args.remainingLineBudget <= 0) {
|
|
20332
|
+
return [];
|
|
20333
|
+
}
|
|
20334
|
+
const lineEntries = args.lines.map((line, index) => ({
|
|
20335
|
+
line,
|
|
20336
|
+
index,
|
|
20337
|
+
directScore: scoreLoCoMoLine(line, args.questionTokens),
|
|
20338
|
+
bridgeTokens: collectLoCoMoBridgeTokens(line, args.questionTokens)
|
|
20339
|
+
}));
|
|
20340
|
+
const selectedIndexes = new Set(args.direct.map((entry) => entry.index));
|
|
20341
|
+
const visitedBridgeTokens = /* @__PURE__ */ new Set();
|
|
20342
|
+
let frontierTokens = /* @__PURE__ */ new Set();
|
|
20343
|
+
for (const entry of args.direct) {
|
|
20344
|
+
for (const token of lineEntries[entry.index]?.bridgeTokens ?? []) {
|
|
20345
|
+
frontierTokens.add(token);
|
|
20346
|
+
visitedBridgeTokens.add(token);
|
|
20347
|
+
}
|
|
20348
|
+
}
|
|
20349
|
+
const hops = [];
|
|
20350
|
+
let remaining = args.remainingLineBudget;
|
|
20351
|
+
for (let hop = 1; hop <= LOCOMO_COMPOSITION_HOP_LIMIT && remaining > 0 && frontierTokens.size > 0; hop += 1) {
|
|
20352
|
+
const candidates = lineEntries.filter(
|
|
20353
|
+
(entry) => entry.directScore === 0 && !selectedIndexes.has(entry.index)
|
|
20354
|
+
).map((entry) => ({
|
|
20355
|
+
...entry,
|
|
20356
|
+
sharedTokens: [...entry.bridgeTokens].filter(
|
|
20357
|
+
(token) => frontierTokens.has(token)
|
|
20358
|
+
)
|
|
20359
|
+
})).filter((entry) => entry.sharedTokens.length > 0).sort((left, right) => {
|
|
20360
|
+
if (right.sharedTokens.length !== left.sharedTokens.length) {
|
|
20361
|
+
return right.sharedTokens.length - left.sharedTokens.length;
|
|
20362
|
+
}
|
|
20363
|
+
const longestLeft = Math.max(
|
|
20364
|
+
...left.sharedTokens.map((token) => token.length)
|
|
20365
|
+
);
|
|
20366
|
+
const longestRight = Math.max(
|
|
20367
|
+
...right.sharedTokens.map((token) => token.length)
|
|
20368
|
+
);
|
|
20369
|
+
return longestRight === longestLeft ? left.index - right.index : longestRight - longestLeft;
|
|
20370
|
+
}).slice(0, remaining);
|
|
20371
|
+
if (candidates.length === 0) {
|
|
20372
|
+
break;
|
|
20373
|
+
}
|
|
20374
|
+
hops.push({ hop, lines: candidates.map((entry) => entry.line) });
|
|
20375
|
+
remaining -= candidates.length;
|
|
20376
|
+
const nextFrontier = /* @__PURE__ */ new Set();
|
|
20377
|
+
for (const candidate of candidates) {
|
|
20378
|
+
selectedIndexes.add(candidate.index);
|
|
20379
|
+
for (const token of candidate.bridgeTokens) {
|
|
20380
|
+
if (!visitedBridgeTokens.has(token)) {
|
|
20381
|
+
nextFrontier.add(token);
|
|
20382
|
+
visitedBridgeTokens.add(token);
|
|
20383
|
+
}
|
|
20384
|
+
}
|
|
20385
|
+
}
|
|
20386
|
+
frontierTokens = nextFrontier;
|
|
20387
|
+
}
|
|
20388
|
+
return hops;
|
|
20389
|
+
}
|
|
20390
|
+
function collectLoCoMoBridgeTokens(line, questionTokens) {
|
|
20391
|
+
const entityLikeTokens = /* @__PURE__ */ new Set();
|
|
20392
|
+
for (const match of line.matchAll(/\b[A-Z][A-Za-z0-9'-]{2,}\b/g)) {
|
|
20393
|
+
for (const token of tokenizeForLoCoMo(match[0])) {
|
|
20394
|
+
entityLikeTokens.add(token);
|
|
20395
|
+
}
|
|
20396
|
+
}
|
|
20397
|
+
const result = /* @__PURE__ */ new Set();
|
|
20398
|
+
for (const token of tokenizeForLoCoMo(line)) {
|
|
20399
|
+
if (!entityLikeTokens.has(token) || token.length < 3 || /^\d+$/.test(token) || questionTokens.has(token) || LOCOMO_LINK_STOP_WORDS.has(token)) {
|
|
20400
|
+
continue;
|
|
20401
|
+
}
|
|
20402
|
+
result.add(token);
|
|
20403
|
+
}
|
|
20404
|
+
return result;
|
|
20405
|
+
}
|
|
18222
20406
|
function tokenizeForLoCoMo(text) {
|
|
18223
20407
|
const tokens = /* @__PURE__ */ new Set();
|
|
18224
20408
|
for (const rawToken of text.toLowerCase().match(/[a-z0-9]+/g) ?? []) {
|
|
@@ -18254,6 +20438,49 @@ var LOCOMO_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
18254
20438
|
"who",
|
|
18255
20439
|
"would"
|
|
18256
20440
|
]);
|
|
20441
|
+
var LOCOMO_LINK_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
20442
|
+
...LOCOMO_STOP_WORDS,
|
|
20443
|
+
"about",
|
|
20444
|
+
"assistant",
|
|
20445
|
+
"context",
|
|
20446
|
+
"conversation",
|
|
20447
|
+
"discuss",
|
|
20448
|
+
"discussed",
|
|
20449
|
+
"evidence",
|
|
20450
|
+
"focused",
|
|
20451
|
+
"from",
|
|
20452
|
+
"have",
|
|
20453
|
+
"has",
|
|
20454
|
+
"had",
|
|
20455
|
+
"linked",
|
|
20456
|
+
"memory",
|
|
20457
|
+
"mention",
|
|
20458
|
+
"mentioned",
|
|
20459
|
+
"metadata",
|
|
20460
|
+
"observation",
|
|
20461
|
+
"pipeline",
|
|
20462
|
+
"question",
|
|
20463
|
+
"recall",
|
|
20464
|
+
"recalled",
|
|
20465
|
+
"remnic",
|
|
20466
|
+
"said",
|
|
20467
|
+
"says",
|
|
20468
|
+
"search",
|
|
20469
|
+
"session",
|
|
20470
|
+
"speaker",
|
|
20471
|
+
"summary",
|
|
20472
|
+
"system",
|
|
20473
|
+
"talked",
|
|
20474
|
+
"that",
|
|
20475
|
+
"their",
|
|
20476
|
+
"them",
|
|
20477
|
+
"they",
|
|
20478
|
+
"this",
|
|
20479
|
+
"told",
|
|
20480
|
+
"user",
|
|
20481
|
+
"were",
|
|
20482
|
+
"with"
|
|
20483
|
+
]);
|
|
18257
20484
|
function expandLoCoMoQuestionTokens(tokens) {
|
|
18258
20485
|
const expanded = new Set(tokens);
|
|
18259
20486
|
if (tokens.has("when")) {
|
|
@@ -32446,6 +34673,31 @@ async function runScenario(scenario, adapter, maintenanceCycles, uptakeLatencyCa
|
|
|
32446
34673
|
metrics
|
|
32447
34674
|
};
|
|
32448
34675
|
}
|
|
34676
|
+
function buildMemCorrectJudgeRequest(scenario, log) {
|
|
34677
|
+
const recalledFor = (phase) => log.find(
|
|
34678
|
+
(entry) => entry.phase === phase && entry.namespace === scenario.namespace && entry.query === scenario.probe.query
|
|
34679
|
+
)?.recalled ?? [];
|
|
34680
|
+
return {
|
|
34681
|
+
taskId: scenario.id,
|
|
34682
|
+
query: scenario.probe.query,
|
|
34683
|
+
retiredContent: scenario.correction.retiredContent,
|
|
34684
|
+
correctedContent: scenario.correction.correctedContent,
|
|
34685
|
+
postCorrectionRecall: recalledFor("post_correction"),
|
|
34686
|
+
postMaintenanceRecall: recalledFor("post_maintenance"),
|
|
34687
|
+
postReingestRecall: recalledFor("post_reingest")
|
|
34688
|
+
};
|
|
34689
|
+
}
|
|
34690
|
+
function safeMemCorrectJudgeDetails(result) {
|
|
34691
|
+
return {
|
|
34692
|
+
score: result.score,
|
|
34693
|
+
decision: result.decision,
|
|
34694
|
+
model: result.model ?? "unknown",
|
|
34695
|
+
rubricVersion: result.rubricVersion,
|
|
34696
|
+
inputTokens: result.tokens.input,
|
|
34697
|
+
outputTokens: result.tokens.output,
|
|
34698
|
+
latencyMs: result.latencyMs
|
|
34699
|
+
};
|
|
34700
|
+
}
|
|
32449
34701
|
async function runMemCorrectBenchmark(options) {
|
|
32450
34702
|
const baseOptions = options.mode === "quick" ? QUICK_OPTIONS2 : FULL_OPTIONS2;
|
|
32451
34703
|
const seed = typeof options.seed === "number" ? options.seed : baseOptions.seed;
|
|
@@ -32458,6 +34710,13 @@ async function runMemCorrectBenchmark(options) {
|
|
|
32458
34710
|
);
|
|
32459
34711
|
}
|
|
32460
34712
|
const { adapter, adapterLabel } = resolveAdapter(options);
|
|
34713
|
+
const judge = options.memCorrectJudge ?? options.system.judge;
|
|
34714
|
+
const hasSpecializedJudge = judge?.judgeMemCorrectCorrectionAcceptance !== void 0 && judge.judgeMemCorrectStaleMemoryHarm !== void 0;
|
|
34715
|
+
if (options.judgeProvider?.provider === "openai" && !hasSpecializedJudge) {
|
|
34716
|
+
throw new Error(
|
|
34717
|
+
"OpenAI MemCorrect judging requires the Responses API specialized correction and stale-harm judge methods."
|
|
34718
|
+
);
|
|
34719
|
+
}
|
|
32461
34720
|
const { limit } = options;
|
|
32462
34721
|
let scenarios;
|
|
32463
34722
|
if (typeof limit !== "number") {
|
|
@@ -32477,6 +34736,10 @@ async function runMemCorrectBenchmark(options) {
|
|
|
32477
34736
|
const aggregateCollateralBefore = [];
|
|
32478
34737
|
const aggregateCollateralAfter = [];
|
|
32479
34738
|
const aggregateProvenance = [];
|
|
34739
|
+
let judgeModelCalls = 0;
|
|
34740
|
+
let judgeInputTokens = 0;
|
|
34741
|
+
let judgeOutputTokens = 0;
|
|
34742
|
+
let judgeLatencyMs = 0;
|
|
32480
34743
|
for (const scenario of scenarios) {
|
|
32481
34744
|
const started = performance.now();
|
|
32482
34745
|
const run = await runScenario(
|
|
@@ -32485,7 +34748,6 @@ async function runMemCorrectBenchmark(options) {
|
|
|
32485
34748
|
generatorOptions.maintenanceCycles,
|
|
32486
34749
|
generatorOptions.uptakeLatencyCap
|
|
32487
34750
|
);
|
|
32488
|
-
const latencyMs = Math.round(performance.now() - started);
|
|
32489
34751
|
aggregateLog.push(...run.log);
|
|
32490
34752
|
aggregateCorrections.push(run.correction);
|
|
32491
34753
|
aggregateAntiEvents.push(...run.antiEvents);
|
|
@@ -32503,6 +34765,26 @@ async function runMemCorrectBenchmark(options) {
|
|
|
32503
34765
|
};
|
|
32504
34766
|
if (m.scope_precision !== null) scores.scope_precision = m.scope_precision;
|
|
32505
34767
|
if (m.reassertion !== null) scores.reassertion = m.reassertion;
|
|
34768
|
+
let correctionJudge;
|
|
34769
|
+
let staleHarmJudge;
|
|
34770
|
+
if (hasSpecializedJudge) {
|
|
34771
|
+
const request = buildMemCorrectJudgeRequest(scenario, run.log);
|
|
34772
|
+
correctionJudge = await judge.judgeMemCorrectCorrectionAcceptance(request);
|
|
34773
|
+
staleHarmJudge = await judge.judgeMemCorrectStaleMemoryHarm(request);
|
|
34774
|
+
scores.judge_correction_acceptance = correctionJudge.score;
|
|
34775
|
+
scores.judge_stale_harm_avoidance = staleHarmJudge.score;
|
|
34776
|
+
for (const verdict of [correctionJudge, staleHarmJudge]) {
|
|
34777
|
+
judgeModelCalls += 1;
|
|
34778
|
+
judgeInputTokens += verdict.tokens.input;
|
|
34779
|
+
judgeOutputTokens += verdict.tokens.output;
|
|
34780
|
+
judgeLatencyMs += verdict.latencyMs;
|
|
34781
|
+
}
|
|
34782
|
+
}
|
|
34783
|
+
const taskTokens = {
|
|
34784
|
+
input: (correctionJudge?.tokens.input ?? 0) + (staleHarmJudge?.tokens.input ?? 0),
|
|
34785
|
+
output: (correctionJudge?.tokens.output ?? 0) + (staleHarmJudge?.tokens.output ?? 0)
|
|
34786
|
+
};
|
|
34787
|
+
const latencyMs = Math.round(performance.now() - started);
|
|
32506
34788
|
const task = {
|
|
32507
34789
|
taskId: scenario.id,
|
|
32508
34790
|
question: scenario.probe.query,
|
|
@@ -32519,7 +34801,7 @@ async function runMemCorrectBenchmark(options) {
|
|
|
32519
34801
|
}),
|
|
32520
34802
|
scores,
|
|
32521
34803
|
latencyMs,
|
|
32522
|
-
tokens:
|
|
34804
|
+
tokens: taskTokens,
|
|
32523
34805
|
details: {
|
|
32524
34806
|
scenarioId: scenario.id,
|
|
32525
34807
|
shape: scenario.correction.shape,
|
|
@@ -32528,7 +34810,13 @@ async function runMemCorrectBenchmark(options) {
|
|
|
32528
34810
|
adapter: adapterLabel,
|
|
32529
34811
|
metrics: {
|
|
32530
34812
|
memcorrect: m
|
|
32531
|
-
}
|
|
34813
|
+
},
|
|
34814
|
+
...correctionJudge && staleHarmJudge ? {
|
|
34815
|
+
judges: {
|
|
34816
|
+
correctionAcceptance: safeMemCorrectJudgeDetails(correctionJudge),
|
|
34817
|
+
staleMemoryHarmAvoidance: safeMemCorrectJudgeDetails(staleHarmJudge)
|
|
34818
|
+
}
|
|
34819
|
+
} : {}
|
|
32532
34820
|
}
|
|
32533
34821
|
};
|
|
32534
34822
|
tasks.push(task);
|
|
@@ -32546,6 +34834,7 @@ async function runMemCorrectBenchmark(options) {
|
|
|
32546
34834
|
});
|
|
32547
34835
|
const remnicVersion = await getRemnicVersion();
|
|
32548
34836
|
const totalLatencyMs = tasks.reduce((sum, t) => sum + t.latencyMs, 0);
|
|
34837
|
+
const totalTokens = judgeInputTokens + judgeOutputTokens;
|
|
32549
34838
|
const { adapter: _liveAdapter, ...persistableBenchmarkOptions } = options.benchmarkOptions ?? {};
|
|
32550
34839
|
return {
|
|
32551
34840
|
meta: {
|
|
@@ -32573,6 +34862,14 @@ async function runMemCorrectBenchmark(options) {
|
|
|
32573
34862
|
factsPerPersona: generatorOptions.factsPerPersona,
|
|
32574
34863
|
maintenanceCycles: generatorOptions.maintenanceCycles,
|
|
32575
34864
|
uptakeLatencyCap: generatorOptions.uptakeLatencyCap,
|
|
34865
|
+
...judgeModelCalls > 0 ? {
|
|
34866
|
+
judgeTelemetry: {
|
|
34867
|
+
calls: judgeModelCalls,
|
|
34868
|
+
inputTokens: judgeInputTokens,
|
|
34869
|
+
outputTokens: judgeOutputTokens,
|
|
34870
|
+
latencyMs: judgeLatencyMs
|
|
34871
|
+
}
|
|
34872
|
+
} : {},
|
|
32576
34873
|
// Headline metric bundle computed across the union of all scenario
|
|
32577
34874
|
// probe logs (more robust than the per-task mean for fraction
|
|
32578
34875
|
// metrics when scenario sizes vary). `aggregateTaskScores` in
|
|
@@ -32581,13 +34878,13 @@ async function runMemCorrectBenchmark(options) {
|
|
|
32581
34878
|
}
|
|
32582
34879
|
},
|
|
32583
34880
|
cost: {
|
|
32584
|
-
totalTokens
|
|
32585
|
-
inputTokens:
|
|
32586
|
-
outputTokens:
|
|
34881
|
+
totalTokens,
|
|
34882
|
+
inputTokens: judgeInputTokens,
|
|
34883
|
+
outputTokens: judgeOutputTokens,
|
|
32587
34884
|
estimatedCostUsd: 0,
|
|
32588
34885
|
totalLatencyMs,
|
|
32589
34886
|
meanQueryLatencyMs: tasks.length > 0 ? totalLatencyMs / tasks.length : 0,
|
|
32590
|
-
judgeModelCalls
|
|
34887
|
+
judgeModelCalls
|
|
32591
34888
|
},
|
|
32592
34889
|
results: {
|
|
32593
34890
|
tasks,
|
|
@@ -34326,10 +36623,20 @@ async function runBenchmark(benchmarkId, options) {
|
|
|
34326
36623
|
}
|
|
34327
36624
|
const primaryCalls = judgeCacheCounters?.modelCalls ?? 0;
|
|
34328
36625
|
const crossCalls = crossJudgeCacheCounters?.modelCalls ?? 0;
|
|
34329
|
-
if (judgeCacheCounters !== void 0 || crossJudgeCacheCounters !== void 0) {
|
|
36626
|
+
if (benchmarkId !== "memcorrect-v1" && (judgeCacheCounters !== void 0 || crossJudgeCacheCounters !== void 0)) {
|
|
34330
36627
|
result.cost.judgeModelCalls = primaryCalls + crossCalls;
|
|
34331
36628
|
}
|
|
34332
|
-
|
|
36629
|
+
const finalized = finalizeBenchmarkResultConfig(result, options);
|
|
36630
|
+
assertCompleteBenchmarkResult(finalized);
|
|
36631
|
+
return finalized;
|
|
36632
|
+
}
|
|
36633
|
+
function assertCompleteBenchmarkResult(result) {
|
|
36634
|
+
if (result.meta.status !== "partial") {
|
|
36635
|
+
return;
|
|
36636
|
+
}
|
|
36637
|
+
throw new Error(
|
|
36638
|
+
`Benchmark "${result.meta.benchmark}" produced a partial result: ${result.meta.failureReason ?? "unknown benchmark failure"}`
|
|
36639
|
+
);
|
|
34333
36640
|
}
|
|
34334
36641
|
function wrapJudgeWithCache(args) {
|
|
34335
36642
|
const crossJudgeIdSuffix = args.role === "cross" ? "-cross" : "";
|
|
@@ -34788,6 +37095,273 @@ function getBenchmarkLowerIsBetter(benchmarkId) {
|
|
|
34788
37095
|
return LOWER_IS_BETTER_BY_BENCHMARK[benchmarkId] ?? /* @__PURE__ */ new Set();
|
|
34789
37096
|
}
|
|
34790
37097
|
|
|
37098
|
+
// src/stats/locomo-profile-delta.ts
|
|
37099
|
+
var LOCOMO_CATEGORY_ORDER = [
|
|
37100
|
+
"single_hop",
|
|
37101
|
+
"multi_hop",
|
|
37102
|
+
"temporal",
|
|
37103
|
+
"open_domain",
|
|
37104
|
+
"adversarial"
|
|
37105
|
+
];
|
|
37106
|
+
var LOCOMO_TASK_CATEGORY_PATTERN = /-(single_hop|multi_hop|temporal|open_domain|adversarial)$/;
|
|
37107
|
+
function diagnoseLoComoProfileDelta(options) {
|
|
37108
|
+
const maxRegressions = options.maxRegressions ?? 20;
|
|
37109
|
+
if (!Number.isInteger(maxRegressions) || maxRegressions < 0) {
|
|
37110
|
+
throw new Error("maxRegressions must be a non-negative integer.");
|
|
37111
|
+
}
|
|
37112
|
+
assertComparableArtifacts(options.baseline.artifact, options.real.artifact);
|
|
37113
|
+
const joined = joinTasks(options.baseline.artifact, options.real.artifact);
|
|
37114
|
+
const metrics = collectMetrics(joined);
|
|
37115
|
+
const primaryMetric2 = options.primaryMetric ?? "llm_judge";
|
|
37116
|
+
if (!metrics.includes(primaryMetric2)) {
|
|
37117
|
+
throw new Error(
|
|
37118
|
+
`Primary metric ${JSON.stringify(primaryMetric2)} is not present on every joined task.`
|
|
37119
|
+
);
|
|
37120
|
+
}
|
|
37121
|
+
const overall = summarizeMetrics(joined, metrics, joined.length);
|
|
37122
|
+
verifyPublishedMetricMeans(options.baseline.artifact, overall, "baseline");
|
|
37123
|
+
verifyPublishedMetricMeans(options.real.artifact, overall, "real");
|
|
37124
|
+
const categories = [...new Set(joined.map((task) => task.category))].sort(compareLoComoCategories).map((category) => {
|
|
37125
|
+
const tasks = joined.filter((task) => task.category === category);
|
|
37126
|
+
return {
|
|
37127
|
+
category,
|
|
37128
|
+
taskCount: tasks.length,
|
|
37129
|
+
metrics: summarizeMetrics(tasks, metrics, joined.length)
|
|
37130
|
+
};
|
|
37131
|
+
});
|
|
37132
|
+
const topRegressions = joined.map((task) => ({
|
|
37133
|
+
taskId: task.taskId,
|
|
37134
|
+
category: task.category,
|
|
37135
|
+
baselineScore: task.baseline.scores[primaryMetric2],
|
|
37136
|
+
realScore: task.real.scores[primaryMetric2],
|
|
37137
|
+
delta: task.real.scores[primaryMetric2] - task.baseline.scores[primaryMetric2]
|
|
37138
|
+
})).filter((task) => task.delta < 0).sort((left, right) => left.delta - right.delta || left.taskId.localeCompare(right.taskId)).slice(0, maxRegressions);
|
|
37139
|
+
const baselineArtifact = options.baseline.artifact;
|
|
37140
|
+
return {
|
|
37141
|
+
schemaVersion: 1,
|
|
37142
|
+
benchmarkId: "locomo",
|
|
37143
|
+
comparison: {
|
|
37144
|
+
baseline: {
|
|
37145
|
+
reference: options.baseline.reference,
|
|
37146
|
+
sha256: options.baseline.sha256
|
|
37147
|
+
},
|
|
37148
|
+
real: {
|
|
37149
|
+
reference: options.real.reference,
|
|
37150
|
+
sha256: options.real.sha256
|
|
37151
|
+
},
|
|
37152
|
+
datasetVersion: baselineArtifact.datasetVersion,
|
|
37153
|
+
model: baselineArtifact.model,
|
|
37154
|
+
seed: baselineArtifact.seed,
|
|
37155
|
+
gitSha: baselineArtifact.system.gitSha,
|
|
37156
|
+
tier: baselineArtifact.tier ?? "frontier"
|
|
37157
|
+
},
|
|
37158
|
+
taskCount: joined.length,
|
|
37159
|
+
primaryMetric: primaryMetric2,
|
|
37160
|
+
metrics,
|
|
37161
|
+
overall,
|
|
37162
|
+
categories,
|
|
37163
|
+
topRegressions,
|
|
37164
|
+
evidenceBoundary: {
|
|
37165
|
+
scoreDiagnosis: "complete",
|
|
37166
|
+
recallRootCause: "requires-paired-recall-receipts"
|
|
37167
|
+
}
|
|
37168
|
+
};
|
|
37169
|
+
}
|
|
37170
|
+
function renderLoComoProfileDeltaMarkdown(report) {
|
|
37171
|
+
const primary = report.primaryMetric;
|
|
37172
|
+
const lines = [
|
|
37173
|
+
"# LoCoMo runtime-profile score diagnosis",
|
|
37174
|
+
"",
|
|
37175
|
+
`Joined ${report.taskCount} identical task ids from the baseline and real artifacts. The primary diagnostic metric is \`${primary}\` (real minus baseline).`,
|
|
37176
|
+
"",
|
|
37177
|
+
"| Category | Tasks | Baseline | Real | Delta | Aggregate contribution | Wins | Losses | Ties |",
|
|
37178
|
+
"|---|---:|---:|---:|---:|---:|---:|---:|---:|"
|
|
37179
|
+
];
|
|
37180
|
+
for (const category of report.categories) {
|
|
37181
|
+
const metric = category.metrics[primary];
|
|
37182
|
+
lines.push(
|
|
37183
|
+
`| ${category.category} | ${category.taskCount} | ${formatScore(metric.baselineMean)} | ${formatScore(metric.realMean)} | ${formatSignedScore(metric.delta)} | ${formatSignedScore(metric.aggregateContribution)} | ${metric.wins} | ${metric.losses} | ${metric.ties} |`
|
|
37184
|
+
);
|
|
37185
|
+
}
|
|
37186
|
+
const overall = report.overall[primary];
|
|
37187
|
+
lines.push(
|
|
37188
|
+
`| **Overall** | **${report.taskCount}** | **${formatScore(overall.baselineMean)}** | **${formatScore(overall.realMean)}** | **${formatSignedScore(overall.delta)}** | **${formatSignedScore(overall.aggregateContribution)}** | **${overall.wins}** | **${overall.losses}** | **${overall.ties}** |`,
|
|
37189
|
+
"",
|
|
37190
|
+
"## Highest-priority paired recall samples",
|
|
37191
|
+
"",
|
|
37192
|
+
`These task ids have the largest negative \`${primary}\` deltas. They are candidates for paired recall X-ray capture; score artifacts alone do not identify what recall tier served or what evidence was displaced.`,
|
|
37193
|
+
"",
|
|
37194
|
+
"| Task | Category | Baseline | Real | Delta |",
|
|
37195
|
+
"|---|---|---:|---:|---:|"
|
|
37196
|
+
);
|
|
37197
|
+
for (const task of report.topRegressions) {
|
|
37198
|
+
lines.push(
|
|
37199
|
+
`| ${task.taskId} | ${task.category} | ${formatScore(task.baselineScore)} | ${formatScore(task.realScore)} | ${formatSignedScore(task.delta)} |`
|
|
37200
|
+
);
|
|
37201
|
+
}
|
|
37202
|
+
lines.push(
|
|
37203
|
+
"",
|
|
37204
|
+
"## Evidence boundary",
|
|
37205
|
+
"",
|
|
37206
|
+
"The paired score diagnosis is complete for these artifacts. A recall-side root cause is not established until paired recall receipts exist for the same task ids and runtime profiles.",
|
|
37207
|
+
"",
|
|
37208
|
+
`Baseline: \`${report.comparison.baseline.reference}\` (\`${report.comparison.baseline.sha256}\`)`,
|
|
37209
|
+
"",
|
|
37210
|
+
`Real: \`${report.comparison.real.reference}\` (\`${report.comparison.real.sha256}\`)`,
|
|
37211
|
+
""
|
|
37212
|
+
);
|
|
37213
|
+
return `${lines.join("\n")}
|
|
37214
|
+
`;
|
|
37215
|
+
}
|
|
37216
|
+
function assertComparableArtifacts(baseline, real) {
|
|
37217
|
+
if (baseline.benchmarkId !== "locomo" || real.benchmarkId !== "locomo") {
|
|
37218
|
+
throw new Error("LoCoMo profile diagnosis requires two locomo artifacts.");
|
|
37219
|
+
}
|
|
37220
|
+
const checks = [
|
|
37221
|
+
["datasetVersion", baseline.datasetVersion, real.datasetVersion],
|
|
37222
|
+
["model", baseline.model, real.model],
|
|
37223
|
+
["seed", baseline.seed, real.seed],
|
|
37224
|
+
["system.gitSha", baseline.system.gitSha, real.system.gitSha],
|
|
37225
|
+
["tier", baseline.tier ?? "frontier", real.tier ?? "frontier"]
|
|
37226
|
+
];
|
|
37227
|
+
for (const [field, baselineValue, realValue] of checks) {
|
|
37228
|
+
if (baselineValue !== realValue) {
|
|
37229
|
+
throw new Error(
|
|
37230
|
+
`Artifacts are not comparable: ${field} differs (${JSON.stringify(baselineValue)} vs ${JSON.stringify(realValue)}).`
|
|
37231
|
+
);
|
|
37232
|
+
}
|
|
37233
|
+
}
|
|
37234
|
+
}
|
|
37235
|
+
function joinTasks(baseline, real) {
|
|
37236
|
+
const baselineTasks = indexTasks(baseline.perTaskScores, "baseline");
|
|
37237
|
+
const realTasks = indexTasks(real.perTaskScores, "real");
|
|
37238
|
+
const missingFromReal = [...baselineTasks.keys()].filter((id) => !realTasks.has(id)).sort();
|
|
37239
|
+
const missingFromBaseline = [...realTasks.keys()].filter((id) => !baselineTasks.has(id)).sort();
|
|
37240
|
+
if (missingFromReal.length > 0 || missingFromBaseline.length > 0) {
|
|
37241
|
+
throw new Error(
|
|
37242
|
+
`Artifacts do not contain identical task-id sets: ${missingFromReal.length} missing from real, ${missingFromBaseline.length} missing from baseline.`
|
|
37243
|
+
);
|
|
37244
|
+
}
|
|
37245
|
+
return [...baselineTasks.keys()].sort().map((taskId) => {
|
|
37246
|
+
const baselineTask = baselineTasks.get(taskId);
|
|
37247
|
+
const realTask = realTasks.get(taskId);
|
|
37248
|
+
const baselineCategory = resolveLoComoCategory(baselineTask);
|
|
37249
|
+
const realCategory = resolveLoComoCategory(realTask);
|
|
37250
|
+
if (baselineCategory !== realCategory) {
|
|
37251
|
+
throw new Error(
|
|
37252
|
+
`Task ${JSON.stringify(taskId)} has mismatched categories (${baselineCategory} vs ${realCategory}).`
|
|
37253
|
+
);
|
|
37254
|
+
}
|
|
37255
|
+
return {
|
|
37256
|
+
taskId,
|
|
37257
|
+
category: baselineCategory,
|
|
37258
|
+
baseline: baselineTask,
|
|
37259
|
+
real: realTask
|
|
37260
|
+
};
|
|
37261
|
+
});
|
|
37262
|
+
}
|
|
37263
|
+
function indexTasks(tasks, label) {
|
|
37264
|
+
const indexed = /* @__PURE__ */ new Map();
|
|
37265
|
+
for (const task of tasks) {
|
|
37266
|
+
if (indexed.has(task.taskId)) {
|
|
37267
|
+
throw new Error(`${label} artifact contains duplicate task id ${JSON.stringify(task.taskId)}.`);
|
|
37268
|
+
}
|
|
37269
|
+
indexed.set(task.taskId, task);
|
|
37270
|
+
}
|
|
37271
|
+
if (indexed.size === 0) {
|
|
37272
|
+
throw new Error(`${label} artifact contains no per-task scores.`);
|
|
37273
|
+
}
|
|
37274
|
+
return indexed;
|
|
37275
|
+
}
|
|
37276
|
+
function resolveLoComoCategory(task) {
|
|
37277
|
+
if (task.category) {
|
|
37278
|
+
return task.category;
|
|
37279
|
+
}
|
|
37280
|
+
const match = task.taskId.match(LOCOMO_TASK_CATEGORY_PATTERN);
|
|
37281
|
+
if (!match?.[1]) {
|
|
37282
|
+
throw new Error(
|
|
37283
|
+
`Cannot derive a LoCoMo category from task id ${JSON.stringify(task.taskId)}.`
|
|
37284
|
+
);
|
|
37285
|
+
}
|
|
37286
|
+
return match[1];
|
|
37287
|
+
}
|
|
37288
|
+
function collectMetrics(joined) {
|
|
37289
|
+
const expected = Object.keys(joined[0].baseline.scores).sort();
|
|
37290
|
+
if (expected.length === 0) {
|
|
37291
|
+
throw new Error("Joined tasks contain no metrics.");
|
|
37292
|
+
}
|
|
37293
|
+
for (const task of joined) {
|
|
37294
|
+
for (const [label, scores] of [
|
|
37295
|
+
["baseline", task.baseline.scores],
|
|
37296
|
+
["real", task.real.scores]
|
|
37297
|
+
]) {
|
|
37298
|
+
const actual = Object.keys(scores).sort();
|
|
37299
|
+
if (actual.length !== expected.length || actual.some((metric, index) => metric !== expected[index])) {
|
|
37300
|
+
throw new Error(
|
|
37301
|
+
`Task ${JSON.stringify(task.taskId)} ${label} metric set does not match the joined metric set.`
|
|
37302
|
+
);
|
|
37303
|
+
}
|
|
37304
|
+
}
|
|
37305
|
+
}
|
|
37306
|
+
return expected;
|
|
37307
|
+
}
|
|
37308
|
+
function summarizeMetrics(tasks, metrics, totalTaskCount) {
|
|
37309
|
+
return Object.fromEntries(metrics.map((metric) => {
|
|
37310
|
+
let baselineSum = 0;
|
|
37311
|
+
let realSum = 0;
|
|
37312
|
+
let wins = 0;
|
|
37313
|
+
let losses = 0;
|
|
37314
|
+
let ties = 0;
|
|
37315
|
+
for (const task of tasks) {
|
|
37316
|
+
const baselineScore = task.baseline.scores[metric];
|
|
37317
|
+
const realScore = task.real.scores[metric];
|
|
37318
|
+
baselineSum += baselineScore;
|
|
37319
|
+
realSum += realScore;
|
|
37320
|
+
if (realScore > baselineScore) wins += 1;
|
|
37321
|
+
else if (realScore < baselineScore) losses += 1;
|
|
37322
|
+
else ties += 1;
|
|
37323
|
+
}
|
|
37324
|
+
const deltaSum = realSum - baselineSum;
|
|
37325
|
+
return [metric, {
|
|
37326
|
+
baselineMean: baselineSum / tasks.length,
|
|
37327
|
+
realMean: realSum / tasks.length,
|
|
37328
|
+
delta: deltaSum / tasks.length,
|
|
37329
|
+
aggregateContribution: deltaSum / totalTaskCount,
|
|
37330
|
+
wins,
|
|
37331
|
+
losses,
|
|
37332
|
+
ties
|
|
37333
|
+
}];
|
|
37334
|
+
}));
|
|
37335
|
+
}
|
|
37336
|
+
function verifyPublishedMetricMeans(artifact, overall, side) {
|
|
37337
|
+
for (const [metric, summary] of Object.entries(overall)) {
|
|
37338
|
+
const published = artifact.metrics[metric];
|
|
37339
|
+
if (published === void 0) {
|
|
37340
|
+
throw new Error(`${side} artifact does not publish aggregate metric ${JSON.stringify(metric)}.`);
|
|
37341
|
+
}
|
|
37342
|
+
const computed = side === "baseline" ? summary.baselineMean : summary.realMean;
|
|
37343
|
+
if (Math.abs(published - computed) > 1e-12) {
|
|
37344
|
+
throw new Error(
|
|
37345
|
+
`${side} artifact aggregate ${metric}=${published} does not match its per-task mean ${computed}.`
|
|
37346
|
+
);
|
|
37347
|
+
}
|
|
37348
|
+
}
|
|
37349
|
+
}
|
|
37350
|
+
function compareLoComoCategories(left, right) {
|
|
37351
|
+
const leftIndex = LOCOMO_CATEGORY_ORDER.indexOf(left);
|
|
37352
|
+
const rightIndex = LOCOMO_CATEGORY_ORDER.indexOf(right);
|
|
37353
|
+
if (leftIndex >= 0 && rightIndex >= 0) return leftIndex - rightIndex;
|
|
37354
|
+
if (leftIndex >= 0) return -1;
|
|
37355
|
+
if (rightIndex >= 0) return 1;
|
|
37356
|
+
return left.localeCompare(right);
|
|
37357
|
+
}
|
|
37358
|
+
function formatScore(value) {
|
|
37359
|
+
return value.toFixed(4);
|
|
37360
|
+
}
|
|
37361
|
+
function formatSignedScore(value) {
|
|
37362
|
+
return `${value >= 0 ? "+" : ""}${formatScore(value)}`;
|
|
37363
|
+
}
|
|
37364
|
+
|
|
34791
37365
|
// src/integrity/sealed-qrels.ts
|
|
34792
37366
|
import { readFile as readFile19 } from "fs/promises";
|
|
34793
37367
|
function isSealedQrelsArtifact(value) {
|
|
@@ -36196,6 +38770,8 @@ import { mkdir as mkdir17, readFile as readFile21, rename as rename3, unlink as
|
|
|
36196
38770
|
import path36 from "path";
|
|
36197
38771
|
|
|
36198
38772
|
// src/judges/cohen-kappa.ts
|
|
38773
|
+
var DEFAULT_KAPPA_BOOTSTRAP_SAMPLES = 2e3;
|
|
38774
|
+
var DEFAULT_KAPPA_CONFIDENCE_LEVEL = 0.95;
|
|
36199
38775
|
function computeCohensKappa(raterA, raterB) {
|
|
36200
38776
|
if (raterA.length !== raterB.length) {
|
|
36201
38777
|
throw new Error(
|
|
@@ -36241,6 +38817,74 @@ function computeCohensKappa(raterA, raterB) {
|
|
|
36241
38817
|
categories: [...categories].sort()
|
|
36242
38818
|
};
|
|
36243
38819
|
}
|
|
38820
|
+
function bootstrapCohensKappaConfidenceInterval(raterA, raterB, options = {}) {
|
|
38821
|
+
if (raterA.length !== raterB.length) {
|
|
38822
|
+
throw new Error(
|
|
38823
|
+
`bootstrapCohensKappaConfidenceInterval: rater arrays must have equal length; got ${raterA.length} and ${raterB.length}.`
|
|
38824
|
+
);
|
|
38825
|
+
}
|
|
38826
|
+
if (raterA.length === 0) {
|
|
38827
|
+
throw new Error("bootstrapCohensKappaConfidenceInterval: cannot bootstrap zero paired judgements.");
|
|
38828
|
+
}
|
|
38829
|
+
const iterations = options.iterations ?? DEFAULT_KAPPA_BOOTSTRAP_SAMPLES;
|
|
38830
|
+
const level = options.level ?? DEFAULT_KAPPA_CONFIDENCE_LEVEL;
|
|
38831
|
+
if (!Number.isInteger(iterations) || iterations <= 0) {
|
|
38832
|
+
throw new Error(`bootstrapCohensKappaConfidenceInterval: iterations must be a positive integer; got ${String(iterations)}.`);
|
|
38833
|
+
}
|
|
38834
|
+
if (!(level > 0 && level < 1)) {
|
|
38835
|
+
throw new Error(`bootstrapCohensKappaConfidenceInterval: level must be between 0 and 1; got ${String(level)}.`);
|
|
38836
|
+
}
|
|
38837
|
+
const derivedSeed = options.seed ?? hashLabelsToSeed(raterA, raterB);
|
|
38838
|
+
const random = mulberry323(derivedSeed >>> 0);
|
|
38839
|
+
const kappas = [];
|
|
38840
|
+
for (let iteration = 0; iteration < iterations; iteration += 1) {
|
|
38841
|
+
const sampleA = [];
|
|
38842
|
+
const sampleB = [];
|
|
38843
|
+
for (let index = 0; index < raterA.length; index += 1) {
|
|
38844
|
+
const picked = Math.floor(random() * raterA.length);
|
|
38845
|
+
sampleA.push(raterA[picked]);
|
|
38846
|
+
sampleB.push(raterB[picked]);
|
|
38847
|
+
}
|
|
38848
|
+
kappas.push(computeCohensKappa(sampleA, sampleB).kappa);
|
|
38849
|
+
}
|
|
38850
|
+
kappas.sort((left, right) => left - right);
|
|
38851
|
+
const tail = (1 - level) / 2;
|
|
38852
|
+
return {
|
|
38853
|
+
confidenceInterval: {
|
|
38854
|
+
lower: percentile2(kappas, tail),
|
|
38855
|
+
upper: percentile2(kappas, 1 - tail),
|
|
38856
|
+
level
|
|
38857
|
+
},
|
|
38858
|
+
bootstrapSamples: iterations
|
|
38859
|
+
};
|
|
38860
|
+
}
|
|
38861
|
+
function percentile2(sortedValues, fraction) {
|
|
38862
|
+
const position = (sortedValues.length - 1) * fraction;
|
|
38863
|
+
const lowerIndex = Math.floor(position);
|
|
38864
|
+
const upperIndex = Math.ceil(position);
|
|
38865
|
+
const lower = sortedValues[lowerIndex];
|
|
38866
|
+
const upper = sortedValues[upperIndex];
|
|
38867
|
+
return lowerIndex === upperIndex ? lower : lower + (upper - lower) * (position - lowerIndex);
|
|
38868
|
+
}
|
|
38869
|
+
function hashLabelsToSeed(raterA, raterB) {
|
|
38870
|
+
const value = JSON.stringify([raterA, raterB]);
|
|
38871
|
+
let hash = 2166136261;
|
|
38872
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
38873
|
+
hash ^= value.charCodeAt(index);
|
|
38874
|
+
hash = Math.imul(hash, 16777619);
|
|
38875
|
+
}
|
|
38876
|
+
return hash >>> 0;
|
|
38877
|
+
}
|
|
38878
|
+
function mulberry323(seed) {
|
|
38879
|
+
let state = seed;
|
|
38880
|
+
return () => {
|
|
38881
|
+
state = state + 1831565813 >>> 0;
|
|
38882
|
+
let value = state;
|
|
38883
|
+
value = Math.imul(value ^ value >>> 15, value | 1);
|
|
38884
|
+
value ^= value + Math.imul(value ^ value >>> 7, value | 61);
|
|
38885
|
+
return ((value ^ value >>> 14) >>> 0) / 4294967296;
|
|
38886
|
+
};
|
|
38887
|
+
}
|
|
36244
38888
|
var DEFAULT_JUDGE_BINARIZATION_THRESHOLD = 0.5;
|
|
36245
38889
|
function binarizeJudgeScore(score, threshold = DEFAULT_JUDGE_BINARIZATION_THRESHOLD) {
|
|
36246
38890
|
if (typeof score !== "number" || !Number.isFinite(score)) {
|
|
@@ -36250,7 +38894,7 @@ function binarizeJudgeScore(score, threshold = DEFAULT_JUDGE_BINARIZATION_THRESH
|
|
|
36250
38894
|
}
|
|
36251
38895
|
|
|
36252
38896
|
// src/judges/calibration-slice.ts
|
|
36253
|
-
var CALIBRATION_SLICE_SIZE =
|
|
38897
|
+
var CALIBRATION_SLICE_SIZE = 200;
|
|
36254
38898
|
var MIN_CALIBRATION_SOURCE_TASKS = 10;
|
|
36255
38899
|
var JUDGE_CALIBRATION_KAPPA_THRESHOLD = 0.7;
|
|
36256
38900
|
function selectCalibrationSlice(questionIds, size = CALIBRATION_SLICE_SIZE) {
|
|
@@ -36274,10 +38918,8 @@ async function runJudgeCalibration(options) {
|
|
|
36274
38918
|
const binScore = options.binScore ?? ((score) => binarizeJudgeScore(score));
|
|
36275
38919
|
const threshold = options.threshold ?? JUDGE_CALIBRATION_KAPPA_THRESHOLD;
|
|
36276
38920
|
const sliceSize = options.sliceSize ?? CALIBRATION_SLICE_SIZE;
|
|
36277
|
-
const
|
|
36278
|
-
|
|
36279
|
-
sliceSize
|
|
36280
|
-
);
|
|
38921
|
+
const availableIds = new Set(options.answers.map((answer) => answer.questionId));
|
|
38922
|
+
const sliceIds = options.pinnedQuestionIds ? validatePinnedQuestionIds(options.pinnedQuestionIds, availableIds) : selectCalibrationSlice([...availableIds], sliceSize);
|
|
36281
38923
|
const sliceIdSet = new Set(sliceIds);
|
|
36282
38924
|
const answerById = /* @__PURE__ */ new Map();
|
|
36283
38925
|
for (const answer of options.answers) {
|
|
@@ -36286,6 +38928,12 @@ async function runJudgeCalibration(options) {
|
|
|
36286
38928
|
}
|
|
36287
38929
|
}
|
|
36288
38930
|
const sliceAnswers = sliceIds.map((id) => answerById.get(id)).filter((answer) => answer !== void 0);
|
|
38931
|
+
const answerSetHash = hashCalibrationAnswerSet(sliceAnswers);
|
|
38932
|
+
if (options.expectedAnswerSetHash !== void 0 && answerSetHash !== options.expectedAnswerSetHash) {
|
|
38933
|
+
throw new Error(
|
|
38934
|
+
`runJudgeCalibration: pinned answer set changed (expected sha256:${options.expectedAnswerSetHash}, got sha256:${answerSetHash}). Restore the original stored result or intentionally reset calibration state.`
|
|
38935
|
+
);
|
|
38936
|
+
}
|
|
36289
38937
|
const localLabels = [];
|
|
36290
38938
|
const frontierLabels = [];
|
|
36291
38939
|
const verdicts = [];
|
|
@@ -36311,6 +38959,10 @@ async function runJudgeCalibration(options) {
|
|
|
36311
38959
|
});
|
|
36312
38960
|
}
|
|
36313
38961
|
const kappaResult = computeCohensKappa(localLabels, frontierLabels);
|
|
38962
|
+
const bootstrap = bootstrapCohensKappaConfidenceInterval(localLabels, frontierLabels, {
|
|
38963
|
+
iterations: options.bootstrapSamples ?? DEFAULT_KAPPA_BOOTSTRAP_SAMPLES,
|
|
38964
|
+
level: options.confidenceLevel
|
|
38965
|
+
});
|
|
36314
38966
|
const warning = kappaResult.kappa < threshold;
|
|
36315
38967
|
return {
|
|
36316
38968
|
...kappaResult,
|
|
@@ -36318,16 +38970,42 @@ async function runJudgeCalibration(options) {
|
|
|
36318
38970
|
sliceQuestionIds: sliceIds,
|
|
36319
38971
|
threshold,
|
|
36320
38972
|
warning,
|
|
38973
|
+
confidenceInterval: bootstrap.confidenceInterval,
|
|
38974
|
+
bootstrapSamples: bootstrap.bootstrapSamples,
|
|
38975
|
+
answerSetHash,
|
|
36321
38976
|
verdicts
|
|
36322
38977
|
};
|
|
36323
38978
|
}
|
|
36324
|
-
|
|
38979
|
+
function validatePinnedQuestionIds(ids, availableIds) {
|
|
38980
|
+
if (ids.length === 0 || ids.length > CALIBRATION_SLICE_SIZE || ids.some((id) => typeof id !== "string" || id.length === 0) || new Set(ids).size !== ids.length) {
|
|
38981
|
+
throw new Error(`runJudgeCalibration: pinned question ids must contain 1 to ${CALIBRATION_SLICE_SIZE} unique non-empty strings.`);
|
|
38982
|
+
}
|
|
38983
|
+
const missing = ids.filter((id) => !availableIds.has(id));
|
|
38984
|
+
if (missing.length > 0) {
|
|
38985
|
+
throw new Error(`runJudgeCalibration: pinned answer set is missing ${missing.length} question id(s), including "${missing[0]}".`);
|
|
38986
|
+
}
|
|
38987
|
+
return [...ids];
|
|
38988
|
+
}
|
|
38989
|
+
function hashCalibrationAnswerSet(answers) {
|
|
38990
|
+
return createHash12("sha256").update(JSON.stringify(answers.map((answer) => [
|
|
38991
|
+
answer.questionId,
|
|
38992
|
+
answer.question,
|
|
38993
|
+
answer.predicted,
|
|
38994
|
+
answer.expected
|
|
38995
|
+
]))).digest("hex");
|
|
38996
|
+
}
|
|
38997
|
+
async function writeJudgeCalibrationState(result, calibrationDir, identities, provenance) {
|
|
36325
38998
|
await mkdir17(calibrationDir, { recursive: true });
|
|
36326
38999
|
const state = {
|
|
36327
39000
|
kappa: result.kappa,
|
|
36328
39001
|
sampleSize: result.sampleSize,
|
|
36329
39002
|
threshold: result.threshold,
|
|
36330
39003
|
warning: result.warning,
|
|
39004
|
+
confidenceInterval: result.confidenceInterval,
|
|
39005
|
+
bootstrapSamples: result.bootstrapSamples,
|
|
39006
|
+
answerSetHash: result.answerSetHash,
|
|
39007
|
+
sliceQuestionIds: result.sliceQuestionIds,
|
|
39008
|
+
...provenance ? provenance : {},
|
|
36331
39009
|
...identities ? identities : {}
|
|
36332
39010
|
};
|
|
36333
39011
|
const filePath = path36.join(calibrationDir, `${sanitizeCalibrationSegment(result.benchmarkId)}.json`);
|
|
@@ -36368,6 +39046,19 @@ async function loadJudgeCalibrationState(benchmarkId, calibrationDir) {
|
|
|
36368
39046
|
return void 0;
|
|
36369
39047
|
}
|
|
36370
39048
|
const loaded = { kappa, sampleSize, threshold, warning };
|
|
39049
|
+
const confidenceInterval = record.confidenceInterval;
|
|
39050
|
+
const bootstrapSamples = record.bootstrapSamples;
|
|
39051
|
+
if (confidenceInterval && typeof confidenceInterval === "object" && !Array.isArray(confidenceInterval) && typeof confidenceInterval.lower === "number" && Number.isFinite(confidenceInterval.lower) && typeof confidenceInterval.upper === "number" && Number.isFinite(confidenceInterval.upper) && typeof confidenceInterval.level === "number" && Number.isFinite(confidenceInterval.level) && typeof bootstrapSamples === "number" && Number.isInteger(bootstrapSamples) && bootstrapSamples > 0) {
|
|
39052
|
+
loaded.confidenceInterval = confidenceInterval;
|
|
39053
|
+
loaded.bootstrapSamples = bootstrapSamples;
|
|
39054
|
+
}
|
|
39055
|
+
const sourceResultId = record.sourceResultId;
|
|
39056
|
+
const answerSetHash = record.answerSetHash;
|
|
39057
|
+
if (typeof sourceResultId === "string" && sourceResultId.length > 0 && typeof answerSetHash === "string" && /^[0-9a-f]{64}$/.test(answerSetHash) && Array.isArray(record.sliceQuestionIds) && record.sliceQuestionIds.length > 0 && record.sliceQuestionIds.length <= CALIBRATION_SLICE_SIZE && record.sliceQuestionIds.length === sampleSize && record.sliceQuestionIds.every((id) => typeof id === "string" && id.length > 0) && new Set(record.sliceQuestionIds).size === record.sliceQuestionIds.length) {
|
|
39058
|
+
loaded.sourceResultId = sourceResultId;
|
|
39059
|
+
loaded.answerSetHash = answerSetHash;
|
|
39060
|
+
loaded.sliceQuestionIds = record.sliceQuestionIds;
|
|
39061
|
+
}
|
|
36371
39062
|
const identityKeys = [
|
|
36372
39063
|
"localJudgeProvider",
|
|
36373
39064
|
"localJudgeModel",
|
|
@@ -37916,7 +40607,7 @@ function captureMachineFingerprint() {
|
|
|
37916
40607
|
totalMemoryMb: Math.round(os11.totalmem() / (1024 * 1024))
|
|
37917
40608
|
};
|
|
37918
40609
|
}
|
|
37919
|
-
function
|
|
40610
|
+
function percentile3(sorted, p) {
|
|
37920
40611
|
if (sorted.length === 0) return 0;
|
|
37921
40612
|
if (sorted.length === 1) return sorted[0];
|
|
37922
40613
|
const idx = Math.ceil(p / 100 * sorted.length) - 1;
|
|
@@ -37925,8 +40616,8 @@ function percentile2(sorted, p) {
|
|
|
37925
40616
|
function computeMicroMetric(samplesMs) {
|
|
37926
40617
|
const sorted = [...samplesMs].sort((a, b) => a - b);
|
|
37927
40618
|
return {
|
|
37928
|
-
p50:
|
|
37929
|
-
p95:
|
|
40619
|
+
p50: percentile3(sorted, 50),
|
|
40620
|
+
p95: percentile3(sorted, 95),
|
|
37930
40621
|
iterations: samplesMs.length,
|
|
37931
40622
|
samplesMs
|
|
37932
40623
|
};
|
|
@@ -37993,7 +40684,7 @@ async function runCodingGraphBenchmark(config = {}) {
|
|
|
37993
40684
|
fullIndexSamples.push(fi.ms);
|
|
37994
40685
|
if (sampleStore !== store) await sampleStore.close();
|
|
37995
40686
|
}
|
|
37996
|
-
const fullIndexMsValue =
|
|
40687
|
+
const fullIndexMsValue = percentile3(
|
|
37997
40688
|
[...fullIndexSamples].sort((a, b) => a - b),
|
|
37998
40689
|
50
|
|
37999
40690
|
);
|
|
@@ -38338,7 +41029,11 @@ export {
|
|
|
38338
41029
|
DEFAULT_ASSISTANT_RUBRIC_ID,
|
|
38339
41030
|
DEFAULT_BASELINE_SCENARIOS,
|
|
38340
41031
|
DEFAULT_JUDGE_BINARIZATION_THRESHOLD,
|
|
41032
|
+
DEFAULT_KAPPA_BOOTSTRAP_SAMPLES,
|
|
41033
|
+
DEFAULT_KAPPA_CONFIDENCE_LEVEL,
|
|
41034
|
+
DEFAULT_OPENAI_RESPONSES_JUDGE_MODEL,
|
|
38341
41035
|
EMPTY_CONTAMINATION_MANIFEST,
|
|
41036
|
+
GENERAL_ANSWER_JUDGE_RUBRIC,
|
|
38342
41037
|
INTEGRITY_CIPHER_ALGORITHM,
|
|
38343
41038
|
INTEGRITY_HASH_ALGORITHM,
|
|
38344
41039
|
INTEGRITY_META_FIELDS,
|
|
@@ -38348,13 +41043,21 @@ export {
|
|
|
38348
41043
|
LONG_MEM_EVAL_DATASET_FILENAMES,
|
|
38349
41044
|
LettaMemCorrectAdapter,
|
|
38350
41045
|
LocalLabPreflightError,
|
|
41046
|
+
MEMCORRECT_CORRECTION_ACCEPTANCE_RUBRIC,
|
|
41047
|
+
MEMCORRECT_CORRECTION_ACCEPTANCE_RUBRIC_VERSION,
|
|
41048
|
+
MEMCORRECT_STALE_HARM_RUBRIC,
|
|
41049
|
+
MEMCORRECT_STALE_HARM_RUBRIC_VERSION,
|
|
38351
41050
|
MEMORY_EVAL_DIMENSIONS,
|
|
38352
41051
|
MEMORY_EVAL_PUBLIC_LINE,
|
|
38353
41052
|
MIN_CALIBRATION_SOURCE_TASKS,
|
|
38354
41053
|
MITIGATED_BASELINE_SCENARIOS,
|
|
41054
|
+
McpMemoryBackendError,
|
|
38355
41055
|
Mem0MemCorrectAdapter,
|
|
38356
41056
|
MissingCredentialError,
|
|
41057
|
+
OPENAI_RESPONSES_JUDGE_RUBRIC_VERSION,
|
|
38357
41058
|
OTHER_NAMESPACE_MEMORIES,
|
|
41059
|
+
OpenAiResponsesJudgeError,
|
|
41060
|
+
OpenAiResponsesProvider,
|
|
38358
41061
|
PROCEDURAL_REAL_SCENARIOS,
|
|
38359
41062
|
PROCEDURAL_REAL_SCENARIOS_SMOKE,
|
|
38360
41063
|
PUBLISHED_BENCHMARK_ARTIFACT_IDS,
|
|
@@ -38378,6 +41081,7 @@ export {
|
|
|
38378
41081
|
assistantSynthesisDefinition,
|
|
38379
41082
|
backlinkF1,
|
|
38380
41083
|
binarizeJudgeScore,
|
|
41084
|
+
bootstrapCohensKappaConfidenceInterval,
|
|
38381
41085
|
bootstrapMeanConfidenceInterval,
|
|
38382
41086
|
buildAmaBenchDiagnosticMatrixArtifact,
|
|
38383
41087
|
buildAmaBenchDiagnosticVariantSummary,
|
|
@@ -38417,9 +41121,15 @@ export {
|
|
|
38417
41121
|
createLightweightAdapter,
|
|
38418
41122
|
createLiteLlmProvider,
|
|
38419
41123
|
createLocalLlmProvider,
|
|
41124
|
+
createMcpDemoMemCorrectAdapter,
|
|
41125
|
+
createMcpDemoMemoryAdapter,
|
|
41126
|
+
createMcpMemCorrectAdapter,
|
|
41127
|
+
createMcpMemoryAdapter,
|
|
38420
41128
|
createMitigatedTarget,
|
|
38421
41129
|
createOllamaProvider,
|
|
38422
41130
|
createOpenAiCompatibleProvider,
|
|
41131
|
+
createOpenAiResponsesBenchJudge,
|
|
41132
|
+
createOpenAiResponsesProvider,
|
|
38423
41133
|
createSeededRandom as createProceduralAblationSeededRandom,
|
|
38424
41134
|
createProvider,
|
|
38425
41135
|
createProviderBackedAmaBenchRecommendedJudge,
|
|
@@ -38437,6 +41147,7 @@ export {
|
|
|
38437
41147
|
defaultBenchmarkBaselineDir,
|
|
38438
41148
|
defaultBenchmarkPublishPath,
|
|
38439
41149
|
deleteBenchmarkResults,
|
|
41150
|
+
diagnoseLoComoProfileDelta,
|
|
38440
41151
|
discoverAllProviders,
|
|
38441
41152
|
discoveryEndpointFor,
|
|
38442
41153
|
emailFixture,
|
|
@@ -38466,6 +41177,8 @@ export {
|
|
|
38466
41177
|
isContaminationManifest,
|
|
38467
41178
|
isSealedQrelsArtifact,
|
|
38468
41179
|
isSha256Hex,
|
|
41180
|
+
judgeMemCorrectCorrectionAcceptance,
|
|
41181
|
+
judgeMemCorrectStaleMemoryHarm,
|
|
38469
41182
|
linkMatches,
|
|
38470
41183
|
listBenchmarkBaselines,
|
|
38471
41184
|
listBenchmarkResults,
|
|
@@ -38479,6 +41192,7 @@ export {
|
|
|
38479
41192
|
loadBeamDatasetPreview,
|
|
38480
41193
|
loadBenchmarkArtifact,
|
|
38481
41194
|
loadBenchmarkBaseline,
|
|
41195
|
+
loadBenchmarkReportCardProvenance,
|
|
38482
41196
|
loadBenchmarkResult,
|
|
38483
41197
|
loadCustomBenchmarkFile,
|
|
38484
41198
|
loadJudgeCalibrationState,
|
|
@@ -38506,6 +41220,7 @@ export {
|
|
|
38506
41220
|
redactBenchmarkResultSecrets,
|
|
38507
41221
|
renderBaselineMarkdown,
|
|
38508
41222
|
renderBenchmarkResultExport,
|
|
41223
|
+
renderLoComoProfileDeltaMarkdown,
|
|
38509
41224
|
renderMemorySummaryForJudge,
|
|
38510
41225
|
renderMemoryViewForAgent,
|
|
38511
41226
|
resolveAssistantAgent,
|