@seanhogg/builderforce-memory 2026.6.18

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +582 -0
  3. package/dist/agent/SSMAgent.d.ts +146 -0
  4. package/dist/agent/SSMAgent.d.ts.map +1 -0
  5. package/dist/agent/SSMAgent.js +231 -0
  6. package/dist/agent/SSMAgent.js.map +1 -0
  7. package/dist/agent/index.d.ts +3 -0
  8. package/dist/agent/index.d.ts.map +1 -0
  9. package/dist/agent/index.js +2 -0
  10. package/dist/agent/index.js.map +1 -0
  11. package/dist/bridges/AnthropicBridge.d.ts +47 -0
  12. package/dist/bridges/AnthropicBridge.d.ts.map +1 -0
  13. package/dist/bridges/AnthropicBridge.js +120 -0
  14. package/dist/bridges/AnthropicBridge.js.map +1 -0
  15. package/dist/bridges/CachingBridge.d.ts +44 -0
  16. package/dist/bridges/CachingBridge.d.ts.map +1 -0
  17. package/dist/bridges/CachingBridge.js +62 -0
  18. package/dist/bridges/CachingBridge.js.map +1 -0
  19. package/dist/bridges/FetchBridge.d.ts +30 -0
  20. package/dist/bridges/FetchBridge.d.ts.map +1 -0
  21. package/dist/bridges/FetchBridge.js +24 -0
  22. package/dist/bridges/FetchBridge.js.map +1 -0
  23. package/dist/bridges/OpenAIBridge.d.ts +33 -0
  24. package/dist/bridges/OpenAIBridge.d.ts.map +1 -0
  25. package/dist/bridges/OpenAIBridge.js +110 -0
  26. package/dist/bridges/OpenAIBridge.js.map +1 -0
  27. package/dist/bridges/ResponseCache.d.ts +65 -0
  28. package/dist/bridges/ResponseCache.d.ts.map +1 -0
  29. package/dist/bridges/ResponseCache.js +97 -0
  30. package/dist/bridges/ResponseCache.js.map +1 -0
  31. package/dist/bridges/SemanticCachingBridge.d.ts +31 -0
  32. package/dist/bridges/SemanticCachingBridge.d.ts.map +1 -0
  33. package/dist/bridges/SemanticCachingBridge.js +44 -0
  34. package/dist/bridges/SemanticCachingBridge.js.map +1 -0
  35. package/dist/bridges/TransformerBridge.d.ts +35 -0
  36. package/dist/bridges/TransformerBridge.d.ts.map +1 -0
  37. package/dist/bridges/TransformerBridge.js +10 -0
  38. package/dist/bridges/TransformerBridge.js.map +1 -0
  39. package/dist/bridges/index.d.ts +14 -0
  40. package/dist/bridges/index.d.ts.map +1 -0
  41. package/dist/bridges/index.js +7 -0
  42. package/dist/bridges/index.js.map +1 -0
  43. package/dist/cache/FetchSemanticCacheBackend.d.ts +40 -0
  44. package/dist/cache/FetchSemanticCacheBackend.d.ts.map +1 -0
  45. package/dist/cache/FetchSemanticCacheBackend.js +61 -0
  46. package/dist/cache/FetchSemanticCacheBackend.js.map +1 -0
  47. package/dist/cache/SemanticCache.d.ts +105 -0
  48. package/dist/cache/SemanticCache.d.ts.map +1 -0
  49. package/dist/cache/SemanticCache.js +130 -0
  50. package/dist/cache/SemanticCache.js.map +1 -0
  51. package/dist/cache/index.d.ts +5 -0
  52. package/dist/cache/index.d.ts.map +1 -0
  53. package/dist/cache/index.js +3 -0
  54. package/dist/cache/index.js.map +1 -0
  55. package/dist/distillation/DistillationEngine.d.ts +107 -0
  56. package/dist/distillation/DistillationEngine.d.ts.map +1 -0
  57. package/dist/distillation/DistillationEngine.js +152 -0
  58. package/dist/distillation/DistillationEngine.js.map +1 -0
  59. package/dist/distillation/index.d.ts +3 -0
  60. package/dist/distillation/index.d.ts.map +1 -0
  61. package/dist/distillation/index.js +2 -0
  62. package/dist/distillation/index.js.map +1 -0
  63. package/dist/errors/SSMError.d.ts +14 -0
  64. package/dist/errors/SSMError.d.ts.map +1 -0
  65. package/dist/errors/SSMError.js +18 -0
  66. package/dist/errors/SSMError.js.map +1 -0
  67. package/dist/errors/index.d.ts +3 -0
  68. package/dist/errors/index.d.ts.map +1 -0
  69. package/dist/errors/index.js +2 -0
  70. package/dist/errors/index.js.map +1 -0
  71. package/dist/index.d.ts +65 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +59 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/memory/MemoryStore.d.ts +152 -0
  76. package/dist/memory/MemoryStore.d.ts.map +1 -0
  77. package/dist/memory/MemoryStore.js +290 -0
  78. package/dist/memory/MemoryStore.js.map +1 -0
  79. package/dist/memory/index.d.ts +3 -0
  80. package/dist/memory/index.d.ts.map +1 -0
  81. package/dist/memory/index.js +2 -0
  82. package/dist/memory/index.js.map +1 -0
  83. package/dist/router/InferenceRouter.d.ts +92 -0
  84. package/dist/router/InferenceRouter.d.ts.map +1 -0
  85. package/dist/router/InferenceRouter.js +113 -0
  86. package/dist/router/InferenceRouter.js.map +1 -0
  87. package/dist/router/index.d.ts +3 -0
  88. package/dist/router/index.d.ts.map +1 -0
  89. package/dist/router/index.js +2 -0
  90. package/dist/router/index.js.map +1 -0
  91. package/dist/runtime/SSMRuntime.d.ts +167 -0
  92. package/dist/runtime/SSMRuntime.d.ts.map +1 -0
  93. package/dist/runtime/SSMRuntime.js +199 -0
  94. package/dist/runtime/SSMRuntime.js.map +1 -0
  95. package/dist/runtime/index.d.ts +3 -0
  96. package/dist/runtime/index.d.ts.map +1 -0
  97. package/dist/runtime/index.js +2 -0
  98. package/dist/runtime/index.js.map +1 -0
  99. package/dist/session/errors.d.ts +10 -0
  100. package/dist/session/errors.d.ts.map +1 -0
  101. package/dist/session/errors.js +14 -0
  102. package/dist/session/errors.js.map +1 -0
  103. package/dist/session/index.d.ts +11 -0
  104. package/dist/session/index.d.ts.map +1 -0
  105. package/dist/session/index.js +7 -0
  106. package/dist/session/index.js.map +1 -0
  107. package/dist/session/persistence.d.ts +14 -0
  108. package/dist/session/persistence.d.ts.map +1 -0
  109. package/dist/session/persistence.js +100 -0
  110. package/dist/session/persistence.js.map +1 -0
  111. package/dist/session/presets.d.ts +31 -0
  112. package/dist/session/presets.d.ts.map +1 -0
  113. package/dist/session/presets.js +91 -0
  114. package/dist/session/presets.js.map +1 -0
  115. package/dist/session/session.d.ts +186 -0
  116. package/dist/session/session.d.ts.map +1 -0
  117. package/dist/session/session.js +358 -0
  118. package/dist/session/session.js.map +1 -0
  119. package/dist/session/streaming.d.ts +13 -0
  120. package/dist/session/streaming.d.ts.map +1 -0
  121. package/dist/session/streaming.js +74 -0
  122. package/dist/session/streaming.js.map +1 -0
  123. package/dist/session/tokenizer.d.ts +18 -0
  124. package/dist/session/tokenizer.d.ts.map +1 -0
  125. package/dist/session/tokenizer.js +11 -0
  126. package/dist/session/tokenizer.js.map +1 -0
  127. package/dist/similarity/index.d.ts +19 -0
  128. package/dist/similarity/index.d.ts.map +1 -0
  129. package/dist/similarity/index.js +42 -0
  130. package/dist/similarity/index.js.map +1 -0
  131. package/package.json +120 -0
  132. package/src/agent/SSMAgent.ts +327 -0
  133. package/src/agent/index.ts +2 -0
  134. package/src/bridges/AnthropicBridge.ts +166 -0
  135. package/src/bridges/CachingBridge.ts +79 -0
  136. package/src/bridges/FetchBridge.ts +41 -0
  137. package/src/bridges/OpenAIBridge.ts +143 -0
  138. package/src/bridges/ResponseCache.ts +131 -0
  139. package/src/bridges/SemanticCachingBridge.ts +60 -0
  140. package/src/bridges/TransformerBridge.ts +38 -0
  141. package/src/bridges/index.ts +13 -0
  142. package/src/cache/FetchSemanticCacheBackend.ts +79 -0
  143. package/src/cache/SemanticCache.ts +196 -0
  144. package/src/cache/index.ts +9 -0
  145. package/src/distillation/DistillationEngine.ts +248 -0
  146. package/src/distillation/index.ts +2 -0
  147. package/src/errors/SSMError.ts +26 -0
  148. package/src/errors/index.ts +2 -0
  149. package/src/index.ts +128 -0
  150. package/src/memory/MemoryStore.ts +408 -0
  151. package/src/memory/index.ts +2 -0
  152. package/src/router/InferenceRouter.ts +201 -0
  153. package/src/router/index.ts +2 -0
  154. package/src/runtime/SSMRuntime.ts +309 -0
  155. package/src/runtime/index.ts +2 -0
  156. package/src/session/errors.ts +24 -0
  157. package/src/session/index.ts +25 -0
  158. package/src/session/persistence.ts +142 -0
  159. package/src/session/presets.ts +122 -0
  160. package/src/session/session.ts +657 -0
  161. package/src/session/streaming.ts +97 -0
  162. package/src/session/tokenizer.ts +18 -0
  163. package/src/similarity/index.ts +42 -0
@@ -0,0 +1,408 @@
1
+ /**
2
+ * MemoryStore – persistent key-value fact store and weight checkpoint helper.
3
+ *
4
+ * Uses IndexedDB with a dedicated 'ssmjs' database containing two object stores:
5
+ * - 'facts' : MemoryEntry records keyed by the fact key string
6
+ * - 'weights' : a single ArrayBuffer keyed by `weightsKey`
7
+ *
8
+ * Weight save/load is delegated to the SSMRuntime passed to saveWeights/loadWeights.
9
+ */
10
+
11
+ import { SSMError } from '../errors/SSMError.js';
12
+ import { tokenize, jaccardSimilarity, cosineSimilarity } from '../similarity/index.js';
13
+
14
+ export type FactType = 'text' | 'json' | 'number' | 'boolean';
15
+
16
+ export interface MemoryEntry {
17
+ key : string;
18
+ content : string;
19
+ timestamp : number;
20
+ /**
21
+ * Monotonic write sequence, used only to break `timestamp` ties so that
22
+ * ordering stays deterministic when several entries are written within the
23
+ * same millisecond (Date.now() resolution). Higher = written later.
24
+ * Optional for backward compatibility with entries persisted/imported
25
+ * before this field existed (those sort as seq 0).
26
+ */
27
+ seq? : number;
28
+ /** Optional time-to-live in milliseconds. */
29
+ ttlMs? : number;
30
+ /** Semantic type of the stored value. Default: 'text'. */
31
+ type? : FactType;
32
+ /** Tags for grouping and filtering. */
33
+ tags? : string[];
34
+ /** Importance weight in the range 0–1. Default: 0.5. */
35
+ importance?: number;
36
+ }
37
+
38
+ export interface RememberOptions {
39
+ /** Override the store-level defaultTtlMs for this entry. */
40
+ ttlMs? : number;
41
+ type? : FactType;
42
+ tags? : string[];
43
+ importance?: number;
44
+ }
45
+
46
+ export interface MemoryStoreOptions {
47
+ /** IndexedDB database name. Default: 'ssmjs'. */
48
+ dbName? : string;
49
+ /** Key used for weight storage within the 'weights' object store. Default: 'ssmjs-weights'. */
50
+ weightsKey? : string;
51
+ /**
52
+ * IDBFactory to use instead of the global `indexedDB`.
53
+ * Use this in Node.js environments with fake-indexeddb:
54
+ * import { IDBFactory } from 'fake-indexeddb';
55
+ * const idbFactory = new IDBFactory();
56
+ */
57
+ idbFactory? : IDBFactory;
58
+ /**
59
+ * Default TTL applied to new entries when no per-entry ttlMs is provided.
60
+ * Entries with an expired TTL are filtered from recallAll() and related methods.
61
+ */
62
+ defaultTtlMs? : number;
63
+ }
64
+
65
+ const FACTS_STORE = 'facts';
66
+ const WEIGHTS_STORE = 'weights';
67
+ const DB_VERSION = 1;
68
+
69
+ /**
70
+ * Process-wide monotonic counter stamped onto each remembered entry. Strictly
71
+ * increasing per write regardless of store instance, so it reliably breaks
72
+ * `timestamp` ties (same-millisecond writes) in newest-first ordering.
73
+ */
74
+ let _writeSeq = 0;
75
+
76
+ // Minimal interface to avoid importing SSMRuntime (circular dep)
77
+ interface SaveLoadRuntime {
78
+ save(opts?: { storage: 'indexedDB'; key: string }): Promise<void>;
79
+ load(opts?: { key: string }): Promise<boolean>;
80
+ }
81
+
82
+ // Forward-declared to avoid circular dependency at import time
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ type SSMRuntimeRef = any;
85
+
86
+ /** Max number of cached content→embedding vectors retained in memory. */
87
+ const EMBED_CACHE_MAX = 2000;
88
+
89
+ export class MemoryStore {
90
+ private readonly _dbName : string;
91
+ private readonly _weightsKey : string;
92
+ private readonly _idb : IDBFactory | undefined;
93
+ private readonly _defaultTtl : number | undefined;
94
+ private _db: IDBDatabase | null = null;
95
+ /** Content → L2-normalised embedding cache, used by recallSimilar. */
96
+ private readonly _embedCache = new Map<string, Float32Array>();
97
+
98
+ constructor(opts: MemoryStoreOptions = {}) {
99
+ this._dbName = opts.dbName ?? 'ssmjs';
100
+ this._weightsKey = opts.weightsKey ?? 'ssmjs-weights';
101
+ this._idb = opts.idbFactory;
102
+ this._defaultTtl = opts.defaultTtlMs;
103
+ }
104
+
105
+ // ── Internal DB open ──────────────────────────────────────────────────────
106
+
107
+ private _open(): Promise<IDBDatabase> {
108
+ if (this._db) return Promise.resolve(this._db);
109
+
110
+ return new Promise((resolve, reject) => {
111
+ const factory = this._idb ?? (typeof indexedDB !== 'undefined' ? indexedDB : undefined);
112
+ if (!factory) {
113
+ reject(new SSMError(
114
+ 'MEMORY_UNAVAILABLE',
115
+ 'IndexedDB is not available in this environment. Pass an idbFactory option (e.g. from fake-indexeddb) for Node.js support.',
116
+ ));
117
+ return;
118
+ }
119
+
120
+ const req = factory.open(this._dbName, DB_VERSION);
121
+
122
+ req.onupgradeneeded = (e) => {
123
+ const db = (e.target as IDBOpenDBRequest).result;
124
+ if (!db.objectStoreNames.contains(FACTS_STORE)) {
125
+ db.createObjectStore(FACTS_STORE, { keyPath: 'key' });
126
+ }
127
+ if (!db.objectStoreNames.contains(WEIGHTS_STORE)) {
128
+ db.createObjectStore(WEIGHTS_STORE);
129
+ }
130
+ };
131
+
132
+ req.onsuccess = () => {
133
+ this._db = req.result;
134
+ resolve(req.result);
135
+ };
136
+ /* istanbul ignore next -- IDB open onerror fires only on storage faults; not reproducible with fake-indexeddb */
137
+ req.onerror = () => reject(new SSMError(
138
+ 'MEMORY_UNAVAILABLE',
139
+ `Failed to open IndexedDB "${this._dbName}": ${req.error?.message ?? 'unknown'}`,
140
+ req.error,
141
+ ));
142
+ });
143
+ }
144
+
145
+ // ── TTL helpers ───────────────────────────────────────────────────────────
146
+
147
+ private _isExpired(entry: MemoryEntry): boolean {
148
+ if (entry.ttlMs == null) return false;
149
+ return Date.now() > entry.timestamp + entry.ttlMs;
150
+ }
151
+
152
+ // ── Semantic facts ────────────────────────────────────────────────────────
153
+
154
+ /** Stores or overwrites a fact. */
155
+ async remember(key: string, content: string, opts?: RememberOptions): Promise<void> {
156
+ const db = await this._open();
157
+ const entry: MemoryEntry = {
158
+ key,
159
+ content,
160
+ timestamp : Date.now(),
161
+ seq : ++_writeSeq,
162
+ ttlMs : opts?.ttlMs ?? this._defaultTtl,
163
+ type : opts?.type,
164
+ tags : opts?.tags,
165
+ importance : opts?.importance,
166
+ };
167
+
168
+ const tx = db.transaction(FACTS_STORE, 'readwrite');
169
+ return requestToPromise(tx.objectStore(FACTS_STORE).put(entry), `Failed to store fact "${key}"`, () => undefined);
170
+ }
171
+
172
+ /**
173
+ * Retrieves a fact by key.
174
+ * Returns `undefined` if the key does not exist or the entry has expired.
175
+ */
176
+ async recall(key: string): Promise<MemoryEntry | undefined> {
177
+ const db = await this._open();
178
+
179
+ const tx = db.transaction(FACTS_STORE, 'readonly');
180
+ return requestToPromise(
181
+ tx.objectStore(FACTS_STORE).get(key) as IDBRequest<MemoryEntry | undefined>,
182
+ `Failed to recall fact "${key}"`,
183
+ (entry) => (entry && this._isExpired(entry) ? undefined : entry),
184
+ );
185
+ }
186
+
187
+ /** Returns all non-expired stored facts, newest first. */
188
+ async recallAll(): Promise<MemoryEntry[]> {
189
+ const db = await this._open();
190
+
191
+ const tx = db.transaction(FACTS_STORE, 'readonly');
192
+ return requestToPromise(
193
+ tx.objectStore(FACTS_STORE).getAll() as IDBRequest<MemoryEntry[]>,
194
+ 'Failed to recall all facts',
195
+ (entries) => entries
196
+ .filter(e => !this._isExpired(e))
197
+ // Newest first; break same-millisecond ties by write sequence so
198
+ // ordering is deterministic (IndexedDB getAll() returns key order,
199
+ // which would otherwise surface same-ms writes oldest-first).
200
+ .sort((a, b) => b.timestamp - a.timestamp || (b.seq ?? 0) - (a.seq ?? 0)),
201
+ );
202
+ }
203
+
204
+ /**
205
+ * Returns the N most recently updated non-expired facts.
206
+ * Equivalent to `recallAll()` truncated to `n` entries.
207
+ */
208
+ async recallRecent(n: number): Promise<MemoryEntry[]> {
209
+ const all = await this.recallAll();
210
+ return all.slice(0, n);
211
+ }
212
+
213
+ /**
214
+ * Returns all non-expired entries that contain the given tag.
215
+ */
216
+ async recallByTag(tag: string): Promise<MemoryEntry[]> {
217
+ const all = await this.recallAll();
218
+ return all.filter(e => e.tags?.includes(tag) ?? false);
219
+ }
220
+
221
+ /**
222
+ * Finds the top-K semantically similar entries to `query`.
223
+ *
224
+ * When `runtime` exposes an `embed()` method (the SSMRuntime does), similarity
225
+ * is computed as cosine distance between SSM hidden-state embeddings — i.e. the
226
+ * memory layer uses the very model it is attached to, and recall quality
227
+ * improves automatically as that model is adapted/distilled. Embeddings are
228
+ * cached per content string to avoid recomputing across calls.
229
+ *
230
+ * If no embedding-capable runtime is provided, or embedding fails for any
231
+ * reason, it transparently falls back to Jaccard word-overlap similarity.
232
+ */
233
+ async recallSimilar(query: string, topK: number, runtime?: SSMRuntimeRef): Promise<MemoryEntry[]> {
234
+ const all = await this.recallAll();
235
+ if (all.length === 0) return [];
236
+
237
+ // ── Preferred path: SSM-embedding cosine similarity ───────────────────
238
+ if (runtime != null && typeof runtime.embed === 'function') {
239
+ const queryVec = await this._embedWithCache(runtime, query);
240
+ if (queryVec) {
241
+ const scored: { entry: MemoryEntry; score: number }[] = [];
242
+ let embeddedAll = true;
243
+ for (const entry of all) {
244
+ const entryVec = await this._embedWithCache(runtime, entry.content);
245
+ if (!entryVec) { embeddedAll = false; break; }
246
+ scored.push({ entry, score: cosineSimilarity(queryVec, entryVec) });
247
+ }
248
+ if (embeddedAll) {
249
+ scored.sort((a, b) => b.score - a.score);
250
+ return scored.slice(0, topK).map(s => s.entry);
251
+ }
252
+ }
253
+ // Any failure falls through to the Jaccard path below.
254
+ }
255
+
256
+ // ── Fallback: Jaccard word-overlap similarity ─────────────────────────
257
+ const queryTokens = new Set(tokenize(query));
258
+ const scored = all.map(entry => {
259
+ const entryTokens = new Set(tokenize(entry.content));
260
+ const score = jaccardSimilarity(queryTokens, entryTokens);
261
+ return { entry, score };
262
+ });
263
+
264
+ scored.sort((a, b) => b.score - a.score);
265
+ return scored.slice(0, topK).map(s => s.entry);
266
+ }
267
+
268
+ /**
269
+ * Returns a cached embedding for `text`, computing it via `runtime.embed()`
270
+ * on a cache miss. Returns `null` (never throws) when embedding is
271
+ * unavailable so callers can fall back to lexical similarity.
272
+ */
273
+ private async _embedWithCache(runtime: SSMRuntimeRef, text: string): Promise<Float32Array | null> {
274
+ const cached = this._embedCache.get(text);
275
+ if (cached) return cached;
276
+ try {
277
+ const vec = await runtime.embed(text);
278
+ if (vec instanceof Float32Array && vec.length > 0) {
279
+ // Bound cache growth — simplest eviction is a full clear.
280
+ if (this._embedCache.size >= EMBED_CACHE_MAX) this._embedCache.clear();
281
+ this._embedCache.set(text, vec);
282
+ return vec;
283
+ }
284
+ } catch {
285
+ // Embedding unavailable (no GPU, destroyed runtime, etc.) — signal fallback.
286
+ }
287
+ return null;
288
+ }
289
+
290
+ /**
291
+ * Hard-deletes all entries whose TTL has expired.
292
+ * @returns The number of entries deleted.
293
+ */
294
+ async purgeExpired(): Promise<number> {
295
+ const db = await this._open();
296
+
297
+ // Load all raw entries (including expired ones) to check each
298
+ const tx = db.transaction(FACTS_STORE, 'readonly');
299
+ const all = await requestToPromise(
300
+ tx.objectStore(FACTS_STORE).getAll() as IDBRequest<MemoryEntry[]>,
301
+ 'Failed to scan facts for purge',
302
+ (r) => r,
303
+ );
304
+
305
+ const expired = all.filter(e => this._isExpired(e));
306
+ if (expired.length === 0) return 0;
307
+
308
+ await Promise.all(expired.map(e => this.forget(e.key)));
309
+ return expired.length;
310
+ }
311
+
312
+ /** Deletes a single fact. No-op if key does not exist. */
313
+ async forget(key: string): Promise<void> {
314
+ const db = await this._open();
315
+
316
+ const tx = db.transaction(FACTS_STORE, 'readwrite');
317
+ return requestToPromise(tx.objectStore(FACTS_STORE).delete(key), `Failed to forget fact "${key}"`, () => undefined);
318
+ }
319
+
320
+ /** Deletes all facts. Does not affect saved weights. */
321
+ async clear(): Promise<void> {
322
+ const db = await this._open();
323
+
324
+ const tx = db.transaction(FACTS_STORE, 'readwrite');
325
+ return requestToPromise(tx.objectStore(FACTS_STORE).clear(), 'Failed to clear facts', () => undefined);
326
+ }
327
+
328
+ // ── Cross-session memory merge ────────────────────────────────────────────
329
+
330
+ /**
331
+ * Returns all non-expired facts as a plain array for export.
332
+ * Suitable for serialisation and import into another MemoryStore instance.
333
+ */
334
+ async exportAll(): Promise<MemoryEntry[]> {
335
+ return this.recallAll();
336
+ }
337
+
338
+ /**
339
+ * Imports entries from an external array.
340
+ *
341
+ * - `'merge'` : only writes an entry if no existing entry with the same
342
+ * key exists, or if the incoming entry has a newer timestamp.
343
+ * - `'overwrite'` : writes all entries unconditionally.
344
+ */
345
+ async importAll(entries: MemoryEntry[], strategy: 'merge' | 'overwrite'): Promise<void> {
346
+ for (const entry of entries) {
347
+ if (strategy === 'overwrite') {
348
+ await this._putRaw(entry);
349
+ } else {
350
+ const existing = await this.recall(entry.key);
351
+ if (existing == null || entry.timestamp > existing.timestamp) {
352
+ await this._putRaw(entry);
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ /** Writes a raw MemoryEntry directly (preserves original timestamp / metadata). */
359
+ private async _putRaw(entry: MemoryEntry): Promise<void> {
360
+ const db = await this._open();
361
+ const tx = db.transaction(FACTS_STORE, 'readwrite');
362
+ return requestToPromise(tx.objectStore(FACTS_STORE).put(entry), `Failed to import fact "${entry.key}"`, () => undefined);
363
+ }
364
+
365
+ // ── Weight persistence ────────────────────────────────────────────────────
366
+
367
+ /**
368
+ * Saves SSM weights via `runtime.save()`.
369
+ * The weights are stored under `weightsKey` in this store's IndexedDB,
370
+ * separate from MambaSession's own key.
371
+ */
372
+ async saveWeights(runtime: SaveLoadRuntime): Promise<void> {
373
+ await runtime.save({ storage: 'indexedDB', key: this._weightsKey });
374
+ }
375
+
376
+ /**
377
+ * Loads SSM weights via `runtime.load()`.
378
+ * Returns `false` when no saved weights exist under `weightsKey`.
379
+ */
380
+ async loadWeights(runtime: SaveLoadRuntime): Promise<boolean> {
381
+ return runtime.load({ key: this._weightsKey });
382
+ }
383
+ }
384
+
385
+ // ── IndexedDB helper ──────────────────────────────────────────────────────────
386
+
387
+ /**
388
+ * Wires an IDBRequest to a promise: resolves with `map(result)` on success,
389
+ * rejects with a normalised MEMORY_UNAVAILABLE error on failure. Extracted so
390
+ * the success/error wiring lives in one place instead of being duplicated across
391
+ * every store method.
392
+ *
393
+ * The onerror branch fires only on IndexedDB storage faults (quota exceeded,
394
+ * disk failure, corruption), which the in-memory fake-indexeddb used in tests
395
+ * cannot reproduce — hence the istanbul ignore on that single line.
396
+ */
397
+ function requestToPromise<R, T>(req: IDBRequest<R>, failMsg: string, map: (result: R) => T): Promise<T> {
398
+ return new Promise<T>((resolve, reject) => {
399
+ req.onsuccess = () => resolve(map(req.result));
400
+ /* istanbul ignore next -- IDB storage-fault path; not reproducible with fake-indexeddb */
401
+ req.onerror = () => reject(new SSMError(
402
+ 'MEMORY_UNAVAILABLE',
403
+ `${failMsg}: ${req.error?.message ?? 'unknown'}`,
404
+ req.error ?? undefined,
405
+ ));
406
+ });
407
+ }
408
+
@@ -0,0 +1,2 @@
1
+ export { MemoryStore } from './MemoryStore.js';
2
+ export type { MemoryEntry, MemoryStoreOptions, RememberOptions, FactType } from './MemoryStore.js';
@@ -0,0 +1,201 @@
1
+ /**
2
+ * InferenceRouter – decides whether a given input should be handled by the
3
+ * local SSM or routed to a transformer bridge.
4
+ *
5
+ * Three heuristics run in priority order (cheapest first):
6
+ * 1. Complexity patterns — regex against the input string (synchronous, zero cost)
7
+ * 2. Input length — char count proxy for token count (synchronous, zero cost)
8
+ * 3. Perplexity probe — async SSM evaluate(); only runs when 1 & 2 are inconclusive
9
+ *
10
+ * When no bridge is available the router always returns target='ssm'.
11
+ */
12
+
13
+ export type RoutingStrategy = 'auto' | 'ssm' | 'transformer';
14
+
15
+ /**
16
+ * The structured result of a routing decision.
17
+ */
18
+ export interface RoutingDecision {
19
+ /** Which model should handle this input. */
20
+ target : 'ssm' | 'transformer';
21
+ /** The primary heuristic that triggered this decision. */
22
+ reason : 'strategy' | 'complexity' | 'length' | 'perplexity' | 'no_bridge';
23
+ /** Confidence score in the range 0–1. */
24
+ confidence : number;
25
+ /** Optional human-readable explanation. */
26
+ details? : string;
27
+ }
28
+
29
+ export interface RouterContext {
30
+ /**
31
+ * Cached SSM perplexity for this input if already computed by the caller.
32
+ * Providing this skips the async perplexity probe in auto mode.
33
+ */
34
+ perplexity?: number;
35
+ }
36
+
37
+ export interface RoutingAuditEntry {
38
+ timestamp : number;
39
+ inputLength: number;
40
+ decision : RoutingDecision;
41
+ durationMs : number;
42
+ }
43
+
44
+ export interface InferenceRouterOptions {
45
+ /**
46
+ * Routing strategy.
47
+ * - 'auto' : heuristic routing (default)
48
+ * - 'ssm' : always route to SSM
49
+ * - 'transformer' : always route to transformer (no-op when bridge absent)
50
+ */
51
+ strategy? : RoutingStrategy;
52
+
53
+ /**
54
+ * Input character length above which auto-routing prefers the transformer.
55
+ * ~1200 chars ≈ 300 tokens — beyond this a large context model tends to
56
+ * outperform a small SSM for most tasks.
57
+ * Default: 1200
58
+ */
59
+ longInputThreshold? : number;
60
+
61
+ /**
62
+ * SSM perplexity above which auto-routing falls back to the transformer.
63
+ * High perplexity signals the model hasn't seen this topic.
64
+ * Default: 80
65
+ */
66
+ perplexityThreshold? : number;
67
+
68
+ /**
69
+ * Whether a transformer bridge is currently attached to the runtime.
70
+ * The router uses this to short-circuit to 'ssm' when no bridge is present.
71
+ * Set by SSMRuntime when constructing the router.
72
+ * Default: false
73
+ */
74
+ hasBridge? : boolean;
75
+
76
+ /**
77
+ * Callback for computing SSM perplexity, provided by SSMRuntime.
78
+ * Avoids a circular import between InferenceRouter and SSMRuntime.
79
+ * Only called in 'auto' mode as a last-resort heuristic.
80
+ */
81
+ perplexityProbe? : (text: string) => Promise<number>;
82
+ }
83
+
84
+ // ── Complexity-signal regex patterns ─────────────────────────────────────────
85
+ // These match inputs that benefit from a larger transformer's broader knowledge.
86
+
87
+ const COMPLEXITY_PATTERNS: RegExp[] = [
88
+ /step[\s-]by[\s-]step/i,
89
+ /\banalyze\b|\banalyse\b/i,
90
+ /\bexplain\s+(why|how|in\s+detail)/i,
91
+ /\bcompare\b.*\band\b/i,
92
+ /\bcontrast\b/i,
93
+ /\bpros?\s+and\s+cons?\b/i,
94
+ /\bsummariz/i,
95
+ /\bwrite\s+a\s+(detailed|comprehensive|complete|full)\b/i,
96
+ /\bwhat\s+are\s+the\s+(key|main|top)\s+(difference|reason|factor)/i,
97
+ ];
98
+
99
+ /** Maximum audit log entries to keep in memory. */
100
+ const MAX_AUDIT_LOG = 500;
101
+
102
+ export class InferenceRouter {
103
+ private readonly _strategy : RoutingStrategy;
104
+ private readonly _longInputThreshold : number;
105
+ private readonly _perplexityThreshold: number;
106
+ private readonly _hasBridge : boolean;
107
+ private readonly _perplexityProbe : ((text: string) => Promise<number>) | undefined;
108
+ private readonly _auditLog : RoutingAuditEntry[] = [];
109
+
110
+ constructor(opts: InferenceRouterOptions = {}) {
111
+ this._strategy = opts.strategy ?? 'auto';
112
+ this._longInputThreshold = opts.longInputThreshold ?? 1200;
113
+ this._perplexityThreshold = opts.perplexityThreshold ?? 80;
114
+ this._hasBridge = opts.hasBridge ?? false;
115
+ this._perplexityProbe = opts.perplexityProbe;
116
+ }
117
+
118
+ /**
119
+ * Routes `input` to either SSM or transformer and returns a RoutingDecision.
120
+ *
121
+ * Always returns target='ssm' when no bridge is attached, regardless of strategy.
122
+ */
123
+ async route(input: string, ctx: RouterContext = {}): Promise<RoutingDecision> {
124
+ const startMs = Date.now();
125
+ const decision = await this._decide(input, ctx);
126
+ const durationMs = Date.now() - startMs;
127
+
128
+ const auditEntry: RoutingAuditEntry = {
129
+ timestamp : Date.now(),
130
+ inputLength: input.length,
131
+ decision,
132
+ durationMs,
133
+ };
134
+ this._auditLog.push(auditEntry);
135
+ if (this._auditLog.length > MAX_AUDIT_LOG) {
136
+ this._auditLog.shift();
137
+ }
138
+
139
+ return decision;
140
+ }
141
+
142
+ /** Returns a copy of the in-memory routing audit log (most recent last). */
143
+ getAuditLog(): RoutingAuditEntry[] {
144
+ return this._auditLog.slice();
145
+ }
146
+
147
+ // ── Private routing logic ─────────────────────────────────────────────────
148
+
149
+ private async _decide(input: string, ctx: RouterContext): Promise<RoutingDecision> {
150
+ // No bridge → always SSM
151
+ if (!this._hasBridge) {
152
+ return { target: 'ssm', reason: 'no_bridge', confidence: 1.0 };
153
+ }
154
+
155
+ // Fixed-strategy overrides
156
+ if (this._strategy === 'ssm') {
157
+ return { target: 'ssm', reason: 'strategy', confidence: 1.0 };
158
+ }
159
+ if (this._strategy === 'transformer') {
160
+ return { target: 'transformer', reason: 'strategy', confidence: 1.0 };
161
+ }
162
+
163
+ // Auto heuristics ────────────────────────────────────────────────────
164
+
165
+ // 1. Complexity patterns (synchronous)
166
+ const matchedPattern = COMPLEXITY_PATTERNS.find(p => p.test(input));
167
+ if (matchedPattern) {
168
+ return {
169
+ target : 'transformer',
170
+ reason : 'complexity',
171
+ confidence: 0.9,
172
+ details : `Matched pattern: ${matchedPattern.source}`,
173
+ };
174
+ }
175
+
176
+ // 2. Input length (synchronous)
177
+ if (input.length > this._longInputThreshold) {
178
+ return {
179
+ target : 'transformer',
180
+ reason : 'length',
181
+ confidence: 0.85,
182
+ details : `Input length ${input.length} exceeds threshold ${this._longInputThreshold}`,
183
+ };
184
+ }
185
+
186
+ // 3. Perplexity probe (async) — only if probe function provided
187
+ const perplexity = ctx.perplexity ??
188
+ (this._perplexityProbe ? await this._perplexityProbe(input) : undefined);
189
+
190
+ if (perplexity !== undefined && perplexity > this._perplexityThreshold) {
191
+ return {
192
+ target : 'transformer',
193
+ reason : 'perplexity',
194
+ confidence: Math.min(0.95, 0.5 + (perplexity - this._perplexityThreshold) / 200),
195
+ details : `SSM perplexity ${perplexity.toFixed(1)} exceeds threshold ${this._perplexityThreshold}`,
196
+ };
197
+ }
198
+
199
+ return { target: 'ssm', reason: 'complexity', confidence: 0.8 };
200
+ }
201
+ }
@@ -0,0 +1,2 @@
1
+ export { InferenceRouter } from './InferenceRouter.js';
2
+ export type { RoutingStrategy, RoutingDecision, RouterContext, InferenceRouterOptions, RoutingAuditEntry } from './InferenceRouter.js';