agent-working-memory 0.7.17 → 0.8.6

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 (188) hide show
  1. package/README.md +156 -15
  2. package/dist/adapters/common.d.ts.map +1 -1
  3. package/dist/adapters/common.js +289 -202
  4. package/dist/adapters/common.js.map +1 -1
  5. package/dist/adapters/http.js +22 -22
  6. package/dist/api/routes.d.ts +1 -1
  7. package/dist/api/routes.d.ts.map +1 -1
  8. package/dist/api/routes.js +258 -79
  9. package/dist/api/routes.js.map +1 -1
  10. package/dist/cli/migrate.d.ts +42 -0
  11. package/dist/cli/migrate.d.ts.map +1 -0
  12. package/dist/cli/migrate.js +268 -0
  13. package/dist/cli/migrate.js.map +1 -0
  14. package/dist/cli.js +147 -103
  15. package/dist/cli.js.map +1 -1
  16. package/dist/coordination/circuit-breaker.d.ts +24 -0
  17. package/dist/coordination/circuit-breaker.d.ts.map +1 -0
  18. package/dist/coordination/circuit-breaker.js +70 -0
  19. package/dist/coordination/circuit-breaker.js.map +1 -0
  20. package/dist/coordination/failure-modes.d.ts +21 -0
  21. package/dist/coordination/failure-modes.d.ts.map +1 -0
  22. package/dist/coordination/failure-modes.js +49 -0
  23. package/dist/coordination/failure-modes.js.map +1 -0
  24. package/dist/coordination/peer-decisions.js +7 -7
  25. package/dist/coordination/routes.d.ts.map +1 -1
  26. package/dist/coordination/routes.js +44 -1
  27. package/dist/coordination/routes.js.map +1 -1
  28. package/dist/coordination/schema.d.ts.map +1 -1
  29. package/dist/coordination/schema.js +18 -0
  30. package/dist/coordination/schema.js.map +1 -1
  31. package/dist/coordination/schemas.d.ts +9 -9
  32. package/dist/coordination/stale.d.ts +9 -1
  33. package/dist/coordination/stale.d.ts.map +1 -1
  34. package/dist/coordination/stale.js +53 -9
  35. package/dist/coordination/stale.js.map +1 -1
  36. package/dist/core/embeddings.d.ts +16 -10
  37. package/dist/core/embeddings.d.ts.map +1 -1
  38. package/dist/core/embeddings.js +54 -38
  39. package/dist/core/embeddings.js.map +1 -1
  40. package/dist/core/ml-worker-entry.d.ts +17 -0
  41. package/dist/core/ml-worker-entry.d.ts.map +1 -0
  42. package/dist/core/ml-worker-entry.js +193 -0
  43. package/dist/core/ml-worker-entry.js.map +1 -0
  44. package/dist/core/ml-worker.d.ts +59 -0
  45. package/dist/core/ml-worker.d.ts.map +1 -0
  46. package/dist/core/ml-worker.js +253 -0
  47. package/dist/core/ml-worker.js.map +1 -0
  48. package/dist/core/query-expander.d.ts +13 -15
  49. package/dist/core/query-expander.d.ts.map +1 -1
  50. package/dist/core/query-expander.js +48 -54
  51. package/dist/core/query-expander.js.map +1 -1
  52. package/dist/core/reranker.d.ts +9 -19
  53. package/dist/core/reranker.d.ts.map +1 -1
  54. package/dist/core/reranker.js +33 -35
  55. package/dist/core/reranker.js.map +1 -1
  56. package/dist/core/salience.d.ts +32 -5
  57. package/dist/core/salience.d.ts.map +1 -1
  58. package/dist/core/salience.js +201 -28
  59. package/dist/core/salience.js.map +1 -1
  60. package/dist/core/write-pipeline.d.ts +10 -2
  61. package/dist/core/write-pipeline.d.ts.map +1 -1
  62. package/dist/core/write-pipeline.js +218 -49
  63. package/dist/core/write-pipeline.js.map +1 -1
  64. package/dist/engine/activation.d.ts +2 -2
  65. package/dist/engine/activation.d.ts.map +1 -1
  66. package/dist/engine/activation.js +238 -115
  67. package/dist/engine/activation.js.map +1 -1
  68. package/dist/engine/confidence.d.ts +62 -0
  69. package/dist/engine/confidence.d.ts.map +1 -0
  70. package/dist/engine/confidence.js +100 -0
  71. package/dist/engine/confidence.js.map +1 -0
  72. package/dist/engine/connections.d.ts +47 -8
  73. package/dist/engine/connections.d.ts.map +1 -1
  74. package/dist/engine/connections.js +83 -22
  75. package/dist/engine/connections.js.map +1 -1
  76. package/dist/engine/consolidation-scheduler.d.ts +46 -10
  77. package/dist/engine/consolidation-scheduler.d.ts.map +1 -1
  78. package/dist/engine/consolidation-scheduler.js +170 -50
  79. package/dist/engine/consolidation-scheduler.js.map +1 -1
  80. package/dist/engine/consolidation.d.ts +16 -2
  81. package/dist/engine/consolidation.d.ts.map +1 -1
  82. package/dist/engine/consolidation.js +143 -44
  83. package/dist/engine/consolidation.js.map +1 -1
  84. package/dist/engine/eval.d.ts +2 -2
  85. package/dist/engine/eval.d.ts.map +1 -1
  86. package/dist/engine/eval.js +9 -9
  87. package/dist/engine/eval.js.map +1 -1
  88. package/dist/engine/eviction.d.ts +4 -4
  89. package/dist/engine/eviction.d.ts.map +1 -1
  90. package/dist/engine/eviction.js +15 -15
  91. package/dist/engine/eviction.js.map +1 -1
  92. package/dist/engine/retraction.d.ts +93 -5
  93. package/dist/engine/retraction.d.ts.map +1 -1
  94. package/dist/engine/retraction.js +238 -19
  95. package/dist/engine/retraction.js.map +1 -1
  96. package/dist/engine/staging.d.ts +1 -1
  97. package/dist/engine/staging.d.ts.map +1 -1
  98. package/dist/engine/staging.js +3 -3
  99. package/dist/engine/staging.js.map +1 -1
  100. package/dist/hooks/sidecar.d.ts +1 -1
  101. package/dist/hooks/sidecar.d.ts.map +1 -1
  102. package/dist/hooks/sidecar.js +4 -4
  103. package/dist/hooks/sidecar.js.map +1 -1
  104. package/dist/index.js +48 -31
  105. package/dist/index.js.map +1 -1
  106. package/dist/mcp.js +167 -141
  107. package/dist/mcp.js.map +1 -1
  108. package/dist/storage/factory.d.ts +55 -0
  109. package/dist/storage/factory.d.ts.map +1 -0
  110. package/dist/storage/factory.js +135 -0
  111. package/dist/storage/factory.js.map +1 -0
  112. package/dist/storage/pglite-schema.d.ts +20 -0
  113. package/dist/storage/pglite-schema.d.ts.map +1 -0
  114. package/dist/storage/pglite-schema.js +165 -0
  115. package/dist/storage/pglite-schema.js.map +1 -0
  116. package/dist/storage/pglite.d.ts +187 -0
  117. package/dist/storage/pglite.d.ts.map +1 -0
  118. package/dist/storage/pglite.js +1114 -0
  119. package/dist/storage/pglite.js.map +1 -0
  120. package/dist/storage/sqlite.d.ts +117 -0
  121. package/dist/storage/sqlite.d.ts.map +1 -1
  122. package/dist/storage/sqlite.js +357 -8
  123. package/dist/storage/sqlite.js.map +1 -1
  124. package/dist/storage/store.d.ts +63 -0
  125. package/dist/storage/store.d.ts.map +1 -0
  126. package/dist/storage/store.js +27 -0
  127. package/dist/storage/store.js.map +1 -0
  128. package/dist/types/engram.d.ts +92 -2
  129. package/dist/types/engram.d.ts.map +1 -1
  130. package/dist/types/engram.js.map +1 -1
  131. package/package.json +3 -1
  132. package/src/adapters/claude-code.ts +218 -218
  133. package/src/adapters/codex.ts +252 -252
  134. package/src/adapters/common.ts +498 -411
  135. package/src/adapters/cursor.ts +135 -135
  136. package/src/adapters/http.ts +93 -93
  137. package/src/adapters/index.ts +31 -31
  138. package/src/adapters/types.ts +75 -75
  139. package/src/api/index.ts +3 -3
  140. package/src/api/routes.ts +970 -714
  141. package/src/cli/migrate.ts +307 -0
  142. package/src/cli.ts +755 -719
  143. package/src/coordination/circuit-breaker.ts +83 -0
  144. package/src/coordination/events.ts +90 -90
  145. package/src/coordination/failure-modes.ts +50 -0
  146. package/src/coordination/peer-decisions.ts +105 -105
  147. package/src/coordination/plugin-loader.ts +60 -60
  148. package/src/coordination/plugin.ts +44 -44
  149. package/src/coordination/routes.ts +56 -1
  150. package/src/coordination/schema.ts +13 -0
  151. package/src/coordination/stale.ts +80 -11
  152. package/src/coordination/types.ts +311 -311
  153. package/src/coordination/write-mutex.ts +69 -69
  154. package/src/core/auto-tagger.ts +168 -168
  155. package/src/core/decay.ts +63 -63
  156. package/src/core/embeddings.ts +110 -93
  157. package/src/core/index.ts +5 -5
  158. package/src/core/logger.ts +36 -36
  159. package/src/core/ml-worker-entry.ts +194 -0
  160. package/src/core/ml-worker.ts +281 -0
  161. package/src/core/query-expander.ts +122 -128
  162. package/src/core/reranker.ts +119 -125
  163. package/src/core/salience.ts +200 -33
  164. package/src/core/write-pipeline.ts +521 -343
  165. package/src/engine/activation.ts +235 -109
  166. package/src/engine/confidence.ts +120 -0
  167. package/src/engine/connections.ts +162 -103
  168. package/src/engine/consolidation-scheduler.ts +242 -125
  169. package/src/engine/consolidation.ts +138 -45
  170. package/src/engine/eval.ts +102 -102
  171. package/src/engine/eviction.ts +101 -101
  172. package/src/engine/index.ts +8 -8
  173. package/src/engine/retraction.ts +366 -116
  174. package/src/engine/staging.ts +74 -74
  175. package/src/hooks/sidecar.ts +5 -5
  176. package/src/index.ts +226 -212
  177. package/src/mcp.ts +1145 -1121
  178. package/src/storage/factory.ts +147 -0
  179. package/src/storage/index.ts +3 -3
  180. package/src/storage/pglite-schema.ts +166 -0
  181. package/src/storage/pglite.ts +1363 -0
  182. package/src/storage/sqlite.ts +402 -7
  183. package/src/storage/store.ts +80 -0
  184. package/src/types/agent.ts +67 -67
  185. package/src/types/checkpoint.ts +46 -46
  186. package/src/types/engram.ts +101 -3
  187. package/src/types/eval.ts +100 -100
  188. package/src/types/index.ts +6 -6
@@ -1,93 +1,110 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * Embedding Engine local vector embeddings via transformers.js
5
- *
6
- * Default: bge-small-en-v1.5 (384 dimensions, ~90MB, MTEB retrieval-optimized).
7
- * Better short-text similarity than MiniLM for agent memory concepts.
8
- * Configurable via AWM_EMBED_MODEL env var.
9
- * Model is downloaded once on first use and cached locally.
10
- *
11
- * Singleton pattern call getEmbedder() to get the shared instance.
12
- *
13
- * NOTE: Changing the model invalidates existing embeddings.
14
- * Set AWM_EMBED_MODEL=Xenova/all-MiniLM-L6-v2 for backward compatibility.
15
- */
16
-
17
- import { pipeline, type FeatureExtractionPipeline } from '@huggingface/transformers';
18
-
19
- const MODEL_ID = process.env.AWM_EMBED_MODEL ?? 'Xenova/bge-small-en-v1.5';
20
- const DIMENSIONS = parseInt(process.env.AWM_EMBED_DIMS ?? '384', 10);
21
- const POOLING = (process.env.AWM_EMBED_POOLING ?? 'mean') as 'cls' | 'mean';
22
-
23
- let instance: FeatureExtractionPipeline | null = null;
24
- let initPromise: Promise<FeatureExtractionPipeline> | null = null;
25
-
26
- /**
27
- * Get or initialize the embedding pipeline (singleton).
28
- * First call downloads the model (~22MB), subsequent calls are instant.
29
- */
30
- export async function getEmbedder(): Promise<FeatureExtractionPipeline> {
31
- if (instance) return instance;
32
- if (initPromise) return initPromise;
33
-
34
- initPromise = pipeline('feature-extraction', MODEL_ID, {
35
- dtype: 'fp32',
36
- }).then(pipe => {
37
- instance = pipe;
38
- console.log(`Embedding model loaded: ${MODEL_ID} (${DIMENSIONS}d)`);
39
- return pipe;
40
- });
41
-
42
- return initPromise;
43
- }
44
-
45
- /**
46
- * Generate an embedding vector for a text string.
47
- * Returns a normalized float32 array of length DIMENSIONS.
48
- */
49
- export async function embed(text: string): Promise<number[]> {
50
- const embedder = await getEmbedder();
51
- const result = await embedder(text, { pooling: POOLING, normalize: true });
52
- // result is a Tensor — extract the data
53
- return Array.from(result.data as Float32Array).slice(0, DIMENSIONS);
54
- }
55
-
56
- /** Get the current embedding model ID (for version tracking in stored embeddings) */
57
- export function getModelId(): string {
58
- return MODEL_ID;
59
- }
60
-
61
- /**
62
- * Generate embeddings for multiple texts in a batch.
63
- * More efficient than calling embed() in a loop.
64
- */
65
- export async function embedBatch(texts: string[]): Promise<number[][]> {
66
- if (texts.length === 0) return [];
67
- const embedder = await getEmbedder();
68
- const result = await embedder(texts, { pooling: POOLING, normalize: true });
69
- const data = result.data as Float32Array;
70
-
71
- const vectors: number[][] = [];
72
- for (let i = 0; i < texts.length; i++) {
73
- vectors.push(Array.from(data.slice(i * DIMENSIONS, (i + 1) * DIMENSIONS)));
74
- }
75
- return vectors;
76
- }
77
-
78
- /**
79
- * Cosine similarity between two normalized vectors.
80
- * Since vectors are pre-normalized, this is just the dot product.
81
- */
82
- export function cosineSimilarity(a: number[], b: number[]): number {
83
- if (a.length !== b.length || a.length === 0) return 0;
84
- let dot = 0;
85
- for (let i = 0; i < a.length; i++) {
86
- dot += a[i] * b[i];
87
- }
88
- // Clamp to [-1, 1] to handle floating point drift
89
- return Math.max(-1, Math.min(1, dot));
90
- }
91
-
92
- /** Vector dimensions for this model */
93
- export const EMBEDDING_DIMENSIONS = DIMENSIONS;
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Embedding Engine - vector embeddings via the ML worker pool.
5
+ *
6
+ * Default model: bge-small-en-v1.5 (384 dimensions, ~90MB, MTEB retrieval-optimized).
7
+ * Configurable via AWM_EMBED_MODEL env var.
8
+ *
9
+ * AWM 0.8.x: inference dispatches through ml-worker.ts. The worker_threads
10
+ * path was planned but reverted to in-process because onnxruntime-node's
11
+ * native bindings store V8 handles that don't cross isolate boundaries
12
+ * safely — see ml-worker.ts for the full status. The dispatch abstraction
13
+ * is preserved for a future child_process or HTTP sidecar pool.
14
+ * `AWM_ML_INPROCESS=1` is honored as a no-op (in-process is now the default).
15
+ *
16
+ * NOTE: Changing the model invalidates existing embeddings.
17
+ * Set AWM_EMBED_MODEL=Xenova/all-MiniLM-L6-v2 for backward compatibility.
18
+ */
19
+
20
+ import { pipeline, type FeatureExtractionPipeline } from '@huggingface/transformers';
21
+ import { dispatchEmbed, registerInProcessHandlers } from './ml-worker.js';
22
+
23
+ const MODEL_ID = process.env.AWM_EMBED_MODEL ?? 'Xenova/bge-small-en-v1.5';
24
+ const DIMENSIONS = parseInt(process.env.AWM_EMBED_DIMS ?? '384', 10);
25
+ const POOLING = (process.env.AWM_EMBED_POOLING ?? 'mean') as 'cls' | 'mean';
26
+
27
+ // --- In-process fallback (used by tests and crash recovery) ---
28
+
29
+ let inProcessInstance: FeatureExtractionPipeline | null = null;
30
+ let inProcessInitPromise: Promise<FeatureExtractionPipeline> | null = null;
31
+
32
+ async function loadInProcess(): Promise<FeatureExtractionPipeline> {
33
+ if (inProcessInstance) return inProcessInstance;
34
+ if (inProcessInitPromise) return inProcessInitPromise;
35
+ inProcessInitPromise = pipeline('feature-extraction', MODEL_ID, { dtype: 'fp32' }).then(pipe => {
36
+ inProcessInstance = pipe;
37
+ console.log(`Embedding model loaded in-process: ${MODEL_ID} (${DIMENSIONS}d)`);
38
+ return pipe;
39
+ });
40
+ return inProcessInitPromise;
41
+ }
42
+
43
+ async function inProcessEmbed(args: { texts: string[]; pooling: 'cls' | 'mean'; dimensions: number }): Promise<number[][]> {
44
+ const { texts, pooling, dimensions } = args;
45
+ if (texts.length === 0) return [];
46
+ const embedder = await loadInProcess();
47
+ const result = await embedder(texts, { pooling, normalize: true });
48
+ const data = result.data as Float32Array;
49
+ const vectors: number[][] = [];
50
+ for (let i = 0; i < texts.length; i++) {
51
+ vectors.push(Array.from(data.slice(i * dimensions, (i + 1) * dimensions)));
52
+ }
53
+ return vectors;
54
+ }
55
+
56
+ // Register the in-process handler with the pool (used in test mode and as fallback)
57
+ registerInProcessHandlers({ embed: inProcessEmbed });
58
+
59
+ // --- Public API ---
60
+
61
+ /**
62
+ * Get or initialize the embedding pipeline (singleton).
63
+ * Kept for backwards compat returns the in-process pipeline only.
64
+ * Most consumers should use embed() / embedBatch() which dispatch
65
+ * to the worker pool by default.
66
+ */
67
+ export async function getEmbedder(): Promise<FeatureExtractionPipeline> {
68
+ return loadInProcess();
69
+ }
70
+
71
+ /**
72
+ * Generate an embedding vector for a text string.
73
+ * Dispatches to the worker pool (or in-process fallback).
74
+ */
75
+ export async function embed(text: string): Promise<number[]> {
76
+ const vectors = await dispatchEmbed({ texts: [text], pooling: POOLING, dimensions: DIMENSIONS });
77
+ return vectors[0] ?? new Array(DIMENSIONS).fill(0);
78
+ }
79
+
80
+ /**
81
+ * Generate embeddings for multiple texts in a batch.
82
+ * More efficient than calling embed() in a loop the worker batches the
83
+ * tokenization + forward pass.
84
+ */
85
+ export async function embedBatch(texts: string[]): Promise<number[][]> {
86
+ if (texts.length === 0) return [];
87
+ return dispatchEmbed({ texts, pooling: POOLING, dimensions: DIMENSIONS });
88
+ }
89
+
90
+ /** Get the current embedding model ID (for version tracking in stored embeddings) */
91
+ export function getModelId(): string {
92
+ return MODEL_ID;
93
+ }
94
+
95
+ /**
96
+ * Cosine similarity between two normalized vectors.
97
+ * Since vectors are pre-normalized, this is just the dot product.
98
+ */
99
+ export function cosineSimilarity(a: number[], b: number[]): number {
100
+ if (a.length !== b.length || a.length === 0) return 0;
101
+ let dot = 0;
102
+ for (let i = 0; i < a.length; i++) {
103
+ dot += a[i] * b[i];
104
+ }
105
+ // Clamp to [-1, 1] to handle floating point drift
106
+ return Math.max(-1, Math.min(1, dot));
107
+ }
108
+
109
+ /** Vector dimensions for this model */
110
+ export const EMBEDDING_DIMENSIONS = DIMENSIONS;
package/src/core/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- export * from './decay.js';
4
- export * from './hebbian.js';
5
- export * from './salience.js';
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export * from './decay.js';
4
+ export * from './hebbian.js';
5
+ export * from './salience.js';
@@ -1,36 +1,36 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * Simple file logger for AWM activity.
5
- *
6
- * Appends one line per event to data/awm.log (next to memory.db).
7
- * Format: ISO timestamp | agent | event | detail
8
- *
9
- * Designed for dev pilot observability — know at a glance what's happening.
10
- */
11
-
12
- import { appendFileSync, mkdirSync } from 'node:fs';
13
- import { dirname, resolve } from 'node:path';
14
-
15
- let logPath: string | null = null;
16
-
17
- export function initLogger(dbPath: string): void {
18
- const dir = dirname(resolve(dbPath));
19
- mkdirSync(dir, { recursive: true });
20
- logPath = resolve(dir, 'awm.log');
21
- }
22
-
23
- export function log(agentId: string, event: string, detail: string): void {
24
- if (!logPath) return;
25
- const ts = new Date().toISOString();
26
- const line = `${ts} | ${agentId} | ${event} | ${detail}\n`;
27
- try {
28
- appendFileSync(logPath, line);
29
- } catch {
30
- // Logging should never crash the server
31
- }
32
- }
33
-
34
- export function getLogPath(): string | null {
35
- return logPath;
36
- }
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Simple file logger for AWM activity.
5
+ *
6
+ * Appends one line per event to data/awm.log (next to memory.db).
7
+ * Format: ISO timestamp | agent | event | detail
8
+ *
9
+ * Designed for dev pilot observability — know at a glance what's happening.
10
+ */
11
+
12
+ import { appendFileSync, mkdirSync } from 'node:fs';
13
+ import { dirname, resolve } from 'node:path';
14
+
15
+ let logPath: string | null = null;
16
+
17
+ export function initLogger(dbPath: string): void {
18
+ const dir = dirname(resolve(dbPath));
19
+ mkdirSync(dir, { recursive: true });
20
+ logPath = resolve(dir, 'awm.log');
21
+ }
22
+
23
+ export function log(agentId: string, event: string, detail: string): void {
24
+ if (!logPath) return;
25
+ const ts = new Date().toISOString();
26
+ const line = `${ts} | ${agentId} | ${event} | ${detail}\n`;
27
+ try {
28
+ appendFileSync(logPath, line);
29
+ } catch {
30
+ // Logging should never crash the server
31
+ }
32
+ }
33
+
34
+ export function getLogPath(): string | null {
35
+ return logPath;
36
+ }
@@ -0,0 +1,194 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * ML worker entry — runs INSIDE a worker_thread.
5
+ *
6
+ * Loaded once per worker, sets up the model for the assigned role
7
+ * (embed | rerank | expand), then handles request messages from the
8
+ * main thread via the parentPort.
9
+ *
10
+ * Protocol:
11
+ * Main thread → worker: { id, op, args } (op matches the worker's role)
12
+ * Worker → main thread: { id, ok: true, result } or { id, ok: false, error }
13
+ * Worker → main thread: { ready: true } (one-time signal after model load)
14
+ * Main thread → worker: { shutdown: true } (drain queue, then terminate)
15
+ *
16
+ * The worker stays loaded — the model lives in memory for the worker's lifetime.
17
+ */
18
+
19
+ import { parentPort, workerData } from 'node:worker_threads';
20
+
21
+ if (!parentPort) {
22
+ throw new Error('ml-worker-entry: must be loaded as a worker_thread');
23
+ }
24
+
25
+ type WorkerRole = 'embed' | 'rerank' | 'expand';
26
+ const role: WorkerRole = workerData?.role;
27
+ if (role !== 'embed' && role !== 'rerank' && role !== 'expand') {
28
+ throw new Error(`ml-worker-entry: invalid role '${role}'`);
29
+ }
30
+
31
+ // --- Lazy model loaders (each worker loads only its own model) ---
32
+
33
+ let embedderPipeline: any = null;
34
+ let rerankerTokenizer: any = null;
35
+ let rerankerModel: any = null;
36
+ let expanderPipeline: any = null;
37
+
38
+ // Inside worker_threads we must use the WASM ONNX backend, not the native one.
39
+ // onnxruntime-node's native bindings store V8 handles that get invalidated when
40
+ // crossing isolate boundaries — calling from a worker crashes with
41
+ // `v8::HandleScope::CreateHandle()` failures. The WASM backend is V8-safe.
42
+ const WORKER_DEVICE = 'wasm' as const;
43
+
44
+ async function loadEmbedder(): Promise<void> {
45
+ const { pipeline } = await import('@huggingface/transformers');
46
+ const modelId = process.env.AWM_EMBED_MODEL ?? 'Xenova/bge-small-en-v1.5';
47
+ embedderPipeline = await pipeline('feature-extraction', modelId, { dtype: 'fp32', device: WORKER_DEVICE });
48
+ }
49
+
50
+ async function loadReranker(): Promise<void> {
51
+ const { AutoTokenizer, AutoModelForSequenceClassification } = await import('@huggingface/transformers');
52
+ const modelId = process.env.AWM_RERANKER_MODEL || 'Xenova/ms-marco-MiniLM-L-6-v2';
53
+ rerankerTokenizer = await AutoTokenizer.from_pretrained(modelId);
54
+ rerankerModel = await AutoModelForSequenceClassification.from_pretrained(modelId, { dtype: 'fp32', device: WORKER_DEVICE });
55
+ }
56
+
57
+ async function loadExpander(): Promise<void> {
58
+ const { pipeline } = await import('@huggingface/transformers');
59
+ expanderPipeline = await pipeline('text2text-generation', 'Xenova/flan-t5-small', { dtype: 'fp32', device: WORKER_DEVICE });
60
+ }
61
+
62
+ // --- Per-role inference handlers ---
63
+
64
+ function sigmoid(x: number): number {
65
+ return 1 / (1 + Math.exp(-x));
66
+ }
67
+
68
+ async function handleEmbed(args: { texts: string[]; pooling: 'cls' | 'mean'; dimensions: number }): Promise<number[][]> {
69
+ if (!embedderPipeline) throw new Error('embedder not loaded');
70
+ const { texts, pooling, dimensions } = args;
71
+ if (texts.length === 0) return [];
72
+ const result = await embedderPipeline(texts, { pooling, normalize: true });
73
+ const data = result.data as Float32Array;
74
+ const vectors: number[][] = [];
75
+ for (let i = 0; i < texts.length; i++) {
76
+ vectors.push(Array.from(data.slice(i * dimensions, (i + 1) * dimensions)));
77
+ }
78
+ return vectors;
79
+ }
80
+
81
+ interface RerankResult { index: number; score: number; }
82
+
83
+ async function handleRerank(args: { query: string; passages: string[] }): Promise<RerankResult[]> {
84
+ if (!rerankerTokenizer || !rerankerModel) throw new Error('reranker not loaded');
85
+ const { query, passages } = args;
86
+ if (passages.length === 0) return [];
87
+
88
+ // Batch path
89
+ try {
90
+ const queries = passages.map(() => query);
91
+ const inputs = rerankerTokenizer(queries, {
92
+ text_pair: passages,
93
+ padding: true,
94
+ truncation: true,
95
+ return_tensors: 'pt',
96
+ });
97
+ const output = await rerankerModel(inputs);
98
+ const logits = output.logits ?? output.last_hidden_state;
99
+ const data = logits.data as Float32Array | number[];
100
+ const results: RerankResult[] = [];
101
+ for (let i = 0; i < passages.length; i++) {
102
+ const rawLogit = Number(data[i] ?? 0);
103
+ results.push({ index: i, score: sigmoid(rawLogit) });
104
+ }
105
+ results.sort((a, b) => b.score - a.score);
106
+ return results;
107
+ } catch {
108
+ // Per-passage fallback
109
+ const results: RerankResult[] = [];
110
+ for (let i = 0; i < passages.length; i++) {
111
+ try {
112
+ const inputs = rerankerTokenizer(query, {
113
+ text_pair: passages[i],
114
+ padding: true,
115
+ truncation: true,
116
+ return_tensors: 'pt',
117
+ });
118
+ const output = await rerankerModel(inputs);
119
+ const logits = output.logits ?? output.last_hidden_state;
120
+ const rawLogit = logits.data[0] as number;
121
+ results.push({ index: i, score: sigmoid(rawLogit) });
122
+ } catch {
123
+ results.push({ index: i, score: 0 });
124
+ }
125
+ }
126
+ results.sort((a, b) => b.score - a.score);
127
+ return results;
128
+ }
129
+ }
130
+
131
+ async function handleExpand(args: { prompt: string; maxNewTokens: number; noRepeatNgramSize: number }): Promise<string> {
132
+ if (!expanderPipeline) throw new Error('expander not loaded');
133
+ const result = await expanderPipeline(args.prompt, {
134
+ max_new_tokens: args.maxNewTokens,
135
+ no_repeat_ngram_size: args.noRepeatNgramSize,
136
+ });
137
+ const text = Array.isArray(result) ? (result[0] as any)?.generated_text ?? '' : '';
138
+ return String(text).trim();
139
+ }
140
+
141
+ // --- Main loop ---
142
+
143
+ let shuttingDown = false;
144
+ const inflight = new Set<Promise<void>>();
145
+
146
+ async function loadModel(): Promise<void> {
147
+ switch (role) {
148
+ case 'embed': await loadEmbedder(); break;
149
+ case 'rerank': await loadReranker(); break;
150
+ case 'expand': await loadExpander(); break;
151
+ }
152
+ }
153
+
154
+ async function handleMessage(msg: { id: number; op: WorkerRole; args: any }): Promise<void> {
155
+ try {
156
+ let result: unknown;
157
+ switch (msg.op) {
158
+ case 'embed': result = await handleEmbed(msg.args); break;
159
+ case 'rerank': result = await handleRerank(msg.args); break;
160
+ case 'expand': result = await handleExpand(msg.args); break;
161
+ }
162
+ parentPort!.postMessage({ id: msg.id, ok: true, result });
163
+ } catch (err) {
164
+ parentPort!.postMessage({ id: msg.id, ok: false, error: String((err as Error)?.message ?? err) });
165
+ }
166
+ }
167
+
168
+ (async () => {
169
+ try {
170
+ await loadModel();
171
+ parentPort!.postMessage({ ready: true, role });
172
+ } catch (err) {
173
+ parentPort!.postMessage({ ready: false, role, error: String((err as Error)?.message ?? err) });
174
+ process.exit(1);
175
+ }
176
+ })();
177
+
178
+ parentPort.on('message', (msg: any) => {
179
+ if (msg?.shutdown) {
180
+ shuttingDown = true;
181
+ // Wait for in-flight work, then exit
182
+ void Promise.allSettled([...inflight]).then(() => {
183
+ parentPort!.postMessage({ shutdown: 'done' });
184
+ process.exit(0);
185
+ });
186
+ return;
187
+ }
188
+ if (shuttingDown) return;
189
+ if (typeof msg?.id !== 'number' || typeof msg?.op !== 'string') return;
190
+
191
+ const promise = handleMessage(msg);
192
+ inflight.add(promise);
193
+ void promise.finally(() => inflight.delete(promise));
194
+ });