@deepthonk/core 0.1.4 → 0.1.5
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 +9 -6
- package/dist/artifacts.js +158 -10
- package/dist/artifacts.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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +32 -1
- package/dist/lifecycle.js +207 -6
- package/dist/lifecycle.js.map +1 -1
- package/dist/resume.d.ts +4 -2
- package/dist/resume.js +318 -32
- package/dist/resume.js.map +1 -1
- package/dist/runner.d.ts +2 -1
- package/dist/runner.js +295 -76
- package/dist/runner.js.map +1 -1
- package/dist/schemas.d.ts +302 -47
- package/dist/schemas.js +24 -2
- package/dist/schemas.js.map +1 -1
- package/dist/services.d.ts +1 -0
- package/dist/services.js +40 -2
- package/dist/services.js.map +1 -1
- package/dist/traceStore.d.ts +4 -1
- package/dist/traceStore.js +40 -13
- package/dist/traceStore.js.map +1 -1
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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";
|
|
@@ -7,12 +8,12 @@ import { planBudget, validateProfile } from "./budget.js";
|
|
|
7
8
|
import { BudgetExceededError, CancelledError, ConfigError, DeepThonkError } from "./errors.js";
|
|
8
9
|
import { runArtifactFiles } from "./artifacts.js";
|
|
9
10
|
import { parseJsonObject } from "./json.js";
|
|
10
|
-
import {
|
|
11
|
+
import { claimRunLockOwnership, emptyUsage, releaseRunLock, verifyRunLockClaim } from "./lifecycle.js";
|
|
11
12
|
import { makeKRegularPairs } from "./pairScheduler.js";
|
|
12
13
|
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 { assertNoPruneInProgress, assertNoLegacyRedactedBudget, buildPopulationMap, buildResumePlan, groupComparisons, persistPrunedTrace, pruneTraceToPlan, readResumeTrace, reconstructScores, replayBudgetUsage, resolveResumeRunId, resumeConfigError, toResumePlanStatus } from "./resume.js";
|
|
16
17
|
import { runConfigSchema, builtInProfiles, phaseCompletedEventSchema } from "./schemas.js";
|
|
17
18
|
import { aggregateCritiques } from "./critique.js";
|
|
18
19
|
import { TraceStore } from "./traceStore.js";
|
|
@@ -29,22 +30,39 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
29
30
|
const config = 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;
|
|
@@ -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({
|
|
@@ -261,7 +280,7 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
261
280
|
finally {
|
|
262
281
|
if (ownsLock) {
|
|
263
282
|
try {
|
|
264
|
-
await releaseRunLock(config.runDir);
|
|
283
|
+
await releaseRunLock(config.runDir, ownedLockClaimId);
|
|
265
284
|
}
|
|
266
285
|
catch (error) {
|
|
267
286
|
if (caughtError !== undefined) {
|
|
@@ -280,6 +299,7 @@ export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
|
280
299
|
resumeConfigError("Run already has summary.json; nothing to resume.", "resume.already_complete", "Use deepthonk inspect/result to read the completed run.");
|
|
281
300
|
}
|
|
282
301
|
const rawConfig = await readRequiredConfig(runDir);
|
|
302
|
+
assertNoLegacyRedactedBudget(rawConfig);
|
|
283
303
|
const currentVersion = await currentPackageVersion();
|
|
284
304
|
if (!sameMajorMinor(typeof rawConfig.version === "string" ? rawConfig.version : undefined, currentVersion)) {
|
|
285
305
|
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.");
|
|
@@ -292,7 +312,7 @@ export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
|
292
312
|
assertResumeProviderMatches(rawConfig, config, driver, options);
|
|
293
313
|
assertNoPruneInProgress(runDir);
|
|
294
314
|
const lockRunId = resolveResumeRunId(config, undefined, []);
|
|
295
|
-
const claimed = await
|
|
315
|
+
const claimed = await claimRunLockOwnership(runDir, lockRunId);
|
|
296
316
|
if (!claimed) {
|
|
297
317
|
throw new ConfigError(`Run directory is already claimed: ${runDir}`, {
|
|
298
318
|
code: "run.directory_locked",
|
|
@@ -312,12 +332,13 @@ export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
|
312
332
|
resumeConfigError("Trace says finalizing completed, but summary.json is missing.", "resume.inconsistent_trace", "Inspect the run directory and restore summary.json, or start a fresh run.");
|
|
313
333
|
}
|
|
314
334
|
const planStatus = toResumePlanStatus(runDir, runId, plan);
|
|
315
|
-
if (options.dryRun)
|
|
316
|
-
return planStatus;
|
|
317
335
|
const pruned = pruneTraceToPlan(trace, plan);
|
|
336
|
+
assertResumeRunIdsAgree(runId, config, status, pruned);
|
|
318
337
|
const populationByGeneration = buildPopulationMap(config, pruned.populations, pruned.candidates, plan);
|
|
319
338
|
const comparisonsByGeneration = groupComparisons(pruned.comparisons);
|
|
320
|
-
const scoresByGeneration = reconstructScores(config, populationByGeneration, comparisonsByGeneration, pruned.scores, plan);
|
|
339
|
+
const scoresByGeneration = reconstructScores(config, populationByGeneration, comparisonsByGeneration, pruned.scores, plan, runId);
|
|
340
|
+
if (options.dryRun)
|
|
341
|
+
return planStatus;
|
|
321
342
|
const tracker = replayBudgetUsage(config, pruned.usage);
|
|
322
343
|
const startedAt = status?.started_at ?? new Date().toISOString();
|
|
323
344
|
await new TraceStore(runDir).writeStatus({
|
|
@@ -343,7 +364,26 @@ export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
|
343
364
|
});
|
|
344
365
|
}
|
|
345
366
|
finally {
|
|
346
|
-
await releaseRunLock(runDir);
|
|
367
|
+
await releaseRunLock(runDir, claimed?.claimId);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function assertResumeRunIdsAgree(resolvedRunId, config, status, trace) {
|
|
371
|
+
const observed = new Set();
|
|
372
|
+
if (config.runId)
|
|
373
|
+
observed.add(config.runId);
|
|
374
|
+
if (status?.run_id)
|
|
375
|
+
observed.add(status.run_id);
|
|
376
|
+
for (const event of trace.events) {
|
|
377
|
+
if (typeof event.run_id === "string")
|
|
378
|
+
observed.add(event.run_id);
|
|
379
|
+
}
|
|
380
|
+
for (const comparison of trace.comparisons) {
|
|
381
|
+
if (comparison.runId)
|
|
382
|
+
observed.add(comparison.runId);
|
|
383
|
+
}
|
|
384
|
+
observed.add(resolvedRunId);
|
|
385
|
+
if (observed.size > 1) {
|
|
386
|
+
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
387
|
}
|
|
348
388
|
}
|
|
349
389
|
async function markPhaseCompleted(trace, phase, generation) {
|
|
@@ -425,10 +465,19 @@ function assertStoredOutputConfigComplete(rawConfig) {
|
|
|
425
465
|
}
|
|
426
466
|
}
|
|
427
467
|
function assertResumeProviderMatches(rawConfig, config, driver, options) {
|
|
468
|
+
assertProviderReplayInternalConsistency(config);
|
|
428
469
|
const runtimeProvider = options.provider ?? providerLabel(driver) ?? config.provider;
|
|
429
470
|
if (runtimeProvider !== config.provider) {
|
|
430
471
|
providerMismatch(`Cannot resume provider ${config.provider} with runtime provider ${runtimeProvider}.`);
|
|
431
472
|
}
|
|
473
|
+
const expectedFingerprint = config.providerReplay?.routeFingerprint;
|
|
474
|
+
if (expectedFingerprint !== undefined && providerRouteFingerprint(driver) !== expectedFingerprint) {
|
|
475
|
+
providerMismatch("Cannot resume because the provider route fingerprint changed.");
|
|
476
|
+
}
|
|
477
|
+
if (config.providerReplay?.baseUrl !== undefined &&
|
|
478
|
+
normalizeBaseUrl(providerBaseUrl(driver)) !== normalizeBaseUrl(config.providerReplay.baseUrl)) {
|
|
479
|
+
providerMismatch(`Cannot resume base URL ${config.providerReplay.baseUrl} with runtime base URL ${providerBaseUrl(driver) ?? "missing"}.`);
|
|
480
|
+
}
|
|
432
481
|
const expectedRoutes = expectedProviderRoutes(rawConfig, config);
|
|
433
482
|
const actualRoutes = runtimeProviderRoutes(driver);
|
|
434
483
|
for (const role of providerRoles) {
|
|
@@ -450,8 +499,92 @@ function assertResumeProviderMatches(rawConfig, config, driver, options) {
|
|
|
450
499
|
}
|
|
451
500
|
}
|
|
452
501
|
}
|
|
502
|
+
function assertProviderReplayInternalConsistency(config) {
|
|
503
|
+
const replay = config.providerReplay;
|
|
504
|
+
if (!replay)
|
|
505
|
+
return;
|
|
506
|
+
const expectedModels = {
|
|
507
|
+
generator: config.generatorModel,
|
|
508
|
+
mutator: config.mutatorModel,
|
|
509
|
+
judge: config.judgeModel,
|
|
510
|
+
finalizer: config.finalizerModel
|
|
511
|
+
};
|
|
512
|
+
if (replay.provider !== config.provider ||
|
|
513
|
+
replay.models.generator !== expectedModels.generator ||
|
|
514
|
+
replay.models.mutator !== expectedModels.mutator ||
|
|
515
|
+
replay.models.judge !== expectedModels.judge ||
|
|
516
|
+
replay.models.finalizer !== expectedModels.finalizer) {
|
|
517
|
+
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.");
|
|
518
|
+
}
|
|
519
|
+
if (replay.routeFingerprint !== undefined && replay.routeFingerprint !== coreProviderReplayFingerprint(replay)) {
|
|
520
|
+
resumeConfigError("Stored providerReplay fingerprint does not match its route fields.", "resume.provider_replay_mismatch", "Restore the original providerReplay block instead of editing route fields.");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function coreProviderReplayFingerprint(replay) {
|
|
524
|
+
const roles = Object.fromEntries(providerRoles.flatMap((role) => {
|
|
525
|
+
const route = replay.roleProviders?.[role];
|
|
526
|
+
return route
|
|
527
|
+
? [[role, routeFingerprintValue(route.provider, route.baseUrl, route.apiKeyEnv, route.model, route.supportsJsonMode)]]
|
|
528
|
+
: [];
|
|
529
|
+
}));
|
|
530
|
+
const value = {
|
|
531
|
+
base: routeFingerprintValue(replay.provider, replay.baseUrl, replay.apiKeyEnv, replay.models, replay.supportsJsonMode),
|
|
532
|
+
roles,
|
|
533
|
+
samplingPreferences: replay.samplingPreferences ?? null
|
|
534
|
+
};
|
|
535
|
+
return `sha256:${createHash("sha256").update(stableJson(value)).digest("hex")}`;
|
|
536
|
+
}
|
|
537
|
+
function routeFingerprintValue(provider, baseUrl, apiKeyEnv, model, supportsJsonMode) {
|
|
538
|
+
return Object.fromEntries(Object.entries({
|
|
539
|
+
provider,
|
|
540
|
+
baseUrl: normalizeFingerprintUrl(baseUrl),
|
|
541
|
+
apiKeyEnv,
|
|
542
|
+
model,
|
|
543
|
+
supportsJsonMode
|
|
544
|
+
}).filter(([, value]) => value !== undefined));
|
|
545
|
+
}
|
|
546
|
+
function normalizeFingerprintUrl(baseUrl) {
|
|
547
|
+
if (!baseUrl)
|
|
548
|
+
return undefined;
|
|
549
|
+
try {
|
|
550
|
+
const parsed = new URL(baseUrl);
|
|
551
|
+
parsed.hash = "";
|
|
552
|
+
parsed.hostname = parsed.hostname.toLowerCase();
|
|
553
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "");
|
|
554
|
+
return parsed.toString().replace(/\/$/, "");
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return baseUrl.replace(/\/+$/, "");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function stableJson(value) {
|
|
561
|
+
if (Array.isArray(value))
|
|
562
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
563
|
+
if (value && typeof value === "object") {
|
|
564
|
+
const entries = Object.entries(value)
|
|
565
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
566
|
+
.map(([key, inner]) => `${JSON.stringify(key)}:${stableJson(inner)}`);
|
|
567
|
+
return `{${entries.join(",")}}`;
|
|
568
|
+
}
|
|
569
|
+
return JSON.stringify(value) ?? "null";
|
|
570
|
+
}
|
|
453
571
|
const providerRoles = ["generator", "mutator", "judge", "finalizer"];
|
|
454
572
|
function expectedProviderRoutes(rawConfig, config) {
|
|
573
|
+
const replayRoutes = config.providerReplay?.roleProviders;
|
|
574
|
+
if (replayRoutes) {
|
|
575
|
+
const routes = {};
|
|
576
|
+
for (const role of providerRoles) {
|
|
577
|
+
const route = replayRoutes[role];
|
|
578
|
+
if (!route)
|
|
579
|
+
continue;
|
|
580
|
+
routes[role] = {
|
|
581
|
+
provider: route.provider,
|
|
582
|
+
baseUrl: route.baseUrl,
|
|
583
|
+
model: route.model
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
return routes;
|
|
587
|
+
}
|
|
455
588
|
const providers = rawConfig.providers;
|
|
456
589
|
if (!isRecord(providers))
|
|
457
590
|
return {};
|
|
@@ -486,6 +619,16 @@ function runtimeProviderRoutes(driver) {
|
|
|
486
619
|
}
|
|
487
620
|
return routes;
|
|
488
621
|
}
|
|
622
|
+
function providerRouteFingerprint(driver) {
|
|
623
|
+
const value = driver;
|
|
624
|
+
if (typeof value.routeFingerprint === "string")
|
|
625
|
+
return value.routeFingerprint;
|
|
626
|
+
if (typeof value.config?.routeFingerprint === "string")
|
|
627
|
+
return value.config.routeFingerprint;
|
|
628
|
+
if (value.baseDriver)
|
|
629
|
+
return providerRouteFingerprint(value.baseDriver);
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
489
632
|
function providerBaseUrl(driver) {
|
|
490
633
|
const value = driver;
|
|
491
634
|
if (typeof value.baseUrl === "string")
|
|
@@ -544,8 +687,8 @@ function isLiveWorker(pid) {
|
|
|
544
687
|
process.kill(pid, 0);
|
|
545
688
|
return true;
|
|
546
689
|
}
|
|
547
|
-
catch {
|
|
548
|
-
return
|
|
690
|
+
catch (error) {
|
|
691
|
+
return error.code !== "ESRCH";
|
|
549
692
|
}
|
|
550
693
|
}
|
|
551
694
|
export function rankPopulation(population, scores) {
|
|
@@ -601,16 +744,32 @@ async function generateInitialPopulation(config, driver, trace, rng, runId, trac
|
|
|
601
744
|
prompt: generatePrompt(config.task, config.rubric, config.promptStyle, config.promptOverrides?.generate)
|
|
602
745
|
}));
|
|
603
746
|
const candidates = await runLimitedPhase(jobs.map((job) => async () => {
|
|
604
|
-
await guards.beforeCall("initial population call");
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
747
|
+
const reservation = await guards.beforeCall("initial population call");
|
|
748
|
+
let result;
|
|
749
|
+
try {
|
|
750
|
+
result = await driver.generate({
|
|
751
|
+
task: config.task,
|
|
752
|
+
rubric: config.rubric,
|
|
753
|
+
model: config.generatorModel,
|
|
754
|
+
temperature: config.profile.sampleTemperature,
|
|
755
|
+
candidateIndex: job.index,
|
|
756
|
+
prompt: job.prompt
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
await recordFailedInvocation({
|
|
761
|
+
tracker,
|
|
762
|
+
reservation,
|
|
763
|
+
trace,
|
|
764
|
+
phase: "gen0_initial",
|
|
765
|
+
role: "generator",
|
|
766
|
+
provider: config.provider,
|
|
767
|
+
model: config.generatorModel,
|
|
768
|
+
error
|
|
769
|
+
});
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
const delta = tracker.record(result, { provider: config.provider, model: config.generatorModel }, reservation);
|
|
614
773
|
await trace.writeUsage(buildUsageRecord({
|
|
615
774
|
phase: "gen0_initial",
|
|
616
775
|
role: "generator",
|
|
@@ -668,17 +827,33 @@ async function judgePairs(args) {
|
|
|
668
827
|
let outputTokens = 0;
|
|
669
828
|
let totalTokens = 0;
|
|
670
829
|
for (let attempt = 0; attempt <= args.config.retry.invalidJsonRetries; attempt += 1) {
|
|
671
|
-
await args.guards.beforeCall(`${args.generation} comparison call`);
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
830
|
+
const reservation = await args.guards.beforeCall(`${args.generation} comparison call`);
|
|
831
|
+
let result;
|
|
832
|
+
try {
|
|
833
|
+
result = await args.driver.compare({
|
|
834
|
+
task: args.config.task,
|
|
835
|
+
rubric: args.config.rubric,
|
|
836
|
+
model: args.config.judgeModel,
|
|
837
|
+
temperature: args.config.profile.judgeTemperature,
|
|
838
|
+
candidateA: job.presentedA,
|
|
839
|
+
candidateB: job.presentedB,
|
|
840
|
+
prompt: job.prompt
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
await recordFailedInvocation({
|
|
845
|
+
tracker: args.tracker,
|
|
846
|
+
reservation,
|
|
847
|
+
trace: args.trace,
|
|
848
|
+
phase: args.generation === "final" ? "final_judge" : `gen${args.generation}_judge`,
|
|
849
|
+
role: "judge",
|
|
850
|
+
provider: args.config.provider,
|
|
851
|
+
model: args.config.judgeModel,
|
|
852
|
+
error
|
|
853
|
+
});
|
|
854
|
+
throw error;
|
|
855
|
+
}
|
|
856
|
+
const delta = args.tracker.record(result, { provider: args.config.provider, model: args.config.judgeModel }, reservation);
|
|
682
857
|
await args.trace.writeUsage(buildUsageRecord({
|
|
683
858
|
phase: args.generation === "final" ? "final_judge" : `gen${args.generation}_judge`,
|
|
684
859
|
role: "judge",
|
|
@@ -722,11 +897,11 @@ async function judgePairs(args) {
|
|
|
722
897
|
candidateBId: job.presentedB.id,
|
|
723
898
|
presentedAOriginalId: job.presentedA.id,
|
|
724
899
|
presentedBOriginalId: job.presentedB.id,
|
|
725
|
-
winner: parsed
|
|
726
|
-
confidence: parsed
|
|
727
|
-
critiqueForA: parsed
|
|
728
|
-
critiqueForB: parsed
|
|
729
|
-
selectionReason: parsed
|
|
900
|
+
winner: parsed.winner,
|
|
901
|
+
confidence: parsed.confidence,
|
|
902
|
+
critiqueForA: parsed.feedback_a ?? parsed.critique_for_A ?? "",
|
|
903
|
+
critiqueForB: parsed.feedback_b ?? parsed.critique_for_B ?? "",
|
|
904
|
+
selectionReason: parsed.selection_reason ?? "",
|
|
730
905
|
rawOutput,
|
|
731
906
|
model: resultModel,
|
|
732
907
|
provider: resultProvider,
|
|
@@ -760,17 +935,33 @@ async function mutateSurvivors(args) {
|
|
|
760
935
|
const mutants = await runLimitedPhase(args.survivors.map((candidate, index) => async () => {
|
|
761
936
|
const critique = args.critiquesByCandidate.get(candidate.id) ?? "";
|
|
762
937
|
const prompt = mutatePrompt(args.config.task, candidate, critique, args.config.rubric, args.config.promptStyle, args.config.promptOverrides?.mutate);
|
|
763
|
-
await args.guards.beforeCall(`generation ${args.generation} mutation call`);
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
938
|
+
const reservation = await args.guards.beforeCall(`generation ${args.generation} mutation call`);
|
|
939
|
+
let result;
|
|
940
|
+
try {
|
|
941
|
+
result = await args.driver.mutate({
|
|
942
|
+
task: args.config.task,
|
|
943
|
+
rubric: args.config.rubric,
|
|
944
|
+
model: args.config.mutatorModel,
|
|
945
|
+
temperature: args.config.profile.mutateTemperature,
|
|
946
|
+
candidate,
|
|
947
|
+
critique,
|
|
948
|
+
prompt
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
await recordFailedInvocation({
|
|
953
|
+
tracker: args.tracker,
|
|
954
|
+
reservation,
|
|
955
|
+
trace: args.trace,
|
|
956
|
+
phase: `gen${args.generation}_mutate`,
|
|
957
|
+
role: "mutator",
|
|
958
|
+
provider: args.config.provider,
|
|
959
|
+
model: args.config.mutatorModel,
|
|
960
|
+
error
|
|
961
|
+
});
|
|
962
|
+
throw error;
|
|
963
|
+
}
|
|
964
|
+
const delta = args.tracker.record(result, { provider: args.config.provider, model: args.config.mutatorModel }, reservation);
|
|
774
965
|
await args.trace.writeUsage(buildUsageRecord({
|
|
775
966
|
phase: `gen${args.generation}_mutate`,
|
|
776
967
|
role: "mutator",
|
|
@@ -798,15 +989,31 @@ async function mutateSurvivors(args) {
|
|
|
798
989
|
async function maybeFinalize(winner, config, driver, trace, tracker, guards) {
|
|
799
990
|
if (!config.finalizerModel || !driver.finalize)
|
|
800
991
|
return winner.content;
|
|
801
|
-
await guards.beforeCall("finalization call");
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
992
|
+
const reservation = await guards.beforeCall("finalization call");
|
|
993
|
+
let result;
|
|
994
|
+
try {
|
|
995
|
+
result = await driver.finalize({
|
|
996
|
+
task: config.task,
|
|
997
|
+
rubric: config.rubric,
|
|
998
|
+
model: config.finalizerModel,
|
|
999
|
+
candidate: winner,
|
|
1000
|
+
prompt: finalizePrompt(config.task, winner, config.rubric, config.promptStyle, config.promptOverrides?.finalize)
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
catch (error) {
|
|
1004
|
+
await recordFailedInvocation({
|
|
1005
|
+
tracker,
|
|
1006
|
+
reservation,
|
|
1007
|
+
trace,
|
|
1008
|
+
phase: "finalize",
|
|
1009
|
+
role: "finalizer",
|
|
1010
|
+
provider: config.provider,
|
|
1011
|
+
model: config.finalizerModel,
|
|
1012
|
+
error
|
|
1013
|
+
});
|
|
1014
|
+
throw error;
|
|
1015
|
+
}
|
|
1016
|
+
const delta = tracker.record(result, { provider: config.provider, model: config.finalizerModel }, reservation);
|
|
810
1017
|
await trace.writeUsage(buildUsageRecord({
|
|
811
1018
|
phase: "finalize",
|
|
812
1019
|
role: "finalizer",
|
|
@@ -892,18 +1099,30 @@ function buildUsageRecord(args) {
|
|
|
892
1099
|
output_usd: args.delta.outputUsd,
|
|
893
1100
|
total_usd: args.delta.usd,
|
|
894
1101
|
latency_ms: args.result.latencyMs,
|
|
895
|
-
retry_count: args.result.retryCount
|
|
1102
|
+
retry_count: args.result.retryCount,
|
|
1103
|
+
outcome: args.outcome,
|
|
1104
|
+
error_code: args.errorCode
|
|
896
1105
|
};
|
|
897
1106
|
}
|
|
1107
|
+
async function recordFailedInvocation(args) {
|
|
1108
|
+
const delta = args.tracker.failCall(args.reservation);
|
|
1109
|
+
await bestEffortTraceWrite("failed call usage", () => args.trace.writeUsage(buildUsageRecord({
|
|
1110
|
+
phase: args.phase,
|
|
1111
|
+
role: args.role,
|
|
1112
|
+
result: { text: "" },
|
|
1113
|
+
delta,
|
|
1114
|
+
provider: args.provider,
|
|
1115
|
+
model: args.model,
|
|
1116
|
+
outcome: "failed",
|
|
1117
|
+
errorCode: args.error instanceof DeepThonkError ? args.error.code : args.error instanceof Error ? args.error.name : "unknown_error"
|
|
1118
|
+
})));
|
|
1119
|
+
}
|
|
898
1120
|
function enforceBudget(config) {
|
|
899
|
-
const plan = planBudget(config.profile
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const
|
|
904
|
-
const retryHeadroom = judgeCalls * config.retry.invalidJsonRetries;
|
|
905
|
-
const finalizerCalls = config.finalizerModel ? 1 : 0;
|
|
906
|
-
const plannedCalls = plan.calls + finalizerCalls + retryHeadroom;
|
|
1121
|
+
const plan = planBudget(config.profile, {
|
|
1122
|
+
invalidJsonRetries: config.retry.invalidJsonRetries,
|
|
1123
|
+
includeFinalizer: Boolean(config.finalizerModel)
|
|
1124
|
+
});
|
|
1125
|
+
const plannedCalls = plan.calls + plan.finalizer_calls;
|
|
907
1126
|
if (config.budget?.maxCalls !== undefined && plannedCalls > config.budget.maxCalls) {
|
|
908
1127
|
throw new BudgetExceededError(`Planned call count ${plannedCalls} exceeds maxCalls ${config.budget.maxCalls}.`, {
|
|
909
1128
|
code: "budget.planned_calls_exceeded",
|