@tuned-tensor/cli 0.19.0 → 0.21.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.
@@ -615,23 +615,52 @@ var TRAINING_MODELS = [
615
615
  gradientCheckpointing: true
616
616
  }
617
617
  ];
618
- function resolveTrainingModel(modelId) {
618
+ var HUGGING_FACE_REPO_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,95}\/[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/;
619
+ function uncertifiedTrainingModel(id) {
620
+ return {
621
+ id,
622
+ family: "uncertified",
623
+ modelLoader: "causal_lm",
624
+ defaultLearningRate: 1e-4,
625
+ defaultPerDeviceBatchSize: 1,
626
+ defaultGradientAccumulationSteps: 8,
627
+ defaultLoraRank: 16,
628
+ defaultLoraAlpha: 32,
629
+ defaultLoraDropout: 0.05,
630
+ defaultMaxSeqLength: 2048,
631
+ loraTargetModules: "all-linear",
632
+ gradientCheckpointing: true
633
+ };
634
+ }
635
+ function findCertifiedTrainingModel(modelId) {
619
636
  const normalized = modelId.trim().toLowerCase();
620
- const model = TRAINING_MODELS.find(
621
- (candidate) => candidate.id.toLowerCase() === normalized
637
+ return TRAINING_MODELS.find((candidate) => candidate.id.toLowerCase() === normalized);
638
+ }
639
+ function isCertifiedTrainingModel(modelId) {
640
+ return findCertifiedTrainingModel(modelId) !== void 0;
641
+ }
642
+ function resolveTrainingModel(modelId) {
643
+ const model = findCertifiedTrainingModel(modelId);
644
+ if (model) return model;
645
+ const id = modelId.trim();
646
+ if (HUGGING_FACE_REPO_ID.test(id)) return uncertifiedTrainingModel(id);
647
+ throw new Error(
648
+ `Unsupported base model "${modelId}". Use a certified model (${TRAINING_MODELS.map((item) => item.id).join(", ")}) or a Hugging Face repo id such as owner/name.`
649
+ );
650
+ }
651
+ function uncertifiedRevisionError(modelId) {
652
+ return new Error(
653
+ `${modelId} is not a TT-certified base model. To fine-tune it anyway, set hyperparameters.base_model_revision to its 40-character commit SHA (see https://huggingface.co/${modelId}/commits/main). Uncertified models load as text causal LMs with safetensors weights and no remote code.`
622
654
  );
623
- if (!model) {
624
- throw new Error(
625
- `Unsupported base model "${modelId}". Supported models: ${TRAINING_MODELS.map((item) => item.id).join(", ")}`
626
- );
627
- }
628
- return model;
629
655
  }
630
656
  function canonicalizeTrainingModel(modelId) {
631
657
  return resolveTrainingModel(modelId).id;
632
658
  }
633
659
  function resolveRequestedBaseModelRevision(modelId, requestedRevision) {
634
660
  const model = resolveTrainingModel(modelId);
661
+ if (!isCertifiedTrainingModel(model.id) && !requestedRevision) {
662
+ throw uncertifiedRevisionError(model.id);
663
+ }
635
664
  if (model.defaultRevision && requestedRevision && requestedRevision !== model.defaultRevision) {
636
665
  throw new Error(
637
666
  `${model.id} must use certified revision ${model.defaultRevision}; got ${requestedRevision}.`
@@ -640,7 +669,7 @@ function resolveRequestedBaseModelRevision(modelId, requestedRevision) {
640
669
  return requestedRevision ?? model.defaultRevision;
641
670
  }
642
671
  function defaultBaseModelRevision(modelId) {
643
- return resolveRequestedBaseModelRevision(modelId);
672
+ return resolveTrainingModel(modelId).defaultRevision;
644
673
  }
645
674
  function resolveModelLoader(modelId) {
646
675
  return resolveTrainingModel(modelId).modelLoader;
@@ -703,6 +732,7 @@ function assertCertifiedBaseModelConfig(value, label = "base-model config.json",
703
732
  throw new Error(`${label} must contain a JSON object.`);
704
733
  }
705
734
  const config = value;
735
+ if (expectedModelId && !isCertifiedTrainingModel(expectedModelId)) return;
706
736
  const expectedFamily = expectedModelId ? resolveTrainingModel(expectedModelId).family : void 0;
707
737
  if (config.model_type === "qwen3_5") {
708
738
  if (expectedFamily && expectedFamily !== "qwen3_5") {
@@ -846,6 +876,7 @@ var TUNED_TENSOR_DIR = ".tuned-tensor";
846
876
  var DEFAULT_ARTIFACT_ROOT = `${TUNED_TENSOR_DIR}/artifacts`;
847
877
  var DEFAULT_PROJECT_STORE_ROOT = `${TUNED_TENSOR_DIR}/store`;
848
878
  var DEFAULT_FOUNDATION_RUNS_DIR = `${TUNED_TENSOR_DIR}/foundation-runs`;
879
+ var DEFAULT_DECISION_RUNS_DIR = `${TUNED_TENSOR_DIR}/decision-runs`;
849
880
  function envHome(env = process.env) {
850
881
  const home = env.HOME?.trim();
851
882
  return home ? resolve3(home) : homedir();
@@ -902,6 +933,38 @@ function pythonEnvironmentPath(kind, hash, env = process.env) {
902
933
  return next;
903
934
  }
904
935
 
936
+ // src/local-runtime/decision-models.ts
937
+ var LAYA_REVISION = "55cf4c4ebb4ebe31b2550e8bdf3bd21b99753851";
938
+ var DECISION_MODELS = [
939
+ {
940
+ id: "convaiinnovations/laya",
941
+ family: "laya",
942
+ revision: LAYA_REVISION,
943
+ parameters: "421M (ModernBERT-large encoder + typed decision head)",
944
+ license: "Apache-2.0",
945
+ defaultLearningRate: 2e-5,
946
+ defaultBatchSize: 8,
947
+ defaultEpochs: 3
948
+ }
949
+ ];
950
+ var DEFAULT_DECISION_MODEL = DECISION_MODELS[0].id;
951
+ function resolveDecisionModel(modelId) {
952
+ const normalized = modelId.trim().toLowerCase();
953
+ const model = DECISION_MODELS.find((candidate) => candidate.id.toLowerCase() === normalized);
954
+ if (!model) {
955
+ throw new Error(
956
+ `Unsupported decision model "${modelId}". Supported decision models: ${DECISION_MODELS.map((item) => item.id).join(", ")}`
957
+ );
958
+ }
959
+ return model;
960
+ }
961
+ function isDecisionModel(modelId) {
962
+ return DECISION_MODELS.some((candidate) => candidate.id.toLowerCase() === modelId.trim().toLowerCase());
963
+ }
964
+ function canonicalizeDecisionModel(modelId) {
965
+ return resolveDecisionModel(modelId).id;
966
+ }
967
+
905
968
  // src/local-runtime/contracts.ts
906
969
  var datasetFormatSchema = z.literal("chat_jsonl");
907
970
  var baseModelRevisionSchema = z.string().regex(/^[0-9a-f]{40}$/i, "base_model_revision must be a 40-character Hugging Face commit SHA").transform((value) => value.toLowerCase());
@@ -974,6 +1037,15 @@ var fineTuneHyperparametersSchema = z.object({
974
1037
  /** Immutable Hugging Face commit used by every stage of the run. */
975
1038
  base_model_revision: baseModelRevisionSchema.optional()
976
1039
  }).strict();
1040
+ function requirePinnedUncertifiedModel(spec, context) {
1041
+ const baseModel = "spec_snapshot" in spec ? spec.spec_snapshot.base_model : spec.base_model;
1042
+ if (isCertifiedTrainingModel(baseModel) || spec.hyperparameters?.base_model_revision) return;
1043
+ context.addIssue({
1044
+ code: z.ZodIssueCode.custom,
1045
+ path: ["hyperparameters", "base_model_revision"],
1046
+ message: uncertifiedRevisionError(baseModel).message
1047
+ });
1048
+ }
977
1049
  var fineTuneRunRequestSchema = z.object({
978
1050
  run_id: z.string().uuid(),
979
1051
  user_id: z.string().min(1),
@@ -983,6 +1055,7 @@ var fineTuneRunRequestSchema = z.object({
983
1055
  hyperparameters: fineTuneHyperparametersSchema.default({ n_epochs: 1 }),
984
1056
  dataset_prebuilt: datasetPrebuiltSchema.optional()
985
1057
  }).strict().superRefine((request, context) => {
1058
+ requirePinnedUncertifiedModel(request, context);
986
1059
  if (request.spec_snapshot.examples.length === 0 && !request.dataset_prebuilt) {
987
1060
  context.addIssue({
988
1061
  code: z.ZodIssueCode.custom,
@@ -1205,9 +1278,54 @@ var localAdapterSpecFileSchema = specSnapshotSchema.extend({
1205
1278
  id: z.string().uuid().optional(),
1206
1279
  hyperparameters: fineTuneHyperparametersSchema.optional(),
1207
1280
  dataset_prebuilt: datasetPrebuiltSchema.optional()
1281
+ }).strict().superRefine(requirePinnedUncertifiedModel);
1282
+ var decisionQuestionSchema = z.discriminatedUnion("type", [
1283
+ z.object({
1284
+ type: z.literal("choice"),
1285
+ /** Answer label -> description the model reads. Key order is option order. */
1286
+ criteria: z.record(z.string().min(1), z.string()).refine(
1287
+ (criteria) => Object.keys(criteria).length >= 2 && Object.keys(criteria).length <= 32,
1288
+ "A choice decision needs 2 to 32 labels."
1289
+ )
1290
+ }).strict(),
1291
+ z.object({
1292
+ type: z.literal("noul"),
1293
+ criteria: z.object({ true: z.string().optional(), false: z.string().optional() }).strict().optional()
1294
+ }).strict(),
1295
+ z.object({
1296
+ type: z.literal("score"),
1297
+ /** Level descriptions, index 0 first; example outputs are level indices. */
1298
+ criteria: z.array(z.string().min(1)).min(2).max(10)
1299
+ }).strict()
1300
+ ]);
1301
+ var decisionHyperparametersSchema = z.object({
1302
+ n_epochs: z.number().int().min(1).max(50).optional(),
1303
+ learning_rate: z.number().positive().max(1).optional(),
1304
+ batch_size: z.number().int().min(1).max(256).optional(),
1305
+ /** Train only the decision head; faster on CPU, usually less accurate. */
1306
+ freeze_encoder: z.boolean().optional(),
1307
+ /** Shuffle option order per training row so the head learns meaning, not position. */
1308
+ shuffle_options: z.boolean().optional(),
1309
+ seed: z.number().int().min(0).max(2147483647).optional(),
1310
+ device: z.enum(["auto", "cpu", "cuda", "mps"]).optional()
1208
1311
  }).strict();
1312
+ var localDecisionSpecFileSchema = behaviorSpecSchema.extend({
1313
+ ...projectWorkflowFields,
1314
+ engine: z.literal("decision"),
1315
+ id: z.string().uuid().optional(),
1316
+ // Refine first: a union member must report an issue, not throw, for other engines' specs.
1317
+ base_model: z.string().refine(isDecisionModel, `Unsupported decision model. Supported decision models: ${DECISION_MODELS.map((model) => model.id).join(", ")}`).transform((value) => canonicalizeDecisionModel(value)),
1318
+ decision: decisionQuestionSchema,
1319
+ hyperparameters: decisionHyperparametersSchema.optional()
1320
+ });
1321
+ function decisionLabels(question) {
1322
+ if (question.type === "choice") return Object.keys(question.criteria);
1323
+ if (question.type === "noul") return ["false", "true"];
1324
+ return question.criteria.map((_, index) => String(index));
1325
+ }
1209
1326
  var localBehaviorSpecFileSchema = z.union([
1210
1327
  localFoundationSpecFileSchema,
1328
+ localDecisionSpecFileSchema,
1211
1329
  localAdapterSpecFileSchema
1212
1330
  ]).superRefine((spec, ctx) => {
1213
1331
  if (spec.pipeline && isFoundationPipeline(parsePipeline(spec.pipeline)) !== (spec.engine === "foundation")) {
@@ -1216,14 +1334,142 @@ var localBehaviorSpecFileSchema = z.union([
1216
1334
  if (spec.engine === "foundation" && (spec.evaluation || spec.runtime?.storeRoot || spec.runtime?.paths)) {
1217
1335
  ctx.addIssue({ code: "custom", path: ["runtime"], message: "Foundation uses foundation settings for evaluation and checkpoints; runtime.artifactRoot and runtime.gpu are supported." });
1218
1336
  }
1337
+ if (spec.engine === "decision" && (spec.evaluation || spec.runtime?.gpu || spec.runtime?.storeRoot || spec.runtime?.paths)) {
1338
+ ctx.addIssue({ code: "custom", path: ["runtime"], message: "Decision specs support runtime.artifactRoot only; the device is set in hyperparameters.device." });
1339
+ }
1219
1340
  });
1220
1341
  function isFoundationSpecFile(spec) {
1221
1342
  return spec.engine === "foundation";
1222
1343
  }
1344
+ function isDecisionSpecFile(spec) {
1345
+ return spec.engine === "decision";
1346
+ }
1347
+
1348
+ // src/local-runtime/dataset.ts
1349
+ import { readFile as readFile2 } from "fs/promises";
1350
+ async function loadNormalizedChatJsonl(path) {
1351
+ const text = await readFile2(path, "utf8");
1352
+ const rows = [];
1353
+ const jsonLines = [];
1354
+ for (const [index, line] of text.split(/\r?\n/).entries()) {
1355
+ if (!line.trim()) continue;
1356
+ let value;
1357
+ try {
1358
+ value = JSON.parse(line);
1359
+ } catch (error) {
1360
+ throw new Error(`Invalid chat JSONL row ${index + 1}: malformed JSON`, { cause: error });
1361
+ }
1362
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1363
+ throw new Error(`Invalid chat JSONL row ${index + 1}: expected an object`);
1364
+ }
1365
+ const messages = value.messages;
1366
+ if (!Array.isArray(messages) || ![2, 3].includes(messages.length) || messages.some(
1367
+ (message) => !message || typeof message !== "object" || Array.isArray(message) || !["system", "user", "assistant"].includes(String(message.role)) || typeof message.content !== "string"
1368
+ )) {
1369
+ throw new Error(
1370
+ `Invalid chat JSONL row ${index + 1}: expected an optional system message, one user message, and one assistant answer`
1371
+ );
1372
+ }
1373
+ const row = { messages };
1374
+ const offset = row.messages.length === 3 ? 1 : 0;
1375
+ if (offset === 1 && row.messages[0]?.role !== "system" || row.messages[offset]?.role !== "user" || !row.messages[offset]?.content.trim() || row.messages[offset + 1]?.role !== "assistant" || !row.messages[offset + 1]?.content.trim()) {
1376
+ throw new Error(
1377
+ `Invalid chat JSONL row ${index + 1}: expected an optional system message, one non-empty user message, and one non-empty assistant answer`
1378
+ );
1379
+ }
1380
+ rows.push(row);
1381
+ jsonLines.push(JSON.stringify(row));
1382
+ }
1383
+ if (rows.length === 0) throw new Error(`Chat JSONL contains no examples: ${path}`);
1384
+ return {
1385
+ rows,
1386
+ jsonl: jsonLines.join("\n")
1387
+ };
1388
+ }
1389
+ async function normalizeChatJsonlForRelocation(path) {
1390
+ return (await loadNormalizedChatJsonl(path)).jsonl;
1391
+ }
1392
+ function buildSystemMessage(spec) {
1393
+ const parts = [];
1394
+ const guidelines = spec.guidelines.map((value) => value.trim()).filter(Boolean);
1395
+ const constraints = spec.constraints.map((value) => value.trim()).filter(Boolean);
1396
+ if (spec.system_prompt.trim()) parts.push(spec.system_prompt.trim());
1397
+ if (guidelines.length > 0) {
1398
+ parts.push(`Guidelines:
1399
+ ${guidelines.map((guideline) => `- ${guideline}`).join("\n")}`);
1400
+ }
1401
+ if (constraints.length > 0) {
1402
+ parts.push(`Constraints:
1403
+ ${constraints.map((constraint) => `- ${constraint}`).join("\n")}`);
1404
+ }
1405
+ return parts.join("\n\n") || "Follow the demonstrated behavior.";
1406
+ }
1407
+ function exampleToChatRow(spec, example) {
1408
+ const system = buildSystemMessage(spec);
1409
+ return {
1410
+ messages: [
1411
+ { role: "system", content: system },
1412
+ { role: "user", content: example.input },
1413
+ { role: "assistant", content: example.output }
1414
+ ]
1415
+ };
1416
+ }
1417
+ function compileSpecToJsonl(spec) {
1418
+ return spec.examples.map((example) => JSON.stringify(exampleToChatRow(spec, example))).join("\n");
1419
+ }
1420
+ function examplesFromSpec(spec) {
1421
+ return spec.examples;
1422
+ }
1423
+ async function examplesFromChatJsonl(path) {
1424
+ const normalized = await loadNormalizedChatJsonl(path);
1425
+ const examples = [];
1426
+ for (const [index, row] of normalized.rows.entries()) {
1427
+ const user = row.messages.at(-2);
1428
+ const assistant = row.messages.at(-1);
1429
+ examples.push({
1430
+ input: user.content,
1431
+ output: assistant.content
1432
+ });
1433
+ }
1434
+ return examples;
1435
+ }
1436
+ async function evaluationSuiteFromChatJsonl(path, configuredSystem) {
1437
+ const normalized = await loadNormalizedChatJsonl(path);
1438
+ const inputIdentities = /* @__PURE__ */ new Set();
1439
+ for (const row of normalized.rows) {
1440
+ const input = row.messages.at(-2).content;
1441
+ const identity = input.trim().replace(/\s+/g, " ").toLowerCase();
1442
+ if (inputIdentities.has(identity)) {
1443
+ throw new Error(
1444
+ "General regression data contains duplicate inputs; evaluation prompts must be unique."
1445
+ );
1446
+ }
1447
+ inputIdentities.add(identity);
1448
+ }
1449
+ const systems = new Set(
1450
+ normalized.rows.map((row) => row.messages.find((message) => message.role === "system")?.content).filter((value) => value !== void 0)
1451
+ );
1452
+ if (systems.size > 1) {
1453
+ throw new Error("General regression rows must use one consistent system message.");
1454
+ }
1455
+ const embeddedSystem = [...systems][0];
1456
+ if (configuredSystem !== void 0 && embeddedSystem !== void 0 && configuredSystem !== embeddedSystem) {
1457
+ throw new Error(
1458
+ "evaluation.generalRegression.systemPrompt must match the system message embedded in the dataset."
1459
+ );
1460
+ }
1461
+ return {
1462
+ examples: normalized.rows.map((row) => ({
1463
+ input: row.messages.at(-2).content,
1464
+ output: row.messages.at(-1).content
1465
+ })),
1466
+ system: configuredSystem ?? embeddedSystem ?? "You are a helpful assistant."
1467
+ };
1468
+ }
1223
1469
 
1224
1470
  // src/local-runtime/local-project.ts
1225
1471
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
1226
- import { readFile as readFile2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1472
+ import { readFile as readFile3, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1227
1473
  import { dirname as dirname2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
1228
1474
  import { homedir as homedir2 } from "os";
1229
1475
  import { mkdir as mkdir2 } from "fs/promises";
@@ -1432,6 +1678,34 @@ async function initLocalSpecFile(args) {
1432
1678
  });
1433
1679
  await mkdir2(dirname2(args.outputPath), { recursive: true });
1434
1680
  await writeFile2(args.outputPath, `${JSON.stringify(spec2, null, 2)}
1681
+ `, "utf8");
1682
+ return spec2;
1683
+ }
1684
+ if (args.engine === "decision") {
1685
+ const spec2 = localDecisionSpecFileSchema.parse({
1686
+ engine: "decision",
1687
+ id: randomUUID2(),
1688
+ name: args.name,
1689
+ description: "Typed decision model: one labelled answer per input.",
1690
+ system_prompt: "Describe the decision this local model should make.",
1691
+ guidelines: [],
1692
+ constraints: [],
1693
+ base_model: args.baseModel ?? DEFAULT_DECISION_MODEL,
1694
+ decision: {
1695
+ type: "choice",
1696
+ criteria: {
1697
+ first_label: "Replace this with what the first label means.",
1698
+ second_label: "Replace this with what the second label means."
1699
+ }
1700
+ },
1701
+ examples: [
1702
+ { input: "Replace this with a representative input.", output: "first_label" },
1703
+ { input: "Replace this with a different input.", output: "second_label" }
1704
+ ],
1705
+ hyperparameters: { n_epochs: 3 }
1706
+ });
1707
+ await mkdir2(dirname2(args.outputPath), { recursive: true });
1708
+ await writeFile2(args.outputPath, `${JSON.stringify(spec2, null, 2)}
1435
1709
  `, "utf8");
1436
1710
  return spec2;
1437
1711
  }
@@ -1491,7 +1765,7 @@ async function initLocalRunnerConfigFile(args) {
1491
1765
  }
1492
1766
  function placeholderIssues(systemPrompt, examples) {
1493
1767
  const issues = [];
1494
- if (/describe the behavior this local model should learn/i.test(systemPrompt)) {
1768
+ if (/describe the (behavior|decision) this local model should (learn|make)/i.test(systemPrompt)) {
1495
1769
  issues.push("system_prompt still contains the generated placeholder");
1496
1770
  }
1497
1771
  examples.forEach((example, index) => {
@@ -1513,6 +1787,14 @@ function assertLocalRunInputReady(request) {
1513
1787
  function foundationPlaceholderIssues(spec) {
1514
1788
  return placeholderIssues(spec.system_prompt, spec.examples);
1515
1789
  }
1790
+ function decisionPlaceholderIssues(spec) {
1791
+ const issues = placeholderIssues(spec.system_prompt, spec.examples);
1792
+ const descriptions = spec.decision.type === "noul" ? Object.values(spec.decision.criteria ?? {}) : Object.values(spec.decision.criteria);
1793
+ if (descriptions.some((value) => /replace this with/i.test(value ?? ""))) {
1794
+ issues.push("decision.criteria still contains generated placeholder text");
1795
+ }
1796
+ return issues;
1797
+ }
1516
1798
  function assertFoundationSpecReady(spec) {
1517
1799
  const issues = foundationPlaceholderIssues(spec);
1518
1800
  if (issues.length > 0) {
@@ -1520,9 +1802,9 @@ function assertFoundationSpecReady(spec) {
1520
1802
  }
1521
1803
  }
1522
1804
  function runRequestFromLocalSpec(spec, options = {}) {
1523
- if (isFoundationSpecFile(spec)) {
1805
+ if (isFoundationSpecFile(spec) || isDecisionSpecFile(spec)) {
1524
1806
  throw new Error(
1525
- "Foundation specs cannot be converted into a LoRA run request. Use `tt pipeline run --spec`."
1807
+ `${isFoundationSpecFile(spec) ? "Foundation" : "Decision"} specs cannot be converted into a LoRA run request. Use \`tt pipeline run --spec\`.`
1526
1808
  );
1527
1809
  }
1528
1810
  const {
@@ -1570,6 +1852,9 @@ function parseLocalRunInput(rawInput, path, options = {}) {
1570
1852
  if (isFoundationSpecFile(spec)) {
1571
1853
  return { kind: "foundation-spec", path, spec };
1572
1854
  }
1855
+ if (isDecisionSpecFile(spec)) {
1856
+ return { kind: "decision-spec", path, spec };
1857
+ }
1573
1858
  return {
1574
1859
  kind: "spec",
1575
1860
  path,
@@ -1579,7 +1864,7 @@ function parseLocalRunInput(rawInput, path, options = {}) {
1579
1864
  }
1580
1865
  async function loadLocalRunInput(path, options = {}) {
1581
1866
  return parseLocalRunInput(
1582
- JSON.parse(await readFile2(path, "utf8")),
1867
+ JSON.parse(await readFile3(path, "utf8")),
1583
1868
  path,
1584
1869
  options
1585
1870
  );
@@ -1594,7 +1879,7 @@ function validateBehaviorSpec(input) {
1594
1879
  return { valid: false, errors: ["Use a behavior spec, not a run request."], warnings };
1595
1880
  }
1596
1881
  const { spec } = input;
1597
- const placeholders = input.kind === "foundation-spec" ? foundationPlaceholderIssues(input.spec) : generatedPlaceholderIssues(input.request);
1882
+ const placeholders = input.kind === "foundation-spec" ? foundationPlaceholderIssues(input.spec) : input.kind === "decision-spec" ? decisionPlaceholderIssues(input.spec) : generatedPlaceholderIssues(input.request);
1598
1883
  if (placeholders.length) {
1599
1884
  errors.push(`Edit the generated behavior spec before training: ${placeholders.join("; ")}.`);
1600
1885
  }
@@ -1609,6 +1894,29 @@ function validateBehaviorSpec(input) {
1609
1894
  errors.push("Foundation RL requires numeric expected outputs in every example.");
1610
1895
  }
1611
1896
  }
1897
+ if (input.kind === "decision-spec") {
1898
+ const labels = decisionLabels(input.spec.decision);
1899
+ const normalized = new Map(labels.map((label) => [label.trim().toLowerCase(), label]));
1900
+ if (normalized.size !== labels.length) {
1901
+ errors.push("decision labels must stay distinct ignoring case and surrounding spaces.");
1902
+ }
1903
+ const counts = /* @__PURE__ */ new Map();
1904
+ for (const [index, example] of spec.examples.entries()) {
1905
+ const label = normalized.get(example.output.trim().toLowerCase());
1906
+ if (!label) {
1907
+ errors.push(`examples[${index}].output must be one of the decision labels: ${labels.join(", ")}.`);
1908
+ } else {
1909
+ counts.set(label, (counts.get(label) ?? 0) + 1);
1910
+ }
1911
+ }
1912
+ const missing = labels.filter((label) => !counts.has(label));
1913
+ if (missing.length && spec.examples.length) {
1914
+ warnings.push(`No examples use decision label(s) ${missing.join(", ")}; the tuned model learns those only from their descriptions.`);
1915
+ }
1916
+ if (buildSystemMessage(spec).length > 600) {
1917
+ warnings.push("The compiled decision instruction exceeds 600 characters; the decision model reads about 190 tokens of instruction and options, so later text may be truncated.");
1918
+ }
1919
+ }
1612
1920
  for (const key of ["name", "system_prompt"]) {
1613
1921
  if (!spec[key].trim()) errors.push(`${key} must not be blank.`);
1614
1922
  }
@@ -1784,7 +2092,7 @@ ${validation.errors.join("\n")}`);
1784
2092
  const pipeline = parsePipeline2(recipe);
1785
2093
  if (isFoundationPipeline2(pipeline) !== foundation) {
1786
2094
  const recipeEngine = isFoundationPipeline2(pipeline) ? "a foundation" : "an adapter";
1787
- const specEngine = foundation ? "a foundation" : "an adapter";
2095
+ const specEngine = foundation ? "a foundation" : input.kind === "decision-spec" ? "a decision" : "an adapter";
1788
2096
  throw new Error(`Pipeline is ${recipeEngine} recipe, but the behavior spec is ${specEngine} spec. Use a matching recipe or omit --file.`);
1789
2097
  }
1790
2098
  if (foundation) assertFoundationSettingsMatch(input.spec.foundation, pipeline);
@@ -1833,128 +2141,6 @@ function assertFoundationSettingsMatch(hp, plan) {
1833
2141
  }
1834
2142
  }
1835
2143
 
1836
- // src/local-runtime/dataset.ts
1837
- import { readFile as readFile3 } from "fs/promises";
1838
- async function loadNormalizedChatJsonl(path) {
1839
- const text = await readFile3(path, "utf8");
1840
- const rows = [];
1841
- const jsonLines = [];
1842
- for (const [index, line] of text.split(/\r?\n/).entries()) {
1843
- if (!line.trim()) continue;
1844
- let value;
1845
- try {
1846
- value = JSON.parse(line);
1847
- } catch (error) {
1848
- throw new Error(`Invalid chat JSONL row ${index + 1}: malformed JSON`, { cause: error });
1849
- }
1850
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1851
- throw new Error(`Invalid chat JSONL row ${index + 1}: expected an object`);
1852
- }
1853
- const messages = value.messages;
1854
- if (!Array.isArray(messages) || ![2, 3].includes(messages.length) || messages.some(
1855
- (message) => !message || typeof message !== "object" || Array.isArray(message) || !["system", "user", "assistant"].includes(String(message.role)) || typeof message.content !== "string"
1856
- )) {
1857
- throw new Error(
1858
- `Invalid chat JSONL row ${index + 1}: expected an optional system message, one user message, and one assistant answer`
1859
- );
1860
- }
1861
- const row = { messages };
1862
- const offset = row.messages.length === 3 ? 1 : 0;
1863
- if (offset === 1 && row.messages[0]?.role !== "system" || row.messages[offset]?.role !== "user" || !row.messages[offset]?.content.trim() || row.messages[offset + 1]?.role !== "assistant" || !row.messages[offset + 1]?.content.trim()) {
1864
- throw new Error(
1865
- `Invalid chat JSONL row ${index + 1}: expected an optional system message, one non-empty user message, and one non-empty assistant answer`
1866
- );
1867
- }
1868
- rows.push(row);
1869
- jsonLines.push(JSON.stringify(row));
1870
- }
1871
- if (rows.length === 0) throw new Error(`Chat JSONL contains no examples: ${path}`);
1872
- return {
1873
- rows,
1874
- jsonl: jsonLines.join("\n")
1875
- };
1876
- }
1877
- async function normalizeChatJsonlForRelocation(path) {
1878
- return (await loadNormalizedChatJsonl(path)).jsonl;
1879
- }
1880
- function buildSystemMessage(spec) {
1881
- const parts = [];
1882
- const guidelines = spec.guidelines.map((value) => value.trim()).filter(Boolean);
1883
- const constraints = spec.constraints.map((value) => value.trim()).filter(Boolean);
1884
- if (spec.system_prompt.trim()) parts.push(spec.system_prompt.trim());
1885
- if (guidelines.length > 0) {
1886
- parts.push(`Guidelines:
1887
- ${guidelines.map((guideline) => `- ${guideline}`).join("\n")}`);
1888
- }
1889
- if (constraints.length > 0) {
1890
- parts.push(`Constraints:
1891
- ${constraints.map((constraint) => `- ${constraint}`).join("\n")}`);
1892
- }
1893
- return parts.join("\n\n") || "Follow the demonstrated behavior.";
1894
- }
1895
- function exampleToChatRow(spec, example) {
1896
- const system = buildSystemMessage(spec);
1897
- return {
1898
- messages: [
1899
- { role: "system", content: system },
1900
- { role: "user", content: example.input },
1901
- { role: "assistant", content: example.output }
1902
- ]
1903
- };
1904
- }
1905
- function compileSpecToJsonl(spec) {
1906
- return spec.examples.map((example) => JSON.stringify(exampleToChatRow(spec, example))).join("\n");
1907
- }
1908
- function examplesFromSpec(spec) {
1909
- return spec.examples;
1910
- }
1911
- async function examplesFromChatJsonl(path) {
1912
- const normalized = await loadNormalizedChatJsonl(path);
1913
- const examples = [];
1914
- for (const [index, row] of normalized.rows.entries()) {
1915
- const user = row.messages.at(-2);
1916
- const assistant = row.messages.at(-1);
1917
- examples.push({
1918
- input: user.content,
1919
- output: assistant.content
1920
- });
1921
- }
1922
- return examples;
1923
- }
1924
- async function evaluationSuiteFromChatJsonl(path, configuredSystem) {
1925
- const normalized = await loadNormalizedChatJsonl(path);
1926
- const inputIdentities = /* @__PURE__ */ new Set();
1927
- for (const row of normalized.rows) {
1928
- const input = row.messages.at(-2).content;
1929
- const identity = input.trim().replace(/\s+/g, " ").toLowerCase();
1930
- if (inputIdentities.has(identity)) {
1931
- throw new Error(
1932
- "General regression data contains duplicate inputs; evaluation prompts must be unique."
1933
- );
1934
- }
1935
- inputIdentities.add(identity);
1936
- }
1937
- const systems = new Set(
1938
- normalized.rows.map((row) => row.messages.find((message) => message.role === "system")?.content).filter((value) => value !== void 0)
1939
- );
1940
- if (systems.size > 1) {
1941
- throw new Error("General regression rows must use one consistent system message.");
1942
- }
1943
- const embeddedSystem = [...systems][0];
1944
- if (configuredSystem !== void 0 && embeddedSystem !== void 0 && configuredSystem !== embeddedSystem) {
1945
- throw new Error(
1946
- "evaluation.generalRegression.systemPrompt must match the system message embedded in the dataset."
1947
- );
1948
- }
1949
- return {
1950
- examples: normalized.rows.map((row) => ({
1951
- input: row.messages.at(-2).content,
1952
- output: row.messages.at(-1).content
1953
- })),
1954
- system: configuredSystem ?? embeddedSystem ?? "You are a helpful assistant."
1955
- };
1956
- }
1957
-
1958
2144
  // src/local-runtime/run-reporter.ts
1959
2145
  function reportInBackground(action) {
1960
2146
  try {
@@ -2600,6 +2786,31 @@ var foundationRuntimeHash = (() => {
2600
2786
  return hash.digest("hex").slice(0, 20);
2601
2787
  })();
2602
2788
  var FOUNDATION_PYTHON_ENVIRONMENT = pythonEnvironmentPath("uv-foundation", foundationRuntimeHash);
2789
+ var decisionProject = join4(packageRoot, "training/decision");
2790
+ var decisionRuntimeHash = (() => {
2791
+ const hash = createHash3("sha256");
2792
+ for (const name of ["pyproject.toml", "uv.lock"]) {
2793
+ hash.update(readFileSync(join4(decisionProject, name)));
2794
+ }
2795
+ return hash.digest("hex").slice(0, 20);
2796
+ })();
2797
+ var DECISION_PYTHON_ENVIRONMENT = pythonEnvironmentPath("uv-decision", decisionRuntimeHash);
2798
+ function buildDecisionPythonCommand(entrypoint, args = []) {
2799
+ const commandArgs = [
2800
+ "run",
2801
+ "--frozen",
2802
+ "--quiet",
2803
+ "--project",
2804
+ decisionProject,
2805
+ "python",
2806
+ join4(decisionProject, "src", entrypoint),
2807
+ ...args
2808
+ ];
2809
+ return { command: "uv", commandArgs, displayCommand: ["uv", ...commandArgs] };
2810
+ }
2811
+ function withDecisionPythonEnvironment(env) {
2812
+ return { ...env, UV_PROJECT_ENVIRONMENT: DECISION_PYTHON_ENVIRONMENT };
2813
+ }
2603
2814
  function buildBundledPythonCommand(entrypoint, args = []) {
2604
2815
  const target = entrypoint === "-c" ? entrypoint : join4(bundledProject, "src", entrypoint);
2605
2816
  const commandArgs = [
@@ -4640,7 +4851,7 @@ async function resolveBaseModelRevision(request, config) {
4640
4851
  request.hyperparameters.base_model_revision
4641
4852
  );
4642
4853
  if (config.paths.baseModel) {
4643
- return defaultBaseModelRevision(request.spec_snapshot.base_model);
4854
+ return isCertifiedTrainingModel(request.spec_snapshot.base_model) ? defaultBaseModelRevision(request.spec_snapshot.base_model) : requested;
4644
4855
  }
4645
4856
  if (requested) return requested;
4646
4857
  if (!request.spec_snapshot.base_model.includes("/")) {
@@ -6026,6 +6237,12 @@ function formatCapabilitySummary(report) {
6026
6237
  }
6027
6238
  function warningsFromSnapshot(report, target) {
6028
6239
  const warnings = [];
6240
+ if (target.engine === "decision") {
6241
+ if (!report.cuda_available) {
6242
+ warnings.push("No CUDA GPU detected: decision training runs on CPU or Apple MPS (roughly 15 s per 8-example step on a 4-core CPU).");
6243
+ }
6244
+ return warnings;
6245
+ }
6029
6246
  if (target.engine === "foundation") {
6030
6247
  for (const kind of ["train", "finetune"]) {
6031
6248
  const verdict = report.foundation[kind];
@@ -6460,6 +6677,7 @@ async function assessHardware(options = {}) {
6460
6677
  }
6461
6678
 
6462
6679
  export {
6680
+ fileUri,
6463
6681
  assertArtifactManifest,
6464
6682
  TRAINING_MODELS,
6465
6683
  resolveTrainingModel,
@@ -6467,9 +6685,12 @@ export {
6467
6685
  defaultBaseModelRevision,
6468
6686
  resolveModelLoader,
6469
6687
  assertUsableModelArtifact,
6688
+ DECISION_MODELS,
6689
+ resolveDecisionModel,
6470
6690
  DEFAULT_ARTIFACT_ROOT,
6471
6691
  DEFAULT_PROJECT_STORE_ROOT,
6472
6692
  DEFAULT_FOUNDATION_RUNS_DIR,
6693
+ DEFAULT_DECISION_RUNS_DIR,
6473
6694
  expandUserPath,
6474
6695
  getConfigDir,
6475
6696
  getAgentConfigDir,
@@ -6495,8 +6716,20 @@ export {
6495
6716
  projectRuntimeSchema,
6496
6717
  localFoundationSpecFileSchema,
6497
6718
  localAdapterSpecFileSchema,
6719
+ decisionQuestionSchema,
6720
+ decisionHyperparametersSchema,
6721
+ localDecisionSpecFileSchema,
6722
+ decisionLabels,
6498
6723
  localBehaviorSpecFileSchema,
6499
6724
  isFoundationSpecFile,
6725
+ isDecisionSpecFile,
6726
+ normalizeChatJsonlForRelocation,
6727
+ buildSystemMessage,
6728
+ exampleToChatRow,
6729
+ compileSpecToJsonl,
6730
+ examplesFromSpec,
6731
+ examplesFromChatJsonl,
6732
+ evaluationSuiteFromChatJsonl,
6500
6733
  validateEvalCases,
6501
6734
  DEFAULT_LOCAL_SPEC_PATH,
6502
6735
  localPathsOverlap,
@@ -6506,6 +6739,7 @@ export {
6506
6739
  generatedPlaceholderIssues,
6507
6740
  assertLocalRunInputReady,
6508
6741
  foundationPlaceholderIssues,
6742
+ decisionPlaceholderIssues,
6509
6743
  assertFoundationSpecReady,
6510
6744
  runRequestFromLocalSpec,
6511
6745
  parseLocalRunInput,
@@ -6522,17 +6756,12 @@ export {
6522
6756
  canonicalJson2 as canonicalJson,
6523
6757
  isFoundationPipeline3 as isFoundationPipeline,
6524
6758
  parsePipeline3 as parsePipeline,
6525
- normalizeChatJsonlForRelocation,
6526
- buildSystemMessage,
6527
- exampleToChatRow,
6528
- compileSpecToJsonl,
6529
- examplesFromSpec,
6530
- examplesFromChatJsonl,
6531
- evaluationSuiteFromChatJsonl,
6532
6759
  reportInBackground,
6533
6760
  sanitizeLogLine,
6534
6761
  forwardStreamLines,
6535
6762
  localRuntimePackageRoot,
6763
+ buildDecisionPythonCommand,
6764
+ withDecisionPythonEnvironment,
6536
6765
  buildBundledPythonCommand,
6537
6766
  withBundledPythonEnvironment,
6538
6767
  buildFoundationPythonCommand,
@@ -6550,6 +6779,9 @@ export {
6550
6779
  writePrivateJsonAtomic,
6551
6780
  createLocalStore,
6552
6781
  tokenF1,
6782
+ deriveSampleSeed,
6783
+ splitSpecExamples,
6784
+ compareEvalReports,
6553
6785
  verifyModelPrefetchCacheReport,
6554
6786
  buildModelPrefetchPayload,
6555
6787
  verifyLocalBaseModel,
@@ -6567,10 +6799,11 @@ export {
6567
6799
  runLocalFineTune,
6568
6800
  configForSpec,
6569
6801
  resolveProjectConfig,
6802
+ evaluateCapabilities,
6570
6803
  warningsFromSnapshot,
6571
6804
  readHardwareSnapshot,
6572
6805
  formatHostStatusLine,
6573
6806
  formatAgentHostBlock,
6574
6807
  assessHardware
6575
6808
  };
6576
- //# sourceMappingURL=chunk-4Q54NHKR.js.map
6809
+ //# sourceMappingURL=chunk-PVLX7LJI.js.map