agent-working-memory 0.7.16 → 0.8.5
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 +156 -15
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +2 -16
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/codex.js +2 -11
- package/dist/adapters/codex.js.map +1 -1
- package/dist/adapters/common.d.ts +18 -0
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +254 -141
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/cursor.d.ts.map +1 -1
- package/dist/adapters/cursor.js +2 -15
- package/dist/adapters/cursor.js.map +1 -1
- package/dist/adapters/http.d.ts.map +1 -1
- package/dist/adapters/http.js +28 -34
- package/dist/adapters/http.js.map +1 -1
- package/dist/api/routes.d.ts +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +280 -113
- package/dist/api/routes.js.map +1 -1
- package/dist/cli/migrate.d.ts +42 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +268 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli.js +45 -1
- package/dist/cli.js.map +1 -1
- package/dist/coordination/circuit-breaker.d.ts +24 -0
- package/dist/coordination/circuit-breaker.d.ts.map +1 -0
- package/dist/coordination/circuit-breaker.js +70 -0
- package/dist/coordination/circuit-breaker.js.map +1 -0
- package/dist/coordination/failure-modes.d.ts +21 -0
- package/dist/coordination/failure-modes.d.ts.map +1 -0
- package/dist/coordination/failure-modes.js +49 -0
- package/dist/coordination/failure-modes.js.map +1 -0
- package/dist/coordination/peer-decisions.js +7 -7
- package/dist/coordination/routes.d.ts.map +1 -1
- package/dist/coordination/routes.js +44 -1
- package/dist/coordination/routes.js.map +1 -1
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +18 -0
- package/dist/coordination/schema.js.map +1 -1
- package/dist/coordination/schemas.d.ts +9 -9
- package/dist/coordination/stale.d.ts +9 -1
- package/dist/coordination/stale.d.ts.map +1 -1
- package/dist/coordination/stale.js +53 -9
- package/dist/coordination/stale.js.map +1 -1
- package/dist/core/embeddings.d.ts +16 -10
- package/dist/core/embeddings.d.ts.map +1 -1
- package/dist/core/embeddings.js +54 -38
- package/dist/core/embeddings.js.map +1 -1
- package/dist/core/ml-worker-entry.d.ts +17 -0
- package/dist/core/ml-worker-entry.d.ts.map +1 -0
- package/dist/core/ml-worker-entry.js +193 -0
- package/dist/core/ml-worker-entry.js.map +1 -0
- package/dist/core/ml-worker.d.ts +59 -0
- package/dist/core/ml-worker.d.ts.map +1 -0
- package/dist/core/ml-worker.js +253 -0
- package/dist/core/ml-worker.js.map +1 -0
- package/dist/core/query-expander.d.ts +13 -15
- package/dist/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js +48 -54
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/reranker.d.ts +9 -19
- package/dist/core/reranker.d.ts.map +1 -1
- package/dist/core/reranker.js +33 -35
- package/dist/core/reranker.js.map +1 -1
- package/dist/core/salience.d.ts +32 -5
- package/dist/core/salience.d.ts.map +1 -1
- package/dist/core/salience.js +201 -28
- package/dist/core/salience.js.map +1 -1
- package/dist/core/write-pipeline.d.ts +128 -0
- package/dist/core/write-pipeline.d.ts.map +1 -0
- package/dist/core/write-pipeline.js +405 -0
- package/dist/core/write-pipeline.js.map +1 -0
- package/dist/engine/activation.d.ts +2 -2
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +238 -115
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/confidence.d.ts +62 -0
- package/dist/engine/confidence.d.ts.map +1 -0
- package/dist/engine/confidence.js +100 -0
- package/dist/engine/confidence.js.map +1 -0
- package/dist/engine/connections.d.ts +47 -8
- package/dist/engine/connections.d.ts.map +1 -1
- package/dist/engine/connections.js +83 -22
- package/dist/engine/connections.js.map +1 -1
- package/dist/engine/consolidation-scheduler.d.ts +46 -10
- package/dist/engine/consolidation-scheduler.d.ts.map +1 -1
- package/dist/engine/consolidation-scheduler.js +170 -50
- package/dist/engine/consolidation-scheduler.js.map +1 -1
- package/dist/engine/consolidation.d.ts +16 -2
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +143 -44
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/engine/eval.d.ts +2 -2
- package/dist/engine/eval.d.ts.map +1 -1
- package/dist/engine/eval.js +9 -9
- package/dist/engine/eval.js.map +1 -1
- package/dist/engine/eviction.d.ts +4 -4
- package/dist/engine/eviction.d.ts.map +1 -1
- package/dist/engine/eviction.js +15 -15
- package/dist/engine/eviction.js.map +1 -1
- package/dist/engine/retraction.d.ts +93 -5
- package/dist/engine/retraction.d.ts.map +1 -1
- package/dist/engine/retraction.js +238 -19
- package/dist/engine/retraction.js.map +1 -1
- package/dist/engine/staging.d.ts +1 -1
- package/dist/engine/staging.d.ts.map +1 -1
- package/dist/engine/staging.js +3 -3
- package/dist/engine/staging.js.map +1 -1
- package/dist/hooks/sidecar.d.ts +1 -1
- package/dist/hooks/sidecar.d.ts.map +1 -1
- package/dist/hooks/sidecar.js +4 -4
- package/dist/hooks/sidecar.js.map +1 -1
- package/dist/index.js +48 -31
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +110 -155
- package/dist/mcp.js.map +1 -1
- package/dist/storage/factory.d.ts +55 -0
- package/dist/storage/factory.d.ts.map +1 -0
- package/dist/storage/factory.js +135 -0
- package/dist/storage/factory.js.map +1 -0
- package/dist/storage/pglite-schema.d.ts +20 -0
- package/dist/storage/pglite-schema.d.ts.map +1 -0
- package/dist/storage/pglite-schema.js +165 -0
- package/dist/storage/pglite-schema.js.map +1 -0
- package/dist/storage/pglite.d.ts +187 -0
- package/dist/storage/pglite.d.ts.map +1 -0
- package/dist/storage/pglite.js +1114 -0
- package/dist/storage/pglite.js.map +1 -0
- package/dist/storage/sqlite.d.ts +117 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +357 -8
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/store.d.ts +63 -0
- package/dist/storage/store.d.ts.map +1 -0
- package/dist/storage/store.js +27 -0
- package/dist/storage/store.js.map +1 -0
- package/dist/types/engram.d.ts +92 -2
- package/dist/types/engram.d.ts.map +1 -1
- package/dist/types/engram.js.map +1 -1
- package/package.json +3 -1
- package/src/adapters/claude-code.ts +218 -234
- package/src/adapters/codex.ts +252 -262
- package/src/adapters/common.ts +411 -284
- package/src/adapters/cursor.ts +135 -150
- package/src/adapters/http.ts +93 -100
- package/src/adapters/index.ts +31 -31
- package/src/adapters/types.ts +75 -75
- package/src/api/index.ts +3 -3
- package/src/api/routes.ts +367 -120
- package/src/cli/migrate.ts +307 -0
- package/src/cli.ts +37 -1
- package/src/coordination/circuit-breaker.ts +83 -0
- package/src/coordination/events.ts +90 -90
- package/src/coordination/failure-modes.ts +50 -0
- package/src/coordination/peer-decisions.ts +105 -105
- package/src/coordination/plugin-loader.ts +60 -60
- package/src/coordination/plugin.ts +44 -44
- package/src/coordination/routes.ts +56 -1
- package/src/coordination/schema.ts +13 -0
- package/src/coordination/stale.ts +80 -11
- package/src/coordination/types.ts +311 -311
- package/src/coordination/write-mutex.ts +69 -69
- package/src/core/auto-tagger.ts +168 -168
- package/src/core/decay.ts +63 -63
- package/src/core/embeddings.ts +110 -93
- package/src/core/index.ts +5 -5
- package/src/core/logger.ts +36 -36
- package/src/core/ml-worker-entry.ts +194 -0
- package/src/core/ml-worker.ts +281 -0
- package/src/core/query-expander.ts +122 -128
- package/src/core/reranker.ts +119 -125
- package/src/core/salience.ts +200 -33
- package/src/core/write-pipeline.ts +521 -0
- package/src/engine/activation.ts +235 -109
- package/src/engine/confidence.ts +120 -0
- package/src/engine/connections.ts +162 -103
- package/src/engine/consolidation-scheduler.ts +242 -125
- package/src/engine/consolidation.ts +138 -45
- package/src/engine/eval.ts +102 -102
- package/src/engine/eviction.ts +101 -101
- package/src/engine/index.ts +8 -8
- package/src/engine/retraction.ts +366 -116
- package/src/engine/staging.ts +74 -74
- package/src/hooks/sidecar.ts +5 -5
- package/src/index.ts +42 -28
- package/src/mcp.ts +105 -152
- package/src/storage/factory.ts +147 -0
- package/src/storage/index.ts +3 -3
- package/src/storage/pglite-schema.ts +166 -0
- package/src/storage/pglite.ts +1363 -0
- package/src/storage/sqlite.ts +402 -7
- package/src/storage/store.ts +80 -0
- package/src/types/agent.ts +67 -67
- package/src/types/checkpoint.ts +46 -46
- package/src/types/engram.ts +101 -3
- package/src/types/eval.ts +100 -100
- package/src/types/index.ts +6 -6
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* ML inference dispatch pool.
|
|
5
|
+
*
|
|
6
|
+
* STATUS (AWM 0.8.x P1 REVISE, 2026-05-25): worker_threads were the original
|
|
7
|
+
* plan, but @huggingface/transformers in Node only supports the `cpu`
|
|
8
|
+
* (native ONNX) and `dml` (Windows GPU) backends. Neither is safe inside
|
|
9
|
+
* a worker_thread — onnxruntime-node's native bindings store V8 handles
|
|
10
|
+
* that get invalidated when crossing isolate boundaries, causing
|
|
11
|
+
* `v8::HandleScope::CreateHandle()` crashes on first inference call.
|
|
12
|
+
* The browser-only `wasm` backend is not loaded in Node builds of
|
|
13
|
+
* transformers.js.
|
|
14
|
+
*
|
|
15
|
+
* The dispatch abstraction is preserved so a future child_process pool
|
|
16
|
+
* or HTTP sidecar (see AWM_ML_SIDECAR_URL design in docs/awm-architecture-history.md)
|
|
17
|
+
* can plug in. For now ALL inference runs in-process. The freeze fix from
|
|
18
|
+
* P0 (sleep-only consolidation) already eliminates the multi-second
|
|
19
|
+
* in-band blocks. Individual inference calls (~50ms each) on the main
|
|
20
|
+
* thread are accepted as-is.
|
|
21
|
+
*
|
|
22
|
+
* Test / dev mode: AWM_ML_INPROCESS=1 is honored but is now the default.
|
|
23
|
+
* The env var remains as a no-op for backwards compatibility.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Worker } from 'node:worker_threads';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { dirname, join } from 'node:path';
|
|
29
|
+
import { existsSync } from 'node:fs';
|
|
30
|
+
|
|
31
|
+
type WorkerRole = 'embed' | 'rerank' | 'expand';
|
|
32
|
+
|
|
33
|
+
interface PendingRequest {
|
|
34
|
+
resolve: (value: any) => void;
|
|
35
|
+
reject: (err: Error) => void;
|
|
36
|
+
timeoutHandle?: ReturnType<typeof setTimeout>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ManagedWorker {
|
|
40
|
+
role: WorkerRole;
|
|
41
|
+
worker: Worker | null;
|
|
42
|
+
ready: Promise<void>;
|
|
43
|
+
setReady: () => void;
|
|
44
|
+
setFailed: (err: Error) => void;
|
|
45
|
+
pending: Map<number, PendingRequest>;
|
|
46
|
+
buffered: Array<{ id: number; op: WorkerRole; args: any }>;
|
|
47
|
+
isReady: boolean;
|
|
48
|
+
restartTimestamps: number[]; // unix-ms of recent restarts (for backoff cap)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let inProcessMode = false;
|
|
52
|
+
let workers: Record<WorkerRole, ManagedWorker> | null = null;
|
|
53
|
+
let nextId = 1;
|
|
54
|
+
|
|
55
|
+
// In-process fallback handles (used in tests and as crash escape hatch).
|
|
56
|
+
let inProcessEmbed: ((args: any) => Promise<number[][]>) | null = null;
|
|
57
|
+
let inProcessRerank: ((args: any) => Promise<Array<{ index: number; score: number }>>) | null = null;
|
|
58
|
+
let inProcessExpand: ((args: any) => Promise<string>) | null = null;
|
|
59
|
+
|
|
60
|
+
const REQUEST_TIMEOUT_MS = 60_000; // any single inference call > 60s is treated as failed
|
|
61
|
+
const MAX_RESTARTS_PER_MINUTE = 3;
|
|
62
|
+
|
|
63
|
+
function shouldUseInProcess(): boolean {
|
|
64
|
+
// AWM 0.8.x P1 REVISE: worker_threads were removed because @huggingface/transformers
|
|
65
|
+
// in Node is not worker_threads-safe (see file header). Always in-process for now.
|
|
66
|
+
// The dispatch abstraction is preserved for a future child_process pool or
|
|
67
|
+
// HTTP sidecar pivot.
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createWorker(role: WorkerRole): ManagedWorker {
|
|
72
|
+
const m: ManagedWorker = {
|
|
73
|
+
role,
|
|
74
|
+
worker: null,
|
|
75
|
+
ready: Promise.resolve(),
|
|
76
|
+
setReady: () => {},
|
|
77
|
+
setFailed: () => {},
|
|
78
|
+
pending: new Map(),
|
|
79
|
+
buffered: [],
|
|
80
|
+
isReady: false,
|
|
81
|
+
restartTimestamps: [],
|
|
82
|
+
};
|
|
83
|
+
m.ready = new Promise<void>((resolve, reject) => {
|
|
84
|
+
m.setReady = () => { m.isReady = true; resolve(); };
|
|
85
|
+
m.setFailed = (err) => reject(err);
|
|
86
|
+
});
|
|
87
|
+
spawnWorker(m);
|
|
88
|
+
return m;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function workerEntryPath(): string {
|
|
92
|
+
// Resolve to the compiled .js. Two possible locations:
|
|
93
|
+
// 1. Same directory as this file (when running from dist/core/)
|
|
94
|
+
// 2. Sibling dist/core/ (when running from src/core/ via tsx)
|
|
95
|
+
// Always prefer the compiled file. If neither exists, shouldUseInProcess()
|
|
96
|
+
// will detect the missing entry and fall back to in-process mode.
|
|
97
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
98
|
+
const samedir = join(here, 'ml-worker-entry.js');
|
|
99
|
+
if (existsSync(samedir)) return samedir;
|
|
100
|
+
// From src/core/ml-worker.ts, dist/core/ml-worker-entry.js is at ../../dist/core/
|
|
101
|
+
const distSibling = join(here, '..', '..', 'dist', 'core', 'ml-worker-entry.js');
|
|
102
|
+
if (existsSync(distSibling)) return distSibling;
|
|
103
|
+
// Fallback to the same-dir path (will fail existsSync in shouldUseInProcess
|
|
104
|
+
// and trigger in-process mode)
|
|
105
|
+
return samedir;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function spawnWorker(m: ManagedWorker): void {
|
|
109
|
+
const w = new Worker(workerEntryPath(), { workerData: { role: m.role } });
|
|
110
|
+
m.worker = w;
|
|
111
|
+
|
|
112
|
+
w.on('message', (msg: any) => {
|
|
113
|
+
if (msg?.ready === true) {
|
|
114
|
+
m.setReady();
|
|
115
|
+
// Drain buffered messages
|
|
116
|
+
for (const buf of m.buffered) w.postMessage(buf);
|
|
117
|
+
m.buffered = [];
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (msg?.ready === false) {
|
|
121
|
+
m.setFailed(new Error(`worker ${m.role} failed to load model: ${msg.error}`));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (msg?.shutdown === 'done') return;
|
|
125
|
+
if (typeof msg?.id !== 'number') return;
|
|
126
|
+
|
|
127
|
+
const p = m.pending.get(msg.id);
|
|
128
|
+
if (!p) return;
|
|
129
|
+
m.pending.delete(msg.id);
|
|
130
|
+
if (p.timeoutHandle) clearTimeout(p.timeoutHandle);
|
|
131
|
+
if (msg.ok) p.resolve(msg.result);
|
|
132
|
+
else p.reject(new Error(msg.error ?? 'worker error'));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
w.on('error', (err) => {
|
|
136
|
+
console.error(`[ml-worker:${m.role}] error:`, err);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
w.on('exit', (code) => {
|
|
140
|
+
if (code === 0) return; // graceful exit
|
|
141
|
+
console.warn(`[ml-worker:${m.role}] exited with code ${code} — recovering`);
|
|
142
|
+
// Reject all pending requests
|
|
143
|
+
for (const [, p] of m.pending) {
|
|
144
|
+
if (p.timeoutHandle) clearTimeout(p.timeoutHandle);
|
|
145
|
+
p.reject(new Error(`worker ${m.role} crashed (exit ${code})`));
|
|
146
|
+
}
|
|
147
|
+
m.pending.clear();
|
|
148
|
+
|
|
149
|
+
// Restart-rate backoff
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
m.restartTimestamps = m.restartTimestamps.filter(t => now - t < 60_000);
|
|
152
|
+
m.restartTimestamps.push(now);
|
|
153
|
+
|
|
154
|
+
if (m.restartTimestamps.length > MAX_RESTARTS_PER_MINUTE) {
|
|
155
|
+
console.error(`[ml-worker:${m.role}] crashed ${m.restartTimestamps.length} times in 60s — falling back to in-process`);
|
|
156
|
+
m.worker = null;
|
|
157
|
+
m.isReady = false;
|
|
158
|
+
inProcessMode = true;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Reset ready promise + respawn
|
|
163
|
+
m.isReady = false;
|
|
164
|
+
m.ready = new Promise<void>((resolve, reject) => {
|
|
165
|
+
m.setReady = () => { m.isReady = true; resolve(); };
|
|
166
|
+
m.setFailed = (err) => reject(err);
|
|
167
|
+
});
|
|
168
|
+
spawnWorker(m);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Initialize the pool. Idempotent — safe to call multiple times. */
|
|
173
|
+
export function initMLPool(): void {
|
|
174
|
+
if (shouldUseInProcess()) {
|
|
175
|
+
inProcessMode = true;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (workers) return;
|
|
179
|
+
workers = {
|
|
180
|
+
embed: createWorker('embed'),
|
|
181
|
+
rerank: createWorker('rerank'),
|
|
182
|
+
expand: createWorker('expand'),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Register in-process fallback handlers. Called once by the consumer modules. */
|
|
187
|
+
export function registerInProcessHandlers(handlers: {
|
|
188
|
+
embed?: typeof inProcessEmbed;
|
|
189
|
+
rerank?: typeof inProcessRerank;
|
|
190
|
+
expand?: typeof inProcessExpand;
|
|
191
|
+
}): void {
|
|
192
|
+
if (handlers.embed) inProcessEmbed = handlers.embed;
|
|
193
|
+
if (handlers.rerank) inProcessRerank = handlers.rerank;
|
|
194
|
+
if (handlers.expand) inProcessExpand = handlers.expand;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** True if the pool is operating in in-process mode (no workers). */
|
|
198
|
+
export function isInProcessMode(): boolean {
|
|
199
|
+
return inProcessMode;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function dispatchToWorker<T>(role: WorkerRole, args: any): Promise<T> {
|
|
203
|
+
if (!workers) initMLPool();
|
|
204
|
+
if (inProcessMode) {
|
|
205
|
+
return dispatchInProcess<T>(role, args);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const m = workers![role];
|
|
209
|
+
await m.ready;
|
|
210
|
+
if (inProcessMode) {
|
|
211
|
+
// Fallback flipped while we awaited ready
|
|
212
|
+
return dispatchInProcess<T>(role, args);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const id = nextId++;
|
|
216
|
+
return new Promise<T>((resolve, reject) => {
|
|
217
|
+
const req: PendingRequest = {
|
|
218
|
+
resolve,
|
|
219
|
+
reject,
|
|
220
|
+
timeoutHandle: setTimeout(() => {
|
|
221
|
+
m.pending.delete(id);
|
|
222
|
+
reject(new Error(`ml-worker:${role} request ${id} timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
223
|
+
}, REQUEST_TIMEOUT_MS),
|
|
224
|
+
};
|
|
225
|
+
m.pending.set(id, req);
|
|
226
|
+
const msg = { id, op: role, args };
|
|
227
|
+
if (m.isReady && m.worker) {
|
|
228
|
+
m.worker.postMessage(msg);
|
|
229
|
+
} else {
|
|
230
|
+
m.buffered.push(msg);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function dispatchInProcess<T>(role: WorkerRole, args: any): Promise<T> {
|
|
236
|
+
switch (role) {
|
|
237
|
+
case 'embed':
|
|
238
|
+
if (!inProcessEmbed) throw new Error('in-process embed handler not registered');
|
|
239
|
+
return inProcessEmbed(args) as Promise<T>;
|
|
240
|
+
case 'rerank':
|
|
241
|
+
if (!inProcessRerank) throw new Error('in-process rerank handler not registered');
|
|
242
|
+
return inProcessRerank(args) as Promise<T>;
|
|
243
|
+
case 'expand':
|
|
244
|
+
if (!inProcessExpand) throw new Error('in-process expand handler not registered');
|
|
245
|
+
return inProcessExpand(args) as Promise<T>;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Public API used by the consumer modules ---
|
|
250
|
+
|
|
251
|
+
export async function dispatchEmbed(args: { texts: string[]; pooling: 'cls' | 'mean'; dimensions: number }): Promise<number[][]> {
|
|
252
|
+
return dispatchToWorker<number[][]>('embed', args);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function dispatchRerank(args: { query: string; passages: string[] }): Promise<Array<{ index: number; score: number }>> {
|
|
256
|
+
return dispatchToWorker<Array<{ index: number; score: number }>>('rerank', args);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function dispatchExpand(args: { prompt: string; maxNewTokens: number; noRepeatNgramSize: number }): Promise<string> {
|
|
260
|
+
return dispatchToWorker<string>('expand', args);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Graceful shutdown. Waits up to 2s for queue drain, then terminates. */
|
|
264
|
+
export async function shutdownMLPool(): Promise<void> {
|
|
265
|
+
if (!workers) return;
|
|
266
|
+
const promises: Promise<void>[] = [];
|
|
267
|
+
for (const role of ['embed', 'rerank', 'expand'] as WorkerRole[]) {
|
|
268
|
+
const m = workers[role];
|
|
269
|
+
if (!m.worker) continue;
|
|
270
|
+
const w = m.worker;
|
|
271
|
+
promises.push(new Promise<void>((resolve) => {
|
|
272
|
+
const timeout = setTimeout(() => {
|
|
273
|
+
w.terminate().finally(() => resolve());
|
|
274
|
+
}, 2000);
|
|
275
|
+
w.once('exit', () => { clearTimeout(timeout); resolve(); });
|
|
276
|
+
w.postMessage({ shutdown: true });
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
await Promise.all(promises);
|
|
280
|
+
workers = null;
|
|
281
|
+
}
|
|
@@ -1,128 +1,122 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* Query Expander
|
|
5
|
-
*
|
|
6
|
-
* Uses Xenova/flan-t5-small (~80MB ONNX) to expand search queries
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}).then(pipe => {
|
|
32
|
-
|
|
33
|
-
console.log(`Query expander loaded: ${MODEL_ID}`);
|
|
34
|
-
return
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Clear the expansion cache (used by tests + cache invalidation if needed). */
|
|
126
|
-
export function clearExpansionCache(): void {
|
|
127
|
-
expansionCache.clear();
|
|
128
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Query Expander - rewrites queries with synonyms and related terms.
|
|
5
|
+
*
|
|
6
|
+
* Uses Xenova/flan-t5-small (~80MB ONNX) to expand search queries with
|
|
7
|
+
* related terms that improve BM25 recall.
|
|
8
|
+
*
|
|
9
|
+
* AWM 0.8.x: inference dispatches through ml-worker.ts (currently in-process
|
|
10
|
+
* — worker_threads reverted because onnxruntime-node bindings cross isolate
|
|
11
|
+
* boundaries unsafely; see ml-worker.ts). The dispatch abstraction is
|
|
12
|
+
* preserved for a future child_process / HTTP sidecar pool.
|
|
13
|
+
*
|
|
14
|
+
* The LRU cache + skip heuristic stay on the main thread — they're pure
|
|
15
|
+
* filter/lookup logic that shouldn't pay IPC cost.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { pipeline, type Text2TextGenerationPipeline } from '@huggingface/transformers';
|
|
19
|
+
import { dispatchExpand, registerInProcessHandlers } from './ml-worker.js';
|
|
20
|
+
|
|
21
|
+
const MODEL_ID = 'Xenova/flan-t5-small';
|
|
22
|
+
|
|
23
|
+
// --- In-process fallback ---
|
|
24
|
+
|
|
25
|
+
let inProcessInstance: Text2TextGenerationPipeline | null = null;
|
|
26
|
+
let inProcessInitPromise: Promise<Text2TextGenerationPipeline> | null = null;
|
|
27
|
+
|
|
28
|
+
async function loadInProcess(): Promise<Text2TextGenerationPipeline> {
|
|
29
|
+
if (inProcessInstance) return inProcessInstance;
|
|
30
|
+
if (inProcessInitPromise) return inProcessInitPromise;
|
|
31
|
+
inProcessInitPromise = pipeline('text2text-generation', MODEL_ID, { dtype: 'fp32' }).then(pipe => {
|
|
32
|
+
inProcessInstance = pipe as Text2TextGenerationPipeline;
|
|
33
|
+
console.log(`Query expander loaded in-process: ${MODEL_ID}`);
|
|
34
|
+
return inProcessInstance;
|
|
35
|
+
});
|
|
36
|
+
return inProcessInitPromise;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function inProcessExpand(args: { prompt: string; maxNewTokens: number; noRepeatNgramSize: number }): Promise<string> {
|
|
40
|
+
const expander = await loadInProcess();
|
|
41
|
+
const result = await expander(args.prompt, {
|
|
42
|
+
max_new_tokens: args.maxNewTokens,
|
|
43
|
+
no_repeat_ngram_size: args.noRepeatNgramSize,
|
|
44
|
+
});
|
|
45
|
+
const text = Array.isArray(result) ? (result[0] as any)?.generated_text ?? '' : '';
|
|
46
|
+
return String(text).trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
registerInProcessHandlers({ expand: inProcessExpand });
|
|
50
|
+
|
|
51
|
+
// --- Public API ---
|
|
52
|
+
|
|
53
|
+
/** Kept for backwards compat. */
|
|
54
|
+
export async function getExpander(): Promise<Text2TextGenerationPipeline> {
|
|
55
|
+
return loadInProcess();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* LRU cache of normalized-query → expanded-query mappings.
|
|
60
|
+
* Lives on the main thread — cache hits skip the worker IPC entirely.
|
|
61
|
+
*/
|
|
62
|
+
const expansionCache = new Map<string, string>();
|
|
63
|
+
const EXPANSION_CACHE_LIMIT = 500;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Skip expansion when the query is already specific (long or many tokens).
|
|
67
|
+
*/
|
|
68
|
+
function shouldSkipExpansion(normalized: string): boolean {
|
|
69
|
+
if (normalized.length === 0) return true;
|
|
70
|
+
if (normalized.length > 50) return true;
|
|
71
|
+
const tokens = new Set(normalized.split(/\s+/).filter(t => t.length > 2));
|
|
72
|
+
return tokens.size >= 5;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Expand a query with related terms and synonyms.
|
|
77
|
+
* Returns the original query + generated expansion terms.
|
|
78
|
+
* Falls back to the original query on any error.
|
|
79
|
+
*
|
|
80
|
+
* Dispatches inference to the worker pool. Cache + skip heuristic stay
|
|
81
|
+
* on the main thread.
|
|
82
|
+
*/
|
|
83
|
+
export async function expandQuery(originalQuery: string): Promise<string> {
|
|
84
|
+
const normalized = originalQuery.toLowerCase().trim();
|
|
85
|
+
const optimizationsEnabled = process.env.AWM_DISABLE_EXPANSION_CACHE !== '1';
|
|
86
|
+
|
|
87
|
+
if (optimizationsEnabled) {
|
|
88
|
+
if (shouldSkipExpansion(normalized)) return originalQuery;
|
|
89
|
+
const cached = expansionCache.get(normalized);
|
|
90
|
+
if (cached !== undefined) {
|
|
91
|
+
// LRU touch
|
|
92
|
+
expansionCache.delete(normalized);
|
|
93
|
+
expansionCache.set(normalized, cached);
|
|
94
|
+
return cached;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const prompt = `Expand this search query with synonyms and related terms. Only output the additional terms, not the original query. Query: ${originalQuery}. Additional terms:`;
|
|
100
|
+
const expansion = await dispatchExpand({ prompt, maxNewTokens: 25, noRepeatNgramSize: 2 });
|
|
101
|
+
const finalQuery = expansion && expansion.length > 2
|
|
102
|
+
? `${originalQuery} ${expansion}`
|
|
103
|
+
: originalQuery;
|
|
104
|
+
|
|
105
|
+
if (optimizationsEnabled) {
|
|
106
|
+
if (expansionCache.size >= EXPANSION_CACHE_LIMIT) {
|
|
107
|
+
const oldestKey = expansionCache.keys().next().value;
|
|
108
|
+
if (oldestKey !== undefined) expansionCache.delete(oldestKey);
|
|
109
|
+
}
|
|
110
|
+
expansionCache.set(normalized, finalQuery);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return finalQuery;
|
|
114
|
+
} catch {
|
|
115
|
+
return originalQuery;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Clear the expansion cache (used by tests + cache invalidation). */
|
|
120
|
+
export function clearExpansionCache(): void {
|
|
121
|
+
expansionCache.clear();
|
|
122
|
+
}
|