Skip to main content

mentedb_context/
serializer.rs

1//! Token-efficient serialization formats for context output.
2
3use crate::layout::{AttentionZone, ContextBlock};
4/// Trait for serializing context blocks into a string.
5pub trait ContextSerializer {
6    fn serialize(&self, blocks: &[ContextBlock]) -> String;
7}
8
9/// Compressed notation using ~3x fewer tokens than JSON.
10/// Format: `M|<type>|<salience>|<content>|tags:<comma-separated>`
11#[derive(Debug, Clone, Copy)]
12pub struct CompactFormat;
13
14impl ContextSerializer for CompactFormat {
15    fn serialize(&self, blocks: &[ContextBlock]) -> String {
16        let mut lines = Vec::new();
17
18        for block in blocks {
19            if block.memories.is_empty() {
20                continue;
21            }
22            lines.push(format!("# {}", zone_label(block.zone)));
23            for sm in &block.memories {
24                let m = &sm.memory;
25                let tags = if m.tags.is_empty() {
26                    String::new()
27                } else {
28                    format!("|tags:{}", m.tags.join(","))
29                };
30                lines.push(format!(
31                    "M|{:?}|{:.2}|{}{}",
32                    m.memory_type, m.salience, m.content, tags
33                ));
34            }
35        }
36
37        lines.join("\n")
38    }
39}
40
41/// Markdown-like structured format with headers and bullet points.
42#[derive(Debug, Clone, Copy)]
43pub struct StructuredFormat;
44
45impl ContextSerializer for StructuredFormat {
46    fn serialize(&self, blocks: &[ContextBlock]) -> String {
47        let mut parts = Vec::new();
48
49        for block in blocks {
50            if block.memories.is_empty() {
51                continue;
52            }
53            parts.push(format!("## {}", zone_label(block.zone)));
54            for sm in &block.memories {
55                let m = &sm.memory;
56                let mut line = format!(
57                    "- **[{:?}]** (salience: {:.2}) {}",
58                    m.memory_type, m.salience, m.content
59                );
60                if !m.tags.is_empty() {
61                    line.push_str(&format!(" [{}]", m.tags.join(", ")));
62                }
63                parts.push(line);
64            }
65            parts.push(String::new());
66        }
67
68        parts.join("\n")
69    }
70}
71
72/// Delta format: only changes since last turn.
73#[derive(Debug, Clone)]
74pub struct DeltaFormat {
75    pub delta_header: String,
76}
77
78impl DeltaFormat {
79    pub fn new(delta_header: String) -> Self {
80        Self { delta_header }
81    }
82}
83
84impl ContextSerializer for DeltaFormat {
85    fn serialize(&self, blocks: &[ContextBlock]) -> String {
86        let mut parts = vec![self.delta_header.clone()];
87        parts.push(String::new());
88
89        // Only serialize non-empty blocks for new content
90        for block in blocks {
91            if block.memories.is_empty() {
92                continue;
93            }
94            parts.push(format!("## {}", zone_label(block.zone)));
95            for sm in &block.memories {
96                parts.push(format!(
97                    "- [NEW] {:?} | {}",
98                    sm.memory.memory_type, sm.memory.content
99                ));
100            }
101        }
102
103        parts.join("\n")
104    }
105}
106
107fn zone_label(zone: AttentionZone) -> &'static str {
108    match zone {
109        AttentionZone::Opening => "⚠️ Warnings & Corrections",
110        AttentionZone::Critical => "🎯 Critical Context",
111        AttentionZone::Primary => "📋 Primary Context",
112        AttentionZone::Supporting => "📎 Supporting Context",
113        AttentionZone::Closing => "🔁 Summary & Reinforcement",
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::layout::{AttentionZone, ContextBlock, ScoredMemory};
121    use mentedb_core::MemoryNode;
122    use mentedb_core::memory::MemoryType;
123    use mentedb_core::types::AgentId;
124
125    fn make_block(zone: AttentionZone, content: &str, mem_type: MemoryType) -> ContextBlock {
126        let mut m = MemoryNode::new(AgentId::new(), mem_type, content.to_string(), vec![]);
127        m.salience = 0.9;
128        m.tags = vec!["test".to_string()];
129        ContextBlock {
130            zone,
131            memories: vec![ScoredMemory {
132                memory: m,
133                score: 0.9,
134            }],
135            estimated_tokens: 10,
136        }
137    }
138
139    #[test]
140    fn test_compact_format() {
141        let blocks = vec![make_block(
142            AttentionZone::Critical,
143            "user likes Rust",
144            MemoryType::Semantic,
145        )];
146        let output = CompactFormat.serialize(&blocks);
147        assert!(output.contains("M|Semantic|0.90|user likes Rust|tags:test"));
148        assert!(output.contains("🎯 Critical Context"));
149    }
150
151    #[test]
152    fn test_structured_format() {
153        let blocks = vec![make_block(
154            AttentionZone::Opening,
155            "avoid eval",
156            MemoryType::AntiPattern,
157        )];
158        let output = StructuredFormat.serialize(&blocks);
159        assert!(output.contains("## ⚠️ Warnings & Corrections"));
160        assert!(output.contains("**[AntiPattern]**"));
161        assert!(output.contains("avoid eval"));
162    }
163
164    #[test]
165    fn test_delta_format() {
166        let blocks = vec![make_block(
167            AttentionZone::Critical,
168            "new info",
169            MemoryType::Episodic,
170        )];
171        let fmt = DeltaFormat::new("[UNCHANGED] 5 memories from previous turn".to_string());
172        let output = fmt.serialize(&blocks);
173        assert!(output.contains("[UNCHANGED] 5 memories"));
174        assert!(output.contains("[NEW] Episodic | new info"));
175    }
176
177    #[test]
178    fn test_empty_blocks_skipped() {
179        let blocks = vec![ContextBlock {
180            zone: AttentionZone::Supporting,
181            memories: vec![],
182            estimated_tokens: 0,
183        }];
184        let output = CompactFormat.serialize(&blocks);
185        assert!(output.is_empty());
186    }
187}