@seanhogg/builderforce-memory 2026.6.31 → 2026.6.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +3 -1
  2. package/dist/cognition/EvermindCognition.d.ts +46 -7
  3. package/dist/cognition/EvermindCognition.d.ts.map +1 -1
  4. package/dist/cognition/EvermindCognition.js +95 -25
  5. package/dist/cognition/EvermindCognition.js.map +1 -1
  6. package/dist/cognition/canonicalize.d.ts +37 -0
  7. package/dist/cognition/canonicalize.d.ts.map +1 -0
  8. package/dist/cognition/canonicalize.js +58 -0
  9. package/dist/cognition/canonicalize.js.map +1 -0
  10. package/dist/cognition/index.d.ts +4 -0
  11. package/dist/cognition/index.d.ts.map +1 -1
  12. package/dist/cognition/index.js +2 -0
  13. package/dist/cognition/index.js.map +1 -1
  14. package/dist/cognition/sanitize.d.ts +58 -0
  15. package/dist/cognition/sanitize.d.ts.map +1 -0
  16. package/dist/cognition/sanitize.js +103 -0
  17. package/dist/cognition/sanitize.js.map +1 -0
  18. package/dist/distillation/DistillationEngine.d.ts +32 -1
  19. package/dist/distillation/DistillationEngine.d.ts.map +1 -1
  20. package/dist/distillation/DistillationEngine.js +53 -5
  21. package/dist/distillation/DistillationEngine.js.map +1 -1
  22. package/dist/distillation/index.d.ts +1 -1
  23. package/dist/distillation/index.d.ts.map +1 -1
  24. package/dist/index.d.ts +5 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/memory/MemoryStore.d.ts +53 -0
  29. package/dist/memory/MemoryStore.d.ts.map +1 -1
  30. package/dist/memory/MemoryStore.js +175 -22
  31. package/dist/memory/MemoryStore.js.map +1 -1
  32. package/dist/publish/huggingface.d.ts +77 -0
  33. package/dist/publish/huggingface.d.ts.map +1 -0
  34. package/dist/publish/huggingface.js +88 -0
  35. package/dist/publish/huggingface.js.map +1 -0
  36. package/dist/publish/index.d.ts +7 -0
  37. package/dist/publish/index.d.ts.map +1 -0
  38. package/dist/publish/index.js +6 -0
  39. package/dist/publish/index.js.map +1 -0
  40. package/dist/retrieval/HybridRetriever.d.ts +5 -0
  41. package/dist/retrieval/HybridRetriever.d.ts.map +1 -1
  42. package/dist/retrieval/HybridRetriever.js +9 -7
  43. package/dist/retrieval/HybridRetriever.js.map +1 -1
  44. package/dist/retrieval/hnsw.d.ts +71 -0
  45. package/dist/retrieval/hnsw.d.ts.map +1 -0
  46. package/dist/retrieval/hnsw.js +201 -0
  47. package/dist/retrieval/hnsw.js.map +1 -0
  48. package/dist/retrieval/index.d.ts +2 -0
  49. package/dist/retrieval/index.d.ts.map +1 -1
  50. package/dist/retrieval/index.js +1 -0
  51. package/dist/retrieval/index.js.map +1 -1
  52. package/dist/session/session.d.ts +9 -0
  53. package/dist/session/session.d.ts.map +1 -1
  54. package/dist/session/session.js +12 -2
  55. package/dist/session/session.js.map +1 -1
  56. package/dist/workflow/steps.d.ts.map +1 -1
  57. package/dist/workflow/steps.js +73 -1
  58. package/dist/workflow/steps.js.map +1 -1
  59. package/dist/workflow/templates.d.ts.map +1 -1
  60. package/dist/workflow/templates.js +3 -1
  61. package/dist/workflow/templates.js.map +1 -1
  62. package/package.json +7 -7
  63. package/src/cognition/EvermindCognition.ts +123 -27
  64. package/src/cognition/canonicalize.ts +61 -0
  65. package/src/cognition/index.ts +12 -0
  66. package/src/cognition/sanitize.ts +126 -0
  67. package/src/distillation/DistillationEngine.ts +72 -5
  68. package/src/distillation/index.ts +1 -1
  69. package/src/index.ts +18 -1
  70. package/src/memory/MemoryStore.ts +197 -18
  71. package/src/publish/huggingface.ts +139 -0
  72. package/src/publish/index.ts +7 -0
  73. package/src/retrieval/HybridRetriever.ts +14 -7
  74. package/src/retrieval/hnsw.ts +242 -0
  75. package/src/retrieval/index.ts +3 -0
  76. package/src/session/session.ts +23 -1
  77. package/src/workflow/steps.ts +93 -0
  78. package/src/workflow/templates.ts +3 -1
@@ -0,0 +1,242 @@
1
+ /**
2
+ * hnsw.ts — a pure-TypeScript HNSW approximate-nearest-neighbour index.
3
+ *
4
+ * Recall over the fact store was an O(N) cosine scan per query, so the
5
+ * "lifetime of always-current knowledge" thesis hit a wall around ~10⁴ facts.
6
+ * HNSW (Hierarchical Navigable Small World) gives O(log N) expected query time
7
+ * with high recall, and this implementation is dependency-free so it keeps the
8
+ * engine/runtime zero-dep promise. For small sets an exact scan is both simpler
9
+ * and faster, so callers use {@link denseSearch} which switches to HNSW only
10
+ * above a threshold.
11
+ *
12
+ * Distance is cosine similarity (higher = closer); the graph stores `1 - cos`
13
+ * as the metric so "nearer" is "smaller". Construction is deterministic given a
14
+ * seed, so builds (and tests) reproduce exactly.
15
+ */
16
+
17
+ import { cosineSimilarity } from '../similarity/index.js';
18
+
19
+ export interface HnswOptions {
20
+ /** Max neighbours per node on layers > 0. Default 16. */
21
+ M?: number;
22
+ /** Candidate-list size during construction. Default 200. */
23
+ efConstruction?: number;
24
+ /** Default candidate-list size during search. Default 50. */
25
+ efSearch?: number;
26
+ /** Deterministic seed for layer assignment. Default 0x5eed. */
27
+ seed?: number;
28
+ }
29
+
30
+ export interface SearchHit {
31
+ id: string;
32
+ /** Cosine similarity to the query (higher = closer). */
33
+ score: number;
34
+ }
35
+
36
+ interface Node {
37
+ id: string;
38
+ vector: Float32Array;
39
+ /** neighbours[layer] = list of node indices. */
40
+ neighbours: number[][];
41
+ }
42
+
43
+ /** A small deterministic LCG → float in [0,1). Mirrors the engine's SeededRng. */
44
+ function makeRng(seed: number): () => number {
45
+ let s = (seed >>> 0) || 1;
46
+ return () => {
47
+ s = (Math.imul(1664525, s) + 1013904223) >>> 0;
48
+ return s / 0x1_0000_0000;
49
+ };
50
+ }
51
+
52
+ export class HnswIndex {
53
+ private readonly _M: number;
54
+ private readonly _Mmax0: number;
55
+ private readonly _efC: number;
56
+ private readonly _efS: number;
57
+ private readonly _mL: number;
58
+ private readonly _rng: () => number;
59
+
60
+ private readonly _nodes: Node[] = [];
61
+ private readonly _idToIdx = new Map<string, number>();
62
+ private _entry = -1; // index of the entry-point node
63
+ private _maxLayer = -1;
64
+
65
+ constructor(opts: HnswOptions = {}) {
66
+ this._M = Math.max(2, opts.M ?? 16);
67
+ this._Mmax0 = this._M * 2;
68
+ this._efC = Math.max(this._M, opts.efConstruction ?? 200);
69
+ this._efS = Math.max(1, opts.efSearch ?? 50);
70
+ this._mL = 1 / Math.log(this._M);
71
+ this._rng = makeRng(opts.seed ?? 0x5eed);
72
+ }
73
+
74
+ get size(): number {
75
+ return this._nodes.length;
76
+ }
77
+
78
+ has(id: string): boolean {
79
+ return this._idToIdx.has(id);
80
+ }
81
+
82
+ private _dist(a: Float32Array, b: Float32Array): number {
83
+ return 1 - cosineSimilarity(a, b);
84
+ }
85
+
86
+ private _randomLayer(): number {
87
+ return Math.floor(-Math.log(Math.max(this._rng(), 1e-12)) * this._mL);
88
+ }
89
+
90
+ /** Insert a vector. Ids are unique — re-adding a known id is ignored. */
91
+ add(id: string, vector: Float32Array): void {
92
+ if (this._idToIdx.has(id)) return;
93
+ const layer = this._randomLayer();
94
+ const node: Node = { id, vector, neighbours: Array.from({ length: layer + 1 }, () => []) };
95
+ const idx = this._nodes.length;
96
+ this._nodes.push(node);
97
+ this._idToIdx.set(id, idx);
98
+
99
+ if (this._entry === -1) {
100
+ this._entry = idx;
101
+ this._maxLayer = layer;
102
+ return;
103
+ }
104
+
105
+ // Descend from the top to just above the new node's top layer.
106
+ let ep = this._entry;
107
+ for (let lc = this._maxLayer; lc > layer; lc--) {
108
+ ep = this._greedyClosest(vector, ep, lc);
109
+ }
110
+
111
+ // From the node's top layer down to 0: find neighbours and link.
112
+ for (let lc = Math.min(layer, this._maxLayer); lc >= 0; lc--) {
113
+ const candidates = this._searchLayer(vector, ep, this._efC, lc);
114
+ const Mmax = lc === 0 ? this._Mmax0 : this._M;
115
+ const selected = candidates.slice(0, this._M).map((c) => c.idx);
116
+ node.neighbours[lc] = selected.slice();
117
+ // Add reverse links, pruning each neighbour back to Mmax.
118
+ for (const nIdx of selected) {
119
+ const nb = this._nodes[nIdx]!;
120
+ const list = nb.neighbours[lc] ?? (nb.neighbours[lc] = []);
121
+ list.push(idx);
122
+ if (list.length > Mmax) this._prune(nb, lc, Mmax);
123
+ }
124
+ ep = candidates[0]?.idx ?? ep;
125
+ }
126
+
127
+ if (layer > this._maxLayer) {
128
+ this._maxLayer = layer;
129
+ this._entry = idx;
130
+ }
131
+ }
132
+
133
+ /** Top-k nearest by cosine similarity (descending). */
134
+ search(query: Float32Array, k: number, ef = this._efS): SearchHit[] {
135
+ if (this._entry === -1 || k <= 0) return [];
136
+ let ep = this._entry;
137
+ for (let lc = this._maxLayer; lc > 0; lc--) {
138
+ ep = this._greedyClosest(query, ep, lc);
139
+ }
140
+ const found = this._searchLayer(query, ep, Math.max(ef, k), 0);
141
+ return found.slice(0, k).map((c) => ({ id: this._nodes[c.idx]!.id, score: 1 - c.dist }));
142
+ }
143
+
144
+ clear(): void {
145
+ this._nodes.length = 0;
146
+ this._idToIdx.clear();
147
+ this._entry = -1;
148
+ this._maxLayer = -1;
149
+ }
150
+
151
+ // ── internals ──────────────────────────────────────────────────────────────
152
+
153
+ /** Greedy hill-climb to the single closest node to `q` on layer `lc`. */
154
+ private _greedyClosest(q: Float32Array, start: number, lc: number): number {
155
+ let best = start;
156
+ let bestDist = this._dist(q, this._nodes[best]!.vector);
157
+ let improved = true;
158
+ while (improved) {
159
+ improved = false;
160
+ for (const nIdx of this._nodes[best]!.neighbours[lc] ?? []) {
161
+ const d = this._dist(q, this._nodes[nIdx]!.vector);
162
+ if (d < bestDist) {
163
+ bestDist = d;
164
+ best = nIdx;
165
+ improved = true;
166
+ }
167
+ }
168
+ }
169
+ return best;
170
+ }
171
+
172
+ /**
173
+ * ef-search on a single layer: returns candidates sorted nearest-first.
174
+ * A bounded best-first expansion (the standard HNSW layer search).
175
+ */
176
+ private _searchLayer(q: Float32Array, entry: number, ef: number, lc: number): Array<{ idx: number; dist: number }> {
177
+ const visited = new Set<number>([entry]);
178
+ const entryDist = this._dist(q, this._nodes[entry]!.vector);
179
+ // `candidates` is a min-heap-ish frontier; `results` keeps the best ef.
180
+ const candidates: Array<{ idx: number; dist: number }> = [{ idx: entry, dist: entryDist }];
181
+ const results: Array<{ idx: number; dist: number }> = [{ idx: entry, dist: entryDist }];
182
+
183
+ while (candidates.length > 0) {
184
+ // Pop nearest candidate.
185
+ let ci = 0;
186
+ for (let i = 1; i < candidates.length; i++) if (candidates[i]!.dist < candidates[ci]!.dist) ci = i;
187
+ const cur = candidates.splice(ci, 1)[0]!;
188
+ // Farthest in results.
189
+ const worst = results.reduce((m, r) => (r.dist > m.dist ? r : m), results[0]!);
190
+ if (cur.dist > worst.dist && results.length >= ef) break;
191
+
192
+ for (const nIdx of this._nodes[cur.idx]!.neighbours[lc] ?? []) {
193
+ if (visited.has(nIdx)) continue;
194
+ visited.add(nIdx);
195
+ const d = this._dist(q, this._nodes[nIdx]!.vector);
196
+ const farthest = results.reduce((m, r) => (r.dist > m.dist ? r : m), results[0]!);
197
+ if (results.length < ef || d < farthest.dist) {
198
+ candidates.push({ idx: nIdx, dist: d });
199
+ results.push({ idx: nIdx, dist: d });
200
+ if (results.length > ef) {
201
+ // Drop the current farthest.
202
+ let wi = 0;
203
+ for (let i = 1; i < results.length; i++) if (results[i]!.dist > results[wi]!.dist) wi = i;
204
+ results.splice(wi, 1);
205
+ }
206
+ }
207
+ }
208
+ }
209
+ return results.sort((a, b) => a.dist - b.dist);
210
+ }
211
+
212
+ /** Keep only the `Mmax` closest neighbours of `node` on layer `lc`. */
213
+ private _prune(node: Node, lc: number, Mmax: number): void {
214
+ const list = node.neighbours[lc]!;
215
+ list.sort((a, b) => this._dist(node.vector, this._nodes[a]!.vector) - this._dist(node.vector, this._nodes[b]!.vector));
216
+ node.neighbours[lc] = list.slice(0, Mmax);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Dense KNN with an exact-scan fast path: below `threshold` candidates a linear
222
+ * cosine scan is simpler and faster; at/above it, build an HNSW index and query
223
+ * it. Returned hits are cosine-similarity descending. Pure given its inputs.
224
+ */
225
+ export function denseSearch(
226
+ query: Float32Array,
227
+ items: Array<{ id: string; vector: Float32Array }>,
228
+ k: number,
229
+ threshold = 256,
230
+ hnsw?: HnswOptions,
231
+ ): SearchHit[] {
232
+ if (items.length === 0 || k <= 0) return [];
233
+ if (items.length < threshold) {
234
+ return items
235
+ .map((it) => ({ id: it.id, score: cosineSimilarity(query, it.vector) }))
236
+ .sort((a, b) => b.score - a.score)
237
+ .slice(0, k);
238
+ }
239
+ const index = new HnswIndex(hnsw);
240
+ for (const it of items) index.add(it.id, it.vector);
241
+ return index.search(query, k);
242
+ }
@@ -22,3 +22,6 @@ export type {
22
22
  HybridRetrieveOptions,
23
23
  HybridHit,
24
24
  } from './HybridRetriever.js';
25
+
26
+ export { HnswIndex, denseSearch } from './hnsw.js';
27
+ export type { HnswOptions, SearchHit } from './hnsw.js';
@@ -12,6 +12,8 @@ import {
12
12
  BPETokenizer,
13
13
  HybridMambaModel,
14
14
  MambaTrainer,
15
+ diffCheckpoints,
16
+ applyCheckpointDiff,
15
17
  type HybridMambaModelConfig,
16
18
  type LayerSpec,
17
19
  type LayerType,
@@ -172,12 +174,21 @@ export interface SaveOptions {
172
174
  storage? : StorageTarget; // Default: 'indexedDB'
173
175
  filename? : string; // Used by 'download' and 'fileSystem'. Default: '<name>.bin'
174
176
  key? : string; // IndexedDB key override. Default: session name
177
+ /**
178
+ * Base checkpoint to diff against (EVM-6b). When provided, save persists only
179
+ * a sparse DELTA vs this base (online adapt() I/O win) instead of the full
180
+ * model. Reload with `load({ deltaBase })`. Must be an f32 checkpoint of the
181
+ * same config (e.g. a prior `await session.export()` / saved buffer).
182
+ */
183
+ deltaBase?: ArrayBuffer;
175
184
  }
176
185
 
177
186
  export interface LoadOptions {
178
187
  storage? : StorageTarget; // Default: 'indexedDB'
179
188
  url? : string; // Used when storage is 'url'
180
189
  key? : string; // IndexedDB key override. Default: session name
190
+ /** Base checkpoint to reconstruct a delta saved with `save({ deltaBase })`. (EVM-6b) */
191
+ deltaBase?: ArrayBuffer;
181
192
  }
182
193
 
183
194
  export type CreateStage = 'gpu' | 'tokenizer' | 'model' | 'weights';
@@ -552,7 +563,12 @@ export class MambaSession {
552
563
  const key = options.key ?? this._name;
553
564
  const filename = options.filename ?? `${this._name}.bin`;
554
565
 
555
- const buffer = await this._model.exportWeights();
566
+ // EVM-6b: when a base is supplied, persist only the sparse delta vs it
567
+ // (f32 both sides) — an online adapt() then writes kilobytes, not the
568
+ // whole model. Otherwise write the full checkpoint as before.
569
+ const buffer = options.deltaBase
570
+ ? diffCheckpoints(options.deltaBase, await this._model.exportWeights({ fp16: false }))
571
+ : await this._model.exportWeights();
556
572
 
557
573
  switch (storage) {
558
574
  case 'indexedDB':
@@ -608,6 +624,12 @@ export class MambaSession {
608
624
 
609
625
  if (buffer == null) return false;
610
626
 
627
+ // EVM-6b: a delta saved with `save({ deltaBase })` is reconstructed against
628
+ // the same base before loading.
629
+ if (options.deltaBase) {
630
+ buffer = applyCheckpointDiff(options.deltaBase, buffer);
631
+ }
632
+
611
633
  try {
612
634
  await this._model.loadWeights(buffer);
613
635
  } catch (err) {
@@ -15,6 +15,10 @@ import {
15
15
  EvermindLMTrainer,
16
16
  BPETokenizer,
17
17
  EvermindModelPackage,
18
+ exportEvermind,
19
+ benchmarkText,
20
+ type ExportFormat,
21
+ type BenchmarkReport,
18
22
  } from "@seanhogg/builderforce-memory-engine";
19
23
  import { EvermindCognition } from "../cognition/index.js";
20
24
  import { hybridRetrieve, chunkText } from "../retrieval/index.js";
@@ -39,6 +43,15 @@ function pNum(cfg: WorkflowStepConfig, key: string, def: number): number {
39
43
  const v = cfg.params?.[key];
40
44
  return typeof v === "number" && Number.isFinite(v) ? v : def;
41
45
  }
46
+ function pBool(cfg: WorkflowStepConfig, key: string, def: boolean): boolean {
47
+ const v = cfg.params?.[key];
48
+ return typeof v === "boolean" ? v : def;
49
+ }
50
+
51
+ /** Byte length of an emitted export file (text or binary). */
52
+ function fileBytes(data: Uint8Array | string): number {
53
+ return typeof data === "string" ? new TextEncoder().encode(data).length : data.length;
54
+ }
42
55
 
43
56
  function mkStep(cfg: WorkflowStepConfig, layer: string, defaultLabel: string, run: StackStep["run"]): StackStep {
44
57
  return { id: cfg.id, layer, label: cfg.label ?? defaultLabel, run };
@@ -214,6 +227,50 @@ export const BUILTIN_STEPS: Record<string, Registered> = {
214
227
  return `sample: "${out.slice(0, 40)}"`;
215
228
  }),
216
229
  },
230
+ benchmark: {
231
+ info: {
232
+ type: "benchmark",
233
+ layer: "BUILD",
234
+ label: "Benchmark",
235
+ description:
236
+ "Score the trained model on held-out text — perplexity, bits/token, top-1/top-k accuracy, throughput (params: evalCorpus, topK, maxPerplexity, minTop1)",
237
+ },
238
+ factory: (cfg) =>
239
+ mkStep(cfg, "BUILD", "Benchmark model (held-out)", async (ctx) => {
240
+ const model = ctx.bag.model as EvermindLM | undefined;
241
+ const tok = ctx.bag.tokenizer as BPETokenizer | undefined;
242
+ if (!model || !tok) throw new Error("no trained model — add 'train-model' first");
243
+ // Score on held-out text: the eval corpus must be text the model did not
244
+ // train on, otherwise perplexity is meaningless. Default to a small
245
+ // held-out probe distinct from the training KNOWLEDGE corpus.
246
+ const evalCorpus = pStr(
247
+ cfg,
248
+ "evalCorpus",
249
+ "Agents recall facts and act on them. The planning loop retrieves context before generating.",
250
+ );
251
+ const topK = pNum(cfg, "topK", 5);
252
+ const report: BenchmarkReport = benchmarkText(model, tok, evalCorpus, { topK });
253
+ if (report.tokens === 0) {
254
+ throw new Error("eval corpus produced no scorable tokens — provide longer held-out text");
255
+ }
256
+ ctx.bag.benchmark = report;
257
+
258
+ // Optional quality gates — only enforced when a threshold is configured.
259
+ const maxPerplexity = pNum(cfg, "maxPerplexity", 0);
260
+ if (maxPerplexity > 0 && report.perplexity > maxPerplexity) {
261
+ throw new Error(
262
+ `perplexity ${report.perplexity.toFixed(2)} exceeds max ${maxPerplexity} (model underfit — raise epochs/lr or enlarge the corpus)`,
263
+ );
264
+ }
265
+ const minTop1 = pNum(cfg, "minTop1", 0);
266
+ if (minTop1 > 0 && report.top1Accuracy < minTop1) {
267
+ throw new Error(
268
+ `top-1 accuracy ${(report.top1Accuracy * 100).toFixed(1)}% below min ${(minTop1 * 100).toFixed(0)}%`,
269
+ );
270
+ }
271
+ return `ppl ${report.perplexity.toFixed(2)}, ${report.bitsPerToken.toFixed(2)} bits/tok, top1 ${(report.top1Accuracy * 100).toFixed(0)}%, top${report.topK} ${(report.topKAccuracy * 100).toFixed(0)}% over ${report.tokens} tok`;
272
+ }),
273
+ },
217
274
  "dataset-quality": {
218
275
  info: { type: "dataset-quality", layer: "BUILD", label: "Dataset quality", description: "Validate the corpus is trainable before spending epochs (params: minSequences, minWords, maxDuplicateRatio)" },
219
276
  factory: (cfg) =>
@@ -322,4 +379,40 @@ export const BUILTIN_STEPS: Record<string, Registered> = {
322
379
  return `packaged ${blob.byteLength} bytes${tok ? " + tokenizer" : ""}`;
323
380
  }),
324
381
  },
382
+ export: {
383
+ info: {
384
+ type: "export",
385
+ layer: "BUILD",
386
+ label: "Export",
387
+ description:
388
+ "Export the trained model to a publishable format (params: format=huggingface|onnx|safetensors|gguf, name, version, license, fp16)",
389
+ },
390
+ factory: (cfg) =>
391
+ mkStep(cfg, "BUILD", "Export model (publishable)", async (ctx) => {
392
+ const model = ctx.bag.model as EvermindLM | undefined;
393
+ if (!model) throw new Error("no trained model — add 'train-model' first");
394
+ const tok = ctx.bag.tokenizer as BPETokenizer | undefined;
395
+ const format = pStr(cfg, "format", "huggingface") as ExportFormat;
396
+ if (format === "huggingface" && !tok) {
397
+ throw new Error("huggingface export needs a tokenizer — add 'train-tokenizer' first");
398
+ }
399
+ const result = exportEvermind(
400
+ model,
401
+ format,
402
+ {
403
+ name: pStr(cfg, "name", "Evermind"),
404
+ version: pStr(cfg, "version", "1.0.0"),
405
+ license: pStr(cfg, "license", "mit"),
406
+ author: pStr(cfg, "author", ""),
407
+ description: pStr(cfg, "description", "workflow-built Evermind model"),
408
+ fp16: pBool(cfg, "fp16", false),
409
+ },
410
+ tok,
411
+ );
412
+ const total = result.files.reduce((n, f) => n + fileBytes(f.data), 0);
413
+ // Land the file set as a workflow OUTPUT so a publish step / portal can ship it.
414
+ ctx.artifacts.export = { format, files: result.files, paramCount: result.paramCount };
415
+ return `exported ${format}: ${result.files.length} file(s), ${total} bytes, ${result.paramCount} params`;
416
+ }),
417
+ },
325
418
  };
@@ -55,7 +55,7 @@ const DEFAULT_CORPUS = [
55
55
  export const TRAIN_LLM: WorkflowConfig = {
56
56
  id: "train-llm",
57
57
  name: "Create an LLM",
58
- description: "Train a custom EvermindLM on your corpus, validate it end to end, and package it as a portable .evermind artifact.",
58
+ description: "Train a custom EvermindLM on your corpus, validate it end to end, package it as a portable .evermind artifact, and export a publishable Hugging Face repo (safetensors + ONNX + GGUF).",
59
59
  steps: [
60
60
  { id: "tok", type: "train-tokenizer", params: { corpus: DEFAULT_CORPUS, numMerges: 120 } },
61
61
  { id: "data", type: "dataset-quality" },
@@ -63,7 +63,9 @@ export const TRAIN_LLM: WorkflowConfig = {
63
63
  { id: "converge", type: "convergence" },
64
64
  { id: "eval", type: "evaluate" },
65
65
  { id: "gen", type: "generate-check" },
66
+ { id: "bench", type: "benchmark" },
66
67
  { id: "pkg", type: "roundtrip", params: { name: "my-llm" } },
68
+ { id: "export", type: "export", params: { format: "huggingface", name: "my-llm", version: "1.0.0" } },
67
69
  ],
68
70
  };
69
71