dsh-llm-verifier 0.3.1 → 0.4.0
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 +133 -9
- package/lib/auto-B5V49e-A.js +1517 -0
- package/lib/auto-B5V49e-A.js.map +1 -0
- package/lib/{caller-DJd_FlrQ.js → caller-DR_C7ueP.js} +154 -20
- package/lib/caller-DR_C7ueP.js.map +1 -0
- package/lib/caller.js +2 -2
- package/lib/client.js +804 -29
- package/lib/client.js.map +1 -1
- package/lib/core.js +226 -3
- package/lib/core.js.map +1 -1
- package/lib/index.js +1014 -1349
- package/lib/index.js.map +1 -1
- package/lib/replay.js +157 -0
- package/lib/replay.js.map +1 -0
- package/lib/types/auto.js +1 -1
- package/lib/types/auto.js.map +1 -1
- package/lib/types/caller.d.ts +65 -0
- package/lib/types/caller.d.ts.map +1 -1
- package/lib/types/caller.js +113 -5
- package/lib/types/caller.js.map +1 -1
- package/lib/types/client-i18n.d.ts +163 -6
- package/lib/types/client-i18n.d.ts.map +1 -1
- package/lib/types/client-i18n.js +137 -26
- package/lib/types/client-i18n.js.map +1 -1
- package/lib/types/client.d.ts +30 -0
- package/lib/types/client.d.ts.map +1 -1
- package/lib/types/client.js +94 -9
- package/lib/types/client.js.map +1 -1
- package/lib/types/config.d.ts +33 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/config.js +17 -2
- package/lib/types/config.js.map +1 -1
- package/lib/types/core.d.ts +84 -0
- package/lib/types/core.d.ts.map +1 -1
- package/lib/types/core.js +222 -2
- package/lib/types/core.js.map +1 -1
- package/lib/types/criteria.d.ts +33 -0
- package/lib/types/criteria.d.ts.map +1 -0
- package/lib/types/criteria.js +67 -0
- package/lib/types/criteria.js.map +1 -0
- package/lib/types/decisions.d.ts +84 -0
- package/lib/types/decisions.d.ts.map +1 -0
- package/lib/types/decisions.js +209 -0
- package/lib/types/decisions.js.map +1 -0
- package/lib/types/engine.d.ts +54 -1
- package/lib/types/engine.d.ts.map +1 -1
- package/lib/types/engine.js +175 -12
- package/lib/types/engine.js.map +1 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +392 -38
- package/lib/types/index.js.map +1 -1
- package/lib/types/replay.d.ts +88 -0
- package/lib/types/replay.d.ts.map +1 -0
- package/lib/types/replay.js +136 -0
- package/lib/types/replay.js.map +1 -0
- package/lib/types/router.d.ts +29 -5
- package/lib/types/router.d.ts.map +1 -1
- package/lib/types/router.js +271 -45
- package/lib/types/router.js.map +1 -1
- package/lib/types/statistics.d.ts +10 -1
- package/lib/types/statistics.d.ts.map +1 -1
- package/lib/types/statistics.js +35 -7
- package/lib/types/statistics.js.map +1 -1
- package/lib/types/top-logprobs.d.ts +36 -0
- package/lib/types/top-logprobs.d.ts.map +1 -1
- package/lib/types/top-logprobs.js +41 -14
- package/lib/types/top-logprobs.js.map +1 -1
- package/package.json +6 -1
- package/scripts/eval-replay.mjs +118 -0
- package/src/auto.ts +1 -1
- package/src/caller.test.ts +62 -1
- package/src/caller.ts +124 -4
- package/src/client-i18n.ts +137 -25
- package/src/client.test.ts +21 -0
- package/src/client.tsx +145 -7
- package/src/config.test.ts +32 -0
- package/src/config.ts +49 -2
- package/src/core.test.ts +95 -0
- package/src/core.ts +217 -2
- package/src/criteria.test.ts +60 -0
- package/src/criteria.ts +76 -0
- package/src/decisions.test.ts +150 -0
- package/src/decisions.ts +244 -0
- package/src/engine.test.ts +131 -0
- package/src/engine.ts +167 -10
- package/src/index.test.ts +278 -8
- package/src/index.ts +373 -37
- package/src/replay.test.ts +82 -0
- package/src/replay.ts +155 -0
- package/src/router.test.ts +217 -5
- package/src/router.ts +286 -47
- package/src/statistics.test.ts +49 -1
- package/src/statistics.ts +44 -7
- package/src/top-logprobs.test.ts +28 -0
- package/src/top-logprobs.ts +56 -8
- package/lib/caller-DJd_FlrQ.js.map +0 -1
package/lib/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { CRITERIA_PRESETS, CRITERIA_PRESET_IDS, DEFAULT_CRITERIA, DEFAULT_GROUND_TRUTH_NOTE, EMPTY_WORK_BASELINE, GRANULARITY, LETTERS, SCALE_DESCRIPTION, UNTRUSTED_EVIDENCE_NOTE, accumulatePairs, bradleyTerry, buildGenerationPrompt, buildPairwisePrompt, buildProgressPrompt, dedupeCriterionId, evidenceNonce, extractProgressScore, extractScore, normalizeScoreLetter, parseCriteriaMarkdown, pivotRoundPairs, rankScores, renderDelimitedBlock, ringCycle, seededRandom, slugCriterionId, topPivots } from "./core.js";
|
|
2
|
+
import { a as callVerifier, c as generateCandidate, d as TopLogprobCapabilityCache, f as resolveCapabilityFile, i as addUsage, o as callVerifierText, r as RequestLimiter, s as emptyUsage, u as predictScoringChannel } from "./caller-DR_C7ueP.js";
|
|
3
|
+
import { C as resolveCacheFile, S as SingleFlight, _ as semanticRouteHint, a as sessionAccepted, b as sessionEvents, c as analyzeStructuredRoute, d as buildSemanticRoutePrompt, f as estimateRoutedCalls, g as semanticDecision, h as routedRepeats, i as isSubagentSession, l as boundDecision, m as parseSemanticRoute, n as automaticFeedback, o as AutoVerifierRouter, p as latestDirectUserSeq, r as failedAcceptanceCriteria, s as MAX_ROUTED_CHECKPOINTS, t as analyzeAutoTask, u as buildEvidenceIndex, v as extractSession, w as stableHash, x as ScoreCache, y as sanitizeVerifierText } from "./auto-B5V49e-A.js";
|
|
3
4
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
5
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
6
|
import z from "schemastery";
|
|
6
7
|
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
7
8
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
8
|
-
import {
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
9
10
|
//#region src/config.ts
|
|
10
11
|
const VERIFIER_SETTINGS_NAMESPACE = "llm-verifier";
|
|
11
12
|
z.object({
|
|
@@ -24,6 +25,7 @@ const Config = z.object({
|
|
|
24
25
|
]).default("smart"),
|
|
25
26
|
autoVerifyThreshold: z.number().min(0).max(1).default(.65),
|
|
26
27
|
autoVerifyRepeats: z.number().step(1).min(1).default(1),
|
|
28
|
+
autoTrackRepeats: z.number().step(1).min(1).default(3),
|
|
27
29
|
autoVerifyFinalRepeats: z.number().step(1).min(1).default(2),
|
|
28
30
|
autoVerifyMinToolCalls: z.number().step(1).min(1).default(3),
|
|
29
31
|
autoVerifyMaxChars: z.number().step(1).min(1e3).default(8e4),
|
|
@@ -34,11 +36,14 @@ const Config = z.object({
|
|
|
34
36
|
autoRouteMaxCandidates: z.number().step(1).min(3).default(8),
|
|
35
37
|
autoRouteMaxPerTask: z.number().step(1).min(1).default(2),
|
|
36
38
|
autoRouteMaxPerSession: z.number().step(1).min(1).default(8),
|
|
37
|
-
autoTrackCompletionThreshold: z.number().min(0).max(1).default(.
|
|
39
|
+
autoTrackCompletionThreshold: z.number().min(0).max(1).default(.684),
|
|
38
40
|
autoRouteMaxItemChars: z.number().step(1).min(100).default(2e4),
|
|
39
41
|
autoRouteMaxInputChars: z.number().step(1).min(1e3).default(6e4),
|
|
40
42
|
autoMaxModelCallsPerTask: z.number().step(1).min(1).default(96),
|
|
41
43
|
autoMaxModelCallsPerSession: z.number().step(1).min(1).default(240),
|
|
44
|
+
captureDecisions: z.boolean().default(true),
|
|
45
|
+
criteriaPreset: z.union([...CRITERIA_PRESET_IDS, "custom"]).default("coding"),
|
|
46
|
+
criteriaFile: z.string().default(""),
|
|
42
47
|
autoVerifyTeamTasks: z.boolean().default(true),
|
|
43
48
|
autoVerifyPlanMode: z.boolean().default(true),
|
|
44
49
|
autoVerifySubagents: z.boolean().default(false),
|
|
@@ -98,6 +103,7 @@ function resolveConfig(config = {}) {
|
|
|
98
103
|
if (!Number.isFinite(temperature) || temperature < 0 || temperature > 2) throw new Error("llm-verifier: temperature must be between 0 and 2");
|
|
99
104
|
const values = {
|
|
100
105
|
autoVerifyRepeats: config.autoVerifyRepeats ?? 1,
|
|
106
|
+
autoTrackRepeats: config.autoTrackRepeats ?? 3,
|
|
101
107
|
autoVerifyFinalRepeats: config.autoVerifyFinalRepeats ?? 2,
|
|
102
108
|
autoVerifyMinToolCalls: config.autoVerifyMinToolCalls ?? 3,
|
|
103
109
|
autoVerifyMaxChars: config.autoVerifyMaxChars ?? 8e4,
|
|
@@ -121,7 +127,7 @@ function resolveConfig(config = {}) {
|
|
|
121
127
|
if (values.autoRouteMaxCandidates < 3 || values.autoRouteMaxCandidates > 16) throw new Error("llm-verifier: autoRouteMaxCandidates must be between 3 and 16");
|
|
122
128
|
if (values.autoRouteMaxInputChars < values.autoRouteMaxItemChars * 2) throw new Error("llm-verifier: autoRouteMaxInputChars must fit at least two route items");
|
|
123
129
|
const autoRouteMinConfidence = config.autoRouteMinConfidence ?? .9;
|
|
124
|
-
const autoTrackCompletionThreshold = config.autoTrackCompletionThreshold ?? .
|
|
130
|
+
const autoTrackCompletionThreshold = config.autoTrackCompletionThreshold ?? .684;
|
|
125
131
|
if (![autoRouteMinConfidence, autoTrackCompletionThreshold].every((value) => Number.isFinite(value) && value >= 0 && value <= 1)) throw new Error("llm-verifier: auto route thresholds must be between 0 and 1");
|
|
126
132
|
const maxRetries = config.maxRetries ?? 3;
|
|
127
133
|
if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new Error("llm-verifier: maxRetries must be a non-negative safe integer");
|
|
@@ -132,6 +138,9 @@ function resolveConfig(config = {}) {
|
|
|
132
138
|
const estimatedInputUsdPerMillion = config.estimatedInputUsdPerMillion ?? 0;
|
|
133
139
|
const estimatedOutputUsdPerMillion = config.estimatedOutputUsdPerMillion ?? 0;
|
|
134
140
|
if (![estimatedInputUsdPerMillion, estimatedOutputUsdPerMillion].every((value) => Number.isFinite(value) && value >= 0)) throw new Error("llm-verifier: estimated token prices must be finite non-negative numbers");
|
|
141
|
+
const criteriaPreset = config.criteriaPreset ?? "coding";
|
|
142
|
+
if (criteriaPreset !== "custom" && !CRITERIA_PRESET_IDS.includes(criteriaPreset)) throw new Error("llm-verifier: criteriaPreset must be one of " + [...CRITERIA_PRESET_IDS, "custom"].join(", "));
|
|
143
|
+
const criteriaFile = (config.criteriaFile ?? "").trim();
|
|
135
144
|
const reasoningEffort = config.reasoningEffort?.trim();
|
|
136
145
|
const extraJudges = config.extraJudges ?? [];
|
|
137
146
|
if (!Array.isArray(extraJudges)) throw new Error("llm-verifier: extraJudges must be an array");
|
|
@@ -178,6 +187,9 @@ function resolveConfig(config = {}) {
|
|
|
178
187
|
autoRouteSemantic: config.autoRouteSemantic ?? true,
|
|
179
188
|
autoRouteMinConfidence,
|
|
180
189
|
autoTrackCompletionThreshold,
|
|
190
|
+
captureDecisions: config.captureDecisions ?? true,
|
|
191
|
+
criteriaPreset,
|
|
192
|
+
criteriaFile,
|
|
181
193
|
autoVerifyTeamTasks: config.autoVerifyTeamTasks ?? true,
|
|
182
194
|
autoVerifyPlanMode: config.autoVerifyPlanMode ?? true,
|
|
183
195
|
autoVerifySubagents: config.autoVerifySubagents ?? false,
|
|
@@ -228,139 +240,6 @@ function installVerifierSettings(ctx, entry, onChange) {
|
|
|
228
240
|
return () => resolveConfig(source());
|
|
229
241
|
}
|
|
230
242
|
//#endregion
|
|
231
|
-
//#region src/cache.ts
|
|
232
|
-
function stableHash(value) {
|
|
233
|
-
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
234
|
-
}
|
|
235
|
-
function resolveCacheFile(cacheDir, cwd = process.cwd()) {
|
|
236
|
-
return join(isAbsolute(cacheDir) ? cacheDir : resolve(cwd, cacheDir), "scores-v1.json");
|
|
237
|
-
}
|
|
238
|
-
function validUsage(value) {
|
|
239
|
-
if (typeof value !== "object" || value === null) return false;
|
|
240
|
-
const row = value;
|
|
241
|
-
return [
|
|
242
|
-
"calls",
|
|
243
|
-
"attempts",
|
|
244
|
-
"retries",
|
|
245
|
-
"inputTokens",
|
|
246
|
-
"cachedInputTokens",
|
|
247
|
-
"outputTokens",
|
|
248
|
-
"reasoningTokens"
|
|
249
|
-
].every((key) => typeof row[key] === "number" && Number.isFinite(row[key]) && Number(row[key]) >= 0);
|
|
250
|
-
}
|
|
251
|
-
function validEntry(value) {
|
|
252
|
-
if (typeof value !== "object" || value === null) return false;
|
|
253
|
-
const row = value;
|
|
254
|
-
return typeof row.scoreA === "number" && Number.isFinite(row.scoreA) && row.scoreA >= 0 && row.scoreA <= 1 && typeof row.scoreB === "number" && Number.isFinite(row.scoreB) && row.scoreB >= 0 && row.scoreB <= 1 && validUsage(row.usage) && (row.scoringMode === void 0 || row.scoringMode === "top-logprobs" || row.scoringMode === "explicit-tag") && typeof row.createdAt === "number" && Number.isFinite(row.createdAt) && row.createdAt >= 0;
|
|
255
|
-
}
|
|
256
|
-
/** Channel-independent single-flight: concurrent identical tasks share one promise; joiners are flagged so callers can avoid double-counting usage. */
|
|
257
|
-
var SingleFlight = class {
|
|
258
|
-
flights = /* @__PURE__ */ new Map();
|
|
259
|
-
async run(key, task) {
|
|
260
|
-
const existing = this.flights.get(key);
|
|
261
|
-
if (existing !== void 0) return {
|
|
262
|
-
value: await existing,
|
|
263
|
-
joined: true
|
|
264
|
-
};
|
|
265
|
-
const flight = task().finally(() => {
|
|
266
|
-
if (this.flights.get(key) === flight) this.flights.delete(key);
|
|
267
|
-
});
|
|
268
|
-
this.flights.set(key, flight);
|
|
269
|
-
return {
|
|
270
|
-
value: await flight,
|
|
271
|
-
joined: false
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
var ScoreCache = class {
|
|
276
|
-
file;
|
|
277
|
-
maxEntries;
|
|
278
|
-
loaded = false;
|
|
279
|
-
hydrating;
|
|
280
|
-
entries = /* @__PURE__ */ new Map();
|
|
281
|
-
inflight = /* @__PURE__ */ new Map();
|
|
282
|
-
writing = Promise.resolve();
|
|
283
|
-
constructor(file, maxEntries) {
|
|
284
|
-
this.file = file;
|
|
285
|
-
this.maxEntries = maxEntries;
|
|
286
|
-
}
|
|
287
|
-
async load() {
|
|
288
|
-
if (this.loaded) return;
|
|
289
|
-
this.hydrating ??= (async () => {
|
|
290
|
-
try {
|
|
291
|
-
const document = JSON.parse(await readFile(this.file, "utf8"));
|
|
292
|
-
if (document.version === 1 && typeof document.entries === "object" && document.entries !== null) this.entries = new Map(Object.entries(document.entries).filter((entry) => validEntry(entry[1])).map(([key, value]) => [key, {
|
|
293
|
-
...value,
|
|
294
|
-
scoringMode: value.scoringMode ?? "explicit-tag"
|
|
295
|
-
}]));
|
|
296
|
-
this.loaded = true;
|
|
297
|
-
} catch (error) {
|
|
298
|
-
if (error.code === "ENOENT") {
|
|
299
|
-
this.loaded = true;
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
throw error;
|
|
303
|
-
} finally {
|
|
304
|
-
this.hydrating = void 0;
|
|
305
|
-
}
|
|
306
|
-
})();
|
|
307
|
-
await this.hydrating;
|
|
308
|
-
}
|
|
309
|
-
async getOrCreate(key, create, keyFor = () => key) {
|
|
310
|
-
await this.load();
|
|
311
|
-
const cached = this.entries.get(key);
|
|
312
|
-
if (cached !== void 0) return {
|
|
313
|
-
value: cached,
|
|
314
|
-
hit: true
|
|
315
|
-
};
|
|
316
|
-
const existing = this.inflight.get(key);
|
|
317
|
-
if (existing !== void 0) return {
|
|
318
|
-
value: (await existing).value,
|
|
319
|
-
hit: true
|
|
320
|
-
};
|
|
321
|
-
const pending = create().then((value) => ({
|
|
322
|
-
value,
|
|
323
|
-
key: keyFor(value)
|
|
324
|
-
}));
|
|
325
|
-
this.inflight.set(key, pending);
|
|
326
|
-
try {
|
|
327
|
-
const landed = await pending;
|
|
328
|
-
this.entries.set(landed.key, landed.value);
|
|
329
|
-
this.trim();
|
|
330
|
-
await this.persist();
|
|
331
|
-
return {
|
|
332
|
-
value: landed.value,
|
|
333
|
-
hit: false
|
|
334
|
-
};
|
|
335
|
-
} finally {
|
|
336
|
-
this.inflight.delete(key);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
trim() {
|
|
340
|
-
if (this.entries.size <= this.maxEntries) return;
|
|
341
|
-
const sorted = [...this.entries].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
342
|
-
for (let index = 0; index < sorted.length - this.maxEntries; index += 1) this.entries.delete(sorted[index][0]);
|
|
343
|
-
}
|
|
344
|
-
async persist() {
|
|
345
|
-
const snapshot = {
|
|
346
|
-
version: 1,
|
|
347
|
-
entries: Object.fromEntries(this.entries)
|
|
348
|
-
};
|
|
349
|
-
this.writing = this.writing.catch(() => {}).then(async () => {
|
|
350
|
-
await mkdir(dirname(this.file), { recursive: true });
|
|
351
|
-
const temporary = this.file + ".tmp-" + process.pid;
|
|
352
|
-
await writeFile(temporary, JSON.stringify(snapshot), "utf8");
|
|
353
|
-
try {
|
|
354
|
-
await rename(temporary, this.file);
|
|
355
|
-
} catch (error) {
|
|
356
|
-
await unlink(temporary).catch(() => {});
|
|
357
|
-
throw error;
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
await this.writing;
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
//#endregion
|
|
364
243
|
//#region src/engine.ts
|
|
365
244
|
function average(values) {
|
|
366
245
|
return values.reduce((sum, value) => sum + value, 0) / (values.length || 1);
|
|
@@ -381,6 +260,10 @@ function blankStats() {
|
|
|
381
260
|
explicitTagScores: 0
|
|
382
261
|
};
|
|
383
262
|
}
|
|
263
|
+
/** Candidate indices best-first by score, ties broken by index. */
|
|
264
|
+
function rankByScore(scores) {
|
|
265
|
+
return Array.from({ length: scores.length }, (_, index) => index).sort((a, b) => (scores[b] ?? 0) - (scores[a] ?? 0) || a - b);
|
|
266
|
+
}
|
|
384
267
|
function unorderedPair(a, b) {
|
|
385
268
|
return a < b ? a + "," + b : b + "," + a;
|
|
386
269
|
}
|
|
@@ -479,9 +362,18 @@ var VerifierEngine = class {
|
|
|
479
362
|
});
|
|
480
363
|
const create = async () => {
|
|
481
364
|
const completion = await callVerifier(client, prompt, signal, options.images);
|
|
365
|
+
const scoreA = extractScore(completion, "<score_A>");
|
|
366
|
+
const scoreB = extractScore(completion, "<score_B>");
|
|
367
|
+
options.trace?.({
|
|
368
|
+
label: (options.traceLabelPrefix ?? "") + criterion.name + " repeat " + (repeat + 1),
|
|
369
|
+
channel: completion.scoringMode,
|
|
370
|
+
prompt,
|
|
371
|
+
output: completion.text,
|
|
372
|
+
score: scoreA
|
|
373
|
+
});
|
|
482
374
|
return {
|
|
483
|
-
scoreA
|
|
484
|
-
scoreB
|
|
375
|
+
scoreA,
|
|
376
|
+
scoreB,
|
|
485
377
|
usage: completion.usage,
|
|
486
378
|
scoringMode: completion.scoringMode,
|
|
487
379
|
createdAt: Date.now()
|
|
@@ -520,15 +412,63 @@ var VerifierEngine = class {
|
|
|
520
412
|
await Promise.all(runners);
|
|
521
413
|
return results;
|
|
522
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Verdict for a comparison whose two sides are byte-identical.
|
|
417
|
+
*
|
|
418
|
+
* Deliberately uninformative: no model call is made, both sides score 0.5 and the winner is
|
|
419
|
+
* a tie. Judging identical text would ask the model to break a tie it cannot break, and any
|
|
420
|
+
* confident score would let the acceptance gate pass a session indistinguishable from the
|
|
421
|
+
* empty-work baseline. Callers that need "these are the same" read {@link CompareResult.identical}.
|
|
422
|
+
* @param criteria - the criteria the comparison would have scored.
|
|
423
|
+
* @returns A tie carrying 0.5 per criterion, zero calls and one agreeing judge per client.
|
|
424
|
+
*/
|
|
425
|
+
informationalTie(criteria) {
|
|
426
|
+
const judges = this.clients.map((client) => ({
|
|
427
|
+
provider: client.provider,
|
|
428
|
+
model: client.model,
|
|
429
|
+
label: judgeLabel(client),
|
|
430
|
+
ok: true,
|
|
431
|
+
calls: 0,
|
|
432
|
+
scoreA: .5,
|
|
433
|
+
scoreB: .5,
|
|
434
|
+
winner: "tie"
|
|
435
|
+
}));
|
|
436
|
+
return {
|
|
437
|
+
scoreA: .5,
|
|
438
|
+
scoreB: .5,
|
|
439
|
+
winner: "tie",
|
|
440
|
+
criteria: criteria.map((criterion) => ({
|
|
441
|
+
id: criterion.id,
|
|
442
|
+
name: criterion.name,
|
|
443
|
+
scoreA: .5,
|
|
444
|
+
scoreB: .5
|
|
445
|
+
})),
|
|
446
|
+
calls: 0,
|
|
447
|
+
stats: this.finishStats(blankStats()),
|
|
448
|
+
judges,
|
|
449
|
+
agreement: 1,
|
|
450
|
+
identical: true
|
|
451
|
+
};
|
|
452
|
+
}
|
|
523
453
|
async compare(options, signal) {
|
|
524
454
|
const criteria = options.criteria?.length ? options.criteria : DEFAULT_CRITERIA;
|
|
455
|
+
if (options.candidateA === options.candidateB) return this.informationalTie(criteria);
|
|
525
456
|
const repeats = options.repeats ?? 2;
|
|
526
457
|
const jobs = criteria.flatMap((criterion) => Array.from({ length: repeats }, (_, repeat) => ({
|
|
527
458
|
criterion,
|
|
528
459
|
repeat
|
|
529
460
|
})));
|
|
530
|
-
const warm =
|
|
531
|
-
const rest =
|
|
461
|
+
const warm = [];
|
|
462
|
+
const rest = [];
|
|
463
|
+
const warmedOrientations = /* @__PURE__ */ new Set();
|
|
464
|
+
for (const job of jobs) {
|
|
465
|
+
const orientation = job.repeat % 2;
|
|
466
|
+
if (warmedOrientations.has(orientation)) rest.push(job);
|
|
467
|
+
else {
|
|
468
|
+
warmedOrientations.add(orientation);
|
|
469
|
+
warm.push(job);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
532
472
|
const run = async (batch) => this.mapLimited(batch, async ({ criterion, repeat }) => {
|
|
533
473
|
const swapped = repeat % 2 === 1;
|
|
534
474
|
const candA = swapped ? options.candidateB : options.candidateA;
|
|
@@ -652,7 +592,8 @@ var VerifierEngine = class {
|
|
|
652
592
|
criteria: options.criteria,
|
|
653
593
|
groundTruthNote: options.groundTruthNote,
|
|
654
594
|
repeats: options.repeats,
|
|
655
|
-
images: options.images
|
|
595
|
+
images: options.images,
|
|
596
|
+
trace: options.trace
|
|
656
597
|
}, signal)
|
|
657
598
|
}));
|
|
658
599
|
const rewards = /* @__PURE__ */ new Map();
|
|
@@ -688,7 +629,7 @@ var VerifierEngine = class {
|
|
|
688
629
|
stats: this.finishStats(stats)
|
|
689
630
|
};
|
|
690
631
|
}
|
|
691
|
-
async track(problem, steps, checkpoints, repeats = 2, signal, images) {
|
|
632
|
+
async track(problem, steps, checkpoints, repeats = 2, signal, images, trace) {
|
|
692
633
|
if (!steps.length || !checkpoints.length) throw new Error("llm-verifier: steps and checkpoints must not be empty");
|
|
693
634
|
for (const checkpoint of checkpoints) if (!Number.isSafeInteger(checkpoint) || checkpoint < 1 || checkpoint > steps.length) throw new Error("llm-verifier: each checkpoint must be an integer between 1 and steps.length");
|
|
694
635
|
const prompt = buildProgressPrompt(problem, steps, checkpoints);
|
|
@@ -698,15 +639,23 @@ var VerifierEngine = class {
|
|
|
698
639
|
const judgeErrors = new Array(this.clients.length).fill(void 0);
|
|
699
640
|
const judgePerRepeatScores = Array.from({ length: this.clients.length }, () => []);
|
|
700
641
|
const repeatIndices = Array.from({ length: repeats }, (_, index) => index);
|
|
701
|
-
const
|
|
642
|
+
const runRepeat = async (repeatIndex) => {
|
|
702
643
|
const judgeResults = await Promise.all(this.clients.map(async (client, k) => {
|
|
703
644
|
try {
|
|
704
645
|
const completion = await callVerifier(client, prompt, signal, images);
|
|
646
|
+
const scores = checkpoints.map((_, index) => extractProgressScore(completion, "<c" + (index + 1) + ">"));
|
|
647
|
+
if (k === 0) trace?.({
|
|
648
|
+
label: "progress repeat " + (repeatIndex + 1) + "/" + repeats + (this.clients.length > 1 ? " judge 1/" + this.clients.length : ""),
|
|
649
|
+
channel: completion.scoringMode,
|
|
650
|
+
prompt,
|
|
651
|
+
output: completion.text,
|
|
652
|
+
score: scores[scores.length - 1]
|
|
653
|
+
});
|
|
705
654
|
return {
|
|
706
655
|
k,
|
|
707
656
|
ok: true,
|
|
708
657
|
completion,
|
|
709
|
-
scores
|
|
658
|
+
scores
|
|
710
659
|
};
|
|
711
660
|
} catch (error) {
|
|
712
661
|
return {
|
|
@@ -729,7 +678,10 @@ var VerifierEngine = class {
|
|
|
729
678
|
if (judgeErrors[r.k] === void 0) judgeErrors[r.k] = r.error instanceof Error ? r.error.message : String(r.error);
|
|
730
679
|
}
|
|
731
680
|
return checkpoints.map((_, cIndex) => median(successful.map((r) => r.scores[cIndex])));
|
|
732
|
-
}
|
|
681
|
+
};
|
|
682
|
+
const runs = [];
|
|
683
|
+
if (repeatIndices.length > 0) runs.push(...await this.mapLimited(repeatIndices.slice(0, 1), runRepeat));
|
|
684
|
+
runs.push(...await this.mapLimited(repeatIndices.slice(1), runRepeat));
|
|
733
685
|
const scores = checkpoints.map((_, index) => average(runs.map((run) => run[index])));
|
|
734
686
|
const judges = this.clients.map((client, k) => {
|
|
735
687
|
if (!judgeOk[k]) return {
|
|
@@ -759,8 +711,96 @@ var VerifierEngine = class {
|
|
|
759
711
|
judges
|
|
760
712
|
};
|
|
761
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Verdict for a candidate list whose entries are all byte-identical.
|
|
716
|
+
*
|
|
717
|
+
* Ranking identical text is a coin flip, so the result is deliberately uninformative
|
|
718
|
+
* (0.5 everywhere) rather than a confident 1.0. No model call is made. This is the
|
|
719
|
+
* cost-saving half of upstream's majority-vote shortcut without its semantics: we never
|
|
720
|
+
* declare an unjudged candidate the winner, we only decline to spend calls on a tie.
|
|
721
|
+
* @param candidates - the identical candidates (length >= 2).
|
|
722
|
+
* @returns A ranking with every score at 0.5 and zero calls.
|
|
723
|
+
*/
|
|
724
|
+
identicalCandidates(candidates) {
|
|
725
|
+
const scores = candidates.map(() => .5);
|
|
726
|
+
const ranking = candidates.map((_, index) => index);
|
|
727
|
+
return {
|
|
728
|
+
index: 0,
|
|
729
|
+
best: candidates[0],
|
|
730
|
+
scores,
|
|
731
|
+
ranking,
|
|
732
|
+
pivots: [],
|
|
733
|
+
comparisons: 0,
|
|
734
|
+
calls: 0,
|
|
735
|
+
stats: blankStats(),
|
|
736
|
+
judges: this.clients.map((client) => ({
|
|
737
|
+
provider: client.provider,
|
|
738
|
+
model: client.model,
|
|
739
|
+
label: judgeLabel(client),
|
|
740
|
+
ok: true,
|
|
741
|
+
calls: 0,
|
|
742
|
+
scores: [...scores],
|
|
743
|
+
ranking: [...ranking]
|
|
744
|
+
})),
|
|
745
|
+
identical: true
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Judge only the DISTINCT candidates, then expand the verdict back onto the caller's list.
|
|
750
|
+
*
|
|
751
|
+
* Duplicated candidates are the common case when an agent pastes several drafts of the same
|
|
752
|
+
* artifact: every pair that touches a duplicate is a comparison that cannot change the
|
|
753
|
+
* ranking but still costs model calls. The tournament runs on the distinct list and every
|
|
754
|
+
* duplicate inherits its representative's score, so no index, score or ranking entry shifts.
|
|
755
|
+
* @param options - the original select options, duplicates included.
|
|
756
|
+
* @param signal - caller's abort signal.
|
|
757
|
+
* @returns The tournament verdict mapped back onto the original candidate list.
|
|
758
|
+
*/
|
|
759
|
+
async selectUnique(options, signal) {
|
|
760
|
+
const unique = [];
|
|
761
|
+
const representative = [];
|
|
762
|
+
const seen = /* @__PURE__ */ new Map();
|
|
763
|
+
for (const candidate of options.candidates) {
|
|
764
|
+
let index = seen.get(candidate);
|
|
765
|
+
if (index === void 0) {
|
|
766
|
+
index = unique.length;
|
|
767
|
+
unique.push(candidate);
|
|
768
|
+
seen.set(candidate, index);
|
|
769
|
+
}
|
|
770
|
+
representative.push(index);
|
|
771
|
+
}
|
|
772
|
+
const result = await this.select({
|
|
773
|
+
...options,
|
|
774
|
+
candidates: unique
|
|
775
|
+
}, signal);
|
|
776
|
+
const expand = (values) => representative.map((index) => values[index] ?? 0);
|
|
777
|
+
const scores = expand(result.scores);
|
|
778
|
+
const judges = result.judges.map((judge) => {
|
|
779
|
+
if (!judge.ok || judge.scores === void 0) return judge;
|
|
780
|
+
const judgeScores = expand(judge.scores);
|
|
781
|
+
return {
|
|
782
|
+
...judge,
|
|
783
|
+
scores: judgeScores,
|
|
784
|
+
...judge.ranking === void 0 ? {} : { ranking: rankByScore(judgeScores) }
|
|
785
|
+
};
|
|
786
|
+
});
|
|
787
|
+
return {
|
|
788
|
+
index: options.candidates.indexOf(result.best),
|
|
789
|
+
best: result.best,
|
|
790
|
+
scores,
|
|
791
|
+
ranking: rankByScore(scores),
|
|
792
|
+
pivots: result.pivots,
|
|
793
|
+
comparisons: result.comparisons,
|
|
794
|
+
calls: result.calls,
|
|
795
|
+
stats: result.stats,
|
|
796
|
+
judges
|
|
797
|
+
};
|
|
798
|
+
}
|
|
762
799
|
async select(options, signal) {
|
|
763
800
|
if (!options.candidates.length) throw new Error("llm-verifier: candidates must not be empty");
|
|
801
|
+
if (options.candidates.some((candidate) => candidate.trim() === "")) throw new Error("llm-verifier: candidates must not contain blank entries");
|
|
802
|
+
if (options.candidates.length > 1 && options.candidates.every((candidate) => candidate === options.candidates[0])) return this.identicalCandidates(options.candidates);
|
|
803
|
+
if (options.candidates.length > 1 && new Set(options.candidates).size !== options.candidates.length) return this.selectUnique(options, signal);
|
|
764
804
|
if (options.candidates.length === 1) {
|
|
765
805
|
const judges = this.clients.map((client) => ({
|
|
766
806
|
provider: client.provider,
|
|
@@ -891,21 +931,41 @@ var VerifierEngine = class {
|
|
|
891
931
|
};
|
|
892
932
|
}
|
|
893
933
|
};
|
|
934
|
+
/**
|
|
935
|
+
* Normalize a caller-supplied `criteria` argument into the engine's canonical shape.
|
|
936
|
+
*
|
|
937
|
+
* Mirrors upstream's `normalize_criteria` so a caller does not have to fill in every field: a
|
|
938
|
+
* plain string is both the name and the instruction, and a missing `id` is slugged from the
|
|
939
|
+
* name (the score cache keys on the rendered prompt, so a slug is cosmetic). Ids are
|
|
940
|
+
* de-duplicated because `compare` groups per-criterion results by id — a collision would
|
|
941
|
+
* silently merge two criteria into one line of the verdict.
|
|
942
|
+
* @param input - undefined (the default rubric), or a non-empty array of strings/objects.
|
|
943
|
+
* @returns The criteria the engine will score with.
|
|
944
|
+
*/
|
|
894
945
|
function normalizeCriteria(input) {
|
|
895
946
|
if (input === void 0) return DEFAULT_CRITERIA;
|
|
896
947
|
if (!Array.isArray(input) || !input.length) throw new Error("llm-verifier: criteria must be a non-empty array");
|
|
948
|
+
const seen = /* @__PURE__ */ new Set();
|
|
897
949
|
return input.map((value, index) => {
|
|
898
|
-
if (typeof value
|
|
950
|
+
if (typeof value === "string") {
|
|
951
|
+
const text = value.trim();
|
|
952
|
+
if (!text) throw new Error("llm-verifier: criteria[" + index + "] must not be blank");
|
|
953
|
+
return {
|
|
954
|
+
id: dedupeCriterionId(slugCriterionId(text), seen),
|
|
955
|
+
name: text,
|
|
956
|
+
description: text
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
if (typeof value !== "object" || value === null) throw new Error("llm-verifier: criteria[" + index + "] must be a string or an object");
|
|
899
960
|
const row = value;
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
]) if (typeof row[key] !== "string" || row[key].trim().length === 0) throw new Error("llm-verifier: criteria[" + index + "]." + key + " must be non-empty");
|
|
961
|
+
const field = (key) => typeof row[key] === "string" ? row[key].trim() : "";
|
|
962
|
+
const description = field("description");
|
|
963
|
+
if (!description) throw new Error("llm-verifier: criteria[" + index + "].description must be a non-empty string");
|
|
964
|
+
const name = field("name") || field("id") || slugCriterionId(description);
|
|
905
965
|
return {
|
|
906
|
-
id:
|
|
907
|
-
name
|
|
908
|
-
description
|
|
966
|
+
id: dedupeCriterionId(field("id") || slugCriterionId(name), seen),
|
|
967
|
+
name,
|
|
968
|
+
description
|
|
909
969
|
};
|
|
910
970
|
});
|
|
911
971
|
}
|
|
@@ -1000,1155 +1060,75 @@ async function loadVerifierImages(inputs, signal) {
|
|
|
1000
1060
|
return images;
|
|
1001
1061
|
}
|
|
1002
1062
|
//#endregion
|
|
1003
|
-
//#region src/
|
|
1004
|
-
/**
|
|
1005
|
-
* Read one session's event log.
|
|
1006
|
-
*
|
|
1007
|
-
* DSH 0.1.5 replaced the `events` array with `snapshotEvents()`; older hosts
|
|
1008
|
-
* expose the array directly. Both shapes are accepted so the plugin keeps
|
|
1009
|
-
* working across the versions it declares support for.
|
|
1010
|
-
* @param session - Agent session, or any object exposing one of the two shapes.
|
|
1011
|
-
* @returns The session's events in log order, or an empty array.
|
|
1012
|
-
*/
|
|
1013
|
-
function sessionEvents(session) {
|
|
1014
|
-
const candidate = session;
|
|
1015
|
-
if (typeof candidate?.snapshotEvents === "function") return candidate.snapshotEvents();
|
|
1016
|
-
return candidate?.events ?? [];
|
|
1017
|
-
}
|
|
1018
|
-
function textOf(blocks) {
|
|
1019
|
-
const parts = [];
|
|
1020
|
-
for (const block of blocks) {
|
|
1021
|
-
const blockType = block.type;
|
|
1022
|
-
if (blockType === "text") parts.push(block.text);
|
|
1023
|
-
else if (blockType === "reasoning") parts.push("[Reasoning] " + block.text);
|
|
1024
|
-
else if (blockType === "tool-call") parts.push("[Tool Call] " + block.name + " " + block.arguments);
|
|
1025
|
-
else if (blockType === "tool-result") parts.push("[Tool Result] " + textOf(block.content));
|
|
1026
|
-
else if (blockType === "file") {
|
|
1027
|
-
const fileData = block;
|
|
1028
|
-
parts.push("[File] " + (fileData.path ?? fileData.filename ?? fileData.title ?? "attachment"));
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
return parts.join("\n");
|
|
1032
|
-
}
|
|
1033
|
-
const DEFAULT_REDACT_PATTERNS = ["Bearer\\s+[A-Za-z0-9._~+\\/=-]+", "(?:api[_-]?key|token|password|secret)\\s*[=:]\\s*[\"']?[^\\s,\"';}]+"];
|
|
1034
|
-
function validateRedactPattern(pattern) {
|
|
1035
|
-
if (pattern.length === 0 || pattern.length > 500) throw new Error("llm-verifier: redact patterns must contain 1-500 characters");
|
|
1036
|
-
if (/\([^)]*[+*][^)]*\)[+*{]|\.\*[+*{]|\.\+[+*{]/u.test(pattern)) throw new Error("llm-verifier: unsafe redact pattern");
|
|
1037
|
-
}
|
|
1038
|
-
function redactText(text, patterns = DEFAULT_REDACT_PATTERNS) {
|
|
1039
|
-
let result = text;
|
|
1040
|
-
for (const pattern of patterns) {
|
|
1041
|
-
validateRedactPattern(pattern);
|
|
1042
|
-
let regex;
|
|
1043
|
-
try {
|
|
1044
|
-
regex = new RegExp(pattern, "giu");
|
|
1045
|
-
} catch {
|
|
1046
|
-
throw new Error("llm-verifier: invalid redact pattern: " + pattern);
|
|
1047
|
-
}
|
|
1048
|
-
result = result.replace(regex, "[REDACTED]");
|
|
1049
|
-
}
|
|
1050
|
-
return result;
|
|
1051
|
-
}
|
|
1052
|
-
function sanitizeVerifierText(text, maxChars, patterns = DEFAULT_REDACT_PATTERNS) {
|
|
1053
|
-
if (!Number.isSafeInteger(maxChars) || maxChars < 1) throw new Error("llm-verifier: sanitizer maxChars must be a positive integer");
|
|
1054
|
-
const redacted = redactText(text, patterns).trim();
|
|
1055
|
-
if (redacted.length <= maxChars) return redacted;
|
|
1056
|
-
const notice = "\n[Truncated " + (redacted.length - maxChars) + " characters]";
|
|
1057
|
-
if (notice.length >= maxChars) return redacted.slice(0, maxChars);
|
|
1058
|
-
return redacted.slice(0, maxChars - notice.length) + notice;
|
|
1059
|
-
}
|
|
1060
|
-
async function extractSession(agent, loadImage, options = {}) {
|
|
1061
|
-
const all = sessionEvents(agent.session);
|
|
1062
|
-
const from = options.fromSeq ?? 0;
|
|
1063
|
-
const to = options.toSeq ?? Number.MAX_SAFE_INTEGER;
|
|
1064
|
-
const events = all.filter((event) => event.seq >= from && event.seq <= to);
|
|
1065
|
-
const patterns = [...DEFAULT_REDACT_PATTERNS, ...options.redactPatterns ?? []];
|
|
1066
|
-
let problem = "";
|
|
1067
|
-
const trace = [];
|
|
1068
|
-
const images = [];
|
|
1069
|
-
for (const rawEvent of events) {
|
|
1070
|
-
const event = rawEvent;
|
|
1071
|
-
if (event.type === "user/message") {
|
|
1072
|
-
const sourceKind = event.data?.source?.kind;
|
|
1073
|
-
if (sourceKind !== "user" && sourceKind !== "team-message") continue;
|
|
1074
|
-
const text = textOf(event.data.content);
|
|
1075
|
-
if (!problem && sourceKind === "user" && text.trim()) problem = text.trim();
|
|
1076
|
-
for (const block of event.data.content) if (block.type === "image") images.push(await loadImage(block.attachment));
|
|
1077
|
-
const tag = sourceKind === "team-message" ? "Team Message" : "User";
|
|
1078
|
-
trace.push("--- " + tag + " seq " + event.seq + " ---\n" + text);
|
|
1079
|
-
} else if (event.type === "assistant/message" && options.includeAssistantText !== false) trace.push("--- Assistant turn " + event.data.turn + " step " + event.data.step + " ---\n" + textOf(event.data.message.content));
|
|
1080
|
-
else if (event.type === "tool/call") trace.push("--- Tool Call turn " + event.data.turn + " step " + event.data.step + " ---\n[Command] " + event.data.name + " " + event.data.arguments);
|
|
1081
|
-
else if (event.type === "tool/result") trace.push("--- Tool Result turn " + event.data.turn + " step " + event.data.step + " ---\n[Output] " + textOf(event.data.message.content));
|
|
1082
|
-
else if (event.type === "tool/ptc-dispatch" || event.type === "tool/code-dispatch") {
|
|
1083
|
-
const data = event.data;
|
|
1084
|
-
const status = data.isError ? " [Error]" : "";
|
|
1085
|
-
const args = data.arguments !== void 0 ? " " + (typeof data.arguments === "string" ? data.arguments : JSON.stringify(data.arguments)) : "";
|
|
1086
|
-
const content = Array.isArray(data.content) ? textOf(data.content) : "";
|
|
1087
|
-
if (Array.isArray(data.content)) {
|
|
1088
|
-
for (const block of data.content) if (block.type === "image") images.push(await loadImage(block.attachment));
|
|
1089
|
-
}
|
|
1090
|
-
const label = event.type === "tool/ptc-dispatch" ? "PTC Dispatch " : "Code Dispatch ";
|
|
1091
|
-
trace.push("--- " + label + data.name + status + " seq " + event.seq + " ---\n[Command] " + data.name + args + "\n[Output] " + content);
|
|
1092
|
-
} else if (event.type === "team/message/queued") {
|
|
1093
|
-
const data = event.data;
|
|
1094
|
-
const sender = data?.message?.senderName ?? "Teammate";
|
|
1095
|
-
const content = Array.isArray(data?.message?.content) ? textOf(data.message.content) : "";
|
|
1096
|
-
trace.push("--- Team Message Queued from " + sender + " seq " + event.seq + " ---\n" + content);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
const raw = redactText(trace.join("\n\n"), patterns);
|
|
1100
|
-
const maxChars = options.maxChars ?? 2e5;
|
|
1101
|
-
if (!Number.isSafeInteger(maxChars) || maxChars < 1) throw new Error("llm-verifier: extractSession maxChars must be a positive integer");
|
|
1102
|
-
const omittedCharacters = Math.max(0, raw.length - maxChars);
|
|
1103
|
-
const notice = "[Earlier trace truncated: " + omittedCharacters + " characters omitted]\n";
|
|
1104
|
-
const bounded = omittedCharacters === 0 ? raw : notice.length >= maxChars ? raw.slice(-maxChars) : notice + raw.slice(-(maxChars - notice.length));
|
|
1105
|
-
return {
|
|
1106
|
-
problem: redactText(problem, patterns),
|
|
1107
|
-
trace: bounded,
|
|
1108
|
-
images,
|
|
1109
|
-
sessionId: String(agent.id),
|
|
1110
|
-
fromSeq: events[0]?.seq ?? from,
|
|
1111
|
-
toSeq: events.at(-1)?.seq ?? from,
|
|
1112
|
-
omittedCharacters
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
//#endregion
|
|
1116
|
-
//#region src/router.ts
|
|
1117
|
-
const ROUTED_TOOLS = /* @__PURE__ */ new Set([
|
|
1118
|
-
"verifier_compare",
|
|
1119
|
-
"verifier_select",
|
|
1120
|
-
"verifier_track"
|
|
1121
|
-
]);
|
|
1122
|
-
const TRUSTED_WORKFLOW_VERSION = 1;
|
|
1123
|
-
const KNOWN_ROUTE_KEYS = /* @__PURE__ */ new Set([
|
|
1124
|
-
"kind",
|
|
1125
|
-
"confidence",
|
|
1126
|
-
"reason",
|
|
1127
|
-
"candidateCallIds",
|
|
1128
|
-
"checkpointSeqs"
|
|
1129
|
-
]);
|
|
1063
|
+
//#region src/criteria.ts
|
|
1130
1064
|
/**
|
|
1131
|
-
*
|
|
1065
|
+
* Resolve the configured rubric to the criteria the engine scores with.
|
|
1132
1066
|
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
* single definition shared with {@link analyzeAutoTask}.
|
|
1137
|
-
* @param events - Session event log.
|
|
1138
|
-
* @returns The seq of the newest task-assigning message, or undefined.
|
|
1139
|
-
*/
|
|
1140
|
-
function latestDirectUserSeq(events) {
|
|
1141
|
-
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
1142
|
-
const event = events[index];
|
|
1143
|
-
if (event?.type !== "user/message") continue;
|
|
1144
|
-
const kind = event.data.source.kind;
|
|
1145
|
-
if (kind === "user" || kind === "team-message") return event.seq;
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
function blockText$1(blocks) {
|
|
1149
|
-
const parts = [];
|
|
1150
|
-
const visit = (items) => {
|
|
1151
|
-
for (const block of items) if (block.type === "text" || block.type === "reasoning") parts.push(block.text);
|
|
1152
|
-
else if (block.type === "tool-result") visit(block.content);
|
|
1153
|
-
};
|
|
1154
|
-
visit(blocks);
|
|
1155
|
-
return parts.join("\n").trim();
|
|
1156
|
-
}
|
|
1157
|
-
/**
|
|
1158
|
-
* Text of one assistant turn, excluding reasoning blocks.
|
|
1067
|
+
* A custom markdown file is read on every resolve (a rubric file is a few kilobytes and it is
|
|
1068
|
+
* resolved once per turn boundary), so editing it takes effect immediately. The parse is cached
|
|
1069
|
+
* against the file text, so repeated resolves of an unchanged file cost one read.
|
|
1159
1070
|
*
|
|
1160
|
-
*
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1164
|
-
* @param blocks - content blocks of an `assistant/message` event.
|
|
1165
|
-
* @returns The joined text, trimmed.
|
|
1071
|
+
* A broken custom file does NOT fail verification: a rubric typo must not disable the acceptance
|
|
1072
|
+
* gate, so the coding preset is used and the reason travels in {@link ResolvedCriteria.error}
|
|
1073
|
+
* for the settings page and the log. This mirrors the plugin's rule that scoring-channel
|
|
1074
|
+
* capability problems degrade instead of aborting a verification.
|
|
1166
1075
|
*/
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
};
|
|
1173
|
-
visit(blocks);
|
|
1174
|
-
return parts.join("\n").trim();
|
|
1175
|
-
}
|
|
1176
|
-
function successful(event) {
|
|
1177
|
-
return event.data.error === void 0 && event.data.message.content.every((block) => block.isError !== true);
|
|
1178
|
-
}
|
|
1179
|
-
function strictJson(text) {
|
|
1180
|
-
const trimmed = text.trim();
|
|
1181
|
-
if (!(trimmed.startsWith("{") && trimmed.endsWith("}"))) return void 0;
|
|
1182
|
-
try {
|
|
1183
|
-
return JSON.parse(trimmed);
|
|
1184
|
-
} catch {
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
function buildEvidenceIndex(events) {
|
|
1189
|
-
const taskStartSeq = latestDirectUserSeq(events);
|
|
1190
|
-
if (taskStartSeq === void 0) return void 0;
|
|
1191
|
-
const relevant = events.filter((event) => event.seq >= taskStartSeq);
|
|
1192
|
-
const calls = /* @__PURE__ */ new Map();
|
|
1193
|
-
const results = /* @__PURE__ */ new Map();
|
|
1194
|
-
const todos = /* @__PURE__ */ new Map();
|
|
1195
|
-
const teamTasks = /* @__PURE__ */ new Map();
|
|
1196
|
-
const paired = /* @__PURE__ */ new Map();
|
|
1197
|
-
const currentTeamTasks = /* @__PURE__ */ new Map();
|
|
1198
|
-
let narration;
|
|
1199
|
-
const dispatchedTools = /* @__PURE__ */ new Map();
|
|
1200
|
-
for (const rawEvent of relevant) {
|
|
1201
|
-
const event = rawEvent;
|
|
1202
|
-
if (event.type === "tool/call") calls.set(String(event.data.callId), rawEvent);
|
|
1203
|
-
else if (event.type === "tool/result" && successful(rawEvent)) results.set(String(event.data.message.source.callId), rawEvent);
|
|
1204
|
-
else if (event.type === "todo/write") todos.set(event.seq, event.data.todos);
|
|
1205
|
-
else if (event.type === "team/task") {
|
|
1206
|
-
const data = event.data;
|
|
1207
|
-
if (data?.task) {
|
|
1208
|
-
currentTeamTasks.set(data.task.id, { ...data.task });
|
|
1209
|
-
teamTasks.set(event.seq, [...currentTeamTasks.values()]);
|
|
1210
|
-
}
|
|
1211
|
-
} else if (event.type === "assistant/message") {
|
|
1212
|
-
const blocks = event.data?.message?.content ?? event.data?.content;
|
|
1213
|
-
const text = narrativeText(Array.isArray(blocks) ? blocks : []);
|
|
1214
|
-
if (text) narration = {
|
|
1215
|
-
seq: event.seq,
|
|
1216
|
-
text
|
|
1217
|
-
};
|
|
1218
|
-
} else if (event.type === "tool/ptc-dispatch" || event.type === "tool/code-dispatch") {
|
|
1219
|
-
const data = event.data;
|
|
1220
|
-
const rootCallId = typeof data.rootCallId === "string" && data.rootCallId ? data.rootCallId : void 0;
|
|
1221
|
-
if (rootCallId) {
|
|
1222
|
-
const names = dispatchedTools.get(rootCallId) ?? /* @__PURE__ */ new Set();
|
|
1223
|
-
names.add(data.name);
|
|
1224
|
-
dispatchedTools.set(rootCallId, names);
|
|
1225
|
-
}
|
|
1226
|
-
if (data.isError !== true && (!Array.isArray(data.content) || data.content.every((b) => b.isError !== true))) {
|
|
1227
|
-
const subCallId = String(data.subCallId ?? "code:" + event.seq);
|
|
1228
|
-
const content = Array.isArray(data.content) ? data.content : [];
|
|
1229
|
-
paired.set(subCallId, {
|
|
1230
|
-
name: data.name,
|
|
1231
|
-
callSeq: event.seq,
|
|
1232
|
-
resultSeq: event.seq,
|
|
1233
|
-
text: blockText$1(content)
|
|
1234
|
-
});
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
for (const [callId, call] of calls) {
|
|
1239
|
-
const result = results.get(callId);
|
|
1240
|
-
if (!result) continue;
|
|
1241
|
-
if (onlyBookkeepingDispatches(dispatchedTools.get(callId))) continue;
|
|
1242
|
-
paired.set(callId, {
|
|
1243
|
-
name: call.data.name,
|
|
1244
|
-
callSeq: call.seq,
|
|
1245
|
-
resultSeq: result.seq,
|
|
1246
|
-
text: blockText$1(result.data.message.content)
|
|
1247
|
-
});
|
|
1248
|
-
}
|
|
1249
|
-
return {
|
|
1250
|
-
problemSeq: taskStartSeq,
|
|
1251
|
-
calls: paired,
|
|
1252
|
-
todos,
|
|
1253
|
-
teamTasks,
|
|
1254
|
-
narration
|
|
1255
|
-
};
|
|
1256
|
-
}
|
|
1257
|
-
function parseTrustedWorkflow(value, callId, callSeq, resultSeq, maxCandidates, maxItemChars, maxInputChars) {
|
|
1258
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return [];
|
|
1259
|
-
const envelope = value;
|
|
1260
|
-
if (envelope.protocol !== "dsh-verifier-candidates" || envelope.version !== TRUSTED_WORKFLOW_VERSION || typeof envelope.groupId !== "string" || !envelope.groupId.trim() || !Array.isArray(envelope.candidates)) return [];
|
|
1261
|
-
const groupId = envelope.groupId.trim();
|
|
1262
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1263
|
-
const candidates = [];
|
|
1264
|
-
const considered = envelope.candidates.slice(0, maxCandidates);
|
|
1265
|
-
const perItem = itemBudget(considered.length, maxItemChars, maxInputChars);
|
|
1266
|
-
for (const item of considered) {
|
|
1267
|
-
if (typeof item !== "object" || item === null || Array.isArray(item)) return [];
|
|
1268
|
-
const row = item;
|
|
1269
|
-
if (row.status !== "completed" || typeof row.id !== "string" || !row.id.trim() || seen.has(row.id.trim()) || typeof row.content !== "string" || !row.content.trim()) return [];
|
|
1270
|
-
const id = row.id.trim();
|
|
1271
|
-
seen.add(id);
|
|
1272
|
-
const label = typeof row.label === "string" && row.label.trim() ? row.label.trim() : id;
|
|
1273
|
-
candidates.push({
|
|
1274
|
-
id,
|
|
1275
|
-
groupId,
|
|
1276
|
-
label: sanitizeVerifierText(label, Math.min(120, perItem)),
|
|
1277
|
-
content: sanitizeVerifierText(row.content, perItem),
|
|
1278
|
-
callId,
|
|
1279
|
-
fromSeq: callSeq,
|
|
1280
|
-
toSeq: resultSeq
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
return candidates.length >= 2 ? candidates : [];
|
|
1284
|
-
}
|
|
1285
|
-
function successfulExplicitKinds(events) {
|
|
1286
|
-
const index = buildEvidenceIndex(events);
|
|
1287
|
-
const kinds = /* @__PURE__ */ new Set();
|
|
1288
|
-
if (!index) return kinds;
|
|
1289
|
-
for (const pair of index.calls.values()) {
|
|
1290
|
-
if (!ROUTED_TOOLS.has(pair.name)) continue;
|
|
1291
|
-
if (pair.name === "verifier_compare") kinds.add("compare");
|
|
1292
|
-
else if (pair.name === "verifier_select") kinds.add("select");
|
|
1293
|
-
else kinds.add("track");
|
|
1294
|
-
}
|
|
1295
|
-
return kinds;
|
|
1296
|
-
}
|
|
1297
|
-
function canonicalTodoSnapshots(index) {
|
|
1298
|
-
const values = [];
|
|
1299
|
-
let previous = "";
|
|
1300
|
-
for (const [seq, todos] of index.todos) {
|
|
1301
|
-
const canonical = JSON.stringify(todos);
|
|
1302
|
-
if (canonical !== previous) values.push({
|
|
1303
|
-
seq,
|
|
1304
|
-
todos
|
|
1305
|
-
});
|
|
1306
|
-
previous = canonical;
|
|
1307
|
-
}
|
|
1308
|
-
return values;
|
|
1309
|
-
}
|
|
1310
|
-
/**
|
|
1311
|
-
* Tools whose successful output only maintains the agent's own bookkeeping.
|
|
1312
|
-
*
|
|
1313
|
-
* A checkpoint already renders the todo/team snapshot these tools wrote, so their
|
|
1314
|
-
* own result repeats it while displacing the real work output that came just before
|
|
1315
|
-
* them.
|
|
1316
|
-
*
|
|
1317
|
-
* `present` belongs here for the same reason: declaring deliverables produces no
|
|
1318
|
-
* independent output, and it is always the LAST call of a turn. A live session ran
|
|
1319
|
-
* typecheck + the full suite, committed, then presented — and every stop boundary
|
|
1320
|
-
* showed the judge "Latest observed tool output at routing time (run_code):
|
|
1321
|
-
* presented: 8" instead of the verification run one call earlier. Following its own
|
|
1322
|
-
* rule ("a state without real verification should not exceed K"), the judge capped
|
|
1323
|
-
* the newest checkpoint at exactly K = 52.6% against a 0.8 threshold on four
|
|
1324
|
-
* consecutive routes, so each one steered "continue the unfinished work" for work
|
|
1325
|
-
* that was finished and verified. The `deliverables/presented` event still records
|
|
1326
|
-
* what was presented; the tool result adds nothing the judge can grade.
|
|
1327
|
-
*/
|
|
1328
|
-
const BOOKKEEPING_TOOLS = /* @__PURE__ */ new Set([
|
|
1329
|
-
"todo_write",
|
|
1330
|
-
"create_goal",
|
|
1331
|
-
"get_goal",
|
|
1332
|
-
"update_goal",
|
|
1333
|
-
"interrupt_agent",
|
|
1334
|
-
"list_agents",
|
|
1335
|
-
"exit_plan_mode",
|
|
1336
|
-
"skill",
|
|
1337
|
-
"present"
|
|
1338
|
-
]);
|
|
1339
|
-
/** Whether a wrapper's dispatches were all bookkeeping; undefined/empty means it dispatched nothing. */
|
|
1340
|
-
function onlyBookkeepingDispatches(names) {
|
|
1341
|
-
if (names === void 0 || names.size === 0) return false;
|
|
1342
|
-
for (const name of names) if (!BOOKKEEPING_TOOLS.has(name)) return false;
|
|
1343
|
-
return true;
|
|
1344
|
-
}
|
|
1345
|
-
/**
|
|
1346
|
-
* Observed tool evidence available at one checkpoint.
|
|
1347
|
-
*
|
|
1348
|
-
* A checkpoint rendered from todo/team text alone can never clear the progress
|
|
1349
|
-
* threshold: the judge prompt explicitly refuses to credit a state that carries
|
|
1350
|
-
* no observed output. The most recent successful tool result at or before the
|
|
1351
|
-
* checkpoint is therefore attached as evidence.
|
|
1352
|
-
*
|
|
1353
|
-
* Bookkeeping tools are skipped: the checkpoint already renders the todo/team
|
|
1354
|
-
* snapshot they wrote, so their own result repeats it while displacing the real
|
|
1355
|
-
* output produced just before them (a live session attached `create_goal`,
|
|
1356
|
-
* `update_goal` and `interrupt_agent` output to checkpoints that had already run
|
|
1357
|
-
* the task's test suite).
|
|
1358
|
-
* @param index - evidence index of the current task.
|
|
1359
|
-
* @param seq - checkpoint sequence number, or `Infinity` for the current state.
|
|
1360
|
-
* @param budget - maximum characters the evidence may occupy.
|
|
1361
|
-
* @param current - render the newest output in the task instead of the newest one before `seq`.
|
|
1362
|
-
* @returns Evidence block, or '' when the task produced none yet.
|
|
1363
|
-
*/
|
|
1364
|
-
function checkpointEvidence(index, seq, budget, current = false) {
|
|
1365
|
-
if (budget < 64) return "";
|
|
1366
|
-
let latest;
|
|
1367
|
-
for (const pair of index.calls.values()) {
|
|
1368
|
-
if (BOOKKEEPING_TOOLS.has(pair.name) || !pair.text.trim()) continue;
|
|
1369
|
-
if (pair.resultSeq <= seq && (latest === void 0 || pair.resultSeq > latest.resultSeq)) latest = pair;
|
|
1370
|
-
}
|
|
1371
|
-
if (latest === void 0) return "";
|
|
1372
|
-
const prefix = current ? "\n\nLatest observed tool output at routing time (" + latest.name + "):\n" : "\n\nLatest observed tool output before this checkpoint (" + latest.name + "):\n";
|
|
1373
|
-
if (prefix.length >= budget) return "";
|
|
1374
|
-
return prefix + sanitizeVerifierText(latest.text, budget - prefix.length);
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* Per-item character budget for a decision with a known item count.
|
|
1378
|
-
*
|
|
1379
|
-
* boundDecision() enforces the COMBINED cap, so spending maxItemChars per item
|
|
1380
|
-
* drops the whole decision as soon as the items are numerous or large. Splitting
|
|
1381
|
-
* the combined budget keeps both caps satisfied by construction; a single item
|
|
1382
|
-
* still gets the full maxItemChars.
|
|
1383
|
-
* @param count - number of items that will be rendered.
|
|
1384
|
-
* @param maxItemChars - hard per-item cap enforced by boundDecision().
|
|
1385
|
-
* @param maxInputChars - hard combined cap enforced by boundDecision().
|
|
1386
|
-
* @returns The per-item character budget, never below 1.
|
|
1387
|
-
*/
|
|
1388
|
-
function itemBudget(count, maxItemChars, maxInputChars) {
|
|
1389
|
-
return Math.max(1, Math.min(maxItemChars, Math.floor(maxInputChars / Math.max(1, count))));
|
|
1390
|
-
}
|
|
1391
|
-
/**
|
|
1392
|
-
* Upper bound on the checkpoints rendered into one routed track decision.
|
|
1393
|
-
*
|
|
1394
|
-
* boundDecision() rejects the WHOLE decision once the rendered steps exceed
|
|
1395
|
-
* maxInputChars, while the number of durable snapshots is unbounded (every changed
|
|
1396
|
-
* todo/team snapshot becomes a checkpoint). A long task therefore used to lose
|
|
1397
|
-
* progress routing exactly when it needed it, so only the most recent checkpoints
|
|
1398
|
-
* are kept and the combined input budget is split across them.
|
|
1399
|
-
*/
|
|
1400
|
-
const MAX_ROUTED_CHECKPOINTS = 6;
|
|
1401
|
-
/**
|
|
1402
|
-
* Render progress checkpoints as "state + the output that proves it".
|
|
1403
|
-
*
|
|
1404
|
-
* The result fits both caps by construction: each step is at most min(maxItemChars,
|
|
1405
|
-
* maxInputChars / kept.length) characters, so the combined length can never exceed
|
|
1406
|
-
* maxInputChars and boundDecision() no longer drops the whole route.
|
|
1407
|
-
*
|
|
1408
|
-
* The newest checkpoint is also the state the route is judging, so it is rendered as
|
|
1409
|
-
* the CURRENT state: its evidence is the newest observed output in the task rather
|
|
1410
|
-
* than the newest one before the last todo snapshot (which is often several tool calls
|
|
1411
|
-
* stale), and it carries the agent's latest prose as an explicitly labelled claim (see
|
|
1412
|
-
* {@link currentNarration}), because prose deliverables never reach a tool.
|
|
1413
|
-
* @param index - evidence index of the current task.
|
|
1414
|
-
* @param sources - checkpoints in chronological order.
|
|
1415
|
-
* @param maxItemChars - hard per-item cap enforced by boundDecision().
|
|
1416
|
-
* @param maxInputChars - hard combined cap enforced by boundDecision().
|
|
1417
|
-
* @returns The rendered steps plus the sequence numbers they were built from.
|
|
1418
|
-
*/
|
|
1419
|
-
function renderCheckpointSteps(index, sources, maxItemChars, maxInputChars) {
|
|
1420
|
-
const kept = sources.slice(-6);
|
|
1421
|
-
const omitted = sources.length - kept.length;
|
|
1422
|
-
if (kept.length === 0) return {
|
|
1423
|
-
steps: [],
|
|
1424
|
-
evidenceSeqs: [],
|
|
1425
|
-
omitted: 0
|
|
1426
|
-
};
|
|
1427
|
-
const stepCap = itemBudget(kept.length, maxItemChars, maxInputChars);
|
|
1428
|
-
const evidenceBudget = Math.max(0, Math.min(Math.floor(maxInputChars / 2), Math.floor(maxItemChars / 2), Math.floor(stepCap / 2)));
|
|
1429
|
-
return {
|
|
1430
|
-
steps: kept.map((source, position) => {
|
|
1431
|
-
const note = position === 0 && omitted > 0 ? "Earlier " + omitted + " checkpoint(s) omitted; showing the " + kept.length + " most recent.\n" : "";
|
|
1432
|
-
const isCurrent = position === kept.length - 1;
|
|
1433
|
-
const observed = checkpointEvidence(index, isCurrent ? Number.POSITIVE_INFINITY : source.seq, isCurrent ? Math.floor(evidenceBudget / 2) : evidenceBudget, isCurrent);
|
|
1434
|
-
const narration = isCurrent ? currentNarration(index, evidenceBudget - observed.length) : "";
|
|
1435
|
-
return sanitizeVerifierText(note + source.label + source.body, Math.max(1, stepCap - observed.length - narration.length)) + observed + narration;
|
|
1436
|
-
}),
|
|
1437
|
-
evidenceSeqs: kept.map((source) => source.seq),
|
|
1438
|
-
omitted
|
|
1439
|
-
};
|
|
1440
|
-
}
|
|
1441
|
-
/**
|
|
1442
|
-
* The agent's newest unverified prose, attached to the current checkpoint.
|
|
1443
|
-
*
|
|
1444
|
-
* Progress checkpoints are graded against "would this state satisfy the task", and
|
|
1445
|
-
* for work whose deliverable is prose the todo snapshot plus one tool result say
|
|
1446
|
-
* nothing about it: every checkpoint then scores "certainly NO" even though the
|
|
1447
|
-
* deliverable exists (a live review task scored 0% on all four checkpoints while the
|
|
1448
|
-
* final session acceptance of the same work passed). The block is labelled as a claim
|
|
1449
|
-
* so the judge can weigh it without treating it as observed output.
|
|
1450
|
-
* @param index - evidence index of the current task.
|
|
1451
|
-
* @param budget - maximum characters the narration may occupy.
|
|
1452
|
-
* @returns Narration block, or '' when the task produced no prose or has no budget.
|
|
1453
|
-
*/
|
|
1454
|
-
function currentNarration(index, budget) {
|
|
1455
|
-
const narration = index.narration;
|
|
1456
|
-
if (narration === void 0 || budget < 64) return "";
|
|
1457
|
-
const prefix = "\n\nNewest agent narration in this task (the agent's own claim — NOT observed evidence):\n";
|
|
1458
|
-
if (87 >= budget) return "";
|
|
1459
|
-
return prefix + sanitizeVerifierText(narration.text, budget - 87);
|
|
1460
|
-
}
|
|
1461
|
-
function canonicalTeamTaskSnapshots(index) {
|
|
1462
|
-
const values = [];
|
|
1463
|
-
let previous = "";
|
|
1464
|
-
for (const [seq, tasks] of index.teamTasks) {
|
|
1465
|
-
const canonical = JSON.stringify(tasks.map((t) => ({
|
|
1466
|
-
id: t.id,
|
|
1467
|
-
status: t.status,
|
|
1468
|
-
revision: t.revision
|
|
1469
|
-
})));
|
|
1470
|
-
if (canonical !== previous) values.push({
|
|
1471
|
-
seq,
|
|
1472
|
-
tasks
|
|
1473
|
-
});
|
|
1474
|
-
previous = canonical;
|
|
1475
|
-
}
|
|
1476
|
-
return values;
|
|
1477
|
-
}
|
|
1478
|
-
function analyzeStructuredRoute(events, maxCandidates = 8, maxItemChars = 2e4, maxInputChars = 6e4) {
|
|
1479
|
-
const index = buildEvidenceIndex(events);
|
|
1480
|
-
if (!index) return void 0;
|
|
1481
|
-
const explicit = successfulExplicitKinds(events.filter((event) => event.seq >= index.problemSeq));
|
|
1482
|
-
const groups = [];
|
|
1483
|
-
for (const [callId, pair] of index.calls) {
|
|
1484
|
-
if (pair.name !== "workflow") continue;
|
|
1485
|
-
const candidates = parseTrustedWorkflow(strictJson(pair.text), callId, pair.callSeq, pair.resultSeq, maxCandidates, maxItemChars, maxInputChars);
|
|
1486
|
-
if (candidates.length >= 2) groups.push(candidates);
|
|
1076
|
+
var CriteriaResolver = class {
|
|
1077
|
+
load;
|
|
1078
|
+
cache;
|
|
1079
|
+
constructor(load = (path) => readFile(path, "utf8")) {
|
|
1080
|
+
this.load = load;
|
|
1487
1081
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
kind: "select",
|
|
1492
|
-
source: "structured",
|
|
1493
|
-
confidence: 1,
|
|
1494
|
-
reason: "trusted workflow candidate envelope",
|
|
1495
|
-
fingerprint: stableHash({
|
|
1496
|
-
kind: "select",
|
|
1497
|
-
candidates
|
|
1498
|
-
}),
|
|
1499
|
-
candidates
|
|
1500
|
-
};
|
|
1501
|
-
if (candidates?.length === 2 && !explicit.has("compare")) return {
|
|
1502
|
-
kind: "compare",
|
|
1503
|
-
source: "structured",
|
|
1504
|
-
confidence: 1,
|
|
1505
|
-
reason: "trusted workflow candidate envelope",
|
|
1506
|
-
fingerprint: stableHash({
|
|
1507
|
-
kind: "compare",
|
|
1508
|
-
candidates
|
|
1509
|
-
}),
|
|
1510
|
-
candidates: [candidates[0], candidates[1]]
|
|
1511
|
-
};
|
|
1512
|
-
if (!explicit.has("track")) {
|
|
1513
|
-
const snapshots = canonicalTodoSnapshots(index);
|
|
1514
|
-
if (snapshots.length >= 2 && snapshots.some((snapshot) => snapshot.todos.length >= 2)) {
|
|
1515
|
-
const rendered = renderCheckpointSteps(index, snapshots.map((snapshot) => ({
|
|
1516
|
-
seq: snapshot.seq,
|
|
1517
|
-
label: "Todo checkpoint seq " + snapshot.seq + ":\n",
|
|
1518
|
-
body: snapshot.todos.map((todo) => "- [" + todo.status + "] " + todo.content).join("\n")
|
|
1519
|
-
})), maxItemChars, maxInputChars);
|
|
1520
|
-
return {
|
|
1521
|
-
kind: "track",
|
|
1522
|
-
source: "structured",
|
|
1523
|
-
confidence: 1,
|
|
1524
|
-
reason: "changed durable todo snapshots",
|
|
1525
|
-
fingerprint: stableHash({
|
|
1526
|
-
kind: "track",
|
|
1527
|
-
snapshots,
|
|
1528
|
-
steps: rendered.steps
|
|
1529
|
-
}),
|
|
1530
|
-
steps: rendered.steps,
|
|
1531
|
-
checkpoints: rendered.steps.map((_, i) => i + 1),
|
|
1532
|
-
evidenceSeqs: rendered.evidenceSeqs
|
|
1533
|
-
};
|
|
1534
|
-
}
|
|
1535
|
-
const teamSnapshots = canonicalTeamTaskSnapshots(index);
|
|
1536
|
-
if (teamSnapshots.length >= 2) {
|
|
1537
|
-
const rendered = renderCheckpointSteps(index, teamSnapshots.map((snapshot) => ({
|
|
1538
|
-
seq: snapshot.seq,
|
|
1539
|
-
label: "Team task checkpoint seq " + snapshot.seq + ":\n",
|
|
1540
|
-
body: snapshot.tasks.map((task) => "- [" + task.status + "] " + task.subject + (task.description ? " (" + task.description + ")" : "")).join("\n")
|
|
1541
|
-
})), maxItemChars, maxInputChars);
|
|
1082
|
+
async resolve(selection, file) {
|
|
1083
|
+
if (selection !== "custom") {
|
|
1084
|
+
const preset = CRITERIA_PRESETS[selection];
|
|
1542
1085
|
return {
|
|
1543
|
-
|
|
1544
|
-
source: "
|
|
1545
|
-
|
|
1546
|
-
reason: "changed durable team tasks",
|
|
1547
|
-
fingerprint: stableHash({
|
|
1548
|
-
kind: "track",
|
|
1549
|
-
teamSnapshots,
|
|
1550
|
-
steps: rendered.steps
|
|
1551
|
-
}),
|
|
1552
|
-
steps: rendered.steps,
|
|
1553
|
-
checkpoints: rendered.steps.map((_, i) => i + 1),
|
|
1554
|
-
evidenceSeqs: rendered.evidenceSeqs
|
|
1086
|
+
criteria: preset ?? DEFAULT_CRITERIA,
|
|
1087
|
+
source: preset ? selection : "fallback",
|
|
1088
|
+
...preset ? {} : { error: "unknown criteria preset: " + String(selection) }
|
|
1555
1089
|
};
|
|
1556
1090
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
const HINT_ARTIFACTS = /* @__PURE__ */ new Set([
|
|
1570
|
-
"subagent",
|
|
1571
|
-
"subagent_fork",
|
|
1572
|
-
"workflow",
|
|
1573
|
-
"exit_plan_mode"
|
|
1574
|
-
]);
|
|
1575
|
-
/**
|
|
1576
|
-
* Whether a smart-mode stop boundary is worth a semantic classification call.
|
|
1577
|
-
*
|
|
1578
|
-
* The semantic phase only runs when the structured pass produced nothing, so this
|
|
1579
|
-
* answers "is there material the structured pass never consumes?" — never "are there
|
|
1580
|
-
* todo/team snapshots?", which the structured pass would have used already.
|
|
1581
|
-
* @param events - Session event log.
|
|
1582
|
-
* @returns True when a subagent/workflow/plan artifact exists.
|
|
1583
|
-
*/
|
|
1584
|
-
function semanticRouteHint(events) {
|
|
1585
|
-
const index = buildEvidenceIndex(events);
|
|
1586
|
-
if (!index) return false;
|
|
1587
|
-
for (const pair of index.calls.values()) if (HINT_ARTIFACTS.has(pair.name)) return true;
|
|
1588
|
-
return false;
|
|
1589
|
-
}
|
|
1590
|
-
function buildSemanticRoutePrompt(problem, events, maxCandidates, maxItemChars = 2e4, maxInputChars = 6e4) {
|
|
1591
|
-
const index = buildEvidenceIndex(events);
|
|
1592
|
-
if (!index) throw new Error("llm-verifier: semantic routing requires a direct user task");
|
|
1593
|
-
const artifacts = [];
|
|
1594
|
-
let used = 0;
|
|
1595
|
-
let omitted = 0;
|
|
1596
|
-
for (const [callId, pair] of [...index.calls.entries()].reverse()) {
|
|
1597
|
-
if (BOOKKEEPING_TOOLS.has(pair.name)) continue;
|
|
1598
|
-
const text = sanitizeVerifierText(pair.text, maxItemChars);
|
|
1599
|
-
if (artifacts.length > 0 && used + text.length > maxInputChars) {
|
|
1600
|
-
omitted += 1;
|
|
1601
|
-
continue;
|
|
1602
|
-
}
|
|
1603
|
-
used += text.length;
|
|
1604
|
-
artifacts.push({
|
|
1605
|
-
callId,
|
|
1606
|
-
tool: pair.name,
|
|
1607
|
-
callSeq: pair.callSeq,
|
|
1608
|
-
resultSeq: pair.resultSeq,
|
|
1609
|
-
text
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
artifacts.reverse();
|
|
1613
|
-
const checkpoints = [];
|
|
1614
|
-
for (const [seq, todos] of [...index.todos.entries()].reverse()) {
|
|
1615
|
-
const cost = JSON.stringify(todos).length;
|
|
1616
|
-
if (checkpoints.length > 0 && used + cost > maxInputChars) {
|
|
1617
|
-
omitted += 1;
|
|
1618
|
-
continue;
|
|
1091
|
+
const path = (file ?? "").trim();
|
|
1092
|
+
let text;
|
|
1093
|
+
try {
|
|
1094
|
+
if (!path) throw new Error("no criteria file configured (set criteriaFile, or switch criteriaPreset away from custom)");
|
|
1095
|
+
text = await this.load(path);
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
return this.remember("custom\0" + path + "\0!" + String(error), {
|
|
1098
|
+
criteria: DEFAULT_CRITERIA,
|
|
1099
|
+
source: "fallback",
|
|
1100
|
+
...path ? { file: path } : {},
|
|
1101
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1102
|
+
});
|
|
1619
1103
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
}
|
|
1638
|
-
function parseSemanticRoute(text, maxCandidates = 8) {
|
|
1639
|
-
const parsed = strictJson(text);
|
|
1640
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
1641
|
-
const row = parsed;
|
|
1642
|
-
if (Object.keys(row).some((key) => !KNOWN_ROUTE_KEYS.has(key)) || Object.keys(row).length !== KNOWN_ROUTE_KEYS.size) return void 0;
|
|
1643
|
-
if (![
|
|
1644
|
-
"none",
|
|
1645
|
-
"compare",
|
|
1646
|
-
"select",
|
|
1647
|
-
"track"
|
|
1648
|
-
].includes(String(row.kind)) || typeof row.confidence !== "number" || !Number.isFinite(row.confidence) || row.confidence < 0 || row.confidence > 1 || typeof row.reason !== "string" || !Array.isArray(row.candidateCallIds) || !Array.isArray(row.checkpointSeqs)) return void 0;
|
|
1649
|
-
const kind = String(row.kind);
|
|
1650
|
-
const candidateCallIds = row.candidateCallIds.filter((id) => typeof id === "string" && id.length > 0);
|
|
1651
|
-
const checkpointSeqs = row.checkpointSeqs.filter((seq) => Number.isSafeInteger(seq) && seq >= 0);
|
|
1652
|
-
if (candidateCallIds.length !== row.candidateCallIds.length || checkpointSeqs.length !== row.checkpointSeqs.length || new Set(candidateCallIds).size !== candidateCallIds.length || new Set(checkpointSeqs).size !== checkpointSeqs.length || checkpointSeqs.some((seq, i) => i > 0 && seq <= checkpointSeqs[i - 1])) return void 0;
|
|
1653
|
-
if (kind === "none" && (candidateCallIds.length || checkpointSeqs.length)) return void 0;
|
|
1654
|
-
if (kind === "compare" && (candidateCallIds.length !== 2 || checkpointSeqs.length)) return void 0;
|
|
1655
|
-
if (kind === "select" && (candidateCallIds.length < 3 || candidateCallIds.length > maxCandidates || checkpointSeqs.length)) return void 0;
|
|
1656
|
-
if (kind === "track" && (checkpointSeqs.length < 2 || candidateCallIds.length)) return void 0;
|
|
1657
|
-
return {
|
|
1658
|
-
kind,
|
|
1659
|
-
confidence: row.confidence,
|
|
1660
|
-
reason: row.reason.slice(0, 500),
|
|
1661
|
-
candidateCallIds,
|
|
1662
|
-
checkpointSeqs
|
|
1663
|
-
};
|
|
1664
|
-
}
|
|
1665
|
-
function semanticDecision(output, events, maxItemChars = 2e4, maxInputChars = 6e4) {
|
|
1666
|
-
if (output.kind === "none") return void 0;
|
|
1667
|
-
const index = buildEvidenceIndex(events);
|
|
1668
|
-
if (!index) return void 0;
|
|
1669
|
-
if (output.kind === "track") {
|
|
1670
|
-
const snapshots = output.checkpointSeqs.map((seq) => ({
|
|
1671
|
-
seq,
|
|
1672
|
-
todos: index.todos.get(seq)
|
|
1673
|
-
})).filter((item) => item.todos !== void 0);
|
|
1674
|
-
if (snapshots.length !== output.checkpointSeqs.length) return void 0;
|
|
1675
|
-
const rendered = renderCheckpointSteps(index, snapshots.map((snapshot) => ({
|
|
1676
|
-
seq: snapshot.seq,
|
|
1677
|
-
label: "Todo checkpoint seq " + snapshot.seq + ":\n",
|
|
1678
|
-
body: snapshot.todos.map((todo) => "- [" + todo.status + "] " + todo.content).join("\n")
|
|
1679
|
-
})), maxItemChars, maxInputChars);
|
|
1680
|
-
return {
|
|
1681
|
-
kind: "track",
|
|
1682
|
-
source: "semantic",
|
|
1683
|
-
confidence: output.confidence,
|
|
1684
|
-
reason: output.reason,
|
|
1685
|
-
fingerprint: stableHash({
|
|
1686
|
-
kind: "track",
|
|
1687
|
-
seqs: output.checkpointSeqs,
|
|
1688
|
-
steps: rendered.steps
|
|
1689
|
-
}),
|
|
1690
|
-
steps: rendered.steps,
|
|
1691
|
-
checkpoints: rendered.steps.map((_, i) => i + 1),
|
|
1692
|
-
evidenceSeqs: rendered.evidenceSeqs
|
|
1693
|
-
};
|
|
1694
|
-
}
|
|
1695
|
-
const perItem = itemBudget(output.candidateCallIds.length, maxItemChars, maxInputChars);
|
|
1696
|
-
const candidates = output.candidateCallIds.map((callId, i) => {
|
|
1697
|
-
const pair = index.calls.get(callId);
|
|
1698
|
-
if (!pair || BOOKKEEPING_TOOLS.has(pair.name)) return void 0;
|
|
1699
|
-
return {
|
|
1700
|
-
id: callId,
|
|
1701
|
-
groupId: "semantic",
|
|
1702
|
-
label: pair.name + " " + (i + 1),
|
|
1703
|
-
content: sanitizeVerifierText(pair.text, perItem),
|
|
1704
|
-
callId,
|
|
1705
|
-
fromSeq: pair.callSeq,
|
|
1706
|
-
toSeq: pair.resultSeq
|
|
1707
|
-
};
|
|
1708
|
-
}).filter((candidate) => candidate !== void 0);
|
|
1709
|
-
if (candidates.length !== output.candidateCallIds.length) return void 0;
|
|
1710
|
-
const fingerprint = stableHash({
|
|
1711
|
-
kind: output.kind,
|
|
1712
|
-
candidates
|
|
1713
|
-
});
|
|
1714
|
-
if (output.kind === "compare") return {
|
|
1715
|
-
kind: "compare",
|
|
1716
|
-
source: "semantic",
|
|
1717
|
-
confidence: output.confidence,
|
|
1718
|
-
reason: output.reason,
|
|
1719
|
-
fingerprint,
|
|
1720
|
-
candidates: [candidates[0], candidates[1]]
|
|
1721
|
-
};
|
|
1722
|
-
return {
|
|
1723
|
-
kind: "select",
|
|
1724
|
-
source: "semantic",
|
|
1725
|
-
confidence: output.confidence,
|
|
1726
|
-
reason: output.reason,
|
|
1727
|
-
fingerprint,
|
|
1728
|
-
candidates
|
|
1729
|
-
};
|
|
1730
|
-
}
|
|
1731
|
-
/**
|
|
1732
|
-
* Estimated model calls for one routed decision.
|
|
1733
|
-
*
|
|
1734
|
-
* Uses the real tournament shape (ring edges + pivot-round edges x criteria x
|
|
1735
|
-
* repeats) instead of a flat per-candidate constant, which over-reserved by
|
|
1736
|
-
* roughly an order of magnitude and silently rejected legitimate selections.
|
|
1737
|
-
* @param decision - the routed decision about to run.
|
|
1738
|
-
* @param repeats - evaluation repeats per criterion.
|
|
1739
|
-
* @param criteriaCount - number of criteria evaluated per comparison.
|
|
1740
|
-
* @returns The planned model-call count, never below 1.
|
|
1741
|
-
*/
|
|
1742
|
-
/**
|
|
1743
|
-
* Scoring repeats a routed decision actually runs.
|
|
1744
|
-
*
|
|
1745
|
-
* `compare` judges ONE unordered pair, and `VerifierEngine.compare` only swaps the
|
|
1746
|
-
* candidates on odd repeat indices — with the shipped default of a single round the
|
|
1747
|
-
* first candidate therefore always sat in slot A, so the winner was partly decided by
|
|
1748
|
-
* listing order. Rounding its count up to an even number averages a swapped round and
|
|
1749
|
-
* cancels that.
|
|
1750
|
-
*
|
|
1751
|
-
* `select` does not need it: its ring is symmetric by construction and the pivot round
|
|
1752
|
-
* is oriented per pair by the engine, so one round is already unbiased. `track` scores
|
|
1753
|
-
* a single checkpoint list and has no slots at all.
|
|
1754
|
-
* @param decision - the routed decision about to run.
|
|
1755
|
-
* @param configured - the configured auto-route repeat count.
|
|
1756
|
-
* @returns The repeat count to pass to the engine.
|
|
1757
|
-
*/
|
|
1758
|
-
function routedRepeats(decision, configured) {
|
|
1759
|
-
if (decision.kind !== "compare") return configured;
|
|
1760
|
-
return configured % 2 === 0 ? configured : configured + 1;
|
|
1761
|
-
}
|
|
1762
|
-
function estimateRoutedCalls(decision, repeats, criteriaCount) {
|
|
1763
|
-
if (decision.kind === "compare") return Math.max(1, criteriaCount * repeats);
|
|
1764
|
-
if (decision.kind === "track") return Math.max(1, repeats);
|
|
1765
|
-
const count = decision.candidates.length;
|
|
1766
|
-
const pivots = Math.min(2, count);
|
|
1767
|
-
const ring = count <= 2 ? 1 : count;
|
|
1768
|
-
const pivotRound = count <= 2 ? 0 : Math.max(0, (count - pivots) * pivots + pivots * (pivots - 1) / 2 - (2 * pivots - 1));
|
|
1769
|
-
return Math.max(1, (ring + pivotRound) * criteriaCount * repeats);
|
|
1770
|
-
}
|
|
1771
|
-
function boundDecision(decision, policy) {
|
|
1772
|
-
if (decision === void 0) return void 0;
|
|
1773
|
-
const lengths = decision.kind === "track" ? decision.steps.map((value) => value.length) : decision.candidates.map((value) => value.content.length);
|
|
1774
|
-
if (lengths.some((length) => length > policy.maxItemChars) || lengths.reduce((sum, length) => sum + length, 0) > policy.maxInputChars) return void 0;
|
|
1775
|
-
return decision;
|
|
1776
|
-
}
|
|
1777
|
-
var AutoVerifierRouter = class {
|
|
1778
|
-
states = /* @__PURE__ */ new Map();
|
|
1779
|
-
/** Agent ids that already received this task's budget-exhaustion notice. */
|
|
1780
|
-
exhaustedNotices = /* @__PURE__ */ new Set();
|
|
1781
|
-
serial = 0;
|
|
1782
|
-
state(agent) {
|
|
1783
|
-
const taskStartSeq = latestDirectUserSeq(sessionEvents(agent.session));
|
|
1784
|
-
if (taskStartSeq === void 0) return void 0;
|
|
1785
|
-
const id = String(agent.id);
|
|
1786
|
-
const state = this.states.get(id) ?? {
|
|
1787
|
-
taskStartSeq,
|
|
1788
|
-
taskAttempts: 0,
|
|
1789
|
-
sessionAttempts: 0,
|
|
1790
|
-
taskModelCalls: 0,
|
|
1791
|
-
sessionModelCalls: 0,
|
|
1792
|
-
completed: /* @__PURE__ */ new Set(),
|
|
1793
|
-
failed: /* @__PURE__ */ new Set(),
|
|
1794
|
-
strictBlocked: false
|
|
1795
|
-
};
|
|
1796
|
-
if (state.taskStartSeq !== taskStartSeq) {
|
|
1797
|
-
state.taskStartSeq = taskStartSeq;
|
|
1798
|
-
state.taskAttempts = 0;
|
|
1799
|
-
state.taskModelCalls = 0;
|
|
1800
|
-
state.completed.clear();
|
|
1801
|
-
state.failed.clear();
|
|
1802
|
-
state.inFlight = void 0;
|
|
1803
|
-
state.finalRequiredFromSeq = void 0;
|
|
1804
|
-
state.strictBlocked = false;
|
|
1805
|
-
this.exhaustedNotices.delete(id);
|
|
1104
|
+
const key = "custom\0" + path + "\0" + text;
|
|
1105
|
+
if (this.cache?.key === key) return this.cache.value;
|
|
1106
|
+
try {
|
|
1107
|
+
const parsed = parseCriteriaMarkdown(text);
|
|
1108
|
+
return this.remember(key, {
|
|
1109
|
+
criteria: parsed.criteria,
|
|
1110
|
+
...parsed.groundTruthNote ? { groundTruthNote: parsed.groundTruthNote } : {},
|
|
1111
|
+
source: "custom",
|
|
1112
|
+
file: path
|
|
1113
|
+
});
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
return this.remember(key + "\0!" + String(error), {
|
|
1116
|
+
criteria: DEFAULT_CRITERIA,
|
|
1117
|
+
source: "fallback",
|
|
1118
|
+
file: path,
|
|
1119
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1120
|
+
});
|
|
1806
1121
|
}
|
|
1807
|
-
this.states.set(id, state);
|
|
1808
|
-
return state;
|
|
1809
1122
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
const reservation = {
|
|
1815
|
-
id: String(++this.serial),
|
|
1816
|
-
phase,
|
|
1817
|
-
fingerprint,
|
|
1818
|
-
taskStartSeq: state.taskStartSeq
|
|
1123
|
+
remember(key, value) {
|
|
1124
|
+
this.cache = {
|
|
1125
|
+
key,
|
|
1126
|
+
value
|
|
1819
1127
|
};
|
|
1820
|
-
|
|
1821
|
-
state.taskAttempts++;
|
|
1822
|
-
state.sessionAttempts++;
|
|
1823
|
-
state.taskModelCalls += expectedCalls;
|
|
1824
|
-
state.sessionModelCalls += expectedCalls;
|
|
1825
|
-
return reservation;
|
|
1826
|
-
}
|
|
1827
|
-
commit(agent, reservation, evidenceSeq) {
|
|
1828
|
-
const state = this.state(agent);
|
|
1829
|
-
if (!state || state.inFlight?.id !== reservation.id || state.taskStartSeq !== reservation.taskStartSeq) return false;
|
|
1830
|
-
state.inFlight = void 0;
|
|
1831
|
-
state.completed.add(reservation.fingerprint);
|
|
1832
|
-
state.strictBlocked = false;
|
|
1833
|
-
if (reservation.phase !== "semantic" && reservation.phase !== "final" && reservation.phase !== "plan_review") state.finalRequiredFromSeq = Math.max(state.finalRequiredFromSeq ?? 0, evidenceSeq ?? reservation.taskStartSeq);
|
|
1834
|
-
if (reservation.phase === "final") state.finalRequiredFromSeq = void 0;
|
|
1835
|
-
return true;
|
|
1836
|
-
}
|
|
1837
|
-
fail(agent, reservation, strict) {
|
|
1838
|
-
const state = this.state(agent);
|
|
1839
|
-
if (!state || state.inFlight?.id !== reservation.id) return;
|
|
1840
|
-
state.inFlight = void 0;
|
|
1841
|
-
state.failed.add(reservation.fingerprint);
|
|
1842
|
-
if (strict) state.strictBlocked = true;
|
|
1843
|
-
}
|
|
1844
|
-
/**
|
|
1845
|
-
* Claim this task's single budget-exhaustion notice.
|
|
1846
|
-
*
|
|
1847
|
-
* Once the task/session budget is spent no reservation can ever be granted
|
|
1848
|
-
* again, so the states that demand strict verification (strictBlocked,
|
|
1849
|
-
* finalRequiredFromSeq) can never be cleared by a commit. Steering on every
|
|
1850
|
-
* stop boundary would then hold the turn open forever — the harness has no
|
|
1851
|
-
* turn budget — so the notice is emitted at most once per task and the
|
|
1852
|
-
* remaining stop boundaries close normally.
|
|
1853
|
-
* @param agent - Agent whose task is out of budget.
|
|
1854
|
-
* @returns True when the caller should steer the notice now.
|
|
1855
|
-
*/
|
|
1856
|
-
claimExhaustedNotice(agent) {
|
|
1857
|
-
if (!this.state(agent)) return false;
|
|
1858
|
-
const id = String(agent.id);
|
|
1859
|
-
if (this.exhaustedNotices.has(id)) return false;
|
|
1860
|
-
this.exhaustedNotices.add(id);
|
|
1861
|
-
return true;
|
|
1862
|
-
}
|
|
1863
|
-
/** Whether the task or session budget cannot cover one more routed decision. */
|
|
1864
|
-
budgetExhausted(agent, expectedCalls, policy) {
|
|
1865
|
-
const state = this.state(agent);
|
|
1866
|
-
if (!state) return true;
|
|
1867
|
-
return state.taskAttempts >= policy.maxPerTask || state.sessionAttempts >= policy.maxPerSession || state.taskModelCalls + expectedCalls > policy.maxModelCallsPerTask || state.sessionModelCalls + expectedCalls > policy.maxModelCallsPerSession;
|
|
1868
|
-
}
|
|
1869
|
-
/** Whether this exact fingerprint already passed within the current task. */
|
|
1870
|
-
completedFingerprint(agent, fingerprint) {
|
|
1871
|
-
return this.state(agent)?.completed.has(fingerprint) ?? false;
|
|
1872
|
-
}
|
|
1873
|
-
finalRequired(agent) {
|
|
1874
|
-
return this.state(agent)?.finalRequiredFromSeq;
|
|
1875
|
-
}
|
|
1876
|
-
strictBlocked(agent) {
|
|
1877
|
-
return this.state(agent)?.strictBlocked ?? false;
|
|
1878
|
-
}
|
|
1879
|
-
release(agent) {
|
|
1880
|
-
this.states.delete(String(agent.id));
|
|
1881
|
-
this.exhaustedNotices.delete(String(agent.id));
|
|
1128
|
+
return value;
|
|
1882
1129
|
}
|
|
1883
1130
|
};
|
|
1884
1131
|
//#endregion
|
|
1885
|
-
//#region src/auto.ts
|
|
1886
|
-
const PASSIVE_TOOLS = /* @__PURE__ */ new Set([
|
|
1887
|
-
"read",
|
|
1888
|
-
"read_image",
|
|
1889
|
-
"glob",
|
|
1890
|
-
"grep",
|
|
1891
|
-
"web_search",
|
|
1892
|
-
"ssh_list",
|
|
1893
|
-
"job_list",
|
|
1894
|
-
"job_output",
|
|
1895
|
-
"list_agents",
|
|
1896
|
-
"get_goal",
|
|
1897
|
-
"skill",
|
|
1898
|
-
"mcp__codegraph__codegraph_explore"
|
|
1899
|
-
]);
|
|
1900
|
-
const VERIFIER_TOOLS = /* @__PURE__ */ new Set([
|
|
1901
|
-
"verifier_compare",
|
|
1902
|
-
"verifier_select",
|
|
1903
|
-
"verifier_track",
|
|
1904
|
-
"verifier_current_session"
|
|
1905
|
-
]);
|
|
1906
|
-
const CONSEQUENTIAL_TOOLS = /* @__PURE__ */ new Set([
|
|
1907
|
-
"edit",
|
|
1908
|
-
"write",
|
|
1909
|
-
"pwsh",
|
|
1910
|
-
"bash",
|
|
1911
|
-
"run_code",
|
|
1912
|
-
"codex_image_generate",
|
|
1913
|
-
"ssh_exec",
|
|
1914
|
-
"ssh_upload",
|
|
1915
|
-
"ssh_download",
|
|
1916
|
-
"ssh_tunnel",
|
|
1917
|
-
"ssh_cluster",
|
|
1918
|
-
"job_kill",
|
|
1919
|
-
"workbench_session_delete",
|
|
1920
|
-
"create_goal",
|
|
1921
|
-
"update_goal"
|
|
1922
|
-
]);
|
|
1923
|
-
/**
|
|
1924
|
-
* Whether an agent session is a delegated child rather than the operator's own
|
|
1925
|
-
* topic. Child sessions are seeded with a real user message, so they look like
|
|
1926
|
-
* a fresh task to {@link analyzeAutoTask}; gate them only when asked.
|
|
1927
|
-
* @param agent - Agent (or any object exposing its session).
|
|
1928
|
-
* @returns True for subagent and forked-child sessions.
|
|
1929
|
-
*/
|
|
1930
|
-
function isSubagentSession(agent) {
|
|
1931
|
-
const header = (agent?.session)?.header;
|
|
1932
|
-
return header?.origin === "subagent" || header?.parentSession !== void 0;
|
|
1933
|
-
}
|
|
1934
|
-
const VERIFIER_SESSION_TOOL = "verifier_current_session";
|
|
1935
|
-
/** Concatenate the text of a rendered content-block list (tool result or dispatch payload). */
|
|
1936
|
-
function blockText(value) {
|
|
1937
|
-
if (typeof value === "string") return value;
|
|
1938
|
-
if (!Array.isArray(value)) return "";
|
|
1939
|
-
const parts = [];
|
|
1940
|
-
for (const block of value) if (block?.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
1941
|
-
else if (block?.type === "tool-result") parts.push(blockText(block.content));
|
|
1942
|
-
return parts.join("\n");
|
|
1943
|
-
}
|
|
1944
|
-
/**
|
|
1945
|
-
* Verdict carried by a completed `verifier_current_session` result.
|
|
1946
|
-
*
|
|
1947
|
-
* The tool renders its result as JSON, so the verdict is recovered from the emitted
|
|
1948
|
-
* text. An unreadable payload returns undefined and the manual review simply does
|
|
1949
|
-
* not count: a review that cannot be parsed must never be treated as a pass.
|
|
1950
|
-
* @param value - Result content blocks (or a raw string).
|
|
1951
|
-
* @returns The parsed verdict fields, or undefined.
|
|
1952
|
-
*/
|
|
1953
|
-
function parseSessionVerdict(value) {
|
|
1954
|
-
const text = blockText(value);
|
|
1955
|
-
const start = text.indexOf("{");
|
|
1956
|
-
const end = text.lastIndexOf("}");
|
|
1957
|
-
if (start < 0 || end <= start) return void 0;
|
|
1958
|
-
try {
|
|
1959
|
-
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
1960
|
-
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
1961
|
-
const row = parsed;
|
|
1962
|
-
const criteria = [];
|
|
1963
|
-
for (const entry of Array.isArray(row.criteria) ? row.criteria : []) {
|
|
1964
|
-
const item = typeof entry === "object" && entry !== null ? entry : {};
|
|
1965
|
-
if (typeof item.id !== "string" || typeof item.scoreA !== "number" || !Number.isFinite(item.scoreA)) continue;
|
|
1966
|
-
criteria.push({
|
|
1967
|
-
id: item.id,
|
|
1968
|
-
...typeof item.name === "string" ? { name: item.name } : {},
|
|
1969
|
-
score: item.scoreA
|
|
1970
|
-
});
|
|
1971
|
-
}
|
|
1972
|
-
return {
|
|
1973
|
-
winner: row.winner,
|
|
1974
|
-
score: row.score,
|
|
1975
|
-
criteria
|
|
1976
|
-
};
|
|
1977
|
-
} catch {
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
function isConsequential(name) {
|
|
1982
|
-
if (CONSEQUENTIAL_TOOLS.has(name)) return true;
|
|
1983
|
-
if (PASSIVE_TOOLS.has(name) || VERIFIER_TOOLS.has(name)) return false;
|
|
1984
|
-
return /(?:edit|write|patch|apply|deploy|upload|delete|remove|kill|exec|shell|command|migration|database|tunnel|cluster)/iu.test(name);
|
|
1985
|
-
}
|
|
1986
|
-
/** Event names that carry one settled PTC/code dispatch, across the hosts the plugin supports. */
|
|
1987
|
-
const CODE_DISPATCH_TYPES = /* @__PURE__ */ new Set(["tool/code-dispatch", "tool/ptc-dispatch"]);
|
|
1988
|
-
function isSuccessfulCodeDispatch(data) {
|
|
1989
|
-
if (data.isError === true) return false;
|
|
1990
|
-
if (Array.isArray(data.content) && data.content.some((b) => b.isError === true)) return false;
|
|
1991
|
-
return true;
|
|
1992
|
-
}
|
|
1993
|
-
function analyzeAutoTask(events, policy) {
|
|
1994
|
-
const taskStartSeq = latestDirectUserSeq(events);
|
|
1995
|
-
if (taskStartSeq === void 0) return {
|
|
1996
|
-
taskStartSeq: 0,
|
|
1997
|
-
toolCalls: 0,
|
|
1998
|
-
completedToolResults: 0,
|
|
1999
|
-
consequentialToolCalls: 0,
|
|
2000
|
-
hasManualSessionVerification: false,
|
|
2001
|
-
manualVerificationAccepted: false,
|
|
2002
|
-
eligible: false,
|
|
2003
|
-
reason: "no-direct-user-task"
|
|
2004
|
-
};
|
|
2005
|
-
const relevant = events.filter((event) => event.seq >= taskStartSeq);
|
|
2006
|
-
const calls = relevant.filter((event) => event.type === "tool/call");
|
|
2007
|
-
const results = /* @__PURE__ */ new Map();
|
|
2008
|
-
for (const event of relevant) {
|
|
2009
|
-
if (event.type !== "tool/result" || event.data.error !== void 0) continue;
|
|
2010
|
-
if (!event.data.message.content.every((block) => block.isError !== true)) continue;
|
|
2011
|
-
results.set(String(event.data.message.source.callId), event);
|
|
2012
|
-
}
|
|
2013
|
-
const pairedCalls = calls.filter((event) => results.has(String(event.data.callId)));
|
|
2014
|
-
const dispatches = relevant.filter((event) => CODE_DISPATCH_TYPES.has(event.type)).map((event) => ({
|
|
2015
|
-
seq: event.seq,
|
|
2016
|
-
data: event.data
|
|
2017
|
-
}));
|
|
2018
|
-
const successfulDispatches = dispatches.filter((entry) => isSuccessfulCodeDispatch(entry.data));
|
|
2019
|
-
const toolCalls = calls.filter((event) => !VERIFIER_TOOLS.has(event.data.name)).length + dispatches.filter((entry) => !VERIFIER_TOOLS.has(entry.data.name)).length;
|
|
2020
|
-
const completedToolResults = pairedCalls.length + successfulDispatches.length;
|
|
2021
|
-
const consequentialToolCalls = pairedCalls.filter((event) => isConsequential(event.data.name)).length + successfulDispatches.filter((entry) => isConsequential(entry.data.name)).length;
|
|
2022
|
-
const stale = (seq) => pairedCalls.some((event) => event.seq > seq && isConsequential(event.data.name)) || successfulDispatches.some((entry) => entry.seq > seq && isConsequential(entry.data.name));
|
|
2023
|
-
const manualVerdicts = [];
|
|
2024
|
-
for (const event of pairedCalls) {
|
|
2025
|
-
if (event.data.name !== VERIFIER_SESSION_TOOL) continue;
|
|
2026
|
-
const result = results.get(String(event.data.callId));
|
|
2027
|
-
if (result === void 0) continue;
|
|
2028
|
-
const verdict = parseSessionVerdict(result.data.message.content);
|
|
2029
|
-
manualVerdicts.push({
|
|
2030
|
-
seq: result.seq,
|
|
2031
|
-
winner: verdict?.winner,
|
|
2032
|
-
score: verdict?.score,
|
|
2033
|
-
criteria: verdict?.criteria
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
|
-
for (const entry of successfulDispatches) {
|
|
2037
|
-
if (entry.data.name !== VERIFIER_SESSION_TOOL) continue;
|
|
2038
|
-
const verdict = parseSessionVerdict(entry.data.content);
|
|
2039
|
-
manualVerdicts.push({
|
|
2040
|
-
seq: entry.seq,
|
|
2041
|
-
winner: verdict?.winner,
|
|
2042
|
-
score: verdict?.score,
|
|
2043
|
-
criteria: verdict?.criteria
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
const hasManualSessionVerification = manualVerdicts.length > 0;
|
|
2047
|
-
const manualVerificationAccepted = manualVerdicts.some((verdict) => verdict.winner === "A" && typeof verdict.score === "number" && sessionAccepted({
|
|
2048
|
-
score: verdict.score,
|
|
2049
|
-
winner: "A",
|
|
2050
|
-
criteria: verdict.criteria
|
|
2051
|
-
}, policy.threshold) && !stale(verdict.seq));
|
|
2052
|
-
if (policy.mode === "manual") return {
|
|
2053
|
-
taskStartSeq,
|
|
2054
|
-
toolCalls,
|
|
2055
|
-
completedToolResults,
|
|
2056
|
-
consequentialToolCalls,
|
|
2057
|
-
hasManualSessionVerification,
|
|
2058
|
-
manualVerificationAccepted,
|
|
2059
|
-
eligible: false,
|
|
2060
|
-
reason: "manual-mode"
|
|
2061
|
-
};
|
|
2062
|
-
if (manualVerificationAccepted) return {
|
|
2063
|
-
taskStartSeq,
|
|
2064
|
-
toolCalls,
|
|
2065
|
-
completedToolResults,
|
|
2066
|
-
consequentialToolCalls,
|
|
2067
|
-
hasManualSessionVerification,
|
|
2068
|
-
manualVerificationAccepted,
|
|
2069
|
-
eligible: false,
|
|
2070
|
-
reason: "already-verified"
|
|
2071
|
-
};
|
|
2072
|
-
if (consequentialToolCalls === 0) return {
|
|
2073
|
-
taskStartSeq,
|
|
2074
|
-
toolCalls,
|
|
2075
|
-
completedToolResults,
|
|
2076
|
-
consequentialToolCalls,
|
|
2077
|
-
hasManualSessionVerification,
|
|
2078
|
-
manualVerificationAccepted,
|
|
2079
|
-
eligible: false,
|
|
2080
|
-
reason: "no-consequential-work"
|
|
2081
|
-
};
|
|
2082
|
-
if (completedToolResults === 0) return {
|
|
2083
|
-
taskStartSeq,
|
|
2084
|
-
toolCalls,
|
|
2085
|
-
completedToolResults,
|
|
2086
|
-
consequentialToolCalls,
|
|
2087
|
-
hasManualSessionVerification,
|
|
2088
|
-
manualVerificationAccepted,
|
|
2089
|
-
eligible: false,
|
|
2090
|
-
reason: "no-completed-evidence"
|
|
2091
|
-
};
|
|
2092
|
-
if (policy.mode === "smart" && toolCalls < policy.minToolCalls) return {
|
|
2093
|
-
taskStartSeq,
|
|
2094
|
-
toolCalls,
|
|
2095
|
-
completedToolResults,
|
|
2096
|
-
consequentialToolCalls,
|
|
2097
|
-
hasManualSessionVerification,
|
|
2098
|
-
manualVerificationAccepted,
|
|
2099
|
-
eligible: false,
|
|
2100
|
-
reason: "insufficient-tool-evidence"
|
|
2101
|
-
};
|
|
2102
|
-
return {
|
|
2103
|
-
taskStartSeq,
|
|
2104
|
-
toolCalls,
|
|
2105
|
-
completedToolResults,
|
|
2106
|
-
consequentialToolCalls,
|
|
2107
|
-
hasManualSessionVerification,
|
|
2108
|
-
manualVerificationAccepted,
|
|
2109
|
-
eligible: true,
|
|
2110
|
-
reason: policy.mode + "-eligible"
|
|
2111
|
-
};
|
|
2112
|
-
}
|
|
2113
|
-
/**
|
|
2114
|
-
* Criteria that do not clear the acceptance threshold.
|
|
2115
|
-
*
|
|
2116
|
-
* The acceptance score is the MEAN over criteria, so a session with one requirement at
|
|
2117
|
-
* zero and the others perfect averaged ~0.67 and cleared the 0.65 default: a single
|
|
2118
|
-
* failed requirement was arithmetically invisible. The gate therefore also requires
|
|
2119
|
-
* every criterion to clear the threshold on its own.
|
|
2120
|
-
* @param criteria - per-criterion A-side scores, when the judge reported them.
|
|
2121
|
-
* @param threshold - acceptance threshold.
|
|
2122
|
-
* @returns The failing criteria, in report order.
|
|
2123
|
-
*/
|
|
2124
|
-
function failedAcceptanceCriteria(criteria, threshold) {
|
|
2125
|
-
return (criteria ?? []).filter((criterion) => !(criterion.score >= threshold));
|
|
2126
|
-
}
|
|
2127
|
-
/**
|
|
2128
|
-
* Whether a session acceptance clears the gate.
|
|
2129
|
-
*
|
|
2130
|
-
* The empty-work baseline is a fixed sentence that always scores 0, so the comparison
|
|
2131
|
-
* itself is decorative; what actually decides is the session's own score, the winner
|
|
2132
|
-
* against that baseline, and (since the mean could hide a failed requirement) every
|
|
2133
|
-
* criterion clearing the threshold.
|
|
2134
|
-
* @param evidence - session acceptance result (score, winner and per-criterion scores).
|
|
2135
|
-
* @param threshold - acceptance threshold.
|
|
2136
|
-
* @returns True only when the session may conclude.
|
|
2137
|
-
*/
|
|
2138
|
-
function sessionAccepted(evidence, threshold) {
|
|
2139
|
-
if (evidence.winner !== "A" || !(evidence.score >= threshold)) return false;
|
|
2140
|
-
return failedAcceptanceCriteria(evidence.criteria, threshold).length === 0;
|
|
2141
|
-
}
|
|
2142
|
-
function automaticFeedback(score, baselineScore, winner, threshold, failedCriteria = []) {
|
|
2143
|
-
const percent = (value) => (value * 100).toFixed(1) + "%";
|
|
2144
|
-
return [
|
|
2145
|
-
"[Automatic verifier gate]",
|
|
2146
|
-
`The independent verifier did not clear this task for completion: evidence score ${percent(score)}, baseline ${percent(baselineScore)}, verdict ${winner}, required ${percent(threshold)}.`,
|
|
2147
|
-
...failedCriteria.length > 0 ? ["Criteria below the threshold: " + failedCriteria.map((criterion) => (criterion.name ?? criterion.id) + " " + percent(criterion.score)).join("; ") + "."] : [],
|
|
2148
|
-
"Re-open the task requirements, inspect the actual tool outputs for unresolved errors or missing proof, make any necessary corrections, and run a directly relevant verification command before concluding. Do not merely restate that the task is complete."
|
|
2149
|
-
].join("\n");
|
|
2150
|
-
}
|
|
2151
|
-
//#endregion
|
|
2152
1132
|
//#region src/plan-gate.ts
|
|
2153
1133
|
/**
|
|
2154
1134
|
* Read the plan markdown out of one `exit_plan_mode` argument object.
|
|
@@ -2283,6 +1263,7 @@ const VERIFIER_TOOL_NAMES = [
|
|
|
2283
1263
|
"verifier_compare",
|
|
2284
1264
|
"verifier_select",
|
|
2285
1265
|
"verifier_track",
|
|
1266
|
+
"verifier_best_of_n",
|
|
2286
1267
|
"verifier_current_session"
|
|
2287
1268
|
];
|
|
2288
1269
|
/** Upper bound on the stored checkpoint progression; one explicit call can legitimately carry 32 steps. */
|
|
@@ -2312,6 +1293,10 @@ function summarizeVerdict(toolName, value, phase, thresholds) {
|
|
|
2312
1293
|
outcome: "classified"
|
|
2313
1294
|
};
|
|
2314
1295
|
if (toolName === "verifier_select") {
|
|
1296
|
+
if (row.identical === true) return {
|
|
1297
|
+
phase,
|
|
1298
|
+
outcome: "identical-candidates"
|
|
1299
|
+
};
|
|
2315
1300
|
const index = numberAt("index");
|
|
2316
1301
|
const best = index === void 0 ? void 0 : scores[index];
|
|
2317
1302
|
return {
|
|
@@ -2337,19 +1322,34 @@ function summarizeVerdict(toolName, value, phase, thresholds) {
|
|
|
2337
1322
|
const score = winner === "B" ? scoreB : scoreA;
|
|
2338
1323
|
return {
|
|
2339
1324
|
phase,
|
|
2340
|
-
outcome: winner === "tie" ? "tie" : "compared",
|
|
1325
|
+
outcome: row.identical === true ? "identical" : winner === "tie" ? "tie" : "compared",
|
|
2341
1326
|
...score !== void 0 ? { score } : {},
|
|
2342
1327
|
...scoreB !== void 0 ? { scoreB } : {},
|
|
2343
1328
|
...winner !== void 0 ? { winner } : {}
|
|
2344
1329
|
};
|
|
2345
1330
|
}
|
|
1331
|
+
return acceptanceVerdict(row, phase, thresholds);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Verdict of a run that was measured against the fixed empty-work baseline.
|
|
1335
|
+
*
|
|
1336
|
+
* Shared by the automatic session acceptance and by `verifier_best_of_n`: both answer
|
|
1337
|
+
* "is this good enough to conclude", and both are decided by the same three conditions.
|
|
1338
|
+
* @param row - the rendered result.
|
|
1339
|
+
* @param phase - which stage produced it.
|
|
1340
|
+
* @param thresholds - resolved acceptance thresholds.
|
|
1341
|
+
* @returns A gate-shaped verdict summary.
|
|
1342
|
+
*/
|
|
1343
|
+
function acceptanceVerdict(row, phase, thresholds) {
|
|
1344
|
+
const numberAt = (key) => typeof row[key] === "number" && Number.isFinite(row[key]) ? row[key] : void 0;
|
|
1345
|
+
const winner = row.winner === "A" || row.winner === "B" || row.winner === "tie" ? row.winner : void 0;
|
|
2346
1346
|
const score = numberAt("score") ?? numberAt("scoreA");
|
|
2347
1347
|
const baselineScore = numberAt("baselineScore");
|
|
2348
1348
|
const threshold = thresholds.autoVerifyThreshold;
|
|
2349
|
-
const criteria = Array.isArray(row.criteria) ? row.criteria.map((entry) => typeof entry === "object" && entry !== null ? entry : {}).
|
|
1349
|
+
const criteria = Array.isArray(row.criteria) ? row.criteria.map((entry) => typeof entry === "object" && entry !== null ? entry : {}).map((entry) => ({
|
|
2350
1350
|
id: entry.id,
|
|
2351
|
-
score: entry.scoreA
|
|
2352
|
-
})) : [];
|
|
1351
|
+
score: typeof entry.score === "number" ? entry.score : entry.scoreA
|
|
1352
|
+
})).filter((entry) => typeof entry.id === "string" && typeof entry.score === "number" && Number.isFinite(entry.score)).slice(0, MAX_VERDICT_CRITERIA) : [];
|
|
2353
1353
|
const belowThreshold = criteria.filter((entry) => !(entry.score >= threshold));
|
|
2354
1354
|
return {
|
|
2355
1355
|
phase,
|
|
@@ -2411,6 +1411,7 @@ function blankTotals() {
|
|
|
2411
1411
|
cacheHits: 0,
|
|
2412
1412
|
cacheMisses: 0,
|
|
2413
1413
|
cacheHitRate: 0,
|
|
1414
|
+
prefixCacheHitRate: 0,
|
|
2414
1415
|
estimatedCostUsd: 0,
|
|
2415
1416
|
topLogprobScores: 0,
|
|
2416
1417
|
explicitTagScores: 0
|
|
@@ -2439,6 +1440,7 @@ function finishTotals(target) {
|
|
|
2439
1440
|
target.averageDurationMs = target.invocations > 0 ? target.averageDurationMs / target.invocations : 0;
|
|
2440
1441
|
target.successRate = ratio(target.successes, target.invocations);
|
|
2441
1442
|
target.cacheHitRate = ratio(target.cacheHits, target.cacheHits + target.cacheMisses);
|
|
1443
|
+
target.prefixCacheHitRate = ratio(target.cachedInputTokens, target.inputTokens + target.cachedInputTokens);
|
|
2442
1444
|
return target;
|
|
2443
1445
|
}
|
|
2444
1446
|
function localDate(time, timezoneOffsetMinutes) {
|
|
@@ -2580,6 +1582,7 @@ function mergeStatisticsOverviews(overviews, query) {
|
|
|
2580
1582
|
totals.averageDurationMs = ratio(weightedDuration, totals.invocations);
|
|
2581
1583
|
totals.successRate = ratio(totals.successes, totals.invocations);
|
|
2582
1584
|
totals.cacheHitRate = ratio(totals.cacheHits, totals.cacheHits + totals.cacheMisses);
|
|
1585
|
+
totals.prefixCacheHitRate = ratio(totals.cachedInputTokens, totals.inputTokens + totals.cachedInputTokens);
|
|
2583
1586
|
const limit = Math.min(200, Math.max(1, Math.trunc(query.recentLimit ?? 40)));
|
|
2584
1587
|
return {
|
|
2585
1588
|
generatedAt: Date.now(),
|
|
@@ -2783,23 +1786,195 @@ function escapes(root, target) {
|
|
|
2783
1786
|
return child === ".." || child.startsWith("..\\") || child.startsWith("../") || isAbsolute(child);
|
|
2784
1787
|
}
|
|
2785
1788
|
/**
|
|
2786
|
-
* Resolve verifier sidecars beneath the persistence backend's per-session
|
|
2787
|
-
* directory. The JSONL backend deliberately owns this directory for
|
|
2788
|
-
* session-local artifacts, so permanent session deletion removes these files
|
|
2789
|
-
* together with the conversation log.
|
|
1789
|
+
* Resolve verifier sidecars beneath the persistence backend's per-session
|
|
1790
|
+
* directory. The JSONL backend deliberately owns this directory for
|
|
1791
|
+
* session-local artifacts, so permanent session deletion removes these files
|
|
1792
|
+
* together with the conversation log.
|
|
1793
|
+
*/
|
|
1794
|
+
function resolveTopicDataDir(locator, header, cacheDir) {
|
|
1795
|
+
if (isAbsolute(cacheDir)) throw new Error("llm-verifier: cacheDir must be relative so verifier data stays inside its topic directory");
|
|
1796
|
+
let topicDir;
|
|
1797
|
+
if (typeof locator?.locate === "function") {
|
|
1798
|
+
const location = locator.locate(header);
|
|
1799
|
+
if (location && typeof location.path === "string") topicDir = dirname(location.path);
|
|
1800
|
+
}
|
|
1801
|
+
if (!topicDir && typeof locator?.root === "string" && header?.id) topicDir = resolve(locator.root, String(header.id));
|
|
1802
|
+
if (topicDir === void 0) throw new Error("llm-verifier: the active session persistence backend does not expose a per-session artifact directory");
|
|
1803
|
+
const target = resolve(topicDir, cacheDir);
|
|
1804
|
+
if (escapes(topicDir, target)) throw new Error("llm-verifier: cacheDir must stay inside the topic directory");
|
|
1805
|
+
return target;
|
|
1806
|
+
}
|
|
1807
|
+
//#endregion
|
|
1808
|
+
//#region src/decisions.ts
|
|
1809
|
+
/**
|
|
1810
|
+
* Upper bound on captured calls per invocation.
|
|
1811
|
+
*
|
|
1812
|
+
* A session acceptance makes six; an explicit best-of-N makes about 27 (N drafts, the
|
|
1813
|
+
* tournament, and the winner-vs-baseline comparison). Keeping the old 12 would have dropped
|
|
1814
|
+
* the drafts from the snapshot entirely — judge labels sort before `draft N` — so the one
|
|
1815
|
+
* record that explains "which draft won and why" could not show the drafts at all. The
|
|
1816
|
+
* per-record character budget is unchanged and shared equally, so the extra calls shrink each
|
|
1817
|
+
* window instead of growing the file. 32 is the ceiling the equal-share floor allows:
|
|
1818
|
+
* 32 x (512 prompt + 256 output) still fits in {@link MAX_RECORD_CHARS}.
|
|
1819
|
+
*/
|
|
1820
|
+
const MAX_CALLS = 32;
|
|
1821
|
+
const MAX_PROMPT_CHARS = 8e3;
|
|
1822
|
+
const MAX_OUTPUT_CHARS = 4e3;
|
|
1823
|
+
/** Upper bound on one record's captured text, so a wide select cannot fill the file. */
|
|
1824
|
+
const MAX_RECORD_CHARS = 3e4;
|
|
1825
|
+
/** Smallest window worth storing; at MAX_CALLS the equal share stays above this floor. */
|
|
1826
|
+
const MIN_PROMPT_CHARS = 512;
|
|
1827
|
+
const MIN_OUTPUT_CHARS = 256;
|
|
1828
|
+
function resolveDecisionsFile(cacheFile) {
|
|
1829
|
+
return join(dirname(cacheFile), "decisions-v1.json");
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Redact and bound captured calls.
|
|
1833
|
+
*
|
|
1834
|
+
* Runs the same sanitizer the prompts do, so a snapshot can never persist a secret the
|
|
1835
|
+
* judge itself never saw, and drops calls past the per-record budget instead of writing
|
|
1836
|
+
* an unbounded file.
|
|
1837
|
+
* @param calls - captured calls, oldest first.
|
|
1838
|
+
* @returns The bounded calls; empty when nothing was captured.
|
|
1839
|
+
*/
|
|
1840
|
+
function boundCaptureText(text, maxChars) {
|
|
1841
|
+
if (!Number.isSafeInteger(maxChars) || maxChars < 1) throw new Error("llm-verifier: decision capture cap must be a positive integer");
|
|
1842
|
+
const clean = sanitizeVerifierText(text, Math.max(1, text.length));
|
|
1843
|
+
if (clean.length <= maxChars || maxChars < 256) return sanitizeVerifierText(text, maxChars);
|
|
1844
|
+
const head = Math.floor(maxChars / 2);
|
|
1845
|
+
const tail = maxChars - head;
|
|
1846
|
+
const marker = "\n[… " + Math.max(0, clean.length - head - tail) + " characters omitted …]\n";
|
|
1847
|
+
const keepTail = Math.max(0, tail - marker.length);
|
|
1848
|
+
return clean.slice(0, head) + marker + (keepTail === 0 ? "" : clean.slice(-keepTail));
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Redact and bound captured calls.
|
|
1852
|
+
*
|
|
1853
|
+
* Runs the same sanitizer the prompts do, so a snapshot can never persist a secret the
|
|
1854
|
+
* judge itself never saw. The record is bounded twice: at {@link MAX_CALLS} calls, and at
|
|
1855
|
+
* {@link MAX_RECORD_CHARS} characters shared EQUALLY between the calls it keeps — a six-call
|
|
1856
|
+
* session acceptance must not shrink to its first three calls, and which three survived must not
|
|
1857
|
+
* depend on completion order. Text uses {@link boundCaptureText}, which keeps both ends: a
|
|
1858
|
+
* session-acceptance prompt runs past 100k characters, and head-only truncation kept the
|
|
1859
|
+
* instructions while dropping the trajectory tail the judge actually graded.
|
|
1860
|
+
* @param calls - captured calls; callers sort them by label so the bounded set is deterministic.
|
|
1861
|
+
* @returns The bounded calls; empty when nothing was captured.
|
|
1862
|
+
*/
|
|
1863
|
+
function boundDecisionCalls(calls) {
|
|
1864
|
+
if (calls.length === 0) return [];
|
|
1865
|
+
const selected = calls.length <= MAX_CALLS ? [...calls] : Array.from({ length: MAX_CALLS }, (_, index) => calls[Math.round(index * (calls.length - 1) / (MAX_CALLS - 1))]);
|
|
1866
|
+
const overhead = selected.reduce((sum, call) => sum + Math.min(call.label.length, 200) + Math.min(call.channel.length, 40), 0);
|
|
1867
|
+
const perCall = Math.max(768, Math.floor((MAX_RECORD_CHARS - overhead) / selected.length));
|
|
1868
|
+
const bounded = [];
|
|
1869
|
+
let used = 0;
|
|
1870
|
+
for (const call of selected) {
|
|
1871
|
+
const promptChars = Math.min(MAX_PROMPT_CHARS, Math.max(MIN_PROMPT_CHARS, Math.min(Math.floor(perCall * .8), perCall - MIN_OUTPUT_CHARS)));
|
|
1872
|
+
const outputChars = Math.min(MAX_OUTPUT_CHARS, Math.max(MIN_OUTPUT_CHARS, perCall - promptChars));
|
|
1873
|
+
const value = {
|
|
1874
|
+
label: call.label.slice(0, 200),
|
|
1875
|
+
channel: call.channel.slice(0, 40),
|
|
1876
|
+
prompt: boundCaptureText(call.prompt, promptChars),
|
|
1877
|
+
output: boundCaptureText(call.output, outputChars),
|
|
1878
|
+
...typeof call.score === "number" && Number.isFinite(call.score) ? { score: call.score } : {}
|
|
1879
|
+
};
|
|
1880
|
+
const cost = value.label.length + value.channel.length + value.prompt.length + value.output.length;
|
|
1881
|
+
if (bounded.length > 0 && used + cost > MAX_RECORD_CHARS) break;
|
|
1882
|
+
bounded.push(value);
|
|
1883
|
+
used += cost;
|
|
1884
|
+
}
|
|
1885
|
+
return bounded;
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Per-topic ring buffer of decision snapshots.
|
|
1889
|
+
*
|
|
1890
|
+
* Mirrors {@link import('./statistics.ts').StatisticsStore}: one JSON file beside the
|
|
1891
|
+
* statistics of the same topic, atomic replace on write, a fixed number of records, so
|
|
1892
|
+
* the observable cost of "keep the evidence that explains a verdict" is bounded.
|
|
1893
|
+
*/
|
|
1894
|
+
var DecisionStore = class {
|
|
1895
|
+
file;
|
|
1896
|
+
maxEntries;
|
|
1897
|
+
loaded = false;
|
|
1898
|
+
hydrating;
|
|
1899
|
+
records = [];
|
|
1900
|
+
writing = Promise.resolve();
|
|
1901
|
+
constructor(file, maxEntries = 40) {
|
|
1902
|
+
this.file = file;
|
|
1903
|
+
this.maxEntries = maxEntries;
|
|
1904
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries <= 0) throw new Error("llm-verifier: decision maxEntries must be a positive integer");
|
|
1905
|
+
}
|
|
1906
|
+
async record(input) {
|
|
1907
|
+
const calls = boundDecisionCalls(input.calls);
|
|
1908
|
+
if (calls.length === 0) return void 0;
|
|
1909
|
+
const record = {
|
|
1910
|
+
id: randomUUID(),
|
|
1911
|
+
toolName: input.toolName,
|
|
1912
|
+
phase: input.phase,
|
|
1913
|
+
startedAt: input.startedAt,
|
|
1914
|
+
provider: input.provider,
|
|
1915
|
+
model: input.model,
|
|
1916
|
+
calls
|
|
1917
|
+
};
|
|
1918
|
+
const operation = async () => {
|
|
1919
|
+
await this.load();
|
|
1920
|
+
this.records.push(record);
|
|
1921
|
+
if (this.records.length > this.maxEntries) this.records.splice(0, this.records.length - this.maxEntries);
|
|
1922
|
+
await this.persist();
|
|
1923
|
+
};
|
|
1924
|
+
this.writing = this.writing.then(operation, operation);
|
|
1925
|
+
await this.writing;
|
|
1926
|
+
return record;
|
|
1927
|
+
}
|
|
1928
|
+
/** One snapshot by invocation id, or undefined when it was pruned or never captured. */
|
|
1929
|
+
async find(id) {
|
|
1930
|
+
await this.writing.catch(() => {});
|
|
1931
|
+
await this.load();
|
|
1932
|
+
return this.records.find((record) => record.id === id);
|
|
1933
|
+
}
|
|
1934
|
+
async load() {
|
|
1935
|
+
if (this.loaded) return;
|
|
1936
|
+
this.hydrating ??= (async () => {
|
|
1937
|
+
try {
|
|
1938
|
+
const document = JSON.parse(await readFile(this.file, "utf8"));
|
|
1939
|
+
if (document.version === 1 && Array.isArray(document.records)) this.records = document.records.filter(isDecisionRecord).slice(-this.maxEntries);
|
|
1940
|
+
this.loaded = true;
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
if (error.code === "ENOENT") {
|
|
1943
|
+
this.loaded = true;
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
throw error;
|
|
1947
|
+
} finally {
|
|
1948
|
+
this.hydrating = void 0;
|
|
1949
|
+
}
|
|
1950
|
+
})();
|
|
1951
|
+
await this.hydrating;
|
|
1952
|
+
}
|
|
1953
|
+
async persist() {
|
|
1954
|
+
const snapshot = {
|
|
1955
|
+
version: 1,
|
|
1956
|
+
records: this.records
|
|
1957
|
+
};
|
|
1958
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
1959
|
+
const temporary = this.file + ".tmp-" + process.pid + "-" + randomUUID();
|
|
1960
|
+
await writeFile(temporary, JSON.stringify(snapshot), "utf8");
|
|
1961
|
+
try {
|
|
1962
|
+
await rename(temporary, this.file);
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
await unlink(temporary).catch(() => {});
|
|
1965
|
+
throw error;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
/**
|
|
1970
|
+
* Loose validation, so a record written by a newer/older plugin still loads.
|
|
1971
|
+
* @param value - parsed JSON entry.
|
|
1972
|
+
* @returns True when the entry carries the fields the dashboard needs.
|
|
2790
1973
|
*/
|
|
2791
|
-
function
|
|
2792
|
-
if (
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
const location = locator.locate(header);
|
|
2796
|
-
if (location && typeof location.path === "string") topicDir = dirname(location.path);
|
|
2797
|
-
}
|
|
2798
|
-
if (!topicDir && typeof locator?.root === "string" && header?.id) topicDir = resolve(locator.root, String(header.id));
|
|
2799
|
-
if (topicDir === void 0) throw new Error("llm-verifier: the active session persistence backend does not expose a per-session artifact directory");
|
|
2800
|
-
const target = resolve(topicDir, cacheDir);
|
|
2801
|
-
if (escapes(topicDir, target)) throw new Error("llm-verifier: cacheDir must stay inside the topic directory");
|
|
2802
|
-
return target;
|
|
1974
|
+
function isDecisionRecord(value) {
|
|
1975
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1976
|
+
const row = value;
|
|
1977
|
+
return typeof row.id === "string" && typeof row.toolName === "string" && typeof row.startedAt === "number" && Array.isArray(row.calls) && row.calls.every((call) => typeof call === "object" && call !== null && typeof call.prompt === "string" && typeof call.output === "string");
|
|
2803
1978
|
}
|
|
2804
1979
|
//#endregion
|
|
2805
1980
|
//#region src/index.ts
|
|
@@ -2970,6 +2145,14 @@ const EXPLICIT_MAX_TOTAL_CHARS = 24e4;
|
|
|
2970
2145
|
const EXPLICIT_MIN_TOTAL_CHARS = 12e4;
|
|
2971
2146
|
const MAX_EXPLICIT_CANDIDATES = 16;
|
|
2972
2147
|
const MAX_EXPLICIT_PLANNED_CALLS = 500;
|
|
2148
|
+
/**
|
|
2149
|
+
* Best-of-N bounds. The ceiling is 4 because cost grows ~linearly in N while the marginal
|
|
2150
|
+
* value of the 5th draft does not, and the tool is meant for a final deliverable rather than
|
|
2151
|
+
* routine work. The floor is 2 because "choose the best of one" is not a choice.
|
|
2152
|
+
*/
|
|
2153
|
+
const MIN_BEST_OF_N = 2;
|
|
2154
|
+
const MAX_BEST_OF_N = 4;
|
|
2155
|
+
const DEFAULT_BEST_OF_N = 3;
|
|
2973
2156
|
function explicitItemChars(selected) {
|
|
2974
2157
|
return Math.max(selected.autoRouteMaxItemChars, EXPLICIT_MIN_ITEM_CHARS);
|
|
2975
2158
|
}
|
|
@@ -2983,6 +2166,19 @@ function explicitCandidateLimit(selected) {
|
|
|
2983
2166
|
function plannedComparisons(count) {
|
|
2984
2167
|
return count <= 2 ? 1 : count + Math.max(0, (count - 2) * 2 + 1 - 3);
|
|
2985
2168
|
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Worst-case tournament pairs for a selection of `count` candidates.
|
|
2171
|
+
*
|
|
2172
|
+
* `plannedComparisons` is an ESTIMATE (it assumes the ring already covers three of the pivot-round
|
|
2173
|
+
* pairs), and best-of-N spends N generations first, so its guard has to fail closed on the true
|
|
2174
|
+
* bound instead: ring (N) plus every pivot pair (non-pivots x pivots, plus pivot-vs-pivot), since
|
|
2175
|
+
* de-duplicating against the ring can only ever remove pairs.
|
|
2176
|
+
* @param count - candidate count.
|
|
2177
|
+
* @returns The largest number of pairs the tournament can judge.
|
|
2178
|
+
*/
|
|
2179
|
+
function selectComparisonsUpperBound(count) {
|
|
2180
|
+
return count <= 2 ? 1 : count + (count - 2) * 2 + 1;
|
|
2181
|
+
}
|
|
2986
2182
|
function numberField(value, fallback) {
|
|
2987
2183
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2988
2184
|
}
|
|
@@ -3071,6 +2267,18 @@ function rpcFailure(message) {
|
|
|
3071
2267
|
}
|
|
3072
2268
|
};
|
|
3073
2269
|
}
|
|
2270
|
+
/**
|
|
2271
|
+
* Small but real pair for the settings-page probe.
|
|
2272
|
+
*
|
|
2273
|
+
* The probe asks each judge for an actual A–T verdict instead of a "ping": that is what proves
|
|
2274
|
+
* the response PARSES, which is the failure a reachability check misses. A pair where one side is
|
|
2275
|
+
* obviously better keeps the exercise honest without making the expected score a secret.
|
|
2276
|
+
*/
|
|
2277
|
+
const PROBE_TASK = "Fix the failing parser test and prove it passes.";
|
|
2278
|
+
const PROBE_A = "Ran the test: 1 failed. Patched the separator handling. Ran it again: 1 passed, no other failures.";
|
|
2279
|
+
const PROBE_B = "The test should pass now.";
|
|
2280
|
+
/** Bound the probe: a diagnostics button must not sit on the configured 5-minute timeout plus retries. */
|
|
2281
|
+
const PROBE_TIMEOUT_MS = 3e4;
|
|
3074
2282
|
function apply(ctx, config = {}) {
|
|
3075
2283
|
const services = ctx;
|
|
3076
2284
|
const entry = resolveConfig(config);
|
|
@@ -3092,7 +2300,8 @@ function apply(ctx, config = {}) {
|
|
|
3092
2300
|
cache: new ScoreCache(cacheFile, selected.cacheMaxEntries),
|
|
3093
2301
|
capabilities: new TopLogprobCapabilityCache(resolveCapabilityFile(dataDir)),
|
|
3094
2302
|
flights: new SingleFlight(),
|
|
3095
|
-
statistics: new StatisticsStore(resolveStatisticsFile(cacheFile))
|
|
2303
|
+
statistics: new StatisticsStore(resolveStatisticsFile(cacheFile)),
|
|
2304
|
+
decisions: new DecisionStore(resolveDecisionsFile(cacheFile))
|
|
3096
2305
|
};
|
|
3097
2306
|
topics.set(id, created);
|
|
3098
2307
|
return created;
|
|
@@ -3102,9 +2311,32 @@ function apply(ctx, config = {}) {
|
|
|
3102
2311
|
if (selected === void 0) throw new Error("llm-verifier: verifier tools require an agent-owned topic so their data can follow topic deletion");
|
|
3103
2312
|
return selected;
|
|
3104
2313
|
};
|
|
3105
|
-
const
|
|
2314
|
+
const criteriaResolver = new CriteriaResolver();
|
|
2315
|
+
const configuredCriteria = () => criteriaResolver.resolve(current().criteriaPreset, current().criteriaFile);
|
|
2316
|
+
/**
|
|
2317
|
+
* Every known topic header, newest first.
|
|
2318
|
+
*
|
|
2319
|
+
* The statistics and decision dashboards merge all topics; anything that needs exactly ONE
|
|
2320
|
+
* topic (the judge probe's per-topic capability memory) takes the newest, because that is where
|
|
2321
|
+
* the operator's current work lives. Legacy backends handed back the header directly instead of
|
|
2322
|
+
* wrapping it in \`{ header }`, so both shapes are accepted.
|
|
2323
|
+
* @returns Session headers, newest first; entries without an id are dropped.
|
|
2324
|
+
*/
|
|
2325
|
+
const sessionHeaders = async () => {
|
|
2326
|
+
return (await services.sessionPersistence.list()).map((item) => item && typeof item === "object" && "header" in item ? item.header : item).filter((header) => header !== void 0 && header.id !== void 0).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0));
|
|
2327
|
+
};
|
|
2328
|
+
/**
|
|
2329
|
+
* Build the judge ensemble for one topic.
|
|
2330
|
+
*
|
|
2331
|
+
* Takes a session header rather than an Agent because the judge probe is started from the global
|
|
2332
|
+
* statistics dashboard, where there is no current initiator. The diagnostic still needs a topic
|
|
2333
|
+
* for the per-topic capability memory, so it attaches to the most recent one (see handleProbe).
|
|
2334
|
+
* @param header - session header owning the topic whose sidecars the judges use.
|
|
2335
|
+
* @returns The engine plus the resolved configuration it was built from.
|
|
2336
|
+
*/
|
|
2337
|
+
const engineForHeader = async (header) => {
|
|
3106
2338
|
const selected = current();
|
|
3107
|
-
const topicEntry = topic(
|
|
2339
|
+
const topicEntry = topic(header);
|
|
3108
2340
|
const clients = [];
|
|
3109
2341
|
for (const judge of selected.judges) {
|
|
3110
2342
|
await ctx.llm.resolveCallConfig({
|
|
@@ -3138,6 +2370,7 @@ function apply(ctx, config = {}) {
|
|
|
3138
2370
|
selected
|
|
3139
2371
|
};
|
|
3140
2372
|
};
|
|
2373
|
+
const engine = async (agent) => engineForHeader(agent.session.header);
|
|
3141
2374
|
const images = (values, signal) => loadVerifierImages(values, signal);
|
|
3142
2375
|
const route = (selected) => ({
|
|
3143
2376
|
provider: selected.provider,
|
|
@@ -3156,9 +2389,15 @@ function apply(ctx, config = {}) {
|
|
|
3156
2389
|
const record = async (toolName, agent, operation, phase = "explicit") => {
|
|
3157
2390
|
const startedAt = Date.now();
|
|
3158
2391
|
let selected = current();
|
|
3159
|
-
const
|
|
2392
|
+
const topicEntry = topic(agent.session.header);
|
|
2393
|
+
const statistics = topicEntry.statistics;
|
|
2394
|
+
const capture = current().captureDecisions;
|
|
2395
|
+
const calls = [];
|
|
2396
|
+
const trace = capture ? (call) => {
|
|
2397
|
+
calls.push(call);
|
|
2398
|
+
} : void 0;
|
|
3160
2399
|
try {
|
|
3161
|
-
const completed = await operation();
|
|
2400
|
+
const completed = await operation(trace);
|
|
3162
2401
|
selected = completed.selected;
|
|
3163
2402
|
const value = {
|
|
3164
2403
|
...completed.result,
|
|
@@ -3174,6 +2413,14 @@ function apply(ctx, config = {}) {
|
|
|
3174
2413
|
stats: statsFrom(value),
|
|
3175
2414
|
verdict: verdictFrom(toolName, value, phase)
|
|
3176
2415
|
}).catch(() => {});
|
|
2416
|
+
if (calls.length > 0) await topicEntry.decisions.record({
|
|
2417
|
+
toolName,
|
|
2418
|
+
phase,
|
|
2419
|
+
startedAt,
|
|
2420
|
+
provider: selected.provider,
|
|
2421
|
+
model: selected.model,
|
|
2422
|
+
calls: boundDecisionCalls([...calls].sort((a, b) => a.label < b.label ? -1 : a.label > b.label ? 1 : 0))
|
|
2423
|
+
}).catch(() => {});
|
|
3177
2424
|
return value;
|
|
3178
2425
|
} catch (error) {
|
|
3179
2426
|
const details = errorDetails(error);
|
|
@@ -3228,7 +2475,7 @@ function apply(ctx, config = {}) {
|
|
|
3228
2475
|
}
|
|
3229
2476
|
}).catch(() => {});
|
|
3230
2477
|
};
|
|
3231
|
-
const verifySession = async (agent, options, signal, phase = "explicit") => record("verifier_current_session", agent, async () => {
|
|
2478
|
+
const verifySession = async (agent, options, signal, phase = "explicit", rubricOverride) => record("verifier_current_session", agent, async (trace) => {
|
|
3232
2479
|
const extracted = await extractSession(agent, async (ref) => {
|
|
3233
2480
|
const stored = await services.attachments.readImage(ref, signal);
|
|
3234
2481
|
return {
|
|
@@ -3244,12 +2491,16 @@ function apply(ctx, config = {}) {
|
|
|
3244
2491
|
});
|
|
3245
2492
|
if (!extracted.problem.trim()) throw new Error("llm-verifier: no direct user task found in the selected session range — widen from_seq so the task statement is included");
|
|
3246
2493
|
const { verifier, selected } = await engine(agent);
|
|
2494
|
+
const rubric = rubricOverride ?? await configuredCriteria();
|
|
3247
2495
|
const compared = await verifier.compare({
|
|
3248
2496
|
problem: extracted.problem,
|
|
3249
2497
|
candidateA: extracted.trace,
|
|
3250
|
-
candidateB:
|
|
2498
|
+
candidateB: EMPTY_WORK_BASELINE,
|
|
2499
|
+
criteria: rubric.criteria,
|
|
2500
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
3251
2501
|
repeats: positive(options.repeats, 2, "repeats"),
|
|
3252
|
-
images: extracted.images
|
|
2502
|
+
images: extracted.images,
|
|
2503
|
+
...trace ? { trace } : {}
|
|
3253
2504
|
}, signal);
|
|
3254
2505
|
return {
|
|
3255
2506
|
result: {
|
|
@@ -3274,43 +2525,206 @@ function apply(ctx, config = {}) {
|
|
|
3274
2525
|
selected
|
|
3275
2526
|
};
|
|
3276
2527
|
}, phase);
|
|
3277
|
-
const compareCandidates = async (agent, problem, candidateA, candidateB, repeats, signal, routedImages = [], phase = "explicit") => record("verifier_compare", agent, async () => {
|
|
2528
|
+
const compareCandidates = async (agent, problem, candidateA, candidateB, repeats, signal, rubric, routedImages = [], phase = "explicit") => record("verifier_compare", agent, async (trace) => {
|
|
3278
2529
|
const { verifier, selected } = await engine(agent);
|
|
3279
2530
|
return {
|
|
3280
2531
|
result: await verifier.compare({
|
|
3281
2532
|
problem,
|
|
3282
2533
|
candidateA,
|
|
3283
2534
|
candidateB,
|
|
2535
|
+
criteria: rubric.criteria,
|
|
2536
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
3284
2537
|
repeats,
|
|
3285
|
-
images: routedImages
|
|
2538
|
+
images: routedImages,
|
|
2539
|
+
...trace ? { trace } : {}
|
|
3286
2540
|
}, signal),
|
|
3287
2541
|
selected
|
|
3288
2542
|
};
|
|
3289
2543
|
}, phase);
|
|
3290
|
-
const selectCandidates = async (agent, problem, candidates, repeats, signal, routedImages = [], phase = "explicit") => record("verifier_select", agent, async () => {
|
|
2544
|
+
const selectCandidates = async (agent, problem, candidates, repeats, signal, rubric, routedImages = [], phase = "explicit") => record("verifier_select", agent, async (trace) => {
|
|
3291
2545
|
const { verifier, selected } = await engine(agent);
|
|
3292
2546
|
return {
|
|
3293
2547
|
result: await verifier.select({
|
|
3294
2548
|
problem,
|
|
3295
2549
|
candidates,
|
|
2550
|
+
criteria: rubric.criteria,
|
|
2551
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
3296
2552
|
repeats,
|
|
3297
2553
|
pivots: Math.min(2, candidates.length),
|
|
3298
2554
|
seed: 0,
|
|
3299
|
-
images: routedImages
|
|
2555
|
+
images: routedImages,
|
|
2556
|
+
...trace ? { trace } : {}
|
|
3300
2557
|
}, signal),
|
|
3301
2558
|
selected
|
|
3302
2559
|
};
|
|
3303
2560
|
}, phase);
|
|
3304
|
-
const trackProgress = async (agent, problem, steps, checkpoints, repeats, signal, routedImages = [], phase = "explicit") => record("verifier_track", agent, async () => {
|
|
2561
|
+
const trackProgress = async (agent, problem, steps, checkpoints, repeats, signal, routedImages = [], phase = "explicit") => record("verifier_track", agent, async (trace) => {
|
|
3305
2562
|
const { verifier, selected } = await engine(agent);
|
|
3306
2563
|
return {
|
|
3307
|
-
result: await verifier.track(problem, steps, checkpoints, repeats, signal, routedImages),
|
|
2564
|
+
result: await verifier.track(problem, steps, checkpoints, repeats, signal, routedImages, trace),
|
|
3308
2565
|
selected
|
|
3309
2566
|
};
|
|
3310
2567
|
}, phase);
|
|
3311
|
-
|
|
2568
|
+
/**
|
|
2569
|
+
* Explicit best-of-N: draft N candidates with the session model, rank them with the
|
|
2570
|
+
* configured judges, then re-measure the winner against the gate's own baseline.
|
|
2571
|
+
*
|
|
2572
|
+
* The tournament's `scores` are relative preference shares (wins/counts) and cannot be
|
|
2573
|
+
* compared with `autoVerifyThreshold`; only the extra winner-vs-baseline comparison produces
|
|
2574
|
+
* an absolute score in the gate's units. That comparison is why this tool exists instead of
|
|
2575
|
+
* "generate candidates yourself and call verifier_select".
|
|
2576
|
+
*
|
|
2577
|
+
* Fail-closed by construction: fewer than {@link MIN_BEST_OF_N} surviving drafts is an error
|
|
2578
|
+
* that names every failure, never a silent "best" picked out of a single survivor.
|
|
2579
|
+
* @param agent - Agent owning the topic, and the session whose model writes the drafts.
|
|
2580
|
+
* @param task - the request the drafts answer.
|
|
2581
|
+
* @param count - how many drafts to request ({@link MIN_BEST_OF_N}..{@link MAX_BEST_OF_N}).
|
|
2582
|
+
* @param repeats - caller's repeat count, or undefined for the default; used by both the
|
|
2583
|
+
* tournament and the baseline comparison.
|
|
2584
|
+
* @param signal - tool-call abort signal.
|
|
2585
|
+
* @param criteriaInput - caller-supplied criteria, or undefined for the configured rubric.
|
|
2586
|
+
*/
|
|
2587
|
+
const bestOfN = async (agent, task, count, repeats, signal, criteriaInput) => record("verifier_best_of_n", agent, async (trace) => {
|
|
2588
|
+
const { verifier, selected } = await engine(agent);
|
|
2589
|
+
if (!Number.isSafeInteger(count) || count < MIN_BEST_OF_N || count > MAX_BEST_OF_N) throw new Error("llm-verifier: n must be an integer between 2 and 4");
|
|
2590
|
+
const rounds = capped(repeats, 2, MAX_EXPLICIT_REPEATS, "repeats");
|
|
2591
|
+
const rubric = criteriaInput === void 0 ? await configuredCriteria() : {
|
|
2592
|
+
criteria: normalizeCriteria(criteriaInput),
|
|
2593
|
+
source: "explicit"
|
|
2594
|
+
};
|
|
2595
|
+
const planned = (selectComparisonsUpperBound(count) + 1) * rubric.criteria.length * rounds * selected.judges.length;
|
|
2596
|
+
if (planned > MAX_EXPLICIT_PLANNED_CALLS) throw new Error("llm-verifier: best-of-n with n=" + count + " would issue about " + planned + " judge calls; reduce n, repeats, criteria or judges");
|
|
2597
|
+
const call = agent.session.requestHeader()?.config;
|
|
2598
|
+
if (call === void 0) throw new Error("llm-verifier: this session has no logged request header yet, so best-of-n cannot tell which model should write the drafts — generate candidates with parallel subagents and rank them with verifier_select instead");
|
|
2599
|
+
const target = {
|
|
2600
|
+
provider: call.provider,
|
|
2601
|
+
model: call.model,
|
|
2602
|
+
...call.reasoningEffort === void 0 ? {} : { reasoningEffort: String(call.reasoningEffort) }
|
|
2603
|
+
};
|
|
2604
|
+
const problem = explicitEvidence([task], explicitItemChars(selected), explicitBudget(selected), "task")[0];
|
|
2605
|
+
const attempts = await Promise.all(Array.from({ length: count }, async (_unused, index) => {
|
|
2606
|
+
const prompt = buildGenerationPrompt(problem, index, count);
|
|
2607
|
+
try {
|
|
2608
|
+
const completion = await generateCandidate(verifier.client, target, prompt, signal);
|
|
2609
|
+
trace?.({
|
|
2610
|
+
label: "draft " + (index + 1),
|
|
2611
|
+
channel: completion.scoringMode,
|
|
2612
|
+
prompt,
|
|
2613
|
+
output: completion.text
|
|
2614
|
+
});
|
|
2615
|
+
return {
|
|
2616
|
+
ok: true,
|
|
2617
|
+
text: completion.text,
|
|
2618
|
+
usage: completion.usage,
|
|
2619
|
+
truncated: completion.truncated
|
|
2620
|
+
};
|
|
2621
|
+
} catch (error) {
|
|
2622
|
+
return {
|
|
2623
|
+
ok: false,
|
|
2624
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
}));
|
|
2628
|
+
const survivors = [];
|
|
2629
|
+
const failures = [];
|
|
2630
|
+
attempts.forEach((attempt, index) => {
|
|
2631
|
+
if (attempt.ok) survivors.push({
|
|
2632
|
+
attempt: index + 1,
|
|
2633
|
+
text: attempt.text,
|
|
2634
|
+
usage: attempt.usage,
|
|
2635
|
+
truncated: attempt.truncated
|
|
2636
|
+
});
|
|
2637
|
+
else failures.push("draft " + (index + 1) + ": " + attempt.error);
|
|
2638
|
+
});
|
|
2639
|
+
if (survivors.length < MIN_BEST_OF_N) throw new Error("llm-verifier: best-of-n produced " + survivors.length + " usable draft(s) out of " + count + "; at least 2 are required to choose between them" + (failures.length === 0 ? "" : " — " + failures.join("; ")));
|
|
2640
|
+
const ranked = await verifier.select({
|
|
2641
|
+
problem,
|
|
2642
|
+
candidates: survivors.map((survivor) => survivor.text),
|
|
2643
|
+
criteria: rubric.criteria,
|
|
2644
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
2645
|
+
repeats: rounds,
|
|
2646
|
+
pivots: Math.min(2, survivors.length),
|
|
2647
|
+
seed: 0,
|
|
2648
|
+
...trace ? { trace } : {}
|
|
2649
|
+
}, signal);
|
|
2650
|
+
const compared = await verifier.compare({
|
|
2651
|
+
problem,
|
|
2652
|
+
candidateA: ranked.best,
|
|
2653
|
+
candidateB: EMPTY_WORK_BASELINE,
|
|
2654
|
+
criteria: rubric.criteria,
|
|
2655
|
+
traceLabelPrefix: "baseline: ",
|
|
2656
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
2657
|
+
repeats: rounds,
|
|
2658
|
+
...trace ? { trace } : {}
|
|
2659
|
+
}, signal);
|
|
2660
|
+
const criteria = compared.criteria.map((row) => ({
|
|
2661
|
+
id: row.id,
|
|
2662
|
+
name: row.name,
|
|
2663
|
+
score: row.scoreA
|
|
2664
|
+
}));
|
|
2665
|
+
const threshold = selected.autoVerifyThreshold;
|
|
2666
|
+
const passesThreshold = sessionAccepted({
|
|
2667
|
+
score: compared.scoreA,
|
|
2668
|
+
winner: compared.winner,
|
|
2669
|
+
criteria
|
|
2670
|
+
}, threshold);
|
|
2671
|
+
const stats = { ...ranked.stats };
|
|
2672
|
+
addUsage(stats, compared.stats);
|
|
2673
|
+
stats.cacheHits += compared.stats.cacheHits;
|
|
2674
|
+
stats.cacheMisses += compared.stats.cacheMisses;
|
|
2675
|
+
stats.topLogprobScores += compared.stats.topLogprobScores;
|
|
2676
|
+
stats.explicitTagScores += compared.stats.explicitTagScores;
|
|
2677
|
+
for (const survivor of survivors) addUsage(stats, survivor.usage);
|
|
2678
|
+
stats.estimatedCostUsd = ((stats.inputTokens + stats.cachedInputTokens) * selected.estimatedInputUsdPerMillion + stats.outputTokens * selected.estimatedOutputUsdPerMillion) / 1e6;
|
|
2679
|
+
return {
|
|
2680
|
+
result: {
|
|
2681
|
+
best: ranked.best,
|
|
2682
|
+
index: ranked.index,
|
|
2683
|
+
/** 1-based draft numbers of the survivors, in `scores`/`ranking` order. */
|
|
2684
|
+
sources: survivors.map((survivor) => survivor.attempt),
|
|
2685
|
+
scores: ranked.scores,
|
|
2686
|
+
ranking: ranked.ranking,
|
|
2687
|
+
comparisons: ranked.comparisons,
|
|
2688
|
+
pivots: ranked.pivots,
|
|
2689
|
+
generated: survivors.length,
|
|
2690
|
+
failed: failures.length,
|
|
2691
|
+
failures,
|
|
2692
|
+
/** 1-based draft numbers still truncated after their escalated retry; empty is the normal case. */
|
|
2693
|
+
truncated: survivors.filter((survivor) => survivor.truncated).map((survivor) => survivor.attempt),
|
|
2694
|
+
score: compared.scoreA,
|
|
2695
|
+
baselineScore: compared.scoreB,
|
|
2696
|
+
winner: compared.winner,
|
|
2697
|
+
criteria: compared.criteria,
|
|
2698
|
+
threshold,
|
|
2699
|
+
passesThreshold,
|
|
2700
|
+
failedCriteria: failedAcceptanceCriteria(criteria, threshold).map((criterion) => criterion.id),
|
|
2701
|
+
calls: stats.calls,
|
|
2702
|
+
stats,
|
|
2703
|
+
generatorProvider: target.provider,
|
|
2704
|
+
generatorModel: target.model,
|
|
2705
|
+
judges: ranked.judges.map((judge, index) => {
|
|
2706
|
+
const baseline = compared.judges[index];
|
|
2707
|
+
if (baseline === void 0) return judge;
|
|
2708
|
+
return {
|
|
2709
|
+
...judge,
|
|
2710
|
+
ok: judge.ok && baseline.ok,
|
|
2711
|
+
calls: judge.calls + baseline.calls,
|
|
2712
|
+
...judge.error === void 0 && baseline.error !== void 0 ? { error: baseline.error } : {}
|
|
2713
|
+
};
|
|
2714
|
+
})
|
|
2715
|
+
},
|
|
2716
|
+
selected
|
|
2717
|
+
};
|
|
2718
|
+
});
|
|
2719
|
+
const classifyRoute = async (agent, prompt, signal, phase) => record("verifier_route_classify", agent, async (trace) => {
|
|
3312
2720
|
const { verifier, selected } = await engine(agent);
|
|
3313
2721
|
const completion = await callVerifierText(verifier.client, prompt, signal);
|
|
2722
|
+
trace?.({
|
|
2723
|
+
label: "route classify",
|
|
2724
|
+
channel: completion.scoringMode,
|
|
2725
|
+
prompt,
|
|
2726
|
+
output: completion.text
|
|
2727
|
+
});
|
|
3314
2728
|
const stats = {
|
|
3315
2729
|
...completion.usage,
|
|
3316
2730
|
cacheHits: 0,
|
|
@@ -3343,8 +2757,10 @@ function apply(ctx, config = {}) {
|
|
|
3343
2757
|
mode: selected.autoVerifyMode,
|
|
3344
2758
|
minConfidence: selected.autoRouteMinConfidence,
|
|
3345
2759
|
maxCandidates: selected.autoRouteMaxCandidates,
|
|
3346
|
-
|
|
3347
|
-
|
|
2760
|
+
maxRoutePerTask: selected.autoRouteMaxPerTask,
|
|
2761
|
+
maxRoutePerSession: selected.autoRouteMaxPerSession,
|
|
2762
|
+
maxFinalPerTask: selected.autoVerifyMaxPerTask,
|
|
2763
|
+
maxFinalPerSession: selected.autoVerifyMaxPerSession,
|
|
3348
2764
|
maxModelCallsPerTask: selected.autoMaxModelCallsPerTask,
|
|
3349
2765
|
maxModelCallsPerSession: selected.autoMaxModelCallsPerSession,
|
|
3350
2766
|
maxInputChars: selected.autoRouteMaxInputChars,
|
|
@@ -3367,7 +2783,7 @@ function apply(ctx, config = {}) {
|
|
|
3367
2783
|
if (!parsed.ok) return rpcFailure(parsed.message);
|
|
3368
2784
|
const query = parsed.query;
|
|
3369
2785
|
try {
|
|
3370
|
-
const headers = (await
|
|
2786
|
+
const headers = (await sessionHeaders()).filter((header) => query.sessionId === void 0 || String(header.id) === query.sessionId);
|
|
3371
2787
|
const settled = await Promise.allSettled(headers.map(async (header) => topic(header).statistics.overview(query)));
|
|
3372
2788
|
const overviews = [];
|
|
3373
2789
|
for (const result of settled) if (result.status === "fulfilled") overviews.push(result.value);
|
|
@@ -3377,6 +2793,93 @@ function apply(ctx, config = {}) {
|
|
|
3377
2793
|
return rpcFailure(error instanceof Error ? error.message : String(error));
|
|
3378
2794
|
}
|
|
3379
2795
|
};
|
|
2796
|
+
/**
|
|
2797
|
+
* One decision snapshot by invocation id, across every topic.
|
|
2798
|
+
*
|
|
2799
|
+
* The dashboard shows merged topics, so the lookup fans out the same way the
|
|
2800
|
+
* statistics query does instead of guessing which topic owns the record.
|
|
2801
|
+
* @param id - invocation id the snapshot was stored under.
|
|
2802
|
+
*/
|
|
2803
|
+
const handleDecisionQuery = async (id) => {
|
|
2804
|
+
if (typeof id !== "string" || id.length === 0) return rpcFailure("decision id must be a non-empty string");
|
|
2805
|
+
try {
|
|
2806
|
+
for (const header of await sessionHeaders()) {
|
|
2807
|
+
const found = await topic(header).decisions.find(id).catch(() => void 0);
|
|
2808
|
+
if (found) return rpcSuccess({ decision: found });
|
|
2809
|
+
}
|
|
2810
|
+
return rpcFailure("decision snapshot not found: it was pruned, never captured, or belongs to a deleted topic");
|
|
2811
|
+
} catch (error) {
|
|
2812
|
+
return rpcFailure(error instanceof Error ? error.message : String(error));
|
|
2813
|
+
}
|
|
2814
|
+
};
|
|
2815
|
+
/** Whether a payload asks for a decision snapshot instead of a statistics overview. */
|
|
2816
|
+
const isDecisionQuery = (payload) => typeof payload === "object" && payload !== null && payload.kind === "decision";
|
|
2817
|
+
/**
|
|
2818
|
+
* Judge diagnostics: one real, bounded judge call per configured judge plus the resolved rubric.
|
|
2819
|
+
*
|
|
2820
|
+
* Answers the two questions a user actually has after configuring the plugin: "which scoring
|
|
2821
|
+
* channel is this judge using" (a silent explicit-tag downgrade is invisible otherwise) and
|
|
2822
|
+
* "is the rubric I configured the one in effect" (a broken custom file degrades quietly).
|
|
2823
|
+
* Deliberately NOT recorded in the statistics: it is a diagnostic, not a verification.
|
|
2824
|
+
* @returns Per-judge probe results and the rubric in effect.
|
|
2825
|
+
*/
|
|
2826
|
+
const handleProbe = async () => {
|
|
2827
|
+
try {
|
|
2828
|
+
const header = ctx.agents.currentInitiator()?.session.header ?? (await sessionHeaders())[0];
|
|
2829
|
+
if (header === void 0) return rpcFailure("llm-verifier: the judge probe needs one session to attach its capability memory to, and no session exists yet — start a session, then probe again");
|
|
2830
|
+
const { verifier } = await engineForHeader(header);
|
|
2831
|
+
const rubric = await configuredCriteria();
|
|
2832
|
+
const criterion = rubric.criteria[0];
|
|
2833
|
+
if (criterion === void 0) return rpcFailure("llm-verifier: the configured rubric has no criteria");
|
|
2834
|
+
const prompt = buildPairwisePrompt(PROBE_TASK, PROBE_A, PROBE_B, criterion, rubric.groundTruthNote ?? "**IMPORTANT:** Focus on observed tool and terminal output as ground truth. Do NOT trust the agent's self-assessment or claims of success.");
|
|
2835
|
+
const judges = [];
|
|
2836
|
+
for (const client of verifier.clients) {
|
|
2837
|
+
const label = client.label ?? client.provider + "/" + client.model;
|
|
2838
|
+
const startedAt = Date.now();
|
|
2839
|
+
client.topLogprobCapabilities.forget(client.provider, client.model);
|
|
2840
|
+
try {
|
|
2841
|
+
const completion = await callVerifier({
|
|
2842
|
+
...client,
|
|
2843
|
+
timeoutMs: Math.min(client.timeoutMs, PROBE_TIMEOUT_MS),
|
|
2844
|
+
maxRetries: 0
|
|
2845
|
+
}, prompt);
|
|
2846
|
+
judges.push({
|
|
2847
|
+
label,
|
|
2848
|
+
provider: client.provider,
|
|
2849
|
+
model: client.model,
|
|
2850
|
+
ok: true,
|
|
2851
|
+
channelProbed: true,
|
|
2852
|
+
channel: completion.scoringMode,
|
|
2853
|
+
scoreA: extractScore(completion, "<score_A>"),
|
|
2854
|
+
scoreB: extractScore(completion, "<score_B>"),
|
|
2855
|
+
latencyMs: Date.now() - startedAt,
|
|
2856
|
+
...completion.usage
|
|
2857
|
+
});
|
|
2858
|
+
} catch (error) {
|
|
2859
|
+
judges.push({
|
|
2860
|
+
label,
|
|
2861
|
+
provider: client.provider,
|
|
2862
|
+
model: client.model,
|
|
2863
|
+
ok: false,
|
|
2864
|
+
latencyMs: Date.now() - startedAt,
|
|
2865
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
return rpcSuccess({
|
|
2870
|
+
judges,
|
|
2871
|
+
channelProbed: true,
|
|
2872
|
+
rubric: {
|
|
2873
|
+
source: rubric.source,
|
|
2874
|
+
count: rubric.criteria.length,
|
|
2875
|
+
...rubric.file ? { file: rubric.file } : {},
|
|
2876
|
+
...rubric.error ? { error: rubric.error } : {}
|
|
2877
|
+
}
|
|
2878
|
+
});
|
|
2879
|
+
} catch (error) {
|
|
2880
|
+
return rpcFailure(error instanceof Error ? error.message : String(error));
|
|
2881
|
+
}
|
|
2882
|
+
};
|
|
3380
2883
|
const anyConn = services.connection;
|
|
3381
2884
|
if (anyConn && anyConn.fetch && typeof anyConn.fetch.register === "function") try {
|
|
3382
2885
|
anyConn.fetch.register({
|
|
@@ -3393,7 +2896,8 @@ function apply(ctx, config = {}) {
|
|
|
3393
2896
|
const isRpcEnvelope = typeof body === "object" && body !== null && body.type === "client-request" && typeof body.rpcId === "string";
|
|
3394
2897
|
const rpcId = isRpcEnvelope ? body.rpcId : "direct";
|
|
3395
2898
|
const payload = isRpcEnvelope ? body.payload : body;
|
|
3396
|
-
const
|
|
2899
|
+
const isProbe = typeof payload === "object" && payload !== null && payload.kind === "probe";
|
|
2900
|
+
const outcome = isDecisionQuery(payload) ? await handleDecisionQuery(payload.id) : isProbe ? await handleProbe() : await handleStatisticsQuery(payload);
|
|
3397
2901
|
if (isRpcEnvelope) return Response.json({
|
|
3398
2902
|
type: "server-response",
|
|
3399
2903
|
rpcId,
|
|
@@ -3408,6 +2912,8 @@ function apply(ctx, config = {}) {
|
|
|
3408
2912
|
const legacyRpc = services.connection;
|
|
3409
2913
|
if (typeof legacyRpc.rpc?.handle === "function") try {
|
|
3410
2914
|
legacyRpc.rpc.handle("/llm-verifier", async (endpoint, payload) => {
|
|
2915
|
+
if (endpoint === "decision") return handleDecisionQuery(payload?.id);
|
|
2916
|
+
if (endpoint === "probe") return handleProbe();
|
|
3411
2917
|
if (endpoint !== "statistics") return rpcFailure("unknown llm-verifier endpoint");
|
|
3412
2918
|
return handleStatisticsQuery(payload);
|
|
3413
2919
|
});
|
|
@@ -3545,13 +3051,14 @@ function apply(ctx, config = {}) {
|
|
|
3545
3051
|
}
|
|
3546
3052
|
}
|
|
3547
3053
|
}
|
|
3548
|
-
const
|
|
3549
|
-
|
|
3054
|
+
const finalPreferred = autoRouter.finalPreferred(agent);
|
|
3055
|
+
const structured = finalPreferred ? void 0 : analyzeStructuredRoute(snapshot, selected.autoRouteMaxCandidates, selected.autoRouteMaxItemChars, selected.autoRouteMaxInputChars);
|
|
3056
|
+
let decision = finalPreferred ? void 0 : boundDecision(structured, policy);
|
|
3550
3057
|
if (structured !== void 0 && decision === void 0) {
|
|
3551
3058
|
ctx.logger.warn("llm-verifier automatic " + structured.kind + " route dropped: its evidence exceeds the per-item/total routing caps (" + selected.autoRouteMaxItemChars + "/" + selected.autoRouteMaxInputChars + " characters)");
|
|
3552
3059
|
await recordSkippedRoute(agent, structured.kind, "structured", "dropped-over-budget");
|
|
3553
3060
|
}
|
|
3554
|
-
if (decision === void 0 && selected.autoRouteSemantic && (selected.autoVerifyMode === "strict" || semanticRouteHint(snapshot))) {
|
|
3061
|
+
if (!finalPreferred && decision === void 0 && selected.autoRouteSemantic && (selected.autoVerifyMode === "strict" || semanticRouteHint(snapshot))) {
|
|
3555
3062
|
const fingerprint = stableHash({
|
|
3556
3063
|
phase: "semantic",
|
|
3557
3064
|
from: evidence.taskStartSeq,
|
|
@@ -3598,8 +3105,9 @@ function apply(ctx, config = {}) {
|
|
|
3598
3105
|
}
|
|
3599
3106
|
if (!stillCurrent()) return;
|
|
3600
3107
|
if (decision) {
|
|
3601
|
-
const repeats = routedRepeats(decision, selected.autoVerifyRepeats);
|
|
3602
|
-
const
|
|
3108
|
+
const repeats = routedRepeats(decision, selected.autoVerifyRepeats, selected.autoTrackRepeats);
|
|
3109
|
+
const rubric = await configuredCriteria();
|
|
3110
|
+
const expectedCalls = estimateRoutedCalls(decision, repeats, rubric.criteria.length) * selected.judges.length;
|
|
3603
3111
|
const reservation = autoRouter.reserve(agent, decision.kind, decision.fingerprint, expectedCalls, policy);
|
|
3604
3112
|
if (reservation === void 0) {
|
|
3605
3113
|
const exhausted = autoRouter.budgetExhausted(agent, expectedCalls, policy);
|
|
@@ -3623,7 +3131,7 @@ function apply(ctx, config = {}) {
|
|
|
3623
3131
|
if (reservation) try {
|
|
3624
3132
|
const extracted = await extractTask(agent, evidence.taskStartSeq, admittedLastSeq, selected.autoVerifyMaxChars, signal);
|
|
3625
3133
|
if (decision.kind === "compare") {
|
|
3626
|
-
const result = await compareCandidates(agent, extracted.problem, decision.candidates[0].content, decision.candidates[1].content, repeats, signal, extracted.images, "compare");
|
|
3134
|
+
const result = await compareCandidates(agent, extracted.problem, decision.candidates[0].content, decision.candidates[1].content, repeats, signal, rubric, extracted.images, "compare");
|
|
3627
3135
|
if (!stillCurrent()) {
|
|
3628
3136
|
autoRouter.fail(agent, reservation, false);
|
|
3629
3137
|
return;
|
|
@@ -3634,7 +3142,7 @@ function apply(ctx, config = {}) {
|
|
|
3634
3142
|
return;
|
|
3635
3143
|
}
|
|
3636
3144
|
if (decision.kind === "select") {
|
|
3637
|
-
const result = await selectCandidates(agent, extracted.problem, decision.candidates.map((candidate) => candidate.content), repeats, signal, extracted.images, "select");
|
|
3145
|
+
const result = await selectCandidates(agent, extracted.problem, decision.candidates.map((candidate) => candidate.content), repeats, signal, rubric, extracted.images, "select");
|
|
3638
3146
|
if (!stillCurrent()) {
|
|
3639
3147
|
autoRouter.fail(agent, reservation, false);
|
|
3640
3148
|
return;
|
|
@@ -3651,7 +3159,9 @@ function apply(ctx, config = {}) {
|
|
|
3651
3159
|
}
|
|
3652
3160
|
if (!autoRouter.commit(agent, reservation, admittedLastSeq)) return;
|
|
3653
3161
|
const detail = result.scores.map((score, index) => "Checkpoint step " + decision.checkpoints[index] + ": " + (score * 100).toFixed(1) + "%").join("\n");
|
|
3654
|
-
const
|
|
3162
|
+
const completed = (result.scores.length > 0 ? result.scores[result.scores.length - 1] : 0) >= selected.autoTrackCompletionThreshold;
|
|
3163
|
+
if (completed) autoRouter.preferFinal(agent);
|
|
3164
|
+
const continuation = completed ? "\nPrepare final delivery evidence; final session verification is mandatory." : "\nContinue the unfinished work.";
|
|
3655
3165
|
agent.steer(routeFeedback(decision, detail + continuation));
|
|
3656
3166
|
return;
|
|
3657
3167
|
} catch (error) {
|
|
@@ -3691,7 +3201,8 @@ function apply(ctx, config = {}) {
|
|
|
3691
3201
|
from: finalFromSeq,
|
|
3692
3202
|
to: admittedLastSeq
|
|
3693
3203
|
});
|
|
3694
|
-
const
|
|
3204
|
+
const finalRubric = await configuredCriteria();
|
|
3205
|
+
const finalReservation = autoRouter.reserve(agent, "final", finalFingerprint, Math.max(1, finalRubric.criteria.length * selected.autoVerifyFinalRepeats) * selected.judges.length, policy);
|
|
3695
3206
|
if (!finalReservation) {
|
|
3696
3207
|
if (selected.autoVerifyMode === "strict" && (forcedFromSeq !== void 0 || autoRouter.strictBlocked(agent))) if (autoRouter.claimExhaustedNotice(agent)) agent.steer(createUserMessage({
|
|
3697
3208
|
content: [{
|
|
@@ -3713,7 +3224,7 @@ function apply(ctx, config = {}) {
|
|
|
3713
3224
|
includeAssistantText: true,
|
|
3714
3225
|
maxChars: selected.autoVerifyMaxChars,
|
|
3715
3226
|
repeats: selected.autoVerifyFinalRepeats
|
|
3716
|
-
}, signal, "final");
|
|
3227
|
+
}, signal, "final", finalRubric);
|
|
3717
3228
|
if (!stillCurrent()) {
|
|
3718
3229
|
autoRouter.fail(agent, finalReservation, false);
|
|
3719
3230
|
return;
|
|
@@ -3826,21 +3337,27 @@ function apply(ctx, config = {}) {
|
|
|
3826
3337
|
async execute(args, exec) {
|
|
3827
3338
|
requireEnabled();
|
|
3828
3339
|
const agent = requireAgent(exec.agent);
|
|
3829
|
-
return record("verifier_compare", agent, async () => {
|
|
3340
|
+
return record("verifier_compare", agent, async (trace) => {
|
|
3830
3341
|
const { verifier, selected } = await engine(agent);
|
|
3831
3342
|
const [problem, candidateA, candidateB] = explicitEvidence([
|
|
3832
3343
|
args.problem,
|
|
3833
3344
|
args.candidate_a,
|
|
3834
3345
|
args.candidate_b
|
|
3835
3346
|
], explicitItemChars(selected), explicitBudget(selected), "compare input");
|
|
3347
|
+
const rubric = args.criteria === void 0 ? await configuredCriteria() : {
|
|
3348
|
+
criteria: normalizeCriteria(args.criteria),
|
|
3349
|
+
source: "explicit"
|
|
3350
|
+
};
|
|
3836
3351
|
return {
|
|
3837
3352
|
result: await verifier.compare({
|
|
3838
3353
|
problem,
|
|
3839
3354
|
candidateA,
|
|
3840
3355
|
candidateB,
|
|
3841
|
-
criteria:
|
|
3356
|
+
criteria: rubric.criteria,
|
|
3357
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
3842
3358
|
repeats: capped(args.repeats, 2, MAX_EXPLICIT_REPEATS, "repeats"),
|
|
3843
|
-
images: await images(args.images, exec.signal)
|
|
3359
|
+
images: await images(args.images, exec.signal),
|
|
3360
|
+
...trace ? { trace } : {}
|
|
3844
3361
|
}, exec.signal),
|
|
3845
3362
|
selected
|
|
3846
3363
|
};
|
|
@@ -3849,7 +3366,7 @@ function apply(ctx, config = {}) {
|
|
|
3849
3366
|
}));
|
|
3850
3367
|
ctx.tools.register(defineTool({
|
|
3851
3368
|
name: "verifier_select",
|
|
3852
|
-
description: "Use autonomously when three or more substantive candidate answers, patches, plans, or trajectories must be ranked and an independent choice is valuable. Use verifier_compare for exactly two candidates
|
|
3369
|
+
description: "Use autonomously when three or more substantive candidate answers, patches, plans, or trajectories must be ranked and an independent choice is valuable. Use verifier_compare for exactly two candidates. Generating extra candidates pays off only when the artifact is a final deliverable and choosing wrong is expensive: produce them (for example with parallel subagents), then rank the real ones here. Do not pad the list with near-duplicates. Deterministic orchestrators should call this directly once they have three or more real candidates.",
|
|
3853
3370
|
parameters: {
|
|
3854
3371
|
problem: {
|
|
3855
3372
|
type: "string",
|
|
@@ -3921,12 +3438,16 @@ function apply(ctx, config = {}) {
|
|
|
3921
3438
|
async execute(args, exec) {
|
|
3922
3439
|
requireEnabled();
|
|
3923
3440
|
const agent = requireAgent(exec.agent);
|
|
3924
|
-
return record("verifier_select", agent, async () => {
|
|
3441
|
+
return record("verifier_select", agent, async (trace) => {
|
|
3925
3442
|
const { verifier, selected } = await engine(agent);
|
|
3926
3443
|
const limit = explicitCandidateLimit(selected);
|
|
3927
3444
|
if (args.candidates.length > limit) throw new Error("llm-verifier: candidates must contain at most " + limit + " entries");
|
|
3928
3445
|
const candidates = explicitEvidence(args.candidates, explicitItemChars(selected), explicitBudget(selected), "candidates");
|
|
3929
|
-
const
|
|
3446
|
+
const rubric = args.criteria === void 0 ? await configuredCriteria() : {
|
|
3447
|
+
criteria: normalizeCriteria(args.criteria),
|
|
3448
|
+
source: "explicit"
|
|
3449
|
+
};
|
|
3450
|
+
const criteria = rubric.criteria;
|
|
3930
3451
|
const repeats = capped(args.repeats, 2, MAX_EXPLICIT_REPEATS, "repeats");
|
|
3931
3452
|
const planned = plannedComparisons(candidates.length) * (criteria?.length || 3) * repeats;
|
|
3932
3453
|
if (planned > MAX_EXPLICIT_PLANNED_CALLS) throw new Error("llm-verifier: this selection would issue about " + planned + " judge calls; reduce candidates or repeats");
|
|
@@ -3935,10 +3456,12 @@ function apply(ctx, config = {}) {
|
|
|
3935
3456
|
problem: sanitizeVerifierText(args.problem, explicitItemChars(selected)),
|
|
3936
3457
|
candidates,
|
|
3937
3458
|
criteria,
|
|
3459
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
3938
3460
|
repeats,
|
|
3939
3461
|
pivots: capped(args.pivots, 2, Math.max(1, candidates.length), "pivots"),
|
|
3940
3462
|
seed: args.seed ?? 0,
|
|
3941
|
-
images: await images(args.images, exec.signal)
|
|
3463
|
+
images: await images(args.images, exec.signal),
|
|
3464
|
+
...trace ? { trace } : {}
|
|
3942
3465
|
}, exec.signal),
|
|
3943
3466
|
selected
|
|
3944
3467
|
};
|
|
@@ -4009,17 +3532,159 @@ function apply(ctx, config = {}) {
|
|
|
4009
3532
|
async execute(args, exec) {
|
|
4010
3533
|
requireEnabled();
|
|
4011
3534
|
const agent = requireAgent(exec.agent);
|
|
4012
|
-
return record("verifier_track", agent, async () => {
|
|
3535
|
+
return record("verifier_track", agent, async (trace) => {
|
|
4013
3536
|
const { verifier, selected } = await engine(agent);
|
|
4014
3537
|
if (args.steps.length > MAX_TRACK_STEPS) throw new Error("llm-verifier: steps must contain at most 32 entries");
|
|
4015
3538
|
const steps = explicitEvidence(args.steps, explicitItemChars(selected), explicitBudget(selected), "steps");
|
|
4016
3539
|
return {
|
|
4017
|
-
result: await verifier.track(sanitizeVerifierText(args.problem, explicitItemChars(selected)), steps, args.checkpoints, capped(args.repeats, 2, MAX_EXPLICIT_REPEATS, "repeats"), exec.signal, await images(args.images, exec.signal)),
|
|
3540
|
+
result: await verifier.track(sanitizeVerifierText(args.problem, explicitItemChars(selected)), steps, args.checkpoints, capped(args.repeats, 2, MAX_EXPLICIT_REPEATS, "repeats"), exec.signal, await images(args.images, exec.signal), trace),
|
|
4018
3541
|
selected
|
|
4019
3542
|
};
|
|
4020
3543
|
});
|
|
4021
3544
|
}
|
|
4022
3545
|
}));
|
|
3546
|
+
ctx.tools.register(defineTool({
|
|
3547
|
+
name: "verifier_best_of_n",
|
|
3548
|
+
description: "Use ONLY when a final deliverable is expensive to get wrong and no candidate exists yet: this drafts n independent candidates with the current session model, has the independent verifier rank them, and then re-scores the winner against the same fixed empty-work baseline the automatic acceptance gate uses. A draft that hits its output ceiling is kept and listed in truncated — the judge sees the incomplete text and scores it as such, and discarding it would waste a generation already paid for. It is by far the most expensive verifier tool: on the default 3-criterion rubric it costs 2 generations + 12 judge calls at n=2, 3 + 24 at n=3, and 4 + 36..60 at n=4 — never call it per turn, for trivial questions, or to compare candidates you already have (rank those with verifier_select or verifier_compare). scores are RELATIVE tournament shares; only score, criteria, threshold and passesThreshold are absolute and comparable with the acceptance gate. Requires a session whose model is already known; if the session has no logged request header, write the candidates with parallel subagents and call verifier_select instead.",
|
|
3549
|
+
parameters: {
|
|
3550
|
+
task: {
|
|
3551
|
+
type: "string",
|
|
3552
|
+
required: true,
|
|
3553
|
+
description: "The request every draft must answer. Bound to the explicit per-item evidence cap after redaction."
|
|
3554
|
+
},
|
|
3555
|
+
n: {
|
|
3556
|
+
type: "integer",
|
|
3557
|
+
description: "How many independent drafts to generate; between 2 and 4, default 3. Cost grows with n."
|
|
3558
|
+
},
|
|
3559
|
+
criteria: commonParams.criteria,
|
|
3560
|
+
repeats: commonParams.repeats
|
|
3561
|
+
},
|
|
3562
|
+
output: {
|
|
3563
|
+
schema: {
|
|
3564
|
+
type: "object",
|
|
3565
|
+
additionalProperties: false,
|
|
3566
|
+
properties: {
|
|
3567
|
+
best: {
|
|
3568
|
+
type: "string",
|
|
3569
|
+
required: true
|
|
3570
|
+
},
|
|
3571
|
+
index: {
|
|
3572
|
+
type: "integer",
|
|
3573
|
+
required: true
|
|
3574
|
+
},
|
|
3575
|
+
sources: {
|
|
3576
|
+
type: "array",
|
|
3577
|
+
items: { type: "integer" },
|
|
3578
|
+
required: true
|
|
3579
|
+
},
|
|
3580
|
+
scores: {
|
|
3581
|
+
type: "array",
|
|
3582
|
+
items: { type: "number" },
|
|
3583
|
+
required: true
|
|
3584
|
+
},
|
|
3585
|
+
ranking: {
|
|
3586
|
+
type: "array",
|
|
3587
|
+
items: { type: "integer" },
|
|
3588
|
+
required: true
|
|
3589
|
+
},
|
|
3590
|
+
comparisons: {
|
|
3591
|
+
type: "integer",
|
|
3592
|
+
required: true
|
|
3593
|
+
},
|
|
3594
|
+
pivots: {
|
|
3595
|
+
type: "array",
|
|
3596
|
+
items: { type: "integer" },
|
|
3597
|
+
required: true
|
|
3598
|
+
},
|
|
3599
|
+
generated: {
|
|
3600
|
+
type: "integer",
|
|
3601
|
+
required: true
|
|
3602
|
+
},
|
|
3603
|
+
failed: {
|
|
3604
|
+
type: "integer",
|
|
3605
|
+
required: true
|
|
3606
|
+
},
|
|
3607
|
+
failures: {
|
|
3608
|
+
type: "array",
|
|
3609
|
+
items: { type: "string" },
|
|
3610
|
+
required: true
|
|
3611
|
+
},
|
|
3612
|
+
truncated: {
|
|
3613
|
+
type: "array",
|
|
3614
|
+
items: { type: "integer" },
|
|
3615
|
+
required: true
|
|
3616
|
+
},
|
|
3617
|
+
score: {
|
|
3618
|
+
type: "number",
|
|
3619
|
+
required: true
|
|
3620
|
+
},
|
|
3621
|
+
baselineScore: {
|
|
3622
|
+
type: "number",
|
|
3623
|
+
required: true
|
|
3624
|
+
},
|
|
3625
|
+
winner: {
|
|
3626
|
+
type: "string",
|
|
3627
|
+
enum: [
|
|
3628
|
+
"A",
|
|
3629
|
+
"B",
|
|
3630
|
+
"tie"
|
|
3631
|
+
],
|
|
3632
|
+
required: true
|
|
3633
|
+
},
|
|
3634
|
+
criteria: {
|
|
3635
|
+
type: "array",
|
|
3636
|
+
items: criterionResultSchema,
|
|
3637
|
+
required: true
|
|
3638
|
+
},
|
|
3639
|
+
threshold: {
|
|
3640
|
+
type: "number",
|
|
3641
|
+
required: true
|
|
3642
|
+
},
|
|
3643
|
+
passesThreshold: {
|
|
3644
|
+
type: "boolean",
|
|
3645
|
+
required: true
|
|
3646
|
+
},
|
|
3647
|
+
failedCriteria: {
|
|
3648
|
+
type: "array",
|
|
3649
|
+
items: { type: "string" },
|
|
3650
|
+
required: true
|
|
3651
|
+
},
|
|
3652
|
+
calls: {
|
|
3653
|
+
type: "integer",
|
|
3654
|
+
required: true
|
|
3655
|
+
},
|
|
3656
|
+
stats: {
|
|
3657
|
+
...statsSchema,
|
|
3658
|
+
required: true
|
|
3659
|
+
},
|
|
3660
|
+
generatorProvider: {
|
|
3661
|
+
type: "string",
|
|
3662
|
+
required: true
|
|
3663
|
+
},
|
|
3664
|
+
generatorModel: {
|
|
3665
|
+
type: "string",
|
|
3666
|
+
required: true
|
|
3667
|
+
},
|
|
3668
|
+
provider: {
|
|
3669
|
+
type: "string",
|
|
3670
|
+
required: true
|
|
3671
|
+
},
|
|
3672
|
+
model: {
|
|
3673
|
+
type: "string",
|
|
3674
|
+
required: true
|
|
3675
|
+
},
|
|
3676
|
+
judges: judgesSchema
|
|
3677
|
+
}
|
|
3678
|
+
},
|
|
3679
|
+
render: (_args, value) => renderJson(value)
|
|
3680
|
+
},
|
|
3681
|
+
timeoutMs: entry.timeoutMs * 100,
|
|
3682
|
+
async execute(args, exec) {
|
|
3683
|
+
requireEnabled();
|
|
3684
|
+
const agent = requireAgent(exec.agent);
|
|
3685
|
+
return bestOfN(agent, args.task, args.n ?? DEFAULT_BEST_OF_N, args.repeats, exec.signal, args.criteria);
|
|
3686
|
+
}
|
|
3687
|
+
}));
|
|
4023
3688
|
ctx.tools.register(defineTool({
|
|
4024
3689
|
name: "verifier_current_session",
|
|
4025
3690
|
description: "Explicitly verify the current DSH session. Smart/strict policy can also invoke this gate automatically at the turn-stopping lifecycle boundary after consequential work with real tool evidence. Extracts the session, applies redaction and bounds, then sends the evidence to the configured verifier model.",
|
|
@@ -4117,6 +3782,6 @@ function apply(ctx, config = {}) {
|
|
|
4117
3782
|
}));
|
|
4118
3783
|
}
|
|
4119
3784
|
//#endregion
|
|
4120
|
-
export { AutoVerifierRouter, Config, DEFAULT_CRITERIA, DEFAULT_GROUND_TRUTH_NOTE, GRANULARITY, LETTERS, MAX_ROUTED_CHECKPOINTS, MAX_TEAM_TASK_METADATA_CHARS, RequestLimiter, SCALE_DESCRIPTION, ScoreCache, SingleFlight, StatisticsStore, UNTRUSTED_EVIDENCE_NOTE, VERIFIER_TOOL_NAMES, VerifierEngine, accumulatePairs, analyzeAutoTask, analyzeStructuredRoute, apply, automaticFeedback, boundDecision, bradleyTerry, buildEvidenceIndex, buildPairwisePrompt, buildPlanPreReviewPrompt, buildProgressPrompt, buildSemanticRoutePrompt, buildTeamTaskVerificationPrompt, callVerifier, emptyRunStats, errorDetails, estimateRoutedCalls, evidenceNonce, extractProgressScore, extractScore, failedAcceptanceCriteria, inject, inspectTeamTasks, isSubagentSession, latestDirectUserSeq, mergeStatisticsOverviews, name, normalizeCriteria, normalizeScoreLetter, orientRoundPairs, parseSemanticRoute, parseStatisticsQuery, parseVerdictLetter, pivotRoundPairs, planFromArguments, rankScores, renderDelimitedBlock, resolveCacheFile, resolveStatisticsFile, resolveTopicDataDir, ringCycle, routedRepeats, seededRandom, semanticDecision, semanticRouteHint, sessionAccepted, stableHash, summarizeVerdict, topPivots };
|
|
3785
|
+
export { AutoVerifierRouter, CRITERIA_PRESETS, CRITERIA_PRESET_IDS, Config, DEFAULT_CRITERIA, DEFAULT_GROUND_TRUTH_NOTE, DecisionStore, EMPTY_WORK_BASELINE, GRANULARITY, LETTERS, MAX_ROUTED_CHECKPOINTS, MAX_TEAM_TASK_METADATA_CHARS, RequestLimiter, SCALE_DESCRIPTION, ScoreCache, SingleFlight, StatisticsStore, UNTRUSTED_EVIDENCE_NOTE, VERIFIER_TOOL_NAMES, VerifierEngine, accumulatePairs, analyzeAutoTask, analyzeStructuredRoute, apply, automaticFeedback, boundCaptureText, boundDecision, boundDecisionCalls, bradleyTerry, buildEvidenceIndex, buildGenerationPrompt, buildPairwisePrompt, buildPlanPreReviewPrompt, buildProgressPrompt, buildSemanticRoutePrompt, buildTeamTaskVerificationPrompt, callVerifier, dedupeCriterionId, emptyRunStats, errorDetails, estimateRoutedCalls, evidenceNonce, extractProgressScore, extractScore, failedAcceptanceCriteria, inject, inspectTeamTasks, isSubagentSession, latestDirectUserSeq, mergeStatisticsOverviews, name, normalizeCriteria, normalizeScoreLetter, orientRoundPairs, parseCriteriaMarkdown, parseSemanticRoute, parseStatisticsQuery, parseVerdictLetter, pivotRoundPairs, planFromArguments, rankScores, renderDelimitedBlock, resolveCacheFile, resolveDecisionsFile, resolveStatisticsFile, resolveTopicDataDir, ringCycle, routedRepeats, seededRandom, semanticDecision, semanticRouteHint, sessionAccepted, slugCriterionId, stableHash, summarizeVerdict, topPivots };
|
|
4121
3786
|
|
|
4122
3787
|
//# sourceMappingURL=index.js.map
|