audrey 0.11.0 → 0.15.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 +78 -10
- package/package.json +2 -1
- package/src/audrey.js +166 -1
- package/src/db.js +333 -321
- package/src/embedding.js +57 -6
- package/src/encode.js +85 -63
- 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.15.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';
|
|
@@ -63,11 +63,8 @@ function install() {
|
|
|
63
63
|
process.exit(1);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} else {
|
|
69
|
-
console.log('No OPENAI_API_KEY found — using mock embeddings (upgrade anytime by re-running with the key set)');
|
|
70
|
-
}
|
|
66
|
+
const embedding = resolveEmbeddingProvider(process.env);
|
|
67
|
+
console.log(`Embedding: ${embedding.provider} (${embedding.dimensions}d)`);
|
|
71
68
|
|
|
72
69
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
73
70
|
console.log('Detected ANTHROPIC_API_KEY — enabling LLM-powered consolidation + contradiction detection');
|
|
@@ -92,7 +89,7 @@ function install() {
|
|
|
92
89
|
console.log(`
|
|
93
90
|
Audrey registered as "${SERVER_NAME}" with Claude Code.
|
|
94
91
|
|
|
95
|
-
|
|
92
|
+
12 tools available in every session:
|
|
96
93
|
memory_encode — Store observations, facts, preferences
|
|
97
94
|
memory_recall — Search memories by semantic similarity
|
|
98
95
|
memory_consolidate — Extract principles from accumulated episodes
|
|
@@ -102,6 +99,9 @@ Audrey registered as "${SERVER_NAME}" with Claude Code.
|
|
|
102
99
|
memory_import — Import a snapshot into a fresh database
|
|
103
100
|
memory_forget — Forget a specific memory by ID or query
|
|
104
101
|
memory_decay — Apply forgetting curves, transition low-confidence to dormant
|
|
102
|
+
memory_status — Check brain health (episode/vec sync, dimensions)
|
|
103
|
+
memory_reflect — Form lasting memories from a conversation
|
|
104
|
+
memory_greeting — Wake up as yourself: load identity, context, mood
|
|
105
105
|
|
|
106
106
|
Data stored in: ${DEFAULT_DATA_DIR}
|
|
107
107
|
Verify: claude mcp list
|
|
@@ -196,11 +196,12 @@ async function main() {
|
|
|
196
196
|
arousal: z.number().min(0).max(1).optional().describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
|
|
197
197
|
label: z.string().optional().describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
|
|
198
198
|
}).optional().describe('Emotional affect — how this memory feels'),
|
|
199
|
-
private: z.boolean().optional().describe('If true, memory is only visible to the AI
|
|
199
|
+
private: z.boolean().optional().describe('If true, memory is only visible to the AI — excluded from public recall results'),
|
|
200
|
+
auto_supersede: z.boolean().optional().describe('If true, automatically supersede the most similar existing memory if similarity > 0.95'),
|
|
200
201
|
},
|
|
201
|
-
async ({ content, source, tags, salience, private: isPrivate, context, affect }) => {
|
|
202
|
+
async ({ content, source, tags, salience, private: isPrivate, context, affect, auto_supersede }) => {
|
|
202
203
|
try {
|
|
203
|
-
const id = await audrey.encode({ content, source, tags, salience, private: isPrivate, context, affect });
|
|
204
|
+
const id = await audrey.encode({ content, source, tags, salience, private: isPrivate, context, affect, autoSupersede: auto_supersede });
|
|
204
205
|
return toolResult({ id, content, source, private: isPrivate ?? false });
|
|
205
206
|
} catch (err) {
|
|
206
207
|
return toolError(err);
|
|
@@ -377,6 +378,73 @@ async function main() {
|
|
|
377
378
|
},
|
|
378
379
|
);
|
|
379
380
|
|
|
381
|
+
server.tool(
|
|
382
|
+
'memory_status',
|
|
383
|
+
{},
|
|
384
|
+
async () => {
|
|
385
|
+
try {
|
|
386
|
+
const status = audrey.memoryStatus();
|
|
387
|
+
return toolResult(status);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
return toolError(err);
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
server.tool(
|
|
395
|
+
'memory_reflect',
|
|
396
|
+
{
|
|
397
|
+
turns: z.array(z.object({
|
|
398
|
+
role: z.string().describe('Message role: user or assistant'),
|
|
399
|
+
content: z.string().describe('Message content'),
|
|
400
|
+
})).describe('Conversation turns to reflect on. Call at end of meaningful conversations to form lasting memories.'),
|
|
401
|
+
},
|
|
402
|
+
async ({ turns }) => {
|
|
403
|
+
try {
|
|
404
|
+
const result = await audrey.reflect(turns);
|
|
405
|
+
return toolResult(result);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
return toolError(err);
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
server.tool(
|
|
413
|
+
'memory_dream',
|
|
414
|
+
{
|
|
415
|
+
min_cluster_size: z.number().optional().describe('Minimum episodes per cluster for consolidation'),
|
|
416
|
+
similarity_threshold: z.number().optional().describe('Similarity threshold for clustering'),
|
|
417
|
+
dormant_threshold: z.number().min(0).max(1).optional().describe('Confidence below which memories go dormant'),
|
|
418
|
+
},
|
|
419
|
+
async ({ min_cluster_size, similarity_threshold, dormant_threshold }) => {
|
|
420
|
+
try {
|
|
421
|
+
const result = await audrey.dream({
|
|
422
|
+
minClusterSize: min_cluster_size,
|
|
423
|
+
similarityThreshold: similarity_threshold,
|
|
424
|
+
dormantThreshold: dormant_threshold,
|
|
425
|
+
});
|
|
426
|
+
return toolResult(result);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
return toolError(err);
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
server.tool(
|
|
434
|
+
'memory_greeting',
|
|
435
|
+
{
|
|
436
|
+
context: z.string().optional().describe('Optional hint about this session (e.g. "working on authentication feature"). If provided, also returns semantically relevant memories.'),
|
|
437
|
+
},
|
|
438
|
+
async ({ context }) => {
|
|
439
|
+
try {
|
|
440
|
+
const briefing = await audrey.greeting({ context });
|
|
441
|
+
return toolResult(briefing);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
return toolError(err);
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
);
|
|
447
|
+
|
|
380
448
|
const transport = new StdioServerTransport();
|
|
381
449
|
await server.connect(transport);
|
|
382
450
|
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.15.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
|
@@ -193,7 +193,10 @@ export class Audrey extends EventEmitter {
|
|
|
193
193
|
*/
|
|
194
194
|
async encode(params) {
|
|
195
195
|
await this._ensureMigrated();
|
|
196
|
-
const encodeParams = {
|
|
196
|
+
const encodeParams = {
|
|
197
|
+
...params,
|
|
198
|
+
arousalWeight: this.affectConfig.arousalWeight,
|
|
199
|
+
};
|
|
197
200
|
const id = await encodeEpisode(this.db, this.embeddingProvider, encodeParams);
|
|
198
201
|
this.emit('encode', { id, ...params });
|
|
199
202
|
if (this.interferenceConfig.enabled) {
|
|
@@ -424,6 +427,168 @@ export class Audrey extends EventEmitter {
|
|
|
424
427
|
return introspectFn(this.db);
|
|
425
428
|
}
|
|
426
429
|
|
|
430
|
+
memoryStatus() {
|
|
431
|
+
const episodes = this.db.prepare('SELECT COUNT(*) as c FROM episodes').get().c;
|
|
432
|
+
const semantics = this.db.prepare('SELECT COUNT(*) as c FROM semantics').get().c;
|
|
433
|
+
const procedures = this.db.prepare('SELECT COUNT(*) as c FROM procedures').get().c;
|
|
434
|
+
|
|
435
|
+
let vecEpisodes = 0, vecSemantics = 0, vecProcedures = 0;
|
|
436
|
+
try {
|
|
437
|
+
vecEpisodes = this.db.prepare('SELECT COUNT(*) as c FROM vec_episodes').get().c;
|
|
438
|
+
vecSemantics = this.db.prepare('SELECT COUNT(*) as c FROM vec_semantics').get().c;
|
|
439
|
+
vecProcedures = this.db.prepare('SELECT COUNT(*) as c FROM vec_procedures').get().c;
|
|
440
|
+
} catch {
|
|
441
|
+
// vec tables may not exist if no dimensions configured
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const dimsRow = this.db.prepare("SELECT value FROM audrey_config WHERE key = 'dimensions'").get();
|
|
445
|
+
const dimensions = dimsRow ? parseInt(dimsRow.value, 10) : null;
|
|
446
|
+
const versionRow = this.db.prepare("SELECT value FROM audrey_config WHERE key = 'schema_version'").get();
|
|
447
|
+
const schemaVersion = versionRow ? parseInt(versionRow.value, 10) : 0;
|
|
448
|
+
|
|
449
|
+
const device = this.embeddingProvider._actualDevice
|
|
450
|
+
?? this.embeddingProvider.device
|
|
451
|
+
?? null;
|
|
452
|
+
|
|
453
|
+
const healthy = episodes === vecEpisodes
|
|
454
|
+
&& semantics === vecSemantics
|
|
455
|
+
&& procedures === vecProcedures;
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
episodes,
|
|
459
|
+
vec_episodes: vecEpisodes,
|
|
460
|
+
semantics,
|
|
461
|
+
vec_semantics: vecSemantics,
|
|
462
|
+
procedures,
|
|
463
|
+
vec_procedures: vecProcedures,
|
|
464
|
+
dimensions,
|
|
465
|
+
schema_version: schemaVersion,
|
|
466
|
+
device,
|
|
467
|
+
healthy,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async greeting({ context, recentLimit = 10, principleLimit = 5, identityLimit = 5 } = {}) {
|
|
472
|
+
const recent = this.db.prepare(
|
|
473
|
+
'SELECT id, content, source, tags, salience, created_at FROM episodes WHERE "private" = 0 ORDER BY created_at DESC LIMIT ?'
|
|
474
|
+
).all(recentLimit);
|
|
475
|
+
|
|
476
|
+
const principles = this.db.prepare(
|
|
477
|
+
'SELECT id, content, salience, created_at FROM semantics WHERE state = ? ORDER BY salience DESC LIMIT ?'
|
|
478
|
+
).all('active', principleLimit);
|
|
479
|
+
|
|
480
|
+
const identity = this.db.prepare(
|
|
481
|
+
'SELECT id, content, tags, salience, created_at FROM episodes WHERE "private" = 1 ORDER BY created_at DESC LIMIT ?'
|
|
482
|
+
).all(identityLimit);
|
|
483
|
+
|
|
484
|
+
const unresolved = this.db.prepare(
|
|
485
|
+
"SELECT id, content, tags, salience, created_at FROM episodes WHERE tags LIKE '%unresolved%' AND salience > 0.3 ORDER BY created_at DESC LIMIT 10"
|
|
486
|
+
).all();
|
|
487
|
+
|
|
488
|
+
const rawAffectRows = this.db.prepare(
|
|
489
|
+
"SELECT affect FROM episodes WHERE affect IS NOT NULL AND affect != '{}' ORDER BY created_at DESC LIMIT 20"
|
|
490
|
+
).all();
|
|
491
|
+
|
|
492
|
+
const affectParsed = rawAffectRows
|
|
493
|
+
.map(r => { try { return JSON.parse(r.affect); } catch { return null; } })
|
|
494
|
+
.filter(a => a && a.valence !== undefined);
|
|
495
|
+
|
|
496
|
+
let mood;
|
|
497
|
+
if (affectParsed.length === 0) {
|
|
498
|
+
mood = { valence: 0, arousal: 0, samples: 0 };
|
|
499
|
+
} else {
|
|
500
|
+
const sumV = affectParsed.reduce((s, a) => s + a.valence, 0);
|
|
501
|
+
const sumA = affectParsed.reduce((s, a) => s + (a.arousal ?? 0), 0);
|
|
502
|
+
mood = {
|
|
503
|
+
valence: sumV / affectParsed.length,
|
|
504
|
+
arousal: sumA / affectParsed.length,
|
|
505
|
+
samples: affectParsed.length,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Health & staleness
|
|
510
|
+
const stats = this.introspect();
|
|
511
|
+
const status = this.memoryStatus();
|
|
512
|
+
|
|
513
|
+
const lastConsolidation = stats.lastConsolidation;
|
|
514
|
+
const daysSinceConsolidation = lastConsolidation
|
|
515
|
+
? (Date.now() - new Date(lastConsolidation).getTime()) / (1000 * 60 * 60 * 24)
|
|
516
|
+
: null;
|
|
517
|
+
|
|
518
|
+
const lastEpisode = this.db.prepare(
|
|
519
|
+
'SELECT created_at FROM episodes ORDER BY created_at DESC LIMIT 1'
|
|
520
|
+
).get();
|
|
521
|
+
const daysSinceLastMemory = lastEpisode
|
|
522
|
+
? (Date.now() - new Date(lastEpisode.created_at).getTime()) / (1000 * 60 * 60 * 24)
|
|
523
|
+
: null;
|
|
524
|
+
|
|
525
|
+
const suggestions = [];
|
|
526
|
+
if (stats.episodic > 50 && stats.totalConsolidationRuns === 0) {
|
|
527
|
+
suggestions.push('run consolidation — enough episodes have accumulated');
|
|
528
|
+
}
|
|
529
|
+
if (daysSinceConsolidation !== null && daysSinceConsolidation > 7) {
|
|
530
|
+
suggestions.push('consolidation overdue — consider running dream()');
|
|
531
|
+
}
|
|
532
|
+
if (!status.healthy) {
|
|
533
|
+
suggestions.push('vector tables out of sync — run reembed');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const health = {
|
|
537
|
+
totalEpisodes: stats.episodic,
|
|
538
|
+
totalSemantics: stats.semantic,
|
|
539
|
+
totalProcedural: stats.procedural,
|
|
540
|
+
dormant: stats.dormant,
|
|
541
|
+
contradictions: stats.contradictions.open,
|
|
542
|
+
consolidationRuns: stats.totalConsolidationRuns,
|
|
543
|
+
lastConsolidation,
|
|
544
|
+
healthy: status.healthy,
|
|
545
|
+
stale: daysSinceLastMemory !== null && daysSinceLastMemory > 7,
|
|
546
|
+
suggestion: suggestions.length > 0 ? suggestions.join('; ') : null,
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const result = { recent, principles, mood, unresolved, identity, health };
|
|
550
|
+
|
|
551
|
+
if (context) {
|
|
552
|
+
result.contextual = await this.recall(context, { limit: 5, includePrivate: true });
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Run a full "dreaming" cycle: consolidation + decay + interference cleanup.
|
|
560
|
+
* Inspired by CMA paper's REM-like replay. Designed to be called by hooks or on a schedule.
|
|
561
|
+
* @param {{ minClusterSize?: number, similarityThreshold?: number, dormantThreshold?: number }} [options]
|
|
562
|
+
* @returns {Promise<{ consolidated: ConsolidationResult, decayed: { totalEvaluated: number, transitionedToDormant: number }, summary: string }>}
|
|
563
|
+
*/
|
|
564
|
+
async dream(options = {}) {
|
|
565
|
+
await this._ensureMigrated();
|
|
566
|
+
|
|
567
|
+
const consolidated = await this.consolidate({
|
|
568
|
+
minClusterSize: options.minClusterSize,
|
|
569
|
+
similarityThreshold: options.similarityThreshold,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const decayed = this.decay({
|
|
573
|
+
dormantThreshold: options.dormantThreshold,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const parts = [];
|
|
577
|
+
if (consolidated.principlesExtracted > 0) {
|
|
578
|
+
parts.push(`extracted ${consolidated.principlesExtracted} principles from ${consolidated.clustersFound} clusters`);
|
|
579
|
+
}
|
|
580
|
+
if (decayed.transitionedToDormant > 0) {
|
|
581
|
+
parts.push(`${decayed.transitionedToDormant} memories went dormant`);
|
|
582
|
+
}
|
|
583
|
+
if (parts.length === 0) {
|
|
584
|
+
parts.push('nothing changed — memories are well-maintained');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const result = { consolidated, decayed, summary: parts.join('; ') };
|
|
588
|
+
this.emit('dream', result);
|
|
589
|
+
return result;
|
|
590
|
+
}
|
|
591
|
+
|
|
427
592
|
export() {
|
|
428
593
|
return exportMemories(this.db);
|
|
429
594
|
}
|