@seanhogg/builderforce-memory 2026.6.39 → 2026.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cognition/EvermindCognition.d.ts.map +1 -1
- package/dist/cognition/EvermindCognition.js +17 -7
- package/dist/cognition/EvermindCognition.js.map +1 -1
- package/dist/diagnostics/stack-diagnostic.d.ts +9 -0
- package/dist/diagnostics/stack-diagnostic.d.ts.map +1 -1
- package/dist/diagnostics/stack-diagnostic.js +28 -1
- package/dist/diagnostics/stack-diagnostic.js.map +1 -1
- package/dist/distillation/DistillationEngine.d.ts +6 -0
- package/dist/distillation/DistillationEngine.d.ts.map +1 -1
- package/dist/distillation/DistillationEngine.js +10 -4
- package/dist/distillation/DistillationEngine.js.map +1 -1
- package/dist/memory/MemoryStore.d.ts +10 -0
- package/dist/memory/MemoryStore.d.ts.map +1 -1
- package/dist/memory/MemoryStore.js +13 -2
- package/dist/memory/MemoryStore.js.map +1 -1
- package/dist/session/session.d.ts +22 -0
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +45 -2
- package/dist/session/session.js.map +1 -1
- package/dist/workflow/steps.d.ts.map +1 -1
- package/dist/workflow/steps.js +206 -1
- package/dist/workflow/steps.js.map +1 -1
- package/dist/workflow/templates.d.ts.map +1 -1
- package/dist/workflow/templates.js +8 -0
- package/dist/workflow/templates.js.map +1 -1
- package/package.json +4 -3
- package/src/cognition/EvermindCognition.ts +24 -7
- package/src/diagnostics/stack-diagnostic.ts +38 -1
- package/src/distillation/DistillationEngine.ts +17 -4
- package/src/memory/MemoryStore.ts +14 -2
- package/src/session/session.ts +72 -2
- package/src/workflow/steps.ts +242 -0
- package/src/workflow/templates.ts +8 -0
|
@@ -66,9 +66,46 @@ export interface StackDiagnosticResult {
|
|
|
66
66
|
firstFailure?: StackStepResult;
|
|
67
67
|
/** Outputs produced by the run (e.g. `artifacts.evermind` for a build workflow). */
|
|
68
68
|
artifacts: Record<string, unknown>;
|
|
69
|
+
/**
|
|
70
|
+
* Curated STRUCTURED metrics the run computed (loss curve, perplexity/top-1,
|
|
71
|
+
* dataset quality, pass@1, convergence, codec MSE) — the numbers steps previously
|
|
72
|
+
* left buried in `ctx.bag` and only echoed into a human `detail` string, forcing a
|
|
73
|
+
* UI to regex-parse them. A UI can now chart/gauge these directly. Only the
|
|
74
|
+
* allow-listed metric keys are included; heavy scratch (model blob, tokenizer,
|
|
75
|
+
* corpus, candidate list) is deliberately excluded.
|
|
76
|
+
*/
|
|
77
|
+
metrics: Record<string, unknown>;
|
|
69
78
|
totalMs: number;
|
|
70
79
|
}
|
|
71
80
|
|
|
81
|
+
/**
|
|
82
|
+
* `ctx.bag` keys that are genuine metrics/reports worth surfacing on the result.
|
|
83
|
+
* Everything else in the bag is scratch (large objects / model instances) and is
|
|
84
|
+
* kept out so the metrics payload stays small and serializable.
|
|
85
|
+
*/
|
|
86
|
+
const METRIC_BAG_KEYS = [
|
|
87
|
+
'trainingHistory', // per-epoch loss curve (train-model / video-train)
|
|
88
|
+
'datasetMetrics', // { words, sequences, avgSeqLen, duplicateRatio }
|
|
89
|
+
'benchmark', // BenchmarkReport: perplexity, bitsPerToken, top1/topK, tok/s
|
|
90
|
+
'converged', // convergence verdict + loss-drop numbers
|
|
91
|
+
'codeBenchmark', // { pass1, passedTasks, totalTasks, perTask[] }
|
|
92
|
+
'codeParse', // code-parse check result
|
|
93
|
+
'codeEval', // code-eval check result
|
|
94
|
+
'videoReconMSE', // video-roundtrip reconstruction error
|
|
95
|
+
'videoCodecMSE', // video-train codec reconstruction error
|
|
96
|
+
'videoTrainingHistory', // per-epoch loss curve for the video model
|
|
97
|
+
'sample', // generate-check sample output
|
|
98
|
+
] as const;
|
|
99
|
+
|
|
100
|
+
/** Pick the present metric keys out of the bag (skips absent + undefined values). */
|
|
101
|
+
function collectMetrics(bag: Record<string, unknown>): Record<string, unknown> {
|
|
102
|
+
const out: Record<string, unknown> = {};
|
|
103
|
+
for (const key of METRIC_BAG_KEYS) {
|
|
104
|
+
if (bag[key] !== undefined) out[key] = bag[key];
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
72
109
|
/**
|
|
73
110
|
* Run the steps in order, capturing each result. Never throws — a step that
|
|
74
111
|
* throws becomes a `fail` row, and the run continues so the caller sees the full
|
|
@@ -99,5 +136,5 @@ export async function runStackDiagnostic(
|
|
|
99
136
|
opts.onStep?.(result);
|
|
100
137
|
}
|
|
101
138
|
|
|
102
|
-
return { ok: !firstFailure, steps: results, ...(firstFailure ? { firstFailure } : {}), artifacts: ctx.artifacts, totalMs: now() - start };
|
|
139
|
+
return { ok: !firstFailure, steps: results, ...(firstFailure ? { firstFailure } : {}), artifacts: ctx.artifacts, metrics: collectMetrics(ctx.bag), totalMs: now() - start };
|
|
103
140
|
}
|
|
@@ -82,6 +82,10 @@ export interface DistillResult {
|
|
|
82
82
|
skipReason? : string;
|
|
83
83
|
/** Number of past exemplars rehearsed alongside this one (EVM-5). */
|
|
84
84
|
rehearsed? : number;
|
|
85
|
+
/** Student's pre-adapt perplexity on the teacher output, when the quality gate
|
|
86
|
+
* measured it — a "how novel was this exemplar" signal. Kept even when the
|
|
87
|
+
* exemplar was NOT skipped (previously discarded on the trained path). */
|
|
88
|
+
gatePerplexity?: number;
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
export interface DistillBatchResult {
|
|
@@ -100,6 +104,8 @@ export interface DistillationLog {
|
|
|
100
104
|
skipReason? : string;
|
|
101
105
|
finalLoss? : number;
|
|
102
106
|
epochs : number;
|
|
107
|
+
/** Pre-adapt perplexity on the teacher output (when the quality gate measured it). */
|
|
108
|
+
gatePerplexity? : number;
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/** Maximum number of distillation log entries to retain in memory. */
|
|
@@ -194,6 +200,11 @@ export class DistillationEngine {
|
|
|
194
200
|
|
|
195
201
|
// ── Quality gate ──────────────────────────────────────────────────────
|
|
196
202
|
|
|
203
|
+
// Pre-adapt novelty of this exemplar (student perplexity on the teacher output).
|
|
204
|
+
// Measured by the gate below; hoisted so it survives to the TRAINED path instead
|
|
205
|
+
// of being dropped for exactly the exemplars we go on to learn from.
|
|
206
|
+
let gatePerplexity: number | undefined;
|
|
207
|
+
|
|
197
208
|
if (opts.qualityGate) {
|
|
198
209
|
const gate = opts.qualityGate;
|
|
199
210
|
|
|
@@ -216,19 +227,19 @@ export class DistillationEngine {
|
|
|
216
227
|
}
|
|
217
228
|
|
|
218
229
|
if (gate.maxPerplexity != null) {
|
|
219
|
-
let perplexity: number | undefined;
|
|
220
230
|
try {
|
|
221
|
-
|
|
231
|
+
gatePerplexity = await this._runtime.evaluate(teacherOutput);
|
|
222
232
|
} catch {
|
|
223
233
|
// Evaluation failure is non-fatal — proceed with adaptation
|
|
224
234
|
}
|
|
225
|
-
if (
|
|
235
|
+
if (gatePerplexity != null && gatePerplexity < gate.maxPerplexity) {
|
|
226
236
|
const result: DistillResult = {
|
|
227
237
|
input,
|
|
228
238
|
teacherOutput,
|
|
229
239
|
adaptResult : { losses: [], epochCount: 0, durationMs: 0 },
|
|
230
240
|
skipped : true,
|
|
231
241
|
skipReason : 'already_learned',
|
|
242
|
+
gatePerplexity,
|
|
232
243
|
};
|
|
233
244
|
this._appendLog({
|
|
234
245
|
input,
|
|
@@ -236,6 +247,7 @@ export class DistillationEngine {
|
|
|
236
247
|
skipped : true,
|
|
237
248
|
skipReason : 'already_learned',
|
|
238
249
|
epochs : 0,
|
|
250
|
+
gatePerplexity,
|
|
239
251
|
});
|
|
240
252
|
return result;
|
|
241
253
|
}
|
|
@@ -270,9 +282,10 @@ export class DistillationEngine {
|
|
|
270
282
|
skipped : false,
|
|
271
283
|
finalLoss : adaptResult.losses.at(-1),
|
|
272
284
|
epochs : adaptResult.epochCount,
|
|
285
|
+
gatePerplexity,
|
|
273
286
|
});
|
|
274
287
|
|
|
275
|
-
return { input, teacherOutput, adaptResult, skipped: false, rehearsed: rehearsed.length };
|
|
288
|
+
return { input, teacherOutput, adaptResult, skipped: false, rehearsed: rehearsed.length, gatePerplexity };
|
|
276
289
|
}
|
|
277
290
|
|
|
278
291
|
/**
|
|
@@ -328,6 +328,16 @@ export class MemoryStore {
|
|
|
328
328
|
* reason, it transparently falls back to Jaccard word-overlap similarity.
|
|
329
329
|
*/
|
|
330
330
|
async recallSimilar(query: string, topK: number, runtime?: SSMRuntimeRef): Promise<MemoryEntry[]> {
|
|
331
|
+
return (await this.recallSimilarScored(query, topK, runtime)).map(s => s.entry);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Like {@link recallSimilar} but returns each hit's real 0..1 relevance score
|
|
336
|
+
* (SSM-embedding cosine, or Jaccard on the fallback path) alongside the entry —
|
|
337
|
+
* so callers can rank/threshold by TRUE similarity instead of list position.
|
|
338
|
+
* {@link recallSimilar} is the score-dropping convenience wrapper over this.
|
|
339
|
+
*/
|
|
340
|
+
async recallSimilarScored(query: string, topK: number, runtime?: SSMRuntimeRef): Promise<Array<{ entry: MemoryEntry; score: number }>> {
|
|
331
341
|
const all = await this.recallAll();
|
|
332
342
|
if (all.length === 0) return [];
|
|
333
343
|
|
|
@@ -344,7 +354,9 @@ export class MemoryStore {
|
|
|
344
354
|
? synced.index.search(queryVec, topK)
|
|
345
355
|
// Below it, an exact scan over the (already-embedded) vectors.
|
|
346
356
|
: denseSearch(queryVec, synced.items, topK, this._annThreshold);
|
|
347
|
-
return hits
|
|
357
|
+
return hits
|
|
358
|
+
.map(h => { const entry = synced.byKey.get(h.id); return entry ? { entry, score: h.score } : null; })
|
|
359
|
+
.filter((e): e is { entry: MemoryEntry; score: number } => !!e);
|
|
348
360
|
}
|
|
349
361
|
}
|
|
350
362
|
// Any failure falls through to the Jaccard path below.
|
|
@@ -359,7 +371,7 @@ export class MemoryStore {
|
|
|
359
371
|
});
|
|
360
372
|
|
|
361
373
|
scored.sort((a, b) => b.score - a.score);
|
|
362
|
-
return scored.slice(0, topK)
|
|
374
|
+
return scored.slice(0, topK);
|
|
363
375
|
}
|
|
364
376
|
|
|
365
377
|
/**
|
package/src/session/session.ts
CHANGED
|
@@ -160,12 +160,34 @@ export interface AdaptOptions {
|
|
|
160
160
|
wsla? : boolean; // Default: true (WSLA fast-adapt mode)
|
|
161
161
|
fullTrain? : boolean; // Convenience alias: sets wsla=false and epochs=5
|
|
162
162
|
onProgress? : (epoch: number, loss: number) => void;
|
|
163
|
+
/**
|
|
164
|
+
* Snapshot the weights before adapting and RESTORE them if the adapt produced
|
|
165
|
+
* a non-finite loss or clearly regressed the model's perplexity on the same
|
|
166
|
+
* text. Default true — this is the last line of defence behind WSLA + the
|
|
167
|
+
* per-step trust region that keeps write-through learning from "dying after
|
|
168
|
+
* several executions". Set false to skip the snapshot cost (e.g. bulk full
|
|
169
|
+
* training where you validate externally).
|
|
170
|
+
*/
|
|
171
|
+
rollbackOnRegression? : boolean; // Default: true
|
|
172
|
+
/** Regression tolerance: roll back if post-perplexity > pre × this. Default 1.25. */
|
|
173
|
+
regressionTolerance? : number;
|
|
163
174
|
}
|
|
164
175
|
|
|
165
176
|
export interface AdaptResult {
|
|
166
177
|
losses : number[];
|
|
167
178
|
epochCount : number;
|
|
168
179
|
durationMs : number;
|
|
180
|
+
/** True when the adapt was reverted to the pre-adapt snapshot (see below). */
|
|
181
|
+
rolledBack? : boolean;
|
|
182
|
+
/** Why it rolled back, when it did (`non-finite loss` | `perplexity regression`). */
|
|
183
|
+
rollbackReason?: string;
|
|
184
|
+
/** Perplexity on the adapt text before / after (when measured). */
|
|
185
|
+
perplexityBefore?: number;
|
|
186
|
+
perplexityAfter? : number;
|
|
187
|
+
/** Mean pre-clip gradient L2 norm across the adapt's epochs — the standard
|
|
188
|
+
* early-warning for an unstable update (it spikes before a loss goes non-finite).
|
|
189
|
+
* Undefined when the trainer didn't measure it. */
|
|
190
|
+
gradNorm?: number;
|
|
169
191
|
}
|
|
170
192
|
|
|
171
193
|
export type StorageTarget = 'indexedDB' | 'download' | 'fileSystem';
|
|
@@ -531,20 +553,68 @@ export class MambaSession {
|
|
|
531
553
|
);
|
|
532
554
|
}
|
|
533
555
|
|
|
556
|
+
const rollbackOnRegression = options.rollbackOnRegression ?? true;
|
|
557
|
+
const regressionTolerance = options.regressionTolerance ?? 1.25;
|
|
558
|
+
|
|
559
|
+
// Snapshot the weights and measure the pre-adapt perplexity so a bad adapt
|
|
560
|
+
// can be reverted. This is what makes repeated write-through learning safe:
|
|
561
|
+
// an update that produces NaN/Inf or clearly worsens the model is undone
|
|
562
|
+
// instead of persisting and compounding across executions.
|
|
563
|
+
let snapshot: ArrayBuffer | null = null;
|
|
564
|
+
let perplexityBefore: number | undefined;
|
|
565
|
+
if (rollbackOnRegression) {
|
|
566
|
+
snapshot = await this._model.exportWeights();
|
|
567
|
+
perplexityBefore = await this._trainer.evaluate(text).catch(() => undefined);
|
|
568
|
+
}
|
|
569
|
+
|
|
534
570
|
const startTime = Date.now();
|
|
571
|
+
// Capture the per-epoch grad norm (instability signal) without disturbing the
|
|
572
|
+
// caller's own progress callback.
|
|
573
|
+
const gradNorms: number[] = [];
|
|
535
574
|
const losses = await this._trainer.train(text, {
|
|
536
575
|
epochs,
|
|
537
576
|
learningRate,
|
|
538
577
|
seqLen,
|
|
539
578
|
wsla,
|
|
540
|
-
|
|
579
|
+
trackGradNorm: true,
|
|
580
|
+
onEpochEnd: (epoch, loss, gradNorm) => {
|
|
581
|
+
if (typeof gradNorm === 'number' && Number.isFinite(gradNorm)) gradNorms.push(gradNorm);
|
|
582
|
+
onProgress?.(epoch, loss);
|
|
583
|
+
},
|
|
541
584
|
});
|
|
585
|
+
const gradNorm = gradNorms.length
|
|
586
|
+
? gradNorms.reduce((a, b) => a + b, 0) / gradNorms.length
|
|
587
|
+
: undefined;
|
|
542
588
|
|
|
543
|
-
|
|
589
|
+
const base: AdaptResult = {
|
|
544
590
|
losses,
|
|
545
591
|
epochCount : losses.length,
|
|
546
592
|
durationMs : Date.now() - startTime,
|
|
593
|
+
gradNorm,
|
|
547
594
|
};
|
|
595
|
+
|
|
596
|
+
if (!rollbackOnRegression || !snapshot) return base;
|
|
597
|
+
|
|
598
|
+
// Validate the adapt. Roll back on a non-finite loss (the update poisoned
|
|
599
|
+
// the weights) or a perplexity regression beyond tolerance.
|
|
600
|
+
const nonFiniteLoss = losses.some((l) => !Number.isFinite(l));
|
|
601
|
+
const perplexityAfter = await this._trainer.evaluate(text).catch(() => Number.POSITIVE_INFINITY);
|
|
602
|
+
const regressed =
|
|
603
|
+
Number.isFinite(perplexityBefore ?? NaN) && Number.isFinite(perplexityAfter) &&
|
|
604
|
+
perplexityAfter > (perplexityBefore as number) * regressionTolerance;
|
|
605
|
+
|
|
606
|
+
if (nonFiniteLoss || !Number.isFinite(perplexityAfter) || regressed) {
|
|
607
|
+
await this._model.loadWeights(snapshot);
|
|
608
|
+
return {
|
|
609
|
+
...base,
|
|
610
|
+
rolledBack : true,
|
|
611
|
+
rollbackReason: nonFiniteLoss || !Number.isFinite(perplexityAfter) ? 'non-finite loss' : 'perplexity regression',
|
|
612
|
+
perplexityBefore,
|
|
613
|
+
perplexityAfter,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return { ...base, rolledBack: false, perplexityBefore, perplexityAfter };
|
|
548
618
|
}
|
|
549
619
|
|
|
550
620
|
// ── Evaluation ─────────────────────────────────────────────────────────────
|
package/src/workflow/steps.ts
CHANGED
|
@@ -18,9 +18,13 @@ import {
|
|
|
18
18
|
exportEvermind,
|
|
19
19
|
importEvermind,
|
|
20
20
|
benchmarkText,
|
|
21
|
+
VideoRVQCodec,
|
|
22
|
+
buildVideoSequence,
|
|
23
|
+
generateVideo,
|
|
21
24
|
type ExportFormat,
|
|
22
25
|
type BenchmarkReport,
|
|
23
26
|
type ImportOptions,
|
|
27
|
+
type Video,
|
|
24
28
|
} from "@seanhogg/builderforce-memory-engine";
|
|
25
29
|
import { EvermindCognition } from "../cognition/index.js";
|
|
26
30
|
import { hybridRetrieve, chunkText } from "../retrieval/index.js";
|
|
@@ -236,6 +240,63 @@ function corpusToSequences(corpus: string, tok: BPETokenizer): number[][] {
|
|
|
236
240
|
.filter((ids) => ids.length >= 2);
|
|
237
241
|
}
|
|
238
242
|
|
|
243
|
+
/** Build a VideoRVQCodec from step params (shared by the video build steps). */
|
|
244
|
+
function makeVideoCodec(cfg: WorkflowStepConfig, textVocabSize: number): VideoRVQCodec {
|
|
245
|
+
return new VideoRVQCodec({
|
|
246
|
+
height: pNum(cfg, "height", 8),
|
|
247
|
+
width: pNum(cfg, "width", 8),
|
|
248
|
+
channels: pNum(cfg, "channels", 3),
|
|
249
|
+
patch: pNum(cfg, "patch", 4),
|
|
250
|
+
levels: pNum(cfg, "levels", 2),
|
|
251
|
+
codebookSize: pNum(cfg, "codebookSize", 16),
|
|
252
|
+
keyframeInterval: pNum(cfg, "keyframeInterval", 12),
|
|
253
|
+
textVocabSize,
|
|
254
|
+
seed: pNum(cfg, "seed", 7),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Deterministic synthetic clips so the video steps are self-runnable (the way the
|
|
260
|
+
* text steps default their corpus). A smooth spatial pattern that drifts a little
|
|
261
|
+
* per frame — enough temporal redundancy to exercise the inter-frame path.
|
|
262
|
+
*/
|
|
263
|
+
function synthVideos(count: number, frames: number, h: number, w: number, c: number): Video[] {
|
|
264
|
+
const out: Video[] = [];
|
|
265
|
+
for (let v = 0; v < count; v++) {
|
|
266
|
+
const clip: Video = [];
|
|
267
|
+
for (let t = 0; t < frames; t++) {
|
|
268
|
+
const f = new Float32Array(h * w * c);
|
|
269
|
+
for (let y = 0; y < h; y++) {
|
|
270
|
+
for (let x = 0; x < w; x++) {
|
|
271
|
+
for (let ch = 0; ch < c; ch++) {
|
|
272
|
+
f[((y * w) + x) * c + ch] = 0.5 + 0.4 * Math.sin((x + v * 1.3 + t * 0.3) * 0.7 + y * 0.5 + ch);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
clip.push(f);
|
|
277
|
+
}
|
|
278
|
+
out.push(clip);
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Mean per-pixel reconstruction MSE of a codec over clips (the video roundtrip metric). */
|
|
284
|
+
function videoReconMSE(codec: VideoRVQCodec, videos: Video[]): number {
|
|
285
|
+
let se = 0;
|
|
286
|
+
let n = 0;
|
|
287
|
+
for (const v of videos) {
|
|
288
|
+
const r = codec.decode(codec.encode(v));
|
|
289
|
+
for (let t = 0; t < v.length; t++) {
|
|
290
|
+
for (let k = 0; k < v[t]!.length; k++) {
|
|
291
|
+
const d = v[t]![k]! - r[t]![k]!;
|
|
292
|
+
se += d * d;
|
|
293
|
+
}
|
|
294
|
+
n += v[t]!.length;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return n > 0 ? se / n : 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
239
300
|
interface Registered {
|
|
240
301
|
info: StepTypeInfo;
|
|
241
302
|
factory: StepFactory;
|
|
@@ -552,6 +613,127 @@ export const BUILTIN_STEPS: Record<string, Registered> = {
|
|
|
552
613
|
return `artifact ${blob.byteLength} bytes; trained vs served output matches`;
|
|
553
614
|
}),
|
|
554
615
|
},
|
|
616
|
+
"video-train": {
|
|
617
|
+
info: {
|
|
618
|
+
type: "video-train",
|
|
619
|
+
layer: "BUILD",
|
|
620
|
+
label: "Train video model",
|
|
621
|
+
description:
|
|
622
|
+
"Fit a temporal residual-VQ codec on clips, then train an EvermindLM on the video token streams — the model that generates video (params: height, width, channels, patch, levels, codebookSize, clips, frames, epochs, lr)",
|
|
623
|
+
},
|
|
624
|
+
factory: (cfg) =>
|
|
625
|
+
mkStep(cfg, "BUILD", "Train video model (codec + EvermindLM)", async (ctx) => {
|
|
626
|
+
const videos =
|
|
627
|
+
(ctx.bag.videos as Video[] | undefined) ??
|
|
628
|
+
synthVideos(
|
|
629
|
+
pNum(cfg, "clips", 3),
|
|
630
|
+
pNum(cfg, "frames", 4),
|
|
631
|
+
pNum(cfg, "height", 8),
|
|
632
|
+
pNum(cfg, "width", 8),
|
|
633
|
+
pNum(cfg, "channels", 3),
|
|
634
|
+
);
|
|
635
|
+
if (videos.length === 0) throw new Error("no clips to train on — provide ctx.bag.videos or a positive 'clips' param");
|
|
636
|
+
|
|
637
|
+
// Learn the codec's codebooks, then autoregress the (unchanged) EvermindLM
|
|
638
|
+
// over the resulting token streams. Text region is 0 for a pure-video model.
|
|
639
|
+
const codec = makeVideoCodec(cfg, 0);
|
|
640
|
+
const codecMSE = codec.fit(videos, { iterations: pNum(cfg, "codecIterations", 10) });
|
|
641
|
+
const sequences = videos.map((v) => buildVideoSequence(codec, [], v));
|
|
642
|
+
const model = new EvermindLM({
|
|
643
|
+
vocabSize: codec.vocabSize,
|
|
644
|
+
dModel: pNum(cfg, "dModel", 32),
|
|
645
|
+
numLayers: pNum(cfg, "numLayers", 2),
|
|
646
|
+
hiddenDim: pNum(cfg, "hiddenDim", 48),
|
|
647
|
+
seed: pNum(cfg, "seed", 11),
|
|
648
|
+
});
|
|
649
|
+
const epochs = pNum(cfg, "epochs", 60);
|
|
650
|
+
const history = new EvermindLMTrainer(model, { lr: pNum(cfg, "lr", 0.03), epochs }).fit(sequences);
|
|
651
|
+
|
|
652
|
+
ctx.bag.videoCodec = codec;
|
|
653
|
+
ctx.bag.videoModel = model;
|
|
654
|
+
ctx.bag.videoTrainingHistory = history;
|
|
655
|
+
// Surface the codec's reconstruction MSE as a metric (symmetric with the
|
|
656
|
+
// video-roundtrip step's ctx.bag.videoReconMSE) so it isn't stranded in the
|
|
657
|
+
// human detail string — runStackDiagnostic().metrics can chart it.
|
|
658
|
+
ctx.bag.videoCodecMSE = codecMSE;
|
|
659
|
+
const last = history.at(-1) ?? 0;
|
|
660
|
+
return `codec MSE ${codecMSE.toFixed(4)}; trained ${epochs} epochs over ${sequences.length} clip(s), loss ${last.toFixed(3)}`;
|
|
661
|
+
}),
|
|
662
|
+
},
|
|
663
|
+
"video-roundtrip": {
|
|
664
|
+
info: {
|
|
665
|
+
type: "video-roundtrip",
|
|
666
|
+
layer: "BUILD",
|
|
667
|
+
label: "Video round-trip",
|
|
668
|
+
description:
|
|
669
|
+
"Fit the video codec and gate reconstruction quality (encode→decode MSE), then smoke-test generate→decode yields valid frames (params: maxMSE, height, width, levels, codebookSize)",
|
|
670
|
+
},
|
|
671
|
+
factory: (cfg) =>
|
|
672
|
+
mkStep(cfg, "BUILD", "Video codec round-trip (encode→decode)", async (ctx) => {
|
|
673
|
+
const videos =
|
|
674
|
+
(ctx.bag.videos as Video[] | undefined) ??
|
|
675
|
+
synthVideos(
|
|
676
|
+
pNum(cfg, "clips", 3),
|
|
677
|
+
pNum(cfg, "frames", 4),
|
|
678
|
+
pNum(cfg, "height", 8),
|
|
679
|
+
pNum(cfg, "width", 8),
|
|
680
|
+
pNum(cfg, "channels", 3),
|
|
681
|
+
);
|
|
682
|
+
// Reuse a codec trained earlier if present; otherwise fit a fresh one.
|
|
683
|
+
let codec = ctx.bag.videoCodec as VideoRVQCodec | undefined;
|
|
684
|
+
const before = codec ? videoReconMSE(codec, videos) : Infinity;
|
|
685
|
+
if (!codec) {
|
|
686
|
+
codec = makeVideoCodec(cfg, 0);
|
|
687
|
+
codec.fit(videos, { iterations: pNum(cfg, "codecIterations", 10) });
|
|
688
|
+
}
|
|
689
|
+
const mse = videoReconMSE(codec, videos);
|
|
690
|
+
|
|
691
|
+
// Structural check: encode is a self-delimiting stream that decodes to the
|
|
692
|
+
// same frame count and shape — the real serve smoke test for video.
|
|
693
|
+
const toks = codec.encode(videos[0]!);
|
|
694
|
+
const recon = codec.decode(toks);
|
|
695
|
+
if (recon.length !== videos[0]!.length) throw new Error(`round-trip frame count ${recon.length} ≠ ${videos[0]!.length}`);
|
|
696
|
+
for (const f of recon) {
|
|
697
|
+
if (f.length !== videos[0]![0]!.length) throw new Error("round-trip frame shape mismatch");
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// A trained model (if present) → the real deploy round-trip: package the
|
|
701
|
+
// model + codec into a self-contained .evermind, reload it, and generate.
|
|
702
|
+
// Served output must match the in-memory model, exactly like text roundtrip.
|
|
703
|
+
const model = ctx.bag.videoModel as EvermindLM | undefined;
|
|
704
|
+
if (model) {
|
|
705
|
+
const before = generateVideo(model, codec, [], { maxNewTokens: toks.length });
|
|
706
|
+
const modality = pNum(cfg, "frames", 4) === 1 ? "image" : "video";
|
|
707
|
+
const blob = EvermindModelPackage.fromMediaLM(model, codec, {
|
|
708
|
+
name: pStr(cfg, "name", "custom-evermind-video"),
|
|
709
|
+
version: pStr(cfg, "version", "1.0.0"),
|
|
710
|
+
modality,
|
|
711
|
+
card: { description: pStr(cfg, "description", "round-trip validated media model") },
|
|
712
|
+
}).toBlob();
|
|
713
|
+
const pkg = EvermindModelPackage.fromBlob(blob);
|
|
714
|
+
const val = pkg.validate();
|
|
715
|
+
if (!val.ok) throw new Error(`packaged media artifact failed validation: ${val.errors.join("; ")}`);
|
|
716
|
+
const served = pkg.loadMediaLM();
|
|
717
|
+
const after = generateVideo(served.lm, served.codec, [], { maxNewTokens: toks.length });
|
|
718
|
+
if (after.tokens.join(",") !== before.tokens.join(",")) {
|
|
719
|
+
throw new Error("served media model produced different tokens than the trained model");
|
|
720
|
+
}
|
|
721
|
+
for (const f of after.video) {
|
|
722
|
+
if (f.length !== videos[0]![0]!.length) throw new Error("generated frame shape mismatch");
|
|
723
|
+
}
|
|
724
|
+
ctx.artifacts.evermind = blob;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Quality gate — only enforced when configured.
|
|
728
|
+
const maxMSE = pNum(cfg, "maxMSE", 0);
|
|
729
|
+
if (maxMSE > 0 && mse > maxMSE) {
|
|
730
|
+
throw new Error(`video reconstruction MSE ${mse.toFixed(4)} exceeds max ${maxMSE} (raise levels/codebookSize or fit longer)`);
|
|
731
|
+
}
|
|
732
|
+
ctx.bag.videoReconMSE = mse;
|
|
733
|
+
const improved = Number.isFinite(before) ? ` (was ${before.toFixed(4)} unfit)` : "";
|
|
734
|
+
return `recon MSE ${mse.toFixed(4)}${improved}; ${recon.length} frames round-trip clean`;
|
|
735
|
+
}),
|
|
736
|
+
},
|
|
555
737
|
package: {
|
|
556
738
|
info: { type: "package", layer: "BUILD", label: "Package", description: "Bundle the trained model into a portable .evermind artifact (params: name, version)" },
|
|
557
739
|
factory: (cfg) =>
|
|
@@ -776,4 +958,64 @@ export const BUILTIN_STEPS: Record<string, Registered> = {
|
|
|
776
958
|
: "no cases — execution reward skipped";
|
|
777
959
|
}),
|
|
778
960
|
},
|
|
961
|
+
"code-benchmark": {
|
|
962
|
+
info: {
|
|
963
|
+
type: "code-benchmark",
|
|
964
|
+
layer: "BUILD",
|
|
965
|
+
label: "Code benchmark (pass@1)",
|
|
966
|
+
description:
|
|
967
|
+
"Held-out coding benchmark: prompt the TRAINED model on N unseen tasks, execute each generated solution against its cases, and score pass@1 (a task passes only if ALL its cases pass). The symmetric coding counterpart to `benchmark`'s held-out perplexity (params: tasks:[{prompt,cases,maxNewTokens?}], maxNewTokens, temperature, minPass1)",
|
|
968
|
+
},
|
|
969
|
+
factory: (cfg) =>
|
|
970
|
+
mkStep(cfg, "BUILD", "Code benchmark (held-out pass@1)", async (ctx) => {
|
|
971
|
+
const model = ctx.bag.model as EvermindLM | undefined;
|
|
972
|
+
const tok = ctx.bag.tokenizer as BPETokenizer | undefined;
|
|
973
|
+
if (!model || !tok) throw new Error("no trained model — add 'train-model' first");
|
|
974
|
+
|
|
975
|
+
// Held-out tasks: each is a prompt the model did NOT train on plus the
|
|
976
|
+
// execution cases its output must satisfy. A task counts as passed only
|
|
977
|
+
// when EVERY case passes (strict pass@1), so this measures whether the
|
|
978
|
+
// student learned to GENERATE working code, not just clean corpus tokens.
|
|
979
|
+
const rawTasks = cfg.params?.tasks;
|
|
980
|
+
const tasks = Array.isArray(rawTasks)
|
|
981
|
+
? rawTasks.filter(
|
|
982
|
+
(t): t is { prompt: string; cases?: unknown; maxNewTokens?: unknown } =>
|
|
983
|
+
typeof (t as { prompt?: unknown })?.prompt === "string",
|
|
984
|
+
)
|
|
985
|
+
: [];
|
|
986
|
+
if (tasks.length === 0) {
|
|
987
|
+
throw new Error("no held-out tasks — pass params.tasks: [{ prompt, cases: [{ call, expect }] }]");
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const defaultMaxNewTokens = pNum(cfg, "maxNewTokens", 24);
|
|
991
|
+
const temperature = pNum(cfg, "temperature", 0);
|
|
992
|
+
const perTask = tasks.map((t) => {
|
|
993
|
+
const cases: CodeCase[] = Array.isArray(t.cases)
|
|
994
|
+
? (t.cases as unknown[]).filter(
|
|
995
|
+
(c): c is CodeCase => typeof (c as { call?: unknown })?.call === "string",
|
|
996
|
+
)
|
|
997
|
+
: [];
|
|
998
|
+
const maxNewTokens = typeof t.maxNewTokens === "number" ? t.maxNewTokens : defaultMaxNewTokens;
|
|
999
|
+
const generated = model.generateText(t.prompt, tok, { maxNewTokens, temperature });
|
|
1000
|
+
const code = stripCodeFences(typeof generated === "string" ? generated : "");
|
|
1001
|
+
const evalResult = runJsCases(code, cases);
|
|
1002
|
+
// Strict pass@1: the task passes only when it HAS cases and they ALL pass.
|
|
1003
|
+
const passed = evalResult.total > 0 && evalResult.passRate === 1;
|
|
1004
|
+
return { prompt: t.prompt, passed, cases: evalResult.total, casesPassed: evalResult.passed, error: evalResult.error };
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
const passedTasks = perTask.filter((r) => r.passed).length;
|
|
1008
|
+
const pass1 = perTask.length > 0 ? passedTasks / perTask.length : 0;
|
|
1009
|
+
ctx.bag.codeBenchmark = { pass1, passedTasks, totalTasks: perTask.length, perTask };
|
|
1010
|
+
|
|
1011
|
+
// Optional quality gate — symmetric with `benchmark`'s maxPerplexity/minTop1.
|
|
1012
|
+
const minPass1 = pNum(cfg, "minPass1", 0); // only enforced when configured
|
|
1013
|
+
if (minPass1 > 0 && pass1 < minPass1) {
|
|
1014
|
+
throw new Error(
|
|
1015
|
+
`code pass@1 ${(pass1 * 100) | 0}% below min ${(minPass1 * 100) | 0}% (student did not learn to generate passing code — more/better TEACH_CODE corpus or epochs)`,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
return `pass@1 ${passedTasks}/${perTask.length} (${(pass1 * 100) | 0}%) over held-out coding tasks`;
|
|
1019
|
+
}),
|
|
1020
|
+
},
|
|
779
1021
|
};
|
|
@@ -122,6 +122,14 @@ export const TEACH_CODE: WorkflowConfig = {
|
|
|
122
122
|
{ id: "parse", type: "code-parse-check", params: { language: "js" } },
|
|
123
123
|
{ id: "reward", type: "code-eval", params: { cases: [{ call: "add(2, 3)", expect: 5 }] } },
|
|
124
124
|
{ id: "bench", type: "benchmark" },
|
|
125
|
+
// Held-out pass@1 — measures whether the student can GENERATE passing code on
|
|
126
|
+
// unseen prompts (symmetric with `bench`'s held-out perplexity). Ungated here
|
|
127
|
+
// (a demo-scale model won't pass) so it reports the metric without failing the
|
|
128
|
+
// pipeline; real corpora set `minPass1` to enforce a student-quality floor.
|
|
129
|
+
{ id: "codebench", type: "code-benchmark", params: { tasks: [
|
|
130
|
+
{ prompt: "function subtract", cases: [{ call: "subtract(5, 2)", expect: 3 }] },
|
|
131
|
+
{ prompt: "function isEven", cases: [{ call: "isEven(4)", expect: true }, { call: "isEven(3)", expect: false }] },
|
|
132
|
+
] } },
|
|
125
133
|
{ id: "pkg", type: "roundtrip", params: { name: "evermind-coder" } },
|
|
126
134
|
{ id: "export", type: "export", params: { format: "huggingface", name: "evermind-coder", version: "1.0.0" } },
|
|
127
135
|
],
|