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.
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +2 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/routes.d.ts +53 -0
- package/dist/api/routes.d.ts.map +1 -0
- package/dist/api/routes.js +388 -0
- package/dist/api/routes.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +245 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/decay.d.ts +36 -0
- package/dist/core/decay.d.ts.map +1 -0
- package/dist/core/decay.js +38 -0
- package/dist/core/decay.js.map +1 -0
- package/dist/core/embeddings.d.ts +33 -0
- package/dist/core/embeddings.d.ts.map +1 -0
- package/dist/core/embeddings.js +76 -0
- package/dist/core/embeddings.js.map +1 -0
- package/dist/core/hebbian.d.ts +38 -0
- package/dist/core/hebbian.d.ts.map +1 -0
- package/dist/core/hebbian.js +74 -0
- package/dist/core/hebbian.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/query-expander.d.ts +24 -0
- package/dist/core/query-expander.d.ts.map +1 -0
- package/dist/core/query-expander.js +58 -0
- package/dist/core/query-expander.js.map +1 -0
- package/dist/core/reranker.d.ts +25 -0
- package/dist/core/reranker.d.ts.map +1 -0
- package/dist/core/reranker.js +75 -0
- package/dist/core/reranker.js.map +1 -0
- package/dist/core/salience.d.ts +30 -0
- package/dist/core/salience.d.ts.map +1 -0
- package/dist/core/salience.js +81 -0
- package/dist/core/salience.js.map +1 -0
- package/dist/engine/activation.d.ts +38 -0
- package/dist/engine/activation.d.ts.map +1 -0
- package/dist/engine/activation.js +516 -0
- package/dist/engine/activation.js.map +1 -0
- package/dist/engine/connections.d.ts +31 -0
- package/dist/engine/connections.d.ts.map +1 -0
- package/dist/engine/connections.js +74 -0
- package/dist/engine/connections.js.map +1 -0
- package/dist/engine/consolidation-scheduler.d.ts +31 -0
- package/dist/engine/consolidation-scheduler.d.ts.map +1 -0
- package/dist/engine/consolidation-scheduler.js +115 -0
- package/dist/engine/consolidation-scheduler.js.map +1 -0
- package/dist/engine/consolidation.d.ts +62 -0
- package/dist/engine/consolidation.d.ts.map +1 -0
- package/dist/engine/consolidation.js +368 -0
- package/dist/engine/consolidation.js.map +1 -0
- package/dist/engine/eval.d.ts +22 -0
- package/dist/engine/eval.d.ts.map +1 -0
- package/dist/engine/eval.js +79 -0
- package/dist/engine/eval.js.map +1 -0
- package/dist/engine/eviction.d.ts +29 -0
- package/dist/engine/eviction.d.ts.map +1 -0
- package/dist/engine/eviction.js +86 -0
- package/dist/engine/eviction.js.map +1 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/retraction.d.ts +32 -0
- package/dist/engine/retraction.d.ts.map +1 -0
- package/dist/engine/retraction.js +77 -0
- package/dist/engine/retraction.js.map +1 -0
- package/dist/engine/staging.d.ts +33 -0
- package/dist/engine/staging.d.ts.map +1 -0
- package/dist/engine/staging.js +63 -0
- package/dist/engine/staging.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +24 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +532 -0
- package/dist/mcp.js.map +1 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/sqlite.d.ts +116 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +750 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/types/agent.d.ts +30 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/agent.js +23 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/checkpoint.d.ts +50 -0
- package/dist/types/checkpoint.d.ts.map +1 -0
- package/dist/types/checkpoint.js +8 -0
- package/dist/types/checkpoint.js.map +1 -0
- package/dist/types/engram.d.ts +165 -0
- package/dist/types/engram.d.ts.map +1 -0
- package/dist/types/engram.js +8 -0
- package/dist/types/engram.js.map +1 -0
- package/dist/types/eval.d.ts +84 -0
- package/dist/types/eval.d.ts.map +1 -0
- package/dist/types/eval.js +11 -0
- package/dist/types/eval.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +55 -0
- package/src/api/index.ts +1 -0
- package/src/api/routes.ts +528 -0
- package/src/cli.ts +260 -0
- package/src/core/decay.ts +61 -0
- package/src/core/embeddings.ts +82 -0
- package/src/core/hebbian.ts +91 -0
- package/src/core/index.ts +3 -0
- package/src/core/query-expander.ts +64 -0
- package/src/core/reranker.ts +99 -0
- package/src/core/salience.ts +95 -0
- package/src/engine/activation.ts +577 -0
- package/src/engine/connections.ts +101 -0
- package/src/engine/consolidation-scheduler.ts +123 -0
- package/src/engine/consolidation.ts +443 -0
- package/src/engine/eval.ts +100 -0
- package/src/engine/eviction.ts +99 -0
- package/src/engine/index.ts +6 -0
- package/src/engine/retraction.ts +98 -0
- package/src/engine/staging.ts +72 -0
- package/src/index.ts +100 -0
- package/src/mcp.ts +635 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/sqlite.ts +893 -0
- package/src/types/agent.ts +65 -0
- package/src/types/checkpoint.ts +44 -0
- package/src/types/engram.ts +194 -0
- package/src/types/eval.ts +98 -0
- 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,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
|
+
}
|