@seanhogg/builderforce-memory 2026.6.35 → 2026.7.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- 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 +27 -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/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/limbic/LimbicSession.d.ts +31 -1
- package/dist/limbic/LimbicSession.d.ts.map +1 -1
- package/dist/limbic/LimbicSession.js +47 -3
- package/dist/limbic/LimbicSession.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/code-eval.d.ts +54 -0
- package/dist/workflow/code-eval.d.ts.map +1 -0
- package/dist/workflow/code-eval.js +135 -0
- package/dist/workflow/code-eval.js.map +1 -0
- package/dist/workflow/index.d.ts +3 -2
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +3 -2
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/steps.d.ts +20 -0
- package/dist/workflow/steps.d.ts.map +1 -1
- package/dist/workflow/steps.js +522 -7
- package/dist/workflow/steps.js.map +1 -1
- package/dist/workflow/templates.d.ts +15 -0
- package/dist/workflow/templates.d.ts.map +1 -1
- package/dist/workflow/templates.js +64 -1
- package/dist/workflow/templates.js.map +1 -1
- package/package.json +8 -4
- package/src/cognition/EvermindCognition.ts +24 -7
- package/src/diagnostics/stack-diagnostic.ts +37 -1
- package/src/distillation/DistillationEngine.ts +17 -4
- package/src/index.ts +6 -0
- package/src/limbic/LimbicSession.ts +61 -1
- package/src/memory/MemoryStore.ts +14 -2
- package/src/session/session.ts +72 -2
- package/src/workflow/code-eval.ts +143 -0
- package/src/workflow/index.ts +9 -1
- package/src/workflow/steps.ts +605 -6
- package/src/workflow/templates.ts +67 -1
package/dist/workflow/steps.js
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
*
|
|
10
10
|
* A custom workflow mixes these freely; new step types register the same way.
|
|
11
11
|
*/
|
|
12
|
-
import { EvermindLM, EvermindLMTrainer, BPETokenizer, EvermindModelPackage, exportEvermind, benchmarkText, } from "@seanhogg/builderforce-memory-engine";
|
|
12
|
+
import { EvermindLM, EvermindLMTrainer, BPETokenizer, EvermindModelPackage, exportEvermind, importEvermind, benchmarkText, VideoRVQCodec, buildVideoSequence, generateVideo, } from "@seanhogg/builderforce-memory-engine";
|
|
13
13
|
import { EvermindCognition } from "../cognition/index.js";
|
|
14
14
|
import { hybridRetrieve, chunkText } from "../retrieval/index.js";
|
|
15
15
|
import { MemoryStore } from "../memory/MemoryStore.js";
|
|
16
|
+
import { OpenAIBridge } from "../bridges/OpenAIBridge.js";
|
|
17
|
+
import { AnthropicBridge } from "../bridges/AnthropicBridge.js";
|
|
18
|
+
import { analyzeCode, runJsCases } from "./code-eval.js";
|
|
16
19
|
const KNOWLEDGE = "BuilderForce orchestrates many agents through a planning loop. " +
|
|
17
20
|
"The memory layer stores facts as SSM embeddings. " +
|
|
18
21
|
"Deployment runs on Cloudflare Workers and Durable Objects. " +
|
|
@@ -31,20 +34,201 @@ function pBool(cfg, key, def) {
|
|
|
31
34
|
const v = cfg.params?.[key];
|
|
32
35
|
return typeof v === "boolean" ? v : def;
|
|
33
36
|
}
|
|
37
|
+
/** Read a string[] param (non-strings dropped). */
|
|
38
|
+
function pStrArr(cfg, key) {
|
|
39
|
+
const v = cfg.params?.[key];
|
|
40
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
41
|
+
}
|
|
42
|
+
/** Default teacher instruction for code distillation — code only, no prose/fences. */
|
|
43
|
+
const CODE_TEACHER_SYSTEM = "You are an expert programmer. Respond with correct, idiomatic code that fulfils the request. " +
|
|
44
|
+
"Output code only — no explanation, no markdown fences.";
|
|
45
|
+
/**
|
|
46
|
+
* Strip a wrapping ```lang … ``` markdown fence some teachers add despite the
|
|
47
|
+
* "code only, no fences" instruction — otherwise the fence fails every code
|
|
48
|
+
* gate and the exemplar is wrongly dropped.
|
|
49
|
+
*/
|
|
50
|
+
export function stripCodeFences(text) {
|
|
51
|
+
const t = text.trim();
|
|
52
|
+
const m = /^```[\w-]*\r?\n([\s\S]*?)\r?\n```$/.exec(t);
|
|
53
|
+
return m ? m[1].trim() : t;
|
|
54
|
+
}
|
|
55
|
+
/** Read `cases` off a param blob, dropping malformed entries. */
|
|
56
|
+
function normalizeCases(raw) {
|
|
57
|
+
return Array.isArray(raw)
|
|
58
|
+
? raw.filter((c) => typeof c?.call === "string")
|
|
59
|
+
: [];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Best-of-N selection across a teacher panel. When the task carries execution
|
|
63
|
+
* `cases`, keep only fully-passing candidates and return the first — teachers
|
|
64
|
+
* are listed strongest-first (e.g. Opus), so the strongest correct answer wins
|
|
65
|
+
* and a wrong teacher answer is dropped rather than trained on. With no cases,
|
|
66
|
+
* take the first non-empty candidate (teacher-order priority).
|
|
67
|
+
*/
|
|
68
|
+
export function selectBestCandidate(candidates, cases) {
|
|
69
|
+
if (candidates.length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
if (cases.length === 0)
|
|
72
|
+
return candidates[0];
|
|
73
|
+
for (const c of candidates) {
|
|
74
|
+
if (runJsCases(c.completion, cases).passRate === 1)
|
|
75
|
+
return c;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build one teacher bridge from its spec. Any external LLM can teach Evermind:
|
|
81
|
+
* • provider "openai" (default) — the BuilderForce gateway, OpenRouter, OpenAI,
|
|
82
|
+
* or any OpenAI-compatible server (Ollama / vLLM / LM Studio), by `baseUrl`.
|
|
83
|
+
* • provider "anthropic" — the Anthropic Messages API directly (Opus etc).
|
|
84
|
+
* Returns null when the entry lacks the credentials it needs.
|
|
85
|
+
*/
|
|
86
|
+
function buildTeacher(spec, defSystem, defMaxTokens) {
|
|
87
|
+
const provider = (spec.provider ?? "openai").toLowerCase();
|
|
88
|
+
const apiKey = spec.apiKey ?? "";
|
|
89
|
+
const model = spec.model ?? "";
|
|
90
|
+
const systemPrompt = spec.systemPrompt ?? defSystem;
|
|
91
|
+
const maxTokens = spec.maxTokens ?? defMaxTokens;
|
|
92
|
+
if (provider === "anthropic") {
|
|
93
|
+
if (!apiKey)
|
|
94
|
+
return null;
|
|
95
|
+
return { label: `anthropic:${model || "default"}`, bridge: new AnthropicBridge({ apiKey, systemPrompt, maxTokens, ...(model ? { model } : {}) }) };
|
|
96
|
+
}
|
|
97
|
+
// openai-compatible (gateway / OpenRouter / OpenAI / local) — needs a baseUrl.
|
|
98
|
+
const baseUrl = spec.baseUrl ?? "";
|
|
99
|
+
if (!baseUrl)
|
|
100
|
+
return null;
|
|
101
|
+
return { label: `${provider}:${model || "default"}`, bridge: new OpenAIBridge({ apiKey, baseUrl, systemPrompt, maxTokens, ...(model ? { model } : {}) }) };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build the teacher panel from step params. `teachers: TeacherSpec[]` runs
|
|
105
|
+
* several models as a best-of-N panel (list the strongest FIRST — e.g. Opus —
|
|
106
|
+
* so it wins ties); the legacy single-teacher shape (top-level
|
|
107
|
+
* provider/baseUrl/apiKey/model) is honoured as a one-entry panel. Entries
|
|
108
|
+
* without credentials are skipped, so an offline `pairs`-only run yields [].
|
|
109
|
+
*/
|
|
110
|
+
function makeTeachers(cfg) {
|
|
111
|
+
const defSystem = pStr(cfg, "systemPrompt", CODE_TEACHER_SYSTEM);
|
|
112
|
+
const defMaxTokens = pNum(cfg, "maxTokens", 512);
|
|
113
|
+
const raw = cfg.params?.teachers;
|
|
114
|
+
const specs = Array.isArray(raw) && raw.length > 0
|
|
115
|
+
? raw
|
|
116
|
+
: [{ provider: pStr(cfg, "provider", "openai"), apiKey: pStr(cfg, "apiKey", ""), model: pStr(cfg, "model", ""), baseUrl: pStr(cfg, "baseUrl", "") }];
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const s of specs) {
|
|
119
|
+
const t = buildTeacher(s ?? {}, defSystem, defMaxTokens);
|
|
120
|
+
if (t)
|
|
121
|
+
out.push(t);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
/** Read the coding tasks to distil: rich `tasks[{prompt,cases}]` or plain `prompts[]`. */
|
|
126
|
+
function readTasks(cfg) {
|
|
127
|
+
const raw = cfg.params?.tasks;
|
|
128
|
+
if (Array.isArray(raw)) {
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const t of raw) {
|
|
131
|
+
const prompt = t?.prompt;
|
|
132
|
+
if (typeof prompt !== "string")
|
|
133
|
+
continue;
|
|
134
|
+
const cases = normalizeCases(t.cases);
|
|
135
|
+
out.push(cases.length ? { prompt, cases } : { prompt });
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
return pStrArr(cfg, "prompts").map((prompt) => ({ prompt }));
|
|
140
|
+
}
|
|
34
141
|
/** Byte length of an emitted export file (text or binary). */
|
|
35
142
|
function fileBytes(data) {
|
|
36
143
|
return typeof data === "string" ? new TextEncoder().encode(data).length : data.length;
|
|
37
144
|
}
|
|
145
|
+
/** Coerce a workflow-param weight blob (bytes, ArrayBuffer, number[], or base64) to Uint8Array. */
|
|
146
|
+
function toBytes(value, base64) {
|
|
147
|
+
if (value instanceof Uint8Array)
|
|
148
|
+
return value;
|
|
149
|
+
if (value instanceof ArrayBuffer)
|
|
150
|
+
return new Uint8Array(value);
|
|
151
|
+
if (Array.isArray(value) && value.every((n) => typeof n === "number"))
|
|
152
|
+
return Uint8Array.from(value);
|
|
153
|
+
if (typeof base64 === "string" && base64) {
|
|
154
|
+
if (typeof atob !== "function")
|
|
155
|
+
throw new Error("import-model: base64 decoding needs atob (browser/Worker/Node 16+)");
|
|
156
|
+
const bin = atob(base64);
|
|
157
|
+
const out = new Uint8Array(bin.length);
|
|
158
|
+
for (let i = 0; i < bin.length; i++)
|
|
159
|
+
out[i] = bin.charCodeAt(i);
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
38
164
|
function mkStep(cfg, layer, defaultLabel, run) {
|
|
39
165
|
return { id: cfg.id, layer, label: cfg.label ?? defaultLabel, run };
|
|
40
166
|
}
|
|
41
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* Encode a corpus into next-token training sequences. Split on sentence
|
|
169
|
+
* boundaries (prose) OR blank lines (code blocks / distilled prompt→code pairs),
|
|
170
|
+
* so both natural-language and code corpora yield multiple trainable sequences.
|
|
171
|
+
*/
|
|
42
172
|
function corpusToSequences(corpus, tok) {
|
|
43
173
|
return corpus
|
|
44
|
-
.split(/(?<=\.)\s
|
|
174
|
+
.split(/(?<=\.)\s+|\n\s*\n/)
|
|
45
175
|
.map((s) => tok.encode(s.trim()))
|
|
46
176
|
.filter((ids) => ids.length >= 2);
|
|
47
177
|
}
|
|
178
|
+
/** Build a VideoRVQCodec from step params (shared by the video build steps). */
|
|
179
|
+
function makeVideoCodec(cfg, textVocabSize) {
|
|
180
|
+
return new VideoRVQCodec({
|
|
181
|
+
height: pNum(cfg, "height", 8),
|
|
182
|
+
width: pNum(cfg, "width", 8),
|
|
183
|
+
channels: pNum(cfg, "channels", 3),
|
|
184
|
+
patch: pNum(cfg, "patch", 4),
|
|
185
|
+
levels: pNum(cfg, "levels", 2),
|
|
186
|
+
codebookSize: pNum(cfg, "codebookSize", 16),
|
|
187
|
+
keyframeInterval: pNum(cfg, "keyframeInterval", 12),
|
|
188
|
+
textVocabSize,
|
|
189
|
+
seed: pNum(cfg, "seed", 7),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Deterministic synthetic clips so the video steps are self-runnable (the way the
|
|
194
|
+
* text steps default their corpus). A smooth spatial pattern that drifts a little
|
|
195
|
+
* per frame — enough temporal redundancy to exercise the inter-frame path.
|
|
196
|
+
*/
|
|
197
|
+
function synthVideos(count, frames, h, w, c) {
|
|
198
|
+
const out = [];
|
|
199
|
+
for (let v = 0; v < count; v++) {
|
|
200
|
+
const clip = [];
|
|
201
|
+
for (let t = 0; t < frames; t++) {
|
|
202
|
+
const f = new Float32Array(h * w * c);
|
|
203
|
+
for (let y = 0; y < h; y++) {
|
|
204
|
+
for (let x = 0; x < w; x++) {
|
|
205
|
+
for (let ch = 0; ch < c; ch++) {
|
|
206
|
+
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);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
clip.push(f);
|
|
211
|
+
}
|
|
212
|
+
out.push(clip);
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
/** Mean per-pixel reconstruction MSE of a codec over clips (the video roundtrip metric). */
|
|
217
|
+
function videoReconMSE(codec, videos) {
|
|
218
|
+
let se = 0;
|
|
219
|
+
let n = 0;
|
|
220
|
+
for (const v of videos) {
|
|
221
|
+
const r = codec.decode(codec.encode(v));
|
|
222
|
+
for (let t = 0; t < v.length; t++) {
|
|
223
|
+
for (let k = 0; k < v[t].length; k++) {
|
|
224
|
+
const d = v[t][k] - r[t][k];
|
|
225
|
+
se += d * d;
|
|
226
|
+
}
|
|
227
|
+
n += v[t].length;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return n > 0 ? se / n : 0;
|
|
231
|
+
}
|
|
48
232
|
export const BUILTIN_STEPS = {
|
|
49
233
|
// ── Agent-stack layers (the diagnostic / 7-layer template) ──────────────────
|
|
50
234
|
foundation: {
|
|
@@ -155,15 +339,36 @@ export const BUILTIN_STEPS = {
|
|
|
155
339
|
},
|
|
156
340
|
// ── LLM-creation pipeline (BUILD) — produces a custom .evermind model ────────
|
|
157
341
|
"train-tokenizer": {
|
|
158
|
-
info: {
|
|
342
|
+
info: {
|
|
343
|
+
type: "train-tokenizer",
|
|
344
|
+
layer: "BUILD",
|
|
345
|
+
label: "Train tokenizer",
|
|
346
|
+
description: "Build a BPE vocab — train fresh from your corpus, OR import merges from a proven code tokenizer (params: corpus, numMerges, hfTokenizer, hfTokenizerUrl)",
|
|
347
|
+
},
|
|
159
348
|
factory: (cfg) => mkStep(cfg, "BUILD", "Train tokenizer (BPE)", async (ctx) => {
|
|
160
|
-
const corpus = pStr(cfg, "corpus", KNOWLEDGE.repeat(4));
|
|
161
|
-
const numMerges = pNum(cfg, "numMerges", 100);
|
|
162
349
|
const tok = new BPETokenizer();
|
|
350
|
+
// Two paths, both first-class (this is an IDE choice):
|
|
351
|
+
// • import-merges: seed from a Hugging Face tokenizer.json (code vocab on day one)
|
|
352
|
+
// • train-fresh: learn a vocab from the corpus
|
|
353
|
+
const hfTokenizer = cfg.params?.hfTokenizer;
|
|
354
|
+
const hfTokenizerUrl = pStr(cfg, "hfTokenizerUrl", "");
|
|
355
|
+
if (hfTokenizer && typeof hfTokenizer === "object") {
|
|
356
|
+
tok.loadHuggingFace(hfTokenizer);
|
|
357
|
+
ctx.bag.tokenizer = tok;
|
|
358
|
+
return `imported vocab ${tok.vocabSize} (Hugging Face merges)`;
|
|
359
|
+
}
|
|
360
|
+
if (hfTokenizerUrl) {
|
|
361
|
+
await tok.loadHuggingFaceUrl(hfTokenizerUrl);
|
|
362
|
+
ctx.bag.tokenizer = tok;
|
|
363
|
+
return `imported vocab ${tok.vocabSize} from ${hfTokenizerUrl}`;
|
|
364
|
+
}
|
|
365
|
+
// train-fresh — default to the distilled corpus from a prior step when present.
|
|
366
|
+
const corpus = pStr(cfg, "corpus", ctx.bag.corpus ?? KNOWLEDGE.repeat(4));
|
|
367
|
+
const numMerges = pNum(cfg, "numMerges", 100);
|
|
163
368
|
tok.train(corpus, { numMerges });
|
|
164
369
|
ctx.bag.tokenizer = tok;
|
|
165
370
|
ctx.bag.corpus = corpus;
|
|
166
|
-
return `vocab ${tok.vocabSize} (${numMerges} merges)`;
|
|
371
|
+
return `trained vocab ${tok.vocabSize} (${numMerges} merges)`;
|
|
167
372
|
}),
|
|
168
373
|
},
|
|
169
374
|
"train-model": {
|
|
@@ -336,6 +541,105 @@ export const BUILTIN_STEPS = {
|
|
|
336
541
|
return `artifact ${blob.byteLength} bytes; trained vs served output matches`;
|
|
337
542
|
}),
|
|
338
543
|
},
|
|
544
|
+
"video-train": {
|
|
545
|
+
info: {
|
|
546
|
+
type: "video-train",
|
|
547
|
+
layer: "BUILD",
|
|
548
|
+
label: "Train video model",
|
|
549
|
+
description: "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)",
|
|
550
|
+
},
|
|
551
|
+
factory: (cfg) => mkStep(cfg, "BUILD", "Train video model (codec + EvermindLM)", async (ctx) => {
|
|
552
|
+
const videos = ctx.bag.videos ??
|
|
553
|
+
synthVideos(pNum(cfg, "clips", 3), pNum(cfg, "frames", 4), pNum(cfg, "height", 8), pNum(cfg, "width", 8), pNum(cfg, "channels", 3));
|
|
554
|
+
if (videos.length === 0)
|
|
555
|
+
throw new Error("no clips to train on — provide ctx.bag.videos or a positive 'clips' param");
|
|
556
|
+
// Learn the codec's codebooks, then autoregress the (unchanged) EvermindLM
|
|
557
|
+
// over the resulting token streams. Text region is 0 for a pure-video model.
|
|
558
|
+
const codec = makeVideoCodec(cfg, 0);
|
|
559
|
+
const codecMSE = codec.fit(videos, { iterations: pNum(cfg, "codecIterations", 10) });
|
|
560
|
+
const sequences = videos.map((v) => buildVideoSequence(codec, [], v));
|
|
561
|
+
const model = new EvermindLM({
|
|
562
|
+
vocabSize: codec.vocabSize,
|
|
563
|
+
dModel: pNum(cfg, "dModel", 32),
|
|
564
|
+
numLayers: pNum(cfg, "numLayers", 2),
|
|
565
|
+
hiddenDim: pNum(cfg, "hiddenDim", 48),
|
|
566
|
+
seed: pNum(cfg, "seed", 11),
|
|
567
|
+
});
|
|
568
|
+
const epochs = pNum(cfg, "epochs", 60);
|
|
569
|
+
const history = new EvermindLMTrainer(model, { lr: pNum(cfg, "lr", 0.03), epochs }).fit(sequences);
|
|
570
|
+
ctx.bag.videoCodec = codec;
|
|
571
|
+
ctx.bag.videoModel = model;
|
|
572
|
+
ctx.bag.videoTrainingHistory = history;
|
|
573
|
+
const last = history.at(-1) ?? 0;
|
|
574
|
+
return `codec MSE ${codecMSE.toFixed(4)}; trained ${epochs} epochs over ${sequences.length} clip(s), loss ${last.toFixed(3)}`;
|
|
575
|
+
}),
|
|
576
|
+
},
|
|
577
|
+
"video-roundtrip": {
|
|
578
|
+
info: {
|
|
579
|
+
type: "video-roundtrip",
|
|
580
|
+
layer: "BUILD",
|
|
581
|
+
label: "Video round-trip",
|
|
582
|
+
description: "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)",
|
|
583
|
+
},
|
|
584
|
+
factory: (cfg) => mkStep(cfg, "BUILD", "Video codec round-trip (encode→decode)", async (ctx) => {
|
|
585
|
+
const videos = ctx.bag.videos ??
|
|
586
|
+
synthVideos(pNum(cfg, "clips", 3), pNum(cfg, "frames", 4), pNum(cfg, "height", 8), pNum(cfg, "width", 8), pNum(cfg, "channels", 3));
|
|
587
|
+
// Reuse a codec trained earlier if present; otherwise fit a fresh one.
|
|
588
|
+
let codec = ctx.bag.videoCodec;
|
|
589
|
+
const before = codec ? videoReconMSE(codec, videos) : Infinity;
|
|
590
|
+
if (!codec) {
|
|
591
|
+
codec = makeVideoCodec(cfg, 0);
|
|
592
|
+
codec.fit(videos, { iterations: pNum(cfg, "codecIterations", 10) });
|
|
593
|
+
}
|
|
594
|
+
const mse = videoReconMSE(codec, videos);
|
|
595
|
+
// Structural check: encode is a self-delimiting stream that decodes to the
|
|
596
|
+
// same frame count and shape — the real serve smoke test for video.
|
|
597
|
+
const toks = codec.encode(videos[0]);
|
|
598
|
+
const recon = codec.decode(toks);
|
|
599
|
+
if (recon.length !== videos[0].length)
|
|
600
|
+
throw new Error(`round-trip frame count ${recon.length} ≠ ${videos[0].length}`);
|
|
601
|
+
for (const f of recon) {
|
|
602
|
+
if (f.length !== videos[0][0].length)
|
|
603
|
+
throw new Error("round-trip frame shape mismatch");
|
|
604
|
+
}
|
|
605
|
+
// A trained model (if present) → the real deploy round-trip: package the
|
|
606
|
+
// model + codec into a self-contained .evermind, reload it, and generate.
|
|
607
|
+
// Served output must match the in-memory model, exactly like text roundtrip.
|
|
608
|
+
const model = ctx.bag.videoModel;
|
|
609
|
+
if (model) {
|
|
610
|
+
const before = generateVideo(model, codec, [], { maxNewTokens: toks.length });
|
|
611
|
+
const modality = pNum(cfg, "frames", 4) === 1 ? "image" : "video";
|
|
612
|
+
const blob = EvermindModelPackage.fromMediaLM(model, codec, {
|
|
613
|
+
name: pStr(cfg, "name", "custom-evermind-video"),
|
|
614
|
+
version: pStr(cfg, "version", "1.0.0"),
|
|
615
|
+
modality,
|
|
616
|
+
card: { description: pStr(cfg, "description", "round-trip validated media model") },
|
|
617
|
+
}).toBlob();
|
|
618
|
+
const pkg = EvermindModelPackage.fromBlob(blob);
|
|
619
|
+
const val = pkg.validate();
|
|
620
|
+
if (!val.ok)
|
|
621
|
+
throw new Error(`packaged media artifact failed validation: ${val.errors.join("; ")}`);
|
|
622
|
+
const served = pkg.loadMediaLM();
|
|
623
|
+
const after = generateVideo(served.lm, served.codec, [], { maxNewTokens: toks.length });
|
|
624
|
+
if (after.tokens.join(",") !== before.tokens.join(",")) {
|
|
625
|
+
throw new Error("served media model produced different tokens than the trained model");
|
|
626
|
+
}
|
|
627
|
+
for (const f of after.video) {
|
|
628
|
+
if (f.length !== videos[0][0].length)
|
|
629
|
+
throw new Error("generated frame shape mismatch");
|
|
630
|
+
}
|
|
631
|
+
ctx.artifacts.evermind = blob;
|
|
632
|
+
}
|
|
633
|
+
// Quality gate — only enforced when configured.
|
|
634
|
+
const maxMSE = pNum(cfg, "maxMSE", 0);
|
|
635
|
+
if (maxMSE > 0 && mse > maxMSE) {
|
|
636
|
+
throw new Error(`video reconstruction MSE ${mse.toFixed(4)} exceeds max ${maxMSE} (raise levels/codebookSize or fit longer)`);
|
|
637
|
+
}
|
|
638
|
+
ctx.bag.videoReconMSE = mse;
|
|
639
|
+
const improved = Number.isFinite(before) ? ` (was ${before.toFixed(4)} unfit)` : "";
|
|
640
|
+
return `recon MSE ${mse.toFixed(4)}${improved}; ${recon.length} frames round-trip clean`;
|
|
641
|
+
}),
|
|
642
|
+
},
|
|
339
643
|
package: {
|
|
340
644
|
info: { type: "package", layer: "BUILD", label: "Package", description: "Bundle the trained model into a portable .evermind artifact (params: name, version)" },
|
|
341
645
|
factory: (cfg) => mkStep(cfg, "BUILD", "Package .evermind artifact", async (ctx) => {
|
|
@@ -386,5 +690,216 @@ export const BUILTIN_STEPS = {
|
|
|
386
690
|
return `exported ${format}: ${result.files.length} file(s), ${total} bytes, ${result.paramCount} params`;
|
|
387
691
|
}),
|
|
388
692
|
},
|
|
693
|
+
"import-model": {
|
|
694
|
+
info: {
|
|
695
|
+
type: "import-model",
|
|
696
|
+
layer: "BUILD",
|
|
697
|
+
label: "Import model (warm-start)",
|
|
698
|
+
description: "Warm-start an EvermindLM from a .safetensors checkpoint instead of random init — round-trips our own exports, or maps a foreign SSM checkpoint via renameMap (params: safetensors, safetensorsBase64, topK, renameMap)",
|
|
699
|
+
},
|
|
700
|
+
factory: (cfg) => mkStep(cfg, "BUILD", "Import model (warm-start)", async (ctx) => {
|
|
701
|
+
const bytes = toBytes(cfg.params?.safetensors, pStr(cfg, "safetensorsBase64", ""));
|
|
702
|
+
if (!bytes) {
|
|
703
|
+
throw new Error("import-model: provide 'safetensors' (bytes/ArrayBuffer/number[]) or 'safetensorsBase64'");
|
|
704
|
+
}
|
|
705
|
+
const opts = {};
|
|
706
|
+
const topK = pNum(cfg, "topK", 0);
|
|
707
|
+
if (topK > 0)
|
|
708
|
+
opts.topK = topK;
|
|
709
|
+
// renameMap is the JSON-friendly form of ImportOptions.rename — used to
|
|
710
|
+
// translate a foreign SSM checkpoint's tensor names to our canonical names.
|
|
711
|
+
const renameMap = cfg.params?.renameMap;
|
|
712
|
+
if (renameMap && typeof renameMap === "object") {
|
|
713
|
+
opts.rename = (name) => (name in renameMap ? renameMap[name] : name);
|
|
714
|
+
}
|
|
715
|
+
const model = importEvermind(bytes, opts);
|
|
716
|
+
ctx.bag.model = model;
|
|
717
|
+
const params = model.parameters().reduce((n, p) => n + p.data.length, 0);
|
|
718
|
+
const c = model.config;
|
|
719
|
+
return `imported ${params} params (dModel ${c.dModel}, ${c.numLayers}L, ${c.numExperts}E) from ${bytes.length} bytes`;
|
|
720
|
+
}),
|
|
721
|
+
},
|
|
722
|
+
// ── Teaching Evermind to code — multi-teacher distillation + exec gates ───────
|
|
723
|
+
"distill-corpus": {
|
|
724
|
+
info: {
|
|
725
|
+
type: "distill-corpus",
|
|
726
|
+
layer: "BUILD",
|
|
727
|
+
label: "Distill from teacher panel",
|
|
728
|
+
description: "Build a training corpus from a panel of teacher LLMs (Opus + any other model — the gateway, OpenRouter, OpenAI, a local server, Anthropic). Per task, queries every teacher, keeps only completions that PASS the task's execution cases (best-of-N, strongest teacher first), tags each with its teacher, and ACCUMULATES so stacked steps add up. Feeds the corpus to train-model (params: teachers[{provider,baseUrl,apiKey,model}], tasks[{prompt,cases}], prompts, provider, baseUrl, apiKey, model, systemPrompt, maxTokens, temperature, pairs)",
|
|
729
|
+
},
|
|
730
|
+
factory: (cfg) => mkStep(cfg, "BUILD", "Distill corpus from teacher panel", async (ctx) => {
|
|
731
|
+
// Accumulate across steps so multiple teacher panels stack instead of
|
|
732
|
+
// overwriting a prior distill step's exemplars.
|
|
733
|
+
const pairs = Array.isArray(ctx.bag.distillPairs) ? ctx.bag.distillPairs : [];
|
|
734
|
+
const startCount = pairs.length;
|
|
735
|
+
let offlineKept = 0;
|
|
736
|
+
let verified = 0;
|
|
737
|
+
let dropped = 0;
|
|
738
|
+
// Offline exemplars (pre-captured) — a single trusted candidate each,
|
|
739
|
+
// execution-verified when the pair carries `cases`.
|
|
740
|
+
const offline = cfg.params?.pairs;
|
|
741
|
+
if (Array.isArray(offline)) {
|
|
742
|
+
for (const p of offline) {
|
|
743
|
+
const prompt = p?.prompt;
|
|
744
|
+
const completion = p?.completion;
|
|
745
|
+
if (typeof prompt !== "string" || typeof completion !== "string")
|
|
746
|
+
continue;
|
|
747
|
+
const cases = normalizeCases(p.cases);
|
|
748
|
+
const chosen = selectBestCandidate([{ teacher: "offline", completion: stripCodeFences(completion) }], cases);
|
|
749
|
+
if (!chosen) {
|
|
750
|
+
dropped++;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
const pair = { prompt, completion: chosen.completion, teacher: "offline" };
|
|
754
|
+
if (cases.length)
|
|
755
|
+
pair.cases = cases;
|
|
756
|
+
pairs.push(pair);
|
|
757
|
+
offlineKept++;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// Live teacher panel — Opus and/or any other models, best-of-N + exec-filtered.
|
|
761
|
+
const teachers = makeTeachers(cfg);
|
|
762
|
+
const tasks = readTasks(cfg);
|
|
763
|
+
if (teachers.length > 0 && tasks.length > 0) {
|
|
764
|
+
const temperature = pNum(cfg, "temperature", 0.2);
|
|
765
|
+
// Dedupe identical (teacher, prompt) calls within this run so a repeated
|
|
766
|
+
// prompt is not billed twice.
|
|
767
|
+
const memo = new Map();
|
|
768
|
+
for (const task of tasks) {
|
|
769
|
+
const candidates = [];
|
|
770
|
+
for (const t of teachers) {
|
|
771
|
+
const key = `${t.label}${task.prompt}`;
|
|
772
|
+
let completion = memo.get(key);
|
|
773
|
+
if (completion === undefined) {
|
|
774
|
+
completion = stripCodeFences(await t.bridge.generate(task.prompt, { temperature }));
|
|
775
|
+
memo.set(key, completion);
|
|
776
|
+
}
|
|
777
|
+
if (completion.trim())
|
|
778
|
+
candidates.push({ teacher: t.label, completion });
|
|
779
|
+
}
|
|
780
|
+
const chosen = selectBestCandidate(candidates, task.cases ?? []);
|
|
781
|
+
if (!chosen) {
|
|
782
|
+
dropped++;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
const pair = { prompt: task.prompt, completion: chosen.completion, teacher: chosen.teacher };
|
|
786
|
+
if (task.cases?.length)
|
|
787
|
+
pair.cases = task.cases;
|
|
788
|
+
pairs.push(pair);
|
|
789
|
+
verified++;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (pairs.length === 0) {
|
|
793
|
+
throw new Error("distill-corpus produced no exemplars — set teachers (provider + apiKey + prompts/tasks; baseUrl for openai-compatible) or supply offline 'pairs'");
|
|
794
|
+
}
|
|
795
|
+
const corpus = pairs.map((p) => `${p.prompt}\n${p.completion}`).join("\n\n");
|
|
796
|
+
ctx.bag.corpus = corpus;
|
|
797
|
+
ctx.bag.distillPairs = pairs;
|
|
798
|
+
const added = pairs.length - startCount;
|
|
799
|
+
const parts = [];
|
|
800
|
+
if (offlineKept)
|
|
801
|
+
parts.push(`${offlineKept} offline`);
|
|
802
|
+
if (verified)
|
|
803
|
+
parts.push(`${verified} verified via ${teachers.length} teacher(s): ${teachers.map((t) => t.label).join(", ")}`);
|
|
804
|
+
if (dropped)
|
|
805
|
+
parts.push(`${dropped} dropped`);
|
|
806
|
+
return `distilled ${added} exemplar(s) (${pairs.length} total, ${corpus.length} chars)${parts.length ? ` — ${parts.join(", ")}` : ""}`;
|
|
807
|
+
}),
|
|
808
|
+
},
|
|
809
|
+
"code-parse-check": {
|
|
810
|
+
info: {
|
|
811
|
+
type: "code-parse-check",
|
|
812
|
+
layer: "BUILD",
|
|
813
|
+
label: "Code parse check",
|
|
814
|
+
description: "Validate generated output is structurally valid code — balanced delimiters/strings, optional JS parse (params: code, language, minScore)",
|
|
815
|
+
},
|
|
816
|
+
factory: (cfg) => mkStep(cfg, "BUILD", "Code structure / parse gate", async (ctx) => {
|
|
817
|
+
const code = pStr(cfg, "code", ctx.bag.sample ?? "");
|
|
818
|
+
if (!code.trim())
|
|
819
|
+
throw new Error("no code to check — run 'evaluate'/'generate-check' first or pass a 'code' param");
|
|
820
|
+
const language = pStr(cfg, "language", "js");
|
|
821
|
+
const analysis = analyzeCode(code, language);
|
|
822
|
+
ctx.bag.codeParse = analysis;
|
|
823
|
+
const minScore = pNum(cfg, "minScore", 0); // only enforced when configured
|
|
824
|
+
if (minScore > 0 && analysis.score < minScore) {
|
|
825
|
+
throw new Error(`code structure score ${analysis.score.toFixed(2)} < min ${minScore} (unbalanced delimiters/strings — output is not valid ${language})`);
|
|
826
|
+
}
|
|
827
|
+
return `structure ${(analysis.score * 100) | 0}%${language === "js" ? `, js-parse ${analysis.jsParse ? "ok" : "fail"}` : ""}`;
|
|
828
|
+
}),
|
|
829
|
+
},
|
|
830
|
+
"code-eval": {
|
|
831
|
+
info: {
|
|
832
|
+
type: "code-eval",
|
|
833
|
+
layer: "BUILD",
|
|
834
|
+
label: "Code eval (reward)",
|
|
835
|
+
description: "Execution-grounded reward: run the generated JS against test cases and score the pass-rate (params: code, cases, minPassRate)",
|
|
836
|
+
},
|
|
837
|
+
factory: (cfg) => mkStep(cfg, "BUILD", "Code execution reward", async (ctx) => {
|
|
838
|
+
const code = pStr(cfg, "code", ctx.bag.sample ?? "");
|
|
839
|
+
if (!code.trim())
|
|
840
|
+
throw new Error("no code to evaluate — run 'evaluate'/'generate-check' first or pass a 'code' param");
|
|
841
|
+
const rawCases = cfg.params?.cases;
|
|
842
|
+
const cases = Array.isArray(rawCases)
|
|
843
|
+
? rawCases.filter((c) => typeof c?.call === "string")
|
|
844
|
+
: [];
|
|
845
|
+
const result = runJsCases(code, cases);
|
|
846
|
+
ctx.bag.codeEval = result;
|
|
847
|
+
const minPassRate = pNum(cfg, "minPassRate", 0); // only enforced when configured
|
|
848
|
+
if (minPassRate > 0 && result.passRate < minPassRate) {
|
|
849
|
+
throw new Error(`code-eval pass-rate ${(result.passRate * 100) | 0}% < min ${(minPassRate * 100) | 0}%${result.error ? ` (${result.error})` : ""}`);
|
|
850
|
+
}
|
|
851
|
+
return result.total > 0
|
|
852
|
+
? `${result.passed}/${result.total} cases passed (${(result.passRate * 100) | 0}%)`
|
|
853
|
+
: "no cases — execution reward skipped";
|
|
854
|
+
}),
|
|
855
|
+
},
|
|
856
|
+
"code-benchmark": {
|
|
857
|
+
info: {
|
|
858
|
+
type: "code-benchmark",
|
|
859
|
+
layer: "BUILD",
|
|
860
|
+
label: "Code benchmark (pass@1)",
|
|
861
|
+
description: "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)",
|
|
862
|
+
},
|
|
863
|
+
factory: (cfg) => mkStep(cfg, "BUILD", "Code benchmark (held-out pass@1)", async (ctx) => {
|
|
864
|
+
const model = ctx.bag.model;
|
|
865
|
+
const tok = ctx.bag.tokenizer;
|
|
866
|
+
if (!model || !tok)
|
|
867
|
+
throw new Error("no trained model — add 'train-model' first");
|
|
868
|
+
// Held-out tasks: each is a prompt the model did NOT train on plus the
|
|
869
|
+
// execution cases its output must satisfy. A task counts as passed only
|
|
870
|
+
// when EVERY case passes (strict pass@1), so this measures whether the
|
|
871
|
+
// student learned to GENERATE working code, not just clean corpus tokens.
|
|
872
|
+
const rawTasks = cfg.params?.tasks;
|
|
873
|
+
const tasks = Array.isArray(rawTasks)
|
|
874
|
+
? rawTasks.filter((t) => typeof t?.prompt === "string")
|
|
875
|
+
: [];
|
|
876
|
+
if (tasks.length === 0) {
|
|
877
|
+
throw new Error("no held-out tasks — pass params.tasks: [{ prompt, cases: [{ call, expect }] }]");
|
|
878
|
+
}
|
|
879
|
+
const defaultMaxNewTokens = pNum(cfg, "maxNewTokens", 24);
|
|
880
|
+
const temperature = pNum(cfg, "temperature", 0);
|
|
881
|
+
const perTask = tasks.map((t) => {
|
|
882
|
+
const cases = Array.isArray(t.cases)
|
|
883
|
+
? t.cases.filter((c) => typeof c?.call === "string")
|
|
884
|
+
: [];
|
|
885
|
+
const maxNewTokens = typeof t.maxNewTokens === "number" ? t.maxNewTokens : defaultMaxNewTokens;
|
|
886
|
+
const generated = model.generateText(t.prompt, tok, { maxNewTokens, temperature });
|
|
887
|
+
const code = stripCodeFences(typeof generated === "string" ? generated : "");
|
|
888
|
+
const evalResult = runJsCases(code, cases);
|
|
889
|
+
// Strict pass@1: the task passes only when it HAS cases and they ALL pass.
|
|
890
|
+
const passed = evalResult.total > 0 && evalResult.passRate === 1;
|
|
891
|
+
return { prompt: t.prompt, passed, cases: evalResult.total, casesPassed: evalResult.passed, error: evalResult.error };
|
|
892
|
+
});
|
|
893
|
+
const passedTasks = perTask.filter((r) => r.passed).length;
|
|
894
|
+
const pass1 = perTask.length > 0 ? passedTasks / perTask.length : 0;
|
|
895
|
+
ctx.bag.codeBenchmark = { pass1, passedTasks, totalTasks: perTask.length, perTask };
|
|
896
|
+
// Optional quality gate — symmetric with `benchmark`'s maxPerplexity/minTop1.
|
|
897
|
+
const minPass1 = pNum(cfg, "minPass1", 0); // only enforced when configured
|
|
898
|
+
if (minPass1 > 0 && pass1 < minPass1) {
|
|
899
|
+
throw new Error(`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)`);
|
|
900
|
+
}
|
|
901
|
+
return `pass@1 ${passedTasks}/${perTask.length} (${(pass1 * 100) | 0}%) over held-out coding tasks`;
|
|
902
|
+
}),
|
|
903
|
+
},
|
|
389
904
|
};
|
|
390
905
|
//# sourceMappingURL=steps.js.map
|