agent_diva_core/memory/
manager.rs1use super::storage::{DailyNote, Memory};
4use std::path::{Path, PathBuf};
5
6#[derive(Debug)]
8pub struct MemoryManager {
9 _workspace: PathBuf,
11 memory_path: PathBuf,
13 notes_dir: PathBuf,
15 history_path: PathBuf,
17}
18
19impl MemoryManager {
20 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 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 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 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 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 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 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 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, ¬e.content)?;
113 Ok(())
114 }
115
116 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)); notes
133 }
134
135 pub fn memory_dir(&self) -> &Path {
137 &self.notes_dir
138 }
139
140 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 let today = chrono::Local::now().format("%Y-%m-%d").to_string();
147 note.content = format!("# {}\n\n{}", today, content);
148 } else {
149 note.content.push('\n');
151 note.content.push_str(content);
152 }
153
154 self.save_daily_note(¬e)
155 }
156
157 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 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 if name.len() == 13 && name.ends_with(".md") && name != "MEMORY.md" {
187 let date_part = &name[..10];
188 if date_part.chars().filter(|c| *c == '-').count() == 2 {
190 files.push(path);
191 }
192 }
193 }
194 }
195 }
196
197 files.sort_by(|a, b| b.cmp(a));
199 files
200 }
201
202 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(¬e).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(¬e1).unwrap();
259
260 let mut note2 = DailyNote::for_date("2024-01-16");
261 note2.content = "Note 2".to_string();
262 manager.save_daily_note(¬e2).unwrap();
263
264 let notes = manager.list_notes();
265 assert_eq!(notes.len(), 2);
266 assert_eq!(notes[0], "2024-01-16"); }
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 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 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 let mut note1 = DailyNote::for_date("2024-01-15");
294 note1.content = "Memory 1".to_string();
295 manager.save_daily_note(¬e1).unwrap();
296
297 let mut note2 = DailyNote::for_date("2024-01-16");
298 note2.content = "Memory 2".to_string();
299 manager.save_daily_note(¬e2).unwrap();
300
301 let recent = manager.get_recent_memories(7);
303 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(¬e1).unwrap();
315
316 let mut note2 = DailyNote::for_date("2024-01-16");
317 note2.content = "Note 2".to_string();
318 manager.save_daily_note(¬e2).unwrap();
319
320 let files = manager.list_memory_files();
321 assert_eq!(files.len(), 2);
322 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 let memory = Memory::with_content("Long term info");
333 manager.save_memory(&memory).unwrap();
334
335 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}