mentedb_context/
layout.rs1use mentedb_core::MemoryNode;
8use mentedb_core::memory::MemoryType;
9
10use crate::budget::estimate_tokens;
11
12#[derive(Debug, Clone)]
14pub struct ScoredMemory {
15 pub memory: MemoryNode,
16 pub score: f32,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum AttentionZone {
22 Opening,
24 Critical,
26 Primary,
28 Supporting,
30 Closing,
32}
33
34impl AttentionZone {
35 pub fn all_ordered() -> &'static [AttentionZone] {
37 &[
38 AttentionZone::Opening,
39 AttentionZone::Critical,
40 AttentionZone::Primary,
41 AttentionZone::Supporting,
42 AttentionZone::Closing,
43 ]
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct ContextBlock {
50 pub zone: AttentionZone,
51 pub memories: Vec<ScoredMemory>,
52 pub estimated_tokens: usize,
53}
54
55#[derive(Debug, Clone)]
57pub struct ZoneThresholds {
58 pub critical_score: f32,
60 pub critical_salience: f32,
62 pub primary_score: f32,
64 pub supporting_score: f32,
66}
67
68impl Default for ZoneThresholds {
69 fn default() -> Self {
70 Self {
71 critical_score: 0.8,
72 critical_salience: 0.7,
73 primary_score: 0.5,
74 supporting_score: 0.2,
75 }
76 }
77}
78
79#[derive(Debug)]
81pub struct ContextLayout {
82 thresholds: ZoneThresholds,
83}
84
85impl ContextLayout {
86 pub fn new(thresholds: ZoneThresholds) -> Self {
87 Self { thresholds }
88 }
89
90 pub fn arrange(&self, memories: Vec<ScoredMemory>) -> Vec<ContextBlock> {
92 let mut opening = Vec::new();
93 let mut critical = Vec::new();
94 let mut primary = Vec::new();
95 let mut supporting = Vec::new();
96 let mut closing = Vec::new();
97
98 for sm in memories {
99 let zone = self.classify(&sm);
100 match zone {
101 AttentionZone::Opening => opening.push(sm),
102 AttentionZone::Critical => critical.push(sm),
103 AttentionZone::Primary => primary.push(sm),
104 AttentionZone::Supporting => supporting.push(sm),
105 AttentionZone::Closing => closing.push(sm),
106 }
107 }
108
109 for group in [
111 &mut opening,
112 &mut critical,
113 &mut primary,
114 &mut supporting,
115 &mut closing,
116 ] {
117 group.sort_by(|a, b| {
118 b.score
119 .partial_cmp(&a.score)
120 .unwrap_or(std::cmp::Ordering::Equal)
121 });
122 }
123
124 let zones = [
125 (AttentionZone::Opening, opening),
126 (AttentionZone::Critical, critical),
127 (AttentionZone::Primary, primary),
128 (AttentionZone::Supporting, supporting),
129 (AttentionZone::Closing, closing),
130 ];
131
132 zones
133 .into_iter()
134 .map(|(zone, memories)| {
135 let estimated_tokens = Self::estimate_block_tokens(&memories);
136 ContextBlock {
137 zone,
138 memories,
139 estimated_tokens,
140 }
141 })
142 .collect()
143 }
144
145 fn classify(&self, sm: &ScoredMemory) -> AttentionZone {
147 let mem = &sm.memory;
148
149 match mem.memory_type {
151 MemoryType::AntiPattern | MemoryType::Correction => return AttentionZone::Opening,
152 _ => {}
153 }
154
155 if sm.score >= self.thresholds.critical_score
157 && mem.salience >= self.thresholds.critical_salience
158 {
159 return AttentionZone::Critical;
160 }
161
162 if sm.score >= self.thresholds.primary_score {
164 return AttentionZone::Primary;
165 }
166
167 if sm.score >= self.thresholds.supporting_score {
169 return AttentionZone::Supporting;
170 }
171
172 AttentionZone::Closing
174 }
175
176 fn estimate_block_tokens(memories: &[ScoredMemory]) -> usize {
177 memories
178 .iter()
179 .map(|sm| estimate_tokens(&sm.memory.content))
180 .sum()
181 }
182}
183
184impl Default for ContextLayout {
185 fn default() -> Self {
186 Self::new(ZoneThresholds::default())
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use mentedb_core::MemoryNode;
194 use mentedb_core::memory::MemoryType;
195
196 fn make_memory(content: &str, memory_type: MemoryType, salience: f32) -> MemoryNode {
197 let mut m = MemoryNode::new(
198 uuid::Uuid::new_v4(),
199 memory_type,
200 content.to_string(),
201 vec![],
202 );
203 m.salience = salience;
204 m
205 }
206
207 #[test]
208 fn test_antipattern_goes_to_opening() {
209 let layout = ContextLayout::default();
210 let memories = vec![ScoredMemory {
211 memory: make_memory("never use eval", MemoryType::AntiPattern, 0.9),
212 score: 0.95,
213 }];
214 let blocks = layout.arrange(memories);
215 let opening = blocks
216 .iter()
217 .find(|b| b.zone == AttentionZone::Opening)
218 .unwrap();
219 assert_eq!(opening.memories.len(), 1);
220 }
221
222 #[test]
223 fn test_high_score_goes_to_critical() {
224 let layout = ContextLayout::default();
225 let memories = vec![ScoredMemory {
226 memory: make_memory("user prefers dark mode", MemoryType::Semantic, 0.9),
227 score: 0.85,
228 }];
229 let blocks = layout.arrange(memories);
230 let critical = blocks
231 .iter()
232 .find(|b| b.zone == AttentionZone::Critical)
233 .unwrap();
234 assert_eq!(critical.memories.len(), 1);
235 }
236
237 #[test]
238 fn test_low_score_goes_to_supporting() {
239 let layout = ContextLayout::default();
240 let memories = vec![ScoredMemory {
241 memory: make_memory("background info", MemoryType::Episodic, 0.3),
242 score: 0.3,
243 }];
244 let blocks = layout.arrange(memories);
245 let supporting = blocks
246 .iter()
247 .find(|b| b.zone == AttentionZone::Supporting)
248 .unwrap();
249 assert_eq!(supporting.memories.len(), 1);
250 }
251
252 #[test]
253 fn test_arrange_produces_all_zones() {
254 let layout = ContextLayout::default();
255 let blocks = layout.arrange(vec![]);
256 assert_eq!(blocks.len(), 5);
257 }
258}