Skip to main content

daimon_core/
memory.rs

1//! Conversation memory trait. Implement [`Memory`] for custom backends;
2//! built-in implementations live in the `daimon` facade crate.
3
4use std::future::Future;
5use std::pin::Pin;
6
7use crate::error::Result;
8use crate::types::Message;
9
10/// Trait for conversation memory backends. Stores and retrieves messages for agent context.
11pub trait Memory: Send + Sync {
12    /// Appends a message to the history. Order is preserved.
13    ///
14    /// Takes a borrow so callers that keep the message (the agent runner
15    /// appends it to its working log after persisting) don't clone it per
16    /// iteration. Backends that store owned messages clone internally;
17    /// serializing backends (Redis, SQLite) never need ownership at all.
18    fn add_message(&self, message: &Message) -> impl Future<Output = Result<()>> + Send;
19
20    /// Returns all stored messages in order. Used to build context for the model.
21    fn get_messages(&self) -> impl Future<Output = Result<Vec<Message>>> + Send;
22
23    /// Runs `f` over the stored messages without handing out an owned copy.
24    ///
25    /// The default implementation falls back to [`Memory::get_messages`]
26    /// (one full clone); in-process backends override it to borrow their
27    /// storage under the lock, so read-only consumers (serializing history
28    /// to disk, measuring it, rendering it) don't pay an O(history) deep
29    /// copy per call.
30    fn with_messages<R, F>(&self, f: F) -> impl Future<Output = Result<R>> + Send
31    where
32        F: FnOnce(&[Message]) -> R + Send,
33        R: Send,
34    {
35        async move { Ok(f(&self.get_messages().await?)) }
36    }
37
38    /// Removes all messages. Use when starting a new conversation.
39    fn clear(&self) -> impl Future<Output = Result<()>> + Send;
40}
41
42/// Object-safe wrapper for the `Memory` trait, enabling dynamic dispatch via `Arc<dyn ErasedMemory>`.
43pub trait ErasedMemory: Send + Sync {
44    fn add_message_erased<'a>(
45        &'a self,
46        message: &'a Message,
47    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
48
49    fn get_messages_erased<'a>(
50        &'a self,
51    ) -> Pin<Box<dyn Future<Output = Result<Vec<Message>>> + Send + 'a>>;
52
53    /// Object-safe counterpart of [`Memory::with_messages`]. Callers return
54    /// values by writing into locals captured by `f`.
55    ///
56    /// Default-implemented (via [`ErasedMemory::get_messages_erased`], one
57    /// full clone) so direct `ErasedMemory` implementors written before this
58    /// method existed keep compiling. The blanket impl for `T: Memory`
59    /// overrides it to route through [`Memory::with_messages`], so backends
60    /// that override that get their no-clone path through [`SharedMemory`]
61    /// too.
62    fn with_messages_erased<'a>(
63        &'a self,
64        f: Box<dyn FnOnce(&[Message]) + Send + 'a>,
65    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
66        Box::pin(async move {
67            let messages = self.get_messages_erased().await?;
68            f(&messages);
69            Ok(())
70        })
71    }
72
73    fn clear_erased<'a>(&'a self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
74}
75
76impl<T: Memory> ErasedMemory for T {
77    fn add_message_erased<'a>(
78        &'a self,
79        message: &'a Message,
80    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
81        Box::pin(self.add_message(message))
82    }
83
84    fn get_messages_erased<'a>(
85        &'a self,
86    ) -> Pin<Box<dyn Future<Output = Result<Vec<Message>>> + Send + 'a>> {
87        Box::pin(self.get_messages())
88    }
89
90    fn with_messages_erased<'a>(
91        &'a self,
92        f: Box<dyn FnOnce(&[Message]) + Send + 'a>,
93    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
94        Box::pin(self.with_messages(f))
95    }
96
97    fn clear_erased<'a>(&'a self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
98        Box::pin(self.clear())
99    }
100}
101
102/// Shared ownership of memory via `Arc<dyn ErasedMemory>`. Used by the agent.
103pub type SharedMemory = std::sync::Arc<dyn ErasedMemory>;
104
105#[cfg(test)]
106mod tests {
107    use super::{ErasedMemory, Memory, SharedMemory};
108    use crate::{Message, Result, Role};
109    use std::future::Future;
110    use std::pin::Pin;
111    use std::sync::{Arc, Mutex};
112
113    /// A provider-crate-style Memory impl using only daimon_core items.
114    ///
115    /// Deliberately does NOT override `with_messages`: it doubles as a
116    /// compile-level regression test that an external `Memory` impl written
117    /// before `with_messages` existed still compiles and works erased.
118    struct VecMemory(Mutex<Vec<Message>>);
119
120    impl Memory for VecMemory {
121        async fn add_message(&self, message: &Message) -> Result<()> {
122            self.0.lock().unwrap().push(message.clone());
123            Ok(())
124        }
125
126        async fn get_messages(&self) -> Result<Vec<Message>> {
127            Ok(self.0.lock().unwrap().clone())
128        }
129
130        async fn clear(&self) -> Result<()> {
131            self.0.lock().unwrap().clear();
132            Ok(())
133        }
134    }
135
136    #[tokio::test]
137    async fn memory_is_implementable_from_core_alone() {
138        let mem = VecMemory(Mutex::new(Vec::new()));
139        mem.add_message(&Message::user("hi")).await.unwrap();
140        assert_eq!(mem.get_messages().await.unwrap().len(), 1);
141        assert_eq!(mem.get_messages().await.unwrap()[0].role, Role::User);
142        mem.clear().await.unwrap();
143        assert!(mem.get_messages().await.unwrap().is_empty());
144
145        let shared: SharedMemory = Arc::new(VecMemory(Mutex::new(Vec::new())));
146        shared
147            .add_message_erased(&Message::user("x"))
148            .await
149            .unwrap();
150        assert_eq!(shared.get_messages_erased().await.unwrap().len(), 1);
151    }
152
153    #[tokio::test]
154    async fn with_messages_default_matches_get_messages() {
155        let mem = VecMemory(Mutex::new(Vec::new()));
156        mem.add_message(&Message::user("one")).await.unwrap();
157        mem.add_message(&Message::assistant("two")).await.unwrap();
158
159        let owned = mem.get_messages().await.unwrap();
160        let borrowed = mem
161            .with_messages(|messages| {
162                messages
163                    .iter()
164                    .map(|m| (m.role.clone(), m.content.clone()))
165                    .collect::<Vec<_>>()
166            })
167            .await
168            .unwrap();
169
170        assert_eq!(borrowed.len(), owned.len());
171        for (seen, expected) in borrowed.iter().zip(&owned) {
172            assert_eq!(seen.0, expected.role);
173            assert_eq!(seen.1, expected.content);
174        }
175    }
176
177    #[tokio::test]
178    async fn with_messages_erased_works_through_shared_memory() {
179        let shared: SharedMemory = Arc::new(VecMemory(Mutex::new(Vec::new())));
180        shared
181            .add_message_erased(&Message::user("x"))
182            .await
183            .unwrap();
184        shared
185            .add_message_erased(&Message::assistant("y"))
186            .await
187            .unwrap();
188
189        // The erased visitor returns () — values come back through a
190        // captured local.
191        let mut seen: Vec<Option<String>> = Vec::new();
192        shared
193            .with_messages_erased(Box::new(|messages| {
194                seen.extend(messages.iter().map(|m| m.content.clone()));
195            }))
196            .await
197            .unwrap();
198
199        assert_eq!(seen, vec![Some("x".into()), Some("y".into())]);
200    }
201
202    /// A direct `ErasedMemory` implementor (no `Memory` impl) that predates
203    /// `with_messages_erased`: relies on the trait's default body, proving
204    /// such impls keep compiling and functioning.
205    struct DirectErased(Mutex<Vec<Message>>);
206
207    impl ErasedMemory for DirectErased {
208        fn add_message_erased<'a>(
209            &'a self,
210            message: &'a Message,
211        ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
212            Box::pin(async move {
213                self.0.lock().unwrap().push(message.clone());
214                Ok(())
215            })
216        }
217
218        fn get_messages_erased<'a>(
219            &'a self,
220        ) -> Pin<Box<dyn Future<Output = Result<Vec<Message>>> + Send + 'a>> {
221            Box::pin(async move { Ok(self.0.lock().unwrap().clone()) })
222        }
223
224        fn clear_erased<'a>(&'a self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
225            Box::pin(async move {
226                self.0.lock().unwrap().clear();
227                Ok(())
228            })
229        }
230    }
231
232    #[tokio::test]
233    async fn direct_erased_impl_gets_default_with_messages_erased() {
234        let shared: SharedMemory = Arc::new(DirectErased(Mutex::new(Vec::new())));
235        shared
236            .add_message_erased(&Message::user("hello"))
237            .await
238            .unwrap();
239
240        let mut count = 0usize;
241        shared
242            .with_messages_erased(Box::new(|messages| count = messages.len()))
243            .await
244            .unwrap();
245        assert_eq!(count, 1);
246    }
247}