audrey 0.5.1 → 0.8.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 +143 -128
- package/mcp-server/config.js +1 -1
- package/mcp-server/index.js +65 -5
- package/package.json +1 -1
- package/src/audrey.js +57 -2
- package/src/confidence.js +10 -2
- package/src/consolidate.js +4 -2
- package/src/context.js +15 -0
- package/src/db.js +7 -2
- package/src/decay.js +12 -5
- package/src/encode.js +4 -2
- package/src/forget.js +111 -0
- package/src/index.js +4 -1
- package/src/interference.js +51 -0
- package/src/recall.js +66 -32
package/README.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Biological memory architecture for AI agents. Gives agents cognitive memory that decays, consolidates, self-validates, and learns from experience — not just a database.
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
## Why Audrey Exists
|
|
7
6
|
|
|
8
7
|
Every AI memory tool today (Mem0, Zep, LangChain Memory) is a filing cabinet. Store stuff, retrieve stuff. None of them do what biological memory actually does:
|
|
@@ -46,7 +45,7 @@ npx audrey status
|
|
|
46
45
|
npx audrey uninstall
|
|
47
46
|
```
|
|
48
47
|
|
|
49
|
-
Every Claude Code session now has
|
|
48
|
+
Every Claude Code session now has 9 memory tools: `memory_encode`, `memory_recall`, `memory_consolidate`, `memory_introspect`, `memory_resolve_truth`, `memory_export`, `memory_import`, `memory_forget`, `memory_decay`.
|
|
50
49
|
|
|
51
50
|
### SDK in Your Code
|
|
52
51
|
|
|
@@ -79,14 +78,26 @@ await brain.encode({
|
|
|
79
78
|
const memories = await brain.recall('stripe rate limits', { limit: 5 });
|
|
80
79
|
// Returns: [{ content, type, confidence, score, ... }]
|
|
81
80
|
|
|
82
|
-
// 4.
|
|
81
|
+
// 4. Filtered recall — by tag, source, or date range
|
|
82
|
+
const recent = await brain.recall('stripe', {
|
|
83
|
+
tags: ['rate-limit'],
|
|
84
|
+
sources: ['direct-observation'],
|
|
85
|
+
after: '2026-02-01T00:00:00Z',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 5. Consolidate episodes into principles (the "sleep" cycle)
|
|
83
89
|
await brain.consolidate();
|
|
84
90
|
|
|
85
|
-
//
|
|
91
|
+
// 6. Forget something
|
|
92
|
+
brain.forget(memoryId); // soft-delete
|
|
93
|
+
brain.forget(memoryId, { purge: true }); // hard-delete
|
|
94
|
+
await brain.forgetByQuery('old API endpoint', { minSimilarity: 0.9 });
|
|
95
|
+
|
|
96
|
+
// 7. Check brain health
|
|
86
97
|
const stats = brain.introspect();
|
|
87
98
|
// { episodic: 47, semantic: 12, procedural: 3, dormant: 8, ... }
|
|
88
99
|
|
|
89
|
-
//
|
|
100
|
+
// 8. Clean up
|
|
90
101
|
brain.close();
|
|
91
102
|
```
|
|
92
103
|
|
|
@@ -219,6 +230,16 @@ Context-dependent truths are modeled explicitly:
|
|
|
219
230
|
|
|
220
231
|
New high-confidence evidence can reopen resolved disputes.
|
|
221
232
|
|
|
233
|
+
### Forget and Purge
|
|
234
|
+
|
|
235
|
+
Memories can be explicitly forgotten — by ID or by semantic query:
|
|
236
|
+
|
|
237
|
+
**Soft-delete** (default) — Marks the memory as forgotten/superseded and removes its vector index. The record stays in the database but is excluded from recall. Reversible via direct database access.
|
|
238
|
+
|
|
239
|
+
**Hard-delete** (`purge: true`) — Permanently removes the memory from both the main table and the vector index. Irreversible.
|
|
240
|
+
|
|
241
|
+
**Bulk purge** — Removes all forgotten, dormant, superseded, and rolled-back memories in one operation. Useful for GDPR compliance or storage cleanup.
|
|
242
|
+
|
|
222
243
|
### Rollback
|
|
223
244
|
|
|
224
245
|
Bad consolidation? Undo it:
|
|
@@ -268,20 +289,38 @@ const id = await brain.encode({
|
|
|
268
289
|
|
|
269
290
|
Episodes are **immutable**. Corrections create new records with `supersedes` links. The original is preserved.
|
|
270
291
|
|
|
292
|
+
### `brain.encodeBatch(paramsList)` → `Promise<string[]>`
|
|
293
|
+
|
|
294
|
+
Encode multiple episodes in one call. Same params as `encode()`, but as an array.
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
const ids = await brain.encodeBatch([
|
|
298
|
+
{ content: 'Stripe returned 429', source: 'direct-observation' },
|
|
299
|
+
{ content: 'Redis timed out', source: 'tool-result' },
|
|
300
|
+
{ content: 'User reports slow checkout', source: 'told-by-user' },
|
|
301
|
+
]);
|
|
302
|
+
```
|
|
303
|
+
|
|
271
304
|
### `brain.recall(query, options)` → `Promise<Memory[]>`
|
|
272
305
|
|
|
273
306
|
Retrieve memories ranked by `similarity * confidence`.
|
|
274
307
|
|
|
275
308
|
```js
|
|
276
309
|
const memories = await brain.recall('stripe rate limits', {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
includeProvenance: true,
|
|
281
|
-
includeDormant: false,
|
|
310
|
+
limit: 5, // Max results (default 10)
|
|
311
|
+
minConfidence: 0.5, // Filter below this confidence
|
|
312
|
+
types: ['semantic'], // Filter by memory type
|
|
313
|
+
includeProvenance: true, // Include evidence chains
|
|
314
|
+
includeDormant: false, // Include dormant memories
|
|
315
|
+
tags: ['rate-limit'], // Only episodic memories with these tags
|
|
316
|
+
sources: ['direct-observation'], // Only episodic memories from these sources
|
|
317
|
+
after: '2026-02-01T00:00:00Z', // Only memories created after this date
|
|
318
|
+
before: '2026-03-01T00:00:00Z', // Only memories created before this date
|
|
282
319
|
});
|
|
283
320
|
```
|
|
284
321
|
|
|
322
|
+
Tag and source filters only apply to episodic memories (semantic and procedural memories don't have tags or sources). Date filters apply to all memory types.
|
|
323
|
+
|
|
285
324
|
Each result:
|
|
286
325
|
|
|
287
326
|
```js
|
|
@@ -304,21 +343,9 @@ Each result:
|
|
|
304
343
|
|
|
305
344
|
Retrieval automatically reinforces matched memories (boosts confidence, resets decay clock).
|
|
306
345
|
|
|
307
|
-
### `brain.encodeBatch(paramsList)` → `Promise<string[]>`
|
|
308
|
-
|
|
309
|
-
Encode multiple episodes in one call. Same params as `encode()`, but as an array.
|
|
310
|
-
|
|
311
|
-
```js
|
|
312
|
-
const ids = await brain.encodeBatch([
|
|
313
|
-
{ content: 'Stripe returned 429', source: 'direct-observation' },
|
|
314
|
-
{ content: 'Redis timed out', source: 'tool-result' },
|
|
315
|
-
{ content: 'User reports slow checkout', source: 'told-by-user' },
|
|
316
|
-
]);
|
|
317
|
-
```
|
|
318
|
-
|
|
319
346
|
### `brain.recallStream(query, options)` → `AsyncGenerator<Memory>`
|
|
320
347
|
|
|
321
|
-
Streaming version of `recall()`. Yields results one at a time. Supports early `break`.
|
|
348
|
+
Streaming version of `recall()`. Yields results one at a time. Supports early `break`. Same options as `recall()`.
|
|
322
349
|
|
|
323
350
|
```js
|
|
324
351
|
for await (const memory of brain.recallStream('stripe issues', { limit: 10 })) {
|
|
@@ -327,6 +354,37 @@ for await (const memory of brain.recallStream('stripe issues', { limit: 10 })) {
|
|
|
327
354
|
}
|
|
328
355
|
```
|
|
329
356
|
|
|
357
|
+
### `brain.forget(id, options)` → `ForgetResult`
|
|
358
|
+
|
|
359
|
+
Forget a memory by ID. Works on any memory type (episodic, semantic, procedural).
|
|
360
|
+
|
|
361
|
+
```js
|
|
362
|
+
brain.forget(memoryId); // soft-delete
|
|
363
|
+
brain.forget(memoryId, { purge: true }); // hard-delete (permanent)
|
|
364
|
+
// { id, type: 'episodic', purged: false }
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### `brain.forgetByQuery(query, options)` → `Promise<ForgetResult | null>`
|
|
368
|
+
|
|
369
|
+
Find the closest matching memory by semantic search and forget it. Searches all three memory types, picks the best match.
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
const result = await brain.forgetByQuery('old API endpoint', {
|
|
373
|
+
minSimilarity: 0.9, // Threshold for match (default 0.9)
|
|
374
|
+
purge: false, // Hard-delete? (default false)
|
|
375
|
+
});
|
|
376
|
+
// null if no match above threshold
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### `brain.purge()` → `PurgeCounts`
|
|
380
|
+
|
|
381
|
+
Bulk hard-delete all dead memories: forgotten episodes, dormant/superseded/rolled-back semantics and procedures.
|
|
382
|
+
|
|
383
|
+
```js
|
|
384
|
+
const counts = brain.purge();
|
|
385
|
+
// { episodes: 12, semantics: 3, procedures: 0 }
|
|
386
|
+
```
|
|
387
|
+
|
|
330
388
|
### `brain.consolidate(options)` → `Promise<ConsolidationResult>`
|
|
331
389
|
|
|
332
390
|
Run the consolidation engine manually.
|
|
@@ -389,6 +447,15 @@ brain.introspect();
|
|
|
389
447
|
|
|
390
448
|
Full audit trail of all consolidation runs.
|
|
391
449
|
|
|
450
|
+
### `brain.export()` / `brain.import(snapshot)`
|
|
451
|
+
|
|
452
|
+
Export all memories as a JSON snapshot, or import from one.
|
|
453
|
+
|
|
454
|
+
```js
|
|
455
|
+
const snapshot = brain.export(); // { version, episodes, semantics, procedures, ... }
|
|
456
|
+
await brain.import(snapshot); // Re-embeds everything with current provider
|
|
457
|
+
```
|
|
458
|
+
|
|
392
459
|
### Events
|
|
393
460
|
|
|
394
461
|
```js
|
|
@@ -398,6 +465,8 @@ brain.on('contradiction', ({ episodeId, contradictionId, semanticId, resolution
|
|
|
398
465
|
brain.on('consolidation', ({ runId, principlesExtracted }) => { ... });
|
|
399
466
|
brain.on('decay', ({ totalEvaluated, transitionedToDormant }) => { ... });
|
|
400
467
|
brain.on('rollback', ({ runId, rolledBackMemories }) => { ... });
|
|
468
|
+
brain.on('forget', ({ id, type, purged }) => { ... });
|
|
469
|
+
brain.on('purge', ({ episodes, semantics, procedures }) => { ... });
|
|
401
470
|
brain.on('migration', ({ episodes, semantics, procedures }) => { ... });
|
|
402
471
|
brain.on('error', (err) => { ... });
|
|
403
472
|
```
|
|
@@ -410,7 +479,7 @@ Close the database connection.
|
|
|
410
479
|
|
|
411
480
|
```
|
|
412
481
|
audrey-data/
|
|
413
|
-
audrey.db
|
|
482
|
+
audrey.db <- Single SQLite file. WAL mode. That's your brain.
|
|
414
483
|
```
|
|
415
484
|
|
|
416
485
|
```
|
|
@@ -418,15 +487,16 @@ src/
|
|
|
418
487
|
audrey.js Main class. EventEmitter. Public API surface.
|
|
419
488
|
causal.js Causal graph management. LLM-powered mechanism articulation.
|
|
420
489
|
confidence.js Compositional confidence formula. Pure math.
|
|
421
|
-
consolidate.js "Sleep" cycle. KNN clustering
|
|
490
|
+
consolidate.js "Sleep" cycle. KNN clustering -> LLM extraction -> promote.
|
|
422
491
|
db.js SQLite + sqlite-vec. Schema, vec0 tables, migrations.
|
|
423
492
|
decay.js Ebbinghaus forgetting curves.
|
|
424
493
|
embedding.js Pluggable providers (Mock, OpenAI). Batch embedding.
|
|
425
494
|
encode.js Immutable episodic memory creation + vec0 writes.
|
|
495
|
+
forget.js Soft-delete, hard-delete, query-based forget, bulk purge.
|
|
426
496
|
introspect.js Health dashboard queries.
|
|
427
497
|
llm.js Pluggable LLM providers (Mock, Anthropic, OpenAI).
|
|
428
498
|
prompts.js Structured prompt templates for LLM operations.
|
|
429
|
-
recall.js KNN retrieval + confidence scoring +
|
|
499
|
+
recall.js KNN retrieval + confidence scoring + filtered recall + streaming.
|
|
430
500
|
rollback.js Undo consolidation runs.
|
|
431
501
|
utils.js Date math, safe JSON parse.
|
|
432
502
|
validate.js KNN validation + LLM contradiction detection.
|
|
@@ -437,7 +507,7 @@ src/
|
|
|
437
507
|
index.js Barrel export.
|
|
438
508
|
|
|
439
509
|
mcp-server/
|
|
440
|
-
index.js MCP tool server (
|
|
510
|
+
index.js MCP tool server (9 tools, stdio transport) + CLI subcommands.
|
|
441
511
|
config.js Shared config (env var parsing, install arg builder).
|
|
442
512
|
```
|
|
443
513
|
|
|
@@ -461,7 +531,7 @@ All mutations use SQLite transactions. CHECK constraints enforce valid states an
|
|
|
461
531
|
## Running Tests
|
|
462
532
|
|
|
463
533
|
```bash
|
|
464
|
-
npm test #
|
|
534
|
+
npm test # 278 tests across 23 files
|
|
465
535
|
npm run test:watch
|
|
466
536
|
```
|
|
467
537
|
|
|
@@ -471,115 +541,60 @@ npm run test:watch
|
|
|
471
541
|
node examples/stripe-demo.js
|
|
472
542
|
```
|
|
473
543
|
|
|
474
|
-
Demonstrates the full pipeline: encode 3 rate-limit observations
|
|
544
|
+
Demonstrates the full pipeline: encode 3 rate-limit observations, consolidate into principle, recall proactively.
|
|
475
545
|
|
|
476
546
|
---
|
|
477
547
|
|
|
478
|
-
##
|
|
548
|
+
## Changelog
|
|
479
549
|
|
|
480
|
-
### v0.
|
|
550
|
+
### v0.6.0 — Filtered Recall + Forget (current)
|
|
481
551
|
|
|
482
|
-
-
|
|
483
|
-
-
|
|
484
|
-
-
|
|
485
|
-
-
|
|
486
|
-
-
|
|
487
|
-
-
|
|
488
|
-
- [x] Retrieval reinforcement (frequently accessed memories resist decay)
|
|
489
|
-
- [x] Consolidation engine with clustering and principle extraction
|
|
490
|
-
- [x] Idempotent consolidation with checkpoint cursors
|
|
491
|
-
- [x] Full consolidation audit trail (input/output IDs per run)
|
|
492
|
-
- [x] Consolidation rollback (undo bad runs, restore episodes)
|
|
493
|
-
- [x] Contradiction lifecycle (open/resolved/context_dependent/reopened)
|
|
494
|
-
- [x] Circular self-confirmation defense (model-generated cap at 0.6)
|
|
495
|
-
- [x] Source type diversity tracking on semantic memories
|
|
496
|
-
- [x] Supersedes links for correcting episodic memories
|
|
497
|
-
- [x] Pluggable embedding providers (Mock for tests, OpenAI for production)
|
|
498
|
-
- [x] Causal context storage (trigger/consequence per episode)
|
|
499
|
-
- [x] Introspection API (memory counts, contradiction stats, consolidation history)
|
|
500
|
-
- [x] EventEmitter lifecycle hooks (encode, reinforcement, consolidation, decay, rollback, error)
|
|
501
|
-
- [x] SQLite with WAL mode, CHECK constraints, indexes, foreign keys
|
|
502
|
-
- [x] Transaction safety on all multi-step mutations
|
|
503
|
-
- [x] Input validation on public API (content, salience, tags, source)
|
|
504
|
-
- [x] Shared utility extraction (cosine similarity, date math, safe JSON parse)
|
|
505
|
-
- [x] 104 tests across 12 test files
|
|
506
|
-
- [x] Proof-of-concept demo (Stripe rate limit scenario)
|
|
552
|
+
- Filtered recall: tag, source, and date-range filters on `recall()` and `recallStream()`
|
|
553
|
+
- `forget()` — soft-delete any memory by ID
|
|
554
|
+
- `forgetByQuery()` — find closest match by semantic search and forget it
|
|
555
|
+
- `purge()` — bulk hard-delete all forgotten/dormant/superseded memories
|
|
556
|
+
- `memory_forget` and `memory_decay` MCP tools (9 tools total)
|
|
557
|
+
- 278 tests across 23 files
|
|
507
558
|
|
|
508
|
-
### v0.
|
|
559
|
+
### v0.5.0 — Feature Depth
|
|
509
560
|
|
|
510
|
-
-
|
|
511
|
-
-
|
|
512
|
-
-
|
|
513
|
-
-
|
|
514
|
-
-
|
|
515
|
-
-
|
|
516
|
-
|
|
517
|
-
|
|
561
|
+
- Configurable confidence weights and decay rates per instance
|
|
562
|
+
- Memory export/import (JSON snapshots with re-embedding)
|
|
563
|
+
- `memory_export` and `memory_import` MCP tools
|
|
564
|
+
- Auto-consolidation scheduling
|
|
565
|
+
- Adaptive consolidation parameter suggestions
|
|
566
|
+
- 243 tests across 22 files
|
|
567
|
+
|
|
568
|
+
### v0.3.1 — MCP Server
|
|
569
|
+
|
|
570
|
+
- MCP tool server via `@modelcontextprotocol/sdk` with stdio transport
|
|
571
|
+
- One-command install: `npx audrey install` (auto-detects API keys)
|
|
572
|
+
- CLI subcommands: `install`, `uninstall`, `status`
|
|
573
|
+
- JSDoc type annotations on all public exports
|
|
574
|
+
- Published to npm
|
|
575
|
+
- 194 tests across 17 files
|
|
518
576
|
|
|
519
577
|
### v0.3.0 — Vector Performance
|
|
520
578
|
|
|
521
|
-
-
|
|
522
|
-
-
|
|
523
|
-
-
|
|
524
|
-
-
|
|
525
|
-
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
-
|
|
537
|
-
-
|
|
538
|
-
-
|
|
539
|
-
-
|
|
540
|
-
|
|
541
|
-
### v0.5.0 — Feature Depth (current)
|
|
542
|
-
|
|
543
|
-
- [x] Configurable confidence weights per Audrey instance
|
|
544
|
-
- [x] Configurable decay rates (half-lives) per Audrey instance
|
|
545
|
-
- [x] Confidence config wired through constructor to recall and decay
|
|
546
|
-
- [x] Memory export (JSON snapshot of all tables, no raw embeddings)
|
|
547
|
-
- [x] Memory import with automatic re-embedding via current provider
|
|
548
|
-
- [x] `memory_export` and `memory_import` MCP tools (7 tools total)
|
|
549
|
-
- [x] Auto-consolidation scheduling (`startAutoConsolidate` / `stopAutoConsolidate`)
|
|
550
|
-
- [x] Consolidation metrics tracking (per-run params and results)
|
|
551
|
-
- [x] Adaptive consolidation parameter suggestions based on historical yield
|
|
552
|
-
- [x] 243 tests across 22 test files
|
|
553
|
-
|
|
554
|
-
### v0.4.0 — Type Safety & Developer Experience
|
|
555
|
-
|
|
556
|
-
- [ ] Full TypeScript conversion with strict mode
|
|
557
|
-
- [ ] Published type declarations (.d.ts)
|
|
558
|
-
- [ ] Schema versioning and migration system
|
|
559
|
-
- [ ] Structured logging (optional, pluggable)
|
|
560
|
-
|
|
561
|
-
### v0.4.5 — Embedding Migration (deferred from v0.3.0)
|
|
562
|
-
|
|
563
|
-
- [ ] Embedding migration pipeline (re-embed when models change)
|
|
564
|
-
- [ ] Re-consolidation queue (re-run consolidation with new embedding model)
|
|
565
|
-
|
|
566
|
-
### v0.6.0 — Scale
|
|
567
|
-
|
|
568
|
-
- [ ] pgvector adapter for PostgreSQL backend
|
|
569
|
-
- [ ] Redis adapter for distributed caching
|
|
570
|
-
- [ ] Connection pooling for concurrent agent access
|
|
571
|
-
- [ ] Pagination on recall queries (cursor-based)
|
|
572
|
-
- [ ] Benchmarks: encode throughput, recall latency at 10k/100k/1M memories
|
|
573
|
-
|
|
574
|
-
### v1.0.0 — Production Ready
|
|
575
|
-
|
|
576
|
-
- [ ] Comprehensive error handling at all boundaries
|
|
577
|
-
- [ ] Rate limiting on embedding API calls
|
|
578
|
-
- [ ] Memory usage profiling and optimization
|
|
579
|
-
- [ ] Security audit (injection, data isolation)
|
|
580
|
-
- [ ] Cross-agent knowledge sharing protocol (Hivemind)
|
|
581
|
-
- [ ] Documentation site
|
|
582
|
-
- [ ] Integration guides (LangChain, CrewAI, Claude Code, custom agents)
|
|
579
|
+
- sqlite-vec native vector indexing (vec0 virtual tables with cosine distance)
|
|
580
|
+
- KNN queries for recall, validation, and consolidation clustering
|
|
581
|
+
- Batch encoding API and streaming recall with async generators
|
|
582
|
+
- Dimension configuration and automatic migration from v0.2.0
|
|
583
|
+
- 168 tests across 16 files
|
|
584
|
+
|
|
585
|
+
### v0.2.0 — LLM Integration
|
|
586
|
+
|
|
587
|
+
- LLM-powered principle extraction, contradiction detection, causal articulation
|
|
588
|
+
- Context-dependent truth resolution
|
|
589
|
+
- Configurable LLM providers (Mock, Anthropic, OpenAI)
|
|
590
|
+
- 142 tests across 15 files
|
|
591
|
+
|
|
592
|
+
### v0.1.0 — Foundation
|
|
593
|
+
|
|
594
|
+
- Immutable episodic memory, compositional confidence, Ebbinghaus forgetting curves
|
|
595
|
+
- Consolidation engine, contradiction lifecycle, rollback
|
|
596
|
+
- Circular self-confirmation defense, causal context, introspection
|
|
597
|
+
- 104 tests across 12 files
|
|
583
598
|
|
|
584
599
|
## Design Decisions
|
|
585
600
|
|
|
@@ -591,7 +606,7 @@ Demonstrates the full pipeline: encode 3 rate-limit observations → consolidate
|
|
|
591
606
|
|
|
592
607
|
**Why model-generated cap at 0.6?** Prevents the most dangerous exploit in AI memory: circular self-confirmation where an agent's own inferences bootstrap themselves into high-confidence "facts" through repeated retrieval.
|
|
593
608
|
|
|
594
|
-
**Why
|
|
609
|
+
**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).
|
|
595
610
|
|
|
596
611
|
## License
|
|
597
612
|
|
package/mcp-server/config.js
CHANGED
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';
|
|
@@ -65,7 +65,7 @@ function install() {
|
|
|
65
65
|
console.log(`
|
|
66
66
|
Audrey registered as "${SERVER_NAME}" with Claude Code.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
9 tools available in every session:
|
|
69
69
|
memory_encode — Store observations, facts, preferences
|
|
70
70
|
memory_recall — Search memories by semantic similarity
|
|
71
71
|
memory_consolidate — Extract principles from accumulated episodes
|
|
@@ -73,6 +73,8 @@ Audrey registered as "${SERVER_NAME}" with Claude Code.
|
|
|
73
73
|
memory_resolve_truth — Resolve contradictions between claims
|
|
74
74
|
memory_export — Export all memories as JSON snapshot
|
|
75
75
|
memory_import — Import a snapshot into a fresh database
|
|
76
|
+
memory_forget — Forget a specific memory by ID or query
|
|
77
|
+
memory_decay — Apply forgetting curves, transition low-confidence to dormant
|
|
76
78
|
|
|
77
79
|
Data stored in: ${DEFAULT_DATA_DIR}
|
|
78
80
|
Verify: claude mcp list
|
|
@@ -161,10 +163,11 @@ async function main() {
|
|
|
161
163
|
source: z.enum(VALID_SOURCES).describe('Source type of the memory'),
|
|
162
164
|
tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
|
|
163
165
|
salience: z.number().min(0).max(1).optional().describe('Importance weight 0-1'),
|
|
166
|
+
context: z.record(z.string()).optional().describe('Situational context as key-value pairs (e.g., {task: "debugging", domain: "payments"})'),
|
|
164
167
|
},
|
|
165
|
-
async ({ content, source, tags, salience }) => {
|
|
168
|
+
async ({ content, source, tags, salience, context }) => {
|
|
166
169
|
try {
|
|
167
|
-
const id = await audrey.encode({ content, source, tags, salience });
|
|
170
|
+
const id = await audrey.encode({ content, source, tags, salience, context });
|
|
168
171
|
return toolResult({ id, content, source });
|
|
169
172
|
} catch (err) {
|
|
170
173
|
return toolError(err);
|
|
@@ -179,13 +182,23 @@ async function main() {
|
|
|
179
182
|
limit: z.number().min(1).max(50).optional().describe('Max results (default 10)'),
|
|
180
183
|
types: z.array(z.enum(VALID_TYPES)).optional().describe('Memory types to search'),
|
|
181
184
|
min_confidence: z.number().min(0).max(1).optional().describe('Minimum confidence threshold'),
|
|
185
|
+
tags: z.array(z.string()).optional().describe('Only return episodic memories with these tags'),
|
|
186
|
+
sources: z.array(z.enum(VALID_SOURCES)).optional().describe('Only return episodic memories from these sources'),
|
|
187
|
+
after: z.string().optional().describe('Only return memories created after this ISO date'),
|
|
188
|
+
before: z.string().optional().describe('Only return memories created before this ISO date'),
|
|
189
|
+
context: z.record(z.string()).optional().describe('Retrieval context — memories encoded in matching context get boosted'),
|
|
182
190
|
},
|
|
183
|
-
async ({ query, limit, types, min_confidence }) => {
|
|
191
|
+
async ({ query, limit, types, min_confidence, tags, sources, after, before, context }) => {
|
|
184
192
|
try {
|
|
185
193
|
const results = await audrey.recall(query, {
|
|
186
194
|
limit: limit ?? 10,
|
|
187
195
|
types,
|
|
188
196
|
minConfidence: min_confidence,
|
|
197
|
+
tags,
|
|
198
|
+
sources,
|
|
199
|
+
after,
|
|
200
|
+
before,
|
|
201
|
+
context,
|
|
189
202
|
});
|
|
190
203
|
return toolResult(results);
|
|
191
204
|
} catch (err) {
|
|
@@ -279,6 +292,53 @@ async function main() {
|
|
|
279
292
|
},
|
|
280
293
|
);
|
|
281
294
|
|
|
295
|
+
server.tool(
|
|
296
|
+
'memory_forget',
|
|
297
|
+
{
|
|
298
|
+
id: z.string().optional().describe('ID of the memory to forget'),
|
|
299
|
+
query: z.string().optional().describe('Semantic query to find and forget the closest matching memory'),
|
|
300
|
+
min_similarity: z.number().min(0).max(1).optional().describe('Minimum similarity for query-based forget (default 0.9)'),
|
|
301
|
+
purge: z.boolean().optional().describe('Hard-delete the memory permanently (default false, soft-delete)'),
|
|
302
|
+
},
|
|
303
|
+
async ({ id, query, min_similarity, purge }) => {
|
|
304
|
+
try {
|
|
305
|
+
if (!id && !query) {
|
|
306
|
+
return toolError(new Error('Provide either id or query'));
|
|
307
|
+
}
|
|
308
|
+
let result;
|
|
309
|
+
if (id) {
|
|
310
|
+
result = audrey.forget(id, { purge: purge ?? false });
|
|
311
|
+
} else {
|
|
312
|
+
result = await audrey.forgetByQuery(query, {
|
|
313
|
+
minSimilarity: min_similarity ?? 0.9,
|
|
314
|
+
purge: purge ?? false,
|
|
315
|
+
});
|
|
316
|
+
if (!result) {
|
|
317
|
+
return toolResult({ forgotten: false, reason: 'No memory found above similarity threshold' });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return toolResult({ forgotten: true, ...result });
|
|
321
|
+
} catch (err) {
|
|
322
|
+
return toolError(err);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
server.tool(
|
|
328
|
+
'memory_decay',
|
|
329
|
+
{
|
|
330
|
+
dormant_threshold: z.number().min(0).max(1).optional().describe('Confidence below which memories go dormant (default 0.1)'),
|
|
331
|
+
},
|
|
332
|
+
async ({ dormant_threshold }) => {
|
|
333
|
+
try {
|
|
334
|
+
const result = audrey.decay({ dormantThreshold: dormant_threshold });
|
|
335
|
+
return toolResult(result);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
return toolError(err);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
282
342
|
const transport = new StdioServerTransport();
|
|
283
343
|
await server.connect(transport);
|
|
284
344
|
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.8.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",
|
package/src/audrey.js
CHANGED
|
@@ -8,12 +8,14 @@ import { validateMemory } from './validate.js';
|
|
|
8
8
|
import { runConsolidation } from './consolidate.js';
|
|
9
9
|
import { applyDecay } from './decay.js';
|
|
10
10
|
import { rollbackConsolidation, getConsolidationHistory } from './rollback.js';
|
|
11
|
+
import { forgetMemory, forgetByQuery as forgetByQueryFn, purgeMemories } from './forget.js';
|
|
11
12
|
import { introspect as introspectFn } from './introspect.js';
|
|
12
13
|
import { buildContextResolutionPrompt } from './prompts.js';
|
|
13
14
|
import { exportMemories } from './export.js';
|
|
14
15
|
import { importMemories } from './import.js';
|
|
15
16
|
import { suggestConsolidationParams as suggestParamsFn } from './adaptive.js';
|
|
16
17
|
import { reembedAll } from './migrate.js';
|
|
18
|
+
import { applyInterference } from './interference.js';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* @typedef {'direct-observation' | 'told-by-user' | 'tool-result' | 'inference' | 'model-generated'} SourceType
|
|
@@ -33,6 +35,10 @@ import { reembedAll } from './migrate.js';
|
|
|
33
35
|
* @property {number} [limit]
|
|
34
36
|
* @property {boolean} [includeProvenance]
|
|
35
37
|
* @property {boolean} [includeDormant]
|
|
38
|
+
* @property {string[]} [tags]
|
|
39
|
+
* @property {string[]} [sources]
|
|
40
|
+
* @property {string} [after]
|
|
41
|
+
* @property {string} [before]
|
|
36
42
|
*
|
|
37
43
|
* @typedef {Object} RecallResult
|
|
38
44
|
* @property {string} id
|
|
@@ -84,6 +90,8 @@ export class Audrey extends EventEmitter {
|
|
|
84
90
|
confidence = {},
|
|
85
91
|
consolidation = {},
|
|
86
92
|
decay = {},
|
|
93
|
+
interference = {},
|
|
94
|
+
context = {},
|
|
87
95
|
} = {}) {
|
|
88
96
|
super();
|
|
89
97
|
|
|
@@ -108,12 +116,24 @@ export class Audrey extends EventEmitter {
|
|
|
108
116
|
weights: confidence.weights,
|
|
109
117
|
halfLives: confidence.halfLives,
|
|
110
118
|
sourceReliability: confidence.sourceReliability,
|
|
119
|
+
interferenceWeight: interference.weight ?? 0.1,
|
|
120
|
+
contextWeight: context.weight ?? 0.3,
|
|
111
121
|
};
|
|
112
122
|
this.consolidationConfig = {
|
|
113
123
|
minEpisodes: consolidation.minEpisodes || 3,
|
|
114
124
|
};
|
|
115
125
|
this.decayConfig = { dormantThreshold: decay.dormantThreshold || 0.1 };
|
|
116
126
|
this._autoConsolidateTimer = null;
|
|
127
|
+
this.interferenceConfig = {
|
|
128
|
+
enabled: interference.enabled ?? true,
|
|
129
|
+
k: interference.k ?? 5,
|
|
130
|
+
threshold: interference.threshold ?? 0.6,
|
|
131
|
+
weight: interference.weight ?? 0.1,
|
|
132
|
+
};
|
|
133
|
+
this.contextConfig = {
|
|
134
|
+
enabled: context.enabled ?? true,
|
|
135
|
+
weight: context.weight ?? 0.3,
|
|
136
|
+
};
|
|
117
137
|
}
|
|
118
138
|
|
|
119
139
|
async _ensureMigrated() {
|
|
@@ -155,6 +175,15 @@ export class Audrey extends EventEmitter {
|
|
|
155
175
|
await this._ensureMigrated();
|
|
156
176
|
const id = await encodeEpisode(this.db, this.embeddingProvider, params);
|
|
157
177
|
this.emit('encode', { id, ...params });
|
|
178
|
+
if (this.interferenceConfig.enabled) {
|
|
179
|
+
applyInterference(this.db, this.embeddingProvider, id, params, this.interferenceConfig)
|
|
180
|
+
.then(affected => {
|
|
181
|
+
if (affected.length > 0) {
|
|
182
|
+
this.emit('interference', { episodeId: id, affected });
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
.catch(err => this.emit('error', err));
|
|
186
|
+
}
|
|
158
187
|
this._emitValidation(id, params);
|
|
159
188
|
return id;
|
|
160
189
|
}
|
|
@@ -188,7 +217,7 @@ export class Audrey extends EventEmitter {
|
|
|
188
217
|
await this._ensureMigrated();
|
|
189
218
|
return recallFn(this.db, this.embeddingProvider, query, {
|
|
190
219
|
...options,
|
|
191
|
-
confidenceConfig:
|
|
220
|
+
confidenceConfig: this._recallConfig(options),
|
|
192
221
|
});
|
|
193
222
|
}
|
|
194
223
|
|
|
@@ -201,10 +230,17 @@ export class Audrey extends EventEmitter {
|
|
|
201
230
|
await this._ensureMigrated();
|
|
202
231
|
yield* recallStreamFn(this.db, this.embeddingProvider, query, {
|
|
203
232
|
...options,
|
|
204
|
-
confidenceConfig:
|
|
233
|
+
confidenceConfig: this._recallConfig(options),
|
|
205
234
|
});
|
|
206
235
|
}
|
|
207
236
|
|
|
237
|
+
_recallConfig(options) {
|
|
238
|
+
const base = options.confidenceConfig ?? this.confidenceConfig;
|
|
239
|
+
return this.contextConfig.enabled && options.context
|
|
240
|
+
? { ...base, retrievalContext: options.context }
|
|
241
|
+
: base;
|
|
242
|
+
}
|
|
243
|
+
|
|
208
244
|
/**
|
|
209
245
|
* @param {{ minClusterSize?: number, similarityThreshold?: number, extractPrinciple?: Function, llmProvider?: import('./llm.js').LLMProvider }} [options]
|
|
210
246
|
* @returns {Promise<ConsolidationResult>}
|
|
@@ -343,6 +379,25 @@ export class Audrey extends EventEmitter {
|
|
|
343
379
|
return suggestParamsFn(this.db);
|
|
344
380
|
}
|
|
345
381
|
|
|
382
|
+
forget(id, options = {}) {
|
|
383
|
+
const result = forgetMemory(this.db, id, options);
|
|
384
|
+
this.emit('forget', result);
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async forgetByQuery(query, options = {}) {
|
|
389
|
+
await this._ensureMigrated();
|
|
390
|
+
const result = await forgetByQueryFn(this.db, this.embeddingProvider, query, options);
|
|
391
|
+
if (result) this.emit('forget', result);
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
purge() {
|
|
396
|
+
const result = purgeMemories(this.db);
|
|
397
|
+
this.emit('purge', result);
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
|
|
346
401
|
/** @returns {void} */
|
|
347
402
|
close() {
|
|
348
403
|
this.stopAutoConsolidate();
|
package/src/confidence.js
CHANGED
|
@@ -67,8 +67,16 @@ export function recencyDecay(ageDays, halfLifeDays) {
|
|
|
67
67
|
*/
|
|
68
68
|
export function retrievalReinforcement(retrievalCount, daysSinceRetrieval) {
|
|
69
69
|
if (retrievalCount === 0) return 0;
|
|
70
|
-
const lambdaRet = Math.LN2 / 14;
|
|
71
|
-
|
|
70
|
+
const lambdaRet = Math.LN2 / 14;
|
|
71
|
+
const baseReinforcement = 0.3 * Math.log(1 + retrievalCount);
|
|
72
|
+
const recencyWeight = Math.exp(-lambdaRet * daysSinceRetrieval);
|
|
73
|
+
const spacedBonus = Math.min(0.15, 0.02 * Math.log(1 + daysSinceRetrieval));
|
|
74
|
+
return Math.min(1.0, baseReinforcement * recencyWeight + spacedBonus);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function salienceModifier(salience) {
|
|
78
|
+
const s = salience ?? 0.5;
|
|
79
|
+
return 0.5 + s;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
/**
|
package/src/consolidate.js
CHANGED
|
@@ -152,6 +152,7 @@ export async function runConsolidation(db, embeddingProvider, options = {}) {
|
|
|
152
152
|
embeddingBuffer,
|
|
153
153
|
semanticId: generateId(),
|
|
154
154
|
semanticNow: new Date().toISOString(),
|
|
155
|
+
maxSalience: Math.max(...cluster.map(ep => ep.salience ?? 0.5)),
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
158
|
|
|
@@ -168,8 +169,8 @@ export async function runConsolidation(db, embeddingProvider, options = {}) {
|
|
|
168
169
|
id, content, embedding, state, evidence_episode_ids,
|
|
169
170
|
evidence_count, supporting_count, source_type_diversity,
|
|
170
171
|
consolidation_checkpoint, embedding_model, embedding_version,
|
|
171
|
-
consolidation_model, created_at
|
|
172
|
-
) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
172
|
+
consolidation_model, created_at, salience
|
|
173
|
+
) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
173
174
|
`).run(
|
|
174
175
|
entry.semanticId,
|
|
175
176
|
entry.principle.content,
|
|
@@ -183,6 +184,7 @@ export async function runConsolidation(db, embeddingProvider, options = {}) {
|
|
|
183
184
|
embeddingProvider.modelVersion,
|
|
184
185
|
llmProvider?.modelName || null,
|
|
185
186
|
entry.semanticNow,
|
|
187
|
+
entry.maxSalience,
|
|
186
188
|
);
|
|
187
189
|
|
|
188
190
|
db.prepare('INSERT INTO vec_semantics(id, embedding, state) VALUES (?, ?, ?)').run(
|
package/src/context.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function contextMatchRatio(encodingContext, retrievalContext) {
|
|
2
|
+
if (!encodingContext || !retrievalContext) return 0;
|
|
3
|
+
const retrievalKeys = Object.keys(retrievalContext);
|
|
4
|
+
if (retrievalKeys.length === 0) return 0;
|
|
5
|
+
const sharedKeys = retrievalKeys.filter(k => k in encodingContext);
|
|
6
|
+
if (sharedKeys.length === 0) return 0;
|
|
7
|
+
const matches = sharedKeys.filter(k => encodingContext[k] === retrievalContext[k]).length;
|
|
8
|
+
return matches / retrievalKeys.length;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function contextModifier(encodingContext, retrievalContext, weight = 0.3) {
|
|
12
|
+
if (!encodingContext || !retrievalContext) return 1.0;
|
|
13
|
+
const ratio = contextMatchRatio(encodingContext, retrievalContext);
|
|
14
|
+
return 1.0 + (weight * ratio);
|
|
15
|
+
}
|
package/src/db.js
CHANGED
|
@@ -11,6 +11,7 @@ const SCHEMA = `
|
|
|
11
11
|
source TEXT NOT NULL CHECK(source IN ('direct-observation','told-by-user','tool-result','inference','model-generated')),
|
|
12
12
|
source_reliability REAL NOT NULL,
|
|
13
13
|
salience REAL DEFAULT 0.5,
|
|
14
|
+
context TEXT DEFAULT '{}',
|
|
14
15
|
tags TEXT,
|
|
15
16
|
causal_trigger TEXT,
|
|
16
17
|
causal_consequence TEXT,
|
|
@@ -42,7 +43,9 @@ const SCHEMA = `
|
|
|
42
43
|
created_at TEXT NOT NULL,
|
|
43
44
|
last_reinforced_at TEXT,
|
|
44
45
|
retrieval_count INTEGER DEFAULT 0,
|
|
45
|
-
challenge_count INTEGER DEFAULT 0
|
|
46
|
+
challenge_count INTEGER DEFAULT 0,
|
|
47
|
+
interference_count INTEGER DEFAULT 0,
|
|
48
|
+
salience REAL DEFAULT 0.5
|
|
46
49
|
);
|
|
47
50
|
|
|
48
51
|
CREATE TABLE IF NOT EXISTS procedures (
|
|
@@ -58,7 +61,9 @@ const SCHEMA = `
|
|
|
58
61
|
embedding_version TEXT,
|
|
59
62
|
created_at TEXT NOT NULL,
|
|
60
63
|
last_reinforced_at TEXT,
|
|
61
|
-
retrieval_count INTEGER DEFAULT 0
|
|
64
|
+
retrieval_count INTEGER DEFAULT 0,
|
|
65
|
+
interference_count INTEGER DEFAULT 0,
|
|
66
|
+
salience REAL DEFAULT 0.5
|
|
62
67
|
);
|
|
63
68
|
|
|
64
69
|
CREATE TABLE IF NOT EXISTS causal_links (
|
package/src/decay.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { computeConfidence, DEFAULT_HALF_LIVES } from './confidence.js';
|
|
1
|
+
import { computeConfidence, DEFAULT_HALF_LIVES, salienceModifier } from './confidence.js';
|
|
2
|
+
import { interferenceModifier } from './interference.js';
|
|
2
3
|
import { daysBetween } from './utils.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -13,7 +14,7 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
|
|
|
13
14
|
|
|
14
15
|
const semantics = db.prepare(`
|
|
15
16
|
SELECT id, supporting_count, contradicting_count, created_at,
|
|
16
|
-
last_reinforced_at, retrieval_count
|
|
17
|
+
last_reinforced_at, retrieval_count, interference_count, salience
|
|
17
18
|
FROM semantics WHERE state = 'active'
|
|
18
19
|
`).all();
|
|
19
20
|
|
|
@@ -26,7 +27,7 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
|
|
|
26
27
|
? daysBetween(sem.last_reinforced_at, now)
|
|
27
28
|
: ageDays;
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
let confidence = computeConfidence({
|
|
30
31
|
sourceType: 'tool-result',
|
|
31
32
|
supportingCount: sem.supporting_count || 0,
|
|
32
33
|
contradictingCount: sem.contradicting_count || 0,
|
|
@@ -35,6 +36,9 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
|
|
|
35
36
|
retrievalCount: sem.retrieval_count || 0,
|
|
36
37
|
daysSinceRetrieval,
|
|
37
38
|
});
|
|
39
|
+
confidence *= interferenceModifier(sem.interference_count || 0);
|
|
40
|
+
confidence *= salienceModifier(sem.salience ?? 0.5);
|
|
41
|
+
confidence = Math.max(0, Math.min(1, confidence));
|
|
38
42
|
|
|
39
43
|
if (confidence < dormantThreshold) {
|
|
40
44
|
markDormantSem.run('dormant', sem.id);
|
|
@@ -44,7 +48,7 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
|
|
|
44
48
|
|
|
45
49
|
const procedures = db.prepare(`
|
|
46
50
|
SELECT id, success_count, failure_count, created_at,
|
|
47
|
-
last_reinforced_at, retrieval_count
|
|
51
|
+
last_reinforced_at, retrieval_count, interference_count, salience
|
|
48
52
|
FROM procedures WHERE state = 'active'
|
|
49
53
|
`).all();
|
|
50
54
|
|
|
@@ -57,7 +61,7 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
|
|
|
57
61
|
? daysBetween(proc.last_reinforced_at, now)
|
|
58
62
|
: ageDays;
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
let confidence = computeConfidence({
|
|
61
65
|
sourceType: 'tool-result',
|
|
62
66
|
supportingCount: proc.success_count || 0,
|
|
63
67
|
contradictingCount: proc.failure_count || 0,
|
|
@@ -66,6 +70,9 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
|
|
|
66
70
|
retrievalCount: proc.retrieval_count || 0,
|
|
67
71
|
daysSinceRetrieval,
|
|
68
72
|
});
|
|
73
|
+
confidence *= interferenceModifier(proc.interference_count || 0);
|
|
74
|
+
confidence *= salienceModifier(proc.salience ?? 0.5);
|
|
75
|
+
confidence = Math.max(0, Math.min(1, confidence));
|
|
69
76
|
|
|
70
77
|
if (confidence < dormantThreshold) {
|
|
71
78
|
markDormantProc.run('dormant', proc.id);
|
package/src/encode.js
CHANGED
|
@@ -14,6 +14,7 @@ export async function encodeEpisode(db, embeddingProvider, {
|
|
|
14
14
|
causal,
|
|
15
15
|
tags,
|
|
16
16
|
supersedes,
|
|
17
|
+
context = {},
|
|
17
18
|
}) {
|
|
18
19
|
if (!content || typeof content !== 'string') throw new Error('content must be a non-empty string');
|
|
19
20
|
if (salience < 0 || salience > 1) throw new Error('salience must be between 0 and 1');
|
|
@@ -28,12 +29,13 @@ export async function encodeEpisode(db, embeddingProvider, {
|
|
|
28
29
|
const insertAndLink = db.transaction(() => {
|
|
29
30
|
db.prepare(`
|
|
30
31
|
INSERT INTO episodes (
|
|
31
|
-
id, content, embedding, source, source_reliability, salience,
|
|
32
|
+
id, content, embedding, source, source_reliability, salience, context,
|
|
32
33
|
tags, causal_trigger, causal_consequence, created_at,
|
|
33
34
|
embedding_model, embedding_version, supersedes
|
|
34
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
35
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
35
36
|
`).run(
|
|
36
37
|
id, content, embeddingBuffer, source, reliability, salience,
|
|
38
|
+
JSON.stringify(context),
|
|
37
39
|
tags ? JSON.stringify(tags) : null,
|
|
38
40
|
causal?.trigger || null, causal?.consequence || null,
|
|
39
41
|
now, embeddingProvider.modelName, embeddingProvider.modelVersion,
|
package/src/forget.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export function forgetMemory(db, id, { purge = false } = {}) {
|
|
2
|
+
const episode = db.prepare('SELECT id FROM episodes WHERE id = ?').get(id);
|
|
3
|
+
if (episode) {
|
|
4
|
+
if (purge) {
|
|
5
|
+
db.prepare('DELETE FROM vec_episodes WHERE id = ?').run(id);
|
|
6
|
+
db.prepare('DELETE FROM episodes WHERE id = ?').run(id);
|
|
7
|
+
} else {
|
|
8
|
+
db.prepare("UPDATE episodes SET superseded_by = 'forgotten' WHERE id = ?").run(id);
|
|
9
|
+
db.prepare('DELETE FROM vec_episodes WHERE id = ?').run(id);
|
|
10
|
+
}
|
|
11
|
+
return { id, type: 'episodic', purged: purge };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const semantic = db.prepare('SELECT id FROM semantics WHERE id = ?').get(id);
|
|
15
|
+
if (semantic) {
|
|
16
|
+
if (purge) {
|
|
17
|
+
db.prepare('DELETE FROM vec_semantics WHERE id = ?').run(id);
|
|
18
|
+
db.prepare('DELETE FROM semantics WHERE id = ?').run(id);
|
|
19
|
+
} else {
|
|
20
|
+
db.prepare("UPDATE semantics SET state = 'superseded' WHERE id = ?").run(id);
|
|
21
|
+
db.prepare('DELETE FROM vec_semantics WHERE id = ?').run(id);
|
|
22
|
+
}
|
|
23
|
+
return { id, type: 'semantic', purged: purge };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const procedure = db.prepare('SELECT id FROM procedures WHERE id = ?').get(id);
|
|
27
|
+
if (procedure) {
|
|
28
|
+
if (purge) {
|
|
29
|
+
db.prepare('DELETE FROM vec_procedures WHERE id = ?').run(id);
|
|
30
|
+
db.prepare('DELETE FROM procedures WHERE id = ?').run(id);
|
|
31
|
+
} else {
|
|
32
|
+
db.prepare("UPDATE procedures SET state = 'superseded' WHERE id = ?").run(id);
|
|
33
|
+
db.prepare('DELETE FROM vec_procedures WHERE id = ?').run(id);
|
|
34
|
+
}
|
|
35
|
+
return { id, type: 'procedural', purged: purge };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw new Error(`Memory not found: ${id}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function purgeMemories(db) {
|
|
42
|
+
const deadEpisodes = db.prepare(
|
|
43
|
+
'SELECT id FROM episodes WHERE superseded_by IS NOT NULL'
|
|
44
|
+
).all();
|
|
45
|
+
const deadSemantics = db.prepare(
|
|
46
|
+
"SELECT id FROM semantics WHERE state IN ('superseded', 'dormant', 'rolled_back')"
|
|
47
|
+
).all();
|
|
48
|
+
const deadProcedures = db.prepare(
|
|
49
|
+
"SELECT id FROM procedures WHERE state IN ('superseded', 'dormant', 'rolled_back')"
|
|
50
|
+
).all();
|
|
51
|
+
|
|
52
|
+
const purgeAll = db.transaction(() => {
|
|
53
|
+
for (const row of deadEpisodes) {
|
|
54
|
+
db.prepare('DELETE FROM vec_episodes WHERE id = ?').run(row.id);
|
|
55
|
+
db.prepare('DELETE FROM episodes WHERE id = ?').run(row.id);
|
|
56
|
+
}
|
|
57
|
+
for (const row of deadSemantics) {
|
|
58
|
+
db.prepare('DELETE FROM vec_semantics WHERE id = ?').run(row.id);
|
|
59
|
+
db.prepare('DELETE FROM semantics WHERE id = ?').run(row.id);
|
|
60
|
+
}
|
|
61
|
+
for (const row of deadProcedures) {
|
|
62
|
+
db.prepare('DELETE FROM vec_procedures WHERE id = ?').run(row.id);
|
|
63
|
+
db.prepare('DELETE FROM procedures WHERE id = ?').run(row.id);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
purgeAll();
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
episodes: deadEpisodes.length,
|
|
71
|
+
semantics: deadSemantics.length,
|
|
72
|
+
procedures: deadProcedures.length,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function forgetByQuery(db, embeddingProvider, query, { minSimilarity = 0.9, purge = false } = {}) {
|
|
77
|
+
const queryVector = await embeddingProvider.embed(query);
|
|
78
|
+
const queryBuffer = embeddingProvider.vectorToBuffer(queryVector);
|
|
79
|
+
|
|
80
|
+
const candidates = [];
|
|
81
|
+
|
|
82
|
+
const epMatch = db.prepare(`
|
|
83
|
+
SELECT e.id, (1.0 - v.distance) AS similarity, 'episodic' AS type
|
|
84
|
+
FROM vec_episodes v JOIN episodes e ON e.id = v.id
|
|
85
|
+
WHERE v.embedding MATCH ? AND k = 1 AND e.superseded_by IS NULL
|
|
86
|
+
`).get(queryBuffer);
|
|
87
|
+
if (epMatch) candidates.push(epMatch);
|
|
88
|
+
|
|
89
|
+
const semMatch = db.prepare(`
|
|
90
|
+
SELECT s.id, (1.0 - v.distance) AS similarity, 'semantic' AS type
|
|
91
|
+
FROM vec_semantics v JOIN semantics s ON s.id = v.id
|
|
92
|
+
WHERE v.embedding MATCH ? AND k = 1 AND (v.state = 'active' OR v.state = 'context_dependent')
|
|
93
|
+
`).get(queryBuffer);
|
|
94
|
+
if (semMatch) candidates.push(semMatch);
|
|
95
|
+
|
|
96
|
+
const procMatch = db.prepare(`
|
|
97
|
+
SELECT p.id, (1.0 - v.distance) AS similarity, 'procedural' AS type
|
|
98
|
+
FROM vec_procedures v JOIN procedures p ON p.id = v.id
|
|
99
|
+
WHERE v.embedding MATCH ? AND k = 1 AND (v.state = 'active' OR v.state = 'context_dependent')
|
|
100
|
+
`).get(queryBuffer);
|
|
101
|
+
if (procMatch) candidates.push(procMatch);
|
|
102
|
+
|
|
103
|
+
if (candidates.length === 0) return null;
|
|
104
|
+
|
|
105
|
+
candidates.sort((a, b) => b.similarity - a.similarity);
|
|
106
|
+
const best = candidates[0];
|
|
107
|
+
|
|
108
|
+
if (best.similarity < minSimilarity) return null;
|
|
109
|
+
|
|
110
|
+
return forgetMemory(db, best.id, { purge });
|
|
111
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Audrey } from './audrey.js';
|
|
2
|
-
export { computeConfidence, sourceReliability, DEFAULT_SOURCE_RELIABILITY, DEFAULT_WEIGHTS, DEFAULT_HALF_LIVES } from './confidence.js';
|
|
2
|
+
export { computeConfidence, sourceReliability, salienceModifier, DEFAULT_SOURCE_RELIABILITY, DEFAULT_WEIGHTS, DEFAULT_HALF_LIVES } from './confidence.js';
|
|
3
3
|
export { createEmbeddingProvider, MockEmbeddingProvider, OpenAIEmbeddingProvider } from './embedding.js';
|
|
4
4
|
export { createLLMProvider, MockLLMProvider, AnthropicLLMProvider, OpenAILLMProvider } from './llm.js';
|
|
5
5
|
export { recall, recallStream } from './recall.js';
|
|
@@ -14,3 +14,6 @@ export { exportMemories } from './export.js';
|
|
|
14
14
|
export { importMemories } from './import.js';
|
|
15
15
|
export { suggestConsolidationParams } from './adaptive.js';
|
|
16
16
|
export { reembedAll } from './migrate.js';
|
|
17
|
+
export { forgetMemory, forgetByQuery, purgeMemories } from './forget.js';
|
|
18
|
+
export { applyInterference, interferenceModifier } from './interference.js';
|
|
19
|
+
export { contextMatchRatio, contextModifier } from './context.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function interferenceModifier(interferenceCount, weight = 0.1) {
|
|
2
|
+
return 1 / (1 + weight * interferenceCount);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export async function applyInterference(db, embeddingProvider, episodeId, { content }, config = {}) {
|
|
6
|
+
const { enabled = true, k = 5, threshold = 0.6, weight = 0.1 } = config;
|
|
7
|
+
|
|
8
|
+
if (!enabled) return [];
|
|
9
|
+
|
|
10
|
+
const vector = await embeddingProvider.embed(content);
|
|
11
|
+
const buffer = embeddingProvider.vectorToBuffer(vector);
|
|
12
|
+
|
|
13
|
+
const semanticHits = db.prepare(`
|
|
14
|
+
SELECT s.id, s.interference_count, (1.0 - v.distance) AS similarity
|
|
15
|
+
FROM vec_semantics v
|
|
16
|
+
JOIN semantics s ON s.id = v.id
|
|
17
|
+
WHERE v.embedding MATCH ?
|
|
18
|
+
AND k = ?
|
|
19
|
+
AND (v.state = 'active' OR v.state = 'context_dependent')
|
|
20
|
+
`).all(buffer, k);
|
|
21
|
+
|
|
22
|
+
const proceduralHits = db.prepare(`
|
|
23
|
+
SELECT p.id, p.interference_count, (1.0 - v.distance) AS similarity
|
|
24
|
+
FROM vec_procedures v
|
|
25
|
+
JOIN procedures p ON p.id = v.id
|
|
26
|
+
WHERE v.embedding MATCH ?
|
|
27
|
+
AND k = ?
|
|
28
|
+
AND (v.state = 'active' OR v.state = 'context_dependent')
|
|
29
|
+
`).all(buffer, k);
|
|
30
|
+
|
|
31
|
+
const affected = [];
|
|
32
|
+
|
|
33
|
+
const updateSemantic = db.prepare('UPDATE semantics SET interference_count = ? WHERE id = ?');
|
|
34
|
+
const updateProcedural = db.prepare('UPDATE procedures SET interference_count = ? WHERE id = ?');
|
|
35
|
+
|
|
36
|
+
for (const hit of semanticHits) {
|
|
37
|
+
if (hit.similarity < threshold) continue;
|
|
38
|
+
const newCount = hit.interference_count + 1;
|
|
39
|
+
updateSemantic.run(newCount, hit.id);
|
|
40
|
+
affected.push({ id: hit.id, type: 'semantic', newCount, similarity: hit.similarity });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const hit of proceduralHits) {
|
|
44
|
+
if (hit.similarity < threshold) continue;
|
|
45
|
+
const newCount = hit.interference_count + 1;
|
|
46
|
+
updateProcedural.run(newCount, hit.id);
|
|
47
|
+
affected.push({ id: hit.id, type: 'procedural', newCount, similarity: hit.similarity });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return affected;
|
|
51
|
+
}
|
package/src/recall.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { computeConfidence, DEFAULT_HALF_LIVES } from './confidence.js';
|
|
1
|
+
import { computeConfidence, DEFAULT_HALF_LIVES, salienceModifier } from './confidence.js';
|
|
2
|
+
import { interferenceModifier } from './interference.js';
|
|
3
|
+
import { contextMatchRatio, contextModifier } from './context.js';
|
|
2
4
|
import { daysBetween, safeJsonParse } from './utils.js';
|
|
3
5
|
|
|
4
6
|
function computeEpisodicConfidence(ep, now, confidenceConfig = {}) {
|
|
5
7
|
const ageDays = daysBetween(ep.created_at, now);
|
|
6
8
|
const halfLives = confidenceConfig.halfLives || DEFAULT_HALF_LIVES;
|
|
7
|
-
|
|
9
|
+
let confidence = computeConfidence({
|
|
8
10
|
sourceType: ep.source,
|
|
9
11
|
supportingCount: 1,
|
|
10
12
|
contradictingCount: 0,
|
|
@@ -15,6 +17,8 @@ function computeEpisodicConfidence(ep, now, confidenceConfig = {}) {
|
|
|
15
17
|
weights: confidenceConfig.weights,
|
|
16
18
|
customSourceReliability: confidenceConfig.sourceReliability,
|
|
17
19
|
});
|
|
20
|
+
confidence *= salienceModifier(ep.salience);
|
|
21
|
+
return Math.max(0, Math.min(1, confidence));
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
function computeSemanticConfidence(sem, now, confidenceConfig = {}) {
|
|
@@ -23,7 +27,7 @@ function computeSemanticConfidence(sem, now, confidenceConfig = {}) {
|
|
|
23
27
|
? daysBetween(sem.last_reinforced_at, now)
|
|
24
28
|
: ageDays;
|
|
25
29
|
const halfLives = confidenceConfig.halfLives || DEFAULT_HALF_LIVES;
|
|
26
|
-
|
|
30
|
+
let confidence = computeConfidence({
|
|
27
31
|
sourceType: 'tool-result',
|
|
28
32
|
supportingCount: sem.supporting_count || 0,
|
|
29
33
|
contradictingCount: sem.contradicting_count || 0,
|
|
@@ -34,6 +38,9 @@ function computeSemanticConfidence(sem, now, confidenceConfig = {}) {
|
|
|
34
38
|
weights: confidenceConfig.weights,
|
|
35
39
|
customSourceReliability: confidenceConfig.sourceReliability,
|
|
36
40
|
});
|
|
41
|
+
confidence *= interferenceModifier(sem.interference_count || 0, confidenceConfig.interferenceWeight);
|
|
42
|
+
confidence *= salienceModifier(sem.salience);
|
|
43
|
+
return Math.max(0, Math.min(1, confidence));
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
function computeProceduralConfidence(proc, now, confidenceConfig = {}) {
|
|
@@ -42,7 +49,7 @@ function computeProceduralConfidence(proc, now, confidenceConfig = {}) {
|
|
|
42
49
|
? daysBetween(proc.last_reinforced_at, now)
|
|
43
50
|
: ageDays;
|
|
44
51
|
const halfLives = confidenceConfig.halfLives || DEFAULT_HALF_LIVES;
|
|
45
|
-
|
|
52
|
+
let confidence = computeConfidence({
|
|
46
53
|
sourceType: 'tool-result',
|
|
47
54
|
supportingCount: proc.success_count || 0,
|
|
48
55
|
contradictingCount: proc.failure_count || 0,
|
|
@@ -53,9 +60,12 @@ function computeProceduralConfidence(proc, now, confidenceConfig = {}) {
|
|
|
53
60
|
weights: confidenceConfig.weights,
|
|
54
61
|
customSourceReliability: confidenceConfig.sourceReliability,
|
|
55
62
|
});
|
|
63
|
+
confidence *= interferenceModifier(proc.interference_count || 0, confidenceConfig.interferenceWeight);
|
|
64
|
+
confidence *= salienceModifier(proc.salience);
|
|
65
|
+
return Math.max(0, Math.min(1, confidence));
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
function buildEpisodicEntry(ep, confidence, score, includeProvenance) {
|
|
68
|
+
function buildEpisodicEntry(ep, confidence, score, includeProvenance, contextMatch) {
|
|
59
69
|
const entry = {
|
|
60
70
|
id: ep.id,
|
|
61
71
|
content: ep.content,
|
|
@@ -65,6 +75,9 @@ function buildEpisodicEntry(ep, confidence, score, includeProvenance) {
|
|
|
65
75
|
source: ep.source,
|
|
66
76
|
createdAt: ep.created_at,
|
|
67
77
|
};
|
|
78
|
+
if (contextMatch !== undefined) {
|
|
79
|
+
entry.contextMatch = contextMatch;
|
|
80
|
+
}
|
|
68
81
|
if (includeProvenance) {
|
|
69
82
|
entry.provenance = {
|
|
70
83
|
source: ep.source,
|
|
@@ -121,7 +134,19 @@ function buildProceduralEntry(proc, confidence, score, includeProvenance) {
|
|
|
121
134
|
return entry;
|
|
122
135
|
}
|
|
123
136
|
|
|
124
|
-
function
|
|
137
|
+
function stateClause(includeDormant) {
|
|
138
|
+
return includeDormant
|
|
139
|
+
? "AND (v.state = 'active' OR v.state = 'context_dependent' OR v.state = 'dormant')"
|
|
140
|
+
: "AND (v.state = 'active' OR v.state = 'context_dependent')";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function matchesDateFilters(createdAt, filters) {
|
|
144
|
+
if (filters.after && createdAt <= filters.after) return false;
|
|
145
|
+
if (filters.before && createdAt >= filters.before) return false;
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters = {}) {
|
|
125
150
|
const rows = db.prepare(`
|
|
126
151
|
SELECT e.*, (1.0 - v.distance) AS similarity
|
|
127
152
|
FROM vec_episodes v
|
|
@@ -133,34 +158,43 @@ function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includePro
|
|
|
133
158
|
|
|
134
159
|
const results = [];
|
|
135
160
|
for (const row of rows) {
|
|
136
|
-
|
|
161
|
+
if (!matchesDateFilters(row.created_at, filters)) continue;
|
|
162
|
+
if (filters.tags?.length) {
|
|
163
|
+
const rowTags = safeJsonParse(row.tags, []);
|
|
164
|
+
if (!filters.tags.some(t => rowTags.includes(t))) continue;
|
|
165
|
+
}
|
|
166
|
+
if (filters.sources?.length && !filters.sources.includes(row.source)) continue;
|
|
167
|
+
let confidence = computeEpisodicConfidence(row, now, confidenceConfig);
|
|
168
|
+
|
|
169
|
+
let ctxMatch;
|
|
170
|
+
if (confidenceConfig?.retrievalContext) {
|
|
171
|
+
const encodingCtx = safeJsonParse(row.context, {});
|
|
172
|
+
ctxMatch = contextMatchRatio(encodingCtx, confidenceConfig.retrievalContext);
|
|
173
|
+
confidence *= contextModifier(encodingCtx, confidenceConfig.retrievalContext, confidenceConfig.contextWeight);
|
|
174
|
+
confidence = Math.max(0, Math.min(1, confidence));
|
|
175
|
+
}
|
|
176
|
+
|
|
137
177
|
if (confidence < minConfidence) continue;
|
|
138
178
|
const score = row.similarity * confidence;
|
|
139
|
-
results.push(buildEpisodicEntry(row, confidence, score, includeProvenance));
|
|
179
|
+
results.push(buildEpisodicEntry(row, confidence, score, includeProvenance, ctxMatch));
|
|
140
180
|
}
|
|
141
181
|
return results;
|
|
142
182
|
}
|
|
143
183
|
|
|
144
|
-
function knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig) {
|
|
145
|
-
let stateFilter;
|
|
146
|
-
if (includeDormant) {
|
|
147
|
-
stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent' OR v.state = 'dormant')";
|
|
148
|
-
} else {
|
|
149
|
-
stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent')";
|
|
150
|
-
}
|
|
151
|
-
|
|
184
|
+
function knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters = {}) {
|
|
152
185
|
const rows = db.prepare(`
|
|
153
186
|
SELECT s.*, (1.0 - v.distance) AS similarity
|
|
154
187
|
FROM vec_semantics v
|
|
155
188
|
JOIN semantics s ON s.id = v.id
|
|
156
189
|
WHERE v.embedding MATCH ?
|
|
157
190
|
AND k = ?
|
|
158
|
-
${
|
|
191
|
+
${stateClause(includeDormant)}
|
|
159
192
|
`).all(queryBuffer, candidateK);
|
|
160
193
|
|
|
161
194
|
const results = [];
|
|
162
195
|
const matchedIds = [];
|
|
163
196
|
for (const row of rows) {
|
|
197
|
+
if (!matchesDateFilters(row.created_at, filters)) continue;
|
|
164
198
|
const confidence = computeSemanticConfidence(row, now, confidenceConfig);
|
|
165
199
|
if (confidence < minConfidence) continue;
|
|
166
200
|
const score = row.similarity * confidence;
|
|
@@ -170,26 +204,20 @@ function knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includePro
|
|
|
170
204
|
return { results, matchedIds };
|
|
171
205
|
}
|
|
172
206
|
|
|
173
|
-
function knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig) {
|
|
174
|
-
let stateFilter;
|
|
175
|
-
if (includeDormant) {
|
|
176
|
-
stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent' OR v.state = 'dormant')";
|
|
177
|
-
} else {
|
|
178
|
-
stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent')";
|
|
179
|
-
}
|
|
180
|
-
|
|
207
|
+
function knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters = {}) {
|
|
181
208
|
const rows = db.prepare(`
|
|
182
209
|
SELECT p.*, (1.0 - v.distance) AS similarity
|
|
183
210
|
FROM vec_procedures v
|
|
184
211
|
JOIN procedures p ON p.id = v.id
|
|
185
212
|
WHERE v.embedding MATCH ?
|
|
186
213
|
AND k = ?
|
|
187
|
-
${
|
|
214
|
+
${stateClause(includeDormant)}
|
|
188
215
|
`).all(queryBuffer, candidateK);
|
|
189
216
|
|
|
190
217
|
const results = [];
|
|
191
218
|
const matchedIds = [];
|
|
192
219
|
for (const row of rows) {
|
|
220
|
+
if (!matchesDateFilters(row.created_at, filters)) continue;
|
|
193
221
|
const confidence = computeProceduralConfidence(row, now, confidenceConfig);
|
|
194
222
|
if (confidence < minConfidence) continue;
|
|
195
223
|
const score = row.similarity * confidence;
|
|
@@ -203,7 +231,7 @@ function knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeP
|
|
|
203
231
|
* @param {import('better-sqlite3').Database} db
|
|
204
232
|
* @param {import('./embedding.js').EmbeddingProvider} embeddingProvider
|
|
205
233
|
* @param {string} query
|
|
206
|
-
* @param {{ minConfidence?: number, types?: string[], limit?: number, includeProvenance?: boolean, includeDormant?: boolean }} [options]
|
|
234
|
+
* @param {{ minConfidence?: number, types?: string[], limit?: number, includeProvenance?: boolean, includeDormant?: boolean, tags?: string[], sources?: string[], after?: string, before?: string }} [options]
|
|
207
235
|
* @returns {AsyncGenerator<{ id: string, content: string, type: string, confidence: number, score: number, source: string, createdAt: string }>}
|
|
208
236
|
*/
|
|
209
237
|
export async function* recallStream(db, embeddingProvider, query, options = {}) {
|
|
@@ -214,24 +242,30 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
|
|
|
214
242
|
includeProvenance = false,
|
|
215
243
|
includeDormant = false,
|
|
216
244
|
confidenceConfig,
|
|
245
|
+
tags,
|
|
246
|
+
sources,
|
|
247
|
+
after,
|
|
248
|
+
before,
|
|
217
249
|
} = options;
|
|
218
250
|
|
|
219
251
|
const queryVector = await embeddingProvider.embed(query);
|
|
220
252
|
const queryBuffer = embeddingProvider.vectorToBuffer(queryVector);
|
|
221
253
|
const searchTypes = types || ['episodic', 'semantic', 'procedural'];
|
|
222
254
|
const now = new Date();
|
|
223
|
-
const
|
|
255
|
+
const hasFilters = tags?.length || sources?.length || after || before;
|
|
256
|
+
const candidateK = hasFilters ? limit * 5 : limit * 3;
|
|
257
|
+
const filters = { tags, sources, after, before };
|
|
224
258
|
|
|
225
259
|
const allResults = [];
|
|
226
260
|
|
|
227
261
|
if (searchTypes.includes('episodic')) {
|
|
228
|
-
const episodic = knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig);
|
|
262
|
+
const episodic = knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters);
|
|
229
263
|
allResults.push(...episodic);
|
|
230
264
|
}
|
|
231
265
|
|
|
232
266
|
if (searchTypes.includes('semantic')) {
|
|
233
267
|
const { results: semResults, matchedIds: semIds } =
|
|
234
|
-
knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig);
|
|
268
|
+
knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters);
|
|
235
269
|
allResults.push(...semResults);
|
|
236
270
|
|
|
237
271
|
if (semIds.length > 0) {
|
|
@@ -247,7 +281,7 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
|
|
|
247
281
|
|
|
248
282
|
if (searchTypes.includes('procedural')) {
|
|
249
283
|
const { results: procResults, matchedIds: procIds } =
|
|
250
|
-
knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig);
|
|
284
|
+
knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters);
|
|
251
285
|
allResults.push(...procResults);
|
|
252
286
|
|
|
253
287
|
if (procIds.length > 0) {
|
|
@@ -272,7 +306,7 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
|
|
|
272
306
|
* @param {import('better-sqlite3').Database} db
|
|
273
307
|
* @param {import('./embedding.js').EmbeddingProvider} embeddingProvider
|
|
274
308
|
* @param {string} query
|
|
275
|
-
* @param {{ minConfidence?: number, types?: string[], limit?: number, includeProvenance?: boolean, includeDormant?: boolean }} [options]
|
|
309
|
+
* @param {{ minConfidence?: number, types?: string[], limit?: number, includeProvenance?: boolean, includeDormant?: boolean, tags?: string[], sources?: string[], after?: string, before?: string }} [options]
|
|
276
310
|
* @returns {Promise<Array<{ id: string, content: string, type: string, confidence: number, score: number, source: string, createdAt: string }>>}
|
|
277
311
|
*/
|
|
278
312
|
export async function recall(db, embeddingProvider, query, options = {}) {
|