Skip to main content

daimon_core/
archival_memory.rs

1//! Archival memory trait: explicit write/search over long-term facts.
2//!
3//! Unlike [`Memory`](crate::memory::Memory) (a linear, append-only
4//! conversation log) or [`VectorStore`](crate::vector_store::VectorStore) (a
5//! low-level embedding index), [`ArchivalMemory`] models a fact store that
6//! consumers write to explicitly and retrieve by relevance rather than by
7//! recency. Implementations may be lexical (full-text search), or compose an
8//! existing [`VectorStore`](crate::vector_store::VectorStore) +
9//! [`EmbeddingModel`](crate::embedding::EmbeddingModel) for semantic search.
10//! Built-in implementations live in the `daimon` facade crate.
11
12use std::collections::HashMap;
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16
17use serde_json::Value;
18
19use crate::error::Result;
20
21/// A fact stored in archival memory, returned from a search.
22#[derive(Debug, Clone)]
23pub struct ArchivalRecord {
24    /// Backend-assigned unique identifier.
25    pub id: String,
26    /// The fact's text content.
27    pub text: String,
28    /// Arbitrary metadata attached at insert time.
29    pub metadata: HashMap<String, Value>,
30    /// Relevance score from the search backend, if any (higher = more
31    /// relevant). `None` for retrieval methods that don't score.
32    pub score: Option<f64>,
33}
34
35/// Trait for long-term archival fact storage, decoupled from the turn-by-turn
36/// conversation log.
37///
38/// Facts are written explicitly via [`insert`](ArchivalMemory::insert) and
39/// retrieved by relevance via [`search`](ArchivalMemory::search) — never by
40/// simply replaying everything in insertion order.
41pub trait ArchivalMemory: Send + Sync {
42    /// Stores a fact and returns its assigned id.
43    fn insert(
44        &self,
45        text: &str,
46        metadata: HashMap<String, Value>,
47    ) -> impl Future<Output = Result<String>> + Send;
48
49    /// Returns up to `top_k` facts most relevant to `query`, ordered by
50    /// descending relevance.
51    fn search(
52        &self,
53        query: &str,
54        top_k: usize,
55    ) -> impl Future<Output = Result<Vec<ArchivalRecord>>> + Send;
56
57    /// Deletes a fact by id. Returns `true` if it existed.
58    fn delete(&self, id: &str) -> impl Future<Output = Result<bool>> + Send;
59
60    /// Returns the total number of stored facts.
61    fn count(&self) -> impl Future<Output = Result<usize>> + Send;
62}
63
64/// Object-safe wrapper for the `ArchivalMemory` trait, enabling dynamic
65/// dispatch via `Arc<dyn ErasedArchivalMemory>`.
66pub trait ErasedArchivalMemory: Send + Sync {
67    fn insert_erased<'a>(
68        &'a self,
69        text: &'a str,
70        metadata: HashMap<String, Value>,
71    ) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>;
72
73    fn search_erased<'a>(
74        &'a self,
75        query: &'a str,
76        top_k: usize,
77    ) -> Pin<Box<dyn Future<Output = Result<Vec<ArchivalRecord>>> + Send + 'a>>;
78
79    fn delete_erased<'a>(
80        &'a self,
81        id: &'a str,
82    ) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + 'a>>;
83
84    fn count_erased(&self) -> Pin<Box<dyn Future<Output = Result<usize>> + Send + '_>>;
85}
86
87impl<T: ArchivalMemory> ErasedArchivalMemory for T {
88    fn insert_erased<'a>(
89        &'a self,
90        text: &'a str,
91        metadata: HashMap<String, Value>,
92    ) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
93        Box::pin(self.insert(text, metadata))
94    }
95
96    fn search_erased<'a>(
97        &'a self,
98        query: &'a str,
99        top_k: usize,
100    ) -> Pin<Box<dyn Future<Output = Result<Vec<ArchivalRecord>>> + Send + 'a>> {
101        Box::pin(self.search(query, top_k))
102    }
103
104    fn delete_erased<'a>(
105        &'a self,
106        id: &'a str,
107    ) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + 'a>> {
108        Box::pin(self.delete(id))
109    }
110
111    fn count_erased(&self) -> Pin<Box<dyn Future<Output = Result<usize>> + Send + '_>> {
112        Box::pin(self.count())
113    }
114}
115
116/// Shared ownership of archival memory via `Arc<dyn ErasedArchivalMemory>`.
117pub type SharedArchivalMemory = Arc<dyn ErasedArchivalMemory>;
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use std::sync::Mutex;
123
124    struct VecArchivalMemory(Mutex<Vec<ArchivalRecord>>);
125
126    impl ArchivalMemory for VecArchivalMemory {
127        async fn insert(&self, text: &str, metadata: HashMap<String, Value>) -> Result<String> {
128            let mut records = self.0.lock().unwrap();
129            let id = format!("rec-{}", records.len());
130            records.push(ArchivalRecord {
131                id: id.clone(),
132                text: text.to_string(),
133                metadata,
134                score: None,
135            });
136            Ok(id)
137        }
138
139        async fn search(&self, query: &str, top_k: usize) -> Result<Vec<ArchivalRecord>> {
140            let records = self.0.lock().unwrap();
141            Ok(records
142                .iter()
143                .filter(|r| r.text.contains(query))
144                .take(top_k)
145                .cloned()
146                .collect())
147        }
148
149        async fn delete(&self, id: &str) -> Result<bool> {
150            let mut records = self.0.lock().unwrap();
151            let before = records.len();
152            records.retain(|r| r.id != id);
153            Ok(records.len() != before)
154        }
155
156        async fn count(&self) -> Result<usize> {
157            Ok(self.0.lock().unwrap().len())
158        }
159    }
160
161    #[tokio::test]
162    async fn archival_memory_is_implementable_from_core_alone() {
163        let mem = VecArchivalMemory(Mutex::new(Vec::new()));
164        let id = mem.insert("the sky is blue", HashMap::new()).await.unwrap();
165        mem.insert("water is wet", HashMap::new()).await.unwrap();
166
167        assert_eq!(mem.count().await.unwrap(), 2);
168        let results = mem.search("sky", 5).await.unwrap();
169        assert_eq!(results.len(), 1);
170        assert_eq!(results[0].id, id);
171
172        assert!(mem.delete(&id).await.unwrap());
173        assert_eq!(mem.count().await.unwrap(), 1);
174
175        let shared: SharedArchivalMemory = Arc::new(VecArchivalMemory(Mutex::new(Vec::new())));
176        shared
177            .insert_erased("erased fact", HashMap::new())
178            .await
179            .unwrap();
180        assert_eq!(shared.count_erased().await.unwrap(), 1);
181    }
182}