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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Audrey
2
2
 
3
- Biological memory architecture for AI agents. Gives agents cognitive memory that decays, consolidates, self-validates, and learns from experience — not just a database.
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 | Salience Scorer | Importance weighting for retention priority |
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', { limit: 5 });
79
- // Returns: [{ content, type, confidence, score, ... }]
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 # 278 tests across 23 files
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.6.0 — Filtered Recall + Forget (current)
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
@@ -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.8.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 embProvider = process.env.AUDREY_EMBEDDING_PROVIDER || 'mock';
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 config = {
16
- dataDir,
17
- agent,
18
- embedding: { provider: embProvider, dimensions: embDimensions },
19
- };
35
+ const embedding = resolveEmbeddingProvider(process.env, explicitProvider);
20
36
 
21
- if (embProvider === 'openai') {
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
- if (env.OPENAI_API_KEY) {
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) {
@@ -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.8.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 id = await encodeEpisode(this.db, this.embeddingProvider, params);
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
- const base = options.confidenceConfig ?? this.confidenceConfig;
239
- return this.contextConfig.enabled && options.context
240
- ? { ...base, retrievalContext: options.context }
241
- : base;
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
  /**