Skip to main content

engram/
memory.rs

1//! `Memory` — the primary public API for the Engram memory layer.
2//!
3//! `Memory` composes a `FactStore`, `VectorStore`, `GraphStore`, and
4//! `EmbeddingProvider` into a single high-level interface. Callers interact
5//! with `Memory` rather than the lower-level trait objects directly.
6
7use crate::consolidation::{ConsolidationConfig, ConsolidationEngine, ConsolidationResult};
8use crate::context::{ContextBlock, ContextBuilder, ContextConfig};
9use crate::embedding::EmbeddingProvider;
10use crate::extract::{ExtractionConfig, Message};
11use crate::fact::{Entity, Fact, FactFilter, FactId, Relationship};
12use crate::graph::GraphStore;
13use crate::graph_sqlite::SqliteGraphStore;
14use crate::llm::LlmClient;
15use crate::pipeline::ExtractionPipeline;
16use crate::scope::Scope;
17use crate::store::{FactStore, MemoryError, StoreStats};
18use crate::store_sqlite::SqliteFactStore;
19use crate::vector::{VectorFilter, VectorStore};
20use crate::vector_embedded::EmbeddedVectorStore;
21use chrono::{DateTime, Utc};
22use std::collections::HashMap;
23use std::sync::Arc;
24
25// ---------------------------------------------------------------------------
26// RecallQuery
27// ---------------------------------------------------------------------------
28
29/// Parameters for a semantic recall operation.
30#[derive(Debug, Clone, Default)]
31pub struct RecallQuery {
32    /// The text to embed and search for semantically similar facts.
33    pub query: String,
34    /// Optional scope filter — only return facts within this scope.
35    pub scope: Option<Scope>,
36    /// Maximum number of results to return (default: 10).
37    pub max_results: usize,
38    /// Point-in-time filter — only return facts valid at this instant.
39    pub as_of: Option<DateTime<Utc>>,
40    /// Minimum cosine similarity score (0.0 – 1.0).
41    pub min_score: Option<f32>,
42}
43
44// ---------------------------------------------------------------------------
45// Memory
46// ---------------------------------------------------------------------------
47
48/// High-level memory API for AI agents.
49///
50/// `Memory` wires together fact storage, vector search, graph storage, and an
51/// embedding provider. Most callers should construct it via `in_memory` or
52/// `open` rather than calling `new` directly.
53pub struct Memory {
54    fact_store: Arc<dyn FactStore>,
55    vector_store: Arc<dyn VectorStore>,
56    graph_store: Arc<dyn GraphStore>,
57    embedding: Arc<dyn EmbeddingProvider>,
58}
59
60impl Memory {
61    // -----------------------------------------------------------------------
62    // Constructors
63    // -----------------------------------------------------------------------
64
65    /// Construct `Memory` from explicit store and embedding instances.
66    pub fn new(
67        fact_store: Arc<dyn FactStore>,
68        vector_store: Arc<dyn VectorStore>,
69        graph_store: Arc<dyn GraphStore>,
70        embedding: Arc<dyn EmbeddingProvider>,
71    ) -> Self {
72        Self {
73            fact_store,
74            vector_store,
75            graph_store,
76            embedding,
77        }
78    }
79
80    /// Create a fully in-memory `Memory` instance backed by SQLite `:memory:`.
81    ///
82    /// Schema migration is applied automatically. Suitable for testing and
83    /// short-lived agent invocations.
84    pub async fn in_memory(embedding: Box<dyn EmbeddingProvider>) -> Result<Self, MemoryError> {
85        let dims = embedding.dimensions();
86        let embedding = Arc::from(embedding);
87
88        let fact_store = SqliteFactStore::open("sqlite::memory:")
89            .await
90            .map_err(|e| MemoryError::Database(format!("failed to open in-memory SQLite: {e}")))?;
91        fact_store
92            .migrate()
93            .await
94            .map_err(|e| MemoryError::Database(format!("fact store migration failed: {e}")))?;
95
96        let graph_store = SqliteGraphStore::open("sqlite::memory:")
97            .await
98            .map_err(|e| MemoryError::Database(format!("failed to open in-memory graph: {e}")))?;
99        graph_store
100            .migrate()
101            .await
102            .map_err(|e| MemoryError::Database(format!("graph store migration failed: {e}")))?;
103
104        let vector_store = EmbeddedVectorStore::new(dims);
105
106        Ok(Self {
107            fact_store: Arc::new(fact_store),
108            vector_store: Arc::new(vector_store),
109            graph_store: Arc::new(graph_store),
110            embedding,
111        })
112    }
113
114    /// Open a file-backed `Memory` instance at `database_url`.
115    ///
116    /// Uses SQLite for facts and graph data, with an in-process
117    /// `EmbeddedVectorStore` for semantic search. Schema migration is applied
118    /// automatically.
119    pub async fn open(
120        database_url: &str,
121        embedding: Box<dyn EmbeddingProvider>,
122    ) -> Result<Self, MemoryError> {
123        let dims = embedding.dimensions();
124        let embedding = Arc::from(embedding);
125
126        let fact_store = SqliteFactStore::open(database_url)
127            .await
128            .map_err(|e| MemoryError::Database(format!("failed to open SQLite: {e}")))?;
129        fact_store
130            .migrate()
131            .await
132            .map_err(|e| MemoryError::Database(format!("fact store migration failed: {e}")))?;
133
134        let graph_store = SqliteGraphStore::open(database_url)
135            .await
136            .map_err(|e| MemoryError::Database(format!("failed to open graph SQLite: {e}")))?;
137        graph_store
138            .migrate()
139            .await
140            .map_err(|e| MemoryError::Database(format!("graph store migration failed: {e}")))?;
141
142        let vector_store = EmbeddedVectorStore::new(dims);
143
144        Ok(Self {
145            fact_store: Arc::new(fact_store),
146            vector_store: Arc::new(vector_store),
147            graph_store: Arc::new(graph_store),
148            embedding,
149        })
150    }
151
152    // -----------------------------------------------------------------------
153    // Write operations
154    // -----------------------------------------------------------------------
155
156    /// Embed `text`, create a `Fact`, and persist it in both the fact store
157    /// and the vector store.
158    ///
159    /// Returns the `FactId` of the newly created fact.
160    pub async fn add_fact(&self, text: &str, scope: Scope) -> Result<FactId, MemoryError> {
161        // Embed text.
162        let mut embeddings = self.embedding.embed(&[text]).await?;
163        let embedding = embeddings.pop().ok_or_else(|| {
164            MemoryError::Embedding("provider returned empty embeddings".to_string())
165        })?;
166
167        // Build and persist the fact.
168        let mut fact = Fact::new(text, scope);
169        fact.embedding = embedding.clone();
170        let id = self.fact_store.insert_fact(fact).await?;
171
172        // Insert into vector store.
173        let metadata = serde_json::json!({ "fact_id": id.to_string() });
174        self.vector_store.upsert(id, embedding, metadata).await?;
175
176        Ok(id)
177    }
178
179    /// Semantically recall facts matching `query`.
180    ///
181    /// Embeds the query text, performs vector search, fetches full facts from
182    /// the fact store, filters by validity and scope, and records an access
183    /// event for each returned fact.
184    pub async fn recall(&self, query: &RecallQuery) -> Result<Vec<Fact>, MemoryError> {
185        let max_results = if query.max_results == 0 {
186            10
187        } else {
188            query.max_results
189        };
190
191        // Embed the query.
192        let mut embeddings = self.embedding.embed(&[query.query.as_str()]).await?;
193        let query_vec = embeddings.pop().ok_or_else(|| {
194            MemoryError::Embedding("provider returned empty embeddings".to_string())
195        })?;
196
197        // Vector search.
198        let filter = VectorFilter {
199            scope: query.scope.clone(),
200            min_score: query.min_score,
201        };
202        let matches = self
203            .vector_store
204            .search(&query_vec, &filter, max_results)
205            .await?;
206
207        // Fetch full facts from the fact store.
208        let mut facts = Vec::with_capacity(matches.len());
209        for vm in matches {
210            match self.fact_store.get_fact(vm.id).await {
211                Ok(fact) => {
212                    // Validity filter.
213                    let valid = match query.as_of {
214                        Some(t) => fact.is_valid_at(t),
215                        None => fact.is_valid(),
216                    };
217                    if !valid {
218                        continue;
219                    }
220                    // Scope filter (post-fetch).
221                    if let Some(ref scope) = query.scope {
222                        if !scope.contains(&fact.scope) {
223                            continue;
224                        }
225                    }
226                    // Record access (fire-and-forget; ignore error).
227                    let _ = self.fact_store.record_access(fact.id).await;
228                    facts.push(fact);
229                }
230                Err(MemoryError::NotFound(_)) => {
231                    // Vector store has a stale entry — skip silently.
232                }
233                Err(e) => return Err(e),
234            }
235        }
236
237        Ok(facts)
238    }
239
240    // -----------------------------------------------------------------------
241    // Read operations
242    // -----------------------------------------------------------------------
243
244    /// List currently-valid facts for the given scope.
245    pub async fn list_facts(&self, scope: Option<Scope>) -> Result<Vec<Fact>, MemoryError> {
246        let filter = match scope {
247            Some(s) => FactFilter::new().with_scope(s),
248            None => FactFilter::new(),
249        };
250        self.fact_store.list_facts(&filter).await
251    }
252
253    // -----------------------------------------------------------------------
254    // Mutation operations
255    // -----------------------------------------------------------------------
256
257    /// Invalidate (soft-delete) a fact and remove it from the vector store.
258    ///
259    /// The fact record is preserved for historical queries. `reason` is
260    /// currently logged via tracing but not stored (reserved for future use).
261    pub async fn forget(&self, id: FactId, _reason: Option<&str>) -> Result<(), MemoryError> {
262        self.fact_store.invalidate_fact(id).await?;
263        self.vector_store.delete(id).await?;
264        Ok(())
265    }
266
267    /// Hard-delete all data (facts, vectors, graph nodes/edges) for `scope`.
268    ///
269    /// Returns the number of facts deleted. Intended for GDPR / right-to-erasure
270    /// requests.
271    pub async fn delete_user_data(&self, scope: Scope) -> Result<u64, MemoryError> {
272        let fact_count = self.fact_store.delete_scope_data(&scope).await?;
273        self.vector_store.delete_by_scope(&scope).await?;
274        self.graph_store.delete_by_scope(&scope).await?;
275        Ok(fact_count)
276    }
277
278    // -----------------------------------------------------------------------
279    // Stats & export/import
280    // -----------------------------------------------------------------------
281
282    /// Return aggregate statistics for the fact store.
283    pub async fn stats(&self, _scope: Option<Scope>) -> Result<StoreStats, MemoryError> {
284        self.fact_store.stats().await
285    }
286
287    /// Export all currently-valid facts for `scope`.
288    pub async fn export(&self, scope: Option<Scope>) -> Result<Vec<Fact>, MemoryError> {
289        let filter = match scope {
290            Some(s) => FactFilter::new().with_scope(s),
291            None => FactFilter::new(),
292        };
293        self.fact_store.export(&filter).await
294    }
295
296    /// Import a batch of facts, re-embedding each one.
297    ///
298    /// Returns the number of facts successfully imported (skips duplicates by id).
299    pub async fn import(&self, facts: Vec<Fact>) -> Result<u64, MemoryError> {
300        let mut imported: u64 = 0;
301        for mut fact in facts {
302            // Re-embed the fact text.
303            let mut embeddings = self.embedding.embed(&[fact.text.as_str()]).await?;
304            let embedding = embeddings.pop().ok_or_else(|| {
305                MemoryError::Embedding("provider returned empty embeddings".to_string())
306            })?;
307            fact.embedding = embedding.clone();
308
309            let fact_id = fact.id;
310            self.fact_store.insert_fact(fact).await?;
311
312            let metadata = serde_json::json!({ "fact_id": fact_id.to_string() });
313            self.vector_store
314                .upsert(fact_id, embedding, metadata)
315                .await?;
316            imported += 1;
317        }
318        Ok(imported)
319    }
320
321    // -----------------------------------------------------------------------
322    // Consolidation
323    // -----------------------------------------------------------------------
324
325    /// Run a consolidation cycle over the given scope.
326    ///
327    /// Operations (decay, promote, dedup, summarize, reflect) are controlled
328    /// by `config.enabled_ops`. LLM-dependent operations (summarize, reflect)
329    /// are skipped when `llm` is `None`.
330    pub async fn consolidate(
331        &self,
332        scope: &Scope,
333        llm: Option<&dyn LlmClient>,
334        config: ConsolidationConfig,
335    ) -> Result<ConsolidationResult, MemoryError> {
336        let engine = ConsolidationEngine::new(
337            self.fact_store.clone(),
338            self.vector_store.clone(),
339            self.embedding.clone(),
340            config,
341        );
342        engine.run(scope, llm).await
343    }
344
345    // -----------------------------------------------------------------------
346    // Context assembly
347    // -----------------------------------------------------------------------
348
349    /// Assemble a token-budgeted context block for LLM prompt injection.
350    ///
351    /// Retrieves relevant facts via hybrid search (vector + keyword + graph),
352    /// ranks by tier priority (Working > Conversation > Knowledge), fills the
353    /// token budget greedily, and formats the output.
354    pub async fn context(
355        &self,
356        query: &str,
357        scope: &Scope,
358        config: ContextConfig,
359    ) -> Result<ContextBlock, MemoryError> {
360        let builder = ContextBuilder::new(
361            self.fact_store.clone(),
362            self.vector_store.clone(),
363            self.graph_store.clone(),
364            self.embedding.clone(),
365            config,
366        );
367        builder.build(query, scope).await
368    }
369
370    // -----------------------------------------------------------------------
371    // Extraction pipeline
372    // -----------------------------------------------------------------------
373
374    /// Ingest conversation messages: extract facts via LLM, detect conflicts,
375    /// store facts + entities + relationships.
376    ///
377    /// Returns the IDs of newly created facts.
378    pub async fn add_messages(
379        &self,
380        messages: &[Message],
381        scope: Scope,
382        llm: Box<dyn LlmClient>,
383        config: ExtractionConfig,
384    ) -> Result<Vec<FactId>, MemoryError> {
385        let pipeline = ExtractionPipeline::new(llm, config);
386        let extraction = pipeline.extract(messages).await?;
387
388        let mut fact_ids = Vec::new();
389
390        for extracted in extraction.facts {
391            // Create and embed the fact
392            let mut embeddings = self.embedding.embed(&[extracted.text.as_str()]).await?;
393            let embedding = embeddings
394                .pop()
395                .ok_or_else(|| MemoryError::Embedding("empty embedding".to_string()))?;
396
397            let mut fact = Fact::new(&extracted.text, scope.clone());
398            fact.confidence = Some(extracted.confidence as f32);
399            fact.category = extracted.category;
400            fact.embedding = embedding.clone();
401
402            let id = self.fact_store.insert_fact(fact).await?;
403            self.vector_store
404                .upsert(id, embedding, serde_json::json!({}))
405                .await?;
406
407            // Store entities in graph
408            let mut entity_map: HashMap<String, uuid::Uuid> = HashMap::new();
409            for ext_entity in &extracted.entities {
410                let entity = Entity::new(&ext_entity.name, scope.clone())
411                    .with_type(ext_entity.entity_type.as_deref().unwrap_or("unknown"));
412                entity_map.insert(ext_entity.name.clone(), entity.id);
413                self.graph_store.upsert_entity(&entity).await?;
414            }
415
416            // Store relationships in graph
417            for ext_rel in &extracted.relationships {
418                if let (Some(&src_id), Some(&tgt_id)) = (
419                    entity_map.get(&ext_rel.source),
420                    entity_map.get(&ext_rel.target),
421                ) {
422                    let rel = Relationship::new(src_id, &ext_rel.relation, tgt_id, scope.clone());
423                    self.graph_store.upsert_relationship(&rel).await?;
424                }
425            }
426
427            fact_ids.push(id);
428        }
429
430        Ok(fact_ids)
431    }
432
433    // -----------------------------------------------------------------------
434    // Accessors
435    // -----------------------------------------------------------------------
436
437    /// Access the underlying `FactStore`.
438    pub fn fact_store(&self) -> &Arc<dyn FactStore> {
439        &self.fact_store
440    }
441
442    /// Access the underlying `VectorStore`.
443    pub fn vector_store(&self) -> &Arc<dyn VectorStore> {
444        &self.vector_store
445    }
446
447    /// Access the underlying `GraphStore`.
448    pub fn graph_store(&self) -> &Arc<dyn GraphStore> {
449        &self.graph_store
450    }
451}