@seanhogg/builderforce-memory-engine 2026.10.1 → 2026.10.3

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 (149) hide show
  1. package/dist/evermind/adapt.d.ts +7 -0
  2. package/dist/evermind/adapt.d.ts.map +1 -1
  3. package/dist/evermind/adapt.js +1 -1
  4. package/dist/evermind/adapt.js.map +1 -1
  5. package/dist/evermind/delta_wire.d.ts +16 -1
  6. package/dist/evermind/delta_wire.d.ts.map +1 -1
  7. package/dist/evermind/delta_wire.js +22 -2
  8. package/dist/evermind/delta_wire.js.map +1 -1
  9. package/dist/evermind/index.d.ts +2 -2
  10. package/dist/evermind/index.d.ts.map +1 -1
  11. package/dist/evermind/index.js +1 -1
  12. package/dist/evermind/index.js.map +1 -1
  13. package/dist/evermind/merge.d.ts +5 -2
  14. package/dist/evermind/merge.d.ts.map +1 -1
  15. package/dist/evermind/merge.js +5 -2
  16. package/dist/evermind/merge.js.map +1 -1
  17. package/dist/evermind/ml_head.d.ts +28 -0
  18. package/dist/evermind/ml_head.d.ts.map +1 -0
  19. package/dist/evermind/ml_head.js +47 -0
  20. package/dist/evermind/ml_head.js.map +1 -0
  21. package/dist/evermind/package.d.ts +2 -2
  22. package/dist/evermind/package.d.ts.map +1 -1
  23. package/dist/evermind/package.js +1 -1
  24. package/dist/evermind/package.js.map +1 -1
  25. package/dist/experience/index.d.ts +2 -2
  26. package/dist/experience/index.d.ts.map +1 -1
  27. package/dist/experience/index.js.map +1 -1
  28. package/dist/experience/store.d.ts +19 -1
  29. package/dist/experience/store.d.ts.map +1 -1
  30. package/dist/experience/store.js +14 -2
  31. package/dist/experience/store.js.map +1 -1
  32. package/dist/experience/types.d.ts +18 -0
  33. package/dist/experience/types.d.ts.map +1 -1
  34. package/dist/experience/types.js.map +1 -1
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/kernels/weight_update.d.ts.map +1 -1
  40. package/dist/kernels/weight_update.js +12 -0
  41. package/dist/kernels/weight_update.js.map +1 -1
  42. package/dist/limbic/limbic_model.d.ts.map +1 -1
  43. package/dist/limbic/limbic_model.js +1 -3
  44. package/dist/limbic/limbic_model.js.map +1 -1
  45. package/dist/ml/classical/anomaly_composite.d.ts +32 -0
  46. package/dist/ml/classical/anomaly_composite.d.ts.map +1 -0
  47. package/dist/ml/classical/anomaly_composite.js +29 -0
  48. package/dist/ml/classical/anomaly_composite.js.map +1 -0
  49. package/dist/ml/classical/ewma.d.ts +7 -1
  50. package/dist/ml/classical/ewma.d.ts.map +1 -1
  51. package/dist/ml/classical/ewma.js +6 -4
  52. package/dist/ml/classical/ewma.js.map +1 -1
  53. package/dist/ml/classical/gbm.d.ts.map +1 -1
  54. package/dist/ml/classical/gbm.js +2 -1
  55. package/dist/ml/classical/gbm.js.map +1 -1
  56. package/dist/ml/classical/index.d.ts +6 -2
  57. package/dist/ml/classical/index.d.ts.map +1 -1
  58. package/dist/ml/classical/index.js +5 -1
  59. package/dist/ml/classical/index.js.map +1 -1
  60. package/dist/ml/classical/kmeans.d.ts.map +1 -1
  61. package/dist/ml/classical/kmeans.js +23 -4
  62. package/dist/ml/classical/kmeans.js.map +1 -1
  63. package/dist/ml/classical/knn.d.ts +15 -5
  64. package/dist/ml/classical/knn.d.ts.map +1 -1
  65. package/dist/ml/classical/knn.js +28 -7
  66. package/dist/ml/classical/knn.js.map +1 -1
  67. package/dist/ml/classical/logistic.d.ts +1 -9
  68. package/dist/ml/classical/logistic.d.ts.map +1 -1
  69. package/dist/ml/classical/logistic.js +17 -9
  70. package/dist/ml/classical/logistic.js.map +1 -1
  71. package/dist/ml/classical/naive_bayes.d.ts +5 -1
  72. package/dist/ml/classical/naive_bayes.d.ts.map +1 -1
  73. package/dist/ml/classical/naive_bayes.js +8 -3
  74. package/dist/ml/classical/naive_bayes.js.map +1 -1
  75. package/dist/ml/classical/robust_z.d.ts +15 -1
  76. package/dist/ml/classical/robust_z.d.ts.map +1 -1
  77. package/dist/ml/classical/robust_z.js +6 -1
  78. package/dist/ml/classical/robust_z.js.map +1 -1
  79. package/dist/ml/eval_loss.d.ts +15 -0
  80. package/dist/ml/eval_loss.d.ts.map +1 -0
  81. package/dist/ml/eval_loss.js +20 -0
  82. package/dist/ml/eval_loss.js.map +1 -0
  83. package/dist/ml/federate.d.ts +11 -25
  84. package/dist/ml/federate.d.ts.map +1 -1
  85. package/dist/ml/federate.js +15 -38
  86. package/dist/ml/federate.js.map +1 -1
  87. package/dist/ml/head_trainer.d.ts +16 -2
  88. package/dist/ml/head_trainer.d.ts.map +1 -1
  89. package/dist/ml/head_trainer.js +64 -27
  90. package/dist/ml/head_trainer.js.map +1 -1
  91. package/dist/ml/run.d.ts +11 -2
  92. package/dist/ml/run.d.ts.map +1 -1
  93. package/dist/ml/run.js +56 -17
  94. package/dist/ml/run.js.map +1 -1
  95. package/dist/ml/tasks/experience.d.ts +95 -13
  96. package/dist/ml/tasks/experience.d.ts.map +1 -1
  97. package/dist/ml/tasks/experience.js +172 -20
  98. package/dist/ml/tasks/experience.js.map +1 -1
  99. package/dist/ml/tasks/run_anomaly.d.ts +37 -19
  100. package/dist/ml/tasks/run_anomaly.d.ts.map +1 -1
  101. package/dist/ml/tasks/run_anomaly.js +50 -31
  102. package/dist/ml/tasks/run_anomaly.js.map +1 -1
  103. package/dist/ml/train.d.ts +8 -7
  104. package/dist/ml/train.d.ts.map +1 -1
  105. package/dist/ml/train.js +21 -30
  106. package/dist/ml/train.js.map +1 -1
  107. package/dist/ml/types.d.ts +6 -3
  108. package/dist/ml/types.d.ts.map +1 -1
  109. package/dist/ml/types.js +3 -2
  110. package/dist/ml/types.js.map +1 -1
  111. package/dist/optim/adamw.d.ts +11 -5
  112. package/dist/optim/adamw.d.ts.map +1 -1
  113. package/dist/optim/adamw.js +19 -4
  114. package/dist/optim/adamw.js.map +1 -1
  115. package/package.json +1 -1
  116. package/src/evermind/adapt.ts +8 -2
  117. package/src/evermind/delta_wire.ts +173 -148
  118. package/src/evermind/index.ts +36 -35
  119. package/src/evermind/merge.ts +10 -4
  120. package/src/evermind/ml_head.ts +63 -0
  121. package/src/evermind/package.ts +488 -488
  122. package/src/experience/index.ts +2 -1
  123. package/src/experience/store.ts +35 -3
  124. package/src/experience/types.ts +19 -0
  125. package/src/index.ts +2 -0
  126. package/src/kernels/weight_update.ts +12 -0
  127. package/src/limbic/limbic_model.ts +1 -4
  128. package/src/limbic/limbic_trainer.ts +91 -91
  129. package/src/ml/classical/anomaly_composite.ts +46 -0
  130. package/src/ml/classical/ewma.ts +8 -2
  131. package/src/ml/classical/gbm.ts +144 -143
  132. package/src/ml/classical/index.ts +8 -2
  133. package/src/ml/classical/kmeans.ts +22 -4
  134. package/src/ml/classical/knn.ts +31 -8
  135. package/src/ml/classical/logistic.ts +9 -9
  136. package/src/ml/classical/math.ts +93 -93
  137. package/src/ml/classical/naive_bayes.ts +10 -3
  138. package/src/ml/classical/robust_z.ts +21 -2
  139. package/src/ml/eval_loss.ts +27 -0
  140. package/src/ml/federate.ts +22 -52
  141. package/src/ml/head_trainer.ts +66 -48
  142. package/src/ml/registry.ts +44 -44
  143. package/src/ml/run.ts +57 -13
  144. package/src/ml/tasks/experience.ts +222 -23
  145. package/src/ml/tasks/run_anomaly.ts +62 -37
  146. package/src/ml/train.ts +34 -31
  147. package/src/ml/types.ts +135 -132
  148. package/src/optim/adamw.ts +21 -5
  149. package/src/training/cpu_ops.ts +141 -141
package/src/ml/train.ts CHANGED
@@ -12,10 +12,11 @@
12
12
  */
13
13
  import { fitClassical, type ClassicalOptions } from "./classical/index.js";
14
14
  import { DenseHead } from "./dense_head.js";
15
+ import { mlEvalLoss } from "./eval_loss.js";
15
16
  import { extractFeatureMatrix, type TextEmbedder } from "./features.js";
16
17
  import { HeadTrainer, type HeadTrainOptions } from "./head_trainer.js";
17
18
  import type { MlModelStore } from "./run.js";
18
- import { engineKey, type MlEngineRef, type MlPredictor, type MlSample, type MlTask, type MlTaskKind } from "./types.js";
19
+ import { engineKey, type MlEngineRef, type MlPredictor, type MlSample, type MlTask } from "./types.js";
19
20
 
20
21
  /** One labelled example in input space. */
21
22
  export interface MlTrainExample<I> {
@@ -48,26 +49,15 @@ export interface MlTrainResult {
48
49
  /** Size of the held-out tail, and the model's loss on it (null when none was held out). */
49
50
  holdout: number;
50
51
  holdoutLoss: number | null;
52
+ /**
53
+ * Head engines: parameter elements the final fit skipped for a non-finite gradient
54
+ * ({@link HeadTrainer.skippedNonFinite}) — above 0, the fit diverged somewhere. 0
55
+ * for classical engines and task-owned fits.
56
+ */
57
+ skippedNonFinite: number;
51
58
  }
52
59
 
53
- /**
54
- * Mean loss of `predictor` on `samples` for a task of `kind` — classify: weighted
55
- * cross-entropy of the true class; regress: weighted ½·squared error. Null for
56
- * kinds without a supervised loss, or an empty set.
57
- */
58
- export function mlEvalLoss(kind: MlTaskKind, predictor: MlPredictor, samples: readonly MlSample[]): number | null {
59
- if (samples.length === 0 || (kind !== "classify" && kind !== "regress")) return null;
60
- let sum = 0;
61
- let wSum = 0;
62
- for (const s of samples) {
63
- const w = s.weight ?? 1;
64
- const p = predictor.predict(s.x);
65
- const loss = kind === "classify" ? -Math.log(Math.max(p.scores[s.y] ?? 0, 1e-12)) : 0.5 * (p.scores[0]! - s.y) ** 2;
66
- sum += w * loss;
67
- wSum += w;
68
- }
69
- return wSum > 0 ? sum / wSum : null;
70
- }
60
+ export { mlEvalLoss } from "./eval_loss.js";
71
61
 
72
62
  /** The task's first local engine the seam can train. */
73
63
  function defaultEngine(task: MlTask<any, any>): MlEngineRef {
@@ -76,9 +66,14 @@ function defaultEngine(task: MlTask<any, any>): MlEngineRef {
76
66
  return e;
77
67
  }
78
68
 
79
- async function fitEngine(task: MlTask<any, any>, ref: MlEngineRef, samples: MlSample[], opts: MlTrainOptions): Promise<MlPredictor> {
80
- if (task.train?.fit) return task.train.fit(samples, ref);
81
- if (ref.engine === "classical") return fitClassical(ref.algorithm, samples, opts.classical);
69
+ interface FittedEngine {
70
+ predictor: MlPredictor;
71
+ skippedNonFinite: number;
72
+ }
73
+
74
+ async function fitEngine(task: MlTask<any, any>, ref: MlEngineRef, samples: MlSample[], opts: MlTrainOptions): Promise<FittedEngine> {
75
+ if (task.train?.fit) return { predictor: await task.train.fit(samples, ref), skippedNonFinite: 0 };
76
+ if (ref.engine === "classical") return { predictor: fitClassical(ref.algorithm, samples, opts.classical), skippedNonFinite: 0 };
82
77
  if (ref.engine === "head") {
83
78
  const objective = task.kind === "classify" ? "softmax" : task.kind === "regress" ? "mse" : null;
84
79
  if (!objective) throw new Error(`trainMlTask(${task.id}): a head engine trains classify or regress tasks only`);
@@ -89,8 +84,9 @@ async function fitEngine(task: MlTask<any, any>, ref: MlEngineRef, samples: MlSa
89
84
  objective,
90
85
  ...(opts.head?.seed != null ? { seed: opts.head.seed } : {}),
91
86
  });
92
- await new HeadTrainer(head, opts.device ?? null, `trainMlTask(${task.id})`).train(samples, opts.head?.train);
93
- return head;
87
+ const trainer = new HeadTrainer(head, opts.device ?? null, `trainMlTask(${task.id})`);
88
+ await trainer.train(samples, opts.head?.train);
89
+ return { predictor: head, skippedNonFinite: trainer.skippedNonFinite };
94
90
  }
95
91
  throw new Error(`trainMlTask(${task.id}): engine '${engineKey(ref)}' is not trained here`);
96
92
  }
@@ -123,14 +119,21 @@ export async function trainMlTask<I>(
123
119
  if (nHold >= 1 && samples.length - nHold >= 1) {
124
120
  const train = samples.slice(0, samples.length - nHold);
125
121
  const hold = samples.slice(samples.length - nHold);
126
- const predictor = await fitEngine(task, ref, train, opts);
127
- holdoutLoss = mlEvalLoss(task.kind, predictor, hold);
122
+ const fit = await fitEngine(task, ref, train, opts);
123
+ holdoutLoss = mlEvalLoss(task.kind, fit.predictor, hold);
128
124
  if (opts.refitOnAll === false) {
129
- opts.models?.set(task.id, engineKey(ref), predictor);
130
- return { predictor, engine: ref, samples: train.length, holdout: nHold, holdoutLoss };
125
+ opts.models?.set(task.id, engineKey(ref), fit.predictor);
126
+ return { predictor: fit.predictor, engine: ref, samples: train.length, holdout: nHold, holdoutLoss, skippedNonFinite: fit.skippedNonFinite };
131
127
  }
132
128
  }
133
- const predictor = await fitEngine(task, ref, samples, opts);
134
- opts.models?.set(task.id, engineKey(ref), predictor);
135
- return { predictor, engine: ref, samples: samples.length, holdout: nHold >= 1 ? nHold : 0, holdoutLoss };
129
+ const fit = await fitEngine(task, ref, samples, opts);
130
+ opts.models?.set(task.id, engineKey(ref), fit.predictor);
131
+ return {
132
+ predictor: fit.predictor,
133
+ engine: ref,
134
+ samples: samples.length,
135
+ holdout: nHold >= 1 ? nHold : 0,
136
+ holdoutLoss,
137
+ skippedNonFinite: fit.skippedNonFinite,
138
+ };
136
139
  }
package/src/ml/types.ts CHANGED
@@ -1,132 +1,135 @@
1
- /**
2
- * ml/types.ts — the ML task catalog contract (docs/ml-task-catalog.md §3).
3
- *
4
- * One bounded question Evermind answers without an LLM is ONE data row: how its
5
- * input becomes a vector, which engines may answer it (in preference order), how
6
- * it trains, and what its output means. Pure types — no vscode, no DOM, no network.
7
- */
8
-
9
- /** The shape of the question. */
10
- export type MlTaskKind = "classify" | "regress" | "cluster" | "anomaly" | "rank";
11
-
12
- /** Pure-TypeScript algorithms that run everywhere (`ml/classical/`). */
13
- export type ClassicalAlgorithm =
14
- | "knn"
15
- | "naive-bayes"
16
- | "logistic"
17
- | "linear"
18
- | "kmeans"
19
- | "robust-z"
20
- | "ewma"
21
- | "isolation-forest"
22
- | "gbm-small";
23
-
24
- /** An engine that can answer a task. Cloud engines are a port the HOST supplies. */
25
- export type MlEngineRef =
26
- | { tier: "local"; engine: "classical"; algorithm: ClassicalAlgorithm }
27
- | { tier: "local"; engine: "head"; head: "mlp" | "linear" }
28
- | { tier: "local"; engine: "onnx"; model: string }
29
- | { tier: "cloud"; engine: "workers-ai"; model: string };
30
-
31
- /**
32
- * Which engines a run may use — the person's inference setting.
33
- * - `local-only`: local tiers only, never the network.
34
- * - `hybrid`: local first, the cloud port when no local engine can answer (or the
35
- * local answer is below the caller's confidence floor).
36
- * - `specific`: exactly the engine the caller names.
37
- */
38
- export type InferenceMode = "local-only" | "hybrid" | "specific";
39
-
40
- /** What an engine returns before the task decodes it. */
41
- export interface MlPrediction {
42
- /**
43
- * classify: one probability per label; regress: the predicted value(s);
44
- * cluster: `[clusterId]`; anomaly: `[score]` where ≥ 1 is anomalous;
45
- * rank: one score per candidate.
46
- */
47
- scores: number[];
48
- /** Calibrated confidence in [0, 1]. */
49
- confidence: number;
50
- }
51
-
52
- /** A fitted model that answers on a feature vector. */
53
- export interface MlPredictor {
54
- predict(x: Float32Array): MlPrediction;
55
- /** Trainable state as one flat vector — present on models that federate. */
56
- exportParams?(): Float32Array;
57
- /** Load a flat vector produced by {@link exportParams} (e.g. after a merge). */
58
- importParams?(params: ArrayLike<number>): void;
59
- }
60
-
61
- /** How an input becomes a vector: an embedded text part, numeric fields, or both. */
62
- export interface FeatureSpec<I> {
63
- /** Text to embed (needs an embedder in the run context). */
64
- text?: (input: I) => string;
65
- /** Numeric fields, already on a stable scale (so federated weights mean the same thing everywhere). */
66
- numeric?: (input: I) => number[];
67
- /** Names of the numeric fields, index-aligned — documentation and Insights labels. */
68
- numericNames?: readonly string[];
69
- }
70
-
71
- /** One labelled training example in feature space. */
72
- export interface MlSample {
73
- x: Float32Array;
74
- /** classify: class index; regress: target value; cluster/anomaly: ignored. */
75
- y: number;
76
- /** Sample weight (default 1). */
77
- weight?: number;
78
- }
79
-
80
- /** How a task trains. Absent on the task = rule-based or pre-trained only. */
81
- export interface TrainSpec {
82
- /** Fewer labelled examples than this and the task stays untrained. */
83
- minSamples: number;
84
- /** Fraction held out for the eval gate (default 0.2). */
85
- holdoutFraction?: number;
86
- /**
87
- * A task-specific fit (e.g. a composite of two classical detectors). When absent
88
- * the trainer fits the engine named by the run (`ml/train.ts`).
89
- */
90
- fit?: (samples: readonly MlSample[], engine: MlEngineRef) => MlPredictor;
91
- /** Who trains it when not `ml/train.ts` (e.g. `LimbicTrainer` for `limbic.affect`). */
92
- trainedBy?: string;
93
- }
94
-
95
- /** What a task's answer means. */
96
- export interface OutputSpec<O> {
97
- /** Label set for classify tasks (index-aligned with `MlPrediction.scores`). */
98
- labels?: readonly string[];
99
- /** Decode an engine prediction into the task's output. */
100
- decode(p: MlPrediction): O;
101
- }
102
-
103
- /** One bounded question Evermind can answer without an LLM. DATA, not a class tree. */
104
- export interface MlTask<I = unknown, O = unknown> {
105
- kind: MlTaskKind;
106
- /** Stable id, e.g. `experience.quality`. */
107
- id: string;
108
- /** One line on what it answers. */
109
- description: string;
110
- features: FeatureSpec<I>;
111
- /** In preference order; filtered by the inference mode. */
112
- engines: readonly MlEngineRef[];
113
- train?: TrainSpec;
114
- output: OutputSpec<O>;
115
- }
116
-
117
- /** Task ids: lowercase dotted segments, e.g. `run.local-success`. */
118
- export const ML_TASK_ID_RE = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/;
119
-
120
- /** A stable string key for an engine ref, e.g. `local:classical:logistic`. */
121
- export function engineKey(ref: MlEngineRef): string {
122
- switch (ref.engine) {
123
- case "classical":
124
- return `local:classical:${ref.algorithm}`;
125
- case "head":
126
- return `local:head:${ref.head}`;
127
- case "onnx":
128
- return `local:onnx:${ref.model}`;
129
- case "workers-ai":
130
- return `cloud:workers-ai:${ref.model}`;
131
- }
132
- }
1
+ /**
2
+ * ml/types.ts — the ML task catalog contract (docs/ml-task-catalog.md §3).
3
+ *
4
+ * One bounded question Evermind answers without an LLM is ONE data row: how its
5
+ * input becomes a vector, which engines may answer it (in preference order), how
6
+ * it trains, and what its output means. Pure types — no vscode, no DOM, no network.
7
+ * (Imports only the leaf `evermind/ml_head.ts`; evermind/ never imports ml/.)
8
+ */
9
+
10
+ /** The shape of the question. */
11
+ export type MlTaskKind = "classify" | "regress" | "cluster" | "anomaly" | "rank";
12
+
13
+ /** Pure-TypeScript algorithms that run everywhere (`ml/classical/`). */
14
+ export type ClassicalAlgorithm =
15
+ | "knn"
16
+ | "naive-bayes"
17
+ | "logistic"
18
+ | "linear"
19
+ | "kmeans"
20
+ | "robust-z"
21
+ | "ewma"
22
+ /** robust z + EWMA as one fitted engine (`classical/anomaly_composite.ts`). */
23
+ | "anomaly-composite"
24
+ | "isolation-forest"
25
+ | "gbm-small";
26
+
27
+ /** An engine that can answer a task. Cloud engines are a port the HOST supplies. */
28
+ export type MlEngineRef =
29
+ | { tier: "local"; engine: "classical"; algorithm: ClassicalAlgorithm }
30
+ | { tier: "local"; engine: "head"; head: "mlp" | "linear" }
31
+ | { tier: "local"; engine: "onnx"; model: string }
32
+ | { tier: "cloud"; engine: "workers-ai"; model: string };
33
+
34
+ /**
35
+ * Which engines a run may use — the person's inference setting.
36
+ * - `local-only`: local tiers only, never the network.
37
+ * - `hybrid`: local first, the cloud port when no local engine can answer (or the
38
+ * local answer is below the caller's confidence floor).
39
+ * - `specific`: exactly the engine the caller names.
40
+ */
41
+ export type InferenceMode = "local-only" | "hybrid" | "specific";
42
+
43
+ /** What an engine returns before the task decodes it. */
44
+ export interface MlPrediction {
45
+ /**
46
+ * classify: one probability per label; regress: the predicted value(s);
47
+ * cluster: `[clusterId]`; anomaly: `[score]` where ≥ 1 is anomalous;
48
+ * rank: one score per candidate.
49
+ */
50
+ scores: number[];
51
+ /** Calibrated confidence in [0, 1]. */
52
+ confidence: number;
53
+ }
54
+
55
+ /** A fitted model that answers on a feature vector. */
56
+ export interface MlPredictor {
57
+ predict(x: Float32Array): MlPrediction;
58
+ /** Trainable state as one flat vector — present on models that federate. */
59
+ exportParams?(): Float32Array;
60
+ /** Load a flat vector produced by {@link exportParams} (e.g. after a merge). */
61
+ importParams?(params: ArrayLike<number>): void;
62
+ }
63
+
64
+ /** How an input becomes a vector: an embedded text part, numeric fields, or both. */
65
+ export interface FeatureSpec<I> {
66
+ /** Text to embed (needs an embedder in the run context). */
67
+ text?: (input: I) => string;
68
+ /** Numeric fields, already on a stable scale (so federated weights mean the same thing everywhere). */
69
+ numeric?: (input: I) => number[];
70
+ /** Names of the numeric fields, index-aligned — documentation and Insights labels. */
71
+ numericNames?: readonly string[];
72
+ }
73
+
74
+ /** One labelled training example in feature space. */
75
+ export interface MlSample {
76
+ x: Float32Array;
77
+ /** classify: class index; regress: target value; cluster/anomaly: ignored. */
78
+ y: number;
79
+ /** Sample weight (default 1). */
80
+ weight?: number;
81
+ }
82
+
83
+ /** How a task trains. Absent on the task = rule-based or pre-trained only. */
84
+ export interface TrainSpec {
85
+ /** Fewer labelled examples than this and the task stays untrained. */
86
+ minSamples: number;
87
+ /** Fraction held out for the eval gate (default 0.2). */
88
+ holdoutFraction?: number;
89
+ /**
90
+ * A task-specific fit (e.g. a composite of two classical detectors). When absent
91
+ * the trainer fits the engine named by the run (`ml/train.ts`).
92
+ */
93
+ fit?: (samples: readonly MlSample[], engine: MlEngineRef) => MlPredictor;
94
+ /** Who trains it when not `ml/train.ts` (e.g. `LimbicTrainer` for `limbic.affect`). */
95
+ trainedBy?: string;
96
+ }
97
+
98
+ /** What a task's answer means. */
99
+ export interface OutputSpec<O> {
100
+ /** Label set for classify tasks (index-aligned with `MlPrediction.scores`). */
101
+ labels?: readonly string[];
102
+ /** Decode an engine prediction into the task's output. */
103
+ decode(p: MlPrediction): O;
104
+ }
105
+
106
+ /** One bounded question Evermind can answer without an LLM. DATA, not a class tree. */
107
+ export interface MlTask<I = unknown, O = unknown> {
108
+ kind: MlTaskKind;
109
+ /** Stable id, e.g. `experience.quality`. */
110
+ id: string;
111
+ /** One line on what it answers. */
112
+ description: string;
113
+ features: FeatureSpec<I>;
114
+ /** In preference order; filtered by the inference mode. */
115
+ engines: readonly MlEngineRef[];
116
+ train?: TrainSpec;
117
+ output: OutputSpec<O>;
118
+ }
119
+
120
+ /** Task ids: lowercase dotted segments — defined once with the artifact's head contract. */
121
+ export { ML_TASK_ID_RE } from "../evermind/ml_head.js";
122
+
123
+ /** A stable string key for an engine ref, e.g. `local:classical:logistic`. */
124
+ export function engineKey(ref: MlEngineRef): string {
125
+ switch (ref.engine) {
126
+ case "classical":
127
+ return `local:classical:${ref.algorithm}`;
128
+ case "head":
129
+ return `local:head:${ref.head}`;
130
+ case "onnx":
131
+ return `local:onnx:${ref.model}`;
132
+ case "workers-ai":
133
+ return `cloud:workers-ai:${ref.model}`;
134
+ }
135
+ }
@@ -138,20 +138,29 @@ export interface AdamWKernelStep {
138
138
  gradScale?: number;
139
139
  }
140
140
 
141
+ /** Largest finite f32 — a moment above it would be stored as ±Inf. */
142
+ const F32_MAX = 3.4028234663852886e38;
143
+
141
144
  /**
142
145
  * One AdamW step over a single parameter tensor — the exact CPU mirror of the
143
- * `adamw_update` entry point in `WEIGHT_UPDATE_WGSL`, including its two safety
146
+ * `adamw_update` entry point in `WEIGHT_UPDATE_WGSL`, including its safety
144
147
  * properties:
145
148
  *
146
- * • the **NaN/Inf guard** — a non-finite step is dropped rather than written
147
- * into a weight, so one bad gradient cannot permanently poison the model;
149
+ * • the **non-finite gradient skip** — an element whose gradient is NaN/±Inf
150
+ * (or whose new moments would overflow f32) is skipped ENTIRELY: m, v and the
151
+ * weight keep their values (no decay either). Writing the moment first and
152
+ * dropping only the step stored NaN in m/v, and every later step of that
153
+ * element was then NaN too — silently dropped, so the weight froze forever;
154
+ * • the **NaN/Inf step guard** — a non-finite step from finite moments is
155
+ * dropped rather than written into a weight;
148
156
  * • the **trust region** — `maxDelta` bounds how far a single step can move a
149
157
  * weight, which is what keeps repeated write-through adapts reversible.
150
158
  *
151
159
  * Decoupled weight decay (`θ * (1 - lr·wd)`), matching the kernel — deliberately
152
160
  * NOT the coupled form {@link AdamW} uses for the MoE/EvermindLM trainers.
153
161
  *
154
- * `param`, `m` and `v` are updated in place.
162
+ * `param`, `m` and `v` are updated in place. Returns how many elements were skipped
163
+ * for a non-finite gradient (0 on a healthy step).
155
164
  */
156
165
  export function adamwUpdateInPlace(
157
166
  param: Float32Array,
@@ -159,15 +168,21 @@ export function adamwUpdateInPlace(
159
168
  m: Float32Array,
160
169
  v: Float32Array,
161
170
  hp: AdamWKernelStep,
162
- ): void {
171
+ ): number {
163
172
  const { lr, beta1, beta2, eps, weightDecay, beta1_t, beta2_t, maxDelta } = hp;
164
173
  const gradScale = hp.gradScale ?? 1;
165
174
  const bc1 = 1 - beta1_t;
166
175
  const bc2 = 1 - beta2_t;
176
+ let skipped = 0;
167
177
  for (let i = 0; i < param.length; i++) {
168
178
  const g = grad[i]! * gradScale;
169
179
  const mNew = beta1 * m[i]! + (1 - beta1) * g;
170
180
  const vNew = beta2 * v[i]! + (1 - beta2) * g * g;
181
+ // NaN fails every comparison, so this also catches a NaN gradient.
182
+ if (!(Math.abs(mNew) <= F32_MAX && vNew <= F32_MAX)) {
183
+ skipped++;
184
+ continue;
185
+ }
171
186
  m[i] = mNew;
172
187
  v[i] = vNew;
173
188
 
@@ -177,4 +192,5 @@ export function adamwUpdateInPlace(
177
192
 
178
193
  param[i] = param[i]! * (1 - lr * weightDecay) - step;
179
194
  }
195
+ return skipped;
180
196
  }