Skip to main content

mentedb_context/
delta.rs

1//! Delta-aware serving: track what changed between turns to save tokens.
2
3use ahash::AHashSet;
4use mentedb_core::MemoryNode;
5use mentedb_core::types::MemoryId;
6
7/// Result of computing a delta between two context sets.
8#[derive(Debug, Clone)]
9pub struct DeltaResult {
10    pub added: Vec<MemoryId>,
11    pub removed: Vec<MemoryId>,
12    pub unchanged: Vec<MemoryId>,
13}
14
15/// Tracks context served across turns for delta computation.
16#[derive(Debug, Clone)]
17pub struct DeltaTracker {
18    pub last_served: AHashSet<MemoryId>,
19    pub last_turn_id: u64,
20}
21
22impl DeltaTracker {
23    pub fn new() -> Self {
24        Self {
25            last_served: AHashSet::new(),
26            last_turn_id: 0,
27        }
28    }
29
30    /// Compute the delta between current memories and previously served set.
31    pub fn compute_delta(
32        &self,
33        current: &[MemoryId],
34        previous: &AHashSet<MemoryId>,
35    ) -> DeltaResult {
36        let current_set: AHashSet<MemoryId> = current.iter().copied().collect();
37
38        let added: Vec<MemoryId> = current
39            .iter()
40            .filter(|id| !previous.contains(id))
41            .copied()
42            .collect();
43        let removed: Vec<MemoryId> = previous
44            .iter()
45            .filter(|id| !current_set.contains(id))
46            .copied()
47            .collect();
48        let unchanged: Vec<MemoryId> = current
49            .iter()
50            .filter(|id| previous.contains(id))
51            .copied()
52            .collect();
53
54        DeltaResult {
55            added,
56            removed,
57            unchanged,
58        }
59    }
60
61    /// Update tracking state after serving context.
62    pub fn update(&mut self, served_ids: &[MemoryId]) {
63        self.last_served = served_ids.iter().copied().collect();
64        self.last_turn_id += 1;
65    }
66
67    /// Format a human-readable delta context string.
68    pub fn format_delta_context(
69        added: &[&MemoryNode],
70        removed_summaries: &[String],
71        unchanged_count: usize,
72    ) -> String {
73        let mut parts = Vec::new();
74
75        for mem in added {
76            parts.push(format!("[NEW] {}", mem.content));
77        }
78
79        if !removed_summaries.is_empty() {
80            if removed_summaries.len() == 1 {
81                parts.push(format!("[REMOVED] {}", removed_summaries[0]));
82            } else {
83                parts.push(format!(
84                    "[REMOVED] {} memories no longer relevant",
85                    removed_summaries.len()
86                ));
87            }
88        }
89
90        if unchanged_count > 0 {
91            parts.push(format!(
92                "[UNCHANGED] {} memories from previous turn",
93                unchanged_count
94            ));
95        }
96
97        parts.join("\n")
98    }
99}
100
101impl Default for DeltaTracker {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use mentedb_core::types::AgentId;
111
112    #[test]
113    fn test_compute_delta_all_new() {
114        let tracker = DeltaTracker::new();
115        let ids = vec![MemoryId::new(), MemoryId::new()];
116        let delta = tracker.compute_delta(&ids, &tracker.last_served);
117        assert_eq!(delta.added.len(), 2);
118        assert!(delta.removed.is_empty());
119        assert!(delta.unchanged.is_empty());
120    }
121
122    #[test]
123    fn test_compute_delta_mixed() {
124        let kept = MemoryId::new();
125        let old = MemoryId::new();
126        let new = MemoryId::new();
127
128        let mut previous = AHashSet::new();
129        previous.insert(kept);
130        previous.insert(old);
131
132        let tracker = DeltaTracker::new();
133        let current = vec![kept, new];
134        let delta = tracker.compute_delta(&current, &previous);
135
136        assert_eq!(delta.added, vec![new]);
137        assert_eq!(delta.removed, vec![old]);
138        assert_eq!(delta.unchanged, vec![kept]);
139    }
140
141    #[test]
142    fn test_update_advances_turn() {
143        let mut tracker = DeltaTracker::new();
144        assert_eq!(tracker.last_turn_id, 0);
145        tracker.update(&[MemoryId::new()]);
146        assert_eq!(tracker.last_turn_id, 1);
147        assert_eq!(tracker.last_served.len(), 1);
148    }
149
150    #[test]
151    fn test_format_delta_context() {
152        use mentedb_core::memory::MemoryType;
153
154        let mem = mentedb_core::MemoryNode::new(
155            AgentId::new(),
156            MemoryType::Episodic,
157            "user switched to MySQL on March 15".to_string(),
158            vec![],
159        );
160        let result = DeltaTracker::format_delta_context(
161            &[&mem],
162            &[
163                "old memory 1".into(),
164                "old memory 2".into(),
165                "old memory 3".into(),
166            ],
167            12,
168        );
169        assert!(result.contains("[NEW] user switched to MySQL on March 15"));
170        assert!(result.contains("[REMOVED] 3 memories no longer relevant"));
171        assert!(result.contains("[UNCHANGED] 12 memories from previous turn"));
172    }
173}