@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.
- package/dist/evermind/adapt.d.ts +7 -0
- package/dist/evermind/adapt.d.ts.map +1 -1
- package/dist/evermind/adapt.js +1 -1
- package/dist/evermind/adapt.js.map +1 -1
- package/dist/evermind/delta_wire.d.ts +16 -1
- package/dist/evermind/delta_wire.d.ts.map +1 -1
- package/dist/evermind/delta_wire.js +22 -2
- package/dist/evermind/delta_wire.js.map +1 -1
- package/dist/evermind/index.d.ts +2 -2
- package/dist/evermind/index.d.ts.map +1 -1
- package/dist/evermind/index.js +1 -1
- package/dist/evermind/index.js.map +1 -1
- package/dist/evermind/merge.d.ts +5 -2
- package/dist/evermind/merge.d.ts.map +1 -1
- package/dist/evermind/merge.js +5 -2
- package/dist/evermind/merge.js.map +1 -1
- package/dist/evermind/ml_head.d.ts +28 -0
- package/dist/evermind/ml_head.d.ts.map +1 -0
- package/dist/evermind/ml_head.js +47 -0
- package/dist/evermind/ml_head.js.map +1 -0
- package/dist/evermind/package.d.ts +2 -2
- package/dist/evermind/package.d.ts.map +1 -1
- package/dist/evermind/package.js +1 -1
- package/dist/evermind/package.js.map +1 -1
- package/dist/experience/index.d.ts +2 -2
- package/dist/experience/index.d.ts.map +1 -1
- package/dist/experience/index.js.map +1 -1
- package/dist/experience/store.d.ts +19 -1
- package/dist/experience/store.d.ts.map +1 -1
- package/dist/experience/store.js +14 -2
- package/dist/experience/store.js.map +1 -1
- package/dist/experience/types.d.ts +18 -0
- package/dist/experience/types.d.ts.map +1 -1
- package/dist/experience/types.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/kernels/weight_update.d.ts.map +1 -1
- package/dist/kernels/weight_update.js +12 -0
- package/dist/kernels/weight_update.js.map +1 -1
- package/dist/limbic/limbic_model.d.ts.map +1 -1
- package/dist/limbic/limbic_model.js +1 -3
- package/dist/limbic/limbic_model.js.map +1 -1
- package/dist/ml/classical/anomaly_composite.d.ts +32 -0
- package/dist/ml/classical/anomaly_composite.d.ts.map +1 -0
- package/dist/ml/classical/anomaly_composite.js +29 -0
- package/dist/ml/classical/anomaly_composite.js.map +1 -0
- package/dist/ml/classical/ewma.d.ts +7 -1
- package/dist/ml/classical/ewma.d.ts.map +1 -1
- package/dist/ml/classical/ewma.js +6 -4
- package/dist/ml/classical/ewma.js.map +1 -1
- package/dist/ml/classical/gbm.d.ts.map +1 -1
- package/dist/ml/classical/gbm.js +2 -1
- package/dist/ml/classical/gbm.js.map +1 -1
- package/dist/ml/classical/index.d.ts +6 -2
- package/dist/ml/classical/index.d.ts.map +1 -1
- package/dist/ml/classical/index.js +5 -1
- package/dist/ml/classical/index.js.map +1 -1
- package/dist/ml/classical/kmeans.d.ts.map +1 -1
- package/dist/ml/classical/kmeans.js +23 -4
- package/dist/ml/classical/kmeans.js.map +1 -1
- package/dist/ml/classical/knn.d.ts +15 -5
- package/dist/ml/classical/knn.d.ts.map +1 -1
- package/dist/ml/classical/knn.js +28 -7
- package/dist/ml/classical/knn.js.map +1 -1
- package/dist/ml/classical/logistic.d.ts +1 -9
- package/dist/ml/classical/logistic.d.ts.map +1 -1
- package/dist/ml/classical/logistic.js +17 -9
- package/dist/ml/classical/logistic.js.map +1 -1
- package/dist/ml/classical/naive_bayes.d.ts +5 -1
- package/dist/ml/classical/naive_bayes.d.ts.map +1 -1
- package/dist/ml/classical/naive_bayes.js +8 -3
- package/dist/ml/classical/naive_bayes.js.map +1 -1
- package/dist/ml/classical/robust_z.d.ts +15 -1
- package/dist/ml/classical/robust_z.d.ts.map +1 -1
- package/dist/ml/classical/robust_z.js +6 -1
- package/dist/ml/classical/robust_z.js.map +1 -1
- package/dist/ml/eval_loss.d.ts +15 -0
- package/dist/ml/eval_loss.d.ts.map +1 -0
- package/dist/ml/eval_loss.js +20 -0
- package/dist/ml/eval_loss.js.map +1 -0
- package/dist/ml/federate.d.ts +11 -25
- package/dist/ml/federate.d.ts.map +1 -1
- package/dist/ml/federate.js +15 -38
- package/dist/ml/federate.js.map +1 -1
- package/dist/ml/head_trainer.d.ts +16 -2
- package/dist/ml/head_trainer.d.ts.map +1 -1
- package/dist/ml/head_trainer.js +64 -27
- package/dist/ml/head_trainer.js.map +1 -1
- package/dist/ml/run.d.ts +11 -2
- package/dist/ml/run.d.ts.map +1 -1
- package/dist/ml/run.js +56 -17
- package/dist/ml/run.js.map +1 -1
- package/dist/ml/tasks/experience.d.ts +95 -13
- package/dist/ml/tasks/experience.d.ts.map +1 -1
- package/dist/ml/tasks/experience.js +172 -20
- package/dist/ml/tasks/experience.js.map +1 -1
- package/dist/ml/tasks/run_anomaly.d.ts +37 -19
- package/dist/ml/tasks/run_anomaly.d.ts.map +1 -1
- package/dist/ml/tasks/run_anomaly.js +50 -31
- package/dist/ml/tasks/run_anomaly.js.map +1 -1
- package/dist/ml/train.d.ts +8 -7
- package/dist/ml/train.d.ts.map +1 -1
- package/dist/ml/train.js +21 -30
- package/dist/ml/train.js.map +1 -1
- package/dist/ml/types.d.ts +6 -3
- package/dist/ml/types.d.ts.map +1 -1
- package/dist/ml/types.js +3 -2
- package/dist/ml/types.js.map +1 -1
- package/dist/optim/adamw.d.ts +11 -5
- package/dist/optim/adamw.d.ts.map +1 -1
- package/dist/optim/adamw.js +19 -4
- package/dist/optim/adamw.js.map +1 -1
- package/package.json +1 -1
- package/src/evermind/adapt.ts +8 -2
- package/src/evermind/delta_wire.ts +173 -148
- package/src/evermind/index.ts +36 -35
- package/src/evermind/merge.ts +10 -4
- package/src/evermind/ml_head.ts +63 -0
- package/src/evermind/package.ts +488 -488
- package/src/experience/index.ts +2 -1
- package/src/experience/store.ts +35 -3
- package/src/experience/types.ts +19 -0
- package/src/index.ts +2 -0
- package/src/kernels/weight_update.ts +12 -0
- package/src/limbic/limbic_model.ts +1 -4
- package/src/limbic/limbic_trainer.ts +91 -91
- package/src/ml/classical/anomaly_composite.ts +46 -0
- package/src/ml/classical/ewma.ts +8 -2
- package/src/ml/classical/gbm.ts +144 -143
- package/src/ml/classical/index.ts +8 -2
- package/src/ml/classical/kmeans.ts +22 -4
- package/src/ml/classical/knn.ts +31 -8
- package/src/ml/classical/logistic.ts +9 -9
- package/src/ml/classical/math.ts +93 -93
- package/src/ml/classical/naive_bayes.ts +10 -3
- package/src/ml/classical/robust_z.ts +21 -2
- package/src/ml/eval_loss.ts +27 -0
- package/src/ml/federate.ts +22 -52
- package/src/ml/head_trainer.ts +66 -48
- package/src/ml/registry.ts +44 -44
- package/src/ml/run.ts +57 -13
- package/src/ml/tasks/experience.ts +222 -23
- package/src/ml/tasks/run_anomaly.ts +62 -37
- package/src/ml/train.ts +34 -31
- package/src/ml/types.ts +135 -132
- package/src/optim/adamw.ts +21 -5
- package/src/training/cpu_ops.ts +141 -141
package/src/experience/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type {
|
|
|
21
21
|
RunStepLog,
|
|
22
22
|
Run,
|
|
23
23
|
Adaptation,
|
|
24
|
+
CuratedOut,
|
|
24
25
|
} from './types.js';
|
|
25
26
|
export { newExperienceId } from './types.js';
|
|
26
27
|
export { isIrreversibleLabel, elementLabel, needsApproval } from './approval.js';
|
|
@@ -32,4 +33,4 @@ export type { ExperienceDocument } from './corpus.js';
|
|
|
32
33
|
export { experienceOverview } from './overview.js';
|
|
33
34
|
export type { ExperienceOverview, ExperienceRegion, ExperienceEvent, ExperienceDay, OverviewOptions } from './overview.js';
|
|
34
35
|
export { EXPERIENCE_SCHEMA, MAX_RUNS, MAX_ADAPTATIONS, emptySnapshot, parseSnapshot, InMemoryExperienceStore } from './store.js';
|
|
35
|
-
export type { ExperienceSnapshot, ExperienceStore } from './store.js';
|
|
36
|
+
export type { ExperienceSnapshot, ExperienceStore, CurationRecord } from './store.js';
|
package/src/experience/store.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* is the reference implementation, and `ExperienceSnapshot` is the one serialized shape,
|
|
8
8
|
* so a store on one runtime can be read by another.
|
|
9
9
|
*/
|
|
10
|
-
import type { Adaptation, Episode, EpisodeSummary, Run, RunStepLog, Skill } from './types.js';
|
|
10
|
+
import type { Adaptation, CuratedOut, Episode, EpisodeSummary, Run, RunStepLog, Skill } from './types.js';
|
|
11
11
|
|
|
12
12
|
export const EXPERIENCE_SCHEMA = 'evermind.experience/1';
|
|
13
13
|
/** Runs kept, newest first — the audit trail is recent history, not an archive. */
|
|
@@ -29,6 +29,18 @@ export interface ExperienceSnapshot {
|
|
|
29
29
|
learned: Record<string, number>;
|
|
30
30
|
/** Every adaptation's measurements, newest first (capped at {@link MAX_ADAPTATIONS}). */
|
|
31
31
|
adaptations: Adaptation[];
|
|
32
|
+
/**
|
|
33
|
+
* Procedures curation left out, by id, until they are learned, forgotten or judged
|
|
34
|
+
* again — so a later curation skips them instead of re-embedding each one.
|
|
35
|
+
*/
|
|
36
|
+
curated: Record<string, CuratedOut>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What one curation call decided: the procedures it left out, and the ones it kept. */
|
|
40
|
+
export interface CurationRecord {
|
|
41
|
+
dropped: Record<string, CuratedOut>;
|
|
42
|
+
/** Ids curation kept this time — any earlier verdict on them is cleared. */
|
|
43
|
+
kept: string[];
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
export interface ExperienceStore {
|
|
@@ -56,13 +68,20 @@ export interface ExperienceStore {
|
|
|
56
68
|
*/
|
|
57
69
|
recordAdaptation(adaptation: Adaptation): Promise<void>;
|
|
58
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Record one curation's verdicts: each dropped procedure is stored as `curated_out`
|
|
73
|
+
* (replacing an earlier verdict), each kept one loses any earlier verdict. Learning
|
|
74
|
+
* or forgetting a procedure also clears its verdict.
|
|
75
|
+
*/
|
|
76
|
+
recordCuration(curation: CurationRecord): Promise<void>;
|
|
77
|
+
|
|
59
78
|
snapshot(): Promise<ExperienceSnapshot>;
|
|
60
79
|
/** Delete every episode, skill and run. */
|
|
61
80
|
clear(): Promise<void>;
|
|
62
81
|
}
|
|
63
82
|
|
|
64
83
|
export function emptySnapshot(): ExperienceSnapshot {
|
|
65
|
-
return { schema: EXPERIENCE_SCHEMA, episodes: [], skills: [], runs: [], learned: {}, adaptations: [] };
|
|
84
|
+
return { schema: EXPERIENCE_SCHEMA, episodes: [], skills: [], runs: [], learned: {}, adaptations: [], curated: {} };
|
|
66
85
|
}
|
|
67
86
|
|
|
68
87
|
/** Parse a snapshot, tolerating a missing or foreign file as empty. */
|
|
@@ -77,6 +96,8 @@ export function parseSnapshot(raw: unknown): ExperienceSnapshot {
|
|
|
77
96
|
learned: s.learned && typeof s.learned === 'object' && !Array.isArray(s.learned) ? s.learned : {},
|
|
78
97
|
// Optional within schema 1: snapshots written before the history existed have none.
|
|
79
98
|
adaptations: Array.isArray(s.adaptations) ? s.adaptations : [],
|
|
99
|
+
// Optional within schema 1 too: written from 2026.10.3.
|
|
100
|
+
curated: s.curated && typeof s.curated === 'object' && !Array.isArray(s.curated) ? s.curated : {},
|
|
80
101
|
};
|
|
81
102
|
}
|
|
82
103
|
|
|
@@ -130,6 +151,7 @@ export class InMemoryExperienceStore implements ExperienceStore {
|
|
|
130
151
|
async deleteEpisode(id: string): Promise<boolean> {
|
|
131
152
|
const ok = InMemoryExperienceStore.remove(this.data.episodes, id);
|
|
132
153
|
delete this.data.learned[id];
|
|
154
|
+
delete this.data.curated[id];
|
|
133
155
|
if (ok) await this.changed();
|
|
134
156
|
return ok;
|
|
135
157
|
}
|
|
@@ -147,6 +169,7 @@ export class InMemoryExperienceStore implements ExperienceStore {
|
|
|
147
169
|
async deleteSkill(id: string): Promise<boolean> {
|
|
148
170
|
const ok = InMemoryExperienceStore.remove(this.data.skills, id);
|
|
149
171
|
delete this.data.learned[id];
|
|
172
|
+
delete this.data.curated[id];
|
|
150
173
|
if (ok) await this.changed();
|
|
151
174
|
return ok;
|
|
152
175
|
}
|
|
@@ -172,12 +195,21 @@ export class InMemoryExperienceStore implements ExperienceStore {
|
|
|
172
195
|
}
|
|
173
196
|
|
|
174
197
|
async recordAdaptation(adaptation: Adaptation): Promise<void> {
|
|
175
|
-
for (const id of adaptation.learned)
|
|
198
|
+
for (const id of adaptation.learned) {
|
|
199
|
+
this.data.learned[id] = adaptation.index;
|
|
200
|
+
delete this.data.curated[id];
|
|
201
|
+
}
|
|
176
202
|
this.data.adaptations.unshift(adaptation);
|
|
177
203
|
this.data.adaptations.length = Math.min(this.data.adaptations.length, MAX_ADAPTATIONS);
|
|
178
204
|
await this.changed();
|
|
179
205
|
}
|
|
180
206
|
|
|
207
|
+
async recordCuration(curation: CurationRecord): Promise<void> {
|
|
208
|
+
for (const id of curation.kept) delete this.data.curated[id];
|
|
209
|
+
Object.assign(this.data.curated, curation.dropped);
|
|
210
|
+
await this.changed();
|
|
211
|
+
}
|
|
212
|
+
|
|
181
213
|
async snapshot(): Promise<ExperienceSnapshot> {
|
|
182
214
|
return structuredClone(this.data);
|
|
183
215
|
}
|
package/src/experience/types.ts
CHANGED
|
@@ -154,6 +154,25 @@ export interface Adaptation {
|
|
|
154
154
|
at: number;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* A procedure curation left out of an adaptation, kept so a later curation does not
|
|
159
|
+
* re-embed and re-judge it while the verdict still stands: a `duplicate` stands while
|
|
160
|
+
* the procedure it repeats (`of`) is learned by the loaded model; a `low-quality` one
|
|
161
|
+
* while its finished-run count is still `runs` (new runs are new evidence). An
|
|
162
|
+
* `excluded` one is recorded but always reconsidered — exclusion is the caller's call.
|
|
163
|
+
*/
|
|
164
|
+
export interface CuratedOut {
|
|
165
|
+
status: 'curated_out';
|
|
166
|
+
reason: 'duplicate' | 'low-quality' | 'excluded';
|
|
167
|
+
/** `duplicate`: the id of the procedure it repeats. */
|
|
168
|
+
of?: string;
|
|
169
|
+
/** Similarity (duplicate) or P(high) (low-quality). */
|
|
170
|
+
score?: number;
|
|
171
|
+
/** Finished runs of the procedure when it was judged. */
|
|
172
|
+
runs: number;
|
|
173
|
+
at: number;
|
|
174
|
+
}
|
|
175
|
+
|
|
157
176
|
/** A short random id — unique enough for one person's store. */
|
|
158
177
|
export function newExperienceId(): string {
|
|
159
178
|
const bytes = new Uint8Array(8);
|
package/src/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ export {
|
|
|
43
43
|
buildDeltaLearnPayload,
|
|
44
44
|
decodeDeltaPayload,
|
|
45
45
|
deltaUnusableReason,
|
|
46
|
+
deltaTargetMismatch,
|
|
46
47
|
MAX_DELTA_B64_CHARS,
|
|
47
48
|
mergeCheckpointDiffs,
|
|
48
49
|
mergeCheckpointDiffsGated,
|
|
@@ -63,6 +64,7 @@ export type {
|
|
|
63
64
|
AdaptTokenizer,
|
|
64
65
|
DeltaLearnPayload,
|
|
65
66
|
DeltaParseResult,
|
|
67
|
+
DeltaParseOptions,
|
|
66
68
|
MergeResult,
|
|
67
69
|
GatedMergeResult,
|
|
68
70
|
MergeGateOptions,
|
|
@@ -35,6 +35,12 @@ struct AdamParams {
|
|
|
35
35
|
// v (N,) – second moment
|
|
36
36
|
@group(0) @binding(4) var<storage, read_write> v_state : array<f32>;
|
|
37
37
|
|
|
38
|
+
// NaN or ±Inf, read from the IEEE-754 exponent bits (all ones) — immune to a
|
|
39
|
+
// compiler that assumes finite floats and folds "x != x" away.
|
|
40
|
+
fn non_finite(x : f32) -> bool {
|
|
41
|
+
return (bitcast<u32>(x) & 0x7f800000u) == 0x7f800000u;
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
// Dispatch: (ceil(N / 256), 1, 1)
|
|
39
45
|
@compute @workgroup_size(256, 1, 1)
|
|
40
46
|
fn adamw_update(
|
|
@@ -49,6 +55,12 @@ fn adamw_update(
|
|
|
49
55
|
// Moment updates
|
|
50
56
|
let m_new = adam.beta1 * m_state[i] + (1.0 - adam.beta1) * g;
|
|
51
57
|
let v_new = adam.beta2 * v_state[i] + (1.0 - adam.beta2) * g * g;
|
|
58
|
+
|
|
59
|
+
// Non-finite gradient (or moments that overflowed): skip this element ENTIRELY —
|
|
60
|
+
// m, v and the weight keep their values. Storing a NaN moment and dropping only
|
|
61
|
+
// the step froze the weight forever (every later step read the NaN back).
|
|
62
|
+
// Mirrors adamwUpdateInPlace (optim/adamw.ts), which counts the skips.
|
|
63
|
+
if (non_finite(g) || non_finite(m_new) || non_finite(v_new)) { return; }
|
|
52
64
|
m_state[i] = m_new;
|
|
53
65
|
v_state[i] = v_new;
|
|
54
66
|
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* (experience, state) → affect pair is close to an independent regression.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import { sigmoid } from "../training/cpu_ops.js";
|
|
31
32
|
import { SeededRng } from "../utils/rng.js";
|
|
32
33
|
import { quantizeFp16, dequantizeFp16 } from "../utils/quantization.js";
|
|
33
34
|
import { LIMBIC_STATE_DIM } from "./regions.js";
|
|
@@ -86,10 +87,6 @@ const MAGIC = 0x4c4d4243; // "LMBC"
|
|
|
86
87
|
/** Fixed default init seed — reproducible byte-identical cold start across machines. */
|
|
87
88
|
export const DEFAULT_LIMBIC_SEED = 0x11b1c5ee;
|
|
88
89
|
|
|
89
|
-
function sigmoid(x: number): number {
|
|
90
|
-
return 1 / (1 + Math.exp(-x));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
90
|
export class LimbicModel {
|
|
94
91
|
readonly config: Required<Omit<LimbicModelConfig, "seed">>;
|
|
95
92
|
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* limbic_trainer.ts – LimbicTrainer: gradient-based training for the LimbicModel.
|
|
3
|
-
*
|
|
4
|
-
* Trains the affect head to predict affect deltas and reward from
|
|
5
|
-
* (experience embedding, current state) pairs, using full-batch gradient
|
|
6
|
-
* descent with AdamW — through the shared {@link HeadTrainer} (`ml/head_trainer.ts`),
|
|
7
|
-
* which runs the optimiser step on the GPU via WEIGHT_UPDATE_WGSL when a device is
|
|
8
|
-
* supplied and on a numerically-equivalent CPU AdamW otherwise. This file is only
|
|
9
|
-
* the limbic registration: the head adapter and the public trainer surface.
|
|
10
|
-
*
|
|
11
|
-
* The objective is MSE (regression), not the cross-entropy used by the
|
|
12
|
-
* language-model {@link MambaTrainer} — a limbic experience has continuous
|
|
13
|
-
* targets, not a next-token distribution.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { HeadTrainer, type HeadTrainOptions, type TrainableHead } from "../ml/head_trainer.js";
|
|
17
|
-
import type { LimbicModel, LimbicParam } from "./limbic_model.js";
|
|
18
|
-
|
|
19
|
-
/** One training example: an experience and the affect change it should produce. */
|
|
20
|
-
export interface LimbicSample {
|
|
21
|
-
/** Experience embedding (length = model.inputDim). */
|
|
22
|
-
input: ArrayLike<number>;
|
|
23
|
-
/** Affective state at the time of the experience (length = model.stateDim). */
|
|
24
|
-
state: ArrayLike<number>;
|
|
25
|
-
/** Observed affect delta target in (-1, 1) per state dim (length = model.stateDim). */
|
|
26
|
-
deltaTarget: ArrayLike<number>;
|
|
27
|
-
/** Observed scalar reward for the experience. */
|
|
28
|
-
reward: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** The limbic trainer's options are the shared head trainer's. */
|
|
32
|
-
export type LimbicTrainOptions = HeadTrainOptions;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The limbic model as a {@link TrainableHead}. Each sample is one experience
|
|
36
|
-
* appraisal: cross-experience memory flows through the affective state `s` (fed
|
|
37
|
-
* back by the runtime), not through the hidden scratch `h`, so the hidden state
|
|
38
|
-
* resets per sample. This also makes BPTT(1) exact — there is no truncated carry.
|
|
39
|
-
*/
|
|
40
|
-
export function limbicHead(model: LimbicModel): TrainableHead<LimbicSample> {
|
|
41
|
-
return {
|
|
42
|
-
parameters: (): LimbicParam[] => model.parameters(),
|
|
43
|
-
gradients: (): LimbicParam[] => model.gradients(),
|
|
44
|
-
zeroGrad: () => model.zeroGrad(),
|
|
45
|
-
accumulate: (s) =>
|
|
46
|
-
model.backwardStep(s.input, model.initHidden(), s.state, s.deltaTarget, s.reward).loss,
|
|
47
|
-
loss: (s) => {
|
|
48
|
-
const { rewardWeight } = model.config;
|
|
49
|
-
const f = model.forward(s.input, model.initHidden(), s.state);
|
|
50
|
-
let loss = 0;
|
|
51
|
-
for (let k = 0; k < f.delta.length; k++) {
|
|
52
|
-
const d = f.delta[k]! - (s.deltaTarget[k] ?? 0);
|
|
53
|
-
loss += 0.5 * d * d;
|
|
54
|
-
}
|
|
55
|
-
const rd = f.reward - s.reward;
|
|
56
|
-
loss += 0.5 * rewardWeight * rd * rd;
|
|
57
|
-
return loss;
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Thin registration of the limbic head over the shared {@link HeadTrainer}. */
|
|
63
|
-
export class LimbicTrainer {
|
|
64
|
-
readonly model: LimbicModel;
|
|
65
|
-
readonly device: GPUDevice | null;
|
|
66
|
-
private readonly _trainer: HeadTrainer<LimbicSample>;
|
|
67
|
-
|
|
68
|
-
constructor(model: LimbicModel, device: GPUDevice | null = null) {
|
|
69
|
-
this.model = model;
|
|
70
|
-
this.device = device;
|
|
71
|
-
this._trainer = new HeadTrainer(limbicHead(model), device, "LimbicTrainer");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Whether the optimiser step runs on the GPU. */
|
|
75
|
-
get gpuTraining(): boolean {
|
|
76
|
-
return this._trainer.gpuTraining;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Train on a batch of samples for `epochs` passes. Returns the per-epoch mean
|
|
81
|
-
* loss (monotonically decreasing on a learnable mapping).
|
|
82
|
-
*/
|
|
83
|
-
train(samples: LimbicSample[], opts: LimbicTrainOptions = {}): Promise<number[]> {
|
|
84
|
-
return this._trainer.train(samples, opts);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Mean MSE loss over samples (no weight update). Hidden resets per sample. */
|
|
88
|
-
evaluate(samples: LimbicSample[]): number {
|
|
89
|
-
return this._trainer.evaluate(samples);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* limbic_trainer.ts – LimbicTrainer: gradient-based training for the LimbicModel.
|
|
3
|
+
*
|
|
4
|
+
* Trains the affect head to predict affect deltas and reward from
|
|
5
|
+
* (experience embedding, current state) pairs, using full-batch gradient
|
|
6
|
+
* descent with AdamW — through the shared {@link HeadTrainer} (`ml/head_trainer.ts`),
|
|
7
|
+
* which runs the optimiser step on the GPU via WEIGHT_UPDATE_WGSL when a device is
|
|
8
|
+
* supplied and on a numerically-equivalent CPU AdamW otherwise. This file is only
|
|
9
|
+
* the limbic registration: the head adapter and the public trainer surface.
|
|
10
|
+
*
|
|
11
|
+
* The objective is MSE (regression), not the cross-entropy used by the
|
|
12
|
+
* language-model {@link MambaTrainer} — a limbic experience has continuous
|
|
13
|
+
* targets, not a next-token distribution.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { HeadTrainer, type HeadTrainOptions, type TrainableHead } from "../ml/head_trainer.js";
|
|
17
|
+
import type { LimbicModel, LimbicParam } from "./limbic_model.js";
|
|
18
|
+
|
|
19
|
+
/** One training example: an experience and the affect change it should produce. */
|
|
20
|
+
export interface LimbicSample {
|
|
21
|
+
/** Experience embedding (length = model.inputDim). */
|
|
22
|
+
input: ArrayLike<number>;
|
|
23
|
+
/** Affective state at the time of the experience (length = model.stateDim). */
|
|
24
|
+
state: ArrayLike<number>;
|
|
25
|
+
/** Observed affect delta target in (-1, 1) per state dim (length = model.stateDim). */
|
|
26
|
+
deltaTarget: ArrayLike<number>;
|
|
27
|
+
/** Observed scalar reward for the experience. */
|
|
28
|
+
reward: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The limbic trainer's options are the shared head trainer's. */
|
|
32
|
+
export type LimbicTrainOptions = HeadTrainOptions;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The limbic model as a {@link TrainableHead}. Each sample is one experience
|
|
36
|
+
* appraisal: cross-experience memory flows through the affective state `s` (fed
|
|
37
|
+
* back by the runtime), not through the hidden scratch `h`, so the hidden state
|
|
38
|
+
* resets per sample. This also makes BPTT(1) exact — there is no truncated carry.
|
|
39
|
+
*/
|
|
40
|
+
export function limbicHead(model: LimbicModel): TrainableHead<LimbicSample> {
|
|
41
|
+
return {
|
|
42
|
+
parameters: (): LimbicParam[] => model.parameters(),
|
|
43
|
+
gradients: (): LimbicParam[] => model.gradients(),
|
|
44
|
+
zeroGrad: () => model.zeroGrad(),
|
|
45
|
+
accumulate: (s) =>
|
|
46
|
+
model.backwardStep(s.input, model.initHidden(), s.state, s.deltaTarget, s.reward).loss,
|
|
47
|
+
loss: (s) => {
|
|
48
|
+
const { rewardWeight } = model.config;
|
|
49
|
+
const f = model.forward(s.input, model.initHidden(), s.state);
|
|
50
|
+
let loss = 0;
|
|
51
|
+
for (let k = 0; k < f.delta.length; k++) {
|
|
52
|
+
const d = f.delta[k]! - (s.deltaTarget[k] ?? 0);
|
|
53
|
+
loss += 0.5 * d * d;
|
|
54
|
+
}
|
|
55
|
+
const rd = f.reward - s.reward;
|
|
56
|
+
loss += 0.5 * rewardWeight * rd * rd;
|
|
57
|
+
return loss;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Thin registration of the limbic head over the shared {@link HeadTrainer}. */
|
|
63
|
+
export class LimbicTrainer {
|
|
64
|
+
readonly model: LimbicModel;
|
|
65
|
+
readonly device: GPUDevice | null;
|
|
66
|
+
private readonly _trainer: HeadTrainer<LimbicSample>;
|
|
67
|
+
|
|
68
|
+
constructor(model: LimbicModel, device: GPUDevice | null = null) {
|
|
69
|
+
this.model = model;
|
|
70
|
+
this.device = device;
|
|
71
|
+
this._trainer = new HeadTrainer(limbicHead(model), device, "LimbicTrainer");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether the optimiser step runs on the GPU. */
|
|
75
|
+
get gpuTraining(): boolean {
|
|
76
|
+
return this._trainer.gpuTraining;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Train on a batch of samples for `epochs` passes. Returns the per-epoch mean
|
|
81
|
+
* loss (monotonically decreasing on a learnable mapping).
|
|
82
|
+
*/
|
|
83
|
+
train(samples: LimbicSample[], opts: LimbicTrainOptions = {}): Promise<number[]> {
|
|
84
|
+
return this._trainer.train(samples, opts);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Mean MSE loss over samples (no weight update). Hidden resets per sample. */
|
|
88
|
+
evaluate(samples: LimbicSample[]): number {
|
|
89
|
+
return this._trainer.evaluate(samples);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical/anomaly_composite.ts — robust z and EWMA as ONE anomaly engine.
|
|
3
|
+
*
|
|
4
|
+
* Two detectors, one answer: robust z (median/MAD over the whole history — "far
|
|
5
|
+
* from normal") and EWMA (the recent trend — "drifting away from lately"). The
|
|
6
|
+
* score is the larger of the two normalised scores, so either one can flag. It is
|
|
7
|
+
* one engine because it is one fitted thing: a task that lists it gets back exactly
|
|
8
|
+
* what it stores under `local:classical:anomaly-composite`.
|
|
9
|
+
*/
|
|
10
|
+
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
11
|
+
import { EwmaModel, fitEwma, type EwmaOptions } from "./ewma.js";
|
|
12
|
+
import { anomalyConfidence, fitRobustZ, RobustZModel, type RobustZOptions } from "./robust_z.js";
|
|
13
|
+
|
|
14
|
+
export interface AnomalyCompositeOptions {
|
|
15
|
+
robustZ?: RobustZOptions;
|
|
16
|
+
ewma?: EwmaOptions;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class AnomalyCompositeModel implements MlPredictor {
|
|
20
|
+
constructor(
|
|
21
|
+
readonly population: RobustZModel,
|
|
22
|
+
readonly trend: EwmaModel,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
/** Per feature, per detector: z / threshold (≥ 1 = out of range). */
|
|
26
|
+
featureScores(x: ArrayLike<number>): { population: number; trend: number }[] {
|
|
27
|
+
const pz = this.population.zScores(x);
|
|
28
|
+
const tz = this.trend.zScores(x);
|
|
29
|
+
return pz.map((z, i) => ({ population: z / this.population.threshold, trend: tz[i]! / this.trend.threshold }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Fold a finished, non-anomalous observation into the trend. */
|
|
33
|
+
observe(x: ArrayLike<number>): void {
|
|
34
|
+
this.trend.update(x);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
predict(x: Float32Array): MlPrediction {
|
|
38
|
+
const score = Math.max(this.population.predict(x).scores[0]!, this.trend.predict(x).scores[0]!);
|
|
39
|
+
return { scores: [score], confidence: anomalyConfidence(score) };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Fit both detectors on a chronological history (feature vectors, oldest first). */
|
|
44
|
+
export function fitAnomalyComposite(samples: readonly MlSample[], opts: AnomalyCompositeOptions = {}): AnomalyCompositeModel {
|
|
45
|
+
return new AnomalyCompositeModel(fitRobustZ(samples, opts.robustZ), fitEwma(samples, opts.ewma));
|
|
46
|
+
}
|
package/src/ml/classical/ewma.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* Per feature, over the history IN ORDER: diff = x − μ; μ += α·diff;
|
|
6
6
|
* σ² = (1 − α)(σ² + α·diff²). A new point scores z = |x − μ| / σ against the
|
|
7
7
|
* state BEFORE it is folded in. Prediction: `scores[0] = max_i z_i / threshold`.
|
|
8
|
+
* σ is floored like robust z's scale: a fraction of |μ|, and `minScale` in absolute
|
|
9
|
+
* units — σ² starts at 0, so a constant history would otherwise flag any change.
|
|
8
10
|
*/
|
|
9
11
|
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
10
12
|
import { featureDim } from "./math.js";
|
|
11
|
-
import { anomalyConfidence } from "./robust_z.js";
|
|
13
|
+
import { anomalyConfidence, minScaleAt, type MinScale } from "./robust_z.js";
|
|
12
14
|
|
|
13
15
|
export interface EwmaOptions {
|
|
14
16
|
/** Smoothing factor in (0, 1]. Default 0.3. */
|
|
@@ -17,6 +19,8 @@ export interface EwmaOptions {
|
|
|
17
19
|
threshold?: number;
|
|
18
20
|
/** Smallest σ per feature, as a fraction of |μ| (plus 1e-9). Default 0.01. */
|
|
19
21
|
minScaleFraction?: number;
|
|
22
|
+
/** Smallest σ in absolute feature units, one or per feature. Default 0 (see `RobustZOptions.minScale`). */
|
|
23
|
+
minScale?: MinScale;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export class EwmaModel implements MlPredictor {
|
|
@@ -26,13 +30,14 @@ export class EwmaModel implements MlPredictor {
|
|
|
26
30
|
readonly alpha: number,
|
|
27
31
|
readonly threshold: number,
|
|
28
32
|
readonly minScaleFraction: number,
|
|
33
|
+
readonly minScale: MinScale = 0,
|
|
29
34
|
) {
|
|
30
35
|
if (!(alpha > 0 && alpha <= 1)) throw new Error(`EwmaModel: alpha must be in (0, 1] (got ${alpha})`);
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
zScores(x: ArrayLike<number>): number[] {
|
|
34
39
|
return Array.from(this.mean, (m, i) => {
|
|
35
|
-
const sd = Math.max(Math.sqrt(this.variance[i]!), this.minScaleFraction * Math.abs(m) + 1e-9);
|
|
40
|
+
const sd = Math.max(Math.sqrt(this.variance[i]!), this.minScaleFraction * Math.abs(m) + 1e-9, minScaleAt(this.minScale, i));
|
|
36
41
|
return Math.abs((x[i] ?? 0) - m) / sd;
|
|
37
42
|
});
|
|
38
43
|
}
|
|
@@ -62,6 +67,7 @@ export function fitEwma(samples: readonly MlSample[], opts: EwmaOptions = {}): E
|
|
|
62
67
|
opts.alpha ?? 0.3,
|
|
63
68
|
opts.threshold ?? 3,
|
|
64
69
|
opts.minScaleFraction ?? 0.01,
|
|
70
|
+
opts.minScale ?? 0,
|
|
65
71
|
);
|
|
66
72
|
for (let i = 1; i < samples.length; i++) model.update(samples[i]!.x);
|
|
67
73
|
return model;
|