@deepthonk/core 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -11
- package/dist/artifacts.js +178 -11
- package/dist/artifacts.js.map +1 -1
- package/dist/bradleyTerry.d.ts +1 -0
- package/dist/bradleyTerry.js +27 -2
- package/dist/bradleyTerry.js.map +1 -1
- package/dist/budget.d.ts +7 -1
- package/dist/budget.js +6 -1
- package/dist/budget.js.map +1 -1
- package/dist/budgetTracker.d.ts +6 -1
- package/dist/budgetTracker.js +32 -1
- package/dist/budgetTracker.js.map +1 -1
- package/dist/checkpointStore.d.ts +56 -0
- package/dist/checkpointStore.js +351 -0
- package/dist/checkpointStore.js.map +1 -0
- package/dist/critique.d.ts +3 -1
- package/dist/critique.js +24 -9
- package/dist/critique.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +33 -1
- package/dist/lifecycle.js +207 -6
- package/dist/lifecycle.js.map +1 -1
- package/dist/phaseRunner.d.ts +6 -1
- package/dist/phaseRunner.js +53 -19
- package/dist/phaseRunner.js.map +1 -1
- package/dist/resume.d.ts +10 -3
- package/dist/resume.js +357 -39
- package/dist/resume.js.map +1 -1
- package/dist/runner.d.ts +3 -1
- package/dist/runner.js +894 -246
- package/dist/runner.js.map +1 -1
- package/dist/schemas.d.ts +397 -47
- package/dist/schemas.js +67 -5
- package/dist/schemas.js.map +1 -1
- package/dist/services.d.ts +9 -0
- package/dist/services.js +115 -12
- package/dist/services.js.map +1 -1
- package/dist/traceStore.d.ts +31 -2
- package/dist/traceStore.js +182 -21
- package/dist/traceStore.js.map +1 -1
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { fitBradleyTerry } from "./bradleyTerry.js";
|
|
5
|
+
import { canonicalizeComparisons, fitBradleyTerry } from "./bradleyTerry.js";
|
|
5
6
|
import { BudgetTracker } from "./budgetTracker.js";
|
|
7
|
+
import { readValidatedPhaseReceiptUsage, runCheckpointedPhase, traceSchemaVersion, validatePhaseCommit, validatePhaseCommitIfPresent, writePhaseCommit } from "./checkpointStore.js";
|
|
6
8
|
import { planBudget, validateProfile } from "./budget.js";
|
|
7
9
|
import { BudgetExceededError, CancelledError, ConfigError, DeepThonkError } from "./errors.js";
|
|
8
10
|
import { runArtifactFiles } from "./artifacts.js";
|
|
9
11
|
import { parseJsonObject } from "./json.js";
|
|
10
|
-
import {
|
|
12
|
+
import { claimRunLockOwnership, emptyUsage, releaseRunLock, verifyRunLockClaim } from "./lifecycle.js";
|
|
11
13
|
import { makeKRegularPairs } from "./pairScheduler.js";
|
|
12
|
-
import { runLimitedPhase } from "./phaseRunner.js";
|
|
13
14
|
import { comparePrompt, finalizePrompt, generatePrompt, mutatePrompt } from "./prompts.js";
|
|
14
15
|
import { createRng } from "./rng.js";
|
|
15
|
-
import { assertNoPruneInProgress, buildPopulationMap, buildResumePlan, groupComparisons, persistPrunedTrace, pruneTraceToPlan, readResumeTrace, reconstructScores, replayBudgetUsage, resolveResumeRunId, resumeConfigError, toResumePlanStatus } from "./resume.js";
|
|
16
|
-
import { runConfigSchema, builtInProfiles, phaseCompletedEventSchema } from "./schemas.js";
|
|
16
|
+
import { assertNoPruneInProgress, assertNoLegacyRedactedBudget, applyUsageRecords, buildPopulationMap, buildResumePlan, groupComparisons, persistPrunedTrace, pruneTraceToPlan, readResumeTrace, reconstructScores, replayBudgetUsage, resolveResumeRunId, resumeConfigError, toResumePlanStatus, validateUsageRecords, validatedReceiptUsageIds } from "./resume.js";
|
|
17
|
+
import { runConfigSchema, resolveRunConfigDefaults, builtInProfiles, phaseCompletedEventSchema } from "./schemas.js";
|
|
17
18
|
import { aggregateCritiques } from "./critique.js";
|
|
18
19
|
import { TraceStore } from "./traceStore.js";
|
|
19
20
|
const compareOutputSchema = z.object({
|
|
@@ -26,25 +27,42 @@ const compareOutputSchema = z.object({
|
|
|
26
27
|
selection_reason: z.string().default("")
|
|
27
28
|
});
|
|
28
29
|
export async function runDeepThonk(configInput, driver, control = {}, resumeState) {
|
|
29
|
-
const config = runConfigSchema.parse(configInput);
|
|
30
|
+
const config = resolveRunConfigDefaults(runConfigSchema.parse(configInput));
|
|
30
31
|
validateProfile(config.profile);
|
|
31
32
|
enforceBudget(config);
|
|
32
|
-
const runId = resumeState?.runId ??
|
|
33
|
+
const runId = resumeState?.runId ??
|
|
34
|
+
config.runId ??
|
|
35
|
+
`run_${new Date().toISOString().replace(/[:.]/g, "-")}_${randomUUID()}`;
|
|
33
36
|
const trace = new TraceStore(config.runDir);
|
|
34
37
|
const rng = createRng(config.seed);
|
|
35
38
|
const tracker = resumeState?.tracker ?? new BudgetTracker(config);
|
|
36
39
|
const startedAt = resumeState?.startedAt ?? new Date().toISOString();
|
|
37
40
|
let ownsLock = false;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
let ownedLockClaimId;
|
|
42
|
+
let verifiedPreclaim = false;
|
|
43
|
+
if (!resumeState) {
|
|
44
|
+
if (control.lockClaim) {
|
|
45
|
+
verifiedPreclaim = await verifyRunLockClaim(config.runDir, control.lockClaim, control.jobId);
|
|
46
|
+
if (!verifiedPreclaim) {
|
|
47
|
+
throw new ConfigError("The supplied run lock claim does not own the current run.lock.", {
|
|
48
|
+
code: "run.lock_not_owned",
|
|
49
|
+
retryable: false,
|
|
50
|
+
fix: "Claim the run directory with claimRunLockOwnership and pass that exact claim to the worker."
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const claimed = await claimRunLockOwnership(config.runDir, control.jobId ?? runId);
|
|
56
|
+
if (!claimed) {
|
|
57
|
+
throw new ConfigError(`Run directory is already claimed: ${config.runDir}`, {
|
|
58
|
+
code: "run.directory_locked",
|
|
59
|
+
retryable: false,
|
|
60
|
+
fix: "Wait for the active run to finish, or choose a fresh run directory."
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
ownsLock = true;
|
|
64
|
+
ownedLockClaimId = claimed.claimId;
|
|
46
65
|
}
|
|
47
|
-
ownsLock = true;
|
|
48
66
|
}
|
|
49
67
|
let stopped = false;
|
|
50
68
|
let caughtError;
|
|
@@ -63,7 +81,7 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
63
81
|
});
|
|
64
82
|
};
|
|
65
83
|
const assertNotCancelled = async (phase) => {
|
|
66
|
-
if (await control.shouldCancel?.()) {
|
|
84
|
+
if (control.signal?.aborted || (await control.shouldCancel?.())) {
|
|
67
85
|
throw new CancelledError(`Run cancelled before ${phase}.`, {
|
|
68
86
|
code: "run.cancelled",
|
|
69
87
|
retryable: false,
|
|
@@ -89,6 +107,7 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
89
107
|
throw new BudgetExceededError(`Run already stopped before ${phase}.`, { code: "budget.stopped" });
|
|
90
108
|
await assertNotCancelled(phase);
|
|
91
109
|
await assertBudget(phase);
|
|
110
|
+
return tracker.reserveCall(phase);
|
|
92
111
|
},
|
|
93
112
|
async afterCall(phase) {
|
|
94
113
|
await assertNotCancelled(phase);
|
|
@@ -97,7 +116,7 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
97
116
|
};
|
|
98
117
|
try {
|
|
99
118
|
if (!resumeState) {
|
|
100
|
-
await trace.init({ ...config, version: await currentPackageVersion() }, runId);
|
|
119
|
+
await trace.init({ ...config, version: await currentPackageVersion() }, runId, { allowOwnedLock: ownsLock || verifiedPreclaim, pendingJobId: verifiedPreclaim ? control.jobId : undefined });
|
|
101
120
|
}
|
|
102
121
|
else {
|
|
103
122
|
await trace.event({
|
|
@@ -115,9 +134,13 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
115
134
|
}
|
|
116
135
|
else {
|
|
117
136
|
await assertNotCancelled("initial population");
|
|
118
|
-
population = await generateInitialPopulation(config, driver, trace, rng, runId, tracker, guards);
|
|
137
|
+
population = await generateInitialPopulation(config, driver, trace, rng, runId, tracker, guards, control.signal);
|
|
119
138
|
await trace.writePopulation(0, population);
|
|
120
139
|
await assertBudget("initial population");
|
|
140
|
+
await writePhaseCommit(config.runDir, "initial_generation", {
|
|
141
|
+
population,
|
|
142
|
+
usage: await phaseUsageRows(trace, "initial_generation")
|
|
143
|
+
});
|
|
121
144
|
await markPhaseCompleted(trace, "initial_generation");
|
|
122
145
|
await writeStatus("running", "population_completed", { generation: 0 });
|
|
123
146
|
}
|
|
@@ -147,11 +170,17 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
147
170
|
pairs,
|
|
148
171
|
population,
|
|
149
172
|
tracker,
|
|
150
|
-
guards
|
|
173
|
+
guards,
|
|
174
|
+
signal: control.signal
|
|
151
175
|
});
|
|
152
176
|
scores = fitBradleyTerry(population, comparisons, config.profile.lambda, gen);
|
|
153
177
|
await trace.writeScores(gen, scores);
|
|
154
178
|
await assertBudget(`generation ${gen} comparisons`);
|
|
179
|
+
await writePhaseCommit(config.runDir, `generation_judging:${gen}`, {
|
|
180
|
+
comparisons,
|
|
181
|
+
scores,
|
|
182
|
+
usage: await phaseUsageRows(trace, `generation_judging:${gen}`)
|
|
183
|
+
});
|
|
155
184
|
await markPhaseCompleted(trace, "generation_judging", gen);
|
|
156
185
|
}
|
|
157
186
|
const ranked = rankPopulation(population, scores);
|
|
@@ -159,7 +188,9 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
159
188
|
const discarded = bottomQuartile(ranked);
|
|
160
189
|
const survivors = ranked.filter((candidate) => !discarded.has(candidate.id));
|
|
161
190
|
const mutationParents = survivors.slice(0, config.profile.n - elites.length);
|
|
162
|
-
const critiquesByCandidate = aggregateCritiques(population, comparisons
|
|
191
|
+
const critiquesByCandidate = aggregateCritiques(population, comparisons, {
|
|
192
|
+
aggregateChars: config.critiqueLimits.aggregateChars
|
|
193
|
+
});
|
|
163
194
|
await assertNotCancelled(`generation ${gen} mutation`);
|
|
164
195
|
await writeStatus("running", "generation_mutation", { generation: gen });
|
|
165
196
|
await traceDiscardedCandidates(trace, ranked, new Set([...elites, ...mutationParents].map((candidate) => candidate.id)), discarded, gen);
|
|
@@ -171,7 +202,8 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
171
202
|
survivors: mutationParents,
|
|
172
203
|
critiquesByCandidate,
|
|
173
204
|
tracker,
|
|
174
|
-
guards
|
|
205
|
+
guards,
|
|
206
|
+
signal: control.signal
|
|
175
207
|
});
|
|
176
208
|
const eliteCopies = copyElites(elites, gen);
|
|
177
209
|
for (const elite of eliteCopies) {
|
|
@@ -181,6 +213,10 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
181
213
|
population = keepPopulationSize([...eliteCopies, ...mutants], config.profile.n);
|
|
182
214
|
await trace.writePopulation(gen, population);
|
|
183
215
|
await assertBudget(`generation ${gen} mutation`);
|
|
216
|
+
await writePhaseCommit(config.runDir, `generation_mutation:${gen}`, {
|
|
217
|
+
population,
|
|
218
|
+
usage: await phaseUsageRows(trace, `generation_mutation:${gen}`)
|
|
219
|
+
});
|
|
184
220
|
await markPhaseCompleted(trace, "generation_mutation", gen);
|
|
185
221
|
await writeStatus("running", "generation_completed", { generation: gen });
|
|
186
222
|
}
|
|
@@ -193,8 +229,12 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
193
229
|
else {
|
|
194
230
|
await assertNotCancelled("final ranking");
|
|
195
231
|
await writeStatus("running", "final_ranking", { generation: "final" });
|
|
196
|
-
const finalRng =
|
|
197
|
-
|
|
232
|
+
const finalRng = config.rank
|
|
233
|
+
? createRng(config.rank.seed ?? hashSeed(`${config.seed}:final_judging:`))
|
|
234
|
+
: phaseRng(config.seed, "final_judging");
|
|
235
|
+
const finalPairs = config.rank
|
|
236
|
+
? configuredFinalPairs(population.map((candidate) => candidate.id), config.rank, finalRng)
|
|
237
|
+
: makeKRegularPairs(population.map((candidate) => candidate.id), config.profile.m, finalRng);
|
|
198
238
|
finalComparisons = await judgePairs({
|
|
199
239
|
config,
|
|
200
240
|
driver,
|
|
@@ -205,11 +245,17 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
205
245
|
pairs: finalPairs,
|
|
206
246
|
population,
|
|
207
247
|
tracker,
|
|
208
|
-
guards
|
|
248
|
+
guards,
|
|
249
|
+
signal: control.signal
|
|
209
250
|
});
|
|
210
251
|
finalScores = fitBradleyTerry(population, finalComparisons, config.profile.lambda, "final");
|
|
211
252
|
await trace.writeScores("final", finalScores);
|
|
212
253
|
await assertBudget("final ranking");
|
|
254
|
+
await writePhaseCommit(config.runDir, "final_judging", {
|
|
255
|
+
comparisons: finalComparisons,
|
|
256
|
+
scores: finalScores,
|
|
257
|
+
usage: await phaseUsageRows(trace, "final_judging")
|
|
258
|
+
});
|
|
213
259
|
await markPhaseCompleted(trace, "final_judging");
|
|
214
260
|
}
|
|
215
261
|
const winner = rankPopulation(population, finalScores)[0];
|
|
@@ -217,7 +263,7 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
217
263
|
throw new ConfigError("Run produced no winner.");
|
|
218
264
|
await assertNotCancelled("finalization");
|
|
219
265
|
await writeStatus("running", "finalizing", { generation: "final" });
|
|
220
|
-
const finalAnswer = await maybeFinalize(winner, config, driver, trace, tracker, guards);
|
|
266
|
+
const finalAnswer = await maybeFinalize(winner, config, driver, trace, tracker, guards, control.signal);
|
|
221
267
|
await assertBudget("finalization");
|
|
222
268
|
const completedAt = new Date().toISOString();
|
|
223
269
|
const summary = {
|
|
@@ -236,6 +282,12 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
236
282
|
completed_at: completedAt
|
|
237
283
|
};
|
|
238
284
|
await trace.writeSummary(summary, finalAnswer, winner.content);
|
|
285
|
+
await writePhaseCommit(config.runDir, "finalizing", {
|
|
286
|
+
summary,
|
|
287
|
+
finalAnswer,
|
|
288
|
+
winner: winner.content,
|
|
289
|
+
usage: await phaseUsageRows(trace, "finalizing")
|
|
290
|
+
});
|
|
239
291
|
await markPhaseCompleted(trace, "finalizing");
|
|
240
292
|
await trace.event({ type: "run.completed", winner_id: winner.id, completed_at: completedAt });
|
|
241
293
|
if (resumeState)
|
|
@@ -259,9 +311,19 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
259
311
|
throw error;
|
|
260
312
|
}
|
|
261
313
|
finally {
|
|
262
|
-
|
|
314
|
+
let closeError;
|
|
315
|
+
try {
|
|
316
|
+
await trace.close();
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
if (caughtError !== undefined)
|
|
320
|
+
process.stderr.write(`deepthonk: failed to close trace handles: ${error.message}\n`);
|
|
321
|
+
else
|
|
322
|
+
closeError = error;
|
|
323
|
+
}
|
|
324
|
+
if (ownsLock || verifiedPreclaim) {
|
|
263
325
|
try {
|
|
264
|
-
await releaseRunLock(config.runDir);
|
|
326
|
+
await releaseRunLock(config.runDir, ownsLock ? ownedLockClaimId : control.lockClaim?.claimId);
|
|
265
327
|
}
|
|
266
328
|
catch (error) {
|
|
267
329
|
if (caughtError !== undefined) {
|
|
@@ -272,27 +334,26 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
272
334
|
}
|
|
273
335
|
}
|
|
274
336
|
}
|
|
337
|
+
if (closeError !== undefined)
|
|
338
|
+
throw closeError;
|
|
275
339
|
}
|
|
276
340
|
}
|
|
277
341
|
export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
278
|
-
const existingSummary = await readOptionalJson(runDir, runArtifactFiles.summary);
|
|
279
|
-
if (existingSummary) {
|
|
280
|
-
resumeConfigError("Run already has summary.json; nothing to resume.", "resume.already_complete", "Use deepthonk inspect/result to read the completed run.");
|
|
281
|
-
}
|
|
282
342
|
const rawConfig = await readRequiredConfig(runDir);
|
|
343
|
+
assertNoLegacyRedactedBudget(rawConfig);
|
|
283
344
|
const currentVersion = await currentPackageVersion();
|
|
284
345
|
if (!sameMajorMinor(typeof rawConfig.version === "string" ? rawConfig.version : undefined, currentVersion)) {
|
|
285
346
|
resumeConfigError(`Cannot resume run from version ${String(rawConfig.version ?? "missing")}; current package version is ${currentVersion}. Resume requires matching major.minor.`, "resume.version_mismatch", "Start a fresh run with the current DeepThonk version, or resume with a package version whose major.minor matches the trace.");
|
|
286
347
|
}
|
|
287
348
|
assertStoredOutputConfigComplete(rawConfig);
|
|
288
349
|
const parsedConfig = runConfigSchema.parse(rawConfig);
|
|
289
|
-
const config = { ...parsedConfig, runDir };
|
|
350
|
+
const config = resolveRunConfigDefaults({ ...parsedConfig, runDir });
|
|
290
351
|
validateProfile(config.profile);
|
|
291
352
|
enforceBudget(config);
|
|
292
353
|
assertResumeProviderMatches(rawConfig, config, driver, options);
|
|
293
354
|
assertNoPruneInProgress(runDir);
|
|
294
355
|
const lockRunId = resolveResumeRunId(config, undefined, []);
|
|
295
|
-
const claimed = await
|
|
356
|
+
const claimed = await claimRunLockOwnership(runDir, lockRunId);
|
|
296
357
|
if (!claimed) {
|
|
297
358
|
throw new ConfigError(`Run directory is already claimed: ${runDir}`, {
|
|
298
359
|
code: "run.directory_locked",
|
|
@@ -301,6 +362,7 @@ export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
|
301
362
|
});
|
|
302
363
|
}
|
|
303
364
|
try {
|
|
365
|
+
const existingSummary = await readOptionalFinalSummary(runDir, rawConfig.traceSchemaVersion === traceSchemaVersion);
|
|
304
366
|
const status = await readOptionalJson(runDir, runArtifactFiles.status);
|
|
305
367
|
if ((status?.state === "running" || status?.state === "pending") && isLiveWorker(status.worker_pid)) {
|
|
306
368
|
resumeConfigError(`Run is still in flight at phase ${status.phase}.`, "resume.in_flight", "Wait for the worker to finish, or cancel/stop it before resuming.");
|
|
@@ -308,16 +370,41 @@ export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
|
308
370
|
const trace = await readResumeTrace(runDir);
|
|
309
371
|
const runId = resolveResumeRunId(config, status, trace.events);
|
|
310
372
|
const plan = buildResumePlan(config, trace.events);
|
|
311
|
-
if (
|
|
312
|
-
resumeConfigError("
|
|
373
|
+
if (existingSummary && rawConfig.traceSchemaVersion !== traceSchemaVersion) {
|
|
374
|
+
resumeConfigError("Run already has summary.json; nothing to resume.", "resume.already_complete", "Use deepthonk inspect/result to read the completed run.");
|
|
375
|
+
}
|
|
376
|
+
if (existingSummary &&
|
|
377
|
+
rawConfig.traceSchemaVersion === traceSchemaVersion &&
|
|
378
|
+
plan.nextPhase.phase !== "finalizing" &&
|
|
379
|
+
plan.nextPhase.phase !== "summary") {
|
|
380
|
+
resumeConfigError(`summary.json exists before the finalizing phase boundary (${plan.nextPhase.phase}).`, "resume.trace_v2_integrity", "Restore the matching finalization artifacts or start a fresh run.");
|
|
313
381
|
}
|
|
314
382
|
const planStatus = toResumePlanStatus(runDir, runId, plan);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
383
|
+
let receiptUsageIds;
|
|
384
|
+
if (rawConfig.traceSchemaVersion === traceSchemaVersion) {
|
|
385
|
+
const receiptUsage = await readValidatedPhaseReceiptUsage(runDir, phaseCursorKey(plan.nextPhase));
|
|
386
|
+
receiptUsageIds = validatedReceiptUsageIds(receiptUsage);
|
|
387
|
+
}
|
|
388
|
+
const pruned = pruneTraceToPlan(trace, plan, { dropUsageIds: receiptUsageIds });
|
|
389
|
+
assertResumeRunIdsAgree(runId, config, status, pruned);
|
|
318
390
|
const populationByGeneration = buildPopulationMap(config, pruned.populations, pruned.candidates, plan);
|
|
319
391
|
const comparisonsByGeneration = groupComparisons(pruned.comparisons);
|
|
320
|
-
const scoresByGeneration = reconstructScores(config, populationByGeneration, comparisonsByGeneration, pruned.scores, plan);
|
|
392
|
+
const scoresByGeneration = reconstructScores(config, populationByGeneration, comparisonsByGeneration, pruned.scores, plan, runId);
|
|
393
|
+
validateUsageRecords(pruned.usage);
|
|
394
|
+
if (rawConfig.traceSchemaVersion === traceSchemaVersion) {
|
|
395
|
+
await validateTraceV2CompletedPhases(runDir, config, plan.completed, populationByGeneration, comparisonsByGeneration, scoresByGeneration, pruned.usage);
|
|
396
|
+
if (plan.nextPhase.phase === "finalizing" && !plan.completed.has("finalizing")) {
|
|
397
|
+
await validatePhaseCommitIfPresent(runDir, "finalizing", await finalizingArtifacts(runDir, trace.usage));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (plan.nextPhase.phase === "summary") {
|
|
401
|
+
if (existingSummary) {
|
|
402
|
+
resumeConfigError("Run already has a validated summary.json and finalizing commit; nothing to resume.", "resume.already_complete", "Use deepthonk inspect/result to read the completed run.");
|
|
403
|
+
}
|
|
404
|
+
resumeConfigError("Trace says finalizing completed, but summary.json is missing or invalid.", rawConfig.traceSchemaVersion === traceSchemaVersion ? "resume.trace_v2_integrity" : "resume.inconsistent_trace", "Inspect the run directory and restore the matching finalization artifacts, or start a fresh run.");
|
|
405
|
+
}
|
|
406
|
+
if (options.dryRun)
|
|
407
|
+
return planStatus;
|
|
321
408
|
const tracker = replayBudgetUsage(config, pruned.usage);
|
|
322
409
|
const startedAt = status?.started_at ?? new Date().toISOString();
|
|
323
410
|
await new TraceStore(runDir).writeStatus({
|
|
@@ -343,7 +430,26 @@ export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
|
343
430
|
});
|
|
344
431
|
}
|
|
345
432
|
finally {
|
|
346
|
-
await releaseRunLock(runDir);
|
|
433
|
+
await releaseRunLock(runDir, claimed?.claimId);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function assertResumeRunIdsAgree(resolvedRunId, config, status, trace) {
|
|
437
|
+
const observed = new Set();
|
|
438
|
+
if (config.runId)
|
|
439
|
+
observed.add(config.runId);
|
|
440
|
+
if (status?.run_id)
|
|
441
|
+
observed.add(status.run_id);
|
|
442
|
+
for (const event of trace.events) {
|
|
443
|
+
if (typeof event.run_id === "string")
|
|
444
|
+
observed.add(event.run_id);
|
|
445
|
+
}
|
|
446
|
+
for (const comparison of trace.comparisons) {
|
|
447
|
+
if (comparison.runId)
|
|
448
|
+
observed.add(comparison.runId);
|
|
449
|
+
}
|
|
450
|
+
observed.add(resolvedRunId);
|
|
451
|
+
if (observed.size > 1) {
|
|
452
|
+
resumeConfigError(`Resume artifacts disagree on run ID: ${[...observed].sort().join(", ")}.`, "resume.run_id_mismatch", "Restore artifacts from one run; do not combine trace files from different run directories.");
|
|
347
453
|
}
|
|
348
454
|
}
|
|
349
455
|
async function markPhaseCompleted(trace, phase, generation) {
|
|
@@ -378,7 +484,7 @@ function resumeComparisons(resumeState, generation) {
|
|
|
378
484
|
fix: "Inspect the run directory for missing comparison trace rows."
|
|
379
485
|
});
|
|
380
486
|
}
|
|
381
|
-
return comparisons;
|
|
487
|
+
return canonicalizeComparisons(comparisons);
|
|
382
488
|
}
|
|
383
489
|
function resumeScores(resumeState, generation, population, comparisons, lambda) {
|
|
384
490
|
return resumeState?.scoresByGeneration.get(generation) ?? fitBradleyTerry(population, comparisons, lambda, generation);
|
|
@@ -399,6 +505,13 @@ function resumePhaseKey(phase, generation) {
|
|
|
399
505
|
return `${phase}:${generation}`;
|
|
400
506
|
return phase;
|
|
401
507
|
}
|
|
508
|
+
function phaseCursorKey(cursor) {
|
|
509
|
+
if ((cursor.phase === "generation_judging" || cursor.phase === "generation_mutation") &&
|
|
510
|
+
typeof cursor.generation === "number") {
|
|
511
|
+
return `${cursor.phase}:${cursor.generation}`;
|
|
512
|
+
}
|
|
513
|
+
return cursor.phase;
|
|
514
|
+
}
|
|
402
515
|
function providerLabel(driver) {
|
|
403
516
|
const value = driver;
|
|
404
517
|
if (typeof value.provider === "string")
|
|
@@ -425,10 +538,19 @@ function assertStoredOutputConfigComplete(rawConfig) {
|
|
|
425
538
|
}
|
|
426
539
|
}
|
|
427
540
|
function assertResumeProviderMatches(rawConfig, config, driver, options) {
|
|
541
|
+
assertProviderReplayInternalConsistency(config);
|
|
428
542
|
const runtimeProvider = options.provider ?? providerLabel(driver) ?? config.provider;
|
|
429
543
|
if (runtimeProvider !== config.provider) {
|
|
430
544
|
providerMismatch(`Cannot resume provider ${config.provider} with runtime provider ${runtimeProvider}.`);
|
|
431
545
|
}
|
|
546
|
+
const expectedFingerprint = config.providerReplay?.routeFingerprint;
|
|
547
|
+
if (expectedFingerprint !== undefined && providerRouteFingerprint(driver) !== expectedFingerprint) {
|
|
548
|
+
providerMismatch("Cannot resume because the provider route fingerprint changed.");
|
|
549
|
+
}
|
|
550
|
+
if (config.providerReplay?.baseUrl !== undefined &&
|
|
551
|
+
normalizeBaseUrl(providerBaseUrl(driver)) !== normalizeBaseUrl(config.providerReplay.baseUrl)) {
|
|
552
|
+
providerMismatch(`Cannot resume base URL ${config.providerReplay.baseUrl} with runtime base URL ${providerBaseUrl(driver) ?? "missing"}.`);
|
|
553
|
+
}
|
|
432
554
|
const expectedRoutes = expectedProviderRoutes(rawConfig, config);
|
|
433
555
|
const actualRoutes = runtimeProviderRoutes(driver);
|
|
434
556
|
for (const role of providerRoles) {
|
|
@@ -450,8 +572,92 @@ function assertResumeProviderMatches(rawConfig, config, driver, options) {
|
|
|
450
572
|
}
|
|
451
573
|
}
|
|
452
574
|
}
|
|
575
|
+
function assertProviderReplayInternalConsistency(config) {
|
|
576
|
+
const replay = config.providerReplay;
|
|
577
|
+
if (!replay)
|
|
578
|
+
return;
|
|
579
|
+
const expectedModels = {
|
|
580
|
+
generator: config.generatorModel,
|
|
581
|
+
mutator: config.mutatorModel,
|
|
582
|
+
judge: config.judgeModel,
|
|
583
|
+
finalizer: config.finalizerModel
|
|
584
|
+
};
|
|
585
|
+
if (replay.provider !== config.provider ||
|
|
586
|
+
replay.models.generator !== expectedModels.generator ||
|
|
587
|
+
replay.models.mutator !== expectedModels.mutator ||
|
|
588
|
+
replay.models.judge !== expectedModels.judge ||
|
|
589
|
+
replay.models.finalizer !== expectedModels.finalizer) {
|
|
590
|
+
resumeConfigError("Stored run provider/model fields disagree with providerReplay.", "resume.provider_replay_mismatch", "Restore config.json from the original run; do not edit provider or model fields independently.");
|
|
591
|
+
}
|
|
592
|
+
if (replay.routeFingerprint !== undefined && replay.routeFingerprint !== coreProviderReplayFingerprint(replay)) {
|
|
593
|
+
resumeConfigError("Stored providerReplay fingerprint does not match its route fields.", "resume.provider_replay_mismatch", "Restore the original providerReplay block instead of editing route fields.");
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function coreProviderReplayFingerprint(replay) {
|
|
597
|
+
const roles = Object.fromEntries(providerRoles.flatMap((role) => {
|
|
598
|
+
const route = replay.roleProviders?.[role];
|
|
599
|
+
return route
|
|
600
|
+
? [[role, routeFingerprintValue(route.provider, route.baseUrl, route.apiKeyEnv, route.model, route.supportsJsonMode)]]
|
|
601
|
+
: [];
|
|
602
|
+
}));
|
|
603
|
+
const value = {
|
|
604
|
+
base: routeFingerprintValue(replay.provider, replay.baseUrl, replay.apiKeyEnv, replay.models, replay.supportsJsonMode),
|
|
605
|
+
roles,
|
|
606
|
+
samplingPreferences: replay.samplingPreferences ?? null
|
|
607
|
+
};
|
|
608
|
+
return `sha256:${createHash("sha256").update(stableJson(value)).digest("hex")}`;
|
|
609
|
+
}
|
|
610
|
+
function routeFingerprintValue(provider, baseUrl, apiKeyEnv, model, supportsJsonMode) {
|
|
611
|
+
return Object.fromEntries(Object.entries({
|
|
612
|
+
provider,
|
|
613
|
+
baseUrl: normalizeFingerprintUrl(baseUrl),
|
|
614
|
+
apiKeyEnv,
|
|
615
|
+
model,
|
|
616
|
+
supportsJsonMode
|
|
617
|
+
}).filter(([, value]) => value !== undefined));
|
|
618
|
+
}
|
|
619
|
+
function normalizeFingerprintUrl(baseUrl) {
|
|
620
|
+
if (!baseUrl)
|
|
621
|
+
return undefined;
|
|
622
|
+
try {
|
|
623
|
+
const parsed = new URL(baseUrl);
|
|
624
|
+
parsed.hash = "";
|
|
625
|
+
parsed.hostname = parsed.hostname.toLowerCase();
|
|
626
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "");
|
|
627
|
+
return parsed.toString().replace(/\/$/, "");
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
return baseUrl.replace(/\/+$/, "");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function stableJson(value) {
|
|
634
|
+
if (Array.isArray(value))
|
|
635
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
636
|
+
if (value && typeof value === "object") {
|
|
637
|
+
const entries = Object.entries(value)
|
|
638
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
639
|
+
.map(([key, inner]) => `${JSON.stringify(key)}:${stableJson(inner)}`);
|
|
640
|
+
return `{${entries.join(",")}}`;
|
|
641
|
+
}
|
|
642
|
+
return JSON.stringify(value) ?? "null";
|
|
643
|
+
}
|
|
453
644
|
const providerRoles = ["generator", "mutator", "judge", "finalizer"];
|
|
454
645
|
function expectedProviderRoutes(rawConfig, config) {
|
|
646
|
+
const replayRoutes = config.providerReplay?.roleProviders;
|
|
647
|
+
if (replayRoutes) {
|
|
648
|
+
const routes = {};
|
|
649
|
+
for (const role of providerRoles) {
|
|
650
|
+
const route = replayRoutes[role];
|
|
651
|
+
if (!route)
|
|
652
|
+
continue;
|
|
653
|
+
routes[role] = {
|
|
654
|
+
provider: route.provider,
|
|
655
|
+
baseUrl: route.baseUrl,
|
|
656
|
+
model: route.model
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
return routes;
|
|
660
|
+
}
|
|
455
661
|
const providers = rawConfig.providers;
|
|
456
662
|
if (!isRecord(providers))
|
|
457
663
|
return {};
|
|
@@ -486,6 +692,16 @@ function runtimeProviderRoutes(driver) {
|
|
|
486
692
|
}
|
|
487
693
|
return routes;
|
|
488
694
|
}
|
|
695
|
+
function providerRouteFingerprint(driver) {
|
|
696
|
+
const value = driver;
|
|
697
|
+
if (typeof value.routeFingerprint === "string")
|
|
698
|
+
return value.routeFingerprint;
|
|
699
|
+
if (typeof value.config?.routeFingerprint === "string")
|
|
700
|
+
return value.config.routeFingerprint;
|
|
701
|
+
if (value.baseDriver)
|
|
702
|
+
return providerRouteFingerprint(value.baseDriver);
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
489
705
|
function providerBaseUrl(driver) {
|
|
490
706
|
const value = driver;
|
|
491
707
|
if (typeof value.baseUrl === "string")
|
|
@@ -527,6 +743,20 @@ async function readRequiredConfig(runDir) {
|
|
|
527
743
|
}
|
|
528
744
|
return config;
|
|
529
745
|
}
|
|
746
|
+
async function readOptionalFinalSummary(runDir, traceV2) {
|
|
747
|
+
try {
|
|
748
|
+
return await readOptionalJson(runDir, runArtifactFiles.summary);
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
if (!traceV2)
|
|
752
|
+
throw error;
|
|
753
|
+
throw new ConfigError(`Could not parse summary.json: ${error.message}`, {
|
|
754
|
+
code: "resume.trace_v2_integrity",
|
|
755
|
+
retryable: false,
|
|
756
|
+
fix: "Restore summary.json from the same completed run, or resume from the pre-finalization artifacts."
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
530
760
|
async function readOptionalJson(runDir, fileName) {
|
|
531
761
|
try {
|
|
532
762
|
return JSON.parse(await readFile(join(runDir, fileName), "utf8"));
|
|
@@ -537,6 +767,16 @@ async function readOptionalJson(runDir, fileName) {
|
|
|
537
767
|
throw error;
|
|
538
768
|
}
|
|
539
769
|
}
|
|
770
|
+
async function readOptionalText(runDir, fileName) {
|
|
771
|
+
try {
|
|
772
|
+
return await readFile(join(runDir, fileName), "utf8");
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
if (error.code === "ENOENT")
|
|
776
|
+
return undefined;
|
|
777
|
+
throw error;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
540
780
|
function isLiveWorker(pid) {
|
|
541
781
|
if (!pid)
|
|
542
782
|
return true;
|
|
@@ -544,8 +784,8 @@ function isLiveWorker(pid) {
|
|
|
544
784
|
process.kill(pid, 0);
|
|
545
785
|
return true;
|
|
546
786
|
}
|
|
547
|
-
catch {
|
|
548
|
-
return
|
|
787
|
+
catch (error) {
|
|
788
|
+
return error.code !== "ESRCH";
|
|
549
789
|
}
|
|
550
790
|
}
|
|
551
791
|
export function rankPopulation(population, scores) {
|
|
@@ -570,10 +810,81 @@ export function copyElites(elites, generation) {
|
|
|
570
810
|
status: "elite",
|
|
571
811
|
metadata: {
|
|
572
812
|
...candidate.metadata,
|
|
573
|
-
createdAt:
|
|
813
|
+
createdAt: candidate.metadata.createdAt
|
|
574
814
|
}
|
|
575
815
|
}));
|
|
576
816
|
}
|
|
817
|
+
async function validateTraceV2CompletedPhases(runDir, config, completed, populations, comparisons, scores, usage) {
|
|
818
|
+
if (completed.has("initial_generation")) {
|
|
819
|
+
await validatePhaseCommit(runDir, "initial_generation", {
|
|
820
|
+
population: populations.get(0),
|
|
821
|
+
usage: usage.filter((record) => usagePhaseKey(record.phase) === "initial_generation")
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
for (let generation = 1; generation <= config.profile.t; generation += 1) {
|
|
825
|
+
if (completed.has(`generation_judging:${generation}`)) {
|
|
826
|
+
const phaseKey = `generation_judging:${generation}`;
|
|
827
|
+
await validatePhaseCommit(runDir, phaseKey, {
|
|
828
|
+
comparisons: comparisons.get(generation),
|
|
829
|
+
scores: scores.get(generation),
|
|
830
|
+
usage: usage.filter((record) => usagePhaseKey(record.phase) === phaseKey)
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
if (completed.has(`generation_mutation:${generation}`)) {
|
|
834
|
+
const phaseKey = `generation_mutation:${generation}`;
|
|
835
|
+
await validatePhaseCommit(runDir, phaseKey, {
|
|
836
|
+
population: populations.get(generation),
|
|
837
|
+
usage: usage.filter((record) => usagePhaseKey(record.phase) === phaseKey)
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (completed.has("final_judging")) {
|
|
842
|
+
await validatePhaseCommit(runDir, "final_judging", {
|
|
843
|
+
comparisons: comparisons.get("final"),
|
|
844
|
+
scores: scores.get("final"),
|
|
845
|
+
usage: usage.filter((record) => usagePhaseKey(record.phase) === "final_judging")
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
if (completed.has("finalizing")) {
|
|
849
|
+
await validatePhaseCommit(runDir, "finalizing", await finalizingArtifacts(runDir, usage));
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function finalizingArtifacts(runDir, usage) {
|
|
853
|
+
try {
|
|
854
|
+
return {
|
|
855
|
+
summary: await readOptionalJson(runDir, runArtifactFiles.summary),
|
|
856
|
+
finalAnswer: await readOptionalText(runDir, runArtifactFiles.final),
|
|
857
|
+
winner: await readOptionalText(runDir, runArtifactFiles.winner),
|
|
858
|
+
usage: usage.filter((record) => usagePhaseKey(record.phase) === "finalizing")
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
throw new ConfigError(`Could not read finalization artifacts: ${error.message}`, {
|
|
863
|
+
code: "resume.trace_v2_integrity",
|
|
864
|
+
retryable: false,
|
|
865
|
+
fix: "Restore summary.json, final.txt, and winner.txt from the same completed run."
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async function phaseUsageRows(trace, phaseKey) {
|
|
870
|
+
const usage = await trace.readJsonl(runArtifactFiles.usage).catch(() => []);
|
|
871
|
+
return usage.filter((record) => usagePhaseKey(record.phase) === phaseKey);
|
|
872
|
+
}
|
|
873
|
+
function usagePhaseKey(phase) {
|
|
874
|
+
if (phase === "gen0_initial")
|
|
875
|
+
return "initial_generation";
|
|
876
|
+
if (phase === "final_judge")
|
|
877
|
+
return "final_judging";
|
|
878
|
+
if (phase === "finalize")
|
|
879
|
+
return "finalizing";
|
|
880
|
+
const judge = /^gen(\d+)_judge$/.exec(phase);
|
|
881
|
+
if (judge)
|
|
882
|
+
return `generation_judging:${judge[1]}`;
|
|
883
|
+
const mutation = /^gen(\d+)_mutate$/.exec(phase);
|
|
884
|
+
if (mutation)
|
|
885
|
+
return `generation_mutation:${mutation[1]}`;
|
|
886
|
+
return undefined;
|
|
887
|
+
}
|
|
577
888
|
export function keepPopulationSize(population, n) {
|
|
578
889
|
return population.slice(0, n);
|
|
579
890
|
}
|
|
@@ -594,44 +905,92 @@ async function traceDiscardedCandidates(trace, ranked, carriedIds, bottomQuartil
|
|
|
594
905
|
await trace.event({ type: "candidate.discarded", candidate_id: candidate.id, generation, reason: discardReason });
|
|
595
906
|
}
|
|
596
907
|
}
|
|
597
|
-
async function generateInitialPopulation(config, driver, trace, rng, runId, tracker, guards) {
|
|
908
|
+
async function generateInitialPopulation(config, driver, trace, rng, runId, tracker, guards, signal) {
|
|
598
909
|
const jobs = Array.from({ length: config.profile.n }, (_, index) => ({
|
|
599
910
|
index,
|
|
600
911
|
id: `g0_c${index}_${rng.id("c")}`,
|
|
601
912
|
prompt: generatePrompt(config.task, config.rubric, config.promptStyle, config.promptOverrides?.generate)
|
|
602
913
|
}));
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
914
|
+
const promptRef = config.output.includePrompts && jobs[0] ? await trace.writePrompt(jobs[0].prompt) : undefined;
|
|
915
|
+
const candidates = await runCheckpointedPhase({
|
|
916
|
+
runDir: config.runDir,
|
|
917
|
+
phaseKey: "initial_generation",
|
|
918
|
+
jobs: jobs.map((job) => {
|
|
919
|
+
const receiptUsage = [];
|
|
920
|
+
return {
|
|
921
|
+
id: job.id,
|
|
922
|
+
input: {
|
|
923
|
+
task: config.task,
|
|
924
|
+
rubric: config.rubric,
|
|
925
|
+
model: config.generatorModel,
|
|
926
|
+
temperature: config.profile.sampleTemperature,
|
|
927
|
+
candidateIndex: job.index,
|
|
928
|
+
prompt: job.prompt,
|
|
929
|
+
maxOutputTokens: config.modelOutputTokens?.generation ?? 4_096
|
|
930
|
+
},
|
|
931
|
+
receiptUsage: () => receiptUsage,
|
|
932
|
+
run: async (phaseSignal) => {
|
|
933
|
+
const reservation = await guards.beforeCall("initial population call");
|
|
934
|
+
let result;
|
|
935
|
+
try {
|
|
936
|
+
result = await driver.generate({
|
|
937
|
+
task: config.task,
|
|
938
|
+
rubric: config.rubric,
|
|
939
|
+
model: config.generatorModel,
|
|
940
|
+
temperature: config.profile.sampleTemperature,
|
|
941
|
+
candidateIndex: job.index,
|
|
942
|
+
prompt: job.prompt,
|
|
943
|
+
signal: phaseSignal,
|
|
944
|
+
maxOutputTokens: config.modelOutputTokens?.generation ?? 4_096
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
await recordFailedInvocation({
|
|
949
|
+
tracker,
|
|
950
|
+
reservation,
|
|
951
|
+
trace,
|
|
952
|
+
phase: "gen0_initial",
|
|
953
|
+
role: "generator",
|
|
954
|
+
provider: config.provider,
|
|
955
|
+
model: config.generatorModel,
|
|
956
|
+
error
|
|
957
|
+
});
|
|
958
|
+
throw error;
|
|
959
|
+
}
|
|
960
|
+
const delta = tracker.record(result, { provider: config.provider, model: config.generatorModel }, reservation);
|
|
961
|
+
const usageRecord = buildUsageRecord({
|
|
962
|
+
phase: "gen0_initial",
|
|
963
|
+
role: "generator",
|
|
964
|
+
result,
|
|
965
|
+
delta,
|
|
966
|
+
provider: result.provider ?? config.provider,
|
|
967
|
+
model: result.model ?? config.generatorModel
|
|
968
|
+
});
|
|
969
|
+
receiptUsage.push(usageRecord);
|
|
970
|
+
await trace.writeUsage(usageRecord);
|
|
971
|
+
await guards.afterCall("initial population call");
|
|
972
|
+
const candidate = {
|
|
973
|
+
id: job.id,
|
|
974
|
+
generation: 0,
|
|
975
|
+
kind: "initial",
|
|
976
|
+
content: result.text,
|
|
977
|
+
status: "generated",
|
|
978
|
+
metadata: resultMetadata(result, config, promptRef)
|
|
979
|
+
};
|
|
980
|
+
return candidate;
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
}),
|
|
984
|
+
concurrency: config.concurrency.generate,
|
|
985
|
+
signal,
|
|
986
|
+
validateOutput: isCandidateCheckpoint,
|
|
987
|
+
onResult: async (candidate, context) => {
|
|
988
|
+
if (context.reused)
|
|
989
|
+
await restoreCheckpointUsage(context.receiptUsage, tracker, trace);
|
|
990
|
+
await trace.writeCandidate(candidate);
|
|
991
|
+
await trace.event({ type: "candidate.generated", run_id: runId, candidate_id: candidate.id });
|
|
992
|
+
}
|
|
993
|
+
});
|
|
635
994
|
return candidates;
|
|
636
995
|
}
|
|
637
996
|
async function judgePairs(args) {
|
|
@@ -651,175 +1010,340 @@ async function judgePairs(args) {
|
|
|
651
1010
|
presentedA,
|
|
652
1011
|
presentedB,
|
|
653
1012
|
id: `${args.generation}_cmp_${pairIndex}_${args.rng.id("cmp")}`,
|
|
654
|
-
prompt: comparePrompt(args.config.task, presentedA, presentedB, args.config.rubric, args.config.promptStyle, args.config.promptOverrides?.compare)
|
|
1013
|
+
prompt: comparePrompt(args.config.task, presentedA, presentedB, args.config.rubric, args.config.promptStyle, args.config.promptOverrides?.compare),
|
|
1014
|
+
promptRef: undefined
|
|
655
1015
|
};
|
|
656
1016
|
});
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1017
|
+
const phaseKey = args.generation === "final" ? "final_judging" : `generation_judging:${args.generation}`;
|
|
1018
|
+
const comparisons = await runCheckpointedPhase({
|
|
1019
|
+
runDir: args.config.runDir,
|
|
1020
|
+
phaseKey,
|
|
1021
|
+
jobs: jobs.map((job) => {
|
|
1022
|
+
const receiptUsage = [];
|
|
1023
|
+
return {
|
|
1024
|
+
id: job.id,
|
|
1025
|
+
input: {
|
|
1026
|
+
task: args.config.task,
|
|
1027
|
+
rubric: args.config.rubric,
|
|
1028
|
+
model: args.config.judgeModel,
|
|
1029
|
+
temperature: args.config.profile.judgeTemperature,
|
|
1030
|
+
candidateA: job.presentedA,
|
|
1031
|
+
candidateB: job.presentedB,
|
|
1032
|
+
prompt: job.prompt,
|
|
1033
|
+
invalidJsonRetries: args.config.retry.invalidJsonRetries,
|
|
1034
|
+
maxOutputTokens: args.config.modelOutputTokens?.judge ?? 1_024
|
|
1035
|
+
},
|
|
1036
|
+
receiptUsage: () => receiptUsage,
|
|
1037
|
+
run: async (phaseSignal) => {
|
|
1038
|
+
if (args.config.output.includePrompts && !job.promptRef)
|
|
1039
|
+
job.promptRef = await args.trace.writePrompt(job.prompt);
|
|
1040
|
+
let parsed;
|
|
1041
|
+
let rawOutput;
|
|
1042
|
+
let invalidJson = false;
|
|
1043
|
+
let resultModel;
|
|
1044
|
+
let resultProvider;
|
|
1045
|
+
let resultRetryCount = 0;
|
|
1046
|
+
let jsonParseFailures = 0;
|
|
1047
|
+
let inputTokens = 0;
|
|
1048
|
+
let inputCacheHitTokens = 0;
|
|
1049
|
+
let inputCacheMissTokens = 0;
|
|
1050
|
+
let outputTokens = 0;
|
|
1051
|
+
let totalTokens = 0;
|
|
1052
|
+
for (let attempt = 0; attempt <= args.config.retry.invalidJsonRetries; attempt += 1) {
|
|
1053
|
+
const reservation = await args.guards.beforeCall(`${args.generation} comparison call`);
|
|
1054
|
+
let result;
|
|
1055
|
+
try {
|
|
1056
|
+
result = await args.driver.compare({
|
|
1057
|
+
task: args.config.task,
|
|
1058
|
+
rubric: args.config.rubric,
|
|
1059
|
+
model: args.config.judgeModel,
|
|
1060
|
+
temperature: args.config.profile.judgeTemperature,
|
|
1061
|
+
candidateA: job.presentedA,
|
|
1062
|
+
candidateB: job.presentedB,
|
|
1063
|
+
prompt: job.prompt,
|
|
1064
|
+
signal: phaseSignal,
|
|
1065
|
+
maxOutputTokens: args.config.modelOutputTokens?.judge ?? 1_024
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
await recordFailedInvocation({
|
|
1070
|
+
tracker: args.tracker,
|
|
1071
|
+
reservation,
|
|
1072
|
+
trace: args.trace,
|
|
1073
|
+
phase: args.generation === "final" ? "final_judge" : `gen${args.generation}_judge`,
|
|
1074
|
+
role: "judge",
|
|
1075
|
+
provider: args.config.provider,
|
|
1076
|
+
model: args.config.judgeModel,
|
|
1077
|
+
error
|
|
1078
|
+
});
|
|
1079
|
+
throw error;
|
|
1080
|
+
}
|
|
1081
|
+
const delta = args.tracker.record(result, { provider: args.config.provider, model: args.config.judgeModel }, reservation);
|
|
1082
|
+
const usageRecord = buildUsageRecord({
|
|
1083
|
+
phase: args.generation === "final" ? "final_judge" : `gen${args.generation}_judge`,
|
|
1084
|
+
role: "judge",
|
|
1085
|
+
result,
|
|
1086
|
+
delta,
|
|
1087
|
+
provider: result.provider ?? args.config.provider,
|
|
1088
|
+
model: result.model ?? args.config.judgeModel
|
|
1089
|
+
});
|
|
1090
|
+
receiptUsage.push(usageRecord);
|
|
1091
|
+
await args.trace.writeUsage(usageRecord);
|
|
1092
|
+
await args.guards.afterCall(`${args.generation} comparison call`);
|
|
1093
|
+
inputTokens += result.usage?.inputTokens ?? 0;
|
|
1094
|
+
inputCacheHitTokens += result.usage?.inputCacheHitTokens ?? 0;
|
|
1095
|
+
inputCacheMissTokens += result.usage?.inputCacheMissTokens ?? 0;
|
|
1096
|
+
outputTokens += result.usage?.outputTokens ?? 0;
|
|
1097
|
+
totalTokens += result.usage?.totalTokens ?? (result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0);
|
|
1098
|
+
rawOutput = args.config.output.includeRawModelOutputs ? result.raw ?? result.text : undefined;
|
|
1099
|
+
resultModel = result.model;
|
|
1100
|
+
resultProvider = result.provider;
|
|
1101
|
+
resultRetryCount += result.retryCount ?? 0;
|
|
1102
|
+
try {
|
|
1103
|
+
parsed = compareOutputSchema.parse(parseJsonObject(result.text));
|
|
1104
|
+
invalidJson = false;
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
catch {
|
|
1108
|
+
invalidJson = true;
|
|
1109
|
+
jsonParseFailures += 1;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (invalidJson || !parsed) {
|
|
1113
|
+
throw new ConfigError(`Judge produced ${jsonParseFailures} consecutive invalid-JSON responses (${args.config.retry.invalidJsonRetries + 1} attempts) for comparison ${job.id}. Refusing to synthesize a tie and pollute the ranking.`, {
|
|
1114
|
+
code: "judge.persistent_invalid_json",
|
|
1115
|
+
retryable: false,
|
|
1116
|
+
fix: "The judge model is producing unparseable output. Inspect the raw response (set output.includeRawModelOutputs: true), switch judge models, or raise retry.invalidJsonRetries if the failures are transient."
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
const comparison = {
|
|
1120
|
+
id: job.id,
|
|
1121
|
+
runId: args.runId,
|
|
1122
|
+
generation: args.generation,
|
|
1123
|
+
candidateAId: job.presentedA.id,
|
|
1124
|
+
candidateBId: job.presentedB.id,
|
|
1125
|
+
presentedAOriginalId: job.presentedA.id,
|
|
1126
|
+
presentedBOriginalId: job.presentedB.id,
|
|
1127
|
+
winner: parsed.winner,
|
|
1128
|
+
confidence: parsed.confidence,
|
|
1129
|
+
critiqueForA: parsed.feedback_a ?? parsed.critique_for_A ?? "",
|
|
1130
|
+
critiqueForB: parsed.feedback_b ?? parsed.critique_for_B ?? "",
|
|
1131
|
+
selectionReason: parsed.selection_reason ?? "",
|
|
1132
|
+
rawOutput,
|
|
1133
|
+
model: resultModel,
|
|
1134
|
+
provider: resultProvider,
|
|
1135
|
+
metadata: compactMetadata({
|
|
1136
|
+
invalid_json_tie: invalidJson ? true : undefined,
|
|
1137
|
+
json_parse_failures: jsonParseFailures || undefined,
|
|
1138
|
+
model_call_count: jsonParseFailures + (parsed ? 1 : 0),
|
|
1139
|
+
input_tokens: inputTokens || undefined,
|
|
1140
|
+
input_cache_hit_tokens: inputCacheHitTokens || undefined,
|
|
1141
|
+
input_cache_miss_tokens: inputCacheMissTokens || undefined,
|
|
1142
|
+
output_tokens: outputTokens || undefined,
|
|
1143
|
+
total_tokens: totalTokens || undefined,
|
|
1144
|
+
scheduled_candidate_a_id: job.originalA.id,
|
|
1145
|
+
scheduled_candidate_b_id: job.originalB.id,
|
|
1146
|
+
provider_retry_count: resultRetryCount || undefined,
|
|
1147
|
+
promptRef: job.promptRef
|
|
1148
|
+
})
|
|
1149
|
+
};
|
|
1150
|
+
return comparison;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
}),
|
|
1154
|
+
concurrency: args.config.concurrency.judge,
|
|
1155
|
+
signal: args.signal,
|
|
1156
|
+
validateOutput: isComparisonCheckpoint,
|
|
1157
|
+
onResult: async (comparison, context) => {
|
|
1158
|
+
if (context.reused)
|
|
1159
|
+
await restoreCheckpointUsage(context.receiptUsage, args.tracker, args.trace);
|
|
1160
|
+
await args.trace.writeComparison(comparison);
|
|
1161
|
+
await args.trace.event({
|
|
1162
|
+
type: "comparison.completed",
|
|
1163
|
+
run_id: args.runId,
|
|
1164
|
+
comparison_id: comparison.id,
|
|
1165
|
+
generation: args.generation
|
|
715
1166
|
});
|
|
716
1167
|
}
|
|
717
|
-
|
|
718
|
-
id: job.id,
|
|
719
|
-
runId: args.runId,
|
|
720
|
-
generation: args.generation,
|
|
721
|
-
candidateAId: job.presentedA.id,
|
|
722
|
-
candidateBId: job.presentedB.id,
|
|
723
|
-
presentedAOriginalId: job.presentedA.id,
|
|
724
|
-
presentedBOriginalId: job.presentedB.id,
|
|
725
|
-
winner: parsed?.winner ?? "tie",
|
|
726
|
-
confidence: parsed?.confidence,
|
|
727
|
-
critiqueForA: parsed?.feedback_a ?? parsed?.critique_for_A ?? "Invalid comparison JSON; recorded as tie.",
|
|
728
|
-
critiqueForB: parsed?.feedback_b ?? parsed?.critique_for_B ?? "Invalid comparison JSON; recorded as tie.",
|
|
729
|
-
selectionReason: parsed?.selection_reason ?? "invalid_json_tie",
|
|
730
|
-
rawOutput,
|
|
731
|
-
model: resultModel,
|
|
732
|
-
provider: resultProvider,
|
|
733
|
-
metadata: compactMetadata({
|
|
734
|
-
invalid_json_tie: invalidJson ? true : undefined,
|
|
735
|
-
json_parse_failures: jsonParseFailures || undefined,
|
|
736
|
-
model_call_count: jsonParseFailures + (parsed ? 1 : 0),
|
|
737
|
-
input_tokens: inputTokens || undefined,
|
|
738
|
-
input_cache_hit_tokens: inputCacheHitTokens || undefined,
|
|
739
|
-
input_cache_miss_tokens: inputCacheMissTokens || undefined,
|
|
740
|
-
output_tokens: outputTokens || undefined,
|
|
741
|
-
total_tokens: totalTokens || undefined,
|
|
742
|
-
scheduled_candidate_a_id: job.originalA.id,
|
|
743
|
-
scheduled_candidate_b_id: job.originalB.id,
|
|
744
|
-
provider_retry_count: resultRetryCount || undefined,
|
|
745
|
-
prompt: args.config.output.includePrompts ? job.prompt : undefined
|
|
746
|
-
})
|
|
747
|
-
};
|
|
748
|
-
await args.trace.writeComparison(comparison);
|
|
749
|
-
await args.trace.event({
|
|
750
|
-
type: "comparison.completed",
|
|
751
|
-
run_id: args.runId,
|
|
752
|
-
comparison_id: comparison.id,
|
|
753
|
-
generation: args.generation
|
|
754
|
-
});
|
|
755
|
-
return comparison;
|
|
756
|
-
}), args.config.concurrency.judge);
|
|
1168
|
+
});
|
|
757
1169
|
return comparisons;
|
|
758
1170
|
}
|
|
759
1171
|
async function mutateSurvivors(args) {
|
|
760
|
-
const mutants = await
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1172
|
+
const mutants = await runCheckpointedPhase({
|
|
1173
|
+
runDir: args.config.runDir,
|
|
1174
|
+
phaseKey: `generation_mutation:${args.generation}`,
|
|
1175
|
+
jobs: args.survivors.map((candidate, index) => {
|
|
1176
|
+
const critique = args.critiquesByCandidate.get(candidate.id) ?? "";
|
|
1177
|
+
const prompt = mutatePrompt(args.config.task, candidate, critique, args.config.rubric, args.config.promptStyle, args.config.promptOverrides?.mutate);
|
|
1178
|
+
const receiptUsage = [];
|
|
1179
|
+
let promptRef;
|
|
1180
|
+
return {
|
|
1181
|
+
id: `g${args.generation}_m${index}_${candidate.id}`,
|
|
1182
|
+
input: {
|
|
1183
|
+
task: args.config.task,
|
|
1184
|
+
rubric: args.config.rubric,
|
|
1185
|
+
model: args.config.mutatorModel,
|
|
1186
|
+
temperature: args.config.profile.mutateTemperature,
|
|
1187
|
+
candidate,
|
|
1188
|
+
critique,
|
|
1189
|
+
prompt,
|
|
1190
|
+
maxOutputTokens: args.config.modelOutputTokens?.mutation ?? 4_096
|
|
1191
|
+
},
|
|
1192
|
+
receiptUsage: () => receiptUsage,
|
|
1193
|
+
run: async (phaseSignal) => {
|
|
1194
|
+
if (args.config.output.includePrompts && !promptRef)
|
|
1195
|
+
promptRef = await args.trace.writePrompt(prompt);
|
|
1196
|
+
const reservation = await args.guards.beforeCall(`generation ${args.generation} mutation call`);
|
|
1197
|
+
let result;
|
|
1198
|
+
try {
|
|
1199
|
+
result = await args.driver.mutate({
|
|
1200
|
+
task: args.config.task,
|
|
1201
|
+
rubric: args.config.rubric,
|
|
1202
|
+
model: args.config.mutatorModel,
|
|
1203
|
+
temperature: args.config.profile.mutateTemperature,
|
|
1204
|
+
candidate,
|
|
1205
|
+
critique,
|
|
1206
|
+
prompt,
|
|
1207
|
+
signal: phaseSignal,
|
|
1208
|
+
maxOutputTokens: args.config.modelOutputTokens?.mutation ?? 4_096
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
catch (error) {
|
|
1212
|
+
await recordFailedInvocation({
|
|
1213
|
+
tracker: args.tracker,
|
|
1214
|
+
reservation,
|
|
1215
|
+
trace: args.trace,
|
|
1216
|
+
phase: `gen${args.generation}_mutate`,
|
|
1217
|
+
role: "mutator",
|
|
1218
|
+
provider: args.config.provider,
|
|
1219
|
+
model: args.config.mutatorModel,
|
|
1220
|
+
error
|
|
1221
|
+
});
|
|
1222
|
+
throw error;
|
|
1223
|
+
}
|
|
1224
|
+
const delta = args.tracker.record(result, { provider: args.config.provider, model: args.config.mutatorModel }, reservation);
|
|
1225
|
+
const usageRecord = buildUsageRecord({
|
|
1226
|
+
phase: `gen${args.generation}_mutate`,
|
|
1227
|
+
role: "mutator",
|
|
1228
|
+
result,
|
|
1229
|
+
delta,
|
|
1230
|
+
provider: result.provider ?? args.config.provider,
|
|
1231
|
+
model: result.model ?? args.config.mutatorModel
|
|
1232
|
+
});
|
|
1233
|
+
receiptUsage.push(usageRecord);
|
|
1234
|
+
await args.trace.writeUsage(usageRecord);
|
|
1235
|
+
await args.guards.afterCall(`generation ${args.generation} mutation call`);
|
|
1236
|
+
const mutant = {
|
|
1237
|
+
id: `g${args.generation}_m${index}_${candidate.id}`,
|
|
1238
|
+
generation: args.generation,
|
|
1239
|
+
parentId: candidate.id,
|
|
1240
|
+
kind: "mutation",
|
|
1241
|
+
content: result.text,
|
|
1242
|
+
status: "mutated",
|
|
1243
|
+
metadata: resultMetadata(result, args.config, promptRef)
|
|
1244
|
+
};
|
|
1245
|
+
return mutant;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
}),
|
|
1249
|
+
concurrency: args.config.concurrency.mutate,
|
|
1250
|
+
signal: args.signal,
|
|
1251
|
+
validateOutput: isCandidateCheckpoint,
|
|
1252
|
+
onResult: async (mutant, context) => {
|
|
1253
|
+
if (context.reused)
|
|
1254
|
+
await restoreCheckpointUsage(context.receiptUsage, args.tracker, args.trace);
|
|
1255
|
+
await args.trace.writeCandidate(mutant);
|
|
1256
|
+
await args.trace.event({ type: "candidate.mutated", candidate_id: mutant.id, parent_id: mutant.parentId });
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
796
1259
|
return mutants;
|
|
797
1260
|
}
|
|
798
|
-
async function maybeFinalize(winner, config, driver, trace, tracker, guards) {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
await
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1261
|
+
async function maybeFinalize(winner, config, driver, trace, tracker, guards, signal) {
|
|
1262
|
+
const enabled = Boolean(config.finalizerModel && driver.finalize);
|
|
1263
|
+
const prompt = finalizePrompt(config.task, winner, config.rubric, config.promptStyle, config.promptOverrides?.finalize);
|
|
1264
|
+
const promptRef = enabled && config.output.includePrompts ? await trace.writePrompt(prompt) : undefined;
|
|
1265
|
+
let finalModel;
|
|
1266
|
+
let finalProvider;
|
|
1267
|
+
const receiptUsage = [];
|
|
1268
|
+
const [finalized] = await runCheckpointedPhase({
|
|
1269
|
+
runDir: config.runDir,
|
|
1270
|
+
phaseKey: "finalizing",
|
|
1271
|
+
jobs: [
|
|
1272
|
+
{
|
|
1273
|
+
id: `finalizer:${winner.id}`,
|
|
1274
|
+
input: {
|
|
1275
|
+
enabled,
|
|
1276
|
+
task: config.task,
|
|
1277
|
+
rubric: config.rubric,
|
|
1278
|
+
model: config.finalizerModel,
|
|
1279
|
+
candidate: winner,
|
|
1280
|
+
prompt,
|
|
1281
|
+
maxOutputTokens: config.modelOutputTokens?.finalizer ?? 4_096
|
|
1282
|
+
},
|
|
1283
|
+
receiptUsage: () => receiptUsage,
|
|
1284
|
+
run: async (phaseSignal) => {
|
|
1285
|
+
if (!enabled || !config.finalizerModel || !driver.finalize)
|
|
1286
|
+
return { answer: winner.content, promptRef: undefined };
|
|
1287
|
+
const reservation = await guards.beforeCall("finalization call");
|
|
1288
|
+
let result;
|
|
1289
|
+
try {
|
|
1290
|
+
result = await driver.finalize({
|
|
1291
|
+
task: config.task,
|
|
1292
|
+
rubric: config.rubric,
|
|
1293
|
+
model: config.finalizerModel,
|
|
1294
|
+
candidate: winner,
|
|
1295
|
+
prompt,
|
|
1296
|
+
signal: phaseSignal,
|
|
1297
|
+
maxOutputTokens: config.modelOutputTokens?.finalizer ?? 4_096
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
catch (error) {
|
|
1301
|
+
await recordFailedInvocation({
|
|
1302
|
+
tracker,
|
|
1303
|
+
reservation,
|
|
1304
|
+
trace,
|
|
1305
|
+
phase: "finalize",
|
|
1306
|
+
role: "finalizer",
|
|
1307
|
+
provider: config.provider,
|
|
1308
|
+
model: config.finalizerModel,
|
|
1309
|
+
error
|
|
1310
|
+
});
|
|
1311
|
+
throw error;
|
|
1312
|
+
}
|
|
1313
|
+
const delta = tracker.record(result, { provider: config.provider, model: config.finalizerModel }, reservation);
|
|
1314
|
+
const usageRecord = buildUsageRecord({
|
|
1315
|
+
phase: "finalize",
|
|
1316
|
+
role: "finalizer",
|
|
1317
|
+
result,
|
|
1318
|
+
delta,
|
|
1319
|
+
provider: result.provider ?? config.provider,
|
|
1320
|
+
model: result.model ?? config.finalizerModel
|
|
1321
|
+
});
|
|
1322
|
+
receiptUsage.push(usageRecord);
|
|
1323
|
+
await trace.writeUsage(usageRecord);
|
|
1324
|
+
await guards.afterCall("finalization call");
|
|
1325
|
+
finalModel = result.model;
|
|
1326
|
+
finalProvider = result.provider;
|
|
1327
|
+
return { answer: result.text, promptRef };
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
],
|
|
1331
|
+
concurrency: 1,
|
|
1332
|
+
signal,
|
|
1333
|
+
validateOutput: (output) => Boolean(output &&
|
|
1334
|
+
typeof output === "object" &&
|
|
1335
|
+
typeof output.answer === "string"),
|
|
1336
|
+
onResult: async (_output, context) => {
|
|
1337
|
+
if (context.reused)
|
|
1338
|
+
await restoreCheckpointUsage(context.receiptUsage, tracker, trace);
|
|
1339
|
+
if (enabled) {
|
|
1340
|
+
await trace.event({ type: "finalized", winner_id: winner.id, model: finalModel, provider: finalProvider, prompt_ref: promptRef });
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
808
1343
|
});
|
|
809
|
-
|
|
810
|
-
await trace.writeUsage(buildUsageRecord({
|
|
811
|
-
phase: "finalize",
|
|
812
|
-
role: "finalizer",
|
|
813
|
-
result,
|
|
814
|
-
delta,
|
|
815
|
-
provider: result.provider ?? config.provider,
|
|
816
|
-
model: result.model ?? config.finalizerModel
|
|
817
|
-
}));
|
|
818
|
-
await guards.afterCall("finalization call");
|
|
819
|
-
await trace.event({ type: "finalized", winner_id: winner.id, model: result.model, provider: result.provider });
|
|
820
|
-
return result.text;
|
|
1344
|
+
return finalized.answer;
|
|
821
1345
|
}
|
|
822
|
-
function resultMetadata(result, config,
|
|
1346
|
+
function resultMetadata(result, config, promptRef) {
|
|
823
1347
|
return compactMetadata({
|
|
824
1348
|
model: result.model,
|
|
825
1349
|
provider: result.provider,
|
|
@@ -831,13 +1355,65 @@ function resultMetadata(result, config, prompt) {
|
|
|
831
1355
|
latencyMs: result.latencyMs,
|
|
832
1356
|
retryCount: result.retryCount,
|
|
833
1357
|
rawOutput: config.output.includeRawModelOutputs ? result.raw : undefined,
|
|
834
|
-
|
|
1358
|
+
promptRef: config.output.includePrompts ? promptRef : undefined,
|
|
835
1359
|
createdAt: new Date().toISOString()
|
|
836
1360
|
});
|
|
837
1361
|
}
|
|
838
1362
|
function compactMetadata(metadata) {
|
|
839
1363
|
return Object.fromEntries(Object.entries(metadata).filter(([, value]) => value !== undefined));
|
|
840
1364
|
}
|
|
1365
|
+
const restoredUsageByTracker = new WeakMap();
|
|
1366
|
+
async function restoreCheckpointUsage(usage, tracker, trace) {
|
|
1367
|
+
if (usage === undefined)
|
|
1368
|
+
return;
|
|
1369
|
+
if (!Array.isArray(usage)) {
|
|
1370
|
+
throw new ConfigError("Trace-v2 checkpoint usage must be an array of UsageRecord rows.", {
|
|
1371
|
+
code: "resume.usage_invalid",
|
|
1372
|
+
retryable: false
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
const ids = validatedReceiptUsageIds(usage);
|
|
1376
|
+
const restored = restoredUsageByTracker.get(tracker) ?? new Set();
|
|
1377
|
+
for (const id of ids) {
|
|
1378
|
+
if (restored.has(id)) {
|
|
1379
|
+
throw new ConfigError(`Trace-v2 usage ${id} was restored more than once.`, {
|
|
1380
|
+
code: "resume.usage_invalid",
|
|
1381
|
+
retryable: false
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
restored.add(id);
|
|
1385
|
+
}
|
|
1386
|
+
restoredUsageByTracker.set(tracker, restored);
|
|
1387
|
+
const records = usage;
|
|
1388
|
+
applyUsageRecords(tracker, records);
|
|
1389
|
+
for (const record of records)
|
|
1390
|
+
await trace.writeUsage(record);
|
|
1391
|
+
}
|
|
1392
|
+
function isCandidateCheckpoint(output) {
|
|
1393
|
+
if (!output || typeof output !== "object" || Array.isArray(output))
|
|
1394
|
+
return false;
|
|
1395
|
+
const candidate = output;
|
|
1396
|
+
return (typeof candidate.id === "string" &&
|
|
1397
|
+
candidate.id.length > 0 &&
|
|
1398
|
+
Number.isInteger(candidate.generation) &&
|
|
1399
|
+
typeof candidate.content === "string" &&
|
|
1400
|
+
typeof candidate.kind === "string" &&
|
|
1401
|
+
Boolean(candidate.metadata && typeof candidate.metadata === "object" && typeof candidate.metadata.createdAt === "string"));
|
|
1402
|
+
}
|
|
1403
|
+
function isComparisonCheckpoint(output) {
|
|
1404
|
+
if (!output || typeof output !== "object" || Array.isArray(output))
|
|
1405
|
+
return false;
|
|
1406
|
+
const comparison = output;
|
|
1407
|
+
return (typeof comparison.id === "string" &&
|
|
1408
|
+
typeof comparison.runId === "string" &&
|
|
1409
|
+
(typeof comparison.generation === "number" || comparison.generation === "final") &&
|
|
1410
|
+
typeof comparison.candidateAId === "string" &&
|
|
1411
|
+
typeof comparison.candidateBId === "string" &&
|
|
1412
|
+
(comparison.winner === "A" || comparison.winner === "B" || comparison.winner === "tie") &&
|
|
1413
|
+
typeof comparison.critiqueForA === "string" &&
|
|
1414
|
+
typeof comparison.critiqueForB === "string" &&
|
|
1415
|
+
typeof comparison.selectionReason === "string");
|
|
1416
|
+
}
|
|
841
1417
|
function summaryProfile(profile) {
|
|
842
1418
|
return {
|
|
843
1419
|
n: profile.n,
|
|
@@ -878,6 +1454,7 @@ function summaryModels(config) {
|
|
|
878
1454
|
function buildUsageRecord(args) {
|
|
879
1455
|
return {
|
|
880
1456
|
schema_version: 1,
|
|
1457
|
+
usage_id: randomUUID(),
|
|
881
1458
|
ts: new Date().toISOString(),
|
|
882
1459
|
phase: args.phase,
|
|
883
1460
|
role: args.role,
|
|
@@ -892,18 +1469,31 @@ function buildUsageRecord(args) {
|
|
|
892
1469
|
output_usd: args.delta.outputUsd,
|
|
893
1470
|
total_usd: args.delta.usd,
|
|
894
1471
|
latency_ms: args.result.latencyMs,
|
|
895
|
-
retry_count: args.result.retryCount
|
|
1472
|
+
retry_count: args.result.retryCount,
|
|
1473
|
+
outcome: args.outcome,
|
|
1474
|
+
error_code: args.errorCode
|
|
896
1475
|
};
|
|
897
1476
|
}
|
|
1477
|
+
async function recordFailedInvocation(args) {
|
|
1478
|
+
const delta = args.tracker.failCall(args.reservation);
|
|
1479
|
+
await bestEffortTraceWrite("failed call usage", () => args.trace.writeUsage(buildUsageRecord({
|
|
1480
|
+
phase: args.phase,
|
|
1481
|
+
role: args.role,
|
|
1482
|
+
result: { text: "" },
|
|
1483
|
+
delta,
|
|
1484
|
+
provider: args.provider,
|
|
1485
|
+
model: args.model,
|
|
1486
|
+
outcome: "failed",
|
|
1487
|
+
errorCode: args.error instanceof DeepThonkError ? args.error.code : args.error instanceof Error ? args.error.name : "unknown_error"
|
|
1488
|
+
})));
|
|
1489
|
+
}
|
|
898
1490
|
function enforceBudget(config) {
|
|
899
|
-
const plan = planBudget(config.profile
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const
|
|
904
|
-
const
|
|
905
|
-
const finalizerCalls = config.finalizerModel ? 1 : 0;
|
|
906
|
-
const plannedCalls = plan.calls + finalizerCalls + retryHeadroom;
|
|
1491
|
+
const plan = planBudget(config.profile, {
|
|
1492
|
+
invalidJsonRetries: config.retry.invalidJsonRetries,
|
|
1493
|
+
includeFinalizer: Boolean(config.finalizerModel)
|
|
1494
|
+
});
|
|
1495
|
+
const explicitFinalCalls = config.rank ? configuredFinalPairCount(config.profile.n, config.rank) : plan.final_judge_calls;
|
|
1496
|
+
const plannedCalls = plan.calls - plan.final_judge_calls + explicitFinalCalls + plan.finalizer_calls;
|
|
907
1497
|
if (config.budget?.maxCalls !== undefined && plannedCalls > config.budget.maxCalls) {
|
|
908
1498
|
throw new BudgetExceededError(`Planned call count ${plannedCalls} exceeds maxCalls ${config.budget.maxCalls}.`, {
|
|
909
1499
|
code: "budget.planned_calls_exceeded",
|
|
@@ -911,6 +1501,64 @@ function enforceBudget(config) {
|
|
|
911
1501
|
});
|
|
912
1502
|
}
|
|
913
1503
|
}
|
|
1504
|
+
function configuredFinalPairs(candidateIds, rank, rng) {
|
|
1505
|
+
if (rank.mode === "k-regular") {
|
|
1506
|
+
if (rank.k === undefined) {
|
|
1507
|
+
throw new ConfigError("rank.mode k-regular requires rank.k.", { code: "rank.k_required", retryable: false });
|
|
1508
|
+
}
|
|
1509
|
+
const pairs = makeKRegularPairs(candidateIds, rank.k, rng);
|
|
1510
|
+
assertRankCallLimit(pairs.length, rank.maxCalls);
|
|
1511
|
+
return pairs;
|
|
1512
|
+
}
|
|
1513
|
+
const pairs = [];
|
|
1514
|
+
for (let left = 0; left < candidateIds.length; left += 1) {
|
|
1515
|
+
for (let right = left + 1; right < candidateIds.length; right += 1) {
|
|
1516
|
+
pairs.push({ a: candidateIds[left], b: candidateIds[right] });
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (pairs.length > 100 && (rank.maxCalls === undefined || rank.maxCalls < pairs.length)) {
|
|
1520
|
+
throw new ConfigError(`All-pairs final ranking requires explicit rank.maxCalls >= ${pairs.length}.`, {
|
|
1521
|
+
code: "rank.max_calls_required",
|
|
1522
|
+
retryable: false,
|
|
1523
|
+
fix: "Raise rank.maxCalls or select k-regular mode."
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
assertRankCallLimit(pairs.length, rank.maxCalls);
|
|
1527
|
+
return rng.shuffle(pairs);
|
|
1528
|
+
}
|
|
1529
|
+
function configuredFinalPairCount(n, rank) {
|
|
1530
|
+
if (rank.mode === "all-pairs") {
|
|
1531
|
+
const calls = (n * (n - 1)) / 2;
|
|
1532
|
+
if (calls > 100 && (rank.maxCalls === undefined || rank.maxCalls < calls)) {
|
|
1533
|
+
throw new ConfigError(`All-pairs final ranking requires explicit rank.maxCalls >= ${calls}.`, {
|
|
1534
|
+
code: "rank.max_calls_required",
|
|
1535
|
+
retryable: false
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
assertRankCallLimit(calls, rank.maxCalls);
|
|
1539
|
+
return calls;
|
|
1540
|
+
}
|
|
1541
|
+
if (rank.k === undefined) {
|
|
1542
|
+
throw new ConfigError("rank.mode k-regular requires rank.k.", { code: "rank.k_required", retryable: false });
|
|
1543
|
+
}
|
|
1544
|
+
if (rank.k >= n || (n * rank.k) % 2 !== 0) {
|
|
1545
|
+
throw new ConfigError(`Invalid k-regular final ranking for n=${n}, k=${rank.k}.`, {
|
|
1546
|
+
code: "rank.k_invalid",
|
|
1547
|
+
retryable: false
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
const calls = (n * rank.k) / 2;
|
|
1551
|
+
assertRankCallLimit(calls, rank.maxCalls);
|
|
1552
|
+
return calls;
|
|
1553
|
+
}
|
|
1554
|
+
function assertRankCallLimit(calls, maxCalls) {
|
|
1555
|
+
if (maxCalls !== undefined && calls > maxCalls) {
|
|
1556
|
+
throw new ConfigError(`Final ranking requires ${calls} calls, exceeding rank.maxCalls ${maxCalls}.`, {
|
|
1557
|
+
code: "rank.max_calls_exceeded",
|
|
1558
|
+
retryable: false
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
914
1562
|
function cloneUsage(usage) {
|
|
915
1563
|
return {
|
|
916
1564
|
...emptyUsage(),
|