@tuned-tensor/cli 0.17.0 → 0.18.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.
@@ -1,6 +1,6 @@
1
1
  # Use your AWS GPU with the local pipeline
2
2
 
3
- TT runs one pipeline on your laptop. Add `gpu` to `local-runner.json` to send
3
+ TT runs one pipeline on your laptop. Add `runtime.gpu` to `tunedtensor.json` to send
4
4
  adapter training and CUDA evaluation, or foundation pretraining, fine-tuning,
5
5
  RL and evaluation, to an existing EC2 instance in your account. Foundation
6
6
  tokenization, dataset preparation, adapter scoring/comparisons, run history,
@@ -52,21 +52,23 @@ only needs `ec2:DescribeInstances` for lookup.
52
52
 
53
53
  ## 3. Configure the connection
54
54
 
55
- Save this as `local-runner.json` beside `tunedtensor.json`,
56
- replacing the example values with your own. You can also copy
57
- [`examples/local-runtime/aws-runner.json`](../../examples/local-runtime/aws-runner.json).
58
- All runner paths refer to the laptop; TT translates the paths needed remotely.
55
+ Add this `runtime` section to your existing `tunedtensor.json`, replacing the
56
+ example values with your own. All paths refer to the laptop; TT translates
57
+ paths needed remotely. Existing sidecars can be consolidated with
58
+ `tt pipeline migrate`, which retains `.bak` copies.
59
59
 
60
60
  ```json
61
61
  {
62
- "gpu": {
63
- "provider": "aws",
64
- "profile": "research",
65
- "region": "eu-west-1",
66
- "instanceId": "i-0123456789abcdef0",
67
- "user": "ubuntu",
68
- "identityFile": "~/.ssh/research-gpu.pem",
69
- "maxSeconds": 86400
62
+ "runtime": {
63
+ "gpu": {
64
+ "provider": "aws",
65
+ "profile": "research",
66
+ "region": "eu-west-1",
67
+ "instanceId": "i-0123456789abcdef0",
68
+ "user": "ubuntu",
69
+ "identityFile": "~/.ssh/research-gpu.pem",
70
+ "maxSeconds": 86400
71
+ }
70
72
  }
71
73
  }
72
74
  ```
@@ -94,7 +96,7 @@ Verify the instance's SSH host key for the selected IP address and connect
94
96
  once yourself before using TT. TT requires a known host key and noninteractive
95
97
  SSH authentication. Omit
96
98
  `identityFile` to use your SSH agent or SSH configuration. Relative identity
97
- file paths resolve beside the runner config; `~` expands locally. Set
99
+ file paths resolve beside the core spec; `~` expands locally. Set
98
100
  `privateIp: true` when connected to the instance's VPC through a VPN or other
99
101
  private network. The default uses its public IPv4 address. Security groups and
100
102
  network routes must allow the laptop to reach that address over SSH.
@@ -137,28 +139,27 @@ Run these commands on your laptop in the project directory. Adapter workflow:
137
139
  ```bash
138
140
  # Fetch the base model locally first.
139
141
  tt validate tunedtensor.json
140
- tt models prefetch tunedtensor.json --config local-runner.json
141
- tt doctor tunedtensor.json --config local-runner.json
142
- tt pipeline run --spec tunedtensor.json --config local-runner.json --dry-run
143
- tt pipeline run --spec tunedtensor.json --config local-runner.json
142
+ tt models prefetch tunedtensor.json
143
+ tt doctor tunedtensor.json
144
+ tt pipeline run --spec tunedtensor.json --dry-run
145
+ tt pipeline run --spec tunedtensor.json
144
146
  ```
145
147
 
146
148
  For a foundation spec, no base-model prefetch is needed. Start a new run with:
147
149
 
148
150
  ```bash
149
151
  tt validate tunedtensor.json
150
- tt doctor tunedtensor.json --config local-runner.json
151
- tt pipeline run --spec tunedtensor.json --config local-runner.json \
152
+ tt doctor tunedtensor.json
153
+ tt pipeline run --spec tunedtensor.json \
152
154
  --dry-run
153
- tt pipeline run --spec tunedtensor.json --config local-runner.json
155
+ tt pipeline run --spec tunedtensor.json
154
156
  ```
155
157
 
156
158
  Choose the command block for your spec's engine. `tt doctor` connects to the
157
159
  instance and prepares/probes its locked Python/CUDA runtime, which may take
158
160
  time on first use. It does not start training or reserve GPU capacity.
159
161
  `tt hardware` reports the laptop's hardware; it does not inspect the AWS
160
- instance. `tt pipeline run` discovers `local-runner.json` beside the selected
161
- spec when `--config` is omitted.
162
+ instance. `tt pipeline run` reads `runtime.gpu` from the selected spec.
162
163
 
163
164
  Keep pipeline step targets set to `local`: the target describes the
164
165
  orchestrator, and `gpu` selects where its GPU processes execute. CPU adapter
@@ -172,10 +173,10 @@ Wait for a successful completion message. Adapter runs return a run ID and a
172
173
  local model ID; use the actual IDs from the output:
173
174
 
174
175
  ```bash
175
- tt runs list --config local-runner.json
176
- tt runs report RUN_ID --config local-runner.json
177
- tt models list --config local-runner.json
178
- tt models verify MODEL_ID --config local-runner.json
176
+ tt runs list
177
+ tt runs report RUN_ID
178
+ tt models list
179
+ tt models verify MODEL_ID
179
180
  ```
180
181
 
181
182
  For foundation runs, inspect `report.json` in the run directory printed by the
@@ -38,7 +38,7 @@ not mutate the training runtime.
38
38
  ```bash
39
39
  tt models prefetch tunedtensor.json
40
40
 
41
- tt serve local-<run-id> --config local-runner.json \
41
+ tt serve local-<run-id> \
42
42
  --context-length 8192 --max-tokens 512
43
43
  ```
44
44
 
@@ -81,7 +81,7 @@ Export a new isolated configuration rather than replacing global settings:
81
81
 
82
82
  ```bash
83
83
  export PI_CODING_AGENT_DIR="$(mktemp -d)"
84
- tt serve local-<run-id> --config local-runner.json \
84
+ tt serve local-<run-id> \
85
85
  --context-length 8192 --max-tokens 512 \
86
86
  --print-client-config pi > "$PI_CODING_AGENT_DIR/models.json"
87
87
  ```
@@ -78,28 +78,23 @@ Edit both generated examples in `tunedtensor.json`. For a meaningful run,
78
78
  replace them with a larger, representative dataset and a separate validation
79
79
  split.
80
80
 
81
- The generated `local-runner.json` uses CUDA and project-local artifacts. A
82
- durable Spark configuration can set:
81
+ Initialization creates only `tunedtensor.json`; CUDA and project-local storage
82
+ are defaults. For durable Spark paths, add this optional section to the spec:
83
83
 
84
84
  ```json
85
85
  {
86
- "artifactRoot": "/home/eve/tuned-tensor-runs/artifacts",
87
- "storeRoot": "/home/eve/tuned-tensor-runs/store",
88
- "paths": {
89
- "modelCache": "/home/eve/.cache/huggingface"
90
- },
91
- "evaluation": {
92
- "inference": {
93
- "device": "cuda"
94
- },
95
- "scoring": {
96
- "mode": "exact_match"
97
- },
98
- "timeoutMs": 1800000
86
+ "runtime": {
87
+ "artifactRoot": "/home/eve/tuned-tensor-runs/artifacts",
88
+ "storeRoot": "/home/eve/tuned-tensor-runs/store",
89
+ "paths": { "modelCache": "/home/eve/.cache/huggingface" }
99
90
  }
100
91
  }
101
92
  ```
102
93
 
94
+ Existing projects can use `tt pipeline migrate` to fold sidecar settings into
95
+ the spec while keeping backups.
96
+
97
+
103
98
  Every Python stage uses the locked runtime included in the npm package; a
104
99
  source checkout and a custom runner path are neither required nor supported.
105
100
 
@@ -110,7 +105,7 @@ tt doctor tunedtensor.json
110
105
  tt validate tunedtensor.json
111
106
  tt models prefetch tunedtensor.json
112
107
  tt models verify-base tunedtensor.json
113
- tt pipeline run --spec tunedtensor.json --config local-runner.json
108
+ tt pipeline run --spec tunedtensor.json
114
109
  ```
115
110
 
116
111
  `doctor` resolves the same bundled project and paths the run will use, imports
@@ -136,7 +131,7 @@ tt serve local-<run-id> --spec tunedtensor.json --port 8000
136
131
  ```
137
132
 
138
133
  `tt serve active` fails unless an adapter is activated. Activation requires a
139
- `generalRegression` suite in `local-runner.json`. Serve a specific adapter
134
+ `generalRegression` suite in `tunedtensor.json`. Serve a specific adapter
140
135
  with `tt serve local-<run-id>` until then.
141
136
 
142
137
  In another shell:
@@ -85,9 +85,9 @@
85
85
  <div class="tt-level">
86
86
  <div class="tt-unit"><button class="btn" type="button" data-component="validation" aria-pressed="false">Shared validation</button><small class="text-small">schema → readiness checks</small></div>
87
87
  <div class="tt-unit"><button class="btn" type="button" data-component="plan" aria-pressed="false">Derive the pipeline</button><small class="text-small">pipelineForRunInput → plan</small></div>
88
- <div class="tt-unit"><button class="btn" type="button" data-component="config" aria-pressed="false">Runner configuration</button><small class="text-small">GPU placement · paths · scoring</small></div>
88
+ <div class="tt-unit"><button class="btn" type="button" data-component="config" aria-pressed="false">Spec runtime and evaluation</button><small class="text-small">GPU placement · paths · scoring</small></div>
89
89
  </div>
90
- <div class="tt-arrow text-small">↓ same spec and plan · preview or explicit run · runner config supplies execution policy</div>
90
+ <div class="tt-arrow text-small">↓ same spec and plan · preview or explicit run · core spec supplies execution policy</div>
91
91
  <div class="tt-boundary text-small">03 / EXECUTE & EVALUATE</div>
92
92
  <div class="tt-level tt-pair">
93
93
  <div class="tt-unit"><button class="btn" type="button" data-component="adapter" aria-pressed="false">Adapter engine</button><small class="text-small">baseline → LoRA → candidate → compare</small></div>
@@ -167,7 +167,7 @@
167
167
  "summary": "One saved recipe for instructions, examples, engine and training parameters.",
168
168
  "input": "Reviewed user intent: prompt, guidelines, constraints, examples and settings.",
169
169
  "output": "One parsed snapshot, identified by a SHA-256, for planning and execution.",
170
- "boundary": "The spec defines behavior and training. Runner configuration still defines placement, paths and evaluation policy.",
170
+ "boundary": "The spec defines behavior and training. Its runtime and evaluation sections define placement, paths and evaluation policy.",
171
171
  "flow": [
172
172
  "read bytes",
173
173
  "parse once",
@@ -185,26 +185,26 @@
185
185
  "example": "/spec\n/spec diff\n/spec validate\n/spec history\n\n# Outside the TT shell:\ntt spec show --json"
186
186
  },
187
187
  "config": {
188
- "title": "Runner configuration",
188
+ "title": "Spec runtime and evaluation",
189
189
  "location": "Project · JSON",
190
190
  "summary": "Execution placement, filesystem paths and evaluation policy remain explicit runtime choices.",
191
- "input": "local-runner.json beside the spec, or --config.",
191
+ "input": "runtime and evaluation sections in tunedtensor.json; legacy --config remains compatible.",
192
192
  "output": "GPU dispatch, artifact locations, inference/scoring configuration and regression policy.",
193
- "boundary": "This is not a second behavior recipe. Evaluation settings remain here; moving them into the spec would require a separate schema change.",
193
+ "boundary": "These optional sections belong to the core spec. Runtime defaults apply when omitted; legacy sidecars can be migrated with backups.",
194
194
  "flow": [
195
195
  "config",
196
196
  "resolve paths",
197
197
  "choose process host"
198
198
  ],
199
- "path": "examples/local-runtime/aws-runner.json",
199
+ "path": "examples/single-spec/adapter/tunedtensor.json",
200
200
  "line": 1,
201
- "code": "{\n \"artifactRoot\": \".tuned-tensor/artifacts\",\n \"storeRoot\": \".tuned-tensor/store\",\n \"gpu\": {\n \"provider\": \"aws\",\n \"profile\": \"research\",\n \"region\": \"eu-west-1\",\n \"instanceId\": \"i-0123456789abcdef0\",\n \"user\": \"ubuntu\",\n \"identityFile\": \"~/.ssh/research-gpu.pem\",\n \"maxSeconds\": 86400\n }\n}",
201
+ "code": "{\n \"id\": \"22222222-2222-4222-8222-222222222222\",\n \"name\": \"Local Smoke Assistant\",\n \"description\": \"Tiny local runner smoke test.\",\n \"system_prompt\": \"Return exactly one sentiment label.\",\n \"guidelines\": [\n \"Reply with positive or negative.\"\n ],\n \"constraints\": [\n \"Do not include extra commentary.\"\n ],\n \"base_model\": \"Qwen/Qwen3.5-2B\",\n \"examples\": [\n {\n \"input\": \"Classify sentiment: I love this product.\",\n \"output\": \"positive\"\n },\n {\n \"input\": \"Classify sentiment: This was disappointing.\",\n \"output\": \"negative\"\n },\n {\n \"input\": \"Classify sentiment: The update works perfectly.\",\n \"output\": \"positive\"\n },\n {\n \"input\": \"Classify sentiment: Support never replied.\",\n \"output\": \"negative\"\n }\n ],\n \"hyperparameters\": {\n \"n_epochs\": 1,\n \"learning_rate\": 0.0002,\n \"batch_size\": 1,\n \"gradient_accumulation_steps\": 1,\n \"lora_rank\": 4,\n \"lora_alpha\": 8,\n \"max_seq_length\": 256,\n \"max_eval_examples\": 2\n },\n \"evaluation\": {\n \"inference\": {\n \"maxNewTokens\": 32\n },\n \"scoring\": {\n \"mode\": \"exact_match\"\n }\n }\n}\n",
202
202
  "files": [
203
- "examples/local-runtime/aws-runner.json",
203
+ "examples/single-spec/adapter/tunedtensor.json",
204
204
  "src/local-runtime/contracts.ts",
205
205
  "src/commands/pipeline.ts"
206
206
  ],
207
- "example": "tt doctor tunedtensor.json --config local-runner.json\ntt pipeline run --spec tunedtensor.json --config local-runner.json --dry-run"
207
+ "example": "tt doctor tunedtensor.json\ntt pipeline run --spec tunedtensor.json --dry-run"
208
208
  },
209
209
  "agent": {
210
210
  "title": "Agent & model provider",
@@ -242,8 +242,8 @@
242
242
  "applySpecUpdate"
243
243
  ],
244
244
  "path": "src/spec-workspace.ts",
245
- "line": 133,
246
- "code": "export async function prepareSpecUpdate(workspaceRoot: string, specPath: string, expectedSha256: string, changes: unknown) {\n const current = await inspectLocalSpec(workspaceRoot, specPath);\n if (current.sha256 !== expectedSha256) throw new Error(\"The behavior spec changed; inspect it and prepare the edit again.\");\n if (!isObject(current.document)) throw new Error(\"Repair the spec JSON before preparing an edit.\");\n if (!isObject(changes) || !Object.keys(changes).length || Object.keys(changes).some(key => !EDIT_KEYS.has(key))) {\n throw new Error(\"Provide supported spec field changes; identity and engine cannot be changed by an edit.\");\n }\n const next = { ...current.document, ...changes };\n for (const key of [\"hyperparameters\", \"foundation\"] as const) {\n if (isObject(changes[key])) {\n const previous = isObject(current.document[key]) ? current.document[key] : {};\n next[key] = { ...previous, ...changes[key] };\n }\n }\n const validation = validateDocument(next, current.path);\n if (!validation.valid) throw new Error(`Invalid behavior spec:\\n${validation.errors.join(\"\\n\")}`);\n const source = `${JSON.stringify(next, null, 2)}\\n`;\n if (Buffer.byteLength(source) > MAX_BYTES) throw new Error(\"Updated spec is too large.\");\n const diff = specDiff(current.document, next);\n if (diff === \"No changes.\") throw new Error(\"The proposed edit makes no changes.\");\n const update: SpecUpdate = {\n spec_path: current.displayPath,\n expected_sha256: current.sha256,\n after_sha256: specHash(source),",
245
+ "line": 138,
246
+ "code": "export async function prepareSpecUpdate(workspaceRoot: string, specPath: string, expectedSha256: string, changes: unknown) {\n const current = await inspectLocalSpec(workspaceRoot, specPath);\n if (current.sha256 !== expectedSha256) throw new Error(\"The behavior spec changed; inspect it and prepare the edit again.\");\n if (!isObject(current.document)) throw new Error(\"Repair the spec JSON before preparing an edit.\");\n if (!isObject(changes) || !Object.keys(changes).length || Object.keys(changes).some(key => !EDIT_KEYS.has(key))) {\n throw new Error(\"Provide supported spec field changes; identity and engine cannot be changed by an edit.\");\n }\n const next = { ...current.document, ...changes };\n function mergeSettings(before: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {\n const result = { ...before };\n for (const [key, value] of Object.entries(patch)) {\n if (value === null) delete result[key];\n else result[key] = isObject(value) && isObject(result[key]) ? mergeSettings(result[key], value) : value;\n }\n return result;\n }\n for (const key of [\"runtime\", \"evaluation\", \"pipeline\"]) {\n if (changes[key] === null) delete next[key];\n else if (key !== \"pipeline\" && isObject(changes[key])) next[key] = mergeSettings(isObject(current.document[key]) ? current.document[key] : {}, changes[key]);\n }\n for (const key of [\"hyperparameters\", \"foundation\"] as const) {\n if (isObject(changes[key])) {\n const previous = isObject(current.document[key]) ? current.document[key] : {};\n next[key] = { ...previous, ...changes[key] };\n }\n }\n const validation = validateDocument(next, current.path);\n if (!validation.valid) throw new Error(`Invalid behavior spec:\\n${validation.errors.join(\"\\n\")}`);",
247
247
  "files": [
248
248
  "src/spec-workspace.ts",
249
249
  "src/agent-approval.ts",
@@ -264,8 +264,8 @@
264
264
  "preview / run"
265
265
  ],
266
266
  "path": "src/pipeline.ts",
267
- "line": 196,
268
- "code": "export function pipelineForRunInput(input: LocalRunInput, requested?: unknown): NormalizedPipeline {\n if (input.kind === \"request\") {\n // Preserve the direct CLI's full run-request compatibility.\n assertLocalRunInputReady(input.request);\n } else {\n const validation = validateBehaviorSpec(input);\n if (!validation.valid) {\n throw new Error(`Invalid behavior spec:\\n${validation.errors.join(\"\\n\")}`);\n }\n }\n\n const foundation = input.kind === \"foundation-spec\";\n const recipe = requested !== undefined ? requested : (foundation\n ? pipelineFromFoundationHyperparameters(input.spec.name, input.spec.foundation)\n : canonicalPipeline(\"local\"));\n const pipeline = parsePipeline(recipe);\n if (isFoundationPipeline(pipeline) !== foundation) {\n const recipeEngine = isFoundationPipeline(pipeline) ? \"a foundation\" : \"an adapter\";\n const specEngine = foundation ? \"a foundation\" : \"an adapter\";\n throw new Error(`Pipeline is ${recipeEngine} recipe, but the behavior spec is ${specEngine} spec. Use a matching recipe or omit --file.`);\n }\n if (foundation) assertFoundationSettingsMatch(input.spec.foundation, pipeline);\n return pipeline;\n}",
267
+ "line": 197,
268
+ "code": "export function pipelineForRunInput(input: LocalRunInput, requested?: unknown): NormalizedPipeline {\n if (input.kind === \"request\") {\n // Preserve the direct CLI's full run-request compatibility.\n assertLocalRunInputReady(input.request);\n } else {\n const validation = validateBehaviorSpec(input);\n if (!validation.valid) {\n throw new Error(`Invalid behavior spec:\\n${validation.errors.join(\"\\n\")}`);\n }\n }\n\n const foundation = input.kind === \"foundation-spec\";\n const embedded = input.kind === \"request\" ? undefined : input.spec.pipeline;\n if (embedded && requested !== undefined && canonicalJson(parsePipeline(embedded)) !== canonicalJson(parsePipeline(requested))) {\n throw new Error(\"Conflicting pipeline settings: update tunedtensor.json.pipeline before running a different recipe.\");\n }\n const recipe = requested !== undefined ? requested : embedded ?? (foundation\n ? pipelineFromFoundationHyperparameters(input.spec.name, input.spec.foundation)\n : canonicalPipeline(\"local\"));\n const pipeline = parsePipeline(recipe);\n if (isFoundationPipeline(pipeline) !== foundation) {\n const recipeEngine = isFoundationPipeline(pipeline) ? \"a foundation\" : \"an adapter\";\n const specEngine = foundation ? \"a foundation\" : \"an adapter\";\n throw new Error(`Pipeline is ${recipeEngine} recipe, but the behavior spec is ${specEngine} spec. Use a matching recipe or omit --file.`);\n }\n if (foundation) assertFoundationSettingsMatch(input.spec.foundation, pipeline);\n return pipeline;\n}",
269
269
  "files": [
270
270
  "src/pipeline.ts",
271
271
  "src/commands/pipeline.ts",
@@ -314,7 +314,7 @@
314
314
  ],
315
315
  "path": "src/local-runtime/foundation-runner.ts",
316
316
  "line": 271,
317
- "code": "export async function runFoundationPipeline(args: {\n spec: LocalFoundationSpecFile;\n plan: ExecutionPlan;\n specPath: string;\n outputDir?: string;\n resume?: boolean;\n spawnStep?: FoundationStepSpawn;\n gpu?: AwsGpuConfig;\n}): Promise<FoundationPipelineResult> {\n assertFoundationPlanSupported(args.spec, args.plan);\n assertFoundationSettingsMatch(args.spec.foundation, args.plan);\n const outputDir = resolve(\n args.outputDir ?? join(dirname(resolve(args.specPath)), DEFAULT_FOUNDATION_RUNS_DIR, randomUUID()),\n );\n if (args.gpu && args.spec.foundation.checkpoint_backup_dir\n && localPathsOverlap(outputDir, args.spec.foundation.checkpoint_backup_dir)) {\n throw new Error(\"AWS checkpoint_backup_dir must be separate from the foundation run directory; overlapping backups can overwrite returned checkpoints.\");\n }",
317
+ "code": "export async function runFoundationPipeline(args: {\n spec: LocalFoundationSpecFile;\n plan: ExecutionPlan;\n specPath: string;\n outputDir?: string;\n resume?: boolean;\n spawnStep?: FoundationStepSpawn;\n gpu?: AwsGpuConfig;\n}): Promise<FoundationPipelineResult> {\n assertFoundationPlanSupported(args.spec, args.plan);\n assertFoundationSettingsMatch(args.spec.foundation, args.plan);\n const outputDir = resolve(\n args.outputDir ?? (args.spec.runtime?.artifactRoot\n ? join(resolve(dirname(args.specPath), args.spec.runtime.artifactRoot), \"foundation-runs\", randomUUID())\n : join(dirname(resolve(args.specPath)), DEFAULT_FOUNDATION_RUNS_DIR, randomUUID())),\n );\n if (args.gpu && args.spec.foundation.checkpoint_backup_dir\n && localPathsOverlap(outputDir, args.spec.foundation.checkpoint_backup_dir)) {\n throw new Error(\"AWS checkpoint_backup_dir must be separate from the foundation run directory; overlapping backups can overwrite returned checkpoints.\");\n }\n let outputExists = false;\n try {\n const outputInfo = await lstat(outputDir);\n if (!args.resume) {\n throw new Error(`Foundation output directory already exists; pass --resume ${outputDir} to recover it.`);\n }\n if (outputInfo.isSymbolicLink()) {\n throw new Error(`Foundation resume directory must not be a symbolic link: ${outputDir}`);\n }\n if (!outputInfo.isDirectory()) {\n throw new Error(`Foundation resume path must be a directory: ${outputDir}`);\n }\n outputExists = true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n }\n if (!outputExists) {\n await mkdir(outputDir, { recursive: true, mode: 0o700 });\n }\n await makePrivateTree(outputDir);\n const workflowPath = join(outputDir, \"resolved-workflow.json\");\n const workflow = { spec: args.spec, plan: args.plan };\n if (outputExists) {\n const previous = await readFile(workflowPath, \"utf8\").catch((error) => {\n if (error.code === \"ENOENT\") return undefined;\n throw error;\n });\n if (previous && JSON.stringify(JSON.parse(previous)) !== JSON.stringify(workflow)) {\n throw new Error(\"Foundation workflow configuration changed; resume requires the original resolved workflow.\");\n }\n }\n await writePrivateTextAtomic(workflowPath, `${JSON.stringify(workflow, null, 2)}\\n`);",
318
318
  "files": [
319
319
  "src/local-runtime/foundation-runner.ts",
320
320
  "src/pipeline.ts",
@@ -345,7 +345,7 @@
345
345
  "src/local-runtime/process-runner.ts",
346
346
  "docs/local-runtime/aws-gpu.md"
347
347
  ],
348
- "example": "tt doctor tunedtensor.json --config local-runner.json\ntt pipeline run --spec tunedtensor.json --config local-runner.json --dry-run\n# After checking the preview and GPU readiness:\ntt pipeline run --spec tunedtensor.json --config local-runner.json"
348
+ "example": "tt doctor tunedtensor.json\ntt pipeline run --spec tunedtensor.json --dry-run\n# After checking the preview and GPU readiness:\ntt pipeline run --spec tunedtensor.json"
349
349
  },
350
350
  "lora": {
351
351
  "title": "LoRA training implementation",
@@ -447,7 +447,7 @@
447
447
  "src/paths.ts",
448
448
  "src/agent-store.ts"
449
449
  ],
450
- "example": "~/.tuned-tensor/config.json\n~/.tuned-tensor/agent/\n~/.tuned-tensor/cache/\n<project>/.tuned-tensor/artifacts/\n<project>/.tuned-tensor/store/ # when configured\n<spec-directory>/.tuned-tensor/foundation-runs/\n# Store fallback: ~/.tuned-tensor/store/"
450
+ "example": "~/.tuned-tensor/config.json\n~/.tuned-tensor/agent/\n~/.tuned-tensor/cache/\n<project>/.tuned-tensor/artifacts/\n<project>/.tuned-tensor/store/ # default for spec projects\n<spec-directory>/.tuned-tensor/foundation-runs/\n# Store fallback: ~/.tuned-tensor/store/"
451
451
  },
452
452
  "serve": {
453
453
  "title": "Serving & API clients",
@@ -472,7 +472,7 @@
472
472
  "training/serving/pyproject.toml",
473
473
  "docs/local-runtime/serving.md"
474
474
  ],
475
- "example": "tt serve local-<run-id> --config local-runner.json\n# Or a foundation checkpoint:\ntt serve foundation --checkpoint <model-dir> --tokenizer <tokenizer.json>"
475
+ "example": "tt serve local-<run-id>\n# Or a foundation checkpoint:\ntt serve foundation --checkpoint <model-dir> --tokenizer <tokenizer.json>"
476
476
  },
477
477
  "publish": {
478
478
  "title": "Publish & account operations",
@@ -47,7 +47,8 @@ for an invalid spec.
47
47
  `get_local_spec` returns the document, validation diagnostics and content hash.
48
48
  `prepare_update_local_spec` requires that hash and a patch of supported fields.
49
49
  Arrays replace their whole field; `hyperparameters` and `foundation` merge by
50
- key. Unmentioned fields remain unchanged. Identity and engine cannot change
50
+ key. `runtime` and `evaluation` merge recursively; null removes optional settings.
51
+ `pipeline` replaces the entire recipe (null restores the derived default). Unmentioned fields remain unchanged. Identity and engine cannot change
51
52
  through this edit tool. The full resulting document must pass the runtime's
52
53
  strict schema, readiness checks, nonblank content checks and distinct-example
53
54
  checks. Small example sets produce a warning; validation is not a claim of model
@@ -82,7 +83,7 @@ tt pipeline run --spec tunedtensor.json --dry-run
82
83
  tt pipeline run --spec tunedtensor.json
83
84
  ```
84
85
 
85
- The default pipeline derives from the current spec. An adjacent
86
+ The pipeline uses the optional `pipeline` section or derives from the current spec. An adjacent
86
87
  `tunedtensor.pipeline.json` is no longer loaded implicitly. To select an advanced
87
88
  recipe explicitly, pass `--file`; a missing explicit recipe or `--spec` file fails. Foundation
88
89
  training parameters in an explicit recipe must agree with the spec, including
@@ -100,10 +101,30 @@ The command reads one spec snapshot for planning, hardware warnings and executio
100
101
  JSON and final result JSON identify its path and SHA-256. The spec's compiled
101
102
  instructions and examples feed training and task evaluation; the independently
102
103
  configured general-regression suite keeps its own prompt. Provider choice and
103
- GPU placement remain separate runtime choices in agent configuration and
104
- `local-runner.json`. Evaluation policy (inference/scoring settings and the
105
- general-regression suite) also remains in the runner config. Moving that policy
106
- into the spec would be a separate schema change.
104
+ GPU placement is configured in `runtime.gpu` in the spec. Model-provider choice
105
+ remains in agent configuration. Evaluation policy belongs in `evaluation`;
106
+ artifact/store locations and model paths belong in `runtime`. All relative paths
107
+ resolve beside the spec. New projects default to `.tuned-tensor/artifacts` and
108
+ `.tuned-tensor/store` beside it (unless `TT_LOCAL_HOME` overrides the store).
109
+ Foundation supports `runtime.artifactRoot` and `runtime.gpu`; its evaluation
110
+ and training settings remain under `foundation`.
111
+
112
+ Only `tunedtensor.json` is required. `tt pipeline init --spec tunedtensor.json`
113
+ embeds an advanced recipe when needed; omit this step to keep automatic derivation.
114
+ An explicit `--file` cannot override a conflicting embedded recipe.
115
+
116
+ For existing projects, run `tt pipeline migrate --spec tunedtensor.json` to
117
+ consolidate adjacent pipeline and runner files. It validates the result first,
118
+ keeps exclusive `.bak` copies, and preserves resolved paths and the old store
119
+ location. Conflicting sources, enabled legacy `dryRun`, or unsupported foundation
120
+ runner settings require reconciliation first. Existing `.bak` files are never
121
+ overwritten. Avoid concurrent edits during migration. Legacy runner files remain
122
+ readable with a warning, but cannot coexist with inline runtime/evaluation settings.
123
+ A legacy adjacent pipeline is used only with explicit `--file` or after migration.
124
+
125
+ Runs write generated `resolved-workflow.json` snapshots for diagnosis and
126
+ reproducibility; these are outputs, not additional user-authored configuration.
127
+
107
128
 
108
129
  Agent `/approve` for a pipeline still runs a preview only. Real training uses
109
130
  the direct command above. The spec is the behavior recipe; approval of a spec
@@ -0,0 +1,49 @@
1
+ {
2
+ "id": "22222222-2222-4222-8222-222222222222",
3
+ "name": "Local Smoke Assistant",
4
+ "description": "Tiny local runner smoke test.",
5
+ "system_prompt": "Return exactly one sentiment label.",
6
+ "guidelines": [
7
+ "Reply with positive or negative."
8
+ ],
9
+ "constraints": [
10
+ "Do not include extra commentary."
11
+ ],
12
+ "base_model": "Qwen/Qwen3.5-2B",
13
+ "examples": [
14
+ {
15
+ "input": "Classify sentiment: I love this product.",
16
+ "output": "positive"
17
+ },
18
+ {
19
+ "input": "Classify sentiment: This was disappointing.",
20
+ "output": "negative"
21
+ },
22
+ {
23
+ "input": "Classify sentiment: The update works perfectly.",
24
+ "output": "positive"
25
+ },
26
+ {
27
+ "input": "Classify sentiment: Support never replied.",
28
+ "output": "negative"
29
+ }
30
+ ],
31
+ "hyperparameters": {
32
+ "n_epochs": 1,
33
+ "learning_rate": 0.0002,
34
+ "batch_size": 1,
35
+ "gradient_accumulation_steps": 1,
36
+ "lora_rank": 4,
37
+ "lora_alpha": 8,
38
+ "max_seq_length": 256,
39
+ "max_eval_examples": 2
40
+ },
41
+ "evaluation": {
42
+ "inference": {
43
+ "maxNewTokens": 32
44
+ },
45
+ "scoring": {
46
+ "mode": "exact_match"
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "engine": "foundation",
3
+ "name": "Foundation smoke chat",
4
+ "description": "Tiny from-scratch tokenizer, pretrain, chat SFT, and eval.",
5
+ "system_prompt": "You are a helpful assistant.",
6
+ "guidelines": [
7
+ "Answer the user request directly."
8
+ ],
9
+ "constraints": [],
10
+ "examples": [
11
+ {
12
+ "input": "Say hello in one sentence.",
13
+ "output": "Hello \u2014 how can I help you today?"
14
+ },
15
+ {
16
+ "input": "What is 2 + 2?",
17
+ "output": "2 + 2 = 4."
18
+ }
19
+ ],
20
+ "foundation": {
21
+ "vocab_size": 256,
22
+ "max_chars": 20000,
23
+ "depth": 2,
24
+ "pretrain_steps": 2,
25
+ "finetune_steps": 2,
26
+ "rl_steps": 0,
27
+ "batch_size": 2,
28
+ "sequence_length": 64,
29
+ "nproc_per_node": 1
30
+ }
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuned-tensor/cli",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Local TT agent and CUDA fine-tuning CLI for Tuned Tensor workflows",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,7 +37,8 @@
37
37
  "docs/spec-workflow.md",
38
38
  "docs/spec-architecture.html",
39
39
  "examples/local-runtime/*.json",
40
- "examples/local-runtime/*.jsonl"
40
+ "examples/local-runtime/*.jsonl",
41
+ "examples/single-spec/**/*.json"
41
42
  ],
42
43
  "engines": {
43
44
  "node": ">=22.19.0"
@@ -68,6 +69,6 @@
68
69
  "tsup": "^8.5.1",
69
70
  "tsx": "^4.23.12",
70
71
  "typescript": "^5.9.3",
71
- "vitest": "^4.1.8"
72
+ "vitest": "^4.1.11"
72
73
  }
73
74
  }
@@ -93,7 +93,13 @@ def build_vllm_args(model_source: str, adapter_path: str | None, temp_dir: Path)
93
93
  # tool calls/results verbatim while merging leading system context.
94
94
  prefix = "{%- set tt = namespace(system=" + json.dumps(prompt) + ", history=[]) -%}"
95
95
  prefix += "{%- for message in messages -%}{%- if message.role == 'system' -%}"
96
+ # vLLM may normalize text into OpenAI content parts for the model's
97
+ # native template. Merge those parts without stringifying the list.
98
+ prefix += "{%- if message.content is string -%}"
96
99
  prefix += "{%- set tt.system = tt.system + '\\n\\n' + message.content -%}"
100
+ prefix += "{%- else -%}{%- for part in message.content -%}{%- if part.type == 'text' -%}"
101
+ prefix += "{%- set tt.system = tt.system + '\\n\\n' + part.text -%}"
102
+ prefix += "{%- endif -%}{%- endfor -%}{%- endif -%}"
97
103
  prefix += "{%- else -%}{%- set tt.history = tt.history + [message] -%}{%- endif -%}{%- endfor -%}"
98
104
  prefix += "{%- set messages = [{'role': 'system', 'content': tt.system}] + tt.history -%}"
99
105
  path = temp_dir / "chat-template.jinja"