audrey 0.8.0 → 0.11.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/README.md +76 -8
- package/mcp-server/config.js +30 -16
- package/mcp-server/index.js +43 -5
- package/package.json +2 -1
- package/src/affect.js +64 -0
- package/src/audrey.js +82 -6
- package/src/db.js +321 -281
- package/src/embedding.js +90 -53
- package/src/encode.js +63 -54
- package/src/export.js +5 -3
- package/src/import.js +15 -8
- package/src/index.js +1 -0
- package/src/migrate.js +27 -9
- package/src/prompts.js +43 -0
- package/src/recall.js +27 -16
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Audrey
|
|
2
2
|
|
|
3
|
-
Biological memory architecture for AI agents.
|
|
3
|
+
Biological memory architecture for AI agents. Memory that decays, consolidates, feels, and learns — not just a database.
|
|
4
4
|
|
|
5
5
|
## Why Audrey Exists
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ Audrey fixes all of this by modeling memory the way the brain does:
|
|
|
19
19
|
| Neocortex | Semantic Memory | Consolidated principles and patterns |
|
|
20
20
|
| Sleep Replay | Consolidation Engine | Extracts patterns from episodes, promotes to principles |
|
|
21
21
|
| Prefrontal Cortex | Validation Engine | Truth-checking, contradiction detection |
|
|
22
|
-
| Amygdala |
|
|
22
|
+
| Amygdala | Affect System | Emotional encoding, arousal-salience coupling, mood-congruent recall |
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
@@ -67,22 +67,26 @@ const brain = new Audrey({
|
|
|
67
67
|
embedding: { provider: 'mock', dimensions: 8 }, // or 'openai' for production
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
// 2. Encode observations
|
|
70
|
+
// 2. Encode observations — with optional emotional context
|
|
71
71
|
await brain.encode({
|
|
72
72
|
content: 'Stripe API returns 429 above 100 req/s',
|
|
73
73
|
source: 'direct-observation',
|
|
74
74
|
tags: ['stripe', 'rate-limit'],
|
|
75
|
+
affect: { valence: -0.4, arousal: 0.7, label: 'frustration' },
|
|
75
76
|
});
|
|
76
77
|
|
|
77
|
-
// 3. Recall what you know
|
|
78
|
-
const memories = await brain.recall('stripe rate limits', {
|
|
79
|
-
|
|
78
|
+
// 3. Recall what you know — mood-congruent retrieval
|
|
79
|
+
const memories = await brain.recall('stripe rate limits', {
|
|
80
|
+
limit: 5,
|
|
81
|
+
mood: { valence: -0.3 }, // frustrated right now? memories encoded in frustration surface first
|
|
82
|
+
});
|
|
80
83
|
|
|
81
84
|
// 4. Filtered recall — by tag, source, or date range
|
|
82
85
|
const recent = await brain.recall('stripe', {
|
|
83
86
|
tags: ['rate-limit'],
|
|
84
87
|
sources: ['direct-observation'],
|
|
85
88
|
after: '2026-02-01T00:00:00Z',
|
|
89
|
+
context: { task: 'debugging', domain: 'payments' }, // context-dependent retrieval
|
|
86
90
|
});
|
|
87
91
|
|
|
88
92
|
// 5. Consolidate episodes into principles (the "sleep" cycle)
|
|
@@ -127,6 +131,31 @@ const brain = new Audrey({
|
|
|
127
131
|
minEpisodes: 3, // Minimum cluster size for principle extraction
|
|
128
132
|
},
|
|
129
133
|
|
|
134
|
+
// Context-dependent retrieval (v0.8.0)
|
|
135
|
+
context: {
|
|
136
|
+
enabled: true, // Enable encoding-specificity principle
|
|
137
|
+
weight: 0.3, // Max 30% confidence boost on full context match
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// Emotional memory (v0.9.0)
|
|
141
|
+
affect: {
|
|
142
|
+
enabled: true, // Enable affect system
|
|
143
|
+
weight: 0.2, // Max 20% mood-congruence boost
|
|
144
|
+
arousalWeight: 0.3, // Yerkes-Dodson arousal-salience coupling
|
|
145
|
+
resonance: { // Detect emotional echoes across experiences
|
|
146
|
+
enabled: true,
|
|
147
|
+
k: 5, // How many past episodes to check
|
|
148
|
+
threshold: 0.5, // Semantic similarity threshold
|
|
149
|
+
affectThreshold: 0.6, // Emotional similarity threshold
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Interference-based forgetting (v0.7.0)
|
|
154
|
+
interference: {
|
|
155
|
+
enabled: true, // New episodes suppress similar existing memories
|
|
156
|
+
weight: 0.15, // Suppression strength
|
|
157
|
+
},
|
|
158
|
+
|
|
130
159
|
// Decay settings
|
|
131
160
|
decay: {
|
|
132
161
|
dormantThreshold: 0.1, // Below this confidence = dormant
|
|
@@ -284,6 +313,12 @@ const id = await brain.encode({
|
|
|
284
313
|
},
|
|
285
314
|
tags: ['stripe', 'production'], // Optional. Array of strings.
|
|
286
315
|
supersedes: 'previous-id', // Optional. ID of episode this corrects.
|
|
316
|
+
context: { task: 'debugging' }, // Optional. Situational context for retrieval.
|
|
317
|
+
affect: { // Optional. Emotional context.
|
|
318
|
+
valence: -0.5, // -1 (negative) to 1 (positive)
|
|
319
|
+
arousal: 0.7, // 0 (calm) to 1 (activated)
|
|
320
|
+
label: 'frustration', // Human-readable emotion label
|
|
321
|
+
},
|
|
287
322
|
});
|
|
288
323
|
```
|
|
289
324
|
|
|
@@ -316,6 +351,8 @@ const memories = await brain.recall('stripe rate limits', {
|
|
|
316
351
|
sources: ['direct-observation'], // Only episodic memories from these sources
|
|
317
352
|
after: '2026-02-01T00:00:00Z', // Only memories created after this date
|
|
318
353
|
before: '2026-03-01T00:00:00Z', // Only memories created before this date
|
|
354
|
+
context: { task: 'debugging' }, // Boost memories encoded in matching context
|
|
355
|
+
mood: { valence: -0.3, arousal: 0.5 }, // Mood-congruent retrieval
|
|
319
356
|
});
|
|
320
357
|
```
|
|
321
358
|
|
|
@@ -332,6 +369,8 @@ Each result:
|
|
|
332
369
|
score: 0.74, // similarity * confidence
|
|
333
370
|
source: 'consolidation',
|
|
334
371
|
state: 'active',
|
|
372
|
+
contextMatch: 0.8, // When retrieval context provided
|
|
373
|
+
moodCongruence: 0.7, // When mood provided
|
|
335
374
|
provenance: { // When includeProvenance: true
|
|
336
375
|
evidenceEpisodeIds: ['01XYZ...', '01DEF...'],
|
|
337
376
|
evidenceCount: 3,
|
|
@@ -467,6 +506,8 @@ brain.on('decay', ({ totalEvaluated, transitionedToDormant }) => { ... });
|
|
|
467
506
|
brain.on('rollback', ({ runId, rolledBackMemories }) => { ... });
|
|
468
507
|
brain.on('forget', ({ id, type, purged }) => { ... });
|
|
469
508
|
brain.on('purge', ({ episodes, semantics, procedures }) => { ... });
|
|
509
|
+
brain.on('interference', ({ newEpisodeId, suppressedId, similarity }) => { ... });
|
|
510
|
+
brain.on('resonance', ({ episodeId, resonances }) => { ... });
|
|
470
511
|
brain.on('migration', ({ episodes, semantics, procedures }) => { ... });
|
|
471
512
|
brain.on('error', (err) => { ... });
|
|
472
513
|
```
|
|
@@ -492,6 +533,9 @@ src/
|
|
|
492
533
|
decay.js Ebbinghaus forgetting curves.
|
|
493
534
|
embedding.js Pluggable providers (Mock, OpenAI). Batch embedding.
|
|
494
535
|
encode.js Immutable episodic memory creation + vec0 writes.
|
|
536
|
+
affect.js Emotional memory: arousal-salience coupling, mood-congruent recall, resonance.
|
|
537
|
+
context.js Context-dependent retrieval modifier (encoding specificity).
|
|
538
|
+
interference.js Competitive memory suppression (engram competition).
|
|
495
539
|
forget.js Soft-delete, hard-delete, query-based forget, bulk purge.
|
|
496
540
|
introspect.js Health dashboard queries.
|
|
497
541
|
llm.js Pluggable LLM providers (Mock, Anthropic, OpenAI).
|
|
@@ -531,7 +575,7 @@ All mutations use SQLite transactions. CHECK constraints enforce valid states an
|
|
|
531
575
|
## Running Tests
|
|
532
576
|
|
|
533
577
|
```bash
|
|
534
|
-
npm test #
|
|
578
|
+
npm test # 379 tests across 28 files
|
|
535
579
|
npm run test:watch
|
|
536
580
|
```
|
|
537
581
|
|
|
@@ -547,7 +591,29 @@ Demonstrates the full pipeline: encode 3 rate-limit observations, consolidate in
|
|
|
547
591
|
|
|
548
592
|
## Changelog
|
|
549
593
|
|
|
550
|
-
### v0.
|
|
594
|
+
### v0.9.0 — Emotional Memory (current)
|
|
595
|
+
|
|
596
|
+
- Valence-arousal affect model (Russell's circumplex) on every episode
|
|
597
|
+
- Arousal-salience coupling via Yerkes-Dodson inverted-U curve
|
|
598
|
+
- Mood-congruent recall — matching emotional state boosts retrieval confidence
|
|
599
|
+
- Emotional resonance detection — new experiences that echo past emotional patterns emit events
|
|
600
|
+
- MCP server: `memory_encode` accepts `affect`, `memory_recall` accepts `mood`
|
|
601
|
+
- 379 tests across 28 test files
|
|
602
|
+
|
|
603
|
+
### v0.8.0 — Context-Dependent Retrieval
|
|
604
|
+
|
|
605
|
+
- Encoding specificity principle: context stored with memory, matching context boosts recall
|
|
606
|
+
- MCP server: `memory_encode` and `memory_recall` accept `context`
|
|
607
|
+
- 340 tests across 27 test files
|
|
608
|
+
|
|
609
|
+
### v0.7.0 — Interference + Salience
|
|
610
|
+
|
|
611
|
+
- Interference-based forgetting: new memories competitively suppress similar existing ones
|
|
612
|
+
- Salience-weighted confidence: high-salience memories resist decay
|
|
613
|
+
- Spaced-repetition reconsolidation: retrieval intervals affect reinforcement strength
|
|
614
|
+
- 310 tests across 25 test files
|
|
615
|
+
|
|
616
|
+
### v0.6.0 — Filtered Recall + Forget
|
|
551
617
|
|
|
552
618
|
- Filtered recall: tag, source, and date-range filters on `recall()` and `recallStream()`
|
|
553
619
|
- `forget()` — soft-delete any memory by ID
|
|
@@ -608,6 +674,8 @@ Demonstrates the full pipeline: encode 3 rate-limit observations, consolidate in
|
|
|
608
674
|
|
|
609
675
|
**Why soft-delete by default?** Hard-deletes are irreversible. Soft-delete preserves data integrity and audit trails while excluding the memory from recall. Use `purge: true` or `brain.purge()` when you need permanent removal (GDPR, storage cleanup).
|
|
610
676
|
|
|
677
|
+
**Why emotional memory?** Every memory system stores facts. Biological memory stores facts with emotional context — and that context changes how memories are retrieved. Emotional arousal modulates encoding strength (amygdala-hippocampal interaction). Current mood biases which memories surface (Bower, 1981). This isn't a novelty feature — it's the foundation for AI that remembers like it cares.
|
|
678
|
+
|
|
611
679
|
## License
|
|
612
680
|
|
|
613
681
|
MIT
|
package/mcp-server/config.js
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
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.11.0';
|
|
5
5
|
export const SERVER_NAME = 'audrey-memory';
|
|
6
6
|
export const DEFAULT_DATA_DIR = join(homedir(), '.audrey', 'data');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Resolves which embedding provider to use.
|
|
10
|
+
* Priority: explicit config -> gemini (if GOOGLE_API_KEY exists) -> local
|
|
11
|
+
* OpenAI is NEVER auto-selected -- must be set explicitly via AUDREY_EMBEDDING_PROVIDER=openai.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveEmbeddingProvider(env, explicit) {
|
|
14
|
+
if (explicit && explicit !== 'auto') {
|
|
15
|
+
const dims = explicit === 'openai' ? 1536 : explicit === 'gemini' ? 768 : 384;
|
|
16
|
+
const apiKey = explicit === 'gemini'
|
|
17
|
+
? (env.GOOGLE_API_KEY || env.GEMINI_API_KEY)
|
|
18
|
+
: explicit === 'openai'
|
|
19
|
+
? env.OPENAI_API_KEY
|
|
20
|
+
: undefined;
|
|
21
|
+
return { provider: explicit, apiKey, dimensions: dims };
|
|
22
|
+
}
|
|
23
|
+
if (env.GOOGLE_API_KEY || env.GEMINI_API_KEY) {
|
|
24
|
+
return { provider: 'gemini', apiKey: env.GOOGLE_API_KEY || env.GEMINI_API_KEY, dimensions: 768 };
|
|
25
|
+
}
|
|
26
|
+
return { provider: 'local', dimensions: 384 };
|
|
27
|
+
}
|
|
28
|
+
|
|
8
29
|
export function buildAudreyConfig() {
|
|
9
30
|
const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
|
|
10
31
|
const agent = process.env.AUDREY_AGENT || 'claude-code';
|
|
11
|
-
const
|
|
12
|
-
const embDimensions = parseInt(process.env.AUDREY_EMBEDDING_DIMENSIONS || '8', 10);
|
|
32
|
+
const explicitProvider = process.env.AUDREY_EMBEDDING_PROVIDER;
|
|
13
33
|
const llmProvider = process.env.AUDREY_LLM_PROVIDER;
|
|
14
34
|
|
|
15
|
-
const
|
|
16
|
-
dataDir,
|
|
17
|
-
agent,
|
|
18
|
-
embedding: { provider: embProvider, dimensions: embDimensions },
|
|
19
|
-
};
|
|
35
|
+
const embedding = resolveEmbeddingProvider(process.env, explicitProvider);
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
config.embedding.apiKey = process.env.OPENAI_API_KEY;
|
|
23
|
-
}
|
|
37
|
+
const config = { dataDir, agent, embedding };
|
|
24
38
|
|
|
25
39
|
if (llmProvider === 'anthropic') {
|
|
26
40
|
config.llm = { provider: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY };
|
|
@@ -36,13 +50,13 @@ export function buildAudreyConfig() {
|
|
|
36
50
|
export function buildInstallArgs(env = process.env) {
|
|
37
51
|
const envPairs = [`AUDREY_DATA_DIR=${DEFAULT_DATA_DIR}`];
|
|
38
52
|
|
|
39
|
-
|
|
53
|
+
const embedding = resolveEmbeddingProvider(env);
|
|
54
|
+
if (embedding.provider === 'gemini') {
|
|
55
|
+
envPairs.push('AUDREY_EMBEDDING_PROVIDER=gemini');
|
|
56
|
+
envPairs.push(`GOOGLE_API_KEY=${embedding.apiKey}`);
|
|
57
|
+
} else if (embedding.provider === 'openai') {
|
|
40
58
|
envPairs.push('AUDREY_EMBEDDING_PROVIDER=openai');
|
|
41
|
-
envPairs.push('AUDREY_EMBEDDING_DIMENSIONS=1536');
|
|
42
59
|
envPairs.push(`OPENAI_API_KEY=${env.OPENAI_API_KEY}`);
|
|
43
|
-
} else {
|
|
44
|
-
envPairs.push('AUDREY_EMBEDDING_PROVIDER=mock');
|
|
45
|
-
envPairs.push('AUDREY_EMBEDDING_DIMENSIONS=8');
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
if (env.ANTHROPIC_API_KEY) {
|
package/mcp-server/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { Audrey } from '../src/index.js';
|
|
10
10
|
import { readStoredDimensions } from '../src/db.js';
|
|
11
|
-
import { VERSION, SERVER_NAME, DEFAULT_DATA_DIR, buildAudreyConfig, buildInstallArgs } from './config.js';
|
|
11
|
+
import { VERSION, SERVER_NAME, DEFAULT_DATA_DIR, buildAudreyConfig, buildInstallArgs, resolveEmbeddingProvider } from './config.js';
|
|
12
12
|
|
|
13
13
|
const VALID_SOURCES = ['direct-observation', 'told-by-user', 'tool-result', 'inference', 'model-generated'];
|
|
14
14
|
const VALID_TYPES = ['episodic', 'semantic', 'procedural'];
|
|
@@ -19,6 +19,11 @@ if (subcommand === 'install') {
|
|
|
19
19
|
install();
|
|
20
20
|
} else if (subcommand === 'uninstall') {
|
|
21
21
|
uninstall();
|
|
22
|
+
} else if (subcommand === 'reembed') {
|
|
23
|
+
reembed().catch(err => {
|
|
24
|
+
console.error('[audrey] reembed failed:', err);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
|
22
27
|
} else if (subcommand === 'status') {
|
|
23
28
|
status();
|
|
24
29
|
} else {
|
|
@@ -28,6 +33,28 @@ if (subcommand === 'install') {
|
|
|
28
33
|
});
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
|
|
37
|
+
async function reembed() {
|
|
38
|
+
const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
|
|
39
|
+
const explicit = process.env.AUDREY_EMBEDDING_PROVIDER;
|
|
40
|
+
const embedding = resolveEmbeddingProvider(process.env, explicit);
|
|
41
|
+
|
|
42
|
+
const storedDims = readStoredDimensions(dataDir);
|
|
43
|
+
const dimensionsChanged = storedDims !== null && storedDims !== embedding.dimensions;
|
|
44
|
+
|
|
45
|
+
console.log(`Re-embedding with ${embedding.provider} (${embedding.dimensions}d)...`);
|
|
46
|
+
if (dimensionsChanged) {
|
|
47
|
+
console.log(`Dimension change: ${storedDims}d -> ${embedding.dimensions}d (will drop and recreate vec tables)`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const audrey = new Audrey({ dataDir, agent: 'reembed', embedding });
|
|
51
|
+
const { reembedAll } = await import('../src/migrate.js');
|
|
52
|
+
const counts = await reembedAll(audrey.db, audrey.embeddingProvider, { dropAndRecreate: dimensionsChanged });
|
|
53
|
+
audrey.close();
|
|
54
|
+
|
|
55
|
+
console.log(`Done. Re-embedded: ${counts.episodes} episodes, ${counts.semantics} semantics, ${counts.procedures} procedures`);
|
|
56
|
+
}
|
|
57
|
+
|
|
31
58
|
function install() {
|
|
32
59
|
try {
|
|
33
60
|
execFileSync('claude', ['--version'], { stdio: 'ignore' });
|
|
@@ -164,11 +191,17 @@ async function main() {
|
|
|
164
191
|
tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
|
|
165
192
|
salience: z.number().min(0).max(1).optional().describe('Importance weight 0-1'),
|
|
166
193
|
context: z.record(z.string()).optional().describe('Situational context as key-value pairs (e.g., {task: "debugging", domain: "payments"})'),
|
|
194
|
+
affect: z.object({
|
|
195
|
+
valence: z.number().min(-1).max(1).describe('Emotional valence: -1 (very negative) to 1 (very positive)'),
|
|
196
|
+
arousal: z.number().min(0).max(1).optional().describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
|
|
197
|
+
label: z.string().optional().describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
|
|
198
|
+
}).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'),
|
|
167
200
|
},
|
|
168
|
-
async ({ content, source, tags, salience, context }) => {
|
|
201
|
+
async ({ content, source, tags, salience, private: isPrivate, context, affect }) => {
|
|
169
202
|
try {
|
|
170
|
-
const id = await audrey.encode({ content, source, tags, salience, context });
|
|
171
|
-
return toolResult({ id, content, source });
|
|
203
|
+
const id = await audrey.encode({ content, source, tags, salience, private: isPrivate, context, affect });
|
|
204
|
+
return toolResult({ id, content, source, private: isPrivate ?? false });
|
|
172
205
|
} catch (err) {
|
|
173
206
|
return toolError(err);
|
|
174
207
|
}
|
|
@@ -187,8 +220,12 @@ async function main() {
|
|
|
187
220
|
after: z.string().optional().describe('Only return memories created after this ISO date'),
|
|
188
221
|
before: z.string().optional().describe('Only return memories created before this ISO date'),
|
|
189
222
|
context: z.record(z.string()).optional().describe('Retrieval context — memories encoded in matching context get boosted'),
|
|
223
|
+
mood: z.object({
|
|
224
|
+
valence: z.number().min(-1).max(1).describe('Current emotional valence: -1 (negative) to 1 (positive)'),
|
|
225
|
+
arousal: z.number().min(0).max(1).optional().describe('Current arousal: 0 (calm) to 1 (activated)'),
|
|
226
|
+
}).optional().describe('Current mood — boosts recall of memories encoded in similar emotional state'),
|
|
190
227
|
},
|
|
191
|
-
async ({ query, limit, types, min_confidence, tags, sources, after, before, context }) => {
|
|
228
|
+
async ({ query, limit, types, min_confidence, tags, sources, after, before, context, mood }) => {
|
|
192
229
|
try {
|
|
193
230
|
const results = await audrey.recall(query, {
|
|
194
231
|
limit: limit ?? 10,
|
|
@@ -199,6 +236,7 @@ async function main() {
|
|
|
199
236
|
after,
|
|
200
237
|
before,
|
|
201
238
|
context,
|
|
239
|
+
mood,
|
|
202
240
|
});
|
|
203
241
|
return toolResult(results);
|
|
204
242
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "audrey",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
},
|
|
64
64
|
"license": "MIT",
|
|
65
65
|
"dependencies": {
|
|
66
|
+
"@huggingface/transformers": "^3.8.1",
|
|
66
67
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
67
68
|
"better-sqlite3": "^12.6.2",
|
|
68
69
|
"sqlite-vec": "^0.1.7-alpha.2",
|
package/src/affect.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function arousalSalienceBoost(arousal) {
|
|
2
|
+
if (arousal === undefined || arousal === null) return 0;
|
|
3
|
+
// Inverted-U (Yerkes-Dodson): peaks at 0.7, Gaussian sigma=0.3
|
|
4
|
+
return Math.exp(-Math.pow(arousal - 0.7, 2) / (2 * 0.3 * 0.3));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function affectSimilarity(a, b) {
|
|
8
|
+
if (!a || !b) return 0;
|
|
9
|
+
if (a.valence === undefined || b.valence === undefined) return 0;
|
|
10
|
+
const valenceDist = Math.abs(a.valence - b.valence);
|
|
11
|
+
const valenceSim = 1.0 - (valenceDist / 2.0);
|
|
12
|
+
if (a.arousal === undefined || b.arousal === undefined) return valenceSim;
|
|
13
|
+
const arousalSim = 1.0 - Math.abs(a.arousal - b.arousal);
|
|
14
|
+
// Valence is primary (70%), arousal secondary (30%) per Bower 1981
|
|
15
|
+
return 0.7 * valenceSim + 0.3 * arousalSim;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function moodCongruenceModifier(encodingAffect, retrievalMood, weight = 0.2) {
|
|
19
|
+
if (!encodingAffect || !retrievalMood) return 1.0;
|
|
20
|
+
const similarity = affectSimilarity(encodingAffect, retrievalMood);
|
|
21
|
+
if (similarity === 0) return 1.0;
|
|
22
|
+
return 1.0 + (weight * similarity);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function detectResonance(db, embeddingProvider, episodeId, { content, affect }, config = {}) {
|
|
26
|
+
const { enabled = true, k = 5, threshold = 0.5, affectThreshold = 0.6 } = config;
|
|
27
|
+
if (!enabled || !affect || affect.valence === undefined) return [];
|
|
28
|
+
|
|
29
|
+
const vector = await embeddingProvider.embed(content);
|
|
30
|
+
const buffer = embeddingProvider.vectorToBuffer(vector);
|
|
31
|
+
|
|
32
|
+
const matches = db.prepare(`
|
|
33
|
+
SELECT e.*, (1.0 - v.distance) AS similarity
|
|
34
|
+
FROM vec_episodes v
|
|
35
|
+
JOIN episodes e ON e.id = v.id
|
|
36
|
+
WHERE v.embedding MATCH ?
|
|
37
|
+
AND k = ?
|
|
38
|
+
AND e.id != ?
|
|
39
|
+
AND e.superseded_by IS NULL
|
|
40
|
+
`).all(buffer, k, episodeId);
|
|
41
|
+
|
|
42
|
+
const resonances = [];
|
|
43
|
+
for (const match of matches) {
|
|
44
|
+
if (match.similarity < threshold) continue;
|
|
45
|
+
let priorAffect;
|
|
46
|
+
try { priorAffect = JSON.parse(match.affect || '{}'); } catch { continue; }
|
|
47
|
+
if (priorAffect.valence === undefined) continue;
|
|
48
|
+
|
|
49
|
+
const emotionalSimilarity = affectSimilarity(affect, priorAffect);
|
|
50
|
+
if (emotionalSimilarity < affectThreshold) continue;
|
|
51
|
+
|
|
52
|
+
resonances.push({
|
|
53
|
+
priorEpisodeId: match.id,
|
|
54
|
+
priorContent: match.content,
|
|
55
|
+
priorAffect,
|
|
56
|
+
semanticSimilarity: match.similarity,
|
|
57
|
+
emotionalSimilarity,
|
|
58
|
+
timeDeltaDays: Math.floor((Date.now() - new Date(match.created_at).getTime()) / 86400000),
|
|
59
|
+
priorCreatedAt: match.created_at,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return resonances;
|
|
64
|
+
}
|
package/src/audrey.js
CHANGED
|
@@ -10,12 +10,13 @@ import { applyDecay } from './decay.js';
|
|
|
10
10
|
import { rollbackConsolidation, getConsolidationHistory } from './rollback.js';
|
|
11
11
|
import { forgetMemory, forgetByQuery as forgetByQueryFn, purgeMemories } from './forget.js';
|
|
12
12
|
import { introspect as introspectFn } from './introspect.js';
|
|
13
|
-
import { buildContextResolutionPrompt } from './prompts.js';
|
|
13
|
+
import { buildContextResolutionPrompt, buildReflectionPrompt } from './prompts.js';
|
|
14
14
|
import { exportMemories } from './export.js';
|
|
15
15
|
import { importMemories } from './import.js';
|
|
16
16
|
import { suggestConsolidationParams as suggestParamsFn } from './adaptive.js';
|
|
17
17
|
import { reembedAll } from './migrate.js';
|
|
18
18
|
import { applyInterference } from './interference.js';
|
|
19
|
+
import { detectResonance } from './affect.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* @typedef {'direct-observation' | 'told-by-user' | 'tool-result' | 'inference' | 'model-generated'} SourceType
|
|
@@ -28,6 +29,8 @@ import { applyInterference } from './interference.js';
|
|
|
28
29
|
* @property {{ trigger?: string, consequence?: string }} [causal]
|
|
29
30
|
* @property {string[]} [tags]
|
|
30
31
|
* @property {string} [supersedes]
|
|
32
|
+
* @property {Record<string, string>} [context]
|
|
33
|
+
* @property {{ valence?: number, arousal?: number, label?: string }} [affect]
|
|
31
34
|
*
|
|
32
35
|
* @typedef {Object} RecallOptions
|
|
33
36
|
* @property {number} [minConfidence]
|
|
@@ -39,6 +42,8 @@ import { applyInterference } from './interference.js';
|
|
|
39
42
|
* @property {string[]} [sources]
|
|
40
43
|
* @property {string} [after]
|
|
41
44
|
* @property {string} [before]
|
|
45
|
+
* @property {Record<string, string>} [context]
|
|
46
|
+
* @property {{ valence?: number, arousal?: number }} [mood]
|
|
42
47
|
*
|
|
43
48
|
* @typedef {Object} RecallResult
|
|
44
49
|
* @property {string} id
|
|
@@ -92,6 +97,8 @@ export class Audrey extends EventEmitter {
|
|
|
92
97
|
decay = {},
|
|
93
98
|
interference = {},
|
|
94
99
|
context = {},
|
|
100
|
+
affect = {},
|
|
101
|
+
autoReflect = false,
|
|
95
102
|
} = {}) {
|
|
96
103
|
super();
|
|
97
104
|
|
|
@@ -118,6 +125,7 @@ export class Audrey extends EventEmitter {
|
|
|
118
125
|
sourceReliability: confidence.sourceReliability,
|
|
119
126
|
interferenceWeight: interference.weight ?? 0.1,
|
|
120
127
|
contextWeight: context.weight ?? 0.3,
|
|
128
|
+
affectWeight: affect.weight ?? 0.2,
|
|
121
129
|
};
|
|
122
130
|
this.consolidationConfig = {
|
|
123
131
|
minEpisodes: consolidation.minEpisodes || 3,
|
|
@@ -134,6 +142,18 @@ export class Audrey extends EventEmitter {
|
|
|
134
142
|
enabled: context.enabled ?? true,
|
|
135
143
|
weight: context.weight ?? 0.3,
|
|
136
144
|
};
|
|
145
|
+
this.affectConfig = {
|
|
146
|
+
enabled: affect.enabled ?? true,
|
|
147
|
+
weight: affect.weight ?? 0.2,
|
|
148
|
+
arousalWeight: affect.arousalWeight ?? 0.3,
|
|
149
|
+
resonance: {
|
|
150
|
+
enabled: affect.resonance?.enabled ?? true,
|
|
151
|
+
k: affect.resonance?.k ?? 5,
|
|
152
|
+
threshold: affect.resonance?.threshold ?? 0.5,
|
|
153
|
+
affectThreshold: affect.resonance?.affectThreshold ?? 0.6,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
this.autoReflect = autoReflect;
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
async _ensureMigrated() {
|
|
@@ -173,7 +193,8 @@ export class Audrey extends EventEmitter {
|
|
|
173
193
|
*/
|
|
174
194
|
async encode(params) {
|
|
175
195
|
await this._ensureMigrated();
|
|
176
|
-
const
|
|
196
|
+
const encodeParams = { ...params, arousalWeight: this.affectConfig.arousalWeight };
|
|
197
|
+
const id = await encodeEpisode(this.db, this.embeddingProvider, encodeParams);
|
|
177
198
|
this.emit('encode', { id, ...params });
|
|
178
199
|
if (this.interferenceConfig.enabled) {
|
|
179
200
|
applyInterference(this.db, this.embeddingProvider, id, params, this.interferenceConfig)
|
|
@@ -184,10 +205,61 @@ export class Audrey extends EventEmitter {
|
|
|
184
205
|
})
|
|
185
206
|
.catch(err => this.emit('error', err));
|
|
186
207
|
}
|
|
208
|
+
if (this.affectConfig.enabled && this.affectConfig.resonance.enabled && params.affect?.valence !== undefined) {
|
|
209
|
+
detectResonance(this.db, this.embeddingProvider, id, params, this.affectConfig.resonance)
|
|
210
|
+
.then(echoes => {
|
|
211
|
+
if (echoes.length > 0) {
|
|
212
|
+
this.emit('resonance', { episodeId: id, affect: params.affect, echoes });
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
.catch(err => this.emit('error', err));
|
|
216
|
+
}
|
|
187
217
|
this._emitValidation(id, params);
|
|
188
218
|
return id;
|
|
189
219
|
}
|
|
190
220
|
|
|
221
|
+
|
|
222
|
+
async reflect(turns) {
|
|
223
|
+
if (!this.llmProvider) return { encoded: 0, memories: [], skipped: 'no llm provider' };
|
|
224
|
+
|
|
225
|
+
const prompt = buildReflectionPrompt(turns);
|
|
226
|
+
let raw;
|
|
227
|
+
try {
|
|
228
|
+
raw = await this.llmProvider.chat(prompt);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
this.emit('error', err);
|
|
231
|
+
return { encoded: 0, memories: [], skipped: 'llm error' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let parsed;
|
|
235
|
+
try {
|
|
236
|
+
parsed = JSON.parse(raw);
|
|
237
|
+
} catch {
|
|
238
|
+
return { encoded: 0, memories: [], skipped: 'invalid llm response' };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const memories = parsed.memories ?? [];
|
|
242
|
+
let encoded = 0;
|
|
243
|
+
for (const mem of memories) {
|
|
244
|
+
if (!mem.content || !mem.source) continue;
|
|
245
|
+
try {
|
|
246
|
+
await this.encode({
|
|
247
|
+
content: mem.content,
|
|
248
|
+
source: mem.source,
|
|
249
|
+
salience: mem.salience,
|
|
250
|
+
tags: mem.tags,
|
|
251
|
+
private: mem.private ?? false,
|
|
252
|
+
affect: mem.affect ?? undefined,
|
|
253
|
+
});
|
|
254
|
+
encoded++;
|
|
255
|
+
} catch (err) {
|
|
256
|
+
this.emit('error', err);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { encoded, memories };
|
|
261
|
+
}
|
|
262
|
+
|
|
191
263
|
/**
|
|
192
264
|
* @param {EncodeParams[]} paramsList
|
|
193
265
|
* @returns {Promise<string[]>}
|
|
@@ -235,10 +307,14 @@ export class Audrey extends EventEmitter {
|
|
|
235
307
|
}
|
|
236
308
|
|
|
237
309
|
_recallConfig(options) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
310
|
+
let config = options.confidenceConfig ?? this.confidenceConfig;
|
|
311
|
+
if (this.contextConfig.enabled && options.context) {
|
|
312
|
+
config = { ...config, retrievalContext: options.context };
|
|
313
|
+
}
|
|
314
|
+
if (this.affectConfig.enabled && options.mood) {
|
|
315
|
+
config = { ...config, retrievalMood: options.mood };
|
|
316
|
+
}
|
|
317
|
+
return config;
|
|
242
318
|
}
|
|
243
319
|
|
|
244
320
|
/**
|