Skip to main content

agent_diva_core/memory/
manager.rs

1//! Memory manager for handling long-term memory
2
3use super::storage::{DailyNote, Memory};
4use std::path::{Path, PathBuf};
5
6/// Manages long-term memory storage
7#[derive(Debug)]
8pub struct MemoryManager {
9    /// Workspace directory
10    _workspace: PathBuf,
11    /// Memory file path
12    memory_path: PathBuf,
13    /// Daily notes directory
14    notes_dir: PathBuf,
15    /// History file path
16    history_path: PathBuf,
17}
18
19impl MemoryManager {
20    /// Create a new memory manager
21    pub fn new<P: AsRef<Path>>(workspace: P) -> Self {
22        let workspace = workspace.as_ref().to_path_buf();
23        let memory_path = workspace.join("memory").join("MEMORY.md");
24        let history_path = workspace.join("memory").join("HISTORY.md");
25        let notes_dir = workspace.join("memory");
26
27        Self {
28            _workspace: workspace,
29            memory_path,
30            notes_dir,
31            history_path,
32        }
33    }
34
35    /// Load the long-term memory
36    pub fn load_memory(&self) -> Memory {
37        if self.memory_path.exists() {
38            match std::fs::read_to_string(&self.memory_path) {
39                Ok(content) => Memory::with_content(content),
40                Err(_) => Memory::new(),
41            }
42        } else {
43            Memory::new()
44        }
45    }
46
47    /// Save the long-term memory
48    pub fn save_memory(&self, memory: &Memory) -> crate::Result<()> {
49        if let Some(parent) = self.memory_path.parent() {
50            std::fs::create_dir_all(parent)?;
51        }
52        std::fs::write(&self.memory_path, &memory.content)?;
53        Ok(())
54    }
55
56    /// Load history entries from `HISTORY.md`
57    pub fn load_history(&self) -> String {
58        if self.history_path.exists() {
59            std::fs::read_to_string(&self.history_path).unwrap_or_default()
60        } else {
61            String::new()
62        }
63    }
64
65    /// Append an entry to `HISTORY.md`
66    pub fn append_history(&self, entry: &str) -> crate::Result<()> {
67        if entry.trim().is_empty() {
68            return Ok(());
69        }
70        if let Some(parent) = self.history_path.parent() {
71            std::fs::create_dir_all(parent)?;
72        }
73        let mut content = self.load_history();
74        if !content.is_empty() && !content.ends_with('\n') {
75            content.push('\n');
76        }
77        content.push_str(entry.trim_end());
78        content.push_str("\n\n");
79        std::fs::write(&self.history_path, content)?;
80        Ok(())
81    }
82
83    /// Load a daily note
84    pub fn load_daily_note(&self, date: impl AsRef<str>) -> DailyNote {
85        let date = date.as_ref();
86        let path = self.notes_dir.join(format!("{}.md", date));
87
88        if path.exists() {
89            match std::fs::read_to_string(&path) {
90                Ok(content) => {
91                    let mut note = DailyNote::for_date(date);
92                    note.content = content;
93                    note
94                }
95                Err(_) => DailyNote::for_date(date),
96            }
97        } else {
98            DailyNote::for_date(date)
99        }
100    }
101
102    /// Load today's note
103    pub fn load_today_note(&self) -> DailyNote {
104        let today = chrono::Local::now().format("%Y-%m-%d").to_string();
105        self.load_daily_note(&today)
106    }
107
108    /// Save a daily note
109    pub fn save_daily_note(&self, note: &DailyNote) -> crate::Result<()> {
110        std::fs::create_dir_all(&self.notes_dir)?;
111        let path = self.notes_dir.join(note.filename());
112        std::fs::write(&path, &note.content)?;
113        Ok(())
114    }
115
116    /// List all daily notes
117    pub fn list_notes(&self) -> Vec<String> {
118        let mut notes = Vec::new();
119
120        if let Ok(entries) = std::fs::read_dir(&self.notes_dir) {
121            for entry in entries.flatten() {
122                if let Some(name) = entry.file_name().to_str() {
123                    if name.ends_with(".md") && name != "MEMORY.md" {
124                        let date = name.trim_end_matches(".md").to_string();
125                        notes.push(date);
126                    }
127                }
128            }
129        }
130
131        notes.sort_by(|a, b| b.cmp(a)); // Newest first
132        notes
133    }
134
135    /// Get the memory directory path
136    pub fn memory_dir(&self) -> &Path {
137        &self.notes_dir
138    }
139
140    /// Append content to today's daily note
141    pub fn append_today(&self, content: &str) -> crate::Result<()> {
142        let mut note = self.load_today_note();
143
144        if note.content.is_empty() {
145            // Add header for new day
146            let today = chrono::Local::now().format("%Y-%m-%d").to_string();
147            note.content = format!("# {}\n\n{}", today, content);
148        } else {
149            // Append to existing content
150            note.content.push('\n');
151            note.content.push_str(content);
152        }
153
154        self.save_daily_note(&note)
155    }
156
157    /// Get memories from the last N days
158    pub fn get_recent_memories(&self, days: usize) -> String {
159        use chrono::Duration;
160
161        let mut memories = Vec::new();
162        let today = chrono::Local::now().date_naive();
163
164        for i in 0..days {
165            let date = today - Duration::days(i as i64);
166            let date_str = date.format("%Y-%m-%d").to_string();
167            let note = self.load_daily_note(&date_str);
168
169            if !note.content.is_empty() {
170                memories.push(note.content);
171            }
172        }
173
174        memories.join("\n\n---\n\n")
175    }
176
177    /// List all memory files sorted by date (newest first)
178    pub fn list_memory_files(&self) -> Vec<PathBuf> {
179        let mut files = Vec::new();
180
181        if let Ok(entries) = std::fs::read_dir(&self.notes_dir) {
182            for entry in entries.flatten() {
183                let path = entry.path();
184                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
185                    // Match pattern YYYY-MM-DD.md
186                    if name.len() == 13 && name.ends_with(".md") && name != "MEMORY.md" {
187                        let date_part = &name[..10];
188                        // Basic validation: check if it looks like a date
189                        if date_part.chars().filter(|c| *c == '-').count() == 2 {
190                            files.push(path);
191                        }
192                    }
193                }
194            }
195        }
196
197        // Sort by filename (which is the date) in reverse order
198        files.sort_by(|a, b| b.cmp(a));
199        files
200    }
201
202    /// Get memory context for the agent.
203    /// The redesigned memory model injects only long-term memory into prompts.
204    pub fn get_memory_context(&self) -> String {
205        let memory = self.load_memory();
206        if memory.content.is_empty() {
207            String::new()
208        } else {
209            format!("## Long-term Memory\n{}", memory.content)
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use tempfile::TempDir;
218
219    #[test]
220    fn test_memory_manager_creation() {
221        let temp_dir = TempDir::new().unwrap();
222        let manager = MemoryManager::new(temp_dir.path());
223        assert_eq!(manager._workspace, temp_dir.path());
224    }
225
226    #[test]
227    fn test_load_save_memory() {
228        let temp_dir = TempDir::new().unwrap();
229        let manager = MemoryManager::new(temp_dir.path());
230
231        let memory = Memory::with_content("Test memory");
232        manager.save_memory(&memory).unwrap();
233
234        let loaded = manager.load_memory();
235        assert_eq!(loaded.content, "Test memory");
236    }
237
238    #[test]
239    fn test_load_save_daily_note() {
240        let temp_dir = TempDir::new().unwrap();
241        let manager = MemoryManager::new(temp_dir.path());
242
243        let mut note = DailyNote::for_date("2024-01-15");
244        note.content = "Test note".to_string();
245        manager.save_daily_note(&note).unwrap();
246
247        let loaded = manager.load_daily_note("2024-01-15");
248        assert_eq!(loaded.content, "Test note");
249    }
250
251    #[test]
252    fn test_list_notes() {
253        let temp_dir = TempDir::new().unwrap();
254        let manager = MemoryManager::new(temp_dir.path());
255
256        let mut note1 = DailyNote::for_date("2024-01-15");
257        note1.content = "Note 1".to_string();
258        manager.save_daily_note(&note1).unwrap();
259
260        let mut note2 = DailyNote::for_date("2024-01-16");
261        note2.content = "Note 2".to_string();
262        manager.save_daily_note(&note2).unwrap();
263
264        let notes = manager.list_notes();
265        assert_eq!(notes.len(), 2);
266        assert_eq!(notes[0], "2024-01-16"); // Newest first
267    }
268
269    #[test]
270    fn test_append_today() {
271        let temp_dir = TempDir::new().unwrap();
272        let manager = MemoryManager::new(temp_dir.path());
273
274        // First append - should add header
275        manager.append_today("First entry").unwrap();
276        let note = manager.load_today_note();
277        assert!(note.content.contains("First entry"));
278        assert!(note.content.starts_with("#"));
279
280        // Second append - should not add another header
281        manager.append_today("Second entry").unwrap();
282        let note = manager.load_today_note();
283        assert!(note.content.contains("First entry"));
284        assert!(note.content.contains("Second entry"));
285    }
286
287    #[test]
288    fn test_get_recent_memories() {
289        let temp_dir = TempDir::new().unwrap();
290        let manager = MemoryManager::new(temp_dir.path());
291
292        // Create some notes
293        let mut note1 = DailyNote::for_date("2024-01-15");
294        note1.content = "Memory 1".to_string();
295        manager.save_daily_note(&note1).unwrap();
296
297        let mut note2 = DailyNote::for_date("2024-01-16");
298        note2.content = "Memory 2".to_string();
299        manager.save_daily_note(&note2).unwrap();
300
301        // Get recent memories (this will get today's date range, so may not include our test dates)
302        let recent = manager.get_recent_memories(7);
303        // Just verify it doesn't panic
304        assert!(recent.is_empty() || !recent.is_empty());
305    }
306
307    #[test]
308    fn test_list_memory_files() {
309        let temp_dir = TempDir::new().unwrap();
310        let manager = MemoryManager::new(temp_dir.path());
311
312        let mut note1 = DailyNote::for_date("2024-01-15");
313        note1.content = "Note 1".to_string();
314        manager.save_daily_note(&note1).unwrap();
315
316        let mut note2 = DailyNote::for_date("2024-01-16");
317        note2.content = "Note 2".to_string();
318        manager.save_daily_note(&note2).unwrap();
319
320        let files = manager.list_memory_files();
321        assert_eq!(files.len(), 2);
322        // Should be sorted newest first
323        assert!(files[0].to_str().unwrap().contains("2024-01-16"));
324    }
325
326    #[test]
327    fn test_get_memory_context() {
328        let temp_dir = TempDir::new().unwrap();
329        let manager = MemoryManager::new(temp_dir.path());
330
331        // Set up long-term memory
332        let memory = Memory::with_content("Long term info");
333        manager.save_memory(&memory).unwrap();
334
335        // Get context
336        let context = manager.get_memory_context();
337        assert!(context.contains("Long-term Memory"));
338        assert!(context.contains("Long term info"));
339    }
340
341    #[test]
342    fn test_get_memory_context_empty() {
343        let temp_dir = TempDir::new().unwrap();
344        let manager = MemoryManager::new(temp_dir.path());
345
346        let context = manager.get_memory_context();
347        assert_eq!(context, "");
348    }
349
350    #[test]
351    fn test_append_history() {
352        let temp_dir = TempDir::new().unwrap();
353        let manager = MemoryManager::new(temp_dir.path());
354        manager
355            .append_history("[2026-02-12 09:00] Added memory event")
356            .unwrap();
357
358        let history = manager.load_history();
359        assert!(history.contains("Added memory event"));
360    }
361}