Skip to main content

mentedb_context/
layout.rs

1//! Attention-aware context layout using U-curve optimization.
2//!
3//! LLMs attend best to content at the START and END of context,
4//! with degradation in the middle. This module arranges memories
5//! to exploit that attention pattern.
6
7use mentedb_core::MemoryNode;
8use mentedb_core::memory::MemoryType;
9
10use crate::budget::estimate_tokens;
11
12/// A memory with an associated relevance score.
13#[derive(Debug, Clone)]
14pub struct ScoredMemory {
15    pub memory: MemoryNode,
16    pub score: f32,
17}
18
19/// Attention zones based on U-curve optimization.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum AttentionZone {
22    /// Zone 1: Anti-patterns, corrections: AI sees first.
23    Opening,
24    /// Zone 2: Direct answers, highest salience.
25    Critical,
26    /// Zone 3: Supporting context, related memories.
27    Primary,
28    /// Zone 4: Background, lower salience.
29    Supporting,
30    /// Zone 5: Summary, open questions: AI sees last (reinforcement).
31    Closing,
32}
33
34impl AttentionZone {
35    /// Returns all zones in layout order.
36    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/// A block of memories assigned to a specific attention zone.
48#[derive(Debug, Clone)]
49pub struct ContextBlock {
50    pub zone: AttentionZone,
51    pub memories: Vec<ScoredMemory>,
52    pub estimated_tokens: usize,
53}
54
55/// Thresholds for classifying memories into attention zones.
56#[derive(Debug, Clone)]
57pub struct ZoneThresholds {
58    /// Minimum score to place a memory in the Critical zone (default: 0.8).
59    pub critical_score: f32,
60    /// Minimum salience to place a memory in the Critical zone (default: 0.7).
61    pub critical_salience: f32,
62    /// Minimum score to place a memory in the Primary zone (default: 0.5).
63    pub primary_score: f32,
64    /// Minimum score to place a memory in the Supporting zone (default: 0.2).
65    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/// Arranges memories into attention zones following the U-curve pattern.
80#[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    /// Arrange scored memories into attention-optimized zones.
91    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        // Sort each zone by score descending
110        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    /// Classify a memory into an attention zone based on its type, salience, and score.
146    fn classify(&self, sm: &ScoredMemory) -> AttentionZone {
147        let mem = &sm.memory;
148
149        // Anti-patterns and corrections go to Opening (highest attention)
150        match mem.memory_type {
151            MemoryType::AntiPattern | MemoryType::Correction => return AttentionZone::Opening,
152            _ => {}
153        }
154
155        // High salience + high score -> Critical
156        if sm.score >= self.thresholds.critical_score
157            && mem.salience >= self.thresholds.critical_salience
158        {
159            return AttentionZone::Critical;
160        }
161
162        // Moderate score -> Primary
163        if sm.score >= self.thresholds.primary_score {
164            return AttentionZone::Primary;
165        }
166
167        // Low score but still included
168        if sm.score >= self.thresholds.supporting_score {
169            return AttentionZone::Supporting;
170        }
171
172        // Very low score -> Closing (reinforcement zone)
173        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}