zouroboros-memory 3.0.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.
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Episodic memory for event-based storage
3
+ */
4
+ import { randomUUID } from 'crypto';
5
+ import { getDatabase } from './database.js';
6
+ /**
7
+ * Create a new episode
8
+ */
9
+ export function createEpisode(input) {
10
+ const db = getDatabase();
11
+ const id = randomUUID();
12
+ const happenedAt = input.happenedAt || new Date();
13
+ db.run(`INSERT INTO episodes (id, summary, outcome, happened_at, duration_ms, procedure_id, metadata)
14
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
15
+ id,
16
+ input.summary,
17
+ input.outcome,
18
+ Math.floor(happenedAt.getTime() / 1000),
19
+ input.durationMs || null,
20
+ input.procedureId || null,
21
+ input.metadata ? JSON.stringify(input.metadata) : null,
22
+ ]);
23
+ // Link entities
24
+ for (const entity of input.entities) {
25
+ db.run('INSERT INTO episode_entities (episode_id, entity) VALUES (?, ?)', [id, entity]);
26
+ }
27
+ return {
28
+ id,
29
+ conversationId: id, // Using episode ID as conversation ID
30
+ summary: input.summary,
31
+ outcome: input.outcome,
32
+ entities: input.entities,
33
+ tags: [],
34
+ createdAt: happenedAt.toISOString(),
35
+ };
36
+ }
37
+ /**
38
+ * Search episodes with temporal filters
39
+ */
40
+ export function searchEpisodes(query) {
41
+ const db = getDatabase();
42
+ let sql = `
43
+ SELECT e.id, e.summary, e.outcome, e.happened_at as happenedAt,
44
+ e.duration_ms as durationMs, e.procedure_id as procedureId,
45
+ e.metadata, e.created_at as createdAt
46
+ FROM episodes e
47
+ WHERE 1=1
48
+ `;
49
+ const params = [];
50
+ if (query.since) {
51
+ sql += ' AND e.happened_at >= ?';
52
+ params.push(Math.floor(new Date(query.since).getTime() / 1000));
53
+ }
54
+ if (query.until) {
55
+ sql += ' AND e.happened_at <= ?';
56
+ params.push(Math.floor(new Date(query.until).getTime() / 1000));
57
+ }
58
+ if (query.outcome) {
59
+ sql += ' AND e.outcome = ?';
60
+ params.push(query.outcome);
61
+ }
62
+ sql += ' ORDER BY e.happened_at DESC';
63
+ if (query.limit) {
64
+ sql += ' LIMIT ?';
65
+ params.push(query.limit);
66
+ }
67
+ const rows = db.query(sql).all(...params);
68
+ // Get entities for each episode
69
+ return rows.map(row => {
70
+ const entityRows = db.query('SELECT entity FROM episode_entities WHERE episode_id = ?').all(row.id);
71
+ return {
72
+ id: row.id,
73
+ conversationId: row.id,
74
+ summary: row.summary,
75
+ outcome: row.outcome,
76
+ entities: entityRows.map(e => e.entity),
77
+ tags: [],
78
+ createdAt: new Date(row.createdAt * 1000).toISOString(),
79
+ tokenCount: row.durationMs || undefined,
80
+ };
81
+ });
82
+ }
83
+ /**
84
+ * Get episodes for a specific entity
85
+ */
86
+ export function getEntityEpisodes(entity, options = {}) {
87
+ const db = getDatabase();
88
+ const { limit = 10, outcome } = options;
89
+ let sql = `
90
+ SELECT e.id, e.summary, e.outcome, e.happened_at as happenedAt,
91
+ e.duration_ms as durationMs, e.procedure_id as procedureId,
92
+ e.metadata, e.created_at as createdAt
93
+ FROM episodes e
94
+ JOIN episode_entities ee ON e.id = ee.episode_id
95
+ WHERE ee.entity = ?
96
+ `;
97
+ const params = [entity];
98
+ if (outcome) {
99
+ sql += ' AND e.outcome = ?';
100
+ params.push(outcome);
101
+ }
102
+ sql += ' ORDER BY e.happened_at DESC LIMIT ?';
103
+ params.push(limit);
104
+ const rows = db.query(sql).all(...params);
105
+ return rows.map(row => ({
106
+ id: row.id,
107
+ conversationId: row.id,
108
+ summary: row.summary,
109
+ outcome: row.outcome,
110
+ entities: [entity],
111
+ tags: [],
112
+ createdAt: new Date(row.createdAt * 1000).toISOString(),
113
+ tokenCount: row.durationMs || undefined,
114
+ }));
115
+ }
116
+ /**
117
+ * Update episode outcome
118
+ */
119
+ export function updateEpisodeOutcome(id, outcome) {
120
+ const db = getDatabase();
121
+ const result = db.run('UPDATE episodes SET outcome = ? WHERE id = ?', [outcome, id]);
122
+ return result.changes > 0;
123
+ }
124
+ /**
125
+ * Get episode statistics
126
+ */
127
+ export function getEpisodeStats() {
128
+ const db = getDatabase();
129
+ const total = db.query('SELECT COUNT(*) as count FROM episodes').get().count;
130
+ const byOutcome = {
131
+ success: 0,
132
+ failure: 0,
133
+ resolved: 0,
134
+ ongoing: 0,
135
+ };
136
+ const rows = db.query('SELECT outcome, COUNT(*) as count FROM episodes GROUP BY outcome').all();
137
+ for (const row of rows) {
138
+ byOutcome[row.outcome] = row.count;
139
+ }
140
+ return { total, byOutcome };
141
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Fact storage and retrieval operations
3
+ */
4
+ import type { MemoryConfig, MemoryEntry, MemorySearchResult, DecayClass } from 'zouroboros-core';
5
+ type Category = 'preference' | 'fact' | 'decision' | 'convention' | 'other' | 'reference' | 'project';
6
+ interface StoreFactInput {
7
+ entity: string;
8
+ key?: string;
9
+ value: string;
10
+ category?: Category;
11
+ decay?: DecayClass;
12
+ importance?: number;
13
+ source?: string;
14
+ confidence?: number;
15
+ metadata?: Record<string, unknown>;
16
+ }
17
+ /**
18
+ * Store a fact in memory
19
+ */
20
+ export declare function storeFact(input: StoreFactInput, config: MemoryConfig): Promise<MemoryEntry>;
21
+ /**
22
+ * Search facts by exact match or keyword
23
+ */
24
+ export declare function searchFacts(query: string, options?: {
25
+ entity?: string;
26
+ category?: string;
27
+ limit?: number;
28
+ }): MemoryEntry[];
29
+ /**
30
+ * Search facts using vector similarity
31
+ */
32
+ export declare function searchFactsVector(query: string, config: MemoryConfig, options?: {
33
+ limit?: number;
34
+ threshold?: number;
35
+ useHyDE?: boolean;
36
+ }): Promise<MemorySearchResult[]>;
37
+ /**
38
+ * Hybrid search combining exact and vector search
39
+ */
40
+ export declare function searchFactsHybrid(query: string, config: MemoryConfig, options?: {
41
+ limit?: number;
42
+ vectorWeight?: number;
43
+ }): Promise<MemorySearchResult[]>;
44
+ /**
45
+ * Get a fact by ID
46
+ */
47
+ export declare function getFact(id: string): MemoryEntry | null;
48
+ /**
49
+ * Delete a fact by ID
50
+ */
51
+ export declare function deleteFact(id: string): boolean;
52
+ /**
53
+ * Update fact access time
54
+ */
55
+ export declare function touchFact(id: string): void;
56
+ /**
57
+ * Clean up expired facts
58
+ */
59
+ export declare function cleanupExpiredFacts(): number;
60
+ export {};
package/dist/facts.js ADDED
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Fact storage and retrieval operations
3
+ */
4
+ import { randomUUID } from 'crypto';
5
+ import { getDatabase } from './database.js';
6
+ import { generateEmbedding, serializeEmbedding, generateHyDEExpansion, blendEmbeddings } from './embeddings.js';
7
+ import { invalidateGraphCache } from './graph.js';
8
+ // Decay class TTLs in seconds
9
+ const TTL_DEFAULTS = {
10
+ permanent: null,
11
+ long: 365 * 24 * 3600,
12
+ medium: 90 * 24 * 3600,
13
+ short: 30 * 24 * 3600,
14
+ };
15
+ /**
16
+ * Store a fact in memory
17
+ */
18
+ export async function storeFact(input, config) {
19
+ const db = getDatabase();
20
+ const id = randomUUID();
21
+ const now = Math.floor(Date.now() / 1000);
22
+ const decay = input.decay || 'medium';
23
+ const ttl = TTL_DEFAULTS[decay];
24
+ const expiresAt = ttl ? now + ttl : null;
25
+ const text = input.key
26
+ ? `${input.entity} ${input.key} ${input.value}`
27
+ : `${input.entity} ${input.value}`;
28
+ const entry = {
29
+ id,
30
+ entity: input.entity,
31
+ key: input.key || null,
32
+ value: input.value,
33
+ decay,
34
+ createdAt: new Date().toISOString(),
35
+ updatedAt: new Date().toISOString(),
36
+ };
37
+ // Insert fact
38
+ db.run(`INSERT INTO facts (id, entity, key, value, text, category, decay_class, importance, source,
39
+ created_at, expires_at, confidence, metadata)
40
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
41
+ id,
42
+ input.entity,
43
+ input.key || null,
44
+ input.value,
45
+ text,
46
+ input.category || 'fact',
47
+ decay,
48
+ input.importance || 1.0,
49
+ input.source || 'manual',
50
+ now,
51
+ expiresAt,
52
+ input.confidence || 1.0,
53
+ input.metadata ? JSON.stringify(input.metadata) : null,
54
+ ]);
55
+ // Generate and store embedding if vector search is enabled
56
+ if (config.vectorEnabled) {
57
+ try {
58
+ const embedding = await generateEmbedding(text, config);
59
+ const serialized = serializeEmbedding(embedding);
60
+ db.run('INSERT INTO fact_embeddings (fact_id, embedding, model) VALUES (?, ?, ?)', [id, serialized, config.ollamaModel]);
61
+ }
62
+ catch (error) {
63
+ console.warn('Failed to generate embedding:', error);
64
+ }
65
+ }
66
+ invalidateGraphCache();
67
+ return entry;
68
+ }
69
+ /**
70
+ * Search facts by exact match or keyword
71
+ */
72
+ export function searchFacts(query, options = {}) {
73
+ const db = getDatabase();
74
+ const { entity, category, limit = 10 } = options;
75
+ let sql = `
76
+ SELECT id, entity, key, value, category, decay_class as decayClass,
77
+ importance, source, created_at as createdAt, expires_at as expiresAt,
78
+ confidence, metadata
79
+ FROM facts
80
+ WHERE (text LIKE ? OR entity LIKE ? OR value LIKE ?)
81
+ AND (expires_at IS NULL OR expires_at > strftime('%s', 'now'))
82
+ `;
83
+ const params = [`%${query}%`, `%${query}%`, `%${query}%`];
84
+ if (entity) {
85
+ sql += ' AND entity = ?';
86
+ params.push(entity);
87
+ }
88
+ if (category) {
89
+ sql += ' AND category = ?';
90
+ params.push(category);
91
+ }
92
+ sql += ' ORDER BY importance DESC, created_at DESC LIMIT ?';
93
+ params.push(limit);
94
+ const rows = db.query(sql).all(...params);
95
+ return rows.map(row => ({
96
+ id: row.id,
97
+ entity: row.entity,
98
+ key: row.key,
99
+ value: row.value,
100
+ decay: row.decayClass,
101
+ createdAt: new Date(row.createdAt * 1000).toISOString(),
102
+ updatedAt: new Date(row.createdAt * 1000).toISOString(),
103
+ tags: row.category ? [row.category] : undefined,
104
+ }));
105
+ }
106
+ /**
107
+ * Search facts using vector similarity
108
+ */
109
+ export async function searchFactsVector(query, config, options = {}) {
110
+ if (!config.vectorEnabled) {
111
+ throw new Error('Vector search is disabled. Enable it in configuration.');
112
+ }
113
+ const db = getDatabase();
114
+ const { limit = 10, threshold = 0.7, useHyDE } = options;
115
+ // Generate query embedding, optionally with HyDE expansion
116
+ let queryEmbedding;
117
+ const shouldUseHyDE = useHyDE ?? config.hydeExpansion;
118
+ if (shouldUseHyDE) {
119
+ const hyde = await generateHyDEExpansion(query, config);
120
+ queryEmbedding = blendEmbeddings(hyde.original, hyde.expanded);
121
+ }
122
+ else {
123
+ queryEmbedding = await generateEmbedding(query, config);
124
+ }
125
+ // Get all facts with embeddings
126
+ const rows = db.query(`
127
+ SELECT f.id, f.entity, f.key, f.value, f.category, f.decay_class as decayClass,
128
+ f.importance, f.created_at as createdAt, fe.embedding
129
+ FROM facts f
130
+ JOIN fact_embeddings fe ON f.id = fe.fact_id
131
+ WHERE f.expires_at IS NULL OR f.expires_at > strftime('%s', 'now')
132
+ `).all();
133
+ // Calculate similarity and filter
134
+ const { cosineSimilarity, deserializeEmbedding } = await import('./embeddings.js');
135
+ const results = rows
136
+ .map(row => {
137
+ const embedding = deserializeEmbedding(row.embedding);
138
+ const similarity = cosineSimilarity(queryEmbedding, embedding);
139
+ return {
140
+ entry: {
141
+ id: row.id,
142
+ entity: row.entity,
143
+ key: row.key,
144
+ value: row.value,
145
+ decay: row.decayClass,
146
+ createdAt: new Date(row.createdAt * 1000).toISOString(),
147
+ updatedAt: new Date(row.createdAt * 1000).toISOString(),
148
+ tags: row.category ? [row.category] : undefined,
149
+ },
150
+ score: similarity,
151
+ matchType: 'semantic',
152
+ };
153
+ })
154
+ .filter(r => r.score >= threshold)
155
+ .sort((a, b) => b.score - a.score)
156
+ .slice(0, limit);
157
+ return results;
158
+ }
159
+ /**
160
+ * Hybrid search combining exact and vector search
161
+ */
162
+ export async function searchFactsHybrid(query, config, options = {}) {
163
+ const { limit = 10, vectorWeight = 0.7 } = options;
164
+ // Get exact matches
165
+ const exactMatches = searchFacts(query, { limit: limit * 2 });
166
+ // Get vector matches
167
+ let vectorMatches = [];
168
+ if (config.vectorEnabled) {
169
+ try {
170
+ vectorMatches = await searchFactsVector(query, config, { limit: limit * 2 });
171
+ }
172
+ catch (error) {
173
+ console.warn('Vector search failed:', error);
174
+ }
175
+ }
176
+ // RRF fusion
177
+ const k = 60;
178
+ const scores = new Map();
179
+ // Score exact matches
180
+ exactMatches.forEach((entry, rank) => {
181
+ const id = entry.id;
182
+ const rrfScore = 1 / (k + rank + 1);
183
+ scores.set(id, { entry, score: rrfScore * (1 - vectorWeight) });
184
+ });
185
+ // Score vector matches
186
+ vectorMatches.forEach((result, rank) => {
187
+ const id = result.entry.id;
188
+ const rrfScore = 1 / (k + rank + 1);
189
+ const existing = scores.get(id);
190
+ if (existing) {
191
+ existing.score += rrfScore * vectorWeight;
192
+ }
193
+ else {
194
+ scores.set(id, { entry: result.entry, score: rrfScore * vectorWeight });
195
+ }
196
+ });
197
+ // Sort by combined score
198
+ return Array.from(scores.values())
199
+ .sort((a, b) => b.score - a.score)
200
+ .slice(0, limit)
201
+ .map(r => ({
202
+ entry: r.entry,
203
+ score: r.score,
204
+ matchType: 'hybrid',
205
+ }));
206
+ }
207
+ /**
208
+ * Get a fact by ID
209
+ */
210
+ export function getFact(id) {
211
+ const db = getDatabase();
212
+ const row = db.query(`
213
+ SELECT id, entity, key, value, category, decay_class as decayClass,
214
+ importance, source, created_at as createdAt, expires_at as expiresAt,
215
+ confidence, metadata
216
+ FROM facts
217
+ WHERE id = ?
218
+ `).get(id);
219
+ if (!row)
220
+ return null;
221
+ return {
222
+ id: row.id,
223
+ entity: row.entity,
224
+ key: row.key,
225
+ value: row.value,
226
+ decay: row.decayClass,
227
+ createdAt: new Date(row.createdAt * 1000).toISOString(),
228
+ updatedAt: new Date(row.createdAt * 1000).toISOString(),
229
+ tags: row.category ? [row.category] : undefined,
230
+ };
231
+ }
232
+ /**
233
+ * Delete a fact by ID
234
+ */
235
+ export function deleteFact(id) {
236
+ const db = getDatabase();
237
+ const result = db.run('DELETE FROM facts WHERE id = ?', [id]);
238
+ if (result.changes > 0)
239
+ invalidateGraphCache();
240
+ return result.changes > 0;
241
+ }
242
+ /**
243
+ * Update fact access time
244
+ */
245
+ export function touchFact(id) {
246
+ const db = getDatabase();
247
+ const now = Math.floor(Date.now() / 1000);
248
+ db.run('UPDATE facts SET last_accessed = ? WHERE id = ?', [now, id]);
249
+ }
250
+ /**
251
+ * Clean up expired facts
252
+ */
253
+ export function cleanupExpiredFacts() {
254
+ const db = getDatabase();
255
+ const result = db.run(`
256
+ DELETE FROM facts
257
+ WHERE expires_at IS NOT NULL
258
+ AND expires_at < strftime('%s', 'now')
259
+ `);
260
+ if (result.changes > 0)
261
+ invalidateGraphCache();
262
+ return result.changes;
263
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * graph-traversal.ts — Enhanced Knowledge Graph Traversal
4
+ * MEM-105: Enhanced Knowledge Graph
5
+ *
6
+ * Usage:
7
+ * bun graph-traversal.ts ancestors --entity <name>
8
+ * bun graph-traversal.ts descendants --entity <name>
9
+ * bun graph-traversal.ts cycles
10
+ * bun graph-traversal.ts infer --entity <name>
11
+ * bun graph-traversal.ts export-dot --entity <name> --output /tmp/graph.dot
12
+ */
13
+ import { Database } from "bun:sqlite";
14
+ export declare const KNOWN_RELATIONS: Set<string>;
15
+ export declare function getAncestors(db: Database, entity: string, maxDepth?: number): Array<{
16
+ entity: string;
17
+ relation: string;
18
+ depth: number;
19
+ path: string[];
20
+ }>;
21
+ export declare function getDescendants(db: Database, entity: string, maxDepth?: number): Array<{
22
+ entity: string;
23
+ relation: string;
24
+ depth: number;
25
+ path: string[];
26
+ }>;
27
+ export declare function detectCycles(db: Database): Array<{
28
+ cycle: string[];
29
+ length: number;
30
+ }>;
31
+ export declare function inferRelations(db: Database, entity: string): Array<{
32
+ from: string;
33
+ to: string;
34
+ inferred_relation: string;
35
+ confidence: number;
36
+ reason: string;
37
+ }>;
38
+ export declare function exportDot(db: Database, entity?: string, outputPath?: string): string;