dsh-llm-verifier 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1044 -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 +402 -39
- 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 +349 -8
- package/src/index.ts +383 -38
- 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);
|
|
1082
|
+
async resolve(selection, file) {
|
|
1083
|
+
if (selection !== "custom") {
|
|
1084
|
+
const preset = CRITERIA_PRESETS[selection];
|
|
1520
1085
|
return {
|
|
1521
|
-
|
|
1522
|
-
source: "
|
|
1523
|
-
|
|
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
|
|
1086
|
+
criteria: preset ?? DEFAULT_CRITERIA,
|
|
1087
|
+
source: preset ? selection : "fallback",
|
|
1088
|
+
...preset ? {} : { error: "unknown criteria preset: " + String(selection) }
|
|
1533
1089
|
};
|
|
1534
1090
|
}
|
|
1535
|
-
const
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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
|
|
1555
|
-
};
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
/**
|
|
1560
|
-
* Artifacts that only the classifier can turn into candidates.
|
|
1561
|
-
*
|
|
1562
|
-
* Todo and team snapshots are deliberately absent. They are the structured track
|
|
1563
|
-
* route's own input, and that route runs first — so listing them here could only make
|
|
1564
|
-
* the hint true in shapes the structured pass already claimed (or in a task that
|
|
1565
|
-
* already ran an explicit `verifier_track`, where re-classifying the same snapshots is
|
|
1566
|
-
* not wanted). The one shape left out is a snapshot series whose lists are all shorter
|
|
1567
|
-
* than two items, which is not worth a classification call.
|
|
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
|
|
@@ -2906,6 +2081,29 @@ const criterionResultSchema = {
|
|
|
2906
2081
|
}
|
|
2907
2082
|
}
|
|
2908
2083
|
};
|
|
2084
|
+
/**
|
|
2085
|
+
* The per-criterion row `verifySession` reports: the compare engine's rows reduced to the
|
|
2086
|
+
* acceptance-facing `{id, name, score}` shape (`score` is compare's `scoreA`).
|
|
2087
|
+
*
|
|
2088
|
+
* Deliberately NOT {@link criterionResultSchema}: that one declares compare's `{scoreA, scoreB}`
|
|
2089
|
+
* rows, and the host rejects any key a tool's declared output schema does not mention — declaring
|
|
2090
|
+
* the wrong criterion shape here is exactly how the explicit session verifier broke.
|
|
2091
|
+
*/
|
|
2092
|
+
const acceptanceCriterionResultSchema = {
|
|
2093
|
+
type: "object",
|
|
2094
|
+
additionalProperties: false,
|
|
2095
|
+
properties: {
|
|
2096
|
+
id: {
|
|
2097
|
+
type: "string",
|
|
2098
|
+
required: true
|
|
2099
|
+
},
|
|
2100
|
+
name: { type: "string" },
|
|
2101
|
+
score: {
|
|
2102
|
+
type: "number",
|
|
2103
|
+
required: true
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2909
2107
|
const commonParams = {
|
|
2910
2108
|
criteria: {
|
|
2911
2109
|
type: "array",
|
|
@@ -2970,6 +2168,14 @@ const EXPLICIT_MAX_TOTAL_CHARS = 24e4;
|
|
|
2970
2168
|
const EXPLICIT_MIN_TOTAL_CHARS = 12e4;
|
|
2971
2169
|
const MAX_EXPLICIT_CANDIDATES = 16;
|
|
2972
2170
|
const MAX_EXPLICIT_PLANNED_CALLS = 500;
|
|
2171
|
+
/**
|
|
2172
|
+
* Best-of-N bounds. The ceiling is 4 because cost grows ~linearly in N while the marginal
|
|
2173
|
+
* value of the 5th draft does not, and the tool is meant for a final deliverable rather than
|
|
2174
|
+
* routine work. The floor is 2 because "choose the best of one" is not a choice.
|
|
2175
|
+
*/
|
|
2176
|
+
const MIN_BEST_OF_N = 2;
|
|
2177
|
+
const MAX_BEST_OF_N = 4;
|
|
2178
|
+
const DEFAULT_BEST_OF_N = 3;
|
|
2973
2179
|
function explicitItemChars(selected) {
|
|
2974
2180
|
return Math.max(selected.autoRouteMaxItemChars, EXPLICIT_MIN_ITEM_CHARS);
|
|
2975
2181
|
}
|
|
@@ -2983,6 +2189,19 @@ function explicitCandidateLimit(selected) {
|
|
|
2983
2189
|
function plannedComparisons(count) {
|
|
2984
2190
|
return count <= 2 ? 1 : count + Math.max(0, (count - 2) * 2 + 1 - 3);
|
|
2985
2191
|
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Worst-case tournament pairs for a selection of `count` candidates.
|
|
2194
|
+
*
|
|
2195
|
+
* `plannedComparisons` is an ESTIMATE (it assumes the ring already covers three of the pivot-round
|
|
2196
|
+
* pairs), and best-of-N spends N generations first, so its guard has to fail closed on the true
|
|
2197
|
+
* bound instead: ring (N) plus every pivot pair (non-pivots x pivots, plus pivot-vs-pivot), since
|
|
2198
|
+
* de-duplicating against the ring can only ever remove pairs.
|
|
2199
|
+
* @param count - candidate count.
|
|
2200
|
+
* @returns The largest number of pairs the tournament can judge.
|
|
2201
|
+
*/
|
|
2202
|
+
function selectComparisonsUpperBound(count) {
|
|
2203
|
+
return count <= 2 ? 1 : count + (count - 2) * 2 + 1;
|
|
2204
|
+
}
|
|
2986
2205
|
function numberField(value, fallback) {
|
|
2987
2206
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2988
2207
|
}
|
|
@@ -3071,6 +2290,18 @@ function rpcFailure(message) {
|
|
|
3071
2290
|
}
|
|
3072
2291
|
};
|
|
3073
2292
|
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Small but real pair for the settings-page probe.
|
|
2295
|
+
*
|
|
2296
|
+
* The probe asks each judge for an actual A–T verdict instead of a "ping": that is what proves
|
|
2297
|
+
* the response PARSES, which is the failure a reachability check misses. A pair where one side is
|
|
2298
|
+
* obviously better keeps the exercise honest without making the expected score a secret.
|
|
2299
|
+
*/
|
|
2300
|
+
const PROBE_TASK = "Fix the failing parser test and prove it passes.";
|
|
2301
|
+
const PROBE_A = "Ran the test: 1 failed. Patched the separator handling. Ran it again: 1 passed, no other failures.";
|
|
2302
|
+
const PROBE_B = "The test should pass now.";
|
|
2303
|
+
/** Bound the probe: a diagnostics button must not sit on the configured 5-minute timeout plus retries. */
|
|
2304
|
+
const PROBE_TIMEOUT_MS = 3e4;
|
|
3074
2305
|
function apply(ctx, config = {}) {
|
|
3075
2306
|
const services = ctx;
|
|
3076
2307
|
const entry = resolveConfig(config);
|
|
@@ -3092,7 +2323,8 @@ function apply(ctx, config = {}) {
|
|
|
3092
2323
|
cache: new ScoreCache(cacheFile, selected.cacheMaxEntries),
|
|
3093
2324
|
capabilities: new TopLogprobCapabilityCache(resolveCapabilityFile(dataDir)),
|
|
3094
2325
|
flights: new SingleFlight(),
|
|
3095
|
-
statistics: new StatisticsStore(resolveStatisticsFile(cacheFile))
|
|
2326
|
+
statistics: new StatisticsStore(resolveStatisticsFile(cacheFile)),
|
|
2327
|
+
decisions: new DecisionStore(resolveDecisionsFile(cacheFile))
|
|
3096
2328
|
};
|
|
3097
2329
|
topics.set(id, created);
|
|
3098
2330
|
return created;
|
|
@@ -3102,9 +2334,32 @@ function apply(ctx, config = {}) {
|
|
|
3102
2334
|
if (selected === void 0) throw new Error("llm-verifier: verifier tools require an agent-owned topic so their data can follow topic deletion");
|
|
3103
2335
|
return selected;
|
|
3104
2336
|
};
|
|
3105
|
-
const
|
|
2337
|
+
const criteriaResolver = new CriteriaResolver();
|
|
2338
|
+
const configuredCriteria = () => criteriaResolver.resolve(current().criteriaPreset, current().criteriaFile);
|
|
2339
|
+
/**
|
|
2340
|
+
* Every known topic header, newest first.
|
|
2341
|
+
*
|
|
2342
|
+
* The statistics and decision dashboards merge all topics; anything that needs exactly ONE
|
|
2343
|
+
* topic (the judge probe's per-topic capability memory) takes the newest, because that is where
|
|
2344
|
+
* the operator's current work lives. Legacy backends handed back the header directly instead of
|
|
2345
|
+
* wrapping it in \`{ header }`, so both shapes are accepted.
|
|
2346
|
+
* @returns Session headers, newest first; entries without an id are dropped.
|
|
2347
|
+
*/
|
|
2348
|
+
const sessionHeaders = async () => {
|
|
2349
|
+
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));
|
|
2350
|
+
};
|
|
2351
|
+
/**
|
|
2352
|
+
* Build the judge ensemble for one topic.
|
|
2353
|
+
*
|
|
2354
|
+
* Takes a session header rather than an Agent because the judge probe is started from the global
|
|
2355
|
+
* statistics dashboard, where there is no current initiator. The diagnostic still needs a topic
|
|
2356
|
+
* for the per-topic capability memory, so it attaches to the most recent one (see handleProbe).
|
|
2357
|
+
* @param header - session header owning the topic whose sidecars the judges use.
|
|
2358
|
+
* @returns The engine plus the resolved configuration it was built from.
|
|
2359
|
+
*/
|
|
2360
|
+
const engineForHeader = async (header) => {
|
|
3106
2361
|
const selected = current();
|
|
3107
|
-
const topicEntry = topic(
|
|
2362
|
+
const topicEntry = topic(header);
|
|
3108
2363
|
const clients = [];
|
|
3109
2364
|
for (const judge of selected.judges) {
|
|
3110
2365
|
await ctx.llm.resolveCallConfig({
|
|
@@ -3138,6 +2393,7 @@ function apply(ctx, config = {}) {
|
|
|
3138
2393
|
selected
|
|
3139
2394
|
};
|
|
3140
2395
|
};
|
|
2396
|
+
const engine = async (agent) => engineForHeader(agent.session.header);
|
|
3141
2397
|
const images = (values, signal) => loadVerifierImages(values, signal);
|
|
3142
2398
|
const route = (selected) => ({
|
|
3143
2399
|
provider: selected.provider,
|
|
@@ -3156,9 +2412,15 @@ function apply(ctx, config = {}) {
|
|
|
3156
2412
|
const record = async (toolName, agent, operation, phase = "explicit") => {
|
|
3157
2413
|
const startedAt = Date.now();
|
|
3158
2414
|
let selected = current();
|
|
3159
|
-
const
|
|
2415
|
+
const topicEntry = topic(agent.session.header);
|
|
2416
|
+
const statistics = topicEntry.statistics;
|
|
2417
|
+
const capture = current().captureDecisions;
|
|
2418
|
+
const calls = [];
|
|
2419
|
+
const trace = capture ? (call) => {
|
|
2420
|
+
calls.push(call);
|
|
2421
|
+
} : void 0;
|
|
3160
2422
|
try {
|
|
3161
|
-
const completed = await operation();
|
|
2423
|
+
const completed = await operation(trace);
|
|
3162
2424
|
selected = completed.selected;
|
|
3163
2425
|
const value = {
|
|
3164
2426
|
...completed.result,
|
|
@@ -3174,6 +2436,14 @@ function apply(ctx, config = {}) {
|
|
|
3174
2436
|
stats: statsFrom(value),
|
|
3175
2437
|
verdict: verdictFrom(toolName, value, phase)
|
|
3176
2438
|
}).catch(() => {});
|
|
2439
|
+
if (calls.length > 0) await topicEntry.decisions.record({
|
|
2440
|
+
toolName,
|
|
2441
|
+
phase,
|
|
2442
|
+
startedAt,
|
|
2443
|
+
provider: selected.provider,
|
|
2444
|
+
model: selected.model,
|
|
2445
|
+
calls: boundDecisionCalls([...calls].sort((a, b) => a.label < b.label ? -1 : a.label > b.label ? 1 : 0))
|
|
2446
|
+
}).catch(() => {});
|
|
3177
2447
|
return value;
|
|
3178
2448
|
} catch (error) {
|
|
3179
2449
|
const details = errorDetails(error);
|
|
@@ -3228,7 +2498,7 @@ function apply(ctx, config = {}) {
|
|
|
3228
2498
|
}
|
|
3229
2499
|
}).catch(() => {});
|
|
3230
2500
|
};
|
|
3231
|
-
const verifySession = async (agent, options, signal, phase = "explicit") => record("verifier_current_session", agent, async () => {
|
|
2501
|
+
const verifySession = async (agent, options, signal, phase = "explicit", rubricOverride) => record("verifier_current_session", agent, async (trace) => {
|
|
3232
2502
|
const extracted = await extractSession(agent, async (ref) => {
|
|
3233
2503
|
const stored = await services.attachments.readImage(ref, signal);
|
|
3234
2504
|
return {
|
|
@@ -3244,12 +2514,16 @@ function apply(ctx, config = {}) {
|
|
|
3244
2514
|
});
|
|
3245
2515
|
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
2516
|
const { verifier, selected } = await engine(agent);
|
|
2517
|
+
const rubric = rubricOverride ?? await configuredCriteria();
|
|
3247
2518
|
const compared = await verifier.compare({
|
|
3248
2519
|
problem: extracted.problem,
|
|
3249
2520
|
candidateA: extracted.trace,
|
|
3250
|
-
candidateB:
|
|
2521
|
+
candidateB: EMPTY_WORK_BASELINE,
|
|
2522
|
+
criteria: rubric.criteria,
|
|
2523
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
3251
2524
|
repeats: positive(options.repeats, 2, "repeats"),
|
|
3252
|
-
images: extracted.images
|
|
2525
|
+
images: extracted.images,
|
|
2526
|
+
...trace ? { trace } : {}
|
|
3253
2527
|
}, signal);
|
|
3254
2528
|
return {
|
|
3255
2529
|
result: {
|
|
@@ -3274,43 +2548,206 @@ function apply(ctx, config = {}) {
|
|
|
3274
2548
|
selected
|
|
3275
2549
|
};
|
|
3276
2550
|
}, phase);
|
|
3277
|
-
const compareCandidates = async (agent, problem, candidateA, candidateB, repeats, signal, routedImages = [], phase = "explicit") => record("verifier_compare", agent, async () => {
|
|
2551
|
+
const compareCandidates = async (agent, problem, candidateA, candidateB, repeats, signal, rubric, routedImages = [], phase = "explicit") => record("verifier_compare", agent, async (trace) => {
|
|
3278
2552
|
const { verifier, selected } = await engine(agent);
|
|
3279
2553
|
return {
|
|
3280
2554
|
result: await verifier.compare({
|
|
3281
2555
|
problem,
|
|
3282
2556
|
candidateA,
|
|
3283
2557
|
candidateB,
|
|
2558
|
+
criteria: rubric.criteria,
|
|
2559
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
3284
2560
|
repeats,
|
|
3285
|
-
images: routedImages
|
|
2561
|
+
images: routedImages,
|
|
2562
|
+
...trace ? { trace } : {}
|
|
3286
2563
|
}, signal),
|
|
3287
2564
|
selected
|
|
3288
2565
|
};
|
|
3289
2566
|
}, phase);
|
|
3290
|
-
const selectCandidates = async (agent, problem, candidates, repeats, signal, routedImages = [], phase = "explicit") => record("verifier_select", agent, async () => {
|
|
2567
|
+
const selectCandidates = async (agent, problem, candidates, repeats, signal, rubric, routedImages = [], phase = "explicit") => record("verifier_select", agent, async (trace) => {
|
|
3291
2568
|
const { verifier, selected } = await engine(agent);
|
|
3292
2569
|
return {
|
|
3293
2570
|
result: await verifier.select({
|
|
3294
2571
|
problem,
|
|
3295
2572
|
candidates,
|
|
2573
|
+
criteria: rubric.criteria,
|
|
2574
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
3296
2575
|
repeats,
|
|
3297
2576
|
pivots: Math.min(2, candidates.length),
|
|
3298
2577
|
seed: 0,
|
|
3299
|
-
images: routedImages
|
|
2578
|
+
images: routedImages,
|
|
2579
|
+
...trace ? { trace } : {}
|
|
3300
2580
|
}, signal),
|
|
3301
2581
|
selected
|
|
3302
2582
|
};
|
|
3303
2583
|
}, phase);
|
|
3304
|
-
const trackProgress = async (agent, problem, steps, checkpoints, repeats, signal, routedImages = [], phase = "explicit") => record("verifier_track", agent, async () => {
|
|
2584
|
+
const trackProgress = async (agent, problem, steps, checkpoints, repeats, signal, routedImages = [], phase = "explicit") => record("verifier_track", agent, async (trace) => {
|
|
3305
2585
|
const { verifier, selected } = await engine(agent);
|
|
3306
2586
|
return {
|
|
3307
|
-
result: await verifier.track(problem, steps, checkpoints, repeats, signal, routedImages),
|
|
2587
|
+
result: await verifier.track(problem, steps, checkpoints, repeats, signal, routedImages, trace),
|
|
3308
2588
|
selected
|
|
3309
2589
|
};
|
|
3310
2590
|
}, phase);
|
|
3311
|
-
|
|
2591
|
+
/**
|
|
2592
|
+
* Explicit best-of-N: draft N candidates with the session model, rank them with the
|
|
2593
|
+
* configured judges, then re-measure the winner against the gate's own baseline.
|
|
2594
|
+
*
|
|
2595
|
+
* The tournament's `scores` are relative preference shares (wins/counts) and cannot be
|
|
2596
|
+
* compared with `autoVerifyThreshold`; only the extra winner-vs-baseline comparison produces
|
|
2597
|
+
* an absolute score in the gate's units. That comparison is why this tool exists instead of
|
|
2598
|
+
* "generate candidates yourself and call verifier_select".
|
|
2599
|
+
*
|
|
2600
|
+
* Fail-closed by construction: fewer than {@link MIN_BEST_OF_N} surviving drafts is an error
|
|
2601
|
+
* that names every failure, never a silent "best" picked out of a single survivor.
|
|
2602
|
+
* @param agent - Agent owning the topic, and the session whose model writes the drafts.
|
|
2603
|
+
* @param task - the request the drafts answer.
|
|
2604
|
+
* @param count - how many drafts to request ({@link MIN_BEST_OF_N}..{@link MAX_BEST_OF_N}).
|
|
2605
|
+
* @param repeats - caller's repeat count, or undefined for the default; used by both the
|
|
2606
|
+
* tournament and the baseline comparison.
|
|
2607
|
+
* @param signal - tool-call abort signal.
|
|
2608
|
+
* @param criteriaInput - caller-supplied criteria, or undefined for the configured rubric.
|
|
2609
|
+
*/
|
|
2610
|
+
const bestOfN = async (agent, task, count, repeats, signal, criteriaInput) => record("verifier_best_of_n", agent, async (trace) => {
|
|
2611
|
+
const { verifier, selected } = await engine(agent);
|
|
2612
|
+
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");
|
|
2613
|
+
const rounds = capped(repeats, 2, MAX_EXPLICIT_REPEATS, "repeats");
|
|
2614
|
+
const rubric = criteriaInput === void 0 ? await configuredCriteria() : {
|
|
2615
|
+
criteria: normalizeCriteria(criteriaInput),
|
|
2616
|
+
source: "explicit"
|
|
2617
|
+
};
|
|
2618
|
+
const planned = (selectComparisonsUpperBound(count) + 1) * rubric.criteria.length * rounds * selected.judges.length;
|
|
2619
|
+
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");
|
|
2620
|
+
const call = agent.session.requestHeader()?.config;
|
|
2621
|
+
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");
|
|
2622
|
+
const target = {
|
|
2623
|
+
provider: call.provider,
|
|
2624
|
+
model: call.model,
|
|
2625
|
+
...call.reasoningEffort === void 0 ? {} : { reasoningEffort: String(call.reasoningEffort) }
|
|
2626
|
+
};
|
|
2627
|
+
const problem = explicitEvidence([task], explicitItemChars(selected), explicitBudget(selected), "task")[0];
|
|
2628
|
+
const attempts = await Promise.all(Array.from({ length: count }, async (_unused, index) => {
|
|
2629
|
+
const prompt = buildGenerationPrompt(problem, index, count);
|
|
2630
|
+
try {
|
|
2631
|
+
const completion = await generateCandidate(verifier.client, target, prompt, signal);
|
|
2632
|
+
trace?.({
|
|
2633
|
+
label: "draft " + (index + 1),
|
|
2634
|
+
channel: completion.scoringMode,
|
|
2635
|
+
prompt,
|
|
2636
|
+
output: completion.text
|
|
2637
|
+
});
|
|
2638
|
+
return {
|
|
2639
|
+
ok: true,
|
|
2640
|
+
text: completion.text,
|
|
2641
|
+
usage: completion.usage,
|
|
2642
|
+
truncated: completion.truncated
|
|
2643
|
+
};
|
|
2644
|
+
} catch (error) {
|
|
2645
|
+
return {
|
|
2646
|
+
ok: false,
|
|
2647
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
}));
|
|
2651
|
+
const survivors = [];
|
|
2652
|
+
const failures = [];
|
|
2653
|
+
attempts.forEach((attempt, index) => {
|
|
2654
|
+
if (attempt.ok) survivors.push({
|
|
2655
|
+
attempt: index + 1,
|
|
2656
|
+
text: attempt.text,
|
|
2657
|
+
usage: attempt.usage,
|
|
2658
|
+
truncated: attempt.truncated
|
|
2659
|
+
});
|
|
2660
|
+
else failures.push("draft " + (index + 1) + ": " + attempt.error);
|
|
2661
|
+
});
|
|
2662
|
+
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("; ")));
|
|
2663
|
+
const ranked = await verifier.select({
|
|
2664
|
+
problem,
|
|
2665
|
+
candidates: survivors.map((survivor) => survivor.text),
|
|
2666
|
+
criteria: rubric.criteria,
|
|
2667
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
2668
|
+
repeats: rounds,
|
|
2669
|
+
pivots: Math.min(2, survivors.length),
|
|
2670
|
+
seed: 0,
|
|
2671
|
+
...trace ? { trace } : {}
|
|
2672
|
+
}, signal);
|
|
2673
|
+
const compared = await verifier.compare({
|
|
2674
|
+
problem,
|
|
2675
|
+
candidateA: ranked.best,
|
|
2676
|
+
candidateB: EMPTY_WORK_BASELINE,
|
|
2677
|
+
criteria: rubric.criteria,
|
|
2678
|
+
traceLabelPrefix: "baseline: ",
|
|
2679
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
2680
|
+
repeats: rounds,
|
|
2681
|
+
...trace ? { trace } : {}
|
|
2682
|
+
}, signal);
|
|
2683
|
+
const criteria = compared.criteria.map((row) => ({
|
|
2684
|
+
id: row.id,
|
|
2685
|
+
name: row.name,
|
|
2686
|
+
score: row.scoreA
|
|
2687
|
+
}));
|
|
2688
|
+
const threshold = selected.autoVerifyThreshold;
|
|
2689
|
+
const passesThreshold = sessionAccepted({
|
|
2690
|
+
score: compared.scoreA,
|
|
2691
|
+
winner: compared.winner,
|
|
2692
|
+
criteria
|
|
2693
|
+
}, threshold);
|
|
2694
|
+
const stats = { ...ranked.stats };
|
|
2695
|
+
addUsage(stats, compared.stats);
|
|
2696
|
+
stats.cacheHits += compared.stats.cacheHits;
|
|
2697
|
+
stats.cacheMisses += compared.stats.cacheMisses;
|
|
2698
|
+
stats.topLogprobScores += compared.stats.topLogprobScores;
|
|
2699
|
+
stats.explicitTagScores += compared.stats.explicitTagScores;
|
|
2700
|
+
for (const survivor of survivors) addUsage(stats, survivor.usage);
|
|
2701
|
+
stats.estimatedCostUsd = ((stats.inputTokens + stats.cachedInputTokens) * selected.estimatedInputUsdPerMillion + stats.outputTokens * selected.estimatedOutputUsdPerMillion) / 1e6;
|
|
2702
|
+
return {
|
|
2703
|
+
result: {
|
|
2704
|
+
best: ranked.best,
|
|
2705
|
+
index: ranked.index,
|
|
2706
|
+
/** 1-based draft numbers of the survivors, in `scores`/`ranking` order. */
|
|
2707
|
+
sources: survivors.map((survivor) => survivor.attempt),
|
|
2708
|
+
scores: ranked.scores,
|
|
2709
|
+
ranking: ranked.ranking,
|
|
2710
|
+
comparisons: ranked.comparisons,
|
|
2711
|
+
pivots: ranked.pivots,
|
|
2712
|
+
generated: survivors.length,
|
|
2713
|
+
failed: failures.length,
|
|
2714
|
+
failures,
|
|
2715
|
+
/** 1-based draft numbers still truncated after their escalated retry; empty is the normal case. */
|
|
2716
|
+
truncated: survivors.filter((survivor) => survivor.truncated).map((survivor) => survivor.attempt),
|
|
2717
|
+
score: compared.scoreA,
|
|
2718
|
+
baselineScore: compared.scoreB,
|
|
2719
|
+
winner: compared.winner,
|
|
2720
|
+
criteria: compared.criteria,
|
|
2721
|
+
threshold,
|
|
2722
|
+
passesThreshold,
|
|
2723
|
+
failedCriteria: failedAcceptanceCriteria(criteria, threshold).map((criterion) => criterion.id),
|
|
2724
|
+
calls: stats.calls,
|
|
2725
|
+
stats,
|
|
2726
|
+
generatorProvider: target.provider,
|
|
2727
|
+
generatorModel: target.model,
|
|
2728
|
+
judges: ranked.judges.map((judge, index) => {
|
|
2729
|
+
const baseline = compared.judges[index];
|
|
2730
|
+
if (baseline === void 0) return judge;
|
|
2731
|
+
return {
|
|
2732
|
+
...judge,
|
|
2733
|
+
ok: judge.ok && baseline.ok,
|
|
2734
|
+
calls: judge.calls + baseline.calls,
|
|
2735
|
+
...judge.error === void 0 && baseline.error !== void 0 ? { error: baseline.error } : {}
|
|
2736
|
+
};
|
|
2737
|
+
})
|
|
2738
|
+
},
|
|
2739
|
+
selected
|
|
2740
|
+
};
|
|
2741
|
+
});
|
|
2742
|
+
const classifyRoute = async (agent, prompt, signal, phase) => record("verifier_route_classify", agent, async (trace) => {
|
|
3312
2743
|
const { verifier, selected } = await engine(agent);
|
|
3313
2744
|
const completion = await callVerifierText(verifier.client, prompt, signal);
|
|
2745
|
+
trace?.({
|
|
2746
|
+
label: "route classify",
|
|
2747
|
+
channel: completion.scoringMode,
|
|
2748
|
+
prompt,
|
|
2749
|
+
output: completion.text
|
|
2750
|
+
});
|
|
3314
2751
|
const stats = {
|
|
3315
2752
|
...completion.usage,
|
|
3316
2753
|
cacheHits: 0,
|
|
@@ -3343,8 +2780,10 @@ function apply(ctx, config = {}) {
|
|
|
3343
2780
|
mode: selected.autoVerifyMode,
|
|
3344
2781
|
minConfidence: selected.autoRouteMinConfidence,
|
|
3345
2782
|
maxCandidates: selected.autoRouteMaxCandidates,
|
|
3346
|
-
|
|
3347
|
-
|
|
2783
|
+
maxRoutePerTask: selected.autoRouteMaxPerTask,
|
|
2784
|
+
maxRoutePerSession: selected.autoRouteMaxPerSession,
|
|
2785
|
+
maxFinalPerTask: selected.autoVerifyMaxPerTask,
|
|
2786
|
+
maxFinalPerSession: selected.autoVerifyMaxPerSession,
|
|
3348
2787
|
maxModelCallsPerTask: selected.autoMaxModelCallsPerTask,
|
|
3349
2788
|
maxModelCallsPerSession: selected.autoMaxModelCallsPerSession,
|
|
3350
2789
|
maxInputChars: selected.autoRouteMaxInputChars,
|
|
@@ -3367,7 +2806,7 @@ function apply(ctx, config = {}) {
|
|
|
3367
2806
|
if (!parsed.ok) return rpcFailure(parsed.message);
|
|
3368
2807
|
const query = parsed.query;
|
|
3369
2808
|
try {
|
|
3370
|
-
const headers = (await
|
|
2809
|
+
const headers = (await sessionHeaders()).filter((header) => query.sessionId === void 0 || String(header.id) === query.sessionId);
|
|
3371
2810
|
const settled = await Promise.allSettled(headers.map(async (header) => topic(header).statistics.overview(query)));
|
|
3372
2811
|
const overviews = [];
|
|
3373
2812
|
for (const result of settled) if (result.status === "fulfilled") overviews.push(result.value);
|
|
@@ -3377,6 +2816,93 @@ function apply(ctx, config = {}) {
|
|
|
3377
2816
|
return rpcFailure(error instanceof Error ? error.message : String(error));
|
|
3378
2817
|
}
|
|
3379
2818
|
};
|
|
2819
|
+
/**
|
|
2820
|
+
* One decision snapshot by invocation id, across every topic.
|
|
2821
|
+
*
|
|
2822
|
+
* The dashboard shows merged topics, so the lookup fans out the same way the
|
|
2823
|
+
* statistics query does instead of guessing which topic owns the record.
|
|
2824
|
+
* @param id - invocation id the snapshot was stored under.
|
|
2825
|
+
*/
|
|
2826
|
+
const handleDecisionQuery = async (id) => {
|
|
2827
|
+
if (typeof id !== "string" || id.length === 0) return rpcFailure("decision id must be a non-empty string");
|
|
2828
|
+
try {
|
|
2829
|
+
for (const header of await sessionHeaders()) {
|
|
2830
|
+
const found = await topic(header).decisions.find(id).catch(() => void 0);
|
|
2831
|
+
if (found) return rpcSuccess({ decision: found });
|
|
2832
|
+
}
|
|
2833
|
+
return rpcFailure("decision snapshot not found: it was pruned, never captured, or belongs to a deleted topic");
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
return rpcFailure(error instanceof Error ? error.message : String(error));
|
|
2836
|
+
}
|
|
2837
|
+
};
|
|
2838
|
+
/** Whether a payload asks for a decision snapshot instead of a statistics overview. */
|
|
2839
|
+
const isDecisionQuery = (payload) => typeof payload === "object" && payload !== null && payload.kind === "decision";
|
|
2840
|
+
/**
|
|
2841
|
+
* Judge diagnostics: one real, bounded judge call per configured judge plus the resolved rubric.
|
|
2842
|
+
*
|
|
2843
|
+
* Answers the two questions a user actually has after configuring the plugin: "which scoring
|
|
2844
|
+
* channel is this judge using" (a silent explicit-tag downgrade is invisible otherwise) and
|
|
2845
|
+
* "is the rubric I configured the one in effect" (a broken custom file degrades quietly).
|
|
2846
|
+
* Deliberately NOT recorded in the statistics: it is a diagnostic, not a verification.
|
|
2847
|
+
* @returns Per-judge probe results and the rubric in effect.
|
|
2848
|
+
*/
|
|
2849
|
+
const handleProbe = async () => {
|
|
2850
|
+
try {
|
|
2851
|
+
const header = ctx.agents.currentInitiator()?.session.header ?? (await sessionHeaders())[0];
|
|
2852
|
+
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");
|
|
2853
|
+
const { verifier } = await engineForHeader(header);
|
|
2854
|
+
const rubric = await configuredCriteria();
|
|
2855
|
+
const criterion = rubric.criteria[0];
|
|
2856
|
+
if (criterion === void 0) return rpcFailure("llm-verifier: the configured rubric has no criteria");
|
|
2857
|
+
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.");
|
|
2858
|
+
const judges = [];
|
|
2859
|
+
for (const client of verifier.clients) {
|
|
2860
|
+
const label = client.label ?? client.provider + "/" + client.model;
|
|
2861
|
+
const startedAt = Date.now();
|
|
2862
|
+
client.topLogprobCapabilities.forget(client.provider, client.model);
|
|
2863
|
+
try {
|
|
2864
|
+
const completion = await callVerifier({
|
|
2865
|
+
...client,
|
|
2866
|
+
timeoutMs: Math.min(client.timeoutMs, PROBE_TIMEOUT_MS),
|
|
2867
|
+
maxRetries: 0
|
|
2868
|
+
}, prompt);
|
|
2869
|
+
judges.push({
|
|
2870
|
+
label,
|
|
2871
|
+
provider: client.provider,
|
|
2872
|
+
model: client.model,
|
|
2873
|
+
ok: true,
|
|
2874
|
+
channelProbed: true,
|
|
2875
|
+
channel: completion.scoringMode,
|
|
2876
|
+
scoreA: extractScore(completion, "<score_A>"),
|
|
2877
|
+
scoreB: extractScore(completion, "<score_B>"),
|
|
2878
|
+
latencyMs: Date.now() - startedAt,
|
|
2879
|
+
...completion.usage
|
|
2880
|
+
});
|
|
2881
|
+
} catch (error) {
|
|
2882
|
+
judges.push({
|
|
2883
|
+
label,
|
|
2884
|
+
provider: client.provider,
|
|
2885
|
+
model: client.model,
|
|
2886
|
+
ok: false,
|
|
2887
|
+
latencyMs: Date.now() - startedAt,
|
|
2888
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
return rpcSuccess({
|
|
2893
|
+
judges,
|
|
2894
|
+
channelProbed: true,
|
|
2895
|
+
rubric: {
|
|
2896
|
+
source: rubric.source,
|
|
2897
|
+
count: rubric.criteria.length,
|
|
2898
|
+
...rubric.file ? { file: rubric.file } : {},
|
|
2899
|
+
...rubric.error ? { error: rubric.error } : {}
|
|
2900
|
+
}
|
|
2901
|
+
});
|
|
2902
|
+
} catch (error) {
|
|
2903
|
+
return rpcFailure(error instanceof Error ? error.message : String(error));
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
3380
2906
|
const anyConn = services.connection;
|
|
3381
2907
|
if (anyConn && anyConn.fetch && typeof anyConn.fetch.register === "function") try {
|
|
3382
2908
|
anyConn.fetch.register({
|
|
@@ -3393,7 +2919,8 @@ function apply(ctx, config = {}) {
|
|
|
3393
2919
|
const isRpcEnvelope = typeof body === "object" && body !== null && body.type === "client-request" && typeof body.rpcId === "string";
|
|
3394
2920
|
const rpcId = isRpcEnvelope ? body.rpcId : "direct";
|
|
3395
2921
|
const payload = isRpcEnvelope ? body.payload : body;
|
|
3396
|
-
const
|
|
2922
|
+
const isProbe = typeof payload === "object" && payload !== null && payload.kind === "probe";
|
|
2923
|
+
const outcome = isDecisionQuery(payload) ? await handleDecisionQuery(payload.id) : isProbe ? await handleProbe() : await handleStatisticsQuery(payload);
|
|
3397
2924
|
if (isRpcEnvelope) return Response.json({
|
|
3398
2925
|
type: "server-response",
|
|
3399
2926
|
rpcId,
|
|
@@ -3408,6 +2935,8 @@ function apply(ctx, config = {}) {
|
|
|
3408
2935
|
const legacyRpc = services.connection;
|
|
3409
2936
|
if (typeof legacyRpc.rpc?.handle === "function") try {
|
|
3410
2937
|
legacyRpc.rpc.handle("/llm-verifier", async (endpoint, payload) => {
|
|
2938
|
+
if (endpoint === "decision") return handleDecisionQuery(payload?.id);
|
|
2939
|
+
if (endpoint === "probe") return handleProbe();
|
|
3411
2940
|
if (endpoint !== "statistics") return rpcFailure("unknown llm-verifier endpoint");
|
|
3412
2941
|
return handleStatisticsQuery(payload);
|
|
3413
2942
|
});
|
|
@@ -3545,13 +3074,14 @@ function apply(ctx, config = {}) {
|
|
|
3545
3074
|
}
|
|
3546
3075
|
}
|
|
3547
3076
|
}
|
|
3548
|
-
const
|
|
3549
|
-
|
|
3077
|
+
const finalPreferred = autoRouter.finalPreferred(agent);
|
|
3078
|
+
const structured = finalPreferred ? void 0 : analyzeStructuredRoute(snapshot, selected.autoRouteMaxCandidates, selected.autoRouteMaxItemChars, selected.autoRouteMaxInputChars);
|
|
3079
|
+
let decision = finalPreferred ? void 0 : boundDecision(structured, policy);
|
|
3550
3080
|
if (structured !== void 0 && decision === void 0) {
|
|
3551
3081
|
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
3082
|
await recordSkippedRoute(agent, structured.kind, "structured", "dropped-over-budget");
|
|
3553
3083
|
}
|
|
3554
|
-
if (decision === void 0 && selected.autoRouteSemantic && (selected.autoVerifyMode === "strict" || semanticRouteHint(snapshot))) {
|
|
3084
|
+
if (!finalPreferred && decision === void 0 && selected.autoRouteSemantic && (selected.autoVerifyMode === "strict" || semanticRouteHint(snapshot))) {
|
|
3555
3085
|
const fingerprint = stableHash({
|
|
3556
3086
|
phase: "semantic",
|
|
3557
3087
|
from: evidence.taskStartSeq,
|
|
@@ -3598,8 +3128,9 @@ function apply(ctx, config = {}) {
|
|
|
3598
3128
|
}
|
|
3599
3129
|
if (!stillCurrent()) return;
|
|
3600
3130
|
if (decision) {
|
|
3601
|
-
const repeats = routedRepeats(decision, selected.autoVerifyRepeats);
|
|
3602
|
-
const
|
|
3131
|
+
const repeats = routedRepeats(decision, selected.autoVerifyRepeats, selected.autoTrackRepeats);
|
|
3132
|
+
const rubric = await configuredCriteria();
|
|
3133
|
+
const expectedCalls = estimateRoutedCalls(decision, repeats, rubric.criteria.length) * selected.judges.length;
|
|
3603
3134
|
const reservation = autoRouter.reserve(agent, decision.kind, decision.fingerprint, expectedCalls, policy);
|
|
3604
3135
|
if (reservation === void 0) {
|
|
3605
3136
|
const exhausted = autoRouter.budgetExhausted(agent, expectedCalls, policy);
|
|
@@ -3623,7 +3154,7 @@ function apply(ctx, config = {}) {
|
|
|
3623
3154
|
if (reservation) try {
|
|
3624
3155
|
const extracted = await extractTask(agent, evidence.taskStartSeq, admittedLastSeq, selected.autoVerifyMaxChars, signal);
|
|
3625
3156
|
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");
|
|
3157
|
+
const result = await compareCandidates(agent, extracted.problem, decision.candidates[0].content, decision.candidates[1].content, repeats, signal, rubric, extracted.images, "compare");
|
|
3627
3158
|
if (!stillCurrent()) {
|
|
3628
3159
|
autoRouter.fail(agent, reservation, false);
|
|
3629
3160
|
return;
|
|
@@ -3634,7 +3165,7 @@ function apply(ctx, config = {}) {
|
|
|
3634
3165
|
return;
|
|
3635
3166
|
}
|
|
3636
3167
|
if (decision.kind === "select") {
|
|
3637
|
-
const result = await selectCandidates(agent, extracted.problem, decision.candidates.map((candidate) => candidate.content), repeats, signal, extracted.images, "select");
|
|
3168
|
+
const result = await selectCandidates(agent, extracted.problem, decision.candidates.map((candidate) => candidate.content), repeats, signal, rubric, extracted.images, "select");
|
|
3638
3169
|
if (!stillCurrent()) {
|
|
3639
3170
|
autoRouter.fail(agent, reservation, false);
|
|
3640
3171
|
return;
|
|
@@ -3651,7 +3182,9 @@ function apply(ctx, config = {}) {
|
|
|
3651
3182
|
}
|
|
3652
3183
|
if (!autoRouter.commit(agent, reservation, admittedLastSeq)) return;
|
|
3653
3184
|
const detail = result.scores.map((score, index) => "Checkpoint step " + decision.checkpoints[index] + ": " + (score * 100).toFixed(1) + "%").join("\n");
|
|
3654
|
-
const
|
|
3185
|
+
const completed = (result.scores.length > 0 ? result.scores[result.scores.length - 1] : 0) >= selected.autoTrackCompletionThreshold;
|
|
3186
|
+
if (completed) autoRouter.preferFinal(agent);
|
|
3187
|
+
const continuation = completed ? "\nPrepare final delivery evidence; final session verification is mandatory." : "\nContinue the unfinished work.";
|
|
3655
3188
|
agent.steer(routeFeedback(decision, detail + continuation));
|
|
3656
3189
|
return;
|
|
3657
3190
|
} catch (error) {
|
|
@@ -3691,7 +3224,8 @@ function apply(ctx, config = {}) {
|
|
|
3691
3224
|
from: finalFromSeq,
|
|
3692
3225
|
to: admittedLastSeq
|
|
3693
3226
|
});
|
|
3694
|
-
const
|
|
3227
|
+
const finalRubric = await configuredCriteria();
|
|
3228
|
+
const finalReservation = autoRouter.reserve(agent, "final", finalFingerprint, Math.max(1, finalRubric.criteria.length * selected.autoVerifyFinalRepeats) * selected.judges.length, policy);
|
|
3695
3229
|
if (!finalReservation) {
|
|
3696
3230
|
if (selected.autoVerifyMode === "strict" && (forcedFromSeq !== void 0 || autoRouter.strictBlocked(agent))) if (autoRouter.claimExhaustedNotice(agent)) agent.steer(createUserMessage({
|
|
3697
3231
|
content: [{
|
|
@@ -3713,7 +3247,7 @@ function apply(ctx, config = {}) {
|
|
|
3713
3247
|
includeAssistantText: true,
|
|
3714
3248
|
maxChars: selected.autoVerifyMaxChars,
|
|
3715
3249
|
repeats: selected.autoVerifyFinalRepeats
|
|
3716
|
-
}, signal, "final");
|
|
3250
|
+
}, signal, "final", finalRubric);
|
|
3717
3251
|
if (!stillCurrent()) {
|
|
3718
3252
|
autoRouter.fail(agent, finalReservation, false);
|
|
3719
3253
|
return;
|
|
@@ -3797,6 +3331,7 @@ function apply(ctx, config = {}) {
|
|
|
3797
3331
|
items: criterionResultSchema,
|
|
3798
3332
|
required: true
|
|
3799
3333
|
},
|
|
3334
|
+
identical: { type: "boolean" },
|
|
3800
3335
|
agreement: {
|
|
3801
3336
|
type: "number",
|
|
3802
3337
|
required: true
|
|
@@ -3826,21 +3361,27 @@ function apply(ctx, config = {}) {
|
|
|
3826
3361
|
async execute(args, exec) {
|
|
3827
3362
|
requireEnabled();
|
|
3828
3363
|
const agent = requireAgent(exec.agent);
|
|
3829
|
-
return record("verifier_compare", agent, async () => {
|
|
3364
|
+
return record("verifier_compare", agent, async (trace) => {
|
|
3830
3365
|
const { verifier, selected } = await engine(agent);
|
|
3831
3366
|
const [problem, candidateA, candidateB] = explicitEvidence([
|
|
3832
3367
|
args.problem,
|
|
3833
3368
|
args.candidate_a,
|
|
3834
3369
|
args.candidate_b
|
|
3835
3370
|
], explicitItemChars(selected), explicitBudget(selected), "compare input");
|
|
3371
|
+
const rubric = args.criteria === void 0 ? await configuredCriteria() : {
|
|
3372
|
+
criteria: normalizeCriteria(args.criteria),
|
|
3373
|
+
source: "explicit"
|
|
3374
|
+
};
|
|
3836
3375
|
return {
|
|
3837
3376
|
result: await verifier.compare({
|
|
3838
3377
|
problem,
|
|
3839
3378
|
candidateA,
|
|
3840
3379
|
candidateB,
|
|
3841
|
-
criteria:
|
|
3380
|
+
criteria: rubric.criteria,
|
|
3381
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
3842
3382
|
repeats: capped(args.repeats, 2, MAX_EXPLICIT_REPEATS, "repeats"),
|
|
3843
|
-
images: await images(args.images, exec.signal)
|
|
3383
|
+
images: await images(args.images, exec.signal),
|
|
3384
|
+
...trace ? { trace } : {}
|
|
3844
3385
|
}, exec.signal),
|
|
3845
3386
|
selected
|
|
3846
3387
|
};
|
|
@@ -3849,7 +3390,7 @@ function apply(ctx, config = {}) {
|
|
|
3849
3390
|
}));
|
|
3850
3391
|
ctx.tools.register(defineTool({
|
|
3851
3392
|
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
|
|
3393
|
+
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
3394
|
parameters: {
|
|
3854
3395
|
problem: {
|
|
3855
3396
|
type: "string",
|
|
@@ -3877,6 +3418,7 @@ function apply(ctx, config = {}) {
|
|
|
3877
3418
|
type: "string",
|
|
3878
3419
|
required: true
|
|
3879
3420
|
},
|
|
3421
|
+
identical: { type: "boolean" },
|
|
3880
3422
|
scores: {
|
|
3881
3423
|
type: "array",
|
|
3882
3424
|
items: { type: "number" },
|
|
@@ -3921,12 +3463,16 @@ function apply(ctx, config = {}) {
|
|
|
3921
3463
|
async execute(args, exec) {
|
|
3922
3464
|
requireEnabled();
|
|
3923
3465
|
const agent = requireAgent(exec.agent);
|
|
3924
|
-
return record("verifier_select", agent, async () => {
|
|
3466
|
+
return record("verifier_select", agent, async (trace) => {
|
|
3925
3467
|
const { verifier, selected } = await engine(agent);
|
|
3926
3468
|
const limit = explicitCandidateLimit(selected);
|
|
3927
3469
|
if (args.candidates.length > limit) throw new Error("llm-verifier: candidates must contain at most " + limit + " entries");
|
|
3928
3470
|
const candidates = explicitEvidence(args.candidates, explicitItemChars(selected), explicitBudget(selected), "candidates");
|
|
3929
|
-
const
|
|
3471
|
+
const rubric = args.criteria === void 0 ? await configuredCriteria() : {
|
|
3472
|
+
criteria: normalizeCriteria(args.criteria),
|
|
3473
|
+
source: "explicit"
|
|
3474
|
+
};
|
|
3475
|
+
const criteria = rubric.criteria;
|
|
3930
3476
|
const repeats = capped(args.repeats, 2, MAX_EXPLICIT_REPEATS, "repeats");
|
|
3931
3477
|
const planned = plannedComparisons(candidates.length) * (criteria?.length || 3) * repeats;
|
|
3932
3478
|
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 +3481,12 @@ function apply(ctx, config = {}) {
|
|
|
3935
3481
|
problem: sanitizeVerifierText(args.problem, explicitItemChars(selected)),
|
|
3936
3482
|
candidates,
|
|
3937
3483
|
criteria,
|
|
3484
|
+
...rubric.groundTruthNote ? { groundTruthNote: rubric.groundTruthNote } : {},
|
|
3938
3485
|
repeats,
|
|
3939
3486
|
pivots: capped(args.pivots, 2, Math.max(1, candidates.length), "pivots"),
|
|
3940
3487
|
seed: args.seed ?? 0,
|
|
3941
|
-
images: await images(args.images, exec.signal)
|
|
3488
|
+
images: await images(args.images, exec.signal),
|
|
3489
|
+
...trace ? { trace } : {}
|
|
3942
3490
|
}, exec.signal),
|
|
3943
3491
|
selected
|
|
3944
3492
|
};
|
|
@@ -4009,17 +3557,159 @@ function apply(ctx, config = {}) {
|
|
|
4009
3557
|
async execute(args, exec) {
|
|
4010
3558
|
requireEnabled();
|
|
4011
3559
|
const agent = requireAgent(exec.agent);
|
|
4012
|
-
return record("verifier_track", agent, async () => {
|
|
3560
|
+
return record("verifier_track", agent, async (trace) => {
|
|
4013
3561
|
const { verifier, selected } = await engine(agent);
|
|
4014
3562
|
if (args.steps.length > MAX_TRACK_STEPS) throw new Error("llm-verifier: steps must contain at most 32 entries");
|
|
4015
3563
|
const steps = explicitEvidence(args.steps, explicitItemChars(selected), explicitBudget(selected), "steps");
|
|
4016
3564
|
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)),
|
|
3565
|
+
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
3566
|
selected
|
|
4019
3567
|
};
|
|
4020
3568
|
});
|
|
4021
3569
|
}
|
|
4022
3570
|
}));
|
|
3571
|
+
ctx.tools.register(defineTool({
|
|
3572
|
+
name: "verifier_best_of_n",
|
|
3573
|
+
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.",
|
|
3574
|
+
parameters: {
|
|
3575
|
+
task: {
|
|
3576
|
+
type: "string",
|
|
3577
|
+
required: true,
|
|
3578
|
+
description: "The request every draft must answer. Bound to the explicit per-item evidence cap after redaction."
|
|
3579
|
+
},
|
|
3580
|
+
n: {
|
|
3581
|
+
type: "integer",
|
|
3582
|
+
description: "How many independent drafts to generate; between 2 and 4, default 3. Cost grows with n."
|
|
3583
|
+
},
|
|
3584
|
+
criteria: commonParams.criteria,
|
|
3585
|
+
repeats: commonParams.repeats
|
|
3586
|
+
},
|
|
3587
|
+
output: {
|
|
3588
|
+
schema: {
|
|
3589
|
+
type: "object",
|
|
3590
|
+
additionalProperties: false,
|
|
3591
|
+
properties: {
|
|
3592
|
+
best: {
|
|
3593
|
+
type: "string",
|
|
3594
|
+
required: true
|
|
3595
|
+
},
|
|
3596
|
+
index: {
|
|
3597
|
+
type: "integer",
|
|
3598
|
+
required: true
|
|
3599
|
+
},
|
|
3600
|
+
sources: {
|
|
3601
|
+
type: "array",
|
|
3602
|
+
items: { type: "integer" },
|
|
3603
|
+
required: true
|
|
3604
|
+
},
|
|
3605
|
+
scores: {
|
|
3606
|
+
type: "array",
|
|
3607
|
+
items: { type: "number" },
|
|
3608
|
+
required: true
|
|
3609
|
+
},
|
|
3610
|
+
ranking: {
|
|
3611
|
+
type: "array",
|
|
3612
|
+
items: { type: "integer" },
|
|
3613
|
+
required: true
|
|
3614
|
+
},
|
|
3615
|
+
comparisons: {
|
|
3616
|
+
type: "integer",
|
|
3617
|
+
required: true
|
|
3618
|
+
},
|
|
3619
|
+
pivots: {
|
|
3620
|
+
type: "array",
|
|
3621
|
+
items: { type: "integer" },
|
|
3622
|
+
required: true
|
|
3623
|
+
},
|
|
3624
|
+
generated: {
|
|
3625
|
+
type: "integer",
|
|
3626
|
+
required: true
|
|
3627
|
+
},
|
|
3628
|
+
failed: {
|
|
3629
|
+
type: "integer",
|
|
3630
|
+
required: true
|
|
3631
|
+
},
|
|
3632
|
+
failures: {
|
|
3633
|
+
type: "array",
|
|
3634
|
+
items: { type: "string" },
|
|
3635
|
+
required: true
|
|
3636
|
+
},
|
|
3637
|
+
truncated: {
|
|
3638
|
+
type: "array",
|
|
3639
|
+
items: { type: "integer" },
|
|
3640
|
+
required: true
|
|
3641
|
+
},
|
|
3642
|
+
score: {
|
|
3643
|
+
type: "number",
|
|
3644
|
+
required: true
|
|
3645
|
+
},
|
|
3646
|
+
baselineScore: {
|
|
3647
|
+
type: "number",
|
|
3648
|
+
required: true
|
|
3649
|
+
},
|
|
3650
|
+
winner: {
|
|
3651
|
+
type: "string",
|
|
3652
|
+
enum: [
|
|
3653
|
+
"A",
|
|
3654
|
+
"B",
|
|
3655
|
+
"tie"
|
|
3656
|
+
],
|
|
3657
|
+
required: true
|
|
3658
|
+
},
|
|
3659
|
+
criteria: {
|
|
3660
|
+
type: "array",
|
|
3661
|
+
items: criterionResultSchema,
|
|
3662
|
+
required: true
|
|
3663
|
+
},
|
|
3664
|
+
threshold: {
|
|
3665
|
+
type: "number",
|
|
3666
|
+
required: true
|
|
3667
|
+
},
|
|
3668
|
+
passesThreshold: {
|
|
3669
|
+
type: "boolean",
|
|
3670
|
+
required: true
|
|
3671
|
+
},
|
|
3672
|
+
failedCriteria: {
|
|
3673
|
+
type: "array",
|
|
3674
|
+
items: { type: "string" },
|
|
3675
|
+
required: true
|
|
3676
|
+
},
|
|
3677
|
+
calls: {
|
|
3678
|
+
type: "integer",
|
|
3679
|
+
required: true
|
|
3680
|
+
},
|
|
3681
|
+
stats: {
|
|
3682
|
+
...statsSchema,
|
|
3683
|
+
required: true
|
|
3684
|
+
},
|
|
3685
|
+
generatorProvider: {
|
|
3686
|
+
type: "string",
|
|
3687
|
+
required: true
|
|
3688
|
+
},
|
|
3689
|
+
generatorModel: {
|
|
3690
|
+
type: "string",
|
|
3691
|
+
required: true
|
|
3692
|
+
},
|
|
3693
|
+
provider: {
|
|
3694
|
+
type: "string",
|
|
3695
|
+
required: true
|
|
3696
|
+
},
|
|
3697
|
+
model: {
|
|
3698
|
+
type: "string",
|
|
3699
|
+
required: true
|
|
3700
|
+
},
|
|
3701
|
+
judges: judgesSchema
|
|
3702
|
+
}
|
|
3703
|
+
},
|
|
3704
|
+
render: (_args, value) => renderJson(value)
|
|
3705
|
+
},
|
|
3706
|
+
timeoutMs: entry.timeoutMs * 100,
|
|
3707
|
+
async execute(args, exec) {
|
|
3708
|
+
requireEnabled();
|
|
3709
|
+
const agent = requireAgent(exec.agent);
|
|
3710
|
+
return bestOfN(agent, args.task, args.n ?? DEFAULT_BEST_OF_N, args.repeats, exec.signal, args.criteria);
|
|
3711
|
+
}
|
|
3712
|
+
}));
|
|
4023
3713
|
ctx.tools.register(defineTool({
|
|
4024
3714
|
name: "verifier_current_session",
|
|
4025
3715
|
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.",
|
|
@@ -4064,6 +3754,11 @@ function apply(ctx, config = {}) {
|
|
|
4064
3754
|
],
|
|
4065
3755
|
required: true
|
|
4066
3756
|
},
|
|
3757
|
+
criteria: {
|
|
3758
|
+
type: "array",
|
|
3759
|
+
items: acceptanceCriterionResultSchema,
|
|
3760
|
+
required: true
|
|
3761
|
+
},
|
|
4067
3762
|
fromSeq: {
|
|
4068
3763
|
type: "integer",
|
|
4069
3764
|
required: true
|
|
@@ -4117,6 +3812,6 @@ function apply(ctx, config = {}) {
|
|
|
4117
3812
|
}));
|
|
4118
3813
|
}
|
|
4119
3814
|
//#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 };
|
|
3815
|
+
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
3816
|
|
|
4122
3817
|
//# sourceMappingURL=index.js.map
|