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