1use ahash::AHashSet;
4use mentedb_core::MemoryNode;
5use mentedb_core::types::MemoryId;
6
7#[derive(Debug, Clone)]
9pub struct DeltaResult {
10 pub added: Vec<MemoryId>,
11 pub removed: Vec<MemoryId>,
12 pub unchanged: Vec<MemoryId>,
13}
14
15#[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 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 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 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(¤t, &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}