agent-working-memory 0.3.0

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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/dist/api/index.d.ts +2 -0
  4. package/dist/api/index.d.ts.map +1 -0
  5. package/dist/api/index.js +2 -0
  6. package/dist/api/index.js.map +1 -0
  7. package/dist/api/routes.d.ts +53 -0
  8. package/dist/api/routes.d.ts.map +1 -0
  9. package/dist/api/routes.js +388 -0
  10. package/dist/api/routes.js.map +1 -0
  11. package/dist/cli.d.ts +12 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +245 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/core/decay.d.ts +36 -0
  16. package/dist/core/decay.d.ts.map +1 -0
  17. package/dist/core/decay.js +38 -0
  18. package/dist/core/decay.js.map +1 -0
  19. package/dist/core/embeddings.d.ts +33 -0
  20. package/dist/core/embeddings.d.ts.map +1 -0
  21. package/dist/core/embeddings.js +76 -0
  22. package/dist/core/embeddings.js.map +1 -0
  23. package/dist/core/hebbian.d.ts +38 -0
  24. package/dist/core/hebbian.d.ts.map +1 -0
  25. package/dist/core/hebbian.js +74 -0
  26. package/dist/core/hebbian.js.map +1 -0
  27. package/dist/core/index.d.ts +4 -0
  28. package/dist/core/index.d.ts.map +1 -0
  29. package/dist/core/index.js +4 -0
  30. package/dist/core/index.js.map +1 -0
  31. package/dist/core/query-expander.d.ts +24 -0
  32. package/dist/core/query-expander.d.ts.map +1 -0
  33. package/dist/core/query-expander.js +58 -0
  34. package/dist/core/query-expander.js.map +1 -0
  35. package/dist/core/reranker.d.ts +25 -0
  36. package/dist/core/reranker.d.ts.map +1 -0
  37. package/dist/core/reranker.js +75 -0
  38. package/dist/core/reranker.js.map +1 -0
  39. package/dist/core/salience.d.ts +30 -0
  40. package/dist/core/salience.d.ts.map +1 -0
  41. package/dist/core/salience.js +81 -0
  42. package/dist/core/salience.js.map +1 -0
  43. package/dist/engine/activation.d.ts +38 -0
  44. package/dist/engine/activation.d.ts.map +1 -0
  45. package/dist/engine/activation.js +516 -0
  46. package/dist/engine/activation.js.map +1 -0
  47. package/dist/engine/connections.d.ts +31 -0
  48. package/dist/engine/connections.d.ts.map +1 -0
  49. package/dist/engine/connections.js +74 -0
  50. package/dist/engine/connections.js.map +1 -0
  51. package/dist/engine/consolidation-scheduler.d.ts +31 -0
  52. package/dist/engine/consolidation-scheduler.d.ts.map +1 -0
  53. package/dist/engine/consolidation-scheduler.js +115 -0
  54. package/dist/engine/consolidation-scheduler.js.map +1 -0
  55. package/dist/engine/consolidation.d.ts +62 -0
  56. package/dist/engine/consolidation.d.ts.map +1 -0
  57. package/dist/engine/consolidation.js +368 -0
  58. package/dist/engine/consolidation.js.map +1 -0
  59. package/dist/engine/eval.d.ts +22 -0
  60. package/dist/engine/eval.d.ts.map +1 -0
  61. package/dist/engine/eval.js +79 -0
  62. package/dist/engine/eval.js.map +1 -0
  63. package/dist/engine/eviction.d.ts +29 -0
  64. package/dist/engine/eviction.d.ts.map +1 -0
  65. package/dist/engine/eviction.js +86 -0
  66. package/dist/engine/eviction.js.map +1 -0
  67. package/dist/engine/index.d.ts +7 -0
  68. package/dist/engine/index.d.ts.map +1 -0
  69. package/dist/engine/index.js +7 -0
  70. package/dist/engine/index.js.map +1 -0
  71. package/dist/engine/retraction.d.ts +32 -0
  72. package/dist/engine/retraction.d.ts.map +1 -0
  73. package/dist/engine/retraction.js +77 -0
  74. package/dist/engine/retraction.js.map +1 -0
  75. package/dist/engine/staging.d.ts +33 -0
  76. package/dist/engine/staging.d.ts.map +1 -0
  77. package/dist/engine/staging.js +63 -0
  78. package/dist/engine/staging.js.map +1 -0
  79. package/dist/index.d.ts +2 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +95 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/mcp.d.ts +24 -0
  84. package/dist/mcp.d.ts.map +1 -0
  85. package/dist/mcp.js +532 -0
  86. package/dist/mcp.js.map +1 -0
  87. package/dist/storage/index.d.ts +2 -0
  88. package/dist/storage/index.d.ts.map +1 -0
  89. package/dist/storage/index.js +2 -0
  90. package/dist/storage/index.js.map +1 -0
  91. package/dist/storage/sqlite.d.ts +116 -0
  92. package/dist/storage/sqlite.d.ts.map +1 -0
  93. package/dist/storage/sqlite.js +750 -0
  94. package/dist/storage/sqlite.js.map +1 -0
  95. package/dist/types/agent.d.ts +30 -0
  96. package/dist/types/agent.d.ts.map +1 -0
  97. package/dist/types/agent.js +23 -0
  98. package/dist/types/agent.js.map +1 -0
  99. package/dist/types/checkpoint.d.ts +50 -0
  100. package/dist/types/checkpoint.d.ts.map +1 -0
  101. package/dist/types/checkpoint.js +8 -0
  102. package/dist/types/checkpoint.js.map +1 -0
  103. package/dist/types/engram.d.ts +165 -0
  104. package/dist/types/engram.d.ts.map +1 -0
  105. package/dist/types/engram.js +8 -0
  106. package/dist/types/engram.js.map +1 -0
  107. package/dist/types/eval.d.ts +84 -0
  108. package/dist/types/eval.d.ts.map +1 -0
  109. package/dist/types/eval.js +11 -0
  110. package/dist/types/eval.js.map +1 -0
  111. package/dist/types/index.d.ts +5 -0
  112. package/dist/types/index.d.ts.map +1 -0
  113. package/dist/types/index.js +5 -0
  114. package/dist/types/index.js.map +1 -0
  115. package/package.json +55 -0
  116. package/src/api/index.ts +1 -0
  117. package/src/api/routes.ts +528 -0
  118. package/src/cli.ts +260 -0
  119. package/src/core/decay.ts +61 -0
  120. package/src/core/embeddings.ts +82 -0
  121. package/src/core/hebbian.ts +91 -0
  122. package/src/core/index.ts +3 -0
  123. package/src/core/query-expander.ts +64 -0
  124. package/src/core/reranker.ts +99 -0
  125. package/src/core/salience.ts +95 -0
  126. package/src/engine/activation.ts +577 -0
  127. package/src/engine/connections.ts +101 -0
  128. package/src/engine/consolidation-scheduler.ts +123 -0
  129. package/src/engine/consolidation.ts +443 -0
  130. package/src/engine/eval.ts +100 -0
  131. package/src/engine/eviction.ts +99 -0
  132. package/src/engine/index.ts +6 -0
  133. package/src/engine/retraction.ts +98 -0
  134. package/src/engine/staging.ts +72 -0
  135. package/src/index.ts +100 -0
  136. package/src/mcp.ts +635 -0
  137. package/src/storage/index.ts +1 -0
  138. package/src/storage/sqlite.ts +893 -0
  139. package/src/types/agent.ts +65 -0
  140. package/src/types/checkpoint.ts +44 -0
  141. package/src/types/engram.ts +194 -0
  142. package/src/types/eval.ts +98 -0
  143. package/src/types/index.ts +4 -0
package/src/cli.ts ADDED
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI entrypoint for AgentWorkingMemory.
5
+ *
6
+ * Commands:
7
+ * awm setup — configure MCP for the current project
8
+ * awm mcp — start the MCP server (called by Claude Code)
9
+ * awm serve — start the HTTP API server
10
+ * awm health — check if a running server is healthy
11
+ */
12
+
13
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
14
+ import { resolve, basename, join, dirname } from 'node:path';
15
+ import { execSync } from 'node:child_process';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+
21
+ // Load .env if present
22
+ try {
23
+ const envPath = resolve(process.cwd(), '.env');
24
+ const envContent = readFileSync(envPath, 'utf-8');
25
+ for (const line of envContent.split('\n')) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed || trimmed.startsWith('#')) continue;
28
+ const eqIdx = trimmed.indexOf('=');
29
+ if (eqIdx === -1) continue;
30
+ const key = trimmed.slice(0, eqIdx).trim();
31
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
32
+ if (!process.env[key]) process.env[key] = val;
33
+ }
34
+ } catch { /* No .env file */ }
35
+
36
+ const args = process.argv.slice(2);
37
+ const command = args[0];
38
+
39
+ function printUsage() {
40
+ console.log(`
41
+ AgentWorkingMemory — Cognitive memory for AI agents
42
+
43
+ Usage:
44
+ awm setup [--agent-id <id>] [--db-path <path>] [--no-claude-md]
45
+ Configure MCP for current project
46
+ awm mcp Start MCP server (used by Claude Code)
47
+ awm serve [--port <port>] Start HTTP API server
48
+ awm health [--port <port>] Check server health
49
+
50
+ Setup:
51
+ Run 'awm setup' in your project directory. This creates .mcp.json
52
+ and appends workflow instructions to CLAUDE.md so Claude Code
53
+ automatically connects to your memory layer.
54
+
55
+ Use --no-claude-md to skip CLAUDE.md modification.
56
+ Restart Claude Code after setup to pick up the new MCP server.
57
+ `.trim());
58
+ }
59
+
60
+ // ─── SETUP ──────────────────────────────────────
61
+
62
+ function setup() {
63
+ const cwd = process.cwd();
64
+ const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, '-');
65
+
66
+ // Parse flags
67
+ let agentId = projectName;
68
+ let dbPath: string | null = null;
69
+ let skipClaudeMd = false;
70
+
71
+ for (let i = 1; i < args.length; i++) {
72
+ if (args[i] === '--agent-id' && args[i + 1]) {
73
+ agentId = args[++i];
74
+ } else if (args[i] === '--db-path' && args[i + 1]) {
75
+ dbPath = args[++i];
76
+ } else if (args[i] === '--no-claude-md') {
77
+ skipClaudeMd = true;
78
+ }
79
+ }
80
+
81
+ // Find the package root (where src/mcp.ts lives)
82
+ const packageRoot = resolve(__dirname, '..');
83
+ const mcpScript = join(packageRoot, 'src', 'mcp.ts');
84
+ const mcpDist = join(packageRoot, 'dist', 'mcp.js');
85
+
86
+ // Determine DB path — default to <awm-root>/data/memory.db (shared across projects)
87
+ if (!dbPath) {
88
+ dbPath = join(packageRoot, 'data', 'memory.db');
89
+ }
90
+ const dbDir = dirname(dbPath);
91
+
92
+ // Ensure data directory exists
93
+ if (!existsSync(dbDir)) {
94
+ mkdirSync(dbDir, { recursive: true });
95
+ console.log(`Created data directory: ${dbDir}`);
96
+ }
97
+
98
+ // Determine command based on platform and whether dist exists
99
+ const isWindows = process.platform === 'win32';
100
+ const hasDist = existsSync(mcpDist);
101
+
102
+ let mcpConfig: { command: string; args: string[]; env: Record<string, string> };
103
+
104
+ if (hasDist) {
105
+ // Use compiled JS (faster startup, no tsx needed)
106
+ mcpConfig = {
107
+ command: 'node',
108
+ args: [mcpDist.replace(/\\/g, '/')],
109
+ env: {
110
+ AWM_DB_PATH: dbPath.replace(/\\/g, '/'),
111
+ AWM_AGENT_ID: agentId,
112
+ },
113
+ };
114
+ } else if (isWindows) {
115
+ mcpConfig = {
116
+ command: 'cmd',
117
+ args: ['/c', 'npx', 'tsx', mcpScript.replace(/\\/g, '/')],
118
+ env: {
119
+ AWM_DB_PATH: dbPath.replace(/\\/g, '/'),
120
+ AWM_AGENT_ID: agentId,
121
+ },
122
+ };
123
+ } else {
124
+ mcpConfig = {
125
+ command: 'npx',
126
+ args: ['tsx', mcpScript],
127
+ env: {
128
+ AWM_DB_PATH: dbPath,
129
+ AWM_AGENT_ID: agentId,
130
+ },
131
+ };
132
+ }
133
+
134
+ // Read or create .mcp.json
135
+ const mcpJsonPath = join(cwd, '.mcp.json');
136
+ let existing: any = { mcpServers: {} };
137
+ if (existsSync(mcpJsonPath)) {
138
+ try {
139
+ existing = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
140
+ if (!existing.mcpServers) existing.mcpServers = {};
141
+ } catch {
142
+ existing = { mcpServers: {} };
143
+ }
144
+ }
145
+
146
+ existing.mcpServers['agent-working-memory'] = mcpConfig;
147
+ writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
148
+
149
+ // Auto-append CLAUDE.md snippet unless --no-claude-md
150
+ let claudeMdAction = '';
151
+ const claudeMdSnippet = `
152
+
153
+ ## Memory (AWM)
154
+ You have persistent memory via the agent-working-memory MCP server.
155
+ - At conversation start: call memory_restore to recover previous context
156
+ - When you learn something important: call memory_write
157
+ - When you need past context: call memory_recall
158
+ - Before long operations: call memory_checkpoint to save your state
159
+ - After using a recalled memory: call memory_feedback (useful/not-useful)
160
+ - To retract incorrect info: call memory_retract
161
+ - To manage tasks: call memory_task_add, memory_task_update, memory_task_list, memory_task_next
162
+ `;
163
+
164
+ const claudeMdPath = join(cwd, 'CLAUDE.md');
165
+ if (skipClaudeMd) {
166
+ claudeMdAction = ' CLAUDE.md: skipped (--no-claude-md)';
167
+ } else if (existsSync(claudeMdPath)) {
168
+ const content = readFileSync(claudeMdPath, 'utf-8');
169
+ if (content.includes('## Memory (AWM)')) {
170
+ claudeMdAction = ' CLAUDE.md: already has AWM section (skipped)';
171
+ } else {
172
+ writeFileSync(claudeMdPath, content.trimEnd() + '\n' + claudeMdSnippet);
173
+ claudeMdAction = ' CLAUDE.md: appended AWM workflow section';
174
+ }
175
+ } else {
176
+ writeFileSync(claudeMdPath, `# ${basename(cwd)}\n${claudeMdSnippet}`);
177
+ claudeMdAction = ' CLAUDE.md: created with AWM workflow section';
178
+ }
179
+
180
+ console.log(`
181
+ AWM configured for: ${cwd}
182
+
183
+ Agent ID: ${agentId}
184
+ DB path: ${dbPath}
185
+ MCP config: ${mcpJsonPath}
186
+ ${claudeMdAction}
187
+
188
+ Next steps:
189
+ 1. Restart Claude Code to pick up the MCP server
190
+ 2. The memory tools will appear automatically
191
+ `.trim());
192
+ }
193
+
194
+ // ─── MCP ──────────────────────────────────────
195
+
196
+ async function mcp() {
197
+ // Dynamic import to avoid loading heavy deps for setup/health commands
198
+ await import('./mcp.js');
199
+ }
200
+
201
+ // ─── SERVE ──────────────────────────────────────
202
+
203
+ async function serve() {
204
+ // Parse --port flag
205
+ for (let i = 1; i < args.length; i++) {
206
+ if (args[i] === '--port' && args[i + 1]) {
207
+ process.env.AWM_PORT = args[++i];
208
+ }
209
+ }
210
+ await import('./index.js');
211
+ }
212
+
213
+ // ─── HEALTH ──────────────────────────────────────
214
+
215
+ function health() {
216
+ let port = '8400';
217
+ for (let i = 1; i < args.length; i++) {
218
+ if (args[i] === '--port' && args[i + 1]) {
219
+ port = args[++i];
220
+ }
221
+ }
222
+
223
+ try {
224
+ const result = execSync(`curl -sf http://localhost:${port}/health`, {
225
+ encoding: 'utf8',
226
+ timeout: 5000,
227
+ });
228
+ const data = JSON.parse(result);
229
+ console.log(`OK — v${data.version} (${data.timestamp})`);
230
+ } catch {
231
+ console.error(`Cannot reach AWM server on port ${port}`);
232
+ process.exit(1);
233
+ }
234
+ }
235
+
236
+ // ─── Dispatch ──────────────────────────────────────
237
+
238
+ switch (command) {
239
+ case 'setup':
240
+ setup();
241
+ break;
242
+ case 'mcp':
243
+ mcp();
244
+ break;
245
+ case 'serve':
246
+ serve();
247
+ break;
248
+ case 'health':
249
+ health();
250
+ break;
251
+ case '--help':
252
+ case '-h':
253
+ case undefined:
254
+ printUsage();
255
+ break;
256
+ default:
257
+ console.error(`Unknown command: ${command}`);
258
+ printUsage();
259
+ process.exit(1);
260
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * ACT-R Base-Level Activation
3
+ *
4
+ * Based on Anderson's ACT-R cognitive architecture (1993).
5
+ * Memories that are accessed more recently and more frequently
6
+ * have higher activation — a well-established model of human memory.
7
+ *
8
+ * Formula: B(M) = ln(n + 1) - d * ln(ageDays / (n + 1))
9
+ *
10
+ * Where:
11
+ * n = access count
12
+ * d = decay exponent (default 0.5)
13
+ * ageDays = age of memory in days
14
+ */
15
+
16
+ export function baseLevelActivation(
17
+ accessCount: number,
18
+ ageDays: number,
19
+ decayExponent: number = 0.5
20
+ ): number {
21
+ const n = Math.max(accessCount, 0);
22
+ const age = Math.max(ageDays, 0.001); // Avoid log(0)
23
+ return Math.log(n + 1) - decayExponent * Math.log(age / (n + 1));
24
+ }
25
+
26
+ /**
27
+ * Softplus — smooth approximation of ReLU.
28
+ * Used to keep activation scores positive without hard clipping.
29
+ */
30
+ export function softplus(x: number): number {
31
+ return Math.log(1 + Math.exp(x));
32
+ }
33
+
34
+ /**
35
+ * Composite activation score combining content match, temporal decay,
36
+ * Hebbian boost, and confidence.
37
+ *
38
+ * Score = contentMatch * softplus(B(M) + scale * hebbianBoost) * confidence
39
+ */
40
+ export function compositeScore(params: {
41
+ contentMatch: number;
42
+ accessCount: number;
43
+ ageDays: number;
44
+ hebbianBoost: number;
45
+ confidence: number;
46
+ decayExponent?: number;
47
+ hebbianScale?: number;
48
+ }): number {
49
+ const {
50
+ contentMatch,
51
+ accessCount,
52
+ ageDays,
53
+ hebbianBoost,
54
+ confidence,
55
+ decayExponent = 0.5,
56
+ hebbianScale = 1.0,
57
+ } = params;
58
+
59
+ const bm = baseLevelActivation(accessCount, ageDays, decayExponent);
60
+ return contentMatch * softplus(bm + hebbianScale * hebbianBoost) * confidence;
61
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Embedding Engine — local vector embeddings via transformers.js
3
+ *
4
+ * Default: gte-small (384 dimensions, ~34MB int8, MTEB 61.4) for semantic similarity.
5
+ * Configurable via AWM_EMBED_MODEL env var.
6
+ * Model is downloaded once on first use and cached locally.
7
+ *
8
+ * Singleton pattern — call getEmbedder() to get the shared instance.
9
+ */
10
+
11
+ import { pipeline, type FeatureExtractionPipeline } from '@huggingface/transformers';
12
+
13
+ const MODEL_ID = process.env.AWM_EMBED_MODEL ?? 'Xenova/all-MiniLM-L6-v2';
14
+ const DIMENSIONS = parseInt(process.env.AWM_EMBED_DIMS ?? '384', 10);
15
+ const POOLING = (process.env.AWM_EMBED_POOLING ?? 'mean') as 'cls' | 'mean';
16
+
17
+ let instance: FeatureExtractionPipeline | null = null;
18
+ let initPromise: Promise<FeatureExtractionPipeline> | null = null;
19
+
20
+ /**
21
+ * Get or initialize the embedding pipeline (singleton).
22
+ * First call downloads the model (~22MB), subsequent calls are instant.
23
+ */
24
+ export async function getEmbedder(): Promise<FeatureExtractionPipeline> {
25
+ if (instance) return instance;
26
+ if (initPromise) return initPromise;
27
+
28
+ initPromise = pipeline('feature-extraction', MODEL_ID, {
29
+ dtype: 'fp32',
30
+ }).then(pipe => {
31
+ instance = pipe;
32
+ console.log(`Embedding model loaded: ${MODEL_ID} (${DIMENSIONS}d)`);
33
+ return pipe;
34
+ });
35
+
36
+ return initPromise;
37
+ }
38
+
39
+ /**
40
+ * Generate an embedding vector for a text string.
41
+ * Returns a normalized float32 array of length DIMENSIONS.
42
+ */
43
+ export async function embed(text: string): Promise<number[]> {
44
+ const embedder = await getEmbedder();
45
+ const result = await embedder(text, { pooling: POOLING, normalize: true });
46
+ // result is a Tensor — extract the data
47
+ return Array.from(result.data as Float32Array).slice(0, DIMENSIONS);
48
+ }
49
+
50
+ /**
51
+ * Generate embeddings for multiple texts in a batch.
52
+ * More efficient than calling embed() in a loop.
53
+ */
54
+ export async function embedBatch(texts: string[]): Promise<number[][]> {
55
+ if (texts.length === 0) return [];
56
+ const embedder = await getEmbedder();
57
+ const result = await embedder(texts, { pooling: POOLING, normalize: true });
58
+ const data = result.data as Float32Array;
59
+
60
+ const vectors: number[][] = [];
61
+ for (let i = 0; i < texts.length; i++) {
62
+ vectors.push(Array.from(data.slice(i * DIMENSIONS, (i + 1) * DIMENSIONS)));
63
+ }
64
+ return vectors;
65
+ }
66
+
67
+ /**
68
+ * Cosine similarity between two normalized vectors.
69
+ * Since vectors are pre-normalized, this is just the dot product.
70
+ */
71
+ export function cosineSimilarity(a: number[], b: number[]): number {
72
+ if (a.length !== b.length || a.length === 0) return 0;
73
+ let dot = 0;
74
+ for (let i = 0; i < a.length; i++) {
75
+ dot += a[i] * b[i];
76
+ }
77
+ // Clamp to [-1, 1] to handle floating point drift
78
+ return Math.max(-1, Math.min(1, dot));
79
+ }
80
+
81
+ /** Vector dimensions for this model */
82
+ export const EMBEDDING_DIMENSIONS = DIMENSIONS;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Hebbian Learning — "neurons that fire together wire together"
3
+ *
4
+ * When two engrams are co-activated (retrieved together in the same
5
+ * activation query), their association weight increases.
6
+ *
7
+ * Log-space weight update prevents runaway growth:
8
+ * logNew = log(w) + signal * log(1 + rate)
9
+ *
10
+ * Associations decay symmetrically when unused.
11
+ */
12
+
13
+ const MIN_WEIGHT = 0.001;
14
+ const MAX_WEIGHT = 5.0; // Cap at 5 to prevent graph walk explosion
15
+
16
+ /**
17
+ * Strengthen an association weight after co-activation.
18
+ */
19
+ export function strengthenAssociation(
20
+ currentWeight: number,
21
+ signal: number = 1.0,
22
+ rate: number = 0.25
23
+ ): number {
24
+ const logW = Math.log(Math.max(currentWeight, MIN_WEIGHT));
25
+ const logNew = logW + signal * Math.log(1 + rate);
26
+ return Math.min(Math.exp(logNew), MAX_WEIGHT);
27
+ }
28
+
29
+ /**
30
+ * Weaken an association weight due to lack of co-activation.
31
+ * Called periodically by the connection engine.
32
+ */
33
+ export function decayAssociation(
34
+ currentWeight: number,
35
+ daysSinceActivation: number,
36
+ halfLife: number = 7.0 // days
37
+ ): number {
38
+ const decayFactor = Math.pow(0.5, daysSinceActivation / halfLife);
39
+ return Math.max(currentWeight * decayFactor, MIN_WEIGHT);
40
+ }
41
+
42
+ /**
43
+ * Ring buffer for tracking recent co-activations.
44
+ * Feeds the Hebbian worker — when two engrams appear in the buffer
45
+ * within a window, their association is strengthened.
46
+ */
47
+ export class CoActivationBuffer {
48
+ private buffer: { engramId: string; timestamp: number }[] = [];
49
+ private maxSize: number;
50
+
51
+ constructor(maxSize: number = 50) {
52
+ this.maxSize = maxSize;
53
+ }
54
+
55
+ push(engramId: string): void {
56
+ this.buffer.push({ engramId, timestamp: Date.now() });
57
+ if (this.buffer.length > this.maxSize) {
58
+ this.buffer.shift();
59
+ }
60
+ }
61
+
62
+ pushBatch(engramIds: string[]): void {
63
+ for (const id of engramIds) {
64
+ this.push(id);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get all pairs of engrams that were co-activated within windowMs.
70
+ */
71
+ getCoActivatedPairs(windowMs: number = 5000): [string, string][] {
72
+ const pairs: [string, string][] = [];
73
+ for (let i = 0; i < this.buffer.length; i++) {
74
+ for (let j = i + 1; j < this.buffer.length; j++) {
75
+ const a = this.buffer[i];
76
+ const b = this.buffer[j];
77
+ if (
78
+ a.engramId !== b.engramId &&
79
+ Math.abs(a.timestamp - b.timestamp) <= windowMs
80
+ ) {
81
+ pairs.push([a.engramId, b.engramId]);
82
+ }
83
+ }
84
+ }
85
+ return pairs;
86
+ }
87
+
88
+ clear(): void {
89
+ this.buffer = [];
90
+ }
91
+ }
@@ -0,0 +1,3 @@
1
+ export * from './decay.js';
2
+ export * from './hebbian.js';
3
+ export * from './salience.js';
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Query Expander — rewrites queries with synonyms and related terms.
3
+ *
4
+ * Uses Xenova/flan-t5-small (~80MB ONNX) to expand search queries
5
+ * with related terms that improve BM25 recall.
6
+ *
7
+ * Example: "What is Caroline's identity?" →
8
+ * "What is Caroline's identity? Caroline personal gender transgender self"
9
+ *
10
+ * Singleton pattern — call getExpander() to get the shared instance.
11
+ */
12
+
13
+ import { pipeline, type Text2TextGenerationPipeline } from '@huggingface/transformers';
14
+
15
+ const MODEL_ID = 'Xenova/flan-t5-small';
16
+
17
+ let instance: Text2TextGenerationPipeline | null = null;
18
+ let initPromise: Promise<Text2TextGenerationPipeline> | null = null;
19
+
20
+ /**
21
+ * Get or initialize the text generation pipeline (singleton).
22
+ * First call downloads the model (~80MB), subsequent calls are instant.
23
+ */
24
+ export async function getExpander(): Promise<Text2TextGenerationPipeline> {
25
+ if (instance) return instance;
26
+ if (initPromise) return initPromise;
27
+
28
+ initPromise = pipeline('text2text-generation', MODEL_ID, {
29
+ dtype: 'fp32',
30
+ }).then(pipe => {
31
+ instance = pipe as Text2TextGenerationPipeline;
32
+ console.log(`Query expander loaded: ${MODEL_ID}`);
33
+ return instance;
34
+ });
35
+
36
+ return initPromise;
37
+ }
38
+
39
+ /**
40
+ * Expand a query with related terms and synonyms.
41
+ * Returns the original query + generated expansion terms.
42
+ * Falls back to the original query on any error.
43
+ */
44
+ export async function expandQuery(originalQuery: string): Promise<string> {
45
+ try {
46
+ const expander = await getExpander();
47
+ const prompt = `Expand this search query with synonyms and related terms. Only output the additional terms, not the original query. Query: ${originalQuery}. Additional terms:`;
48
+
49
+ const result = await expander(prompt, {
50
+ max_new_tokens: 25,
51
+ no_repeat_ngram_size: 2,
52
+ });
53
+
54
+ const expanded = Array.isArray(result) ? (result[0] as any)?.generated_text ?? '' : '';
55
+ const cleanExpanded = expanded.trim();
56
+
57
+ if (cleanExpanded && cleanExpanded.length > 2) {
58
+ return `${originalQuery} ${cleanExpanded}`;
59
+ }
60
+ return originalQuery;
61
+ } catch {
62
+ return originalQuery;
63
+ }
64
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Cross-Encoder Re-Ranker — scores (query, passage) pairs for relevance.
3
+ *
4
+ * Uses Xenova/ms-marco-MiniLM-L-6-v2 (~22MB ONNX) which is trained on
5
+ * MS-MARCO passage ranking. Unlike bi-encoders, cross-encoders see both
6
+ * query and passage together via full attention — much better at judging
7
+ * if a passage actually answers a question.
8
+ *
9
+ * Uses direct tokenizer + model inference (NOT the text-classification
10
+ * pipeline, which doesn't support text_pair and returns identical scores).
11
+ *
12
+ * Singleton pattern — call getReranker() to get the shared instance.
13
+ */
14
+
15
+ import {
16
+ AutoTokenizer,
17
+ AutoModelForSequenceClassification,
18
+ type PreTrainedTokenizer,
19
+ type PreTrainedModel,
20
+ } from '@huggingface/transformers';
21
+
22
+ const DEFAULT_MODEL = 'Xenova/ms-marco-MiniLM-L-6-v2';
23
+ const MODEL_ID = process.env.AWM_RERANKER_MODEL || DEFAULT_MODEL;
24
+
25
+ let tokenizer: PreTrainedTokenizer | null = null;
26
+ let model: PreTrainedModel | null = null;
27
+ let initPromise: Promise<void> | null = null;
28
+
29
+ async function ensureLoaded(): Promise<void> {
30
+ if (tokenizer && model) return;
31
+ if (initPromise) return initPromise;
32
+
33
+ initPromise = (async () => {
34
+ tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID);
35
+ model = await AutoModelForSequenceClassification.from_pretrained(MODEL_ID, {
36
+ dtype: 'fp32',
37
+ });
38
+ console.log(`Re-ranker model loaded: ${MODEL_ID}`);
39
+ })();
40
+
41
+ return initPromise;
42
+ }
43
+
44
+ /** Kept for backwards compat — returns the model (unused externally). */
45
+ export async function getReranker(): Promise<any> {
46
+ await ensureLoaded();
47
+ return model;
48
+ }
49
+
50
+ export interface RerankResult {
51
+ index: number;
52
+ score: number; // sigmoid-normalized relevance (0-1)
53
+ }
54
+
55
+ function sigmoid(x: number): number {
56
+ return 1 / (1 + Math.exp(-x));
57
+ }
58
+
59
+ /**
60
+ * Re-rank candidate passages against a query using the cross-encoder.
61
+ * Returns results sorted by relevance score (descending).
62
+ */
63
+ export async function rerank(
64
+ query: string,
65
+ passages: string[],
66
+ ): Promise<RerankResult[]> {
67
+ if (passages.length === 0) return [];
68
+
69
+ await ensureLoaded();
70
+
71
+ const results: RerankResult[] = [];
72
+
73
+ for (let i = 0; i < passages.length; i++) {
74
+ try {
75
+ // Tokenize as a query-passage PAIR using text_pair
76
+ const inputs = tokenizer!(query, {
77
+ text_pair: passages[i],
78
+ padding: true,
79
+ truncation: true,
80
+ return_tensors: 'pt',
81
+ });
82
+
83
+ const output = await model!(inputs);
84
+
85
+ // Model outputs raw logits — extract the single relevance logit
86
+ const logits = output.logits ?? output.last_hidden_state;
87
+ const rawLogit = logits.data[0] as number;
88
+
89
+ // Apply sigmoid to map to 0-1 probability
90
+ results.push({ index: i, score: sigmoid(rawLogit) });
91
+ } catch {
92
+ results.push({ index: i, score: 0 });
93
+ }
94
+ }
95
+
96
+ // Sort by score descending
97
+ results.sort((a, b) => b.score - a.score);
98
+ return results;
99
+ }