@seanhogg/builderforce-memory-engine 2026.9.34 → 2026.10.2

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 (189) hide show
  1. package/dist/evermind/delta_wire.d.ts +26 -2
  2. package/dist/evermind/delta_wire.d.ts.map +1 -1
  3. package/dist/evermind/delta_wire.js +46 -4
  4. package/dist/evermind/delta_wire.js.map +1 -1
  5. package/dist/evermind/index.d.ts +5 -5
  6. package/dist/evermind/index.d.ts.map +1 -1
  7. package/dist/evermind/index.js +2 -2
  8. package/dist/evermind/index.js.map +1 -1
  9. package/dist/evermind/merge.d.ts +28 -0
  10. package/dist/evermind/merge.d.ts.map +1 -1
  11. package/dist/evermind/merge.js +33 -0
  12. package/dist/evermind/merge.js.map +1 -1
  13. package/dist/evermind/ml_head.d.ts +28 -0
  14. package/dist/evermind/ml_head.d.ts.map +1 -0
  15. package/dist/evermind/ml_head.js +47 -0
  16. package/dist/evermind/ml_head.js.map +1 -0
  17. package/dist/evermind/package.d.ts +9 -0
  18. package/dist/evermind/package.d.ts.map +1 -1
  19. package/dist/evermind/package.js +20 -0
  20. package/dist/evermind/package.js.map +1 -1
  21. package/dist/index.d.ts +6 -5
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +5 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/limbic/index.d.ts +3 -0
  26. package/dist/limbic/index.d.ts.map +1 -1
  27. package/dist/limbic/index.js +3 -0
  28. package/dist/limbic/index.js.map +1 -1
  29. package/dist/limbic/limbic_model.d.ts.map +1 -1
  30. package/dist/limbic/limbic_model.js +1 -3
  31. package/dist/limbic/limbic_model.js.map +1 -1
  32. package/dist/limbic/limbic_task.d.ts +22 -0
  33. package/dist/limbic/limbic_task.d.ts.map +1 -0
  34. package/dist/limbic/limbic_task.js +45 -0
  35. package/dist/limbic/limbic_task.js.map +1 -0
  36. package/dist/limbic/limbic_trainer.d.ts +17 -30
  37. package/dist/limbic/limbic_trainer.d.ts.map +1 -1
  38. package/dist/limbic/limbic_trainer.js +38 -144
  39. package/dist/limbic/limbic_trainer.js.map +1 -1
  40. package/dist/ml/classical/anomaly_composite.d.ts +32 -0
  41. package/dist/ml/classical/anomaly_composite.d.ts.map +1 -0
  42. package/dist/ml/classical/anomaly_composite.js +29 -0
  43. package/dist/ml/classical/anomaly_composite.js.map +1 -0
  44. package/dist/ml/classical/ewma.d.ts +37 -0
  45. package/dist/ml/classical/ewma.d.ts.map +1 -0
  46. package/dist/ml/classical/ewma.js +48 -0
  47. package/dist/ml/classical/ewma.js.map +1 -0
  48. package/dist/ml/classical/gbm.d.ts +42 -0
  49. package/dist/ml/classical/gbm.d.ts.map +1 -0
  50. package/dist/ml/classical/gbm.js +117 -0
  51. package/dist/ml/classical/gbm.js.map +1 -0
  52. package/dist/ml/classical/index.d.ts +52 -0
  53. package/dist/ml/classical/index.d.ts.map +1 -0
  54. package/dist/ml/classical/index.js +49 -0
  55. package/dist/ml/classical/index.js.map +1 -0
  56. package/dist/ml/classical/isolation_forest.d.ts +37 -0
  57. package/dist/ml/classical/isolation_forest.d.ts.map +1 -0
  58. package/dist/ml/classical/isolation_forest.js +72 -0
  59. package/dist/ml/classical/isolation_forest.js.map +1 -0
  60. package/dist/ml/classical/kmeans.d.ts +31 -0
  61. package/dist/ml/classical/kmeans.d.ts.map +1 -0
  62. package/dist/ml/classical/kmeans.js +112 -0
  63. package/dist/ml/classical/kmeans.js.map +1 -0
  64. package/dist/ml/classical/knn.d.ts +51 -0
  65. package/dist/ml/classical/knn.d.ts.map +1 -0
  66. package/dist/ml/classical/knn.js +69 -0
  67. package/dist/ml/classical/knn.js.map +1 -0
  68. package/dist/ml/classical/linear.d.ts +29 -0
  69. package/dist/ml/classical/linear.d.ts.map +1 -0
  70. package/dist/ml/classical/linear.js +114 -0
  71. package/dist/ml/classical/linear.js.map +1 -0
  72. package/dist/ml/classical/logistic.d.ts +31 -0
  73. package/dist/ml/classical/logistic.d.ts.map +1 -0
  74. package/dist/ml/classical/logistic.js +111 -0
  75. package/dist/ml/classical/logistic.js.map +1 -0
  76. package/dist/ml/classical/math.d.ts +26 -0
  77. package/dist/ml/classical/math.d.ts.map +1 -0
  78. package/dist/ml/classical/math.js +90 -0
  79. package/dist/ml/classical/math.js.map +1 -0
  80. package/dist/ml/classical/naive_bayes.d.ts +28 -0
  81. package/dist/ml/classical/naive_bayes.d.ts.map +1 -0
  82. package/dist/ml/classical/naive_bayes.js +72 -0
  83. package/dist/ml/classical/naive_bayes.js.map +1 -0
  84. package/dist/ml/classical/robust_z.d.ts +38 -0
  85. package/dist/ml/classical/robust_z.d.ts.map +1 -0
  86. package/dist/ml/classical/robust_z.js +47 -0
  87. package/dist/ml/classical/robust_z.js.map +1 -0
  88. package/dist/ml/dense_head.d.ts +33 -0
  89. package/dist/ml/dense_head.d.ts.map +1 -0
  90. package/dist/ml/dense_head.js +140 -0
  91. package/dist/ml/dense_head.js.map +1 -0
  92. package/dist/ml/eval_loss.d.ts +15 -0
  93. package/dist/ml/eval_loss.d.ts.map +1 -0
  94. package/dist/ml/eval_loss.js +20 -0
  95. package/dist/ml/eval_loss.js.map +1 -0
  96. package/dist/ml/features.d.ts +25 -0
  97. package/dist/ml/features.d.ts.map +1 -0
  98. package/dist/ml/features.js +46 -0
  99. package/dist/ml/features.js.map +1 -0
  100. package/dist/ml/federate.d.ts +56 -0
  101. package/dist/ml/federate.d.ts.map +1 -0
  102. package/dist/ml/federate.js +99 -0
  103. package/dist/ml/federate.js.map +1 -0
  104. package/dist/ml/head_trainer.d.ts +78 -0
  105. package/dist/ml/head_trainer.d.ts.map +1 -0
  106. package/dist/ml/head_trainer.js +182 -0
  107. package/dist/ml/head_trainer.js.map +1 -0
  108. package/dist/ml/index.d.ts +23 -0
  109. package/dist/ml/index.d.ts.map +1 -0
  110. package/dist/ml/index.js +11 -0
  111. package/dist/ml/index.js.map +1 -0
  112. package/dist/ml/registry.d.ts +20 -0
  113. package/dist/ml/registry.d.ts.map +1 -0
  114. package/dist/ml/registry.js +43 -0
  115. package/dist/ml/registry.js.map +1 -0
  116. package/dist/ml/run.d.ts +103 -0
  117. package/dist/ml/run.d.ts.map +1 -0
  118. package/dist/ml/run.js +169 -0
  119. package/dist/ml/run.js.map +1 -0
  120. package/dist/ml/tasks/experience.d.ts +127 -0
  121. package/dist/ml/tasks/experience.d.ts.map +1 -0
  122. package/dist/ml/tasks/experience.js +213 -0
  123. package/dist/ml/tasks/experience.js.map +1 -0
  124. package/dist/ml/tasks/index.d.ts +10 -0
  125. package/dist/ml/tasks/index.d.ts.map +1 -0
  126. package/dist/ml/tasks/index.js +28 -0
  127. package/dist/ml/tasks/index.js.map +1 -0
  128. package/dist/ml/tasks/local_success.d.ts +55 -0
  129. package/dist/ml/tasks/local_success.d.ts.map +1 -0
  130. package/dist/ml/tasks/local_success.js +77 -0
  131. package/dist/ml/tasks/local_success.js.map +1 -0
  132. package/dist/ml/tasks/run_anomaly.d.ts +52 -0
  133. package/dist/ml/tasks/run_anomaly.d.ts.map +1 -0
  134. package/dist/ml/tasks/run_anomaly.js +50 -0
  135. package/dist/ml/tasks/run_anomaly.js.map +1 -0
  136. package/dist/ml/train.d.ts +59 -0
  137. package/dist/ml/train.d.ts.map +1 -0
  138. package/dist/ml/train.js +84 -0
  139. package/dist/ml/train.js.map +1 -0
  140. package/dist/ml/types.d.ts +115 -0
  141. package/dist/ml/types.d.ts.map +1 -0
  142. package/dist/ml/types.js +24 -0
  143. package/dist/ml/types.js.map +1 -0
  144. package/dist/moe/moe_model.d.ts.map +1 -1
  145. package/dist/moe/moe_model.js +1 -16
  146. package/dist/moe/moe_model.js.map +1 -1
  147. package/dist/training/cpu_ops.d.ts +2 -0
  148. package/dist/training/cpu_ops.d.ts.map +1 -1
  149. package/dist/training/cpu_ops.js +16 -0
  150. package/dist/training/cpu_ops.js.map +1 -1
  151. package/package.json +1 -1
  152. package/src/evermind/delta_wire.ts +63 -4
  153. package/src/evermind/index.ts +7 -3
  154. package/src/evermind/merge.ts +59 -0
  155. package/src/evermind/ml_head.ts +63 -0
  156. package/src/evermind/package.ts +24 -0
  157. package/src/index.ts +17 -1
  158. package/src/limbic/index.ts +5 -0
  159. package/src/limbic/limbic_model.ts +1 -4
  160. package/src/limbic/limbic_task.ts +62 -0
  161. package/src/limbic/limbic_trainer.ts +41 -208
  162. package/src/ml/classical/anomaly_composite.ts +46 -0
  163. package/src/ml/classical/ewma.ts +74 -0
  164. package/src/ml/classical/gbm.ts +144 -0
  165. package/src/ml/classical/index.ts +82 -0
  166. package/src/ml/classical/isolation_forest.ts +88 -0
  167. package/src/ml/classical/kmeans.ts +120 -0
  168. package/src/ml/classical/knn.ts +102 -0
  169. package/src/ml/classical/linear.ts +119 -0
  170. package/src/ml/classical/logistic.ts +125 -0
  171. package/src/ml/classical/math.ts +93 -0
  172. package/src/ml/classical/naive_bayes.ts +84 -0
  173. package/src/ml/classical/robust_z.ts +74 -0
  174. package/src/ml/dense_head.ts +157 -0
  175. package/src/ml/eval_loss.ts +27 -0
  176. package/src/ml/features.ts +61 -0
  177. package/src/ml/federate.ts +130 -0
  178. package/src/ml/head_trainer.ts +247 -0
  179. package/src/ml/index.ts +57 -0
  180. package/src/ml/registry.ts +44 -0
  181. package/src/ml/run.ts +222 -0
  182. package/src/ml/tasks/experience.ts +293 -0
  183. package/src/ml/tasks/index.ts +30 -0
  184. package/src/ml/tasks/local_success.ts +118 -0
  185. package/src/ml/tasks/run_anomaly.ts +84 -0
  186. package/src/ml/train.ts +120 -0
  187. package/src/ml/types.ts +135 -0
  188. package/src/moe/moe_model.ts +1 -13
  189. package/src/training/cpu_ops.ts +14 -0
@@ -0,0 +1,247 @@
1
+ /**
2
+ * ml/head_trainer.ts — HeadTrainer: the ONE gradient trainer for a small head over
3
+ * an embedding (extracted from the limbic affect trainer, which is now a thin
4
+ * registration over it).
5
+ *
6
+ * Full-batch gradient descent with AdamW. When a GPUDevice is supplied the
7
+ * optimiser step runs on the GPU via the shared WEIGHT_UPDATE_WGSL kernel; with
8
+ * no device it runs the kernel's CPU mirror, `adamwUpdateInPlace` (`optim/adamw.ts`,
9
+ * NaN/Inf guard included), so training works everywhere (CI, Node without
10
+ * @webgpu/node, a laptop with no GPU). Neither path bounds the step (maxDelta 0).
11
+ *
12
+ * The head owns its forward/backward and its loss (MSE for regression, BCE /
13
+ * softmax cross-entropy for classification); this file owns only the loop:
14
+ * zero → accumulate per sample → average → clip → AdamW step.
15
+ */
16
+
17
+ import {
18
+ createUniformBuffer,
19
+ createStorageBuffer,
20
+ createComputePipeline,
21
+ createBindGroup,
22
+ dispatchKernel,
23
+ readBuffer,
24
+ cdiv,
25
+ } from "../utils/gpu_utils.js";
26
+ import { WEIGHT_UPDATE_WGSL } from "../kernels/weight_update.js";
27
+ import { adamwUpdateInPlace, type AdamWKernelStep } from "../optim/adamw.js";
28
+
29
+ /** A named trainable tensor (flat row-major Float32Array). */
30
+ export interface HeadParam {
31
+ name: string;
32
+ data: Float32Array;
33
+ numel: number;
34
+ }
35
+
36
+ /** What a head must expose to be trained by {@link HeadTrainer}. */
37
+ export interface TrainableHead<S> {
38
+ /** Trainable parameters, in a stable order. */
39
+ parameters(): HeadParam[];
40
+ /** Gradient buffers, index-aligned with {@link parameters}. */
41
+ gradients(): HeadParam[];
42
+ zeroGrad(): void;
43
+ /** Accumulate gradients for one sample into {@link gradients}; returns its loss. */
44
+ accumulate(sample: S): number;
45
+ /** Forward-only loss of one sample. */
46
+ loss(sample: S): number;
47
+ }
48
+
49
+ export interface HeadTrainOptions {
50
+ learningRate?: number;
51
+ epochs?: number;
52
+ weightDecay?: number;
53
+ beta1?: number;
54
+ beta2?: number;
55
+ eps?: number;
56
+ /** Max global gradient L2 norm before the optimiser step. Default 1.0. */
57
+ maxGradNorm?: number;
58
+ /** Per-epoch hook. `gradNorm` is the pre-clip gradient L2 norm this epoch (the
59
+ * instability early-warning) — computed CPU-side so it's always available. */
60
+ onEpochEnd?: ((epoch: number, loss: number, gradNorm?: number) => void) | null;
61
+ }
62
+
63
+ interface AdamMoment {
64
+ m: Float32Array;
65
+ v: Float32Array;
66
+ }
67
+
68
+ /** Byte size of WEIGHT_UPDATE_WGSL's `AdamParams` uniform: 1 u32 + 8 f32 + 3 f32 pad = 48. */
69
+ const ADAM_PARAMS_BYTES = 48;
70
+
71
+ /**
72
+ * Pack `AdamParams` exactly as WEIGHT_UPDATE_WGSL declares it:
73
+ * `num_elements: u32, lr, beta1, beta2, eps, weight_decay, beta1_t, beta2_t,
74
+ * max_delta, _pad0, _pad1, _pad2` (all f32 after the first) — 48 bytes.
75
+ */
76
+ function packAdamParams(numElements: number, hp: AdamWKernelStep): ArrayBuffer {
77
+ const buf = new ArrayBuffer(ADAM_PARAMS_BYTES);
78
+ new Uint32Array(buf, 0, 1).set([numElements]);
79
+ // Pads stay 0 (fresh ArrayBuffer).
80
+ new Float32Array(buf, 4, 8).set([hp.lr, hp.beta1, hp.beta2, hp.eps, hp.weightDecay, hp.beta1_t, hp.beta2_t, hp.maxDelta]);
81
+ return buf;
82
+ }
83
+
84
+ export class HeadTrainer<S> {
85
+ readonly head: TrainableHead<S>;
86
+ readonly device: GPUDevice | null;
87
+ private _moments: AdamMoment[] | null = null;
88
+ private _step = 0;
89
+ private readonly _adamwPipeline: GPUComputePipeline | null;
90
+
91
+ constructor(
92
+ head: TrainableHead<S>,
93
+ device: GPUDevice | null = null,
94
+ /** Name used in error messages (the registration that owns this trainer). */
95
+ private readonly label: string = "HeadTrainer",
96
+ ) {
97
+ this.head = head;
98
+ this.device = device;
99
+ this._adamwPipeline = device
100
+ ? createComputePipeline(device, WEIGHT_UPDATE_WGSL, "adamw_update")
101
+ : null;
102
+ }
103
+
104
+ /** Whether the optimiser step runs on the GPU. */
105
+ get gpuTraining(): boolean {
106
+ return this.device != null && this._adamwPipeline != null;
107
+ }
108
+
109
+ private _initMoments(): void {
110
+ if (this._moments) return;
111
+ this._moments = this.head.parameters().map((p) => ({
112
+ m: new Float32Array(p.numel),
113
+ v: new Float32Array(p.numel),
114
+ }));
115
+ }
116
+
117
+ /**
118
+ * Train on `samples` for `epochs` passes; returns the per-epoch mean loss.
119
+ * Full-batch: grads accumulate over every sample, are averaged, clipped, then
120
+ * applied once per epoch.
121
+ */
122
+ async train(samples: S[], opts: HeadTrainOptions = {}): Promise<number[]> {
123
+ if (samples.length === 0) throw new Error(`${this.label}.train: no samples`);
124
+ const {
125
+ learningRate = 0.05,
126
+ epochs = 50,
127
+ weightDecay = 0.0,
128
+ beta1 = 0.9,
129
+ beta2 = 0.999,
130
+ eps = 1e-8,
131
+ maxGradNorm = 1.0,
132
+ onEpochEnd = null,
133
+ } = opts;
134
+
135
+ this._initMoments();
136
+ const losses: number[] = [];
137
+
138
+ for (let epoch = 0; epoch < epochs; epoch++) {
139
+ this.head.zeroGrad();
140
+ let epochLoss = 0;
141
+ for (const s of samples) epochLoss += this.head.accumulate(s);
142
+
143
+ const grads = this.head.gradients();
144
+ const invN = 1 / samples.length;
145
+ for (const g of grads) {
146
+ for (let i = 0; i < g.data.length; i++) g.data[i]! *= invN;
147
+ }
148
+
149
+ const gradNorm = clipGradients(grads, maxGradNorm);
150
+
151
+ this._step++;
152
+ const beta1_t = Math.pow(beta1, this._step);
153
+ const beta2_t = Math.pow(beta2, this._step);
154
+ const hp: AdamWKernelStep = { lr: learningRate, weightDecay, beta1, beta2, eps, beta1_t, beta2_t, maxDelta: 0 };
155
+ if (this.gpuTraining) {
156
+ await this._adamwStepGpu(grads, hp);
157
+ } else {
158
+ this._adamwStepCpu(grads, hp);
159
+ }
160
+
161
+ const avg = epochLoss / samples.length;
162
+ losses.push(avg);
163
+ if (onEpochEnd) onEpochEnd(epoch + 1, avg, gradNorm);
164
+ }
165
+ return losses;
166
+ }
167
+
168
+ /** Mean loss over samples (no weight update). 0 for an empty set. */
169
+ evaluate(samples: S[]): number {
170
+ if (samples.length === 0) return 0;
171
+ let total = 0;
172
+ for (const s of samples) total += this.head.loss(s);
173
+ return total / samples.length;
174
+ }
175
+
176
+ /** AdamW on the CPU via the kernel's shared mirror (`adamwUpdateInPlace`). */
177
+ private _adamwStepCpu(grads: HeadParam[], hp: AdamWKernelStep): void {
178
+ const params = this.head.parameters();
179
+ for (let pi = 0; pi < params.length; pi++) {
180
+ const mom = this._moments![pi]!;
181
+ adamwUpdateInPlace(params[pi]!.data, grads[pi]!.data, mom.m, mom.v, hp);
182
+ }
183
+ }
184
+
185
+ /** AdamW on the GPU via the shared WEIGHT_UPDATE_WGSL kernel. Awaited per step. */
186
+ private async _adamwStepGpu(grads: HeadParam[], hp: AdamWKernelStep): Promise<void> {
187
+ const device = this.device!;
188
+ const pipeline = this._adamwPipeline!;
189
+ const params = this.head.parameters();
190
+
191
+ for (let pi = 0; pi < params.length; pi++) {
192
+ const p = params[pi]!;
193
+ const mom = this._moments![pi]!;
194
+ const paramBuf = createStorageBuffer(device, p.data, true);
195
+ const gradBuf = createStorageBuffer(device, grads[pi]!.data, false);
196
+ const mBuf = createStorageBuffer(device, mom.m, true);
197
+ const vBuf = createStorageBuffer(device, mom.v, true);
198
+ const uni = createUniformBuffer(device, packAdamParams(p.numel, hp));
199
+ const bg = createBindGroup(device, pipeline, [uni, paramBuf, gradBuf, mBuf, vBuf]);
200
+ dispatchKernel(device, pipeline, bg, [cdiv(p.numel, 256), 1, 1]);
201
+
202
+ p.data.set((await readBuffer(device, paramBuf, p.numel * 4)).subarray(0, p.numel));
203
+ mom.m.set((await readBuffer(device, mBuf, p.numel * 4)).subarray(0, p.numel));
204
+ mom.v.set((await readBuffer(device, vBuf, p.numel * 4)).subarray(0, p.numel));
205
+
206
+ paramBuf.destroy();
207
+ gradBuf.destroy();
208
+ mBuf.destroy();
209
+ vBuf.destroy();
210
+ uni.destroy();
211
+ }
212
+ }
213
+ }
214
+
215
+ /** Clip grads to `maxNorm` in place; returns the PRE-clip L2 norm (the signal). */
216
+ function clipGradients(grads: HeadParam[], maxNorm: number): number {
217
+ let normSq = 0;
218
+ for (const g of grads) for (let i = 0; i < g.data.length; i++) normSq += g.data[i]! * g.data[i]!;
219
+ const norm = Math.sqrt(normSq);
220
+ if (norm > maxNorm && norm > 0) {
221
+ const scale = maxNorm / norm;
222
+ for (const g of grads) for (let i = 0; i < g.data.length; i++) g.data[i]! *= scale;
223
+ }
224
+ return norm;
225
+ }
226
+
227
+ /** Concatenate a head's parameters into one flat vector (the federation unit). */
228
+ export function flattenParams(params: readonly HeadParam[]): Float32Array {
229
+ const out = new Float32Array(params.reduce((n, p) => n + p.numel, 0));
230
+ let o = 0;
231
+ for (const p of params) {
232
+ out.set(p.data, o);
233
+ o += p.numel;
234
+ }
235
+ return out;
236
+ }
237
+
238
+ /** Write a flat vector back into a head's parameters (inverse of {@link flattenParams}). */
239
+ export function assignParams(params: readonly HeadParam[], flat: ArrayLike<number>): void {
240
+ const total = params.reduce((n, p) => n + p.numel, 0);
241
+ if (flat.length !== total) throw new Error(`assignParams: expected ${total} values, got ${flat.length}`);
242
+ let o = 0;
243
+ for (const p of params) {
244
+ for (let i = 0; i < p.numel; i++) p.data[i] = flat[o + i]!;
245
+ o += p.numel;
246
+ }
247
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * ml — the ML task catalog (docs/ml-task-catalog.md): small, purpose-built models
3
+ * for bounded questions, answered on the person's own machine without a GPU or an
4
+ * LLM call. ONE registry, ONE runner, ONE trainer seam; trained heads federate on
5
+ * the existing delta wire. Pure and portable: no vscode, no DOM, no network.
6
+ */
7
+ export type {
8
+ MlTask,
9
+ MlTaskKind,
10
+ MlEngineRef,
11
+ ClassicalAlgorithm,
12
+ InferenceMode,
13
+ MlPrediction,
14
+ MlPredictor,
15
+ FeatureSpec,
16
+ MlSample,
17
+ TrainSpec,
18
+ OutputSpec,
19
+ } from "./types.js";
20
+ export { engineKey, ML_TASK_ID_RE } from "./types.js";
21
+
22
+ export { MlTaskRegistry } from "./registry.js";
23
+ export {
24
+ runMlTask,
25
+ candidateEngines,
26
+ InMemoryMlModels,
27
+ MlDecisionLog,
28
+ MlEngineUnavailableError,
29
+ } from "./run.js";
30
+ export type { MlRunContext, MlResult, MlModelStore, MlCloudPort, MlDecision, MlDecisionSink, MlSkipReason } from "./run.js";
31
+
32
+ export { trainMlTask, mlEvalLoss } from "./train.js";
33
+ export type { MlTrainExample, MlTrainOptions, MlTrainResult } from "./train.js";
34
+
35
+ export { HeadTrainer, flattenParams, assignParams } from "./head_trainer.js";
36
+ export type { HeadParam, TrainableHead, HeadTrainOptions } from "./head_trainer.js";
37
+ export { DenseHead } from "./dense_head.js";
38
+ export type { DenseHeadConfig } from "./dense_head.js";
39
+
40
+ export { extractFeatures, extractFeatureMatrix, logScale, clampRatio } from "./features.js";
41
+
42
+ export * from "./classical/index.js";
43
+
44
+ export {
45
+ headCheckpoint,
46
+ headParams,
47
+ mlHeadEntry,
48
+ mlHeadParams,
49
+ mlHeadUnusableReason,
50
+ packageMlHead,
51
+ withPackageMlHead,
52
+ buildMlHeadDelta,
53
+ mergeMlHeadDeltas,
54
+ } from "./federate.js";
55
+ export type { EvermindMlHead, MlHeadMergeOutcome } from "./federate.js";
56
+
57
+ export * from "./tasks/index.js";
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ml/registry.ts — the ONE registry of ML tasks, keyed by task id.
3
+ *
4
+ * A new task is a new row, not a new module. The registry only stores and looks
5
+ * up rows; running lives in `run.ts`, training in `train.ts`.
6
+ */
7
+ import { ML_TASK_ID_RE, type MlTask } from "./types.js";
8
+
9
+ export class MlTaskRegistry {
10
+ private readonly _tasks = new Map<string, MlTask<any, any>>();
11
+
12
+ /** Register a row. Throws on a malformed id, a duplicate id, or a row with no engine. */
13
+ register<I, O>(task: MlTask<I, O>): MlTask<I, O> {
14
+ if (!ML_TASK_ID_RE.test(task.id)) throw new Error(`MlTaskRegistry: malformed task id '${task.id}'`);
15
+ if (this._tasks.has(task.id)) throw new Error(`MlTaskRegistry: task '${task.id}' is already registered`);
16
+ if (task.engines.length === 0) throw new Error(`MlTaskRegistry: task '${task.id}' lists no engine`);
17
+ if (task.kind === "classify" && !(task.output.labels && task.output.labels.length >= 2)) {
18
+ throw new Error(`MlTaskRegistry: classify task '${task.id}' needs at least two output labels`);
19
+ }
20
+ this._tasks.set(task.id, task);
21
+ return task;
22
+ }
23
+
24
+ /** The row for `id`, or undefined. */
25
+ get<I = unknown, O = unknown>(id: string): MlTask<I, O> | undefined {
26
+ return this._tasks.get(id) as MlTask<I, O> | undefined;
27
+ }
28
+
29
+ /** The row for `id`; throws when it is not registered. */
30
+ require<I = unknown, O = unknown>(id: string): MlTask<I, O> {
31
+ const t = this.get<I, O>(id);
32
+ if (!t) throw new Error(`MlTaskRegistry: unknown task '${id}'`);
33
+ return t;
34
+ }
35
+
36
+ has(id: string): boolean {
37
+ return this._tasks.has(id);
38
+ }
39
+
40
+ /** Every row, in registration order. */
41
+ list(): MlTask<any, any>[] {
42
+ return [...this._tasks.values()];
43
+ }
44
+ }
package/src/ml/run.ts ADDED
@@ -0,0 +1,222 @@
1
+ /**
2
+ * ml/run.ts — the ONE runner: `runMlTask(id, input, ctx)`.
3
+ *
4
+ * Picks the first engine the inference mode allows that can answer (Local only:
5
+ * local tiers; Hybrid: local first, then the host's cloud port; Specific: exactly
6
+ * the named engine), returns `{ output, confidence, engine }`, and records the
7
+ * decision so routing can be evaluated later. The cloud is a PORT the host
8
+ * supplies — nothing here touches the network.
9
+ */
10
+ import { extractFeatures, type TextEmbedder } from "./features.js";
11
+ import type { MlTaskRegistry } from "./registry.js";
12
+ import { engineKey, type InferenceMode, type MlEngineRef, type MlPrediction, type MlPredictor, type MlTask } from "./types.js";
13
+
14
+ /** Fitted local models, per (task id, engine key). */
15
+ export interface MlModelStore {
16
+ get(taskId: string, engine: string): MlPredictor | undefined;
17
+ set(taskId: string, engine: string, predictor: MlPredictor): void;
18
+ }
19
+
20
+ /** The plain in-process store. */
21
+ export class InMemoryMlModels implements MlModelStore {
22
+ private readonly _m = new Map<string, MlPredictor>();
23
+ get(taskId: string, engine: string): MlPredictor | undefined {
24
+ return this._m.get(`${taskId}|${engine}`);
25
+ }
26
+ set(taskId: string, engine: string, predictor: MlPredictor): void {
27
+ this._m.set(`${taskId}|${engine}`, predictor);
28
+ }
29
+ }
30
+
31
+ /** A cloud engine, supplied by the host (Builderforce.ai passes its gateway client). */
32
+ export interface MlCloudPort {
33
+ run(request: { taskId: string; engine: MlEngineRef; input: unknown }): Promise<MlPrediction>;
34
+ }
35
+
36
+ /** Why the runner moved past an engine. */
37
+ export type MlSkipReason = "unavailable" | "low-confidence" | "error";
38
+
39
+ /** One routing decision, recorded for eval. */
40
+ export interface MlDecision {
41
+ taskId: string;
42
+ mode: InferenceMode;
43
+ /** The engine that answered (key), or null when none could. */
44
+ engine: string | null;
45
+ tier: "local" | "cloud" | null;
46
+ confidence: number | null;
47
+ /** Engines passed over before the answer, with why. */
48
+ skipped: { engine: string; reason: MlSkipReason; detail?: string }[];
49
+ }
50
+
51
+ export interface MlDecisionSink {
52
+ record(decision: MlDecision): void;
53
+ }
54
+
55
+ /** A bounded in-memory decision log with a per-task summary. */
56
+ export class MlDecisionLog implements MlDecisionSink {
57
+ private readonly _d: MlDecision[] = [];
58
+ constructor(readonly capacity = 500) {}
59
+
60
+ record(decision: MlDecision): void {
61
+ this._d.push(decision);
62
+ if (this._d.length > this.capacity) this._d.splice(0, this._d.length - this.capacity);
63
+ }
64
+
65
+ list(): readonly MlDecision[] {
66
+ return this._d;
67
+ }
68
+
69
+ /** Per task: how many runs, which engines answered, mean confidence, unanswered count. */
70
+ summary(): Record<string, { runs: number; byEngine: Record<string, number>; unanswered: number; meanConfidence: number | null }> {
71
+ const out: Record<string, { runs: number; byEngine: Record<string, number>; unanswered: number; meanConfidence: number | null }> = {};
72
+ const sums: Record<string, { s: number; n: number }> = {};
73
+ for (const d of this._d) {
74
+ const t = (out[d.taskId] ??= { runs: 0, byEngine: {}, unanswered: 0, meanConfidence: null });
75
+ t.runs++;
76
+ if (d.engine == null) {
77
+ t.unanswered++;
78
+ continue;
79
+ }
80
+ t.byEngine[d.engine] = (t.byEngine[d.engine] ?? 0) + 1;
81
+ const c = (sums[d.taskId] ??= { s: 0, n: 0 });
82
+ c.s += d.confidence ?? 0;
83
+ c.n++;
84
+ t.meanConfidence = c.s / c.n;
85
+ }
86
+ return out;
87
+ }
88
+ }
89
+
90
+ export interface MlRunContext {
91
+ registry: MlTaskRegistry;
92
+ mode: InferenceMode;
93
+ /** For `specific`: the engine (ref or key) to use. */
94
+ engine?: MlEngineRef | string;
95
+ models?: MlModelStore;
96
+ embedder?: TextEmbedder;
97
+ cloud?: MlCloudPort;
98
+ /** Hybrid only: a local answer below this confidence defers to the cloud when it can answer. */
99
+ minConfidence?: number;
100
+ decisions?: MlDecisionSink;
101
+ }
102
+
103
+ export interface MlResult<O> {
104
+ output: O;
105
+ confidence: number;
106
+ engine: MlEngineRef;
107
+ }
108
+
109
+ /** No engine allowed by the mode could answer. */
110
+ export class MlEngineUnavailableError extends Error {
111
+ constructor(
112
+ readonly taskId: string,
113
+ readonly decision: MlDecision,
114
+ ) {
115
+ super(
116
+ `runMlTask(${taskId}): no engine could answer in '${decision.mode}' mode (${
117
+ decision.skipped.map((s) => `${s.engine}: ${s.reason}${s.detail ? ` (${s.detail})` : ""}`).join(", ") || "none allowed"
118
+ })`,
119
+ );
120
+ this.name = "MlEngineUnavailableError";
121
+ }
122
+ }
123
+
124
+ /**
125
+ * The task's engines the mode allows, in the order the runner tries them. Exhaustive:
126
+ * a mode string outside {@link InferenceMode} (a host passing a stale or mistyped
127
+ * setting) throws instead of being read as `specific`.
128
+ */
129
+ export function candidateEngines(task: MlTask<any, any>, mode: InferenceMode, specific?: MlEngineRef | string): MlEngineRef[] {
130
+ switch (mode) {
131
+ case "local-only":
132
+ return task.engines.filter((e) => e.tier === "local");
133
+ case "hybrid":
134
+ return [...task.engines.filter((e) => e.tier === "local"), ...task.engines.filter((e) => e.tier === "cloud")];
135
+ case "specific": {
136
+ if (specific == null) throw new Error(`runMlTask(${task.id}): 'specific' mode needs ctx.engine`);
137
+ const key = typeof specific === "string" ? specific : engineKey(specific);
138
+ const match = task.engines.filter((e) => engineKey(e) === key);
139
+ if (match.length === 0) throw new Error(`runMlTask(${task.id}): engine '${key}' is not one of this task's engines`);
140
+ return match;
141
+ }
142
+ default: {
143
+ const unknown: never = mode;
144
+ throw new Error(`runMlTask(${task.id}): unknown inference mode '${String(unknown)}'`);
145
+ }
146
+ }
147
+ }
148
+
149
+ const errorDetail = (error: unknown): string => (error instanceof Error ? error.message : String(error));
150
+
151
+ /**
152
+ * Answer task `id` for `input` with the first engine the mode allows that can. An
153
+ * engine that throws (feature extraction or its own predict) is recorded as an
154
+ * `error` skip and the next allowed engine is tried — in Hybrid, the cloud. Throws
155
+ * {@link MlEngineUnavailableError}, carrying every engine's reason, only when none answers.
156
+ */
157
+ export async function runMlTask<O = unknown, I = unknown>(id: string, input: I, ctx: MlRunContext): Promise<MlResult<O>> {
158
+ const task = ctx.registry.require<I, O>(id);
159
+ const engines = candidateEngines(task, ctx.mode, ctx.engine);
160
+ const skipped: MlDecision["skipped"] = [];
161
+ let features: Float32Array | null = null;
162
+ let featureError: unknown = null;
163
+ let deferred: { ref: MlEngineRef; p: MlPrediction } | null = null;
164
+
165
+ const finish = (ref: MlEngineRef, p: MlPrediction): MlResult<O> => {
166
+ ctx.decisions?.record({ taskId: id, mode: ctx.mode, engine: engineKey(ref), tier: ref.tier, confidence: p.confidence, skipped });
167
+ return { output: task.output.decode(p), confidence: p.confidence, engine: ref };
168
+ };
169
+
170
+ for (let i = 0; i < engines.length; i++) {
171
+ const ref = engines[i]!;
172
+ const key = engineKey(ref);
173
+ if (ref.tier === "local") {
174
+ const predictor = ctx.models?.get(id, key);
175
+ if (!predictor) {
176
+ skipped.push({ engine: key, reason: "unavailable" });
177
+ continue;
178
+ }
179
+ // Extract once; a failure (e.g. no embedder) fails every local engine alike, so
180
+ // it is remembered rather than retried per engine.
181
+ if (!features && featureError == null) {
182
+ try {
183
+ features = await extractFeatures(task.features, input, ctx.embedder);
184
+ } catch (error) {
185
+ featureError = error;
186
+ }
187
+ }
188
+ if (!features) {
189
+ skipped.push({ engine: key, reason: "error", detail: `features: ${errorDetail(featureError)}` });
190
+ continue;
191
+ }
192
+ let p: MlPrediction;
193
+ try {
194
+ p = predictor.predict(features);
195
+ } catch (error) {
196
+ skipped.push({ engine: key, reason: "error", detail: errorDetail(error) });
197
+ continue;
198
+ }
199
+ const cloudLater = ctx.mode === "hybrid" && !!ctx.cloud && engines.slice(i + 1).some((e) => e.tier === "cloud");
200
+ if (ctx.minConfidence != null && p.confidence < ctx.minConfidence && cloudLater) {
201
+ skipped.push({ engine: key, reason: "low-confidence", detail: p.confidence.toFixed(3) });
202
+ deferred ??= { ref, p };
203
+ continue;
204
+ }
205
+ return finish(ref, p);
206
+ }
207
+ if (!ctx.cloud) {
208
+ skipped.push({ engine: key, reason: "unavailable" });
209
+ continue;
210
+ }
211
+ try {
212
+ return finish(ref, await ctx.cloud.run({ taskId: id, engine: ref, input }));
213
+ } catch (error) {
214
+ skipped.push({ engine: key, reason: "error", detail: errorDetail(error) });
215
+ }
216
+ }
217
+ // The cloud could not answer after a low-confidence local one: the local answer stands.
218
+ if (deferred) return finish(deferred.ref, deferred.p);
219
+ const decision: MlDecision = { taskId: id, mode: ctx.mode, engine: null, tier: null, confidence: null, skipped };
220
+ ctx.decisions?.record(decision);
221
+ throw new MlEngineUnavailableError(id, decision);
222
+ }