1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct EpisodicConfig {
14 #[serde(default = "default_snapshot_dir")]
16 pub snapshot_dir: String,
17
18 #[serde(default = "default_max_snapshots")]
20 pub max_snapshots: usize,
21
22 #[serde(default = "default_max_daily_entries")]
24 pub max_daily_entries: usize,
25
26 #[serde(default = "default_auto_consolidate")]
28 pub auto_consolidate: bool,
29
30 #[serde(default = "default_consolidation_time")]
32 pub consolidation_time: String,
33
34 #[serde(default = "default_retention_days")]
36 pub retention_days: u32,
37
38 #[serde(default = "default_include_timestamps")]
40 pub include_timestamps: bool,
41}
42
43impl Default for EpisodicConfig {
44 fn default() -> Self {
45 Self {
46 snapshot_dir: default_snapshot_dir(),
47 max_snapshots: default_max_snapshots(),
48 max_daily_entries: default_max_daily_entries(),
49 auto_consolidate: default_auto_consolidate(),
50 consolidation_time: default_consolidation_time(),
51 retention_days: default_retention_days(),
52 include_timestamps: default_include_timestamps(),
53 }
54 }
55}
56
57fn default_snapshot_dir() -> String {
58 "memory/sessions".to_string()
59}
60
61fn default_max_snapshots() -> usize {
62 50
63}
64
65fn default_max_daily_entries() -> usize {
66 100
67}
68
69fn default_auto_consolidate() -> bool {
70 true
71}
72
73fn default_consolidation_time() -> String {
74 "03:00".to_string()
75}
76
77fn default_retention_days() -> u32 {
78 30
79}
80
81fn default_include_timestamps() -> bool {
82 true
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct MemoryConfig {
88 #[serde(default = "default_backend")]
90 pub backend: String,
91
92 #[serde(default = "default_db_path")]
94 pub db_path: String,
95
96 #[serde(default = "default_markdown_dir")]
98 pub markdown_dir: String,
99
100 #[serde(default)]
102 pub episodic: EpisodicConfig,
103}
104
105impl Default for MemoryConfig {
106 fn default() -> Self {
107 Self {
108 backend: default_backend(),
109 db_path: default_db_path(),
110 markdown_dir: default_markdown_dir(),
111 episodic: EpisodicConfig::default(),
112 }
113 }
114}
115
116fn default_backend() -> String {
117 "sqlite".to_string()
118}
119
120fn default_db_path() -> String {
121 "memory/brain.db".to_string()
122}
123
124fn default_markdown_dir() -> String {
125 "memory".to_string()
126}
127
128impl MemoryConfig {
129 pub fn from_file(path: &std::path::Path) -> anyhow::Result<Self> {
131 let content = std::fs::read_to_string(path)?;
132 let config: MemoryConfig = serde_yaml::from_str(&content)?;
133 Ok(config)
134 }
135}
136
137pub fn load_default_memory_config() -> MemoryConfig {
154 if let Some(path) = enact_config::resolve_config_file("memory.yaml", "ENACT_MEMORY_CONFIG_PATH")
156 {
157 match MemoryConfig::from_file(&path) {
158 Ok(config) => {
159 tracing::debug!("Loaded memory config from {:?}", path);
160 return config;
161 }
162 Err(e) => {
163 tracing::warn!("Failed to load memory config from {:?}: {}", path, e);
164 }
165 }
166 }
167
168 tracing::debug!("Using default memory configuration");
170 MemoryConfig::default()
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_default_memory_config() {
179 let config = MemoryConfig::default();
180 assert_eq!(config.backend, "sqlite");
181 assert_eq!(config.db_path, "memory/brain.db");
182 assert_eq!(config.markdown_dir, "memory");
183 }
184
185 #[test]
186 fn test_default_episodic_config() {
187 let config = EpisodicConfig::default();
188 assert_eq!(config.snapshot_dir, "memory/sessions");
189 assert_eq!(config.max_snapshots, 50);
190 assert_eq!(config.max_daily_entries, 100);
191 assert!(config.auto_consolidate);
192 assert_eq!(config.consolidation_time, "03:00");
193 assert_eq!(config.retention_days, 30);
194 assert!(config.include_timestamps);
195 }
196
197 #[test]
198 fn test_config_serialization() {
199 let config = MemoryConfig::default();
200 let yaml = serde_yaml::to_string(&config).unwrap();
201 let deserialized: MemoryConfig = serde_yaml::from_str(&yaml).unwrap();
202 assert_eq!(deserialized.backend, config.backend);
203 assert_eq!(deserialized.db_path, config.db_path);
204 }
205
206 #[test]
207 fn test_load_default_memory_config_fallback() {
208 let config = load_default_memory_config();
210 assert_eq!(config.backend, "sqlite");
211 }
212
213 #[test]
214 fn test_from_file() {
215 let temp_dir = tempfile::tempdir().unwrap();
216 let config_path = temp_dir.path().join("memory.yaml");
217
218 std::fs::write(
219 &config_path,
220 r#"
221backend: markdown
222db_path: "custom/db.sqlite"
223markdown_dir: "custom/memory"
224episodic:
225 snapshot_dir: "custom/sessions"
226 max_snapshots: 100
227 max_daily_entries: 200
228 auto_consolidate: false
229 consolidation_time: "02:00"
230 retention_days: 60
231 include_timestamps: false
232"#,
233 )
234 .unwrap();
235
236 let config = MemoryConfig::from_file(&config_path).unwrap();
237 assert_eq!(config.backend, "markdown");
238 assert_eq!(config.db_path, "custom/db.sqlite");
239 assert_eq!(config.markdown_dir, "custom/memory");
240 assert_eq!(config.episodic.snapshot_dir, "custom/sessions");
241 assert_eq!(config.episodic.max_snapshots, 100);
242 assert_eq!(config.episodic.max_daily_entries, 200);
243 assert!(!config.episodic.auto_consolidate);
244 assert_eq!(config.episodic.consolidation_time, "02:00");
245 assert_eq!(config.episodic.retention_days, 60);
246 assert!(!config.episodic.include_timestamps);
247 }
248}