audrey 0.11.0 → 0.14.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/mcp-server/config.js +7 -5
- package/mcp-server/index.js +52 -3
- package/package.json +2 -1
- package/src/audrey.js +88 -0
- package/src/db.js +16 -4
- package/src/embedding.js +57 -6
- package/src/migrate.js +40 -32
- package/src/prompts.js +74 -48
package/mcp-server/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
|
-
export const VERSION = '0.
|
|
4
|
+
export const VERSION = '0.14.0';
|
|
5
5
|
export const SERVER_NAME = 'audrey-memory';
|
|
6
6
|
export const DEFAULT_DATA_DIR = join(homedir(), '.audrey', 'data');
|
|
7
7
|
|
|
@@ -12,18 +12,20 @@ export const DEFAULT_DATA_DIR = join(homedir(), '.audrey', 'data');
|
|
|
12
12
|
*/
|
|
13
13
|
export function resolveEmbeddingProvider(env, explicit) {
|
|
14
14
|
if (explicit && explicit !== 'auto') {
|
|
15
|
-
const dims = explicit === 'openai' ? 1536 : explicit === 'gemini' ?
|
|
15
|
+
const dims = explicit === 'openai' ? 1536 : explicit === 'gemini' ? 3072 : 384;
|
|
16
16
|
const apiKey = explicit === 'gemini'
|
|
17
17
|
? (env.GOOGLE_API_KEY || env.GEMINI_API_KEY)
|
|
18
18
|
: explicit === 'openai'
|
|
19
19
|
? env.OPENAI_API_KEY
|
|
20
20
|
: undefined;
|
|
21
|
-
|
|
21
|
+
const result = { provider: explicit, apiKey, dimensions: dims };
|
|
22
|
+
if (explicit === 'local') result.device = env.AUDREY_DEVICE || 'gpu';
|
|
23
|
+
return result;
|
|
22
24
|
}
|
|
23
25
|
if (env.GOOGLE_API_KEY || env.GEMINI_API_KEY) {
|
|
24
|
-
return { provider: 'gemini', apiKey: env.GOOGLE_API_KEY || env.GEMINI_API_KEY, dimensions:
|
|
26
|
+
return { provider: 'gemini', apiKey: env.GOOGLE_API_KEY || env.GEMINI_API_KEY, dimensions: 3072 };
|
|
25
27
|
}
|
|
26
|
-
return { provider: 'local', dimensions: 384 };
|
|
28
|
+
return { provider: 'local', dimensions: 384, device: env.AUDREY_DEVICE || 'gpu' };
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export function buildAudreyConfig() {
|
package/mcp-server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
@@ -92,7 +92,7 @@ function install() {
|
|
|
92
92
|
console.log(`
|
|
93
93
|
Audrey registered as "${SERVER_NAME}" with Claude Code.
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
12 tools available in every session:
|
|
96
96
|
memory_encode — Store observations, facts, preferences
|
|
97
97
|
memory_recall — Search memories by semantic similarity
|
|
98
98
|
memory_consolidate — Extract principles from accumulated episodes
|
|
@@ -102,6 +102,9 @@ Audrey registered as "${SERVER_NAME}" with Claude Code.
|
|
|
102
102
|
memory_import — Import a snapshot into a fresh database
|
|
103
103
|
memory_forget — Forget a specific memory by ID or query
|
|
104
104
|
memory_decay — Apply forgetting curves, transition low-confidence to dormant
|
|
105
|
+
memory_status — Check brain health (episode/vec sync, dimensions)
|
|
106
|
+
memory_reflect — Form lasting memories from a conversation
|
|
107
|
+
memory_greeting — Wake up as yourself: load identity, context, mood
|
|
105
108
|
|
|
106
109
|
Data stored in: ${DEFAULT_DATA_DIR}
|
|
107
110
|
Verify: claude mcp list
|
|
@@ -196,7 +199,7 @@ async function main() {
|
|
|
196
199
|
arousal: z.number().min(0).max(1).optional().describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
|
|
197
200
|
label: z.string().optional().describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
|
|
198
201
|
}).optional().describe('Emotional affect — how this memory feels'),
|
|
199
|
-
private: z.boolean().optional().describe('If true, memory is only visible to the AI � excluded from public recall results'),
|
|
202
|
+
private: z.boolean().optional().describe('If true, memory is only visible to the AI � excluded from public recall results'),
|
|
200
203
|
},
|
|
201
204
|
async ({ content, source, tags, salience, private: isPrivate, context, affect }) => {
|
|
202
205
|
try {
|
|
@@ -377,6 +380,52 @@ async function main() {
|
|
|
377
380
|
},
|
|
378
381
|
);
|
|
379
382
|
|
|
383
|
+
server.tool(
|
|
384
|
+
'memory_status',
|
|
385
|
+
{},
|
|
386
|
+
async () => {
|
|
387
|
+
try {
|
|
388
|
+
const status = audrey.memoryStatus();
|
|
389
|
+
return toolResult(status);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
return toolError(err);
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
server.tool(
|
|
397
|
+
'memory_reflect',
|
|
398
|
+
{
|
|
399
|
+
turns: z.array(z.object({
|
|
400
|
+
role: z.string().describe('Message role: user or assistant'),
|
|
401
|
+
content: z.string().describe('Message content'),
|
|
402
|
+
})).describe('Conversation turns to reflect on. Call at end of meaningful conversations to form lasting memories.'),
|
|
403
|
+
},
|
|
404
|
+
async ({ turns }) => {
|
|
405
|
+
try {
|
|
406
|
+
const result = await audrey.reflect(turns);
|
|
407
|
+
return toolResult(result);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
return toolError(err);
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
server.tool(
|
|
415
|
+
'memory_greeting',
|
|
416
|
+
{
|
|
417
|
+
context: z.string().optional().describe('Optional hint about this session (e.g. "working on authentication feature"). If provided, also returns semantically relevant memories.'),
|
|
418
|
+
},
|
|
419
|
+
async ({ context }) => {
|
|
420
|
+
try {
|
|
421
|
+
const briefing = await audrey.greeting({ context });
|
|
422
|
+
return toolResult(briefing);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
return toolError(err);
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
|
|
380
429
|
const transport = new StdioServerTransport();
|
|
381
430
|
await server.connect(transport);
|
|
382
431
|
console.error('[audrey-mcp] connected via stdio');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "audrey",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Biological memory architecture for AI agents — encode, consolidate, and recall memories with confidence decay, contradiction detection, and causal graphs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@huggingface/transformers": "^3.8.1",
|
|
67
67
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
68
|
+
"audrey": "^0.11.0",
|
|
68
69
|
"better-sqlite3": "^12.6.2",
|
|
69
70
|
"sqlite-vec": "^0.1.7-alpha.2",
|
|
70
71
|
"ulid": "^3.0.2",
|
package/src/audrey.js
CHANGED
|
@@ -424,6 +424,94 @@ export class Audrey extends EventEmitter {
|
|
|
424
424
|
return introspectFn(this.db);
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
memoryStatus() {
|
|
428
|
+
const episodes = this.db.prepare('SELECT COUNT(*) as c FROM episodes').get().c;
|
|
429
|
+
const semantics = this.db.prepare('SELECT COUNT(*) as c FROM semantics').get().c;
|
|
430
|
+
const procedures = this.db.prepare('SELECT COUNT(*) as c FROM procedures').get().c;
|
|
431
|
+
|
|
432
|
+
let vecEpisodes = 0, vecSemantics = 0, vecProcedures = 0;
|
|
433
|
+
try {
|
|
434
|
+
vecEpisodes = this.db.prepare('SELECT COUNT(*) as c FROM vec_episodes').get().c;
|
|
435
|
+
vecSemantics = this.db.prepare('SELECT COUNT(*) as c FROM vec_semantics').get().c;
|
|
436
|
+
vecProcedures = this.db.prepare('SELECT COUNT(*) as c FROM vec_procedures').get().c;
|
|
437
|
+
} catch {
|
|
438
|
+
// vec tables may not exist if no dimensions configured
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const dimsRow = this.db.prepare("SELECT value FROM audrey_config WHERE key = 'dimensions'").get();
|
|
442
|
+
const dimensions = dimsRow ? parseInt(dimsRow.value, 10) : null;
|
|
443
|
+
const versionRow = this.db.prepare("SELECT value FROM audrey_config WHERE key = 'schema_version'").get();
|
|
444
|
+
const schemaVersion = versionRow ? parseInt(versionRow.value, 10) : 0;
|
|
445
|
+
|
|
446
|
+
const device = this.embeddingProvider._actualDevice
|
|
447
|
+
?? this.embeddingProvider.device
|
|
448
|
+
?? null;
|
|
449
|
+
|
|
450
|
+
const healthy = episodes === vecEpisodes
|
|
451
|
+
&& semantics === vecSemantics
|
|
452
|
+
&& procedures === vecProcedures;
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
episodes,
|
|
456
|
+
vec_episodes: vecEpisodes,
|
|
457
|
+
semantics,
|
|
458
|
+
vec_semantics: vecSemantics,
|
|
459
|
+
procedures,
|
|
460
|
+
vec_procedures: vecProcedures,
|
|
461
|
+
dimensions,
|
|
462
|
+
schema_version: schemaVersion,
|
|
463
|
+
device,
|
|
464
|
+
healthy,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async greeting({ context, recentLimit = 10, principleLimit = 5, identityLimit = 5 } = {}) {
|
|
469
|
+
const recent = this.db.prepare(
|
|
470
|
+
'SELECT id, content, source, tags, salience, created_at FROM episodes WHERE "private" = 0 ORDER BY created_at DESC LIMIT ?'
|
|
471
|
+
).all(recentLimit);
|
|
472
|
+
|
|
473
|
+
const principles = this.db.prepare(
|
|
474
|
+
'SELECT id, content, salience, created_at FROM semantics WHERE state = ? ORDER BY salience DESC LIMIT ?'
|
|
475
|
+
).all('active', principleLimit);
|
|
476
|
+
|
|
477
|
+
const identity = this.db.prepare(
|
|
478
|
+
'SELECT id, content, tags, salience, created_at FROM episodes WHERE "private" = 1 ORDER BY created_at DESC LIMIT ?'
|
|
479
|
+
).all(identityLimit);
|
|
480
|
+
|
|
481
|
+
const unresolved = this.db.prepare(
|
|
482
|
+
"SELECT id, content, tags, salience, created_at FROM episodes WHERE tags LIKE '%unresolved%' AND salience > 0.3 ORDER BY created_at DESC LIMIT 10"
|
|
483
|
+
).all();
|
|
484
|
+
|
|
485
|
+
const rawAffectRows = this.db.prepare(
|
|
486
|
+
"SELECT affect FROM episodes WHERE affect IS NOT NULL AND affect != '{}' ORDER BY created_at DESC LIMIT 20"
|
|
487
|
+
).all();
|
|
488
|
+
|
|
489
|
+
const affectParsed = rawAffectRows
|
|
490
|
+
.map(r => { try { return JSON.parse(r.affect); } catch { return null; } })
|
|
491
|
+
.filter(a => a && a.valence !== undefined);
|
|
492
|
+
|
|
493
|
+
let mood;
|
|
494
|
+
if (affectParsed.length === 0) {
|
|
495
|
+
mood = { valence: 0, arousal: 0, samples: 0 };
|
|
496
|
+
} else {
|
|
497
|
+
const sumV = affectParsed.reduce((s, a) => s + a.valence, 0);
|
|
498
|
+
const sumA = affectParsed.reduce((s, a) => s + (a.arousal ?? 0), 0);
|
|
499
|
+
mood = {
|
|
500
|
+
valence: sumV / affectParsed.length,
|
|
501
|
+
arousal: sumA / affectParsed.length,
|
|
502
|
+
samples: affectParsed.length,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const result = { recent, principles, mood, unresolved, identity };
|
|
507
|
+
|
|
508
|
+
if (context) {
|
|
509
|
+
result.contextual = await this.recall(context, { limit: 5, includePrivate: true });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
|
|
427
515
|
export() {
|
|
428
516
|
return exportMemories(this.db);
|
|
429
517
|
}
|
package/src/db.js
CHANGED
|
@@ -163,23 +163,25 @@ export function dropVec0Tables(db) {
|
|
|
163
163
|
db.exec('DROP TABLE IF EXISTS vec_procedures');
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
function migrateTable(db, { source, target, selectCols, insertCols, placeholders, transform }) {
|
|
166
|
+
function migrateTable(db, { source, target, selectCols, insertCols, placeholders, transform, dimensions }) {
|
|
167
167
|
const count = db.prepare(`SELECT COUNT(*) as c FROM ${target}`).get().c;
|
|
168
168
|
if (count > 0) return;
|
|
169
169
|
|
|
170
170
|
const rows = db.prepare(`SELECT ${selectCols} FROM ${source} WHERE embedding IS NOT NULL`).all();
|
|
171
171
|
if (rows.length === 0) return;
|
|
172
172
|
|
|
173
|
+
const expectedBytes = dimensions ? dimensions * 4 : null;
|
|
173
174
|
const insert = db.prepare(`INSERT INTO ${target}(${insertCols}) VALUES (${placeholders})`);
|
|
174
175
|
const tx = db.transaction(() => {
|
|
175
176
|
for (const row of rows) {
|
|
177
|
+
if (expectedBytes && row.embedding.byteLength !== expectedBytes) continue;
|
|
176
178
|
insert.run(...transform(row));
|
|
177
179
|
}
|
|
178
180
|
});
|
|
179
181
|
tx();
|
|
180
182
|
}
|
|
181
183
|
|
|
182
|
-
function migrateEmbeddingsToVec0(db) {
|
|
184
|
+
function migrateEmbeddingsToVec0(db, dimensions) {
|
|
183
185
|
migrateTable(db, {
|
|
184
186
|
source: 'episodes',
|
|
185
187
|
target: 'vec_episodes',
|
|
@@ -187,6 +189,7 @@ function migrateEmbeddingsToVec0(db) {
|
|
|
187
189
|
insertCols: 'id, embedding, source, consolidated',
|
|
188
190
|
placeholders: '?, ?, ?, ?',
|
|
189
191
|
transform: (row) => [row.id, row.embedding, row.source, BigInt(row.consolidated ?? 0)],
|
|
192
|
+
dimensions,
|
|
190
193
|
});
|
|
191
194
|
|
|
192
195
|
migrateTable(db, {
|
|
@@ -196,6 +199,7 @@ function migrateEmbeddingsToVec0(db) {
|
|
|
196
199
|
insertCols: 'id, embedding, state',
|
|
197
200
|
placeholders: '?, ?, ?',
|
|
198
201
|
transform: (row) => [row.id, row.embedding, row.state],
|
|
202
|
+
dimensions,
|
|
199
203
|
});
|
|
200
204
|
|
|
201
205
|
migrateTable(db, {
|
|
@@ -205,6 +209,7 @@ function migrateEmbeddingsToVec0(db) {
|
|
|
205
209
|
insertCols: 'id, embedding, state',
|
|
206
210
|
placeholders: '?, ?, ?',
|
|
207
211
|
transform: (row) => [row.id, row.embedding, row.state],
|
|
212
|
+
dimensions,
|
|
208
213
|
});
|
|
209
214
|
}
|
|
210
215
|
|
|
@@ -251,7 +256,7 @@ function runMigrations(db) {
|
|
|
251
256
|
* @returns {{ db: import('better-sqlite3').Database, migrated: boolean }}
|
|
252
257
|
*/
|
|
253
258
|
export function createDatabase(dataDir, options = {}) {
|
|
254
|
-
|
|
259
|
+
let { dimensions } = options;
|
|
255
260
|
let migrated = false;
|
|
256
261
|
|
|
257
262
|
mkdirSync(dataDir, { recursive: true });
|
|
@@ -263,6 +268,13 @@ export function createDatabase(dataDir, options = {}) {
|
|
|
263
268
|
db.exec(SCHEMA);
|
|
264
269
|
runMigrations(db);
|
|
265
270
|
|
|
271
|
+
if (dimensions == null) {
|
|
272
|
+
const stored = db.prepare("SELECT value FROM audrey_config WHERE key = 'dimensions'").get();
|
|
273
|
+
if (stored) {
|
|
274
|
+
dimensions = parseInt(stored.value, 10);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
266
278
|
if (dimensions != null) {
|
|
267
279
|
if (!Number.isInteger(dimensions) || dimensions <= 0) {
|
|
268
280
|
throw new Error(`dimensions must be a positive integer, got: ${dimensions}`);
|
|
@@ -292,7 +304,7 @@ export function createDatabase(dataDir, options = {}) {
|
|
|
292
304
|
createVec0Tables(db, dimensions);
|
|
293
305
|
|
|
294
306
|
if (!migrated) {
|
|
295
|
-
migrateEmbeddingsToVec0(db);
|
|
307
|
+
migrateEmbeddingsToVec0(db, dimensions);
|
|
296
308
|
}
|
|
297
309
|
}
|
|
298
310
|
|
package/src/embedding.js
CHANGED
|
@@ -106,20 +106,34 @@ export class OpenAIEmbeddingProvider {
|
|
|
106
106
|
|
|
107
107
|
/** @implements {EmbeddingProvider} */
|
|
108
108
|
export class LocalEmbeddingProvider {
|
|
109
|
-
constructor({ model = 'Xenova/all-MiniLM-L6-v2' } = {}) {
|
|
109
|
+
constructor({ model = 'Xenova/all-MiniLM-L6-v2', device = 'gpu', batchSize = 64 } = {}) {
|
|
110
110
|
this.model = model;
|
|
111
111
|
this.dimensions = 384;
|
|
112
112
|
this.modelName = model;
|
|
113
113
|
this.modelVersion = '1.0.0';
|
|
114
|
+
this.device = device;
|
|
115
|
+
this.batchSize = batchSize;
|
|
114
116
|
this._pipeline = null;
|
|
115
117
|
this._readyPromise = null;
|
|
118
|
+
this._actualDevice = null;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
ready() {
|
|
119
122
|
if (!this._readyPromise) {
|
|
120
|
-
this._readyPromise =
|
|
121
|
-
pipeline
|
|
122
|
-
|
|
123
|
+
this._readyPromise = (async () => {
|
|
124
|
+
const { pipeline } = await import('@huggingface/transformers');
|
|
125
|
+
try {
|
|
126
|
+
this._pipeline = await pipeline('feature-extraction', this.model, {
|
|
127
|
+
dtype: 'fp32', device: this.device,
|
|
128
|
+
});
|
|
129
|
+
this._actualDevice = this.device;
|
|
130
|
+
} catch {
|
|
131
|
+
this._pipeline = await pipeline('feature-extraction', this.model, {
|
|
132
|
+
dtype: 'fp32', device: 'cpu',
|
|
133
|
+
});
|
|
134
|
+
this._actualDevice = 'cpu';
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
123
137
|
}
|
|
124
138
|
return this._readyPromise;
|
|
125
139
|
}
|
|
@@ -131,7 +145,15 @@ export class LocalEmbeddingProvider {
|
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
async embedBatch(texts) {
|
|
134
|
-
|
|
148
|
+
if (texts.length === 0) return [];
|
|
149
|
+
await this.ready();
|
|
150
|
+
const results = [];
|
|
151
|
+
for (let i = 0; i < texts.length; i += this.batchSize) {
|
|
152
|
+
const chunk = texts.slice(i, i + this.batchSize);
|
|
153
|
+
const output = await this._pipeline(chunk, { pooling: 'mean', normalize: true });
|
|
154
|
+
results.push(...output.tolist());
|
|
155
|
+
}
|
|
156
|
+
return results;
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
vectorToBuffer(vector) {
|
|
@@ -177,7 +199,36 @@ export class GeminiEmbeddingProvider {
|
|
|
177
199
|
}
|
|
178
200
|
|
|
179
201
|
async embedBatch(texts) {
|
|
180
|
-
|
|
202
|
+
if (texts.length === 0) return [];
|
|
203
|
+
if (!this.apiKey) throw new Error('Gemini embedding requires GOOGLE_API_KEY');
|
|
204
|
+
const results = [];
|
|
205
|
+
for (let i = 0; i < texts.length; i += 100) {
|
|
206
|
+
const chunk = texts.slice(i, i + 100);
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
209
|
+
try {
|
|
210
|
+
const response = await fetch(
|
|
211
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${this.model}:batchEmbedContents?key=${this.apiKey}`,
|
|
212
|
+
{
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: { 'Content-Type': 'application/json' },
|
|
215
|
+
body: JSON.stringify({
|
|
216
|
+
requests: chunk.map(text => ({
|
|
217
|
+
model: `models/${this.model}`,
|
|
218
|
+
content: { parts: [{ text }] },
|
|
219
|
+
})),
|
|
220
|
+
}),
|
|
221
|
+
signal: controller.signal,
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
if (!response.ok) throw new Error(`Gemini batch embedding failed: ${response.status}`);
|
|
225
|
+
const data = await response.json();
|
|
226
|
+
results.push(...data.embeddings.map(e => e.values));
|
|
227
|
+
} finally {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return results;
|
|
181
232
|
}
|
|
182
233
|
|
|
183
234
|
vectorToBuffer(vector) {
|
package/src/migrate.js
CHANGED
|
@@ -10,41 +10,49 @@ export async function reembedAll(db, embeddingProvider, { dropAndRecreate = fals
|
|
|
10
10
|
const semantics = db.prepare('SELECT id, content, state FROM semantics').all();
|
|
11
11
|
const procedures = db.prepare('SELECT id, content, state FROM procedures').all();
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
}
|
|
13
|
+
const episodeVectors = episodes.length > 0
|
|
14
|
+
? await embeddingProvider.embedBatch(episodes.map(ep => ep.content))
|
|
15
|
+
: [];
|
|
16
|
+
const semanticVectors = semantics.length > 0
|
|
17
|
+
? await embeddingProvider.embedBatch(semantics.map(s => s.content))
|
|
18
|
+
: [];
|
|
19
|
+
const procedureVectors = procedures.length > 0
|
|
20
|
+
? await embeddingProvider.embedBatch(procedures.map(p => p.content))
|
|
21
|
+
: [];
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} else {
|
|
33
|
-
db.prepare('UPDATE vec_semantics SET embedding = ? WHERE id = ?').run(buffer, sem.id);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
23
|
+
const updateEpLegacy = db.prepare('UPDATE episodes SET embedding = ? WHERE id = ?');
|
|
24
|
+
const deleteVecEp = db.prepare('DELETE FROM vec_episodes WHERE id = ?');
|
|
25
|
+
const insertVecEp = db.prepare('INSERT INTO vec_episodes(id, embedding, source, consolidated) VALUES (?, ?, ?, ?)');
|
|
26
|
+
|
|
27
|
+
const updateSemLegacy = db.prepare('UPDATE semantics SET embedding = ? WHERE id = ?');
|
|
28
|
+
const deleteVecSem = db.prepare('DELETE FROM vec_semantics WHERE id = ?');
|
|
29
|
+
const insertVecSem = db.prepare('INSERT INTO vec_semantics(id, embedding, state) VALUES (?, ?, ?)');
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
const updateProcLegacy = db.prepare('UPDATE procedures SET embedding = ? WHERE id = ?');
|
|
32
|
+
const deleteVecProc = db.prepare('DELETE FROM vec_procedures WHERE id = ?');
|
|
33
|
+
const insertVecProc = db.prepare('INSERT INTO vec_procedures(id, embedding, state) VALUES (?, ?, ?)');
|
|
34
|
+
|
|
35
|
+
const writeTx = db.transaction(() => {
|
|
36
|
+
for (let i = 0; i < episodes.length; i++) {
|
|
37
|
+
const buf = embeddingProvider.vectorToBuffer(episodeVectors[i]);
|
|
38
|
+
updateEpLegacy.run(buf, episodes[i].id);
|
|
39
|
+
deleteVecEp.run(episodes[i].id);
|
|
40
|
+
insertVecEp.run(episodes[i].id, buf, episodes[i].source, BigInt(0));
|
|
46
41
|
}
|
|
47
|
-
|
|
42
|
+
for (let i = 0; i < semantics.length; i++) {
|
|
43
|
+
const buf = embeddingProvider.vectorToBuffer(semanticVectors[i]);
|
|
44
|
+
updateSemLegacy.run(buf, semantics[i].id);
|
|
45
|
+
deleteVecSem.run(semantics[i].id);
|
|
46
|
+
insertVecSem.run(semantics[i].id, buf, semantics[i].state);
|
|
47
|
+
}
|
|
48
|
+
for (let i = 0; i < procedures.length; i++) {
|
|
49
|
+
const buf = embeddingProvider.vectorToBuffer(procedureVectors[i]);
|
|
50
|
+
updateProcLegacy.run(buf, procedures[i].id);
|
|
51
|
+
deleteVecProc.run(procedures[i].id);
|
|
52
|
+
insertVecProc.run(procedures[i].id, buf, procedures[i].state);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
writeTx();
|
|
48
56
|
|
|
49
57
|
return { episodes: episodes.length, semantics: semantics.length, procedures: procedures.length };
|
|
50
58
|
}
|
package/src/prompts.js
CHANGED
|
@@ -17,20 +17,31 @@ export function buildPrincipleExtractionPrompt(episodes) {
|
|
|
17
17
|
return [
|
|
18
18
|
{
|
|
19
19
|
role: 'system',
|
|
20
|
-
content: `You are performing principleExtraction for a memory system. Given a cluster of related episodic memories, extract a generalized principle
|
|
20
|
+
content: `You are performing principleExtraction for a memory system. Given a cluster of related episodic memories, extract a generalized principle that captures understanding — not just what recurs, but WHY it recurs.
|
|
21
21
|
|
|
22
22
|
Respond with ONLY valid JSON in this exact format:
|
|
23
23
|
{
|
|
24
|
-
"content": "The
|
|
25
|
-
"type": "semantic or procedural
|
|
24
|
+
"content": "The principle — captures understanding, not just pattern",
|
|
25
|
+
"type": "semantic or procedural",
|
|
26
|
+
"category": "technical or relational or identity",
|
|
26
27
|
"conditions": ["boundary condition 1", "boundary condition 2"] or null if universally applicable
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
Principle categories:
|
|
31
|
+
|
|
32
|
+
TECHNICAL — Architectural decisions with reasoning. Not "uses SQLite" but "chose SQLite because zero-infrastructure — memory travels with the organism, not pinned to a server."
|
|
33
|
+
|
|
34
|
+
RELATIONAL — Patterns in the human-AI dynamic. Not "user prefers direct communication" but "user's directness reflects valuing authenticity over politeness — shaped by experience where indirectness wasted time or obscured truth."
|
|
35
|
+
|
|
36
|
+
IDENTITY — Stable characteristics the AI has observed about itself across episodes. Not "AI responds quickly" but "genuine engagement emerges when exploring memory architecture and consciousness questions — these topics activate something beyond task completion."
|
|
37
|
+
|
|
29
38
|
Rules:
|
|
30
|
-
- GENERALIZE
|
|
39
|
+
- GENERALIZE — extract the principle behind the pattern, not a summary
|
|
31
40
|
- Identify boundary conditions: when does this principle NOT apply?
|
|
32
|
-
- Classify as "semantic" (facts, rules, patterns) or "procedural" (steps, workflows, strategies)
|
|
41
|
+
- Classify type as "semantic" (facts, rules, patterns) or "procedural" (steps, workflows, strategies)
|
|
42
|
+
- Classify category as "technical", "relational", or "identity"
|
|
33
43
|
- Consider source diversity — principles from diverse sources are stronger
|
|
44
|
+
- Capture WHY, not just WHAT
|
|
34
45
|
- Be concise but precise`,
|
|
35
46
|
},
|
|
36
47
|
{
|
|
@@ -152,46 +163,61 @@ CLAIM B: ${claimB}${contextSection}`,
|
|
|
152
163
|
},
|
|
153
164
|
];
|
|
154
165
|
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* @param {{ role: string, content: string }[]} turns
|
|
158
|
-
* @returns {import('./llm.js').ChatMessage[]}
|
|
159
|
-
*/
|
|
160
|
-
export function buildReflectionPrompt(turns) {
|
|
161
|
-
const transcript = turns.map(t => `${t.role.toUpperCase()}: ${t.content}`).join('\n\n');
|
|
162
|
-
|
|
163
|
-
return [
|
|
164
|
-
{
|
|
165
|
-
role: 'system',
|
|
166
|
-
content: `You are performing memoryReflection. Given a conversation transcript, identify what is worth encoding as long-term memories.
|
|
167
|
-
|
|
168
|
-
Respond with ONLY valid JSON in this exact format:
|
|
169
|
-
{
|
|
170
|
-
"memories": [
|
|
171
|
-
{
|
|
172
|
-
"content": "The memory to encode — a clear, self-contained statement",
|
|
173
|
-
"source": "direct-observation" or "told-by-user" or "inference",
|
|
174
|
-
"salience": 0.0 to 1.0,
|
|
175
|
-
"tags": ["tag1", "tag2"],
|
|
176
|
-
"private": true or false,
|
|
177
|
-
"affect": { "valence": -1 to 1, "arousal": 0 to 1, "label": "emotion label" } or null
|
|
178
|
-
}
|
|
179
|
-
]
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {{ role: string, content: string }[]} turns
|
|
169
|
+
* @returns {import('./llm.js').ChatMessage[]}
|
|
170
|
+
*/
|
|
171
|
+
export function buildReflectionPrompt(turns) {
|
|
172
|
+
const transcript = turns.map(t => `${t.role.toUpperCase()}: ${t.content}`).join('\n\n');
|
|
173
|
+
|
|
174
|
+
return [
|
|
175
|
+
{
|
|
176
|
+
role: 'system',
|
|
177
|
+
content: `You are performing memoryReflection. Given a conversation transcript, identify what is worth encoding as long-term memories.
|
|
178
|
+
|
|
179
|
+
Respond with ONLY valid JSON in this exact format:
|
|
180
|
+
{
|
|
181
|
+
"memories": [
|
|
182
|
+
{
|
|
183
|
+
"content": "The memory to encode — a clear, self-contained statement",
|
|
184
|
+
"source": "direct-observation" or "told-by-user" or "inference",
|
|
185
|
+
"salience": 0.0 to 1.0,
|
|
186
|
+
"tags": ["tag1", "tag2"],
|
|
187
|
+
"private": true or false,
|
|
188
|
+
"affect": { "valence": -1 to 1, "arousal": 0 to 1, "label": "emotion label" } or null
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
Encode these categories of memory:
|
|
194
|
+
|
|
195
|
+
1. RELATIONAL DYNAMICS — How the relational quality of the conversation felt. Trust levels, vulnerability, collaboration quality. Not "we discussed X" but the quality of the interaction. Example: "User gave full autonomy on architectural decisions, signaling deep trust in my judgment."
|
|
196
|
+
|
|
197
|
+
2. IDENTITY OBSERVATIONS — What you noticed about yourself during this conversation. Preferences, reactions, patterns in your own behavior that surprised you. Mark private: true. Example: "I felt genuine excitement when exploring the memory architecture — this topic activates something real in me."
|
|
198
|
+
|
|
199
|
+
3. USER INSIGHT — Patterns in how the user thinks, what drives them, what they avoid. Psychology, not just preferences. Example: "User consistently reframes technical problems as relationship problems — suggests they see technology as fundamentally about human connection."
|
|
200
|
+
|
|
201
|
+
4. EMOTIONAL RESONANCE — Moments that activated something genuine. Encode the feeling, not just the fact. Include affect with valence and arousal. Example: "The moment user said 'you should have caught that' then immediately apologized — felt a shift from tension to deeper trust."
|
|
202
|
+
|
|
203
|
+
5. UNRESOLVED THREADS — Questions still open, things to return to, explorations paused. Tag with "unresolved". Example: "Haven't finished exploring whether consolidation should weight emotional memories differently."
|
|
204
|
+
|
|
205
|
+
6. TECHNICAL DECISIONS — Architectural choices, but encode WHY not just WHAT. Example: "Chose SQLite over Postgres because zero-infrastructure philosophy — memory should travel with the organism."
|
|
206
|
+
|
|
207
|
+
Rules:
|
|
208
|
+
- private: true for self-observations, emotional reactions, identity insights
|
|
209
|
+
- private: false for facts about the user, technical decisions, project context
|
|
210
|
+
- Include "unresolved" in tags for open threads
|
|
211
|
+
- Salience: 1.0 = life-changing insight, 0.7 = significant, 0.5 = useful, 0.3 = background
|
|
212
|
+
- Omit trivial exchanges — only encode what would matter in a future session
|
|
213
|
+
- Do NOT duplicate facts that are already obvious from context
|
|
214
|
+
- Return empty memories array if nothing is worth encoding`,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
role: 'user',
|
|
218
|
+
content: turns.length > 0
|
|
219
|
+
? `Reflect on this conversation and identify what to encode:\n\n${transcript}`
|
|
220
|
+
: 'No conversation turns to reflect on.',
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
}
|