Skip to main content

mentedb_context/
serializer.rs

1//! Token-efficient serialization formats for context output.
2
3use crate::layout::{AttentionZone, ContextBlock};
4
5/// Trait for serializing context blocks into a string.
6pub trait ContextSerializer {
7    fn serialize(&self, blocks: &[ContextBlock]) -> String;
8}
9
10/// Compressed notation using ~3x fewer tokens than JSON.
11/// Format: `M|<type>|<salience>|<content>|tags:<comma-separated>`
12#[derive(Debug, Clone, Copy)]
13pub struct CompactFormat;
14
15impl ContextSerializer for CompactFormat {
16    fn serialize(&self, blocks: &[ContextBlock]) -> String {
17        let mut lines = Vec::new();
18
19        for block in blocks {
20            if block.memories.is_empty() {
21                continue;
22            }
23            lines.push(format!("# {}", zone_label(block.zone)));
24            for sm in &block.memories {
25                let m = &sm.memory;
26                let tags = if m.tags.is_empty() {
27                    String::new()
28                } else {
29                    format!("|tags:{}", m.tags.join(","))
30                };
31                lines.push(format!(
32                    "M|{:?}|{:.2}|{}{}",
33                    m.memory_type, m.salience, m.content, tags
34                ));
35            }
36        }
37
38        lines.join("\n")
39    }
40}
41
42/// Markdown-like structured format with headers and bullet points.
43#[derive(Debug, Clone, Copy)]
44pub struct StructuredFormat;
45
46impl ContextSerializer for StructuredFormat {
47    fn serialize(&self, blocks: &[ContextBlock]) -> String {
48        let mut parts = Vec::new();
49
50        for block in blocks {
51            if block.memories.is_empty() {
52                continue;
53            }
54            parts.push(format!("## {}", zone_label(block.zone)));
55            for sm in &block.memories {
56                let m = &sm.memory;
57                let mut line = format!(
58                    "- **[{:?}]** (salience: {:.2}) {}",
59                    m.memory_type, m.salience, m.content
60                );
61                if !m.tags.is_empty() {
62                    line.push_str(&format!(" [{}]", m.tags.join(", ")));
63                }
64                parts.push(line);
65            }
66            parts.push(String::new());
67        }
68
69        parts.join("\n")
70    }
71}
72
73/// Delta format: only changes since last turn.
74#[derive(Debug, Clone)]
75pub struct DeltaFormat {
76    pub delta_header: String,
77}
78
79impl DeltaFormat {
80    pub fn new(delta_header: String) -> Self {
81        Self { delta_header }
82    }
83}
84
85impl ContextSerializer for DeltaFormat {
86    fn serialize(&self, blocks: &[ContextBlock]) -> String {
87        let mut parts = vec![self.delta_header.clone()];
88        parts.push(String::new());
89
90        // Only serialize non-empty blocks for new content
91        for block in blocks {
92            if block.memories.is_empty() {
93                continue;
94            }
95            parts.push(format!("## {}", zone_label(block.zone)));
96            for sm in &block.memories {
97                parts.push(format!(
98                    "- [NEW] {:?} | {}",
99                    sm.memory.memory_type, sm.memory.content
100                ));
101            }
102        }
103
104        parts.join("\n")
105    }
106}
107
108fn zone_label(zone: AttentionZone) -> &'static str {
109    match zone {
110        AttentionZone::Opening => "⚠️ Warnings & Corrections",
111        AttentionZone::Critical => "🎯 Critical Context",
112        AttentionZone::Primary => "📋 Primary Context",
113        AttentionZone::Supporting => "📎 Supporting Context",
114        AttentionZone::Closing => "🔁 Summary & Reinforcement",
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::layout::{AttentionZone, ContextBlock, ScoredMemory};
122    use mentedb_core::MemoryNode;
123    use mentedb_core::memory::MemoryType;
124
125    fn make_block(zone: AttentionZone, content: &str, mem_type: MemoryType) -> ContextBlock {
126        let mut m = MemoryNode::new(uuid::Uuid::new_v4(), 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}