use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WorkingNote {
pub content: String,
pub category: NoteCategory,
pub created_at: String,
pub relevance: f64,
}
impl WorkingNote {
pub fn new(content: impl Into<String>, category: NoteCategory) -> Self {
Self {
content: content.into(),
category,
created_at: String::new(), relevance: 1.0,
}
}
#[must_use]
pub fn with_timestamp(mut self, timestamp: impl Into<String>) -> Self {
self.created_at = timestamp.into();
self
}
pub fn decay(&mut self, factor: f64) {
self.relevance = (self.relevance * factor).max(0.0);
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum NoteCategory {
Observation,
Plan,
Concern,
Discovery,
Reflection,
Bookmark,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MemoryConfig {
pub short_term_limit: u32,
pub working_limit: u32,
pub long_term_limit: u32,
pub compression_ratio: f64,
}
impl Default for MemoryConfig {
fn default() -> Self {
Self {
short_term_limit: 8192,
working_limit: 2048,
long_term_limit: 16384,
compression_ratio: 0.5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MeditateConfig {
pub prune_threshold: f64,
pub decay_factor: f64,
pub max_notes_before_trigger: usize,
pub max_failures_before_trigger: usize,
pub use_llm: bool,
pub max_notes_after_consolidation: usize,
pub max_failures_after_prune: usize,
}
impl Default for MeditateConfig {
fn default() -> Self {
Self {
prune_threshold: 0.1,
decay_factor: 0.9,
max_notes_before_trigger: 50,
max_failures_before_trigger: 20,
use_llm: true,
max_notes_after_consolidation: 30,
max_failures_after_prune: 10,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct MeditateResult {
pub notes_before: usize,
pub notes_after: usize,
pub notes_pruned: usize,
pub notes_merged: usize,
pub failures_pruned: usize,
pub constraints_removed: usize,
pub used_llm: bool,
pub insights_summary: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_working_note_creation() {
let note = WorkingNote::new("test note", NoteCategory::Observation);
assert_eq!(note.content, "test note");
assert_eq!(note.category, NoteCategory::Observation);
assert_eq!(note.relevance, 1.0);
}
#[test]
fn test_working_note_with_timestamp() {
let note =
WorkingNote::new("test", NoteCategory::Plan).with_timestamp("2026-03-23T12:00:00Z");
assert_eq!(note.created_at, "2026-03-23T12:00:00Z");
}
#[test]
fn test_working_note_decay() {
let mut note = WorkingNote::new("test", NoteCategory::Discovery);
assert_eq!(note.relevance, 1.0);
note.decay(0.9);
assert!((note.relevance - 0.9).abs() < f64::EPSILON);
note.decay(0.5);
assert!((note.relevance - 0.45).abs() < f64::EPSILON);
}
#[test]
fn test_decay_floor_at_zero() {
let mut note = WorkingNote::new("test", NoteCategory::Concern);
note.relevance = 0.01;
note.decay(0.0);
assert_eq!(note.relevance, 0.0);
}
#[test]
fn test_note_category_custom() {
let cat = NoteCategory::Custom("legal_flag".into());
let json = serde_json::to_string(&cat).unwrap();
let back: NoteCategory = serde_json::from_str(&json).unwrap();
assert_eq!(back, NoteCategory::Custom("legal_flag".into()));
}
#[test]
fn test_memory_config_defaults() {
let config = MemoryConfig::default();
assert_eq!(config.short_term_limit, 8192);
assert_eq!(config.working_limit, 2048);
assert_eq!(config.long_term_limit, 16384);
assert!((config.compression_ratio - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_working_note_serialization() {
let note = WorkingNote::new("important", NoteCategory::Reflection)
.with_timestamp("2026-03-23T10:00:00Z");
let json = serde_json::to_string(¬e).unwrap();
let back: WorkingNote = serde_json::from_str(&json).unwrap();
assert_eq!(back, note);
}
#[test]
fn test_meditate_config_defaults() {
let config = MeditateConfig::default();
assert!((config.prune_threshold - 0.1).abs() < f64::EPSILON);
assert!((config.decay_factor - 0.9).abs() < f64::EPSILON);
assert_eq!(config.max_notes_before_trigger, 50);
assert_eq!(config.max_failures_before_trigger, 20);
assert!(config.use_llm);
assert_eq!(config.max_notes_after_consolidation, 30);
assert_eq!(config.max_failures_after_prune, 10);
}
#[test]
fn test_meditate_config_serialization() {
let config = MeditateConfig {
prune_threshold: 0.2,
decay_factor: 0.8,
max_notes_before_trigger: 100,
max_failures_before_trigger: 10,
use_llm: false,
max_notes_after_consolidation: 50,
max_failures_after_prune: 5,
};
let json = serde_json::to_string(&config).unwrap();
let back: MeditateConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back, config);
}
#[test]
fn test_meditate_result_default() {
let result = MeditateResult::default();
assert_eq!(result.notes_before, 0);
assert_eq!(result.notes_after, 0);
assert_eq!(result.notes_pruned, 0);
assert_eq!(result.notes_merged, 0);
assert_eq!(result.failures_pruned, 0);
assert_eq!(result.constraints_removed, 0);
assert!(!result.used_llm);
assert_eq!(result.insights_summary, None);
}
#[test]
fn test_meditate_result_serialization() {
let result = MeditateResult {
notes_before: 45,
notes_after: 28,
notes_pruned: 12,
notes_merged: 5,
failures_pruned: 3,
constraints_removed: 1,
used_llm: true,
insights_summary: Some("Consolidated observation notes".into()),
};
let json = serde_json::to_string(&result).unwrap();
let back: MeditateResult = serde_json::from_str(&json).unwrap();
assert_eq!(back, result);
assert_eq!(
back.insights_summary.as_deref(),
Some("Consolidated observation notes")
);
}
}