1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub struct WorkingNote {
27 pub content: String,
29
30 pub category: NoteCategory,
32
33 pub created_at: String,
35
36 pub relevance: f64,
38}
39
40impl WorkingNote {
41 pub fn new(content: impl Into<String>, category: NoteCategory) -> Self {
43 Self {
44 content: content.into(),
45 category,
46 created_at: String::new(), relevance: 1.0,
48 }
49 }
50
51 #[must_use]
56 pub fn with_timestamp(mut self, timestamp: impl Into<String>) -> Self {
57 self.created_at = timestamp.into();
58 self
59 }
60
61 pub fn decay(&mut self, factor: f64) {
63 self.relevance = (self.relevance * factor).max(0.0);
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
72#[non_exhaustive]
73pub enum NoteCategory {
74 Observation,
76 Plan,
78 Concern,
80 Discovery,
82 Reflection,
84 Bookmark,
86 Custom(String),
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104pub struct MemoryConfig {
105 pub short_term_limit: u32,
107
108 pub working_limit: u32,
110
111 pub long_term_limit: u32,
113
114 pub compression_ratio: f64,
116}
117
118impl Default for MemoryConfig {
119 fn default() -> Self {
120 Self {
121 short_term_limit: 8192,
122 working_limit: 2048,
123 long_term_limit: 16384,
124 compression_ratio: 0.5,
125 }
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
145pub struct MeditateConfig {
146 pub prune_threshold: f64,
149
150 pub decay_factor: f64,
153
154 pub max_notes_before_trigger: usize,
156
157 pub max_failures_before_trigger: usize,
159
160 pub use_llm: bool,
163
164 pub max_notes_after_consolidation: usize,
166
167 pub max_failures_after_prune: usize,
169}
170
171impl Default for MeditateConfig {
172 fn default() -> Self {
173 Self {
174 prune_threshold: 0.1,
175 decay_factor: 0.9,
176 max_notes_before_trigger: 50,
177 max_failures_before_trigger: 20,
178 use_llm: true,
179 max_notes_after_consolidation: 30,
180 max_failures_after_prune: 10,
181 }
182 }
183}
184
185#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
209pub struct MeditateResult {
210 pub notes_before: usize,
212
213 pub notes_after: usize,
215
216 pub notes_pruned: usize,
218
219 pub notes_merged: usize,
221
222 pub failures_pruned: usize,
224
225 pub constraints_removed: usize,
227
228 pub used_llm: bool,
230
231 pub insights_summary: Option<String>,
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_working_note_creation() {
241 let note = WorkingNote::new("test note", NoteCategory::Observation);
242 assert_eq!(note.content, "test note");
243 assert_eq!(note.category, NoteCategory::Observation);
244 assert_eq!(note.relevance, 1.0);
245 }
246
247 #[test]
248 fn test_working_note_with_timestamp() {
249 let note =
250 WorkingNote::new("test", NoteCategory::Plan).with_timestamp("2026-03-23T12:00:00Z");
251 assert_eq!(note.created_at, "2026-03-23T12:00:00Z");
252 }
253
254 #[test]
255 fn test_working_note_decay() {
256 let mut note = WorkingNote::new("test", NoteCategory::Discovery);
257 assert_eq!(note.relevance, 1.0);
258 note.decay(0.9);
259 assert!((note.relevance - 0.9).abs() < f64::EPSILON);
260 note.decay(0.5);
261 assert!((note.relevance - 0.45).abs() < f64::EPSILON);
262 }
263
264 #[test]
265 fn test_decay_floor_at_zero() {
266 let mut note = WorkingNote::new("test", NoteCategory::Concern);
267 note.relevance = 0.01;
268 note.decay(0.0);
269 assert_eq!(note.relevance, 0.0);
270 }
271
272 #[test]
273 fn test_note_category_custom() {
274 let cat = NoteCategory::Custom("legal_flag".into());
275 let json = serde_json::to_string(&cat).unwrap();
276 let back: NoteCategory = serde_json::from_str(&json).unwrap();
277 assert_eq!(back, NoteCategory::Custom("legal_flag".into()));
278 }
279
280 #[test]
281 fn test_memory_config_defaults() {
282 let config = MemoryConfig::default();
283 assert_eq!(config.short_term_limit, 8192);
284 assert_eq!(config.working_limit, 2048);
285 assert_eq!(config.long_term_limit, 16384);
286 assert!((config.compression_ratio - 0.5).abs() < f64::EPSILON);
287 }
288
289 #[test]
290 fn test_working_note_serialization() {
291 let note = WorkingNote::new("important", NoteCategory::Reflection)
292 .with_timestamp("2026-03-23T10:00:00Z");
293 let json = serde_json::to_string(¬e).unwrap();
294 let back: WorkingNote = serde_json::from_str(&json).unwrap();
295 assert_eq!(back, note);
296 }
297
298 #[test]
299 fn test_meditate_config_defaults() {
300 let config = MeditateConfig::default();
301 assert!((config.prune_threshold - 0.1).abs() < f64::EPSILON);
302 assert!((config.decay_factor - 0.9).abs() < f64::EPSILON);
303 assert_eq!(config.max_notes_before_trigger, 50);
304 assert_eq!(config.max_failures_before_trigger, 20);
305 assert!(config.use_llm);
306 assert_eq!(config.max_notes_after_consolidation, 30);
307 assert_eq!(config.max_failures_after_prune, 10);
308 }
309
310 #[test]
311 fn test_meditate_config_serialization() {
312 let config = MeditateConfig {
313 prune_threshold: 0.2,
314 decay_factor: 0.8,
315 max_notes_before_trigger: 100,
316 max_failures_before_trigger: 10,
317 use_llm: false,
318 max_notes_after_consolidation: 50,
319 max_failures_after_prune: 5,
320 };
321 let json = serde_json::to_string(&config).unwrap();
322 let back: MeditateConfig = serde_json::from_str(&json).unwrap();
323 assert_eq!(back, config);
324 }
325
326 #[test]
327 fn test_meditate_result_default() {
328 let result = MeditateResult::default();
329 assert_eq!(result.notes_before, 0);
330 assert_eq!(result.notes_after, 0);
331 assert_eq!(result.notes_pruned, 0);
332 assert_eq!(result.notes_merged, 0);
333 assert_eq!(result.failures_pruned, 0);
334 assert_eq!(result.constraints_removed, 0);
335 assert!(!result.used_llm);
336 assert_eq!(result.insights_summary, None);
337 }
338
339 #[test]
340 fn test_meditate_result_serialization() {
341 let result = MeditateResult {
342 notes_before: 45,
343 notes_after: 28,
344 notes_pruned: 12,
345 notes_merged: 5,
346 failures_pruned: 3,
347 constraints_removed: 1,
348 used_llm: true,
349 insights_summary: Some("Consolidated observation notes".into()),
350 };
351 let json = serde_json::to_string(&result).unwrap();
352 let back: MeditateResult = serde_json::from_str(&json).unwrap();
353 assert_eq!(back, result);
354 assert_eq!(
355 back.insights_summary.as_deref(),
356 Some("Consolidated observation notes")
357 );
358 }
359}