dsh-llm-verifier 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,1031 @@
1
+ import { a as TopLogprobCapabilityCache, i as emptyUsage, n as addUsage, r as callVerifier, t as RequestLimiter } from "./caller-CGlgZ-Su.js";
2
+ import { DEFAULT_CRITERIA, DEFAULT_GROUND_TRUTH_NOTE, GRANULARITY, LETTERS, SCALE_DESCRIPTION, accumulatePairs, bradleyTerry, buildPairwisePrompt, buildProgressPrompt, extractProgressScore, extractScore, normalizeScoreLetter, pivotRoundPairs, rankScores, ringCycle, seededRandom, topPivots } from "./core.js";
3
+ import { defineTool } from "@deepseek-ai/dsh-tools";
4
+ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
5
+ import z from "schemastery";
6
+ import { createHash } from "node:crypto";
7
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
8
+ import { dirname, isAbsolute, join, resolve } from "node:path";
9
+ //#region src/config.ts
10
+ const VERIFIER_SETTINGS_NAMESPACE = settingsNamespace("llm-verifier");
11
+ const Config = z.object({
12
+ provider: z.string().default("deepseek-official"),
13
+ model: z.string().default("deepseek-v4-flash"),
14
+ reasoningEffort: z.string(),
15
+ maxTokens: z.number().step(1).min(1).default(32768),
16
+ timeoutMs: z.number().step(1).min(1).default(3e5),
17
+ maxConcurrency: z.number().step(1).min(1).default(8),
18
+ maxRetries: z.number().step(1).min(0).default(3),
19
+ retryBaseDelayMs: z.number().step(1).min(1).default(500),
20
+ cacheDir: z.string().default(".dsh-verifier-cache"),
21
+ cacheMaxEntries: z.number().step(1).min(1).default(1e4),
22
+ estimatedInputUsdPerMillion: z.number().min(0).default(0),
23
+ estimatedOutputUsdPerMillion: z.number().min(0).default(0)
24
+ });
25
+ function resolveConfig(config = {}) {
26
+ const provider = (config.provider ?? "deepseek-official").trim();
27
+ const model = (config.model ?? "deepseek-v4-flash").trim();
28
+ if (!provider) throw new Error("llm-verifier: provider must be non-empty");
29
+ if (!model) throw new Error("llm-verifier: model must be non-empty");
30
+ const values = {
31
+ maxTokens: config.maxTokens ?? 32768,
32
+ timeoutMs: config.timeoutMs ?? 3e5,
33
+ maxConcurrency: config.maxConcurrency ?? 8,
34
+ retryBaseDelayMs: config.retryBaseDelayMs ?? 500,
35
+ cacheMaxEntries: config.cacheMaxEntries ?? 1e4
36
+ };
37
+ for (const [name, value] of Object.entries(values)) if (!Number.isSafeInteger(value) || value <= 0) throw new Error("llm-verifier: " + name + " must be a positive safe integer");
38
+ const maxRetries = config.maxRetries ?? 3;
39
+ if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new Error("llm-verifier: maxRetries must be a non-negative safe integer");
40
+ const cacheDir = (config.cacheDir ?? ".dsh-verifier-cache").trim();
41
+ if (!cacheDir) throw new Error("llm-verifier: cacheDir must be non-empty");
42
+ const estimatedInputUsdPerMillion = config.estimatedInputUsdPerMillion ?? 0;
43
+ const estimatedOutputUsdPerMillion = config.estimatedOutputUsdPerMillion ?? 0;
44
+ if (![estimatedInputUsdPerMillion, estimatedOutputUsdPerMillion].every((value) => Number.isFinite(value) && value >= 0)) throw new Error("llm-verifier: estimated token prices must be finite non-negative numbers");
45
+ const reasoningEffort = config.reasoningEffort?.trim();
46
+ return {
47
+ provider,
48
+ model,
49
+ ...reasoningEffort ? { reasoningEffort } : {},
50
+ maxRetries,
51
+ cacheDir,
52
+ estimatedInputUsdPerMillion,
53
+ estimatedOutputUsdPerMillion,
54
+ ...values
55
+ };
56
+ }
57
+ function installVerifierSettings(ctx, entry, onChange) {
58
+ let source = () => entry;
59
+ installSettingsSection(ctx, VERIFIER_SETTINGS_NAMESPACE, Config, entry, {
60
+ setSource(current) {
61
+ source = current;
62
+ },
63
+ onChange,
64
+ validate(value) {
65
+ resolveConfig(value);
66
+ }
67
+ });
68
+ return () => resolveConfig(source());
69
+ }
70
+ //#endregion
71
+ //#region src/cache.ts
72
+ function stableHash(value) {
73
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
74
+ }
75
+ function resolveCacheFile(cacheDir, cwd = process.cwd()) {
76
+ return join(isAbsolute(cacheDir) ? cacheDir : resolve(cwd, cacheDir), "scores-v1.json");
77
+ }
78
+ var ScoreCache = class {
79
+ file;
80
+ maxEntries;
81
+ loaded = false;
82
+ entries = /* @__PURE__ */ new Map();
83
+ inflight = /* @__PURE__ */ new Map();
84
+ writing = Promise.resolve();
85
+ constructor(file, maxEntries) {
86
+ this.file = file;
87
+ this.maxEntries = maxEntries;
88
+ }
89
+ async load() {
90
+ if (this.loaded) return;
91
+ this.loaded = true;
92
+ try {
93
+ const document = JSON.parse(await readFile(this.file, "utf8"));
94
+ if (document.version !== 1 || typeof document.entries !== "object" || document.entries === null) return;
95
+ this.entries = new Map(Object.entries(document.entries).map(([key, value]) => [key, {
96
+ ...value,
97
+ scoringMode: value.scoringMode ?? "explicit-tag"
98
+ }]));
99
+ } catch (error) {
100
+ if (error.code !== "ENOENT") throw error;
101
+ }
102
+ }
103
+ async getOrCreate(key, create) {
104
+ await this.load();
105
+ const cached = this.entries.get(key);
106
+ if (cached !== void 0) return {
107
+ value: cached,
108
+ hit: true
109
+ };
110
+ const existing = this.inflight.get(key);
111
+ if (existing !== void 0) return {
112
+ value: await existing,
113
+ hit: true
114
+ };
115
+ const pending = create();
116
+ this.inflight.set(key, pending);
117
+ try {
118
+ const value = await pending;
119
+ this.entries.set(key, value);
120
+ this.trim();
121
+ await this.persist();
122
+ return {
123
+ value,
124
+ hit: false
125
+ };
126
+ } finally {
127
+ this.inflight.delete(key);
128
+ }
129
+ }
130
+ trim() {
131
+ if (this.entries.size <= this.maxEntries) return;
132
+ const sorted = [...this.entries].sort((a, b) => a[1].createdAt - b[1].createdAt);
133
+ for (let index = 0; index < sorted.length - this.maxEntries; index += 1) this.entries.delete(sorted[index][0]);
134
+ }
135
+ async persist() {
136
+ const snapshot = {
137
+ version: 1,
138
+ entries: Object.fromEntries(this.entries)
139
+ };
140
+ this.writing = this.writing.then(async () => {
141
+ await mkdir(dirname(this.file), { recursive: true });
142
+ const temporary = this.file + ".tmp-" + process.pid;
143
+ await writeFile(temporary, JSON.stringify(snapshot), "utf8");
144
+ try {
145
+ await rename(temporary, this.file);
146
+ } catch (error) {
147
+ await unlink(temporary).catch(() => {});
148
+ throw error;
149
+ }
150
+ });
151
+ await this.writing;
152
+ }
153
+ };
154
+ //#endregion
155
+ //#region src/engine.ts
156
+ function average(values) {
157
+ return values.reduce((sum, value) => sum + value, 0) / (values.length || 1);
158
+ }
159
+ function blankStats() {
160
+ return {
161
+ ...emptyUsage(),
162
+ cacheHits: 0,
163
+ cacheMisses: 0,
164
+ estimatedCostUsd: 0,
165
+ topLogprobScores: 0,
166
+ explicitTagScores: 0
167
+ };
168
+ }
169
+ var VerifierEngine = class {
170
+ client;
171
+ maxConcurrency;
172
+ cache;
173
+ inputPrice;
174
+ outputPrice;
175
+ constructor(client, maxConcurrency = 8, cache, prices = {
176
+ input: 0,
177
+ output: 0
178
+ }) {
179
+ this.client = client;
180
+ this.maxConcurrency = maxConcurrency;
181
+ this.cache = cache;
182
+ this.inputPrice = prices.input;
183
+ this.outputPrice = prices.output;
184
+ }
185
+ finishStats(stats) {
186
+ stats.estimatedCostUsd = ((stats.inputTokens + stats.cachedInputTokens) * this.inputPrice + stats.outputTokens * this.outputPrice) / 1e6;
187
+ return stats;
188
+ }
189
+ async scoreOne(options, candidateA, candidateB, criterion, repeat, signal) {
190
+ const ground = options.groundTruthNote ?? "**IMPORTANT:** Focus on observed tool and terminal output as ground truth. Do NOT trust the agent's self-assessment or claims of success.";
191
+ const prompt = buildPairwisePrompt(options.problem, candidateA, candidateB, criterion, ground);
192
+ const imageKey = options.images?.map((image) => stableHash([image.mediaType, Buffer.from(image.data).toString("base64")]));
193
+ const key = stableHash({
194
+ version: 2,
195
+ scoringPolicy: "auto-top-logprobs",
196
+ provider: this.client.provider,
197
+ model: this.client.model,
198
+ effort: this.client.reasoningEffort,
199
+ maxTokens: this.client.maxTokens,
200
+ problem: options.problem,
201
+ candidateA,
202
+ candidateB,
203
+ criterion,
204
+ ground,
205
+ repeat,
206
+ imageKey
207
+ });
208
+ const create = async () => {
209
+ const completion = await callVerifier(this.client, prompt, signal, options.images);
210
+ return {
211
+ scoreA: extractScore(completion, "<score_A>"),
212
+ scoreB: extractScore(completion, "<score_B>"),
213
+ usage: completion.usage,
214
+ scoringMode: completion.scoringMode,
215
+ createdAt: Date.now()
216
+ };
217
+ };
218
+ if (this.cache === void 0) {
219
+ const value = await create();
220
+ return {
221
+ scores: [value.scoreA, value.scoreB],
222
+ usage: value.usage,
223
+ scoringMode: value.scoringMode,
224
+ hit: false
225
+ };
226
+ }
227
+ const cached = await this.cache.getOrCreate(key, create);
228
+ return {
229
+ scores: [cached.value.scoreA, cached.value.scoreB],
230
+ usage: cached.hit ? emptyUsage() : cached.value.usage,
231
+ scoringMode: cached.value.scoringMode,
232
+ hit: cached.hit
233
+ };
234
+ }
235
+ async mapLimited(items, worker) {
236
+ const results = new Array(items.length);
237
+ let cursor = 0;
238
+ const runners = Array.from({ length: Math.min(this.maxConcurrency, items.length) }, async () => {
239
+ while (cursor < items.length) {
240
+ const index = cursor++;
241
+ results[index] = await worker(items[index]);
242
+ }
243
+ });
244
+ await Promise.all(runners);
245
+ return results;
246
+ }
247
+ async compare(options, signal) {
248
+ const criteria = options.criteria?.length ? options.criteria : DEFAULT_CRITERIA;
249
+ const repeats = options.repeats ?? 2;
250
+ const jobs = criteria.flatMap((criterion) => Array.from({ length: repeats }, (_, repeat) => ({
251
+ criterion,
252
+ repeat
253
+ })));
254
+ const warm = jobs.slice(0, 1);
255
+ const rest = jobs.slice(1);
256
+ const run = async (batch) => this.mapLimited(batch, async ({ criterion, repeat }) => {
257
+ const swapped = repeat % 2 === 1;
258
+ const result = await this.scoreOne(options, swapped ? options.candidateB : options.candidateA, swapped ? options.candidateA : options.candidateB, criterion, repeat, signal);
259
+ return {
260
+ criterion,
261
+ scoreA: swapped ? result.scores[1] : result.scores[0],
262
+ scoreB: swapped ? result.scores[0] : result.scores[1],
263
+ usage: result.usage,
264
+ scoringMode: result.scoringMode,
265
+ hit: result.hit
266
+ };
267
+ });
268
+ const values = [...await run(warm), ...await run(rest)];
269
+ const stats = blankStats();
270
+ for (const value of values) {
271
+ addUsage(stats, value.usage);
272
+ value.hit ? stats.cacheHits++ : stats.cacheMisses++;
273
+ value.scoringMode === "top-logprobs" ? stats.topLogprobScores++ : stats.explicitTagScores++;
274
+ }
275
+ const byCriterion = criteria.map((criterion) => {
276
+ const rows = values.filter((value) => value.criterion.id === criterion.id);
277
+ return {
278
+ id: criterion.id,
279
+ name: criterion.name,
280
+ scoreA: average(rows.map((row) => row.scoreA)),
281
+ scoreB: average(rows.map((row) => row.scoreB))
282
+ };
283
+ });
284
+ const scoreA = average(byCriterion.map((value) => value.scoreA));
285
+ const scoreB = average(byCriterion.map((value) => value.scoreB));
286
+ return {
287
+ scoreA,
288
+ scoreB,
289
+ winner: Math.abs(scoreA - scoreB) < 1e-12 ? "tie" : scoreA > scoreB ? "A" : "B",
290
+ criteria: byCriterion,
291
+ calls: stats.calls,
292
+ stats: this.finishStats(stats)
293
+ };
294
+ }
295
+ async scorePairs(options, pairs, signal) {
296
+ const unique = [...new Map(pairs.map((pair) => [pair[0] + "," + pair[1], pair])).values()];
297
+ const values = await this.mapLimited(unique, async ([a, b]) => ({
298
+ a,
299
+ b,
300
+ result: await this.compare({
301
+ problem: options.problem,
302
+ candidateA: options.candidates[a],
303
+ candidateB: options.candidates[b],
304
+ criteria: options.criteria,
305
+ groundTruthNote: options.groundTruthNote,
306
+ repeats: options.repeats,
307
+ images: options.images
308
+ }, signal)
309
+ }));
310
+ const rewards = /* @__PURE__ */ new Map();
311
+ const stats = blankStats();
312
+ for (const value of values) {
313
+ rewards.set(value.a + "," + value.b, [value.result.scoreA, value.result.scoreB]);
314
+ addUsage(stats, value.result.stats);
315
+ stats.cacheHits += value.result.stats.cacheHits;
316
+ stats.cacheMisses += value.result.stats.cacheMisses;
317
+ stats.topLogprobScores += value.result.stats.topLogprobScores;
318
+ stats.explicitTagScores += value.result.stats.explicitTagScores;
319
+ }
320
+ return {
321
+ rewards,
322
+ stats: this.finishStats(stats)
323
+ };
324
+ }
325
+ async track(problem, steps, checkpoints, repeats = 2, signal, images) {
326
+ if (!steps.length || !checkpoints.length) throw new Error("llm-verifier: steps and checkpoints must not be empty");
327
+ 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");
328
+ const prompt = buildProgressPrompt(problem, steps, checkpoints);
329
+ const completions = await this.mapLimited(Array.from({ length: repeats }, (_, index) => index), async () => callVerifier(this.client, prompt, signal, images));
330
+ const stats = blankStats();
331
+ for (const completion of completions) {
332
+ addUsage(stats, completion.usage);
333
+ completion.scoringMode === "top-logprobs" ? stats.topLogprobScores++ : stats.explicitTagScores++;
334
+ }
335
+ const runs = completions.map((completion) => checkpoints.map((_, index) => extractProgressScore(completion, "<c" + (index + 1) + ">")));
336
+ return {
337
+ scores: checkpoints.map((_, index) => average(runs.map((run) => run[index]))),
338
+ perRepeat: runs,
339
+ calls: stats.calls,
340
+ stats: this.finishStats(stats)
341
+ };
342
+ }
343
+ async select(options, signal) {
344
+ if (!options.candidates.length) throw new Error("llm-verifier: candidates must not be empty");
345
+ if (options.candidates.length === 1) return {
346
+ index: 0,
347
+ best: options.candidates[0],
348
+ scores: [1],
349
+ ranking: [0],
350
+ pivots: [0],
351
+ comparisons: 0,
352
+ calls: 0,
353
+ stats: blankStats()
354
+ };
355
+ const ring = ringCycle(options.candidates.length, options.seed ?? 0);
356
+ const ringScores = await this.scorePairs(options, ring, signal);
357
+ const firstWins = new Array(options.candidates.length).fill(0);
358
+ const firstCounts = new Array(options.candidates.length).fill(0);
359
+ accumulatePairs(ring, ringScores.rewards, firstWins, firstCounts);
360
+ const pivots = topPivots(firstWins, firstCounts, options.pivots ?? 2);
361
+ const rounds = pivotRoundPairs(options.candidates.length, pivots);
362
+ const roundScores = await this.scorePairs(options, rounds, signal);
363
+ const allRewards = new Map([...ringScores.rewards, ...roundScores.rewards]);
364
+ const wins = new Array(options.candidates.length).fill(0);
365
+ const counts = new Array(options.candidates.length).fill(0);
366
+ accumulatePairs(ring, allRewards, wins, counts);
367
+ accumulatePairs(rounds, allRewards, wins, counts);
368
+ const ranked = rankScores(wins, counts);
369
+ const index = ranked[0].index;
370
+ const stats = blankStats();
371
+ for (const source of [ringScores.stats, roundScores.stats]) {
372
+ addUsage(stats, source);
373
+ stats.cacheHits += source.cacheHits;
374
+ stats.cacheMisses += source.cacheMisses;
375
+ stats.topLogprobScores += source.topLogprobScores;
376
+ stats.explicitTagScores += source.explicitTagScores;
377
+ }
378
+ return {
379
+ index,
380
+ best: options.candidates[index],
381
+ scores: Array.from({ length: options.candidates.length }, (_, candidate) => wins[candidate] / (counts[candidate] || 1)),
382
+ ranking: ranked.map((value) => value.index),
383
+ pivots,
384
+ comparisons: ring.length + rounds.length,
385
+ calls: stats.calls,
386
+ stats: this.finishStats(stats)
387
+ };
388
+ }
389
+ };
390
+ function normalizeCriteria(input) {
391
+ if (input === void 0) return DEFAULT_CRITERIA;
392
+ if (!Array.isArray(input) || !input.length) throw new Error("llm-verifier: criteria must be a non-empty array");
393
+ return input.map((value, index) => {
394
+ if (typeof value !== "object" || value === null) throw new Error("llm-verifier: criteria[" + index + "] must be an object");
395
+ const row = value;
396
+ for (const key of [
397
+ "id",
398
+ "name",
399
+ "description"
400
+ ]) if (typeof row[key] !== "string" || row[key].trim().length === 0) throw new Error("llm-verifier: criteria[" + index + "]." + key + " must be non-empty");
401
+ return {
402
+ id: String(row.id),
403
+ name: String(row.name),
404
+ description: String(row.description)
405
+ };
406
+ });
407
+ }
408
+ //#endregion
409
+ //#region src/images.ts
410
+ const TYPES = /* @__PURE__ */ new Set([
411
+ "image/png",
412
+ "image/jpeg",
413
+ "image/webp",
414
+ "image/gif"
415
+ ]);
416
+ const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
417
+ function parseDataUrl(value) {
418
+ const match = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/=\s]+)$/i.exec(value);
419
+ if (!match) return void 0;
420
+ const data = Buffer.from(match[2].replace(/\s/g, ""), "base64");
421
+ if (data.byteLength > MAX_IMAGE_BYTES) throw new Error("llm-verifier: image exceeds 20 MiB");
422
+ return {
423
+ mediaType: match[1].toLowerCase(),
424
+ data
425
+ };
426
+ }
427
+ async function loadVerifierImages(inputs, signal) {
428
+ const images = [];
429
+ for (const input of inputs ?? []) {
430
+ const data = parseDataUrl(input);
431
+ if (data !== void 0) {
432
+ images.push(data);
433
+ continue;
434
+ }
435
+ let url;
436
+ try {
437
+ url = new URL(input);
438
+ } catch {
439
+ throw new Error("llm-verifier: images accept only HTTPS URLs or data:image/...;base64 URLs");
440
+ }
441
+ if (url.protocol !== "https:") throw new Error("llm-verifier: remote images must use HTTPS");
442
+ const response = await fetch(url, {
443
+ redirect: "error",
444
+ signal
445
+ });
446
+ if (!response.ok) throw new Error("llm-verifier: image fetch returned HTTP " + response.status);
447
+ const type = (response.headers.get("content-type") ?? "").split(";")[0].toLowerCase();
448
+ if (!TYPES.has(type)) throw new Error("llm-verifier: unsupported image media type " + type);
449
+ if (Number(response.headers.get("content-length") ?? 0) > MAX_IMAGE_BYTES) throw new Error("llm-verifier: image exceeds 20 MiB");
450
+ const bytes = new Uint8Array(await response.arrayBuffer());
451
+ if (bytes.byteLength > MAX_IMAGE_BYTES) throw new Error("llm-verifier: image exceeds 20 MiB");
452
+ images.push({
453
+ mediaType: type,
454
+ data: bytes
455
+ });
456
+ }
457
+ return images;
458
+ }
459
+ //#endregion
460
+ //#region src/session.ts
461
+ function textOf(blocks) {
462
+ const parts = [];
463
+ for (const block of blocks) if (block.type === "text") parts.push(block.text);
464
+ else if (block.type === "reasoning") parts.push("[Reasoning] " + block.text);
465
+ else if (block.type === "tool-call") parts.push("[Tool Call] " + block.name + " " + block.arguments);
466
+ else if (block.type === "tool-result") parts.push("[Tool Result] " + textOf(block.content));
467
+ return parts.join("\n");
468
+ }
469
+ function redact(text, patterns) {
470
+ let result = text;
471
+ for (const pattern of patterns) {
472
+ let regex;
473
+ try {
474
+ regex = new RegExp(pattern, "giu");
475
+ } catch {
476
+ throw new Error("llm-verifier: invalid redact pattern: " + pattern);
477
+ }
478
+ result = result.replace(regex, "[REDACTED]");
479
+ }
480
+ return result;
481
+ }
482
+ async function extractSession(agent, loadImage, options = {}) {
483
+ const all = agent.session.events;
484
+ const from = options.fromSeq ?? 0;
485
+ const to = options.toSeq ?? Number.MAX_SAFE_INTEGER;
486
+ const events = all.filter((event) => event.seq >= from && event.seq <= to);
487
+ const patterns = [...["Bearers+[A-Za-z0-9._~+/=-]+", "(?:api[_-]?key|token|password|secret)s*[:=]s*[^s,;]+"], ...options.redactPatterns ?? []];
488
+ let problem = "";
489
+ const trace = [];
490
+ const images = [];
491
+ for (const event of events) if (event.type === "user/message") {
492
+ if (event.data.source.kind !== "user") continue;
493
+ const text = textOf(event.data.content);
494
+ if (!problem && text.trim()) problem = text.trim();
495
+ for (const block of event.data.content) if (block.type === "image") images.push(await loadImage(block.attachment));
496
+ trace.push("--- User seq " + event.seq + " ---\n" + text);
497
+ } 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));
498
+ 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);
499
+ 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));
500
+ const raw = redact(trace.join("\n\n"), patterns);
501
+ const maxChars = options.maxChars ?? 2e5;
502
+ const omittedCharacters = Math.max(0, raw.length - maxChars);
503
+ const bounded = omittedCharacters ? "[Earlier trace truncated: " + omittedCharacters + " characters omitted]\n" + raw.slice(-maxChars) : raw;
504
+ return {
505
+ problem: redact(problem, patterns),
506
+ trace: bounded,
507
+ images,
508
+ sessionId: String(agent.id),
509
+ fromSeq: events[0]?.seq ?? from,
510
+ toSeq: events.at(-1)?.seq ?? from,
511
+ omittedCharacters
512
+ };
513
+ }
514
+ //#endregion
515
+ //#region src/index.ts
516
+ const name = "llm-verifier";
517
+ const inject = [
518
+ "tools",
519
+ "agents",
520
+ "attachments",
521
+ "llm"
522
+ ];
523
+ const criterionSchema = {
524
+ type: "object",
525
+ additionalProperties: false,
526
+ properties: {
527
+ id: {
528
+ type: "string",
529
+ required: true
530
+ },
531
+ name: {
532
+ type: "string",
533
+ required: true
534
+ },
535
+ description: {
536
+ type: "string",
537
+ required: true
538
+ }
539
+ }
540
+ };
541
+ const statsSchema = {
542
+ type: "object",
543
+ additionalProperties: false,
544
+ properties: {
545
+ calls: {
546
+ type: "integer",
547
+ required: true
548
+ },
549
+ attempts: {
550
+ type: "integer",
551
+ required: true
552
+ },
553
+ retries: {
554
+ type: "integer",
555
+ required: true
556
+ },
557
+ inputTokens: {
558
+ type: "integer",
559
+ required: true
560
+ },
561
+ cachedInputTokens: {
562
+ type: "integer",
563
+ required: true
564
+ },
565
+ outputTokens: {
566
+ type: "integer",
567
+ required: true
568
+ },
569
+ reasoningTokens: {
570
+ type: "integer",
571
+ required: true
572
+ },
573
+ cacheHits: {
574
+ type: "integer",
575
+ required: true
576
+ },
577
+ cacheMisses: {
578
+ type: "integer",
579
+ required: true
580
+ },
581
+ estimatedCostUsd: {
582
+ type: "number",
583
+ required: true
584
+ },
585
+ topLogprobScores: {
586
+ type: "integer",
587
+ required: true
588
+ },
589
+ explicitTagScores: {
590
+ type: "integer",
591
+ required: true
592
+ }
593
+ }
594
+ };
595
+ const criterionResultSchema = {
596
+ type: "object",
597
+ additionalProperties: false,
598
+ properties: {
599
+ id: {
600
+ type: "string",
601
+ required: true
602
+ },
603
+ name: {
604
+ type: "string",
605
+ required: true
606
+ },
607
+ scoreA: {
608
+ type: "number",
609
+ required: true
610
+ },
611
+ scoreB: {
612
+ type: "number",
613
+ required: true
614
+ }
615
+ }
616
+ };
617
+ const commonParams = {
618
+ criteria: {
619
+ type: "array",
620
+ items: criterionSchema
621
+ },
622
+ repeats: { type: "integer" },
623
+ images: {
624
+ type: "array",
625
+ items: { type: "string" },
626
+ description: "Optional HTTPS or data:image/...;base64 images. The selected DSH model must accept image input."
627
+ }
628
+ };
629
+ function renderJson(value) {
630
+ return [{
631
+ type: "text",
632
+ text: JSON.stringify(value, null, 2)
633
+ }];
634
+ }
635
+ function positive(value, fallback, field) {
636
+ const result = value ?? fallback;
637
+ if (!Number.isSafeInteger(result) || result <= 0) throw new Error("llm-verifier: " + field + " must be a positive integer");
638
+ return result;
639
+ }
640
+ function apply(ctx, config = {}) {
641
+ const entry = resolveConfig(config);
642
+ let limiter = new RequestLimiter(entry.maxConcurrency);
643
+ const current = installVerifierSettings(ctx, entry, () => {
644
+ limiter = new RequestLimiter(current().maxConcurrency);
645
+ });
646
+ const cache = new ScoreCache(resolveCacheFile(entry.cacheDir), entry.cacheMaxEntries);
647
+ const topLogprobCapabilities = new TopLogprobCapabilityCache();
648
+ const engine = async () => {
649
+ const selected = current();
650
+ await ctx.llm.resolveCallConfig({
651
+ provider: selected.provider,
652
+ model: selected.model,
653
+ ...selected.reasoningEffort ? { reasoningEffort: selected.reasoningEffort } : {},
654
+ maxTokens: selected.maxTokens
655
+ });
656
+ return {
657
+ verifier: new VerifierEngine({
658
+ ...selected,
659
+ ctx,
660
+ llm: ctx.llm,
661
+ attachments: ctx.attachments,
662
+ topLogprobCapabilities,
663
+ limiter
664
+ }, selected.maxConcurrency, cache, {
665
+ input: selected.estimatedInputUsdPerMillion,
666
+ output: selected.estimatedOutputUsdPerMillion
667
+ }),
668
+ selected
669
+ };
670
+ };
671
+ const images = (values, signal) => loadVerifierImages(values, signal);
672
+ const route = (selected) => ({
673
+ provider: selected.provider,
674
+ model: selected.model
675
+ });
676
+ ctx.tools.register(defineTool({
677
+ name: "verifier_compare",
678
+ description: "Use autonomously when exactly two substantive answers, patches, plans, or execution trajectories need an independent evidence-based comparison and the choice is consequential or uncertain. Do not use for trivial deterministic questions or when there is only one candidate. Uses the verifier model selected in DSH Settings, with top-logprob A–T expectations when supported and explicit-tag fallback otherwise.",
679
+ parameters: {
680
+ problem: {
681
+ type: "string",
682
+ required: true
683
+ },
684
+ candidate_a: {
685
+ type: "string",
686
+ required: true
687
+ },
688
+ candidate_b: {
689
+ type: "string",
690
+ required: true
691
+ },
692
+ ...commonParams
693
+ },
694
+ output: {
695
+ schema: {
696
+ type: "object",
697
+ additionalProperties: false,
698
+ properties: {
699
+ scoreA: {
700
+ type: "number",
701
+ required: true
702
+ },
703
+ scoreB: {
704
+ type: "number",
705
+ required: true
706
+ },
707
+ winner: {
708
+ type: "string",
709
+ enum: [
710
+ "A",
711
+ "B",
712
+ "tie"
713
+ ],
714
+ required: true
715
+ },
716
+ criteria: {
717
+ type: "array",
718
+ items: criterionResultSchema,
719
+ required: true
720
+ },
721
+ calls: {
722
+ type: "integer",
723
+ required: true
724
+ },
725
+ stats: {
726
+ ...statsSchema,
727
+ required: true
728
+ },
729
+ provider: {
730
+ type: "string",
731
+ required: true
732
+ },
733
+ model: {
734
+ type: "string",
735
+ required: true
736
+ }
737
+ }
738
+ },
739
+ render: (_args, value) => renderJson(value)
740
+ },
741
+ timeoutMs: entry.timeoutMs * 20,
742
+ async execute(args, exec) {
743
+ const { verifier, selected } = await engine();
744
+ return {
745
+ ...await verifier.compare({
746
+ problem: args.problem,
747
+ candidateA: args.candidate_a,
748
+ candidateB: args.candidate_b,
749
+ criteria: normalizeCriteria(args.criteria),
750
+ repeats: positive(args.repeats, 2, "repeats"),
751
+ images: await images(args.images, exec.signal)
752
+ }, exec.signal),
753
+ ...route(selected)
754
+ };
755
+ }
756
+ }));
757
+ ctx.tools.register(defineTool({
758
+ name: "verifier_select",
759
+ 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; do not generate extra candidates merely to invoke this tool. Uses the configured DSH verifier model and the O(Nk) Probabilistic Pivot Tournament.",
760
+ parameters: {
761
+ problem: {
762
+ type: "string",
763
+ required: true
764
+ },
765
+ candidates: {
766
+ type: "array",
767
+ items: { type: "string" },
768
+ required: true
769
+ },
770
+ ...commonParams,
771
+ pivots: { type: "integer" },
772
+ seed: { type: "integer" }
773
+ },
774
+ output: {
775
+ schema: {
776
+ type: "object",
777
+ additionalProperties: false,
778
+ properties: {
779
+ index: {
780
+ type: "integer",
781
+ required: true
782
+ },
783
+ best: {
784
+ type: "string",
785
+ required: true
786
+ },
787
+ scores: {
788
+ type: "array",
789
+ items: { type: "number" },
790
+ required: true
791
+ },
792
+ ranking: {
793
+ type: "array",
794
+ items: { type: "integer" },
795
+ required: true
796
+ },
797
+ pivots: {
798
+ type: "array",
799
+ items: { type: "integer" },
800
+ required: true
801
+ },
802
+ comparisons: {
803
+ type: "integer",
804
+ required: true
805
+ },
806
+ calls: {
807
+ type: "integer",
808
+ required: true
809
+ },
810
+ stats: {
811
+ ...statsSchema,
812
+ required: true
813
+ },
814
+ provider: {
815
+ type: "string",
816
+ required: true
817
+ },
818
+ model: {
819
+ type: "string",
820
+ required: true
821
+ }
822
+ }
823
+ },
824
+ render: (_args, value) => renderJson(value)
825
+ },
826
+ timeoutMs: entry.timeoutMs * 100,
827
+ async execute(args, exec) {
828
+ const { verifier, selected } = await engine();
829
+ return {
830
+ ...await verifier.select({
831
+ problem: args.problem,
832
+ candidates: args.candidates,
833
+ criteria: normalizeCriteria(args.criteria),
834
+ repeats: positive(args.repeats, 2, "repeats"),
835
+ pivots: positive(args.pivots, 2, "pivots"),
836
+ seed: args.seed ?? 0,
837
+ images: await images(args.images, exec.signal)
838
+ }, exec.signal),
839
+ ...route(selected)
840
+ };
841
+ }
842
+ }));
843
+ ctx.tools.register(defineTool({
844
+ name: "verifier_track",
845
+ description: "Use autonomously for a genuinely multi-step task when progress at explicit checkpoints is uncertain or needs evidence-based measurement. Do not use for a single completed answer or invent checkpoints that were not supplied by the task history. Trusts observed output rather than narration.",
846
+ parameters: {
847
+ problem: {
848
+ type: "string",
849
+ required: true
850
+ },
851
+ steps: {
852
+ type: "array",
853
+ items: { type: "string" },
854
+ required: true
855
+ },
856
+ checkpoints: {
857
+ type: "array",
858
+ items: { type: "integer" },
859
+ required: true
860
+ },
861
+ repeats: commonParams.repeats,
862
+ images: commonParams.images
863
+ },
864
+ output: {
865
+ schema: {
866
+ type: "object",
867
+ additionalProperties: false,
868
+ properties: {
869
+ scores: {
870
+ type: "array",
871
+ items: { type: "number" },
872
+ required: true
873
+ },
874
+ perRepeat: {
875
+ type: "array",
876
+ items: {
877
+ type: "array",
878
+ items: { type: "number" }
879
+ },
880
+ required: true
881
+ },
882
+ calls: {
883
+ type: "integer",
884
+ required: true
885
+ },
886
+ stats: {
887
+ ...statsSchema,
888
+ required: true
889
+ },
890
+ provider: {
891
+ type: "string",
892
+ required: true
893
+ },
894
+ model: {
895
+ type: "string",
896
+ required: true
897
+ }
898
+ }
899
+ },
900
+ render: (_args, value) => renderJson(value)
901
+ },
902
+ timeoutMs: entry.timeoutMs * 20,
903
+ async execute(args, exec) {
904
+ const { verifier, selected } = await engine();
905
+ return {
906
+ ...await verifier.track(args.problem, args.steps, args.checkpoints, positive(args.repeats, 2, "repeats"), exec.signal, await images(args.images, exec.signal)),
907
+ ...route(selected)
908
+ };
909
+ }
910
+ }));
911
+ ctx.tools.register(defineTool({
912
+ name: "verifier_current_session",
913
+ description: "Use autonomously near the end of a non-trivial coding, debugging, migration, deployment, or operations task when independent completion verification would materially reduce risk and the session contains real tool evidence. Do not use for routine conversation, simple factual answers, or every turn. Extracts the current DSH session, applies secret redaction, bounds and truncation, then sends the evidence to the verifier model selected in Settings.",
914
+ parameters: {
915
+ from_seq: { type: "integer" },
916
+ to_seq: { type: "integer" },
917
+ include_assistant_text: { type: "boolean" },
918
+ redact_patterns: {
919
+ type: "array",
920
+ items: { type: "string" }
921
+ },
922
+ max_chars: { type: "integer" },
923
+ repeats: { type: "integer" }
924
+ },
925
+ output: {
926
+ schema: {
927
+ type: "object",
928
+ additionalProperties: false,
929
+ properties: {
930
+ sessionId: {
931
+ type: "string",
932
+ required: true
933
+ },
934
+ problem: {
935
+ type: "string",
936
+ required: true
937
+ },
938
+ score: {
939
+ type: "number",
940
+ required: true
941
+ },
942
+ baselineScore: {
943
+ type: "number",
944
+ required: true
945
+ },
946
+ winner: {
947
+ type: "string",
948
+ enum: [
949
+ "A",
950
+ "B",
951
+ "tie"
952
+ ],
953
+ required: true
954
+ },
955
+ fromSeq: {
956
+ type: "integer",
957
+ required: true
958
+ },
959
+ toSeq: {
960
+ type: "integer",
961
+ required: true
962
+ },
963
+ omittedCharacters: {
964
+ type: "integer",
965
+ required: true
966
+ },
967
+ calls: {
968
+ type: "integer",
969
+ required: true
970
+ },
971
+ stats: {
972
+ ...statsSchema,
973
+ required: true
974
+ },
975
+ provider: {
976
+ type: "string",
977
+ required: true
978
+ },
979
+ model: {
980
+ type: "string",
981
+ required: true
982
+ }
983
+ }
984
+ },
985
+ render: (_args, value) => renderJson(value)
986
+ },
987
+ timeoutMs: entry.timeoutMs * 20,
988
+ async execute(args, exec) {
989
+ const agent = exec.agent ?? ctx.agents.currentInitiator();
990
+ if (agent === void 0) throw new Error("llm-verifier: verifier_current_session requires an agent-owned tool call");
991
+ const extracted = await extractSession(agent, async (ref) => {
992
+ const stored = await ctx.attachments.readImage(ref, exec.signal);
993
+ return {
994
+ data: stored.data,
995
+ mediaType: stored.ref.mediaType
996
+ };
997
+ }, {
998
+ fromSeq: args.from_seq,
999
+ toSeq: args.to_seq,
1000
+ includeAssistantText: args.include_assistant_text,
1001
+ redactPatterns: args.redact_patterns,
1002
+ maxChars: args.max_chars
1003
+ });
1004
+ const { verifier, selected } = await engine();
1005
+ const result = await verifier.compare({
1006
+ problem: extracted.problem,
1007
+ candidateA: extracted.trace,
1008
+ candidateB: "(No useful work or verification was performed.)",
1009
+ repeats: positive(args.repeats, 2, "repeats"),
1010
+ images: extracted.images
1011
+ }, exec.signal);
1012
+ return {
1013
+ sessionId: extracted.sessionId,
1014
+ problem: extracted.problem,
1015
+ score: result.scoreA,
1016
+ baselineScore: result.scoreB,
1017
+ winner: result.winner,
1018
+ fromSeq: extracted.fromSeq,
1019
+ toSeq: extracted.toSeq,
1020
+ omittedCharacters: extracted.omittedCharacters,
1021
+ calls: result.calls,
1022
+ stats: result.stats,
1023
+ ...route(selected)
1024
+ };
1025
+ }
1026
+ }));
1027
+ }
1028
+ //#endregion
1029
+ export { Config, DEFAULT_CRITERIA, DEFAULT_GROUND_TRUTH_NOTE, GRANULARITY, LETTERS, RequestLimiter, SCALE_DESCRIPTION, ScoreCache, VerifierEngine, accumulatePairs, apply, bradleyTerry, buildPairwisePrompt, buildProgressPrompt, callVerifier, extractProgressScore, extractScore, inject, name, normalizeCriteria, normalizeScoreLetter, pivotRoundPairs, rankScores, resolveCacheFile, ringCycle, seededRandom, stableHash, topPivots };
1030
+
1031
+ //# sourceMappingURL=index.js.map