@deepthonk/core 0.1.3 → 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 +22 -10
- package/dist/artifacts.js +158 -10
- package/dist/artifacts.js.map +1 -1
- package/dist/bradleyTerry.js +20 -5
- 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/index.d.ts +2 -0
- package/dist/index.js +2 -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/pairScheduler.js +11 -42
- package/dist/pairScheduler.js.map +1 -1
- package/dist/phaseRunner.d.ts +1 -0
- package/dist/phaseRunner.js +26 -0
- package/dist/phaseRunner.js.map +1 -0
- 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 -0
- package/dist/runner.js +341 -88
- package/dist/runner.js.map +1 -1
- package/dist/schemas.d.ts +371 -1
- package/dist/schemas.js +44 -1
- package/dist/schemas.js.map +1 -1
- package/dist/services.d.ts +1 -0
- package/dist/services.js +46 -11
- package/dist/services.js.map +1 -1
- package/dist/traceStore.d.ts +5 -1
- package/dist/traceStore.js +52 -16
- package/dist/traceStore.js.map +1 -1
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -8,11 +8,12 @@ import { planBudget, validateProfile } from "./budget.js";
|
|
|
8
8
|
import { BudgetExceededError, CancelledError, ConfigError, DeepThonkError } from "./errors.js";
|
|
9
9
|
import { runArtifactFiles } from "./artifacts.js";
|
|
10
10
|
import { parseJsonObject } from "./json.js";
|
|
11
|
-
import {
|
|
11
|
+
import { claimRunLockOwnership, emptyUsage, releaseRunLock, verifyRunLockClaim } from "./lifecycle.js";
|
|
12
12
|
import { makeKRegularPairs } from "./pairScheduler.js";
|
|
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,24 +30,42 @@ 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();
|
|
40
|
+
let ownsLock = false;
|
|
41
|
+
let ownedLockClaimId;
|
|
42
|
+
let verifiedPreclaim = false;
|
|
37
43
|
if (!resumeState) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
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;
|
|
65
|
+
}
|
|
48
66
|
}
|
|
49
67
|
let stopped = false;
|
|
68
|
+
let caughtError;
|
|
50
69
|
const writeStatus = async (state, phase, extra = {}) => {
|
|
51
70
|
await trace.writeStatus({
|
|
52
71
|
job_id: control.jobId,
|
|
@@ -77,7 +96,7 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
77
96
|
catch (error) {
|
|
78
97
|
if (error instanceof BudgetExceededError) {
|
|
79
98
|
stopped = true;
|
|
80
|
-
await trace.event({ type: "budget.exceeded", phase, usage: cloneUsage(tracker.usage), error: serializeRunError(error) });
|
|
99
|
+
await bestEffortTraceWrite("budget exceeded event", () => trace.event({ type: "budget.exceeded", phase, usage: cloneUsage(tracker.usage), error: serializeRunError(error) }));
|
|
81
100
|
}
|
|
82
101
|
throw error;
|
|
83
102
|
}
|
|
@@ -88,6 +107,7 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
88
107
|
throw new BudgetExceededError(`Run already stopped before ${phase}.`, { code: "budget.stopped" });
|
|
89
108
|
await assertNotCancelled(phase);
|
|
90
109
|
await assertBudget(phase);
|
|
110
|
+
return tracker.reserveCall(phase);
|
|
91
111
|
},
|
|
92
112
|
async afterCall(phase) {
|
|
93
113
|
await assertNotCancelled(phase);
|
|
@@ -95,6 +115,18 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
95
115
|
}
|
|
96
116
|
};
|
|
97
117
|
try {
|
|
118
|
+
if (!resumeState) {
|
|
119
|
+
await trace.init({ ...config, version: await currentPackageVersion() }, runId, { allowOwnedLock: ownsLock || verifiedPreclaim, pendingJobId: verifiedPreclaim ? control.jobId : undefined });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
await trace.event({
|
|
123
|
+
type: "run.resumed",
|
|
124
|
+
run_id: runId,
|
|
125
|
+
resumed_at: new Date().toISOString(),
|
|
126
|
+
next_phase: resumeState.nextPhase.phase,
|
|
127
|
+
generation: resumeState.nextPhase.generation
|
|
128
|
+
});
|
|
129
|
+
}
|
|
98
130
|
await writeStatus("running", resumeState ? "resume_replay" : "initialized");
|
|
99
131
|
let population;
|
|
100
132
|
if (isResumePhaseCompleted(resumeState, "initial_generation")) {
|
|
@@ -238,12 +270,28 @@ export async function runDeepThonk(configInput, driver, control = {}, resumeStat
|
|
|
238
270
|
};
|
|
239
271
|
}
|
|
240
272
|
catch (error) {
|
|
273
|
+
caughtError = error;
|
|
241
274
|
const serialized = serializeRunError(error);
|
|
242
275
|
const state = error instanceof CancelledError ? "cancelled" : error instanceof BudgetExceededError ? "budget_exceeded" : "failed";
|
|
243
|
-
await trace.event({ type: `run.${state}`, run_id: runId, error: serialized, updated_at: new Date().toISOString() });
|
|
244
|
-
await writeStatus(state, state, { error: serialized });
|
|
276
|
+
await bestEffortTraceWrite("run failure event", () => trace.event({ type: `run.${state}`, run_id: runId, error: serialized, updated_at: new Date().toISOString() }));
|
|
277
|
+
await bestEffortTraceWrite("run failure status", () => writeStatus(state, state, { error: serialized }));
|
|
245
278
|
throw error;
|
|
246
279
|
}
|
|
280
|
+
finally {
|
|
281
|
+
if (ownsLock) {
|
|
282
|
+
try {
|
|
283
|
+
await releaseRunLock(config.runDir, ownedLockClaimId);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (caughtError !== undefined) {
|
|
287
|
+
process.stderr.write(`deepthonk: failed to release run lock: ${error.message}\n`);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
247
295
|
}
|
|
248
296
|
export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
249
297
|
const existingSummary = await readOptionalJson(runDir, runArtifactFiles.summary);
|
|
@@ -251,6 +299,7 @@ export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
|
251
299
|
resumeConfigError("Run already has summary.json; nothing to resume.", "resume.already_complete", "Use deepthonk inspect/result to read the completed run.");
|
|
252
300
|
}
|
|
253
301
|
const rawConfig = await readRequiredConfig(runDir);
|
|
302
|
+
assertNoLegacyRedactedBudget(rawConfig);
|
|
254
303
|
const currentVersion = await currentPackageVersion();
|
|
255
304
|
if (!sameMajorMinor(typeof rawConfig.version === "string" ? rawConfig.version : undefined, currentVersion)) {
|
|
256
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.");
|
|
@@ -263,7 +312,7 @@ export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
|
263
312
|
assertResumeProviderMatches(rawConfig, config, driver, options);
|
|
264
313
|
assertNoPruneInProgress(runDir);
|
|
265
314
|
const lockRunId = resolveResumeRunId(config, undefined, []);
|
|
266
|
-
const claimed = await
|
|
315
|
+
const claimed = await claimRunLockOwnership(runDir, lockRunId);
|
|
267
316
|
if (!claimed) {
|
|
268
317
|
throw new ConfigError(`Run directory is already claimed: ${runDir}`, {
|
|
269
318
|
code: "run.directory_locked",
|
|
@@ -283,12 +332,13 @@ export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
|
283
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.");
|
|
284
333
|
}
|
|
285
334
|
const planStatus = toResumePlanStatus(runDir, runId, plan);
|
|
286
|
-
if (options.dryRun)
|
|
287
|
-
return planStatus;
|
|
288
335
|
const pruned = pruneTraceToPlan(trace, plan);
|
|
336
|
+
assertResumeRunIdsAgree(runId, config, status, pruned);
|
|
289
337
|
const populationByGeneration = buildPopulationMap(config, pruned.populations, pruned.candidates, plan);
|
|
290
338
|
const comparisonsByGeneration = groupComparisons(pruned.comparisons);
|
|
291
|
-
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;
|
|
292
342
|
const tracker = replayBudgetUsage(config, pruned.usage);
|
|
293
343
|
const startedAt = status?.started_at ?? new Date().toISOString();
|
|
294
344
|
await new TraceStore(runDir).writeStatus({
|
|
@@ -314,7 +364,26 @@ export async function resumeDeepThonk(runDir, driver, options = {}) {
|
|
|
314
364
|
});
|
|
315
365
|
}
|
|
316
366
|
finally {
|
|
317
|
-
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.");
|
|
318
387
|
}
|
|
319
388
|
}
|
|
320
389
|
async function markPhaseCompleted(trace, phase, generation) {
|
|
@@ -396,10 +465,19 @@ function assertStoredOutputConfigComplete(rawConfig) {
|
|
|
396
465
|
}
|
|
397
466
|
}
|
|
398
467
|
function assertResumeProviderMatches(rawConfig, config, driver, options) {
|
|
468
|
+
assertProviderReplayInternalConsistency(config);
|
|
399
469
|
const runtimeProvider = options.provider ?? providerLabel(driver) ?? config.provider;
|
|
400
470
|
if (runtimeProvider !== config.provider) {
|
|
401
471
|
providerMismatch(`Cannot resume provider ${config.provider} with runtime provider ${runtimeProvider}.`);
|
|
402
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
|
+
}
|
|
403
481
|
const expectedRoutes = expectedProviderRoutes(rawConfig, config);
|
|
404
482
|
const actualRoutes = runtimeProviderRoutes(driver);
|
|
405
483
|
for (const role of providerRoles) {
|
|
@@ -421,8 +499,92 @@ function assertResumeProviderMatches(rawConfig, config, driver, options) {
|
|
|
421
499
|
}
|
|
422
500
|
}
|
|
423
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
|
+
}
|
|
424
571
|
const providerRoles = ["generator", "mutator", "judge", "finalizer"];
|
|
425
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
|
+
}
|
|
426
588
|
const providers = rawConfig.providers;
|
|
427
589
|
if (!isRecord(providers))
|
|
428
590
|
return {};
|
|
@@ -457,6 +619,16 @@ function runtimeProviderRoutes(driver) {
|
|
|
457
619
|
}
|
|
458
620
|
return routes;
|
|
459
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
|
+
}
|
|
460
632
|
function providerBaseUrl(driver) {
|
|
461
633
|
const value = driver;
|
|
462
634
|
if (typeof value.baseUrl === "string")
|
|
@@ -515,8 +687,8 @@ function isLiveWorker(pid) {
|
|
|
515
687
|
process.kill(pid, 0);
|
|
516
688
|
return true;
|
|
517
689
|
}
|
|
518
|
-
catch {
|
|
519
|
-
return
|
|
690
|
+
catch (error) {
|
|
691
|
+
return error.code !== "ESRCH";
|
|
520
692
|
}
|
|
521
693
|
}
|
|
522
694
|
export function rankPopulation(population, scores) {
|
|
@@ -566,23 +738,38 @@ async function traceDiscardedCandidates(trace, ranked, carriedIds, bottomQuartil
|
|
|
566
738
|
}
|
|
567
739
|
}
|
|
568
740
|
async function generateInitialPopulation(config, driver, trace, rng, runId, tracker, guards) {
|
|
569
|
-
const limit = pLimit(config.concurrency.generate);
|
|
570
741
|
const jobs = Array.from({ length: config.profile.n }, (_, index) => ({
|
|
571
742
|
index,
|
|
572
743
|
id: `g0_c${index}_${rng.id("c")}`,
|
|
573
744
|
prompt: generatePrompt(config.task, config.rubric, config.promptStyle, config.promptOverrides?.generate)
|
|
574
745
|
}));
|
|
575
|
-
const candidates = await
|
|
576
|
-
await guards.beforeCall("initial population call");
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
746
|
+
const candidates = await runLimitedPhase(jobs.map((job) => async () => {
|
|
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);
|
|
586
773
|
await trace.writeUsage(buildUsageRecord({
|
|
587
774
|
phase: "gen0_initial",
|
|
588
775
|
role: "generator",
|
|
@@ -603,12 +790,11 @@ async function generateInitialPopulation(config, driver, trace, rng, runId, trac
|
|
|
603
790
|
await trace.writeCandidate(candidate);
|
|
604
791
|
await trace.event({ type: "candidate.generated", run_id: runId, candidate_id: candidate.id });
|
|
605
792
|
return candidate;
|
|
606
|
-
}))
|
|
793
|
+
}), config.concurrency.generate);
|
|
607
794
|
return candidates;
|
|
608
795
|
}
|
|
609
796
|
async function judgePairs(args) {
|
|
610
797
|
const byId = new Map(args.population.map((candidate) => [candidate.id, candidate]));
|
|
611
|
-
const limit = pLimit(args.config.concurrency.judge);
|
|
612
798
|
const jobs = args.pairs.map((pair, pairIndex) => {
|
|
613
799
|
const originalA = byId.get(pair.a);
|
|
614
800
|
const originalB = byId.get(pair.b);
|
|
@@ -627,7 +813,7 @@ async function judgePairs(args) {
|
|
|
627
813
|
prompt: comparePrompt(args.config.task, presentedA, presentedB, args.config.rubric, args.config.promptStyle, args.config.promptOverrides?.compare)
|
|
628
814
|
};
|
|
629
815
|
});
|
|
630
|
-
const comparisons = await
|
|
816
|
+
const comparisons = await runLimitedPhase(jobs.map((job) => async () => {
|
|
631
817
|
let parsed;
|
|
632
818
|
let rawOutput;
|
|
633
819
|
let invalidJson = false;
|
|
@@ -641,17 +827,33 @@ async function judgePairs(args) {
|
|
|
641
827
|
let outputTokens = 0;
|
|
642
828
|
let totalTokens = 0;
|
|
643
829
|
for (let attempt = 0; attempt <= args.config.retry.invalidJsonRetries; attempt += 1) {
|
|
644
|
-
await args.guards.beforeCall(`${args.generation} comparison call`);
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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);
|
|
655
857
|
await args.trace.writeUsage(buildUsageRecord({
|
|
656
858
|
phase: args.generation === "final" ? "final_judge" : `gen${args.generation}_judge`,
|
|
657
859
|
role: "judge",
|
|
@@ -695,11 +897,11 @@ async function judgePairs(args) {
|
|
|
695
897
|
candidateBId: job.presentedB.id,
|
|
696
898
|
presentedAOriginalId: job.presentedA.id,
|
|
697
899
|
presentedBOriginalId: job.presentedB.id,
|
|
698
|
-
winner: parsed
|
|
699
|
-
confidence: parsed
|
|
700
|
-
critiqueForA: parsed
|
|
701
|
-
critiqueForB: parsed
|
|
702
|
-
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 ?? "",
|
|
703
905
|
rawOutput,
|
|
704
906
|
model: resultModel,
|
|
705
907
|
provider: resultProvider,
|
|
@@ -726,25 +928,40 @@ async function judgePairs(args) {
|
|
|
726
928
|
generation: args.generation
|
|
727
929
|
});
|
|
728
930
|
return comparison;
|
|
729
|
-
}))
|
|
931
|
+
}), args.config.concurrency.judge);
|
|
730
932
|
return comparisons;
|
|
731
933
|
}
|
|
732
934
|
async function mutateSurvivors(args) {
|
|
733
|
-
const
|
|
734
|
-
const mutants = await Promise.all(args.survivors.map((candidate, index) => limit(async () => {
|
|
935
|
+
const mutants = await runLimitedPhase(args.survivors.map((candidate, index) => async () => {
|
|
735
936
|
const critique = args.critiquesByCandidate.get(candidate.id) ?? "";
|
|
736
937
|
const prompt = mutatePrompt(args.config.task, candidate, critique, args.config.rubric, args.config.promptStyle, args.config.promptOverrides?.mutate);
|
|
737
|
-
await args.guards.beforeCall(`generation ${args.generation} mutation call`);
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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);
|
|
748
965
|
await args.trace.writeUsage(buildUsageRecord({
|
|
749
966
|
phase: `gen${args.generation}_mutate`,
|
|
750
967
|
role: "mutator",
|
|
@@ -766,21 +983,37 @@ async function mutateSurvivors(args) {
|
|
|
766
983
|
await args.trace.writeCandidate(mutant);
|
|
767
984
|
await args.trace.event({ type: "candidate.mutated", candidate_id: mutant.id, parent_id: mutant.parentId });
|
|
768
985
|
return mutant;
|
|
769
|
-
}))
|
|
986
|
+
}), args.config.concurrency.mutate);
|
|
770
987
|
return mutants;
|
|
771
988
|
}
|
|
772
989
|
async function maybeFinalize(winner, config, driver, trace, tracker, guards) {
|
|
773
990
|
if (!config.finalizerModel || !driver.finalize)
|
|
774
991
|
return winner.content;
|
|
775
|
-
await guards.beforeCall("finalization call");
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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);
|
|
784
1017
|
await trace.writeUsage(buildUsageRecord({
|
|
785
1018
|
phase: "finalize",
|
|
786
1019
|
role: "finalizer",
|
|
@@ -866,18 +1099,30 @@ function buildUsageRecord(args) {
|
|
|
866
1099
|
output_usd: args.delta.outputUsd,
|
|
867
1100
|
total_usd: args.delta.usd,
|
|
868
1101
|
latency_ms: args.result.latencyMs,
|
|
869
|
-
retry_count: args.result.retryCount
|
|
1102
|
+
retry_count: args.result.retryCount,
|
|
1103
|
+
outcome: args.outcome,
|
|
1104
|
+
error_code: args.errorCode
|
|
870
1105
|
};
|
|
871
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
|
+
}
|
|
872
1120
|
function enforceBudget(config) {
|
|
873
|
-
const plan = planBudget(config.profile
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const
|
|
878
|
-
const retryHeadroom = judgeCalls * config.retry.invalidJsonRetries;
|
|
879
|
-
const finalizerCalls = config.finalizerModel ? 1 : 0;
|
|
880
|
-
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;
|
|
881
1126
|
if (config.budget?.maxCalls !== undefined && plannedCalls > config.budget.maxCalls) {
|
|
882
1127
|
throw new BudgetExceededError(`Planned call count ${plannedCalls} exceeds maxCalls ${config.budget.maxCalls}.`, {
|
|
883
1128
|
code: "budget.planned_calls_exceeded",
|
|
@@ -891,6 +1136,14 @@ function cloneUsage(usage) {
|
|
|
891
1136
|
...usage
|
|
892
1137
|
};
|
|
893
1138
|
}
|
|
1139
|
+
async function bestEffortTraceWrite(label, action) {
|
|
1140
|
+
try {
|
|
1141
|
+
await action();
|
|
1142
|
+
}
|
|
1143
|
+
catch (error) {
|
|
1144
|
+
process.stderr.write(`deepthonk: failed to persist ${label}: ${error.message}\n`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
894
1147
|
function serializeRunError(error) {
|
|
895
1148
|
if (error instanceof DeepThonkError) {
|
|
896
1149
|
return {
|