ai_agent/memdir/
memory_scan.rs1use std::fs;
4use std::path::Path;
5
6use crate::memdir::memory_types::{MemoryType, parse_frontmatter};
7
8pub const MAX_MEMORY_FILES: usize = 200;
10const FRONTMATTER_MAX_LINES: usize = 30;
12
13#[derive(Debug, Clone)]
15pub struct MemoryHeader {
16 pub filename: String,
17 pub file_path: String,
18 pub mtime_ms: u64,
19 pub description: Option<String>,
20 pub memory_type: Option<MemoryType>,
21}
22
23pub async fn scan_memory_files(memory_dir: &str) -> Vec<MemoryHeader> {
26 let path = Path::new(memory_dir);
27
28 if !path.exists() {
29 return Vec::new();
30 }
31
32 let mut md_files = Vec::new();
33
34 if let Ok(entries) = fs::read_dir(path) {
35 for entry in entries.flatten() {
36 let file_path = entry.path();
37 if file_path.is_file() {
38 if let Some(ext) = file_path.extension() {
39 if ext == "md" {
40 if let Some(name) = file_path.file_name() {
41 let name_str = name.to_string_lossy();
42 if name_str != "MEMORY.md" {
44 md_files.push(file_path);
45 }
46 }
47 }
48 }
49 }
50 }
51 }
52
53 let mut headers = Vec::new();
54
55 for file_path in md_files {
56 if let Some(filename) = file_path.file_name() {
57 let filename_str = filename.to_string_lossy().to_string();
58 let file_path_str = file_path.to_string_lossy().to_string();
59
60 let mtime_ms = file_path
62 .metadata()
63 .ok()
64 .and_then(|m| m.modified().ok())
65 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
66 .map(|d| d.as_millis() as u64)
67 .unwrap_or(0);
68
69 let (description, memory_type) = if let Ok(content) = read_frontmatter_lines(&file_path)
71 {
72 if let Some(fm) = parse_frontmatter(&content) {
73 (Some(fm.description), Some(fm.memory_type))
74 } else {
75 (None, None)
76 }
77 } else {
78 (None, None)
79 };
80
81 headers.push(MemoryHeader {
82 filename: filename_str,
83 file_path: file_path_str,
84 mtime_ms,
85 description,
86 memory_type,
87 });
88 }
89 }
90
91 headers.sort_by(|a, b| b.mtime_ms.cmp(&a.mtime_ms));
93
94 headers.truncate(MAX_MEMORY_FILES);
96
97 headers
98}
99
100fn read_frontmatter_lines(path: &Path) -> std::io::Result<String> {
102 use std::io::{BufRead, BufReader};
103
104 let file = fs::File::open(path)?;
105 let reader = BufReader::new(file);
106
107 let mut lines = Vec::new();
108 for (i, line) in reader.lines().enumerate() {
109 if i >= FRONTMATTER_MAX_LINES {
110 break;
111 }
112 if let Ok(l) = line {
113 lines.push(l);
114 }
115 }
116
117 Ok(lines.join("\n"))
118}
119
120pub fn format_memory_manifest(memories: &[MemoryHeader]) -> String {
123 memories
124 .iter()
125 .map(|m| {
126 let tag = m
127 .memory_type
128 .as_ref()
129 .map(|t| format!("[{}] ", t.as_str()))
130 .unwrap_or_default();
131
132 let ts = chrono::DateTime::from_timestamp_millis(m.mtime_ms as i64)
133 .map(|dt| dt.to_rfc3339())
134 .unwrap_or_else(|| "unknown".to_string());
135
136 match &m.description {
137 Some(desc) => format!("- {}{} ({}): {}", tag, m.filename, ts, desc),
138 None => format!("- {}{} ({})", tag, m.filename, ts),
139 }
140 })
141 .collect::<Vec<_>>()
142 .join("\n")
143}
144
145#[cfg(test)]
146#[allow(unused_imports)]
147mod tests {
148 use super::*;
149 use tempfile::TempDir;
150
151 #[test]
152 fn test_format_memory_manifest() {
153 let headers = vec![
154 MemoryHeader {
155 filename: "user_test.md".to_string(),
156 file_path: "/tmp/memory/user_test.md".to_string(),
157 mtime_ms: 1700000000000,
158 description: Some("Test description".to_string()),
159 memory_type: Some(MemoryType::User),
160 },
161 MemoryHeader {
162 filename: "feedback_test.md".to_string(),
163 file_path: "/tmp/memory/feedback_test.md".to_string(),
164 mtime_ms: 1700000000000,
165 description: None,
166 memory_type: Some(MemoryType::Feedback),
167 },
168 ];
169
170 let manifest = format_memory_manifest(&headers);
171 assert!(manifest.contains("user_test.md"));
172 assert!(manifest.contains("Test description"));
173 assert!(manifest.contains("[user]"));
174 }
175
176 #[tokio::test]
177 async fn test_scan_empty_directory() {
178 let temp_dir = TempDir::new().unwrap();
179 let result = scan_memory_files(temp_dir.path().to_str().unwrap()).await;
180 assert!(result.is_empty());
181 }
182
183 #[tokio::test]
184 async fn test_scan_nonexistent_directory() {
185 let result = scan_memory_files("/nonexistent/path").await;
186 assert!(result.is_empty());
187 }
188}