Skip to main content

daimon_core/
episodic_memory.rs

1//! Episodic memory trait: a structured, timestamped event log.
2//!
3//! Unlike [`Memory`](crate::memory::Memory), which records what was *said*
4//! (chat messages), [`EpisodicMemory`] records what *happened* — discrete,
5//! typed events with metadata, queryable by type and time range. Built-in
6//! implementations live in the `daimon` facade crate.
7
8use std::future::Future;
9use std::pin::Pin;
10use std::sync::Arc;
11
12use serde_json::Value;
13
14use crate::error::Result;
15
16/// A single recorded event.
17#[derive(Debug, Clone)]
18pub struct EpisodicEvent {
19    /// Backend-assigned unique identifier.
20    pub id: String,
21    /// Caller-defined event category (e.g. `"tool_call"`, `"user_login"`).
22    pub event_type: String,
23    /// Arbitrary structured payload.
24    pub payload: Value,
25    /// Unix epoch milliseconds at which the event was recorded.
26    pub timestamp_ms: i64,
27}
28
29/// Filter for [`EpisodicMemory::query`]. All fields are optional; an
30/// all-`None` query returns every event (subject to `limit`).
31#[derive(Debug, Clone, Default)]
32pub struct EpisodicQuery {
33    /// Restrict to events with this exact `event_type`.
34    pub event_type: Option<String>,
35    /// Restrict to events at or after this Unix epoch millisecond timestamp.
36    pub since_ms: Option<i64>,
37    /// Restrict to events at or before this Unix epoch millisecond timestamp.
38    pub until_ms: Option<i64>,
39    /// Maximum number of events to return, most recent first.
40    pub limit: Option<usize>,
41}
42
43impl EpisodicQuery {
44    /// An unfiltered query with no limit.
45    pub fn all() -> Self {
46        Self::default()
47    }
48
49    /// Restricts the query to a single event type.
50    pub fn of_type(mut self, event_type: impl Into<String>) -> Self {
51        self.event_type = Some(event_type.into());
52        self
53    }
54
55    /// Restricts the query to the given time range (inclusive).
56    pub fn between(mut self, since_ms: i64, until_ms: i64) -> Self {
57        self.since_ms = Some(since_ms);
58        self.until_ms = Some(until_ms);
59        self
60    }
61
62    /// Caps the number of returned events.
63    pub fn limit(mut self, limit: usize) -> Self {
64        self.limit = Some(limit);
65        self
66    }
67}
68
69/// Trait for structured, timestamped event log backends.
70pub trait EpisodicMemory: Send + Sync {
71    /// Records an event and returns its assigned id. Implementations stamp
72    /// `timestamp_ms` with the current time.
73    fn record(
74        &self,
75        event_type: &str,
76        payload: Value,
77    ) -> impl Future<Output = Result<String>> + Send;
78
79    /// Returns events matching `query`, most recent first.
80    fn query(
81        &self,
82        query: EpisodicQuery,
83    ) -> impl Future<Output = Result<Vec<EpisodicEvent>>> + Send;
84
85    /// Removes all recorded events.
86    fn clear(&self) -> impl Future<Output = Result<()>> + Send;
87}
88
89/// Object-safe wrapper for the `EpisodicMemory` trait, enabling dynamic
90/// dispatch via `Arc<dyn ErasedEpisodicMemory>`.
91pub trait ErasedEpisodicMemory: Send + Sync {
92    fn record_erased<'a>(
93        &'a self,
94        event_type: &'a str,
95        payload: Value,
96    ) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>;
97
98    fn query_erased(
99        &self,
100        query: EpisodicQuery,
101    ) -> Pin<Box<dyn Future<Output = Result<Vec<EpisodicEvent>>> + Send + '_>>;
102
103    fn clear_erased(&self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
104}
105
106impl<T: EpisodicMemory> ErasedEpisodicMemory for T {
107    fn record_erased<'a>(
108        &'a self,
109        event_type: &'a str,
110        payload: Value,
111    ) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
112        Box::pin(self.record(event_type, payload))
113    }
114
115    fn query_erased(
116        &self,
117        query: EpisodicQuery,
118    ) -> Pin<Box<dyn Future<Output = Result<Vec<EpisodicEvent>>> + Send + '_>> {
119        Box::pin(self.query(query))
120    }
121
122    fn clear_erased(&self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
123        Box::pin(self.clear())
124    }
125}
126
127/// Shared ownership of episodic memory via `Arc<dyn ErasedEpisodicMemory>`.
128pub type SharedEpisodicMemory = Arc<dyn ErasedEpisodicMemory>;
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use std::sync::Mutex;
134
135    struct VecEpisodicMemory(Mutex<Vec<EpisodicEvent>>);
136
137    impl EpisodicMemory for VecEpisodicMemory {
138        async fn record(&self, event_type: &str, payload: Value) -> Result<String> {
139            let mut events = self.0.lock().unwrap();
140            let seq = events.len() as i64;
141            let id = format!("evt-{seq}");
142            events.push(EpisodicEvent {
143                id: id.clone(),
144                event_type: event_type.to_string(),
145                payload,
146                timestamp_ms: seq,
147            });
148            Ok(id)
149        }
150
151        async fn query(&self, query: EpisodicQuery) -> Result<Vec<EpisodicEvent>> {
152            let events = self.0.lock().unwrap();
153            let mut matched: Vec<EpisodicEvent> = events
154                .iter()
155                .filter(|e| {
156                    query.event_type.as_ref().is_none_or(|t| &e.event_type == t)
157                        && query.since_ms.is_none_or(|s| e.timestamp_ms >= s)
158                        && query.until_ms.is_none_or(|u| e.timestamp_ms <= u)
159                })
160                .cloned()
161                .collect();
162            matched.sort_by_key(|e| std::cmp::Reverse(e.timestamp_ms));
163            if let Some(limit) = query.limit {
164                matched.truncate(limit);
165            }
166            Ok(matched)
167        }
168
169        async fn clear(&self) -> Result<()> {
170            self.0.lock().unwrap().clear();
171            Ok(())
172        }
173    }
174
175    #[tokio::test]
176    async fn episodic_memory_is_implementable_from_core_alone() {
177        let mem = VecEpisodicMemory(Mutex::new(Vec::new()));
178        mem.record("login", serde_json::json!({"user": "a"}))
179            .await
180            .unwrap();
181        mem.record("logout", serde_json::json!({"user": "a"}))
182            .await
183            .unwrap();
184        mem.record("login", serde_json::json!({"user": "b"}))
185            .await
186            .unwrap();
187
188        let logins = mem
189            .query(EpisodicQuery::all().of_type("login"))
190            .await
191            .unwrap();
192        assert_eq!(logins.len(), 2);
193        // Most recent first.
194        assert_eq!(logins[0].payload["user"], "b");
195
196        let ranged = mem.query(EpisodicQuery::all().between(0, 0)).await.unwrap();
197        assert_eq!(ranged.len(), 1);
198
199        let limited = mem.query(EpisodicQuery::all().limit(1)).await.unwrap();
200        assert_eq!(limited.len(), 1);
201
202        mem.clear().await.unwrap();
203        assert!(mem.query(EpisodicQuery::all()).await.unwrap().is_empty());
204
205        let shared: SharedEpisodicMemory = Arc::new(VecEpisodicMemory(Mutex::new(Vec::new())));
206        shared
207            .record_erased("test", serde_json::json!(null))
208            .await
209            .unwrap();
210        assert_eq!(
211            shared
212                .query_erased(EpisodicQuery::all())
213                .await
214                .unwrap()
215                .len(),
216            1
217        );
218    }
219}