@seanhogg/builderforce-memory 2026.6.34 → 2026.6.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/limbic/LimbicSession.d.ts +31 -1
- package/dist/limbic/LimbicSession.d.ts.map +1 -1
- package/dist/limbic/LimbicSession.js +47 -3
- package/dist/limbic/LimbicSession.js.map +1 -1
- package/dist/memory/MemoryStore.d.ts +18 -0
- package/dist/memory/MemoryStore.d.ts.map +1 -1
- package/dist/memory/MemoryStore.js +97 -17
- package/dist/memory/MemoryStore.js.map +1 -1
- package/dist/session/session.d.ts +9 -0
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +12 -2
- package/dist/session/session.js.map +1 -1
- package/dist/workflow/code-eval.d.ts +54 -0
- package/dist/workflow/code-eval.d.ts.map +1 -0
- package/dist/workflow/code-eval.js +135 -0
- package/dist/workflow/code-eval.js.map +1 -0
- package/dist/workflow/index.d.ts +3 -2
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +3 -2
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/steps.d.ts +20 -0
- package/dist/workflow/steps.d.ts.map +1 -1
- package/dist/workflow/steps.js +321 -7
- package/dist/workflow/steps.js.map +1 -1
- package/dist/workflow/templates.d.ts +15 -0
- package/dist/workflow/templates.d.ts.map +1 -1
- package/dist/workflow/templates.js +56 -1
- package/dist/workflow/templates.js.map +1 -1
- package/package.json +11 -8
- package/src/index.ts +6 -0
- package/src/limbic/LimbicSession.ts +61 -1
- package/src/memory/MemoryStore.ts +98 -14
- package/src/session/session.ts +23 -1
- package/src/workflow/code-eval.ts +143 -0
- package/src/workflow/index.ts +9 -1
- package/src/workflow/steps.ts +367 -6
- package/src/workflow/templates.ts +59 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanhogg/builderforce-memory",
|
|
3
|
-
"version": "2026.6.
|
|
3
|
+
"version": "2026.6.39",
|
|
4
4
|
"description": "BuilderForce Agent Memory — runtime layer. SSM execution, Transformer orchestration, online distillation, hybrid RAG retrieval, and persistent agent memory for BuilderForce.ai agents.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"homepage": "https://github.com/SeanHogg/builderforce-memory/tree/main/packages/memory#readme",
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@seanhogg/builderforce-memory-engine": "^2026.6.
|
|
56
|
+
"@seanhogg/builderforce-memory-engine": "^2026.6.39"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@jest/globals": "^29.7.0",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"ts-jest": "^29.2.0",
|
|
68
68
|
"typescript": "^6.0.3",
|
|
69
69
|
"@typescript/native-preview": "7.0.0-dev.20260627.2",
|
|
70
|
-
"@seanhogg/builderforce-memory-engine": "2026.6.
|
|
70
|
+
"@seanhogg/builderforce-memory-engine": "2026.6.39"
|
|
71
71
|
},
|
|
72
72
|
"jest": {
|
|
73
73
|
"preset": "ts-jest/presets/default-esm",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"strict": false,
|
|
86
86
|
"esModuleInterop": true,
|
|
87
87
|
"skipLibCheck": true,
|
|
88
|
+
"rootDir": "..",
|
|
88
89
|
"types": [
|
|
89
90
|
"@webgpu/types",
|
|
90
91
|
"jest"
|
|
@@ -94,19 +95,21 @@
|
|
|
94
95
|
]
|
|
95
96
|
},
|
|
96
97
|
"moduleNameMapper": {
|
|
98
|
+
"^@seanhogg/builderforce-memory-engine$": "<rootDir>/../memory-engine/src/index.ts",
|
|
97
99
|
"^(\\.{1,2}/.*)\\.js$": "$1"
|
|
98
100
|
},
|
|
99
101
|
"collectCoverageFrom": [
|
|
100
102
|
"src/**/*.ts",
|
|
101
103
|
"!src/**/index.ts",
|
|
102
|
-
"!src/session/**"
|
|
104
|
+
"!src/session/**",
|
|
105
|
+
"!src/limbic/LimbicSession.ts"
|
|
103
106
|
],
|
|
104
107
|
"coverageThreshold": {
|
|
105
108
|
"global": {
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
109
|
+
"statements": 90,
|
|
110
|
+
"branches": 85,
|
|
111
|
+
"functions": 95,
|
|
112
|
+
"lines": 92
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
},
|
package/src/index.ts
CHANGED
|
@@ -217,9 +217,12 @@ export {
|
|
|
217
217
|
BUILTIN_STEPS,
|
|
218
218
|
AGENTIC_SEVEN_LAYER,
|
|
219
219
|
TRAIN_LLM,
|
|
220
|
+
TEACH_CODE,
|
|
220
221
|
WORKFLOW_TEMPLATES,
|
|
221
222
|
getTemplate,
|
|
222
223
|
cloneTemplate,
|
|
224
|
+
analyzeCode,
|
|
225
|
+
runJsCases,
|
|
223
226
|
} from './workflow/index.js';
|
|
224
227
|
export type {
|
|
225
228
|
WorkflowConfig,
|
|
@@ -228,6 +231,9 @@ export type {
|
|
|
228
231
|
StepFactory,
|
|
229
232
|
WorkflowValidationError,
|
|
230
233
|
RunWorkflowOptions,
|
|
234
|
+
CodeAnalysis,
|
|
235
|
+
CodeCase,
|
|
236
|
+
CodeEvalResult,
|
|
231
237
|
} from './workflow/index.js';
|
|
232
238
|
|
|
233
239
|
// ── Publish (ship an engine model export to HF / local folder) ──────────────────
|
|
@@ -24,10 +24,15 @@ import {
|
|
|
24
24
|
createBindGroup,
|
|
25
25
|
dispatchKernel,
|
|
26
26
|
readBuffer,
|
|
27
|
+
personalitySetpoint,
|
|
28
|
+
clampState,
|
|
29
|
+
neutralState,
|
|
30
|
+
LIMBIC_STATE_DIM,
|
|
27
31
|
type LimbicModelConfig,
|
|
28
32
|
type LimbicForward,
|
|
29
33
|
type LimbicSample,
|
|
30
34
|
type LimbicTrainOptions,
|
|
35
|
+
type PersonalityTraits,
|
|
31
36
|
} from "@seanhogg/builderforce-memory-engine";
|
|
32
37
|
|
|
33
38
|
import { saveToIndexedDB, loadFromIndexedDB } from "../session/persistence.js";
|
|
@@ -49,6 +54,18 @@ export interface LimbicSessionOptions {
|
|
|
49
54
|
modelConfig?: Partial<LimbicModelConfig>;
|
|
50
55
|
/** Deterministic init seed. */
|
|
51
56
|
seed?: number;
|
|
57
|
+
/**
|
|
58
|
+
* The agent's static personality traits (0..100). Mapped to the resting affective
|
|
59
|
+
* SETPOINT the learned dynamics ride on top of, so training + relaxation happen
|
|
60
|
+
* around the personality baseline instead of neutral ("personality = setpoints,
|
|
61
|
+
* limbic = dynamics"). Ignored when {@link personalitySetpoint} is given.
|
|
62
|
+
*/
|
|
63
|
+
personalityTraits?: PersonalityTraits;
|
|
64
|
+
/**
|
|
65
|
+
* An explicit 8-dim resting setpoint (e.g. one already derived by the runtime's
|
|
66
|
+
* `deriveLimbicSetpoints`). Overrides {@link personalityTraits}. Clamped to bounds.
|
|
67
|
+
*/
|
|
68
|
+
personalitySetpoint?: ArrayLike<number>;
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
interface StepBuffers {
|
|
@@ -64,6 +81,11 @@ export class LimbicSession {
|
|
|
64
81
|
readonly trainer: LimbicTrainer;
|
|
65
82
|
readonly device: GPUDevice | null;
|
|
66
83
|
readonly gpuMode: LimbicGpuMode;
|
|
84
|
+
/** The personality-conditioned resting setpoint (8-dim). NEUTRAL when no
|
|
85
|
+
* personality was supplied. The homeostatic baseline the learned dynamics
|
|
86
|
+
* settle toward — see {@link baselineState}. Mutable via {@link setSetpoint} so a
|
|
87
|
+
* long-lived service can re-point it when the active personas change. */
|
|
88
|
+
setpoint: Float32Array;
|
|
67
89
|
private readonly _name: string;
|
|
68
90
|
private readonly _idbFactory: IDBFactory | undefined;
|
|
69
91
|
|
|
@@ -80,6 +102,7 @@ export class LimbicSession {
|
|
|
80
102
|
gpuMode: LimbicGpuMode,
|
|
81
103
|
name: string,
|
|
82
104
|
idbFactory: IDBFactory | undefined,
|
|
105
|
+
setpoint: Float32Array,
|
|
83
106
|
) {
|
|
84
107
|
this.model = model;
|
|
85
108
|
this.trainer = trainer;
|
|
@@ -87,6 +110,32 @@ export class LimbicSession {
|
|
|
87
110
|
this.gpuMode = gpuMode;
|
|
88
111
|
this._name = name;
|
|
89
112
|
this._idbFactory = idbFactory;
|
|
113
|
+
this.setpoint = setpoint;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A fresh copy of the personality-conditioned resting state. Seed training-sample
|
|
118
|
+
* states and initial affect from this so the learned dynamics move AROUND the
|
|
119
|
+
* agent's personality baseline rather than a fixed neutral. NEUTRAL when the
|
|
120
|
+
* session was created with no personality.
|
|
121
|
+
*/
|
|
122
|
+
baselineState(): Float32Array {
|
|
123
|
+
return Float32Array.from(this.setpoint);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Re-point the resting setpoint after construction — e.g. a long-lived service
|
|
128
|
+
* recomputing it when the active personas change. An explicit 8-dim vector wins;
|
|
129
|
+
* else the traits are mapped; else neutral. Clamped to bounds.
|
|
130
|
+
*/
|
|
131
|
+
setSetpoint(traits?: PersonalityTraits, explicit?: ArrayLike<number>): void {
|
|
132
|
+
if (explicit && explicit.length >= LIMBIC_STATE_DIM) {
|
|
133
|
+
this.setpoint = clampState(Float32Array.from(explicit));
|
|
134
|
+
} else if (traits) {
|
|
135
|
+
this.setpoint = personalitySetpoint(traits);
|
|
136
|
+
} else {
|
|
137
|
+
this.setpoint = neutralState();
|
|
138
|
+
}
|
|
90
139
|
}
|
|
91
140
|
|
|
92
141
|
static async create(options: LimbicSessionOptions = {}): Promise<LimbicSession> {
|
|
@@ -129,7 +178,18 @@ export class LimbicSession {
|
|
|
129
178
|
model.loadWeights(options.checkpointBuffer);
|
|
130
179
|
}
|
|
131
180
|
const trainer = new LimbicTrainer(model, device);
|
|
132
|
-
|
|
181
|
+
// Resolve the resting setpoint: an explicit 8-dim vector wins; else map the
|
|
182
|
+
// personality traits; else neutral. This is where the STATIC personality enters
|
|
183
|
+
// the (dynamic, trainable) limbic system.
|
|
184
|
+
let setpoint: Float32Array;
|
|
185
|
+
if (options.personalitySetpoint && options.personalitySetpoint.length >= LIMBIC_STATE_DIM) {
|
|
186
|
+
setpoint = clampState(Float32Array.from(options.personalitySetpoint));
|
|
187
|
+
} else if (options.personalityTraits) {
|
|
188
|
+
setpoint = personalitySetpoint(options.personalityTraits);
|
|
189
|
+
} else {
|
|
190
|
+
setpoint = neutralState();
|
|
191
|
+
}
|
|
192
|
+
return new LimbicSession(model, trainer, device, gpuMode, options.name ?? "limbic-default", options.idbFactory, setpoint);
|
|
133
193
|
}
|
|
134
194
|
|
|
135
195
|
/** One affect step. Uses the GPU kernel when a device is available, else CPU. */
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { SSMError } from '../errors/SSMError.js';
|
|
12
12
|
import { tokenize, jaccardSimilarity } from '../similarity/index.js';
|
|
13
|
-
import { hybridRetrieve, denseSearch, type RetrievalCandidate, type HybridRetrieveOptions } from '../retrieval/index.js';
|
|
13
|
+
import { hybridRetrieve, denseSearch, HnswIndex, type RetrievalCandidate, type HybridRetrieveOptions } from '../retrieval/index.js';
|
|
14
14
|
|
|
15
15
|
export type FactType = 'text' | 'json' | 'number' | 'boolean';
|
|
16
16
|
|
|
@@ -116,6 +116,20 @@ export class MemoryStore {
|
|
|
116
116
|
private _sweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
117
117
|
/** Content → L2-normalised embedding cache, used by recallSimilar. */
|
|
118
118
|
private readonly _embedCache = new Map<string, Float32Array>();
|
|
119
|
+
/** Runtime whose embeddings populate `_embedCache`; a change invalidates it. */
|
|
120
|
+
private _embedRuntime: unknown = null;
|
|
121
|
+
|
|
122
|
+
// ── Persistent dense index (EVM-1b) ────────────────────────────────────────
|
|
123
|
+
// Fact embeddings keyed by `${key}@${seq}` so an EDIT (seq bump) is a new key
|
|
124
|
+
// and an unchanged fact is never re-embedded. The HNSW index is derived from
|
|
125
|
+
// these vectors and reused across recalls until the fact set changes — so
|
|
126
|
+
// steady-state recall is O(1) query-embed + O(log N) search, not O(N) embed.
|
|
127
|
+
private readonly _denseVectors = new Map<string, Float32Array>();
|
|
128
|
+
private _denseIndex: HnswIndex | null = null;
|
|
129
|
+
/** Signature of the fact set the current index was built from. */
|
|
130
|
+
private _denseSig = '';
|
|
131
|
+
/** Identity of the runtime whose embeddings populated the vectors. */
|
|
132
|
+
private _denseRuntime: unknown = null;
|
|
119
133
|
|
|
120
134
|
constructor(opts: MemoryStoreOptions = {}) {
|
|
121
135
|
this._dbName = opts.dbName ?? 'ssmjs';
|
|
@@ -321,19 +335,16 @@ export class MemoryStore {
|
|
|
321
335
|
if (runtime != null && typeof runtime.embed === 'function') {
|
|
322
336
|
const queryVec = await this._embedWithCache(runtime, query);
|
|
323
337
|
if (queryVec) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
// EVM-1: exact scan under the threshold, HNSW ANN above it.
|
|
335
|
-
const hits = denseSearch(queryVec, items, topK, this._annThreshold);
|
|
336
|
-
return hits.map(h => byKey.get(h.id)).filter((e): e is MemoryEntry => !!e);
|
|
338
|
+
// EVM-1b: sync the persistent vector store + index (embeds only the
|
|
339
|
+
// delta), then query it — no per-call re-embed of every fact.
|
|
340
|
+
const synced = await this._syncDense(runtime, all);
|
|
341
|
+
if (synced) {
|
|
342
|
+
const hits = synced.index
|
|
343
|
+
// EVM-1: above the threshold, query the persistent HNSW (O(log N)).
|
|
344
|
+
? synced.index.search(queryVec, topK)
|
|
345
|
+
// Below it, an exact scan over the (already-embedded) vectors.
|
|
346
|
+
: denseSearch(queryVec, synced.items, topK, this._annThreshold);
|
|
347
|
+
return hits.map(h => synced.byKey.get(h.id)).filter((e): e is MemoryEntry => !!e);
|
|
337
348
|
}
|
|
338
349
|
}
|
|
339
350
|
// Any failure falls through to the Jaccard path below.
|
|
@@ -392,6 +403,12 @@ export class MemoryStore {
|
|
|
392
403
|
* unavailable so callers can fall back to lexical similarity.
|
|
393
404
|
*/
|
|
394
405
|
private async _embedWithCache(runtime: SSMRuntimeRef, text: string): Promise<Float32Array | null> {
|
|
406
|
+
// Embeddings are model-specific: if the runtime instance changed, the cached
|
|
407
|
+
// vectors are from a different model and must be dropped. (EVM-1b)
|
|
408
|
+
if (runtime !== this._embedRuntime) {
|
|
409
|
+
this._embedCache.clear();
|
|
410
|
+
this._embedRuntime = runtime;
|
|
411
|
+
}
|
|
395
412
|
const cached = this._embedCache.get(text);
|
|
396
413
|
if (cached) {
|
|
397
414
|
// LRU touch: re-insert so this entry becomes most-recently-used. (EVM-8)
|
|
@@ -418,6 +435,68 @@ export class MemoryStore {
|
|
|
418
435
|
return null;
|
|
419
436
|
}
|
|
420
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Synchronise the persistent dense vector store + HNSW index to `all` (EVM-1b).
|
|
440
|
+
* Embeds ONLY new/changed facts (keyed by `key@seq`), prunes removed ones, and
|
|
441
|
+
* rebuilds the index only when the fact set actually changed — so a read-heavy
|
|
442
|
+
* workload reuses both the embeddings and the index across recalls. Returns
|
|
443
|
+
* `null` (caller falls back to lexical) if any embedding is unavailable, or
|
|
444
|
+
* resets everything when the runtime identity changes (embeddings are
|
|
445
|
+
* model-specific).
|
|
446
|
+
*/
|
|
447
|
+
private async _syncDense(
|
|
448
|
+
runtime: SSMRuntimeRef,
|
|
449
|
+
all: MemoryEntry[],
|
|
450
|
+
): Promise<{ byKey: Map<string, MemoryEntry>; items: Array<{ id: string; vector: Float32Array }>; index: HnswIndex | null } | null> {
|
|
451
|
+
if (runtime !== this._denseRuntime) {
|
|
452
|
+
this._denseVectors.clear();
|
|
453
|
+
this._denseIndex = null;
|
|
454
|
+
this._denseSig = '';
|
|
455
|
+
this._denseRuntime = runtime;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const vkeyOf = (e: MemoryEntry): string => `${e.key}@${e.seq ?? 0}`;
|
|
459
|
+
const currentVkeys = new Set<string>();
|
|
460
|
+
let sig = `${all.length}`;
|
|
461
|
+
for (const e of all) { const k = vkeyOf(e); currentVkeys.add(k); sig += '|' + k; }
|
|
462
|
+
|
|
463
|
+
const byKey = new Map<string, MemoryEntry>();
|
|
464
|
+
const items: Array<{ id: string; vector: Float32Array }> = [];
|
|
465
|
+
for (const entry of all) {
|
|
466
|
+
const vk = vkeyOf(entry);
|
|
467
|
+
let vec = this._denseVectors.get(vk);
|
|
468
|
+
if (!vec) {
|
|
469
|
+
const v = await this._embedWithCache(runtime, entry.content);
|
|
470
|
+
if (!v) return null; // embedding failed → lexical fallback
|
|
471
|
+
vec = v;
|
|
472
|
+
this._denseVectors.set(vk, vec);
|
|
473
|
+
}
|
|
474
|
+
items.push({ id: entry.key, vector: vec });
|
|
475
|
+
byKey.set(entry.key, entry);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Drop vectors for facts that are gone or were edited (stale vkeys).
|
|
479
|
+
if (this._denseVectors.size > currentVkeys.size) {
|
|
480
|
+
for (const k of this._denseVectors.keys()) {
|
|
481
|
+
if (!currentVkeys.has(k)) this._denseVectors.delete(k);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Maintain the ANN index only above the threshold; (re)build it only when
|
|
486
|
+
// the fact set changed (no embedding involved — purely in-memory).
|
|
487
|
+
if (all.length >= this._annThreshold) {
|
|
488
|
+
if (sig !== this._denseSig || this._denseIndex === null) {
|
|
489
|
+
const index = new HnswIndex();
|
|
490
|
+
for (const it of items) index.add(it.id, it.vector);
|
|
491
|
+
this._denseIndex = index;
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
this._denseIndex = null;
|
|
495
|
+
}
|
|
496
|
+
this._denseSig = sig;
|
|
497
|
+
return { byKey, items, index: this._denseIndex };
|
|
498
|
+
}
|
|
499
|
+
|
|
421
500
|
/**
|
|
422
501
|
* Hard-deletes all entries whose TTL has expired.
|
|
423
502
|
* @returns The number of entries deleted.
|
|
@@ -451,6 +530,11 @@ export class MemoryStore {
|
|
|
451
530
|
/** Deletes all facts. Does not affect saved weights. */
|
|
452
531
|
async clear(): Promise<void> {
|
|
453
532
|
const db = await this._open();
|
|
533
|
+
// Drop the persistent dense index/vectors too (EVM-1b) — they describe a
|
|
534
|
+
// fact set that no longer exists.
|
|
535
|
+
this._denseVectors.clear();
|
|
536
|
+
this._denseIndex = null;
|
|
537
|
+
this._denseSig = '';
|
|
454
538
|
|
|
455
539
|
const tx = db.transaction(FACTS_STORE, 'readwrite');
|
|
456
540
|
return requestToPromise(tx.objectStore(FACTS_STORE).clear(), 'Failed to clear facts', () => undefined);
|
package/src/session/session.ts
CHANGED
|
@@ -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
|
-
|
|
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) {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workflow/code-eval.ts — dependency-free code-validity + execution-reward checks.
|
|
3
|
+
*
|
|
4
|
+
* These power the code-specific BUILD diagnostics that grade what Evermind's
|
|
5
|
+
* generator produces when we teach it to program:
|
|
6
|
+
* • {@link analyzeCode} — structural validity (balanced delimiters/strings) +
|
|
7
|
+
* an optional JS parse. The portable "does this look
|
|
8
|
+
* like valid code" gate.
|
|
9
|
+
* • {@link runJsCases} — execution-grounded reward: run generated JS against
|
|
10
|
+
* test cases and score the pass-rate. Code's signal is
|
|
11
|
+
* that it RUNS — this turns that into a number.
|
|
12
|
+
*
|
|
13
|
+
* Both are zero-dependency (no AST library) so they run in the same WebGPU/TS
|
|
14
|
+
* runtime as the rest of the engine. Execution uses the `Function` constructor —
|
|
15
|
+
* a heuristic reward sandbox, NOT a security boundary; only feed it code from a
|
|
16
|
+
* trusted teacher / the model under training.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const OPEN: Record<string, string> = { ")": "(", "]": "[", "}": "{" };
|
|
20
|
+
|
|
21
|
+
export interface CodeAnalysis {
|
|
22
|
+
/** 0..1 structural-validity score (1 = delimiters and strings all balanced). */
|
|
23
|
+
score: number;
|
|
24
|
+
/** True when every bracket/brace/paren and string literal closes cleanly. */
|
|
25
|
+
balanced: boolean;
|
|
26
|
+
/** For language="js": whether the source parses (Function constructor). undefined otherwise. */
|
|
27
|
+
jsParse?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Score how structurally valid `code` is: walk the source tracking string state
|
|
32
|
+
* (so brackets inside strings are ignored) and a delimiter stack. A clean parse
|
|
33
|
+
* scores 1; each unmatched/mismatched delimiter or unterminated string lowers it.
|
|
34
|
+
*/
|
|
35
|
+
export function analyzeCode(code: string, language = "js"): CodeAnalysis {
|
|
36
|
+
const stack: string[] = [];
|
|
37
|
+
let mismatches = 0;
|
|
38
|
+
let delims = 0;
|
|
39
|
+
let inString: string | null = null; // the opening quote char, or null
|
|
40
|
+
let escaped = false;
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < code.length; i++) {
|
|
43
|
+
const ch = code[i]!;
|
|
44
|
+
if (inString) {
|
|
45
|
+
if (escaped) { escaped = false; continue; }
|
|
46
|
+
if (ch === "\\") { escaped = true; continue; }
|
|
47
|
+
if (ch === inString) inString = null;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ch === '"' || ch === "'" || ch === "`") { inString = ch; continue; }
|
|
51
|
+
if (ch === "(" || ch === "[" || ch === "{") { stack.push(ch); delims++; continue; }
|
|
52
|
+
if (ch === ")" || ch === "]" || ch === "}") {
|
|
53
|
+
delims++;
|
|
54
|
+
if (stack.pop() !== OPEN[ch]) mismatches++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Anything left open is an unmatched delimiter; an open string is one more fault.
|
|
58
|
+
mismatches += stack.length;
|
|
59
|
+
const faults = mismatches + (inString ? 1 : 0);
|
|
60
|
+
const denom = Math.max(1, delims + (inString ? 1 : 0));
|
|
61
|
+
const balanced = faults === 0;
|
|
62
|
+
const score = balanced ? 1 : Math.max(0, 1 - faults / denom);
|
|
63
|
+
|
|
64
|
+
const analysis: CodeAnalysis = { score, balanced };
|
|
65
|
+
if (language === "js" || language === "ts" || language === "javascript" || language === "typescript") {
|
|
66
|
+
analysis.jsParse = jsParses(code);
|
|
67
|
+
}
|
|
68
|
+
return analysis;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** True when `code` parses as JS (Function constructor parses but does not run). */
|
|
72
|
+
function jsParses(code: string): boolean {
|
|
73
|
+
try {
|
|
74
|
+
// eslint-disable-next-line no-new-func
|
|
75
|
+
new Function(code);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface CodeCase {
|
|
83
|
+
/** An expression to evaluate after the code is loaded, e.g. "add(2, 3)". */
|
|
84
|
+
call: string;
|
|
85
|
+
/** The expected result (deep-equal compared). */
|
|
86
|
+
expect: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface CodeEvalResult {
|
|
90
|
+
passed: number;
|
|
91
|
+
total: number;
|
|
92
|
+
/** passed / total (0 when there are no cases). */
|
|
93
|
+
passRate: number;
|
|
94
|
+
/** A whole-program error (e.g. syntax error) that failed every case, if any. */
|
|
95
|
+
error?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Load `code` (which defines functions) and evaluate each case's `call`
|
|
100
|
+
* expression, comparing the result to `expect`. A syntax error in `code` fails
|
|
101
|
+
* every case. Per-case runtime errors fail only that case.
|
|
102
|
+
*
|
|
103
|
+
* NOTE: heuristic reward sandbox, not a security boundary — see file header.
|
|
104
|
+
*/
|
|
105
|
+
export function runJsCases(code: string, cases: CodeCase[]): CodeEvalResult {
|
|
106
|
+
const total = cases.length;
|
|
107
|
+
if (total === 0) return { passed: 0, total: 0, passRate: 0 };
|
|
108
|
+
|
|
109
|
+
let passed = 0;
|
|
110
|
+
for (const c of cases) {
|
|
111
|
+
try {
|
|
112
|
+
// Code defines functions (hoisted); the trailing return evaluates the call.
|
|
113
|
+
// eslint-disable-next-line no-new-func
|
|
114
|
+
const fn = new Function(`${code}\n;return (${c.call});`);
|
|
115
|
+
const actual = fn();
|
|
116
|
+
if (deepEqual(actual, c.expect)) passed++;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
// A syntax error throws at construction for ALL cases — report it once.
|
|
119
|
+
if (err instanceof SyntaxError) {
|
|
120
|
+
return { passed: 0, total, passRate: 0, error: err.message };
|
|
121
|
+
}
|
|
122
|
+
// Runtime error → this case simply fails.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { passed, total, passRate: passed / total };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Structural deep equality good enough for JSON-shaped test expectations. */
|
|
129
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
130
|
+
if (a === b) return true;
|
|
131
|
+
if (typeof a !== typeof b || a === null || b === null) return false;
|
|
132
|
+
if (typeof a !== "object") return Object.is(a, b);
|
|
133
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
134
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
135
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
136
|
+
}
|
|
137
|
+
const ao = a as Record<string, unknown>;
|
|
138
|
+
const bo = b as Record<string, unknown>;
|
|
139
|
+
const ak = Object.keys(ao);
|
|
140
|
+
const bk = Object.keys(bo);
|
|
141
|
+
if (ak.length !== bk.length) return false;
|
|
142
|
+
return ak.every((k) => deepEqual(ao[k], bo[k]));
|
|
143
|
+
}
|
package/src/workflow/index.ts
CHANGED
|
@@ -6,14 +6,22 @@
|
|
|
6
6
|
|
|
7
7
|
export type { WorkflowConfig, WorkflowStepConfig, StepTypeInfo, StepFactory } from "./types.js";
|
|
8
8
|
export { StepTypeRegistry, createDefaultRegistry, defaultStepRegistry } from "./registry.js";
|
|
9
|
-
export { BUILTIN_STEPS } from "./steps.js";
|
|
9
|
+
export { BUILTIN_STEPS, selectBestCandidate, stripCodeFences } from "./steps.js";
|
|
10
10
|
export {
|
|
11
11
|
AGENTIC_SEVEN_LAYER,
|
|
12
12
|
TRAIN_LLM,
|
|
13
|
+
TEACH_CODE,
|
|
13
14
|
WORKFLOW_TEMPLATES,
|
|
14
15
|
getTemplate,
|
|
15
16
|
cloneTemplate,
|
|
16
17
|
} from "./templates.js";
|
|
18
|
+
export {
|
|
19
|
+
analyzeCode,
|
|
20
|
+
runJsCases,
|
|
21
|
+
type CodeAnalysis,
|
|
22
|
+
type CodeCase,
|
|
23
|
+
type CodeEvalResult,
|
|
24
|
+
} from "./code-eval.js";
|
|
17
25
|
export {
|
|
18
26
|
compileWorkflow,
|
|
19
27
|
validateWorkflow,
|