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