@tuned-tensor/local 0.2.8 → 0.3.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +84 -204
  3. package/dist/artifacts.d.ts +3 -4
  4. package/dist/artifacts.js +55 -33
  5. package/dist/artifacts.js.map +1 -1
  6. package/dist/compare.d.ts +1 -8
  7. package/dist/compare.js +1 -23
  8. package/dist/compare.js.map +1 -1
  9. package/dist/contracts.d.ts +59 -366
  10. package/dist/contracts.js +73 -224
  11. package/dist/contracts.js.map +1 -1
  12. package/dist/dataset.d.ts +2 -4
  13. package/dist/dataset.js +31 -125
  14. package/dist/dataset.js.map +1 -1
  15. package/dist/doctor.d.ts +2 -3
  16. package/dist/doctor.js +23 -161
  17. package/dist/doctor.js.map +1 -1
  18. package/dist/evaluation.d.ts +11 -71
  19. package/dist/evaluation.js +212 -572
  20. package/dist/evaluation.js.map +1 -1
  21. package/dist/huggingface-cache.d.ts +8 -7
  22. package/dist/huggingface-cache.js +16 -8
  23. package/dist/huggingface-cache.js.map +1 -1
  24. package/dist/index.d.ts +0 -10
  25. package/dist/index.js +205 -628
  26. package/dist/index.js.map +1 -1
  27. package/dist/local-project.d.ts +1 -11
  28. package/dist/local-project.js +33 -61
  29. package/dist/local-project.js.map +1 -1
  30. package/dist/model-registry.d.ts +3 -16
  31. package/dist/model-registry.js +76 -292
  32. package/dist/model-registry.js.map +1 -1
  33. package/dist/model-server.d.ts +1 -2
  34. package/dist/model-server.js +9 -18
  35. package/dist/model-server.js.map +1 -1
  36. package/dist/orchestrator.d.ts +11 -25
  37. package/dist/orchestrator.js +246 -566
  38. package/dist/orchestrator.js.map +1 -1
  39. package/dist/prefetch.d.ts +1 -5
  40. package/dist/prefetch.js +14 -19
  41. package/dist/prefetch.js.map +1 -1
  42. package/dist/process-runner.d.ts +10 -29
  43. package/dist/process-runner.js +63 -169
  44. package/dist/process-runner.js.map +1 -1
  45. package/dist/process-training.d.ts +0 -1
  46. package/dist/process-training.js +10 -87
  47. package/dist/process-training.js.map +1 -1
  48. package/dist/store.d.ts +1 -3
  49. package/dist/store.js +92 -290
  50. package/dist/store.js.map +1 -1
  51. package/docs/architecture.md +87 -152
  52. package/docs/spark.md +66 -97
  53. package/examples/dry-runner.json +14 -0
  54. package/examples/local-runner.json +8 -6
  55. package/examples/smoke-spec.json +41 -0
  56. package/package.json +6 -6
  57. package/training/local-runner/pyproject.toml +0 -5
  58. package/training/local-runner/src/evaluate.py +89 -234
  59. package/training/local-runner/src/model_contract.py +47 -0
  60. package/training/local-runner/src/prefetch.py +18 -4
  61. package/training/local-runner/src/serve.py +54 -115
  62. package/training/local-runner/src/sft_data.py +97 -0
  63. package/training/local-runner/src/train.py +146 -346
  64. package/training/local-runner/uv.lock +0 -1197
  65. package/dist/labeling-sanitize.d.ts +0 -31
  66. package/dist/labeling-sanitize.js +0 -158
  67. package/dist/labeling-sanitize.js.map +0 -1
  68. package/dist/labeling.d.ts +0 -155
  69. package/dist/labeling.js +0 -496
  70. package/dist/labeling.js.map +0 -1
  71. package/dist/openrouter.d.ts +0 -29
  72. package/dist/openrouter.js +0 -66
  73. package/dist/openrouter.js.map +0 -1
  74. package/dist/server.d.ts +0 -9
  75. package/dist/server.js +0 -211
  76. package/dist/server.js.map +0 -1
  77. package/docs/local-workflow-remediation-2026-07-13.md +0 -130
  78. package/docs/local-workflow-ux-review-2026-07-13.md +0 -458
  79. package/examples/dpo-preferences.jsonl +0 -2
  80. package/examples/dpo-run-request.json +0 -34
  81. package/examples/smoke-run-request.json +0 -34
  82. package/training/local-runner/src/train_dpo.py +0 -286
@@ -1,14 +1,14 @@
1
- import { copyFile, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
1
+ import { lstat, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
2
2
  import { createReadStream } from "node:fs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { performance } from "node:perf_hooks";
5
5
  import { homedir } from "node:os";
6
- import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { basename, dirname, join, relative, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { defaultArtifactPrefix, fileUri, assertArtifactManifest, ARTIFACT_WORKFLOW_LOCK_FILE, claimRunArtifactDirectory, prepareRunDirectories, readJson, resolveRunArtifacts, writeArtifactManifest, writeFileAtomic, writeJsonAtomic, } from "./artifacts.js";
9
9
  import { evalReportSchema, fineTuneRunRequestSchema, localRunnerConfigSchema, runReportSchema, trainingReportSchema, } from "./contracts.js";
10
- import { buildSystemMessage, compileSpecToJsonl, examplesFromChatJsonl, examplesFromSpec, localAssetPathsFromChatJsonl, normalizeChatJsonlForRelocation, } from "./dataset.js";
11
- import { compareEvalReports, deriveSampleSeed, evaluateExamples, rescoreEvalReport, splitSpecExamples } from "./evaluation.js";
10
+ import { buildSystemMessage, compileSpecToJsonl, examplesFromChatJsonl, examplesFromSpec, normalizeChatJsonlForRelocation, } from "./dataset.js";
11
+ import { INFERENCE_PROTOCOL_VERSION, compareEvalReports, deriveSampleSeed, evaluateExamples, splitSpecExamples, } from "./evaluation.js";
12
12
  import { launchProcessTraining } from "./process-training.js";
13
13
  import { assertUsableModelArtifact, localModelArtifactPath } from "./model-registry.js";
14
14
  import { ProcessCancelledError } from "./process-runner.js";
@@ -27,12 +27,9 @@ export async function loadLocalRunnerConfig(path) {
27
27
  const configPath = resolve(path);
28
28
  const base = dirname(configPath);
29
29
  const config = localRunnerConfigSchema.parse(await loadJsonFile(configPath));
30
- const configPathValue = (value, preserveBundled = false) => {
30
+ const configPathValue = (value) => {
31
31
  if (!value)
32
32
  return undefined;
33
- if (preserveBundled && (value === "training/local-runner" || value.startsWith("training/local-runner/"))) {
34
- return value;
35
- }
36
33
  if (value === "~")
37
34
  return homedir();
38
35
  if (value.startsWith("~/"))
@@ -43,25 +40,10 @@ export async function loadLocalRunnerConfig(path) {
43
40
  ...config,
44
41
  artifactRoot: configPathValue(config.artifactRoot),
45
42
  storeRoot: configPathValue(config.storeRoot),
46
- training: {
47
- ...config.training,
48
- cwd: configPathValue(config.training.cwd),
49
- project: configPathValue(config.training.project, true),
50
- script: configPathValue(config.training.script, true),
51
- },
52
43
  paths: {
53
44
  baseModel: configPathValue(config.paths.baseModel),
54
45
  modelCache: configPathValue(config.paths.modelCache),
55
46
  },
56
- evaluation: {
57
- ...config.evaluation,
58
- inference: {
59
- ...config.evaluation.inference,
60
- cwd: configPathValue(config.evaluation.inference.cwd),
61
- project: configPathValue(config.evaluation.inference.project, true),
62
- script: configPathValue(config.evaluation.inference.script, true),
63
- },
64
- },
65
47
  };
66
48
  }
67
49
  function elapsed(started) {
@@ -80,26 +62,146 @@ async function pathExists(path) {
80
62
  return false;
81
63
  }
82
64
  }
83
- function selectPrebuiltEvaluation(dataset) {
65
+ function selectPrebuiltEvaluationSplit(dataset) {
84
66
  if (!dataset)
85
67
  throw new Error("dataset_prebuilt is required");
86
- if (dataset.test)
87
- return { path: stripFileUri(dataset.test), split: "prebuilt_test" };
88
68
  if (dataset.validation)
89
- return { path: stripFileUri(dataset.validation), split: "prebuilt_validation" };
90
- return { path: stripFileUri(dataset.training), split: "prebuilt_training" };
69
+ return "prebuilt_validation";
70
+ if (dataset.test)
71
+ return "prebuilt_test";
72
+ throw new Error("dataset_prebuilt requires a distinct validation or test split");
91
73
  }
92
- function selectDpoEvaluation(request) {
93
- if (request.dataset_prebuilt?.test) {
94
- return { path: stripFileUri(request.dataset_prebuilt.test), split: "prebuilt_test" };
74
+ function assertMatchingDatasetSystems(jsonl, expectedSystem, split) {
75
+ for (const [index, line] of jsonl.split(/\r?\n/).entries()) {
76
+ if (!line.trim())
77
+ continue;
78
+ const row = JSON.parse(line);
79
+ const actualSystem = row.messages.find((message) => message.role === "system")?.content ?? "";
80
+ if (actualSystem !== expectedSystem) {
81
+ throw new Error(`Prebuilt ${split} row ${index + 1} system message must match the behavior spec system message.`);
82
+ }
95
83
  }
96
- if (request.dataset_prebuilt?.validation) {
97
- return { path: stripFileUri(request.dataset_prebuilt.validation), split: "prebuilt_validation" };
84
+ }
85
+ function addDryRunPlaceholders(request, dryRun) {
86
+ if (!dryRun || !request || typeof request !== "object" || Array.isArray(request))
87
+ return request;
88
+ const candidate = request;
89
+ if (candidate.dataset_prebuilt)
90
+ return request;
91
+ const snapshot = candidate.spec_snapshot;
92
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
93
+ return request;
94
+ const examples = snapshot.examples;
95
+ if (!Array.isArray(examples) || examples.length > 0)
96
+ return request;
97
+ return {
98
+ ...candidate,
99
+ spec_snapshot: {
100
+ ...snapshot,
101
+ examples: [
102
+ { input: "Dry-run placeholder input A", output: "Dry-run placeholder output A" },
103
+ { input: "Dry-run placeholder input B", output: "Dry-run placeholder output B" },
104
+ ],
105
+ },
106
+ };
107
+ }
108
+ async function validateDatasetInputs(request, config) {
109
+ const inputIdentity = (value) => value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
110
+ const evalSampleSeed = config.evaluation.sampleSeed
111
+ ?? deriveSampleSeed(request.behavior_spec_id);
112
+ if (!request.dataset_prebuilt) {
113
+ const inlineExamples = examplesFromSpec(request.spec_snapshot);
114
+ const seenInputs = new Map();
115
+ for (const [index, example] of inlineExamples.entries()) {
116
+ const identity = inputIdentity(example.input);
117
+ const previous = seenInputs.get(identity);
118
+ if (previous !== undefined) {
119
+ throw new Error(`Inline examples ${previous + 1} and ${index + 1} have duplicate inputs; `
120
+ + "training and held-out evaluation require distinct prompts.");
121
+ }
122
+ seenInputs.set(identity, index);
123
+ }
124
+ if (!config.dryRun && inlineExamples.length < 2) {
125
+ throw new Error("A real local fine-tune requires at least 2 inline examples so training and evaluation use distinct data.");
126
+ }
127
+ const split = splitSpecExamples(inlineExamples, evalSampleSeed);
128
+ const hasHoldout = split.holdout.length > 0;
129
+ const trainingExamples = hasHoldout ? split.train : inlineExamples;
130
+ const evaluationExamples = hasHoldout ? split.holdout : inlineExamples;
131
+ return {
132
+ trainingJsonl: compileSpecToJsonl({ ...request.spec_snapshot, examples: trainingExamples }),
133
+ trainingExampleCount: trainingExamples.length,
134
+ evaluationExamples,
135
+ evalSplit: hasHoldout ? "spec_holdout" : "spec_examples",
136
+ };
137
+ }
138
+ const dataset = request.dataset_prebuilt;
139
+ const splitEntries = [
140
+ ["training", dataset.training],
141
+ ["validation", dataset.validation],
142
+ ["test", dataset.test],
143
+ ];
144
+ const normalized = new Map();
145
+ const expectedSystem = buildSystemMessage(request.spec_snapshot);
146
+ for (const [name, value] of splitEntries) {
147
+ if (!value)
148
+ continue;
149
+ const path = stripFileUri(value);
150
+ const jsonl = await normalizeChatJsonlForRelocation(path);
151
+ assertMatchingDatasetSystems(jsonl, expectedSystem, name);
152
+ normalized.set(name, {
153
+ jsonl,
154
+ examples: await examplesFromChatJsonl(path),
155
+ });
156
+ }
157
+ const evaluationSplit = selectPrebuiltEvaluationSplit(dataset);
158
+ const evaluationKey = evaluationSplit === "prebuilt_validation"
159
+ ? "validation"
160
+ : "test";
161
+ const training = normalized.get("training");
162
+ const evaluationData = normalized.get(evaluationKey);
163
+ if (!training || !evaluationData) {
164
+ throw new Error("Unable to load the configured prebuilt training and evaluation datasets.");
165
+ }
166
+ const trainingInputs = new Set(training.examples.map((example) => inputIdentity(example.input)));
167
+ for (const splitName of ["validation", "test"]) {
168
+ const split = normalized.get(splitName);
169
+ if (!split)
170
+ continue;
171
+ const splitInputs = new Set();
172
+ for (const example of split.examples) {
173
+ const identity = inputIdentity(example.input);
174
+ if (splitInputs.has(identity)) {
175
+ throw new Error(`Prebuilt ${splitName} data contains duplicate inputs; `
176
+ + "evaluation prompts must be unique so metrics and comparisons remain well-defined.");
177
+ }
178
+ splitInputs.add(identity);
179
+ }
180
+ const overlap = split.examples.filter((example) => trainingInputs.has(inputIdentity(example.input)));
181
+ if (overlap.length > 0) {
182
+ throw new Error(`Prebuilt training and ${splitName} data overlap on ${overlap.length} input(s); `
183
+ + "evaluation examples must be held out from training.");
184
+ }
98
185
  }
99
- return { split: "spec_examples" };
186
+ return {
187
+ trainingJsonl: training.jsonl,
188
+ trainingExampleCount: training.examples.length,
189
+ evaluationExamples: evaluationData.examples,
190
+ evalSplit: evaluationSplit,
191
+ };
192
+ }
193
+ /**
194
+ * Parses the public contracts and fully validates every dataset before a run
195
+ * lock, store record, or artifact-directory claim is created.
196
+ */
197
+ export async function validateLocalFineTuneInput(input) {
198
+ const config = localRunnerConfigSchema.parse(input.config);
199
+ const request = fineTuneRunRequestSchema.parse(addDryRunPlaceholders(input.request, config.dryRun));
200
+ await validateDatasetInputs(request, config);
201
+ return { request, config };
100
202
  }
101
203
  function artifactPrefix(request) {
102
- return request.artifacts?.prefix ?? defaultArtifactPrefix({
204
+ return defaultArtifactPrefix({
103
205
  userId: request.user_id,
104
206
  behaviorSpecId: request.behavior_spec_id,
105
207
  runId: request.run_id,
@@ -134,33 +236,6 @@ async function packageVersion() {
134
236
  return "unknown";
135
237
  }
136
238
  }
137
- async function effectiveConfigFingerprint(config) {
138
- const bundledProject = resolve(packageRoot, "training/local-runner");
139
- const projectRoots = [...new Set([
140
- bundledProject,
141
- config.training.project === "training/local-runner" ? bundledProject : config.training.project,
142
- config.evaluation.inference.project === "training/local-runner"
143
- ? bundledProject
144
- : config.evaluation.inference.project,
145
- ].filter((value) => Boolean(value)).map((value) => resolve(value)))];
146
- const projectFingerprints = {};
147
- for (const project of projectRoots) {
148
- projectFingerprints[project] = {
149
- pyproject: await hashFileIfPresent(resolve(project, "pyproject.toml")),
150
- uv_lock: await hashFileIfPresent(resolve(project, "uv.lock")),
151
- };
152
- }
153
- return hashJson({
154
- config,
155
- runtime: {
156
- tt_local_version: await packageVersion(),
157
- node_version: process.version,
158
- platform: process.platform,
159
- architecture: process.arch,
160
- project_fingerprints: projectFingerprints,
161
- },
162
- });
163
- }
164
239
  async function runtimeFingerprint() {
165
240
  const bundledProject = resolve(packageRoot, "training/local-runner");
166
241
  return hashJson({
@@ -179,14 +254,9 @@ function preparedSourceFingerprint(args) {
179
254
  preparation_config: args.preparationConfig,
180
255
  base_model_revision: args.baseModelRevision ?? null,
181
256
  base_model_fingerprint: args.baseModelFingerprint ?? null,
182
- parent_model_fingerprint: args.parentModelFingerprint ?? null,
183
257
  dataset_fingerprints: args.datasetFingerprints,
184
258
  });
185
259
  }
186
- async function countJsonlRows(path) {
187
- const text = await readFile(path, "utf8");
188
- return text.split(/\r?\n/).filter((line) => line.trim()).length;
189
- }
190
260
  async function hashFile(path) {
191
261
  const hash = createHash("sha256");
192
262
  for await (const chunk of createReadStream(path))
@@ -206,21 +276,6 @@ async function datasetFingerprints(request) {
206
276
  continue;
207
277
  const datasetPath = stripFileUri(value);
208
278
  fingerprints[key] = await hashFile(datasetPath);
209
- for (const assetPath of await localAssetPathsFromChatJsonl(datasetPath)) {
210
- fingerprints[`${key}_asset:${assetPath}`] = await hashFile(assetPath);
211
- }
212
- }
213
- for (const [exampleIndex, example] of request.spec_snapshot.examples.entries()) {
214
- for (const [assetIndex, asset] of (example.input_assets ?? []).entries()) {
215
- const value = asset.image ?? asset.data_uri ?? asset.uri ?? asset.path;
216
- if (!value
217
- || value.startsWith("data:")
218
- || (/^[a-z][a-z0-9+.-]*:/i.test(value) && !value.startsWith("file://"))) {
219
- continue;
220
- }
221
- const assetPath = stripFileUri(value);
222
- fingerprints[`spec_asset:${exampleIndex}:${assetIndex}:${assetPath}`] = await hashFile(assetPath);
223
- }
224
279
  }
225
280
  return fingerprints;
226
281
  }
@@ -246,44 +301,6 @@ async function resolveBaseModelRevision(request, config) {
246
301
  return undefined;
247
302
  }
248
303
  }
249
- async function fingerprintLocalArtifact(uri) {
250
- const root = localModelArtifactPath(uri);
251
- const metadata = await lstat(root);
252
- if (metadata.isSymbolicLink()) {
253
- throw new Error(`Parent model artifact must not be a symbolic link: ${root}`);
254
- }
255
- if (metadata.isFile()) {
256
- return hashJson({ kind: "file", size_bytes: metadata.size, sha256: await hashFile(root) });
257
- }
258
- if (!metadata.isDirectory())
259
- throw new Error(`Parent model artifact is not a file or directory: ${root}`);
260
- const files = [];
261
- const visit = async (directory) => {
262
- for (const entry of await readdir(directory, { withFileTypes: true })) {
263
- const child = resolve(directory, entry.name);
264
- if (entry.isSymbolicLink()) {
265
- throw new Error(`Parent model artifact must not contain symbolic links: ${child}`);
266
- }
267
- if (entry.isDirectory()) {
268
- await visit(child);
269
- continue;
270
- }
271
- const childMetadata = await stat(child);
272
- if (!childMetadata.isFile())
273
- continue;
274
- files.push({
275
- path: relative(root, child).split("\\").join("/"),
276
- size_bytes: childMetadata.size,
277
- sha256: await hashFile(child),
278
- });
279
- }
280
- };
281
- await visit(root);
282
- files.sort((left, right) => left.path.localeCompare(right.path));
283
- if (files.length === 0)
284
- throw new Error(`Parent model artifact directory is empty: ${root}`);
285
- return hashJson({ kind: "directory", files });
286
- }
287
304
  /** Hash a local base model while permitting Hugging Face snapshot file links. */
288
305
  export async function fingerprintLocalBaseModel(uri) {
289
306
  const root = localModelArtifactPath(uri);
@@ -331,8 +348,6 @@ function statusForProgressStage(stage) {
331
348
  return "training";
332
349
  if (stage === "preparing")
333
350
  return "preparing";
334
- if (stage === "scoring")
335
- return "scoring";
336
351
  if (stage === "reporting")
337
352
  return "reporting";
338
353
  return "training";
@@ -348,9 +363,6 @@ async function readStageMetadata(path) {
348
363
  return null;
349
364
  }
350
365
  }
351
- async function clearDependentStageArtifacts(artifacts) {
352
- await cleanupStageArtifacts(artifacts, "prepare");
353
- }
354
366
  async function removePrefixedArtifacts(path) {
355
367
  const directory = dirname(path);
356
368
  const prefix = `${basename(path)}.`;
@@ -489,54 +501,29 @@ async function acquireArtifactLock(artifacts) {
489
501
  function isDryTraining(training) {
490
502
  return training.metrics?.dry_run === true;
491
503
  }
492
- function stringMetadata(value, key) {
493
- const entry = value?.[key];
494
- return typeof entry === "string" && entry.length > 0 ? entry : undefined;
495
- }
496
504
  async function modelManifestContract(prepared, training) {
497
505
  if (isDryTraining(training) || !training.model_artifact_uri)
498
506
  return undefined;
499
- const metadata = training.artifact_metadata;
500
- const explicitContract = Boolean(metadata?.framework && metadata?.format);
501
- const inspection = await assertUsableModelArtifact(training.model_artifact_uri, {
502
- allowUnrecognizedPayload: explicitContract,
503
- });
507
+ const inspection = await assertUsableModelArtifact(training.model_artifact_uri);
504
508
  const adapterWeights = inspection.adapter_weight_file_count > 0
505
509
  && inspection.adapter_weight_bytes > 0;
506
- const fullModelWeights = inspection.full_model_weight_file_count > 0
507
- && inspection.full_model_weight_bytes > 0;
508
- if (!adapterWeights && !fullModelWeights && !explicitContract) {
509
- throw new Error(`Model artifact ${inspection.path} has no loadable adapter or Transformers full-model weights. `
510
- + "Custom trainers must set training.artifact.framework and training.artifact.format.");
511
- }
512
- if ((metadata?.framework === "transformers-peft" || metadata?.servable === true)
513
- && (!adapterWeights || !inspection.has_adapter_config)) {
514
- throw new Error(`Model artifact ${inspection.path} requires adapter_model.safetensors or adapter_model.bin and a non-empty `
515
- + "adapter_config.json for a PEFT/servable contract.");
516
- }
517
- if (metadata?.framework === "transformers-full" && !fullModelWeights) {
518
- throw new Error(`Model artifact ${inspection.path} has no Transformers full-model weights.`);
510
+ if (!adapterWeights || !inspection.has_adapter_config) {
511
+ throw new Error(`Local training must produce a PEFT adapter with adapter_model.safetensors or adapter_model.bin and `
512
+ + `a non-empty adapter_config.json: ${inspection.path}`);
519
513
  }
520
- const implicitPeftAdapter = adapterWeights && inspection.has_adapter_config;
521
- const format = metadata?.format
522
- ?? (inspection.kind === "file" && inspection.path.endsWith(".tar.gz")
523
- ? "tar.gz"
524
- : inspection.kind === "directory" ? "huggingface-directory" : "file");
514
+ const format = inspection.kind === "file" ? "tar.gz" : "huggingface-directory";
525
515
  return {
526
516
  artifact_kind: inspection.kind,
527
517
  format,
528
- framework: metadata?.framework ?? (implicitPeftAdapter ? "transformers-peft" : fullModelWeights ? "transformers-full" : "custom"),
518
+ framework: "transformers-peft",
529
519
  base_model: prepared.request.spec_snapshot.base_model,
530
- base_model_revision: stringMetadata(metadata, "base_model_revision")
531
- ?? stringMetadata(training.metrics, "base_model_revision")
532
- ?? prepared.metadata.base_model_revision
520
+ base_model_revision: prepared.metadata.base_model_revision
533
521
  ?? prepared.request.hyperparameters.base_model_revision,
534
522
  base_model_artifact_uri: training.base_model_artifact_uri,
535
523
  base_model_fingerprint: prepared.metadata.base_model_fingerprint ?? undefined,
536
- parent_model_artifact: prepared.request.hyperparameters.parent_model_artifact,
537
524
  artifact_uri: training.model_artifact_uri,
538
525
  artifact_root: inspection.path,
539
- servable: metadata?.servable ?? implicitPeftAdapter,
526
+ servable: true,
540
527
  };
541
528
  }
542
529
  async function refreshArtifactManifest(prepared) {
@@ -557,37 +544,19 @@ function stageOutputPath(prepared, stage) {
557
544
  function stageFingerprintPath(prepared, stage) {
558
545
  return `${stageOutputPath(prepared, stage)}.stage.json`;
559
546
  }
560
- function configuredPath(path, cwd) {
561
- if (path === "training/local-runner" || path.startsWith("training/local-runner/")) {
562
- return resolve(packageRoot, path);
563
- }
564
- if (path.startsWith("file://"))
565
- return stripFileUri(path);
566
- if (isAbsolute(path))
567
- return path;
568
- const fileLike = path.startsWith("./")
569
- || path.startsWith("../")
570
- || path.includes("/")
571
- || path.includes("\\")
572
- || /\.(?:py|js|mjs|cjs|ts|tsx|sh|bash|zsh|pl|rb)$/i.test(path);
573
- return fileLike ? resolve(cwd ?? process.cwd(), path) : null;
574
- }
575
- async function entrypointFileFingerprints(args) {
576
- const candidates = new Set();
577
- for (const value of args.values) {
578
- if (!value)
579
- continue;
580
- const path = configuredPath(value, args.cwd);
581
- if (path)
582
- candidates.add(path);
583
- }
584
- if (args.project) {
585
- const project = configuredPath(args.project, args.cwd) ?? resolve(args.cwd ?? process.cwd(), args.project);
586
- candidates.add(resolve(project, "pyproject.toml"));
587
- candidates.add(resolve(project, "uv.lock"));
588
- }
547
+ async function runnerFileFingerprints() {
548
+ const project = resolve(packageRoot, "training/local-runner");
549
+ const bundledSource = resolve(packageRoot, "training/local-runner/src");
550
+ const pythonSources = (await readdir(bundledSource, { withFileTypes: true }))
551
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".py"))
552
+ .map((entry) => resolve(bundledSource, entry.name));
553
+ const candidates = [...new Set([
554
+ ...pythonSources,
555
+ resolve(project, "pyproject.toml"),
556
+ resolve(project, "uv.lock"),
557
+ ])];
589
558
  const fingerprints = {};
590
- for (const path of [...candidates].sort())
559
+ for (const path of candidates.sort())
591
560
  fingerprints[path] = await hashFileIfPresent(path);
592
561
  return fingerprints;
593
562
  }
@@ -598,45 +567,20 @@ async function stageFingerprint(args) {
598
567
  dry_run: args.config.dryRun,
599
568
  };
600
569
  if (args.stage === "train") {
601
- const entrypointFiles = await entrypointFileFingerprints({
602
- values: [
603
- args.config.training.script
604
- ?? (args.prepared.request.training_method === "dpo"
605
- ? "training/local-runner/src/train_dpo.py"
606
- : "training/local-runner/src/train.py"),
607
- ...(args.config.training.command ?? []),
608
- ],
609
- cwd: args.config.training.cwd,
610
- project: args.config.training.project,
611
- });
570
+ const entrypointFiles = await runnerFileFingerprints();
612
571
  return hashJson({
613
572
  ...common,
614
- training: args.config.training,
615
573
  paths: args.config.paths,
616
574
  entrypoint_files: entrypointFiles,
617
575
  });
618
576
  }
619
- const selectedCommand = args.stage === "baseline"
620
- ? args.config.evaluation.baselineCommand
621
- : args.config.evaluation.candidateCommand;
622
- const entrypointFiles = await entrypointFileFingerprints({
623
- values: [
624
- args.config.evaluation.inference.script,
625
- ...(args.config.evaluation.inference.command ?? []),
626
- ...(selectedCommand ?? []),
627
- ],
628
- cwd: args.config.evaluation.inference.cwd,
629
- project: args.config.evaluation.inference.project,
630
- });
577
+ const entrypointFiles = await runnerFileFingerprints();
631
578
  const evaluation = {
632
- command: args.stage === "baseline"
633
- ? args.config.evaluation.baselineCommand
634
- : args.config.evaluation.candidateCommand,
579
+ inference_protocol_version: INFERENCE_PROTOCOL_VERSION,
635
580
  inference: args.config.evaluation.inference,
636
581
  scoring: args.config.evaluation.scoring,
637
582
  timeout_ms: args.config.evaluation.timeoutMs,
638
583
  baseline_cache: args.config.evaluation.baselineCache,
639
- llm: args.config.llm,
640
584
  model_cache: args.config.paths.modelCache,
641
585
  };
642
586
  return hashJson({
@@ -739,64 +683,19 @@ async function ensureRunRecord(args) {
739
683
  }
740
684
  async function computePreparedRun(args) {
741
685
  const { request, config, artifacts } = args;
742
- const evalSampleSeed = config.evaluation.sampleSeed ?? deriveSampleSeed(request.run_id);
743
- let examples = examplesFromSpec(request.spec_snapshot);
744
- let evalSplit = "spec_examples";
745
- let trainingExampleCount = examples.length;
746
- if (request.training_method === "dpo") {
747
- if (!request.dataset_prebuilt)
748
- throw new Error("DPO training requires dataset_prebuilt.");
749
- const trainingPath = stripFileUri(request.dataset_prebuilt.training);
750
- if (args.writeArtifacts)
751
- await copyFile(trainingPath, artifacts.trainingJsonl);
752
- trainingExampleCount = await countJsonlRows(trainingPath);
753
- const evaluation = selectDpoEvaluation(request);
754
- evalSplit = evaluation.split;
755
- examples = evaluation.path
756
- ? await examplesFromChatJsonl(evaluation.path)
757
- : examplesFromSpec(request.spec_snapshot);
758
- }
759
- else if (request.dataset_prebuilt) {
760
- const trainingPath = stripFileUri(request.dataset_prebuilt.training);
761
- const evaluation = selectPrebuiltEvaluation(request.dataset_prebuilt);
762
- evalSplit = evaluation.split;
763
- if (evalSplit === "prebuilt_training" && !config.dryRun && !config.evaluation.allowPrebuiltTrainingEval) {
764
- throw new Error("dataset_prebuilt has no test or validation split, so evaluation would run on the training data and "
765
- + "overstate improvement. Provide dataset_prebuilt.test or dataset_prebuilt.validation, or set "
766
- + "evaluation.allowPrebuiltTrainingEval=true to evaluate on the training split anyway.");
767
- }
768
- if (args.writeArtifacts) {
769
- const normalizedTraining = await normalizeChatJsonlForRelocation(trainingPath);
770
- await writeFile(artifacts.trainingJsonl, `${normalizedTraining}\n`, "utf8");
771
- }
772
- examples = await examplesFromChatJsonl(evaluation.path);
773
- trainingExampleCount = null;
774
- }
775
- else {
776
- const split = splitSpecExamples(request.spec_snapshot.examples, evalSampleSeed);
777
- let trainingExamples = request.spec_snapshot.examples;
778
- if (split.holdout.length > 0) {
779
- trainingExamples = split.train;
780
- examples = split.holdout;
781
- evalSplit = "spec_holdout";
782
- }
783
- trainingExampleCount = trainingExamples.length;
784
- if (args.writeArtifacts) {
785
- const jsonl = compileSpecToJsonl({ ...request.spec_snapshot, examples: trainingExamples });
786
- await writeFile(artifacts.trainingJsonl, `${jsonl}\n`, "utf8");
787
- }
686
+ const evalSampleSeed = config.evaluation.sampleSeed
687
+ ?? deriveSampleSeed(request.behavior_spec_id);
688
+ const dataset = await validateDatasetInputs(request, config);
689
+ const examples = dataset.evaluationExamples;
690
+ if (args.writeArtifacts) {
691
+ await writeFileAtomic(artifacts.trainingJsonl, `${dataset.trainingJsonl}\n`);
788
692
  }
789
693
  const system = buildSystemMessage(request.spec_snapshot);
790
694
  const baseModelForEvaluation = config.paths.baseModel ?? request.spec_snapshot.base_model;
791
- const parentModelArtifact = request.hyperparameters.parent_model_artifact;
792
- const parentModelFingerprint = parentModelArtifact
793
- ? await fingerprintLocalArtifact(parentModelArtifact)
794
- : undefined;
795
695
  const maxEvalExamples = config.evaluation.maxExamples ?? request.hyperparameters.max_eval_examples;
796
696
  const evalExamplesUsed = Math.min(maxEvalExamples ?? examples.length, examples.length);
797
697
  const fingerprints = await datasetFingerprints(request);
798
698
  const requestFingerprint = hashJson(request);
799
- const effectiveConfigFingerprintValue = await effectiveConfigFingerprint(config);
800
699
  const runtimeFingerprintValue = await runtimeFingerprint();
801
700
  const baseModelRevision = await resolveBaseModelRevision(request, config);
802
701
  const baseModelFingerprint = config.paths.baseModel
@@ -806,9 +705,7 @@ async function computePreparedRun(args) {
806
705
  run_id: request.run_id,
807
706
  behavior_spec_id: request.behavior_spec_id,
808
707
  user_id: request.user_id,
809
- training_method: request.training_method,
810
708
  request_fingerprint: requestFingerprint,
811
- effective_config_fingerprint: effectiveConfigFingerprintValue,
812
709
  runtime_fingerprint: runtimeFingerprintValue,
813
710
  source_fingerprint: preparedSourceFingerprint({
814
711
  requestFingerprint,
@@ -818,19 +715,17 @@ async function computePreparedRun(args) {
818
715
  base_model_path: config.paths.baseModel,
819
716
  max_eval_examples: config.evaluation.maxExamples,
820
717
  eval_sample_seed: config.evaluation.sampleSeed,
821
- allow_prebuilt_training_eval: config.evaluation.allowPrebuiltTrainingEval,
822
718
  },
823
719
  baseModelRevision,
824
720
  baseModelFingerprint,
825
- parentModelFingerprint,
826
721
  datasetFingerprints: fingerprints,
827
722
  }),
828
- eval_split: evalSplit,
723
+ eval_split: dataset.evalSplit,
829
724
  eval_sample_seed: evalSampleSeed,
830
725
  eval_examples_total: examples.length,
831
726
  eval_examples_used: evalExamplesUsed,
832
727
  max_eval_examples: maxEvalExamples ?? null,
833
- training_example_count: trainingExampleCount,
728
+ training_example_count: dataset.trainingExampleCount,
834
729
  dataset_prebuilt: Boolean(request.dataset_prebuilt),
835
730
  dataset_format: request.dataset_prebuilt?.format ?? null,
836
731
  dataset_fingerprints: fingerprints,
@@ -838,8 +733,6 @@ async function computePreparedRun(args) {
838
733
  base_model_for_evaluation: baseModelForEvaluation,
839
734
  base_model_revision: baseModelRevision ?? null,
840
735
  base_model_fingerprint: baseModelFingerprint ?? null,
841
- parent_model_artifact: parentModelArtifact ?? null,
842
- parent_model_fingerprint: parentModelFingerprint ?? null,
843
736
  system_prompt_sha256: createHash("sha256").update(system).digest("hex"),
844
737
  prepared_at: new Date().toISOString(),
845
738
  };
@@ -865,7 +758,6 @@ async function prepareStage(args) {
865
758
  ? await readStageMetadata(args.artifacts.stageMetadataJson)
866
759
  : null;
867
760
  let canReuse = preparedExists
868
- && !args.force
869
761
  && existingMetadata?.source_fingerprint === prepared.metadata.source_fingerprint;
870
762
  if (canReuse) {
871
763
  canReuse = await verifyReusableArtifacts(args.artifacts, [
@@ -893,28 +785,17 @@ async function prepareStage(args) {
893
785
  });
894
786
  await throwIfCancelled(args.store, args.request);
895
787
  await args.store.invalidateRunOutputs(args.request.run_id, { report: true, model: true });
896
- await clearDependentStageArtifacts(args.artifacts);
788
+ await cleanupStageArtifacts(args.artifacts, "prepare");
897
789
  const refreshed = await computePreparedRun({ ...args, writeArtifacts: true });
898
790
  await refreshArtifactManifest(refreshed);
899
791
  return refreshed;
900
792
  }
901
- async function ensurePrepared(args) {
902
- return prepareStage({
903
- request: args.request,
904
- config: args.config,
905
- artifacts: args.artifacts,
906
- store: args.store,
907
- reporter: args.reporter,
908
- force: args.forcePrepare,
909
- });
910
- }
911
793
  async function runBaselineStage(args) {
912
- if (!args.force
913
- && await canReuseStageArtifact({
914
- stage: "baseline",
915
- prepared: args.prepared,
916
- config: args.config,
917
- })) {
794
+ if (await canReuseStageArtifact({
795
+ stage: "baseline",
796
+ prepared: args.prepared,
797
+ config: args.config,
798
+ })) {
918
799
  await throwIfCancelled(args.store, args.prepared.request);
919
800
  await updateRun({
920
801
  store: args.store,
@@ -943,19 +824,17 @@ async function runBaselineStage(args) {
943
824
  examples: args.prepared.examples.length,
944
825
  eval_examples_used: args.prepared.metadata.eval_examples_used,
945
826
  eval_split: args.prepared.metadata.eval_split,
946
- model_id: args.prepared.metadata.parent_model_artifact ?? args.prepared.baseModelForEvaluation,
947
- parent_model_artifact: args.prepared.metadata.parent_model_artifact,
827
+ model_id: args.prepared.baseModelForEvaluation,
948
828
  },
949
829
  });
950
830
  const report = await evaluateExamples({
951
831
  kind: "baseline",
952
- modelId: args.prepared.metadata.parent_model_artifact ?? args.prepared.baseModelForEvaluation,
832
+ modelId: args.prepared.baseModelForEvaluation,
953
833
  baseModelId: args.prepared.baseModelForEvaluation,
954
834
  baseModelRevision: args.config.paths.baseModel
955
835
  ? undefined
956
836
  : args.prepared.metadata.base_model_revision ?? undefined,
957
- sourceFingerprint: args.prepared.metadata.source_fingerprint,
958
- adapterPath: args.prepared.metadata.parent_model_artifact ?? undefined,
837
+ sourceFingerprint: args.prepared.metadata.base_model_fingerprint ?? undefined,
959
838
  examples: args.prepared.examples,
960
839
  system: args.prepared.system,
961
840
  config: args.config,
@@ -971,13 +850,12 @@ async function runBaselineStage(args) {
971
850
  return report;
972
851
  }
973
852
  async function runTrainStage(args) {
974
- if (!args.force
975
- && await canReuseStageArtifact({
976
- stage: "train",
977
- prepared: args.prepared,
978
- config: args.config,
979
- verifyModel: true,
980
- })) {
853
+ if (await canReuseStageArtifact({
854
+ stage: "train",
855
+ prepared: args.prepared,
856
+ config: args.config,
857
+ verifyModel: true,
858
+ })) {
981
859
  await throwIfCancelled(args.store, args.prepared.request);
982
860
  await updateRun({
983
861
  store: args.store,
@@ -1011,7 +889,7 @@ async function runTrainStage(args) {
1011
889
  status: "training",
1012
890
  stage: "training",
1013
891
  message: args.config.dryRun ? "Recording dry-run training result." : "Launching local training process.",
1014
- details: { training_backend: args.config.training.backend, dry_run: args.config.dryRun },
892
+ details: { training_backend: "local-uv", dry_run: args.config.dryRun },
1015
893
  });
1016
894
  const training = await launchProcessTraining({
1017
895
  request: args.prepared.request,
@@ -1043,27 +921,9 @@ async function runTrainStage(args) {
1043
921
  }
1044
922
  return training;
1045
923
  }
1046
- async function writeExternalTrainingReport(args) {
1047
- const training = trainingReportSchema.parse({
1048
- provider: args.config.training.backend === "command" ? "local-command" : "local-uv",
1049
- training_job_name: `external-${args.prepared.request.run_id}`,
1050
- model_artifact_uri: fileUri(localModelArtifactPath(args.modelArtifact)),
1051
- base_model_artifact_uri: args.config.paths.baseModel ? fileUri(args.config.paths.baseModel) : undefined,
1052
- parent_model_artifact_uri: args.prepared.metadata.parent_model_artifact ?? undefined,
1053
- artifact_metadata: {
1054
- ...(args.config.training.artifact ?? {}),
1055
- notes: args.config.training.artifact?.notes ?? "External model artifact supplied with --model-artifact.",
1056
- },
1057
- metrics: { external_model_artifact: true },
1058
- exit_code: null,
1059
- log_uri: fileUri(args.prepared.artifacts.trainingReportJson),
1060
- });
1061
- await writeJsonAtomic(args.prepared.artifacts.trainingReportJson, training);
1062
- return training;
1063
- }
1064
924
  async function runCandidateStage(args) {
1065
925
  let verifiedTraining;
1066
- if (!args.modelArtifact && await pathExists(args.prepared.artifacts.trainingReportJson)) {
926
+ if (await pathExists(args.prepared.artifacts.trainingReportJson)) {
1067
927
  const trainingCurrent = await canReuseStageArtifact({
1068
928
  stage: "train",
1069
929
  prepared: args.prepared,
@@ -1074,9 +934,7 @@ async function runCandidateStage(args) {
1074
934
  verifiedTraining = trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson));
1075
935
  }
1076
936
  }
1077
- if (!args.force
1078
- && !args.modelArtifact
1079
- && verifiedTraining
937
+ if (verifiedTraining
1080
938
  && await canReuseStageArtifact({
1081
939
  stage: "candidate",
1082
940
  prepared: args.prepared,
@@ -1110,33 +968,16 @@ async function runCandidateStage(args) {
1110
968
  await throwIfCancelled(args.store, args.prepared.request);
1111
969
  return report;
1112
970
  }
1113
- if (!args.modelArtifact && !verifiedTraining) {
1114
- throw new Error("candidate stage requires current verified training output. Run --stage train first.");
971
+ if (!verifiedTraining) {
972
+ throw new Error("Candidate evaluation requires current verified training output.");
1115
973
  }
1116
974
  await throwIfCancelled(args.store, args.prepared.request);
1117
- await args.store.invalidateRunOutputs(args.prepared.request.run_id, {
1118
- report: true,
1119
- model: Boolean(args.modelArtifact),
1120
- });
975
+ await args.store.invalidateRunOutputs(args.prepared.request.run_id, { report: true });
1121
976
  await cleanupStageArtifacts(args.prepared.artifacts, "candidate");
1122
- let training;
1123
- if (args.modelArtifact) {
1124
- training = await writeExternalTrainingReport({
1125
- prepared: args.prepared,
1126
- config: args.config,
1127
- modelArtifact: args.modelArtifact,
1128
- });
1129
- await writeStageFingerprint({ stage: "train", prepared: args.prepared, config: args.config });
1130
- }
1131
- else if (verifiedTraining) {
1132
- training = verifiedTraining;
1133
- }
1134
- else {
1135
- throw new Error("candidate stage requires training output or --model-artifact.");
1136
- }
977
+ const training = verifiedTraining;
1137
978
  const modelArtifact = training.model_artifact_uri;
1138
979
  if (!modelArtifact)
1139
- throw new Error("candidate stage requires a model_artifact_uri in training-report.json or --model-artifact.");
980
+ throw new Error("candidate stage requires a model_artifact_uri in training-report.json.");
1140
981
  if (!isDryTraining(training)) {
1141
982
  await modelManifestContract(args.prepared, training);
1142
983
  await refreshArtifactManifest(args.prepared);
@@ -1183,82 +1024,15 @@ async function runCandidateStage(args) {
1183
1024
  });
1184
1025
  return report;
1185
1026
  }
1186
- async function runScoreStage(args) {
1187
- if (!await pathExists(args.prepared.artifacts.baselineEvalJson)) {
1188
- throw new Error("score stage requires baseline-eval.json. Run --stage baseline first.");
1189
- }
1190
- if (!await pathExists(args.prepared.artifacts.candidateEvalJson)) {
1191
- throw new Error("score stage requires candidate-eval.json. Run --stage candidate first.");
1192
- }
1193
- await verifyReusableArtifacts(args.prepared.artifacts, [
1194
- args.prepared.artifacts.baselineEvalJson,
1195
- stageFingerprintPath(args.prepared, "baseline"),
1196
- args.prepared.artifacts.candidateEvalJson,
1197
- stageFingerprintPath(args.prepared, "candidate"),
1198
- ]);
1199
- await throwIfCancelled(args.store, args.prepared.request);
1200
- await args.store.invalidateRunOutputs(args.prepared.request.run_id, { report: true });
1201
- await cleanupStageArtifacts(args.prepared.artifacts, "score");
1202
- await updateRun({
1203
- store: args.store,
1204
- reporter: args.reporter,
1205
- request: args.prepared.request,
1206
- status: "scoring",
1207
- stage: "scoring",
1208
- message: "Rescoring existing baseline and candidate outputs.",
1209
- details: { scoring_mode: args.config.evaluation.scoring.mode },
1210
- });
1211
- const rollbackPaths = [
1212
- args.prepared.artifacts.baselineEvalJson,
1213
- stageFingerprintPath(args.prepared, "baseline"),
1214
- args.prepared.artifacts.candidateEvalJson,
1215
- stageFingerprintPath(args.prepared, "candidate"),
1216
- ];
1217
- const originals = new Map();
1218
- for (const path of rollbackPaths)
1219
- originals.set(path, await readFile(path));
1220
- try {
1221
- const baseline = await rescoreEvalReport({
1222
- report: evalReportSchema.parse(await readJson(args.prepared.artifacts.baselineEvalJson)),
1223
- config: args.config,
1224
- outputPath: args.prepared.artifacts.baselineEvalJson,
1225
- system: args.prepared.system,
1226
- reporter: args.runReporter,
1227
- });
1228
- const candidate = await rescoreEvalReport({
1229
- report: evalReportSchema.parse(await readJson(args.prepared.artifacts.candidateEvalJson)),
1230
- config: args.config,
1231
- outputPath: args.prepared.artifacts.candidateEvalJson,
1232
- system: args.prepared.system,
1233
- reporter: args.runReporter,
1234
- });
1235
- await throwIfCancelled(args.store, args.prepared.request);
1236
- await writeStageFingerprint({ stage: "baseline", prepared: args.prepared, config: args.config });
1237
- const training = await pathExists(args.prepared.artifacts.trainingReportJson)
1238
- ? trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson))
1239
- : undefined;
1240
- await writeStageFingerprint({
1241
- stage: "candidate",
1242
- prepared: args.prepared,
1243
- config: args.config,
1244
- training,
1245
- });
1246
- return { baseline, candidate };
1247
- }
1248
- catch (error) {
1249
- await Promise.all([...originals].map(([path, contents]) => writeFileAtomic(path, contents)));
1250
- throw error;
1251
- }
1252
- }
1253
1027
  async function runReportStage(args) {
1254
1028
  if (!await pathExists(args.prepared.artifacts.baselineEvalJson)) {
1255
- throw new Error("report stage requires baseline-eval.json. Run --stage baseline first.");
1029
+ throw new Error("Run reporting requires baseline-eval.json.");
1256
1030
  }
1257
1031
  if (!await pathExists(args.prepared.artifacts.candidateEvalJson)) {
1258
- throw new Error("report stage requires candidate-eval.json. Run --stage candidate first.");
1032
+ throw new Error("Run reporting requires candidate-eval.json.");
1259
1033
  }
1260
1034
  if (!await pathExists(args.prepared.artifacts.trainingReportJson)) {
1261
- throw new Error("report stage requires training-report.json. Run --stage train first, or --stage candidate --model-artifact <path>.");
1035
+ throw new Error("Run reporting requires training-report.json.");
1262
1036
  }
1263
1037
  const currentTraining = await canReuseStageArtifact({
1264
1038
  stage: "train",
@@ -1291,17 +1065,15 @@ async function runReportStage(args) {
1291
1065
  await throwIfCancelled(args.store, args.prepared.request);
1292
1066
  await args.store.invalidateRunOutputs(args.prepared.request.run_id, { report: true });
1293
1067
  await cleanupStageArtifacts(args.prepared.artifacts, "report");
1294
- if (args.emitReportingEvent) {
1295
- await updateRun({
1296
- store: args.store,
1297
- reporter: args.reporter,
1298
- request: args.prepared.request,
1299
- status: "reporting",
1300
- stage: "reporting",
1301
- message: "Writing run report.",
1302
- details: { report_path: args.prepared.artifacts.runReportJson },
1303
- });
1304
- }
1068
+ await updateRun({
1069
+ store: args.store,
1070
+ reporter: args.reporter,
1071
+ request: args.prepared.request,
1072
+ status: "reporting",
1073
+ stage: "reporting",
1074
+ message: "Writing run report.",
1075
+ details: { report_path: args.prepared.artifacts.runReportJson },
1076
+ });
1305
1077
  const baseline = evalReportSchema.parse(await readJson(args.prepared.artifacts.baselineEvalJson));
1306
1078
  const candidate = evalReportSchema.parse(await readJson(args.prepared.artifacts.candidateEvalJson));
1307
1079
  const training = trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson));
@@ -1329,8 +1101,6 @@ async function runReportStage(args) {
1329
1101
  run_metadata: {
1330
1102
  base_model: args.prepared.request.spec_snapshot.base_model,
1331
1103
  fine_tuned_model_id: training.model_artifact_uri ?? training.training_job_name,
1332
- parent_model_artifact: args.prepared.metadata.parent_model_artifact,
1333
- training_method: args.prepared.request.training_method,
1334
1104
  dataset_prebuilt: args.prepared.metadata.dataset_prebuilt,
1335
1105
  dataset_format: args.prepared.metadata.dataset_format,
1336
1106
  dataset_uri: fileUri(args.prepared.artifacts.trainingJsonl),
@@ -1367,10 +1137,14 @@ async function runReportStage(args) {
1367
1137
  });
1368
1138
  return report;
1369
1139
  }
1370
- export async function runLocalFineTuneStage(input) {
1140
+ export async function runLocalFineTune(input) {
1141
+ const validated = await validateLocalFineTuneInput({
1142
+ request: input.request,
1143
+ config: input.config,
1144
+ });
1145
+ input = { ...input, ...validated };
1371
1146
  const startedPerf = performance.now();
1372
1147
  const startedAt = new Date().toISOString();
1373
- const stage = input.stage ?? "all";
1374
1148
  const prefix = artifactPrefix(input.request);
1375
1149
  const artifacts = resolveRunArtifacts({ artifactRoot: input.config.artifactRoot, prefix });
1376
1150
  const store = createLocalStore(input.config.storeRoot);
@@ -1389,110 +1163,51 @@ export async function runLocalFineTuneStage(input) {
1389
1163
  let prepared;
1390
1164
  try {
1391
1165
  await throwIfCancelled(store, input.request);
1392
- prepared = await ensurePrepared({
1166
+ prepared = await prepareStage({
1393
1167
  request: input.request,
1394
1168
  config: input.config,
1395
1169
  artifacts,
1396
1170
  store,
1397
1171
  reporter: input.reporter,
1398
- forcePrepare: Boolean(input.force && (stage === "prepare" || stage === "all")),
1399
1172
  });
1400
1173
  await throwIfCancelled(store, input.request);
1401
- let report;
1402
- const completeRequestedStage = async (completedStage) => {
1403
- await throwIfCancelled(store, input.request);
1404
- const message = `${completedStage} stage completed.`;
1405
- const completedState = await store.updateRun({
1406
- runId: input.request.run_id,
1407
- status: "stage_completed",
1408
- stage: `${completedStage}_completed`,
1409
- message,
1410
- details: { requested_stage: completedStage },
1411
- });
1412
- if (completedState.status === "cancelled") {
1413
- throw new ProcessCancelledError(`Run ${input.request.run_id} was cancelled.`);
1414
- }
1415
- await input.reporter?.onEvent?.({
1416
- stage: `${completedStage}_completed`,
1417
- status: "completed",
1418
- message,
1419
- details: { requested_stage: completedStage },
1420
- });
1174
+ await runBaselineStage({
1175
+ prepared,
1176
+ config: input.config,
1177
+ store,
1178
+ reporter: input.reporter,
1179
+ runReporter,
1180
+ });
1181
+ await refreshArtifactManifest(prepared);
1182
+ await runTrainStage({
1183
+ prepared,
1184
+ config: input.config,
1185
+ store,
1186
+ reporter: input.reporter,
1187
+ runReporter,
1188
+ });
1189
+ await runCandidateStage({
1190
+ prepared,
1191
+ config: input.config,
1192
+ store,
1193
+ reporter: input.reporter,
1194
+ runReporter,
1195
+ });
1196
+ await refreshArtifactManifest(prepared);
1197
+ const report = await runReportStage({
1198
+ prepared,
1199
+ config: input.config,
1200
+ store,
1201
+ reporter: input.reporter,
1202
+ startedAt,
1203
+ startedPerf,
1204
+ });
1205
+ return {
1206
+ request: input.request,
1207
+ report,
1208
+ reportPath: artifacts.runReportJson,
1209
+ artifactDir: artifacts.runDir,
1421
1210
  };
1422
- if (stage === "prepare") {
1423
- await completeRequestedStage(stage);
1424
- return stageResult({ request: input.request, stage, artifacts });
1425
- }
1426
- if (stage === "baseline" || stage === "all") {
1427
- await runBaselineStage({
1428
- prepared,
1429
- config: input.config,
1430
- store,
1431
- reporter: input.reporter,
1432
- runReporter,
1433
- force: Boolean(input.force),
1434
- });
1435
- await refreshArtifactManifest(prepared);
1436
- if (stage === "baseline") {
1437
- await completeRequestedStage(stage);
1438
- return stageResult({ request: input.request, stage, artifacts });
1439
- }
1440
- }
1441
- if (stage === "train" || stage === "all") {
1442
- await runTrainStage({
1443
- prepared,
1444
- config: input.config,
1445
- store,
1446
- reporter: input.reporter,
1447
- runReporter,
1448
- force: Boolean(input.force),
1449
- });
1450
- if (stage === "train") {
1451
- await completeRequestedStage(stage);
1452
- return stageResult({ request: input.request, stage, artifacts });
1453
- }
1454
- }
1455
- if (stage === "candidate" || stage === "all") {
1456
- await runCandidateStage({
1457
- prepared,
1458
- config: input.config,
1459
- store,
1460
- reporter: input.reporter,
1461
- runReporter,
1462
- force: Boolean(input.force),
1463
- modelArtifact: input.modelArtifact,
1464
- });
1465
- await refreshArtifactManifest(prepared);
1466
- if (stage === "candidate") {
1467
- await completeRequestedStage(stage);
1468
- return stageResult({ request: input.request, stage, artifacts });
1469
- }
1470
- }
1471
- if (stage === "score") {
1472
- await runScoreStage({
1473
- prepared,
1474
- config: input.config,
1475
- store,
1476
- reporter: input.reporter,
1477
- runReporter,
1478
- });
1479
- await refreshArtifactManifest(prepared);
1480
- await completeRequestedStage(stage);
1481
- return stageResult({ request: input.request, stage, artifacts });
1482
- }
1483
- if (stage === "report" || stage === "all") {
1484
- report = await runReportStage({
1485
- prepared,
1486
- config: input.config,
1487
- store,
1488
- reporter: input.reporter,
1489
- startedAt,
1490
- startedPerf,
1491
- emitReportingEvent: stage === "report",
1492
- });
1493
- return stageResult({ request: input.request, stage, artifacts, report });
1494
- }
1495
- throw new Error(`Unknown run stage: ${stage}`);
1496
1211
  }
1497
1212
  catch (error) {
1498
1213
  const cancelled = error instanceof ProcessCancelledError
@@ -1524,39 +1239,4 @@ export async function runLocalFineTuneStage(input) {
1524
1239
  await releaseRunLock();
1525
1240
  }
1526
1241
  }
1527
- function stageResult(args) {
1528
- return {
1529
- request: args.request,
1530
- stage: args.stage,
1531
- report: args.report,
1532
- reportPath: args.report ? args.artifacts.runReportJson : undefined,
1533
- artifactDir: dirname(args.artifacts.runReportJson),
1534
- artifacts: {
1535
- training_jsonl: args.artifacts.trainingJsonl,
1536
- stage_metadata: args.artifacts.stageMetadataJson,
1537
- training_report: args.artifacts.trainingReportJson,
1538
- baseline_eval: args.artifacts.baselineEvalJson,
1539
- candidate_eval: args.artifacts.candidateEvalJson,
1540
- report: args.artifacts.runReportJson,
1541
- artifact_manifest: args.artifacts.artifactManifestJson,
1542
- },
1543
- };
1544
- }
1545
- export async function runLocalFineTune(input) {
1546
- const result = await runLocalFineTuneStage({
1547
- request: input.request,
1548
- config: input.config,
1549
- reporter: input.reporter,
1550
- stage: "all",
1551
- });
1552
- if (!result.report || !result.reportPath) {
1553
- throw new Error("Full local fine-tune run did not produce a report.");
1554
- }
1555
- return {
1556
- request: result.request,
1557
- report: result.report,
1558
- reportPath: result.reportPath,
1559
- artifactDir: result.artifactDir,
1560
- };
1561
- }
1562
1242
  //# sourceMappingURL=orchestrator.js.map