rexis_rag/agent/memory/
working.rs

1//! Working memory - temporary scratchpad for agent reasoning
2//!
3//! Working memory provides a temporary space for agents to store intermediate
4//! results, thoughts, and data during execution. It's session-scoped and typically
5//! cleared when the session ends.
6
7use crate::error::RragResult;
8use crate::storage::{Memory, MemoryValue};
9use std::sync::Arc;
10
11/// Working memory for temporary agent data
12pub struct WorkingMemory {
13    /// Storage backend
14    storage: Arc<dyn Memory>,
15
16    /// Namespace for this working memory (session::{session_id}::working)
17    namespace: String,
18
19    /// Whether to auto-clear on drop
20    auto_clear: bool,
21}
22
23impl WorkingMemory {
24    /// Create a new working memory
25    pub fn new(storage: Arc<dyn Memory>, session_id: String) -> Self {
26        let namespace = format!("session::{}::working", session_id);
27
28        Self {
29            storage,
30            namespace,
31            auto_clear: true,
32        }
33    }
34
35    /// Create working memory without auto-clear
36    pub fn new_persistent(storage: Arc<dyn Memory>, session_id: String) -> Self {
37        let namespace = format!("session::{}::working", session_id);
38
39        Self {
40            storage,
41            namespace,
42            auto_clear: false,
43        }
44    }
45
46    /// Set a value in working memory
47    pub async fn set(&self, key: &str, value: impl Into<MemoryValue>) -> RragResult<()> {
48        let full_key = self.make_key(key);
49        self.storage.set(&full_key, value.into()).await
50    }
51
52    /// Get a value from working memory
53    pub async fn get(&self, key: &str) -> RragResult<Option<MemoryValue>> {
54        let full_key = self.make_key(key);
55        self.storage.get(&full_key).await
56    }
57
58    /// Delete a value from working memory
59    pub async fn delete(&self, key: &str) -> RragResult<bool> {
60        let full_key = self.make_key(key);
61        self.storage.delete(&full_key).await
62    }
63
64    /// Check if a key exists in working memory
65    pub async fn exists(&self, key: &str) -> RragResult<bool> {
66        let full_key = self.make_key(key);
67        self.storage.exists(&full_key).await
68    }
69
70    /// Clear all working memory
71    pub async fn clear(&self) -> RragResult<()> {
72        self.storage.clear(Some(&self.namespace)).await
73    }
74
75    /// Get all keys in working memory
76    pub async fn keys(&self) -> RragResult<Vec<String>> {
77        use crate::storage::MemoryQuery;
78
79        let query = MemoryQuery::new().with_namespace(self.namespace.clone());
80        let all_keys = self.storage.keys(&query).await?;
81
82        // Strip namespace prefix
83        let prefix = format!("{}::", self.namespace);
84        let keys = all_keys
85            .into_iter()
86            .filter_map(|k| k.strip_prefix(&prefix).map(String::from))
87            .collect();
88
89        Ok(keys)
90    }
91
92    /// Set multiple values at once
93    pub async fn set_many(&self, pairs: &[(&str, MemoryValue)]) -> RragResult<()> {
94        let full_pairs: Vec<(String, MemoryValue)> = pairs
95            .iter()
96            .map(|(k, v)| (self.make_key(k), v.clone()))
97            .collect();
98
99        self.storage.mset(&full_pairs).await
100    }
101
102    /// Get multiple values at once
103    pub async fn get_many(&self, keys: &[&str]) -> RragResult<Vec<Option<MemoryValue>>> {
104        let full_keys: Vec<String> = keys.iter().map(|k| self.make_key(k)).collect();
105        self.storage.mget(&full_keys).await
106    }
107
108    /// Get count of items in working memory
109    pub async fn count(&self) -> RragResult<usize> {
110        self.storage.count(Some(&self.namespace)).await
111    }
112
113    /// Make a fully qualified key
114    fn make_key(&self, key: &str) -> String {
115        format!("{}::{}", self.namespace, key)
116    }
117
118    /// Disable auto-clear on drop
119    pub fn disable_auto_clear(&mut self) {
120        self.auto_clear = false;
121    }
122
123    /// Enable auto-clear on drop
124    pub fn enable_auto_clear(&mut self) {
125        self.auto_clear = true;
126    }
127}
128
129impl Drop for WorkingMemory {
130    fn drop(&mut self) {
131        if self.auto_clear {
132            // Best effort cleanup - we can't await in Drop
133            // In production, consider using a cleanup task
134            tracing::debug!(
135                namespace = %self.namespace,
136                "WorkingMemory dropped with auto_clear enabled - cleanup deferred"
137            );
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::storage::InMemoryStorage;
146
147    #[tokio::test]
148    async fn test_working_memory_basic_operations() {
149        let storage = Arc::new(InMemoryStorage::new());
150        let working = WorkingMemory::new(storage, "test-session".to_string());
151
152        // Set and get
153        working
154            .set("temp_result", MemoryValue::from(42i64))
155            .await
156            .unwrap();
157        let value = working.get("temp_result").await.unwrap();
158        assert_eq!(value.unwrap().as_integer(), Some(42));
159
160        // Exists
161        assert!(working.exists("temp_result").await.unwrap());
162        assert!(!working.exists("nonexistent").await.unwrap());
163
164        // Delete
165        assert!(working.delete("temp_result").await.unwrap());
166        assert!(!working.exists("temp_result").await.unwrap());
167    }
168
169    #[tokio::test]
170    async fn test_working_memory_multiple_operations() {
171        let storage = Arc::new(InMemoryStorage::new());
172        let working = WorkingMemory::new(storage, "test-session".to_string());
173
174        // Set multiple
175        let pairs = [
176            ("key1", MemoryValue::from("value1")),
177            ("key2", MemoryValue::from(100i64)),
178            ("key3", MemoryValue::from(true)),
179        ];
180        working.set_many(&pairs).await.unwrap();
181
182        // Get multiple
183        let keys = ["key1", "key2", "key3"];
184        let values = working.get_many(&keys).await.unwrap();
185
186        assert_eq!(values[0].as_ref().unwrap().as_string(), Some("value1"));
187        assert_eq!(values[1].as_ref().unwrap().as_integer(), Some(100));
188        assert_eq!(values[2].as_ref().unwrap().as_boolean(), Some(true));
189
190        // Count
191        assert_eq!(working.count().await.unwrap(), 3);
192
193        // Keys
194        let all_keys = working.keys().await.unwrap();
195        assert_eq!(all_keys.len(), 3);
196    }
197
198    #[tokio::test]
199    async fn test_working_memory_clear() {
200        let storage = Arc::new(InMemoryStorage::new());
201        let working = WorkingMemory::new(storage, "test-session".to_string());
202
203        // Add some data
204        working
205            .set("key1", MemoryValue::from("value1"))
206            .await
207            .unwrap();
208        working
209            .set("key2", MemoryValue::from("value2"))
210            .await
211            .unwrap();
212
213        assert_eq!(working.count().await.unwrap(), 2);
214
215        // Clear
216        working.clear().await.unwrap();
217        assert_eq!(working.count().await.unwrap(), 0);
218    }
219
220    #[tokio::test]
221    async fn test_working_memory_namespace_isolation() {
222        let storage = Arc::new(InMemoryStorage::new());
223        let working1 = WorkingMemory::new(storage.clone(), "session1".to_string());
224        let working2 = WorkingMemory::new(storage.clone(), "session2".to_string());
225
226        // Set in different sessions
227        working1
228            .set("data", MemoryValue::from("session1-data"))
229            .await
230            .unwrap();
231        working2
232            .set("data", MemoryValue::from("session2-data"))
233            .await
234            .unwrap();
235
236        // Verify isolation
237        let value1 = working1.get("data").await.unwrap();
238        let value2 = working2.get("data").await.unwrap();
239
240        assert_eq!(value1.unwrap().as_string(), Some("session1-data"));
241        assert_eq!(value2.unwrap().as_string(), Some("session2-data"));
242    }
243}