umi_memory/memory/
block.rs

1//! Memory Block - Individual blocks within Core Memory
2//!
3//! TigerStyle: Each block has explicit type, label, and size tracking.
4
5use std::fmt;
6use uuid::Uuid;
7
8use crate::constants::{CORE_MEMORY_BLOCK_LABEL_BYTES_MAX, CORE_MEMORY_BLOCK_SIZE_BYTES_MAX};
9
10/// Unique identifier for a memory block.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct MemoryBlockId(Uuid);
13
14impl MemoryBlockId {
15    /// Create a new random block ID.
16    #[must_use]
17    pub fn new() -> Self {
18        Self(Uuid::new_v4())
19    }
20
21    /// Create a block ID from a UUID.
22    #[must_use]
23    pub fn from_uuid(uuid: Uuid) -> Self {
24        Self(uuid)
25    }
26
27    /// Get the underlying UUID.
28    #[must_use]
29    pub fn as_uuid(&self) -> &Uuid {
30        &self.0
31    }
32}
33
34impl Default for MemoryBlockId {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl fmt::Display for MemoryBlockId {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(f, "{}", self.0)
43    }
44}
45
46/// Types of memory blocks in core memory.
47///
48/// TigerStyle: Fixed set of block types with clear purposes.
49///
50/// # Block Types
51///
52/// - `System` - System instructions and prompts (highest priority)
53/// - `Persona` - AI personality and behavior guidelines
54/// - `Human` - Information about the human user
55/// - `Facts` - Key facts and knowledge to remember
56/// - `Goals` - Current objectives and tasks
57/// - `Scratch` - Temporary working space for reasoning
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub enum MemoryBlockType {
60    /// System instructions and prompts
61    System,
62    /// AI personality and behavior guidelines
63    Persona,
64    /// Information about the human user
65    Human,
66    /// Key facts and knowledge to remember
67    Facts,
68    /// Current objectives and tasks
69    Goals,
70    /// Temporary working space for reasoning
71    Scratch,
72}
73
74impl MemoryBlockType {
75    /// Get the string representation for XML rendering.
76    #[must_use]
77    pub fn as_str(&self) -> &'static str {
78        match self {
79            Self::System => "system",
80            Self::Persona => "persona",
81            Self::Human => "human",
82            Self::Facts => "facts",
83            Self::Goals => "goals",
84            Self::Scratch => "scratch",
85        }
86    }
87
88    /// Get render priority (lower = rendered first).
89    ///
90    /// TigerStyle: Explicit ordering for deterministic rendering.
91    #[must_use]
92    pub fn priority(&self) -> u8 {
93        match self {
94            Self::System => 0,
95            Self::Persona => 1,
96            Self::Human => 2,
97            Self::Facts => 3,
98            Self::Goals => 4,
99            Self::Scratch => 5,
100        }
101    }
102
103    /// Get all block types in render order.
104    #[must_use]
105    pub fn all_ordered() -> &'static [MemoryBlockType] {
106        &[
107            Self::System,
108            Self::Persona,
109            Self::Human,
110            Self::Facts,
111            Self::Goals,
112            Self::Scratch,
113        ]
114    }
115}
116
117impl fmt::Display for MemoryBlockType {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(f, "{}", self.as_str())
120    }
121}
122
123/// A single memory block.
124///
125/// TigerStyle: Immutable content with explicit size tracking.
126#[derive(Debug, Clone)]
127pub struct MemoryBlock {
128    /// Unique identifier
129    id: MemoryBlockId,
130    /// Block type
131    block_type: MemoryBlockType,
132    /// Optional human-readable label
133    label: Option<String>,
134    /// Block content
135    content: String,
136    /// Cached content size in bytes
137    size_bytes: usize,
138    /// Creation timestamp (milliseconds since epoch)
139    created_at_ms: u64,
140    /// Last modification timestamp (milliseconds since epoch)
141    modified_at_ms: u64,
142}
143
144impl MemoryBlock {
145    /// Create a new memory block.
146    ///
147    /// # Panics
148    /// Panics if content exceeds `CORE_MEMORY_BLOCK_SIZE_BYTES_MAX`.
149    /// Panics if label exceeds `CORE_MEMORY_BLOCK_LABEL_BYTES_MAX`.
150    #[must_use]
151    pub fn new(block_type: MemoryBlockType, content: impl Into<String>, now_ms: u64) -> Self {
152        let content = content.into();
153
154        // Preconditions
155        assert!(
156            content.len() <= CORE_MEMORY_BLOCK_SIZE_BYTES_MAX,
157            "block content {} bytes exceeds max {}",
158            content.len(),
159            CORE_MEMORY_BLOCK_SIZE_BYTES_MAX
160        );
161
162        let size_bytes = content.len();
163        let id = MemoryBlockId::new();
164
165        // Postconditions
166        let result = Self {
167            id,
168            block_type,
169            label: None,
170            content,
171            size_bytes,
172            created_at_ms: now_ms,
173            modified_at_ms: now_ms,
174        };
175
176        assert_eq!(
177            result.size_bytes,
178            result.content.len(),
179            "size must match content"
180        );
181
182        result
183    }
184
185    /// Create a new memory block with a label.
186    ///
187    /// # Panics
188    /// Panics if content exceeds `CORE_MEMORY_BLOCK_SIZE_BYTES_MAX`.
189    /// Panics if label exceeds `CORE_MEMORY_BLOCK_LABEL_BYTES_MAX`.
190    #[must_use]
191    pub fn with_label(
192        block_type: MemoryBlockType,
193        label: impl Into<String>,
194        content: impl Into<String>,
195        now_ms: u64,
196    ) -> Self {
197        let label = label.into();
198        let content = content.into();
199
200        // Preconditions
201        assert!(
202            label.len() <= CORE_MEMORY_BLOCK_LABEL_BYTES_MAX,
203            "block label {} bytes exceeds max {}",
204            label.len(),
205            CORE_MEMORY_BLOCK_LABEL_BYTES_MAX
206        );
207        assert!(
208            content.len() <= CORE_MEMORY_BLOCK_SIZE_BYTES_MAX,
209            "block content {} bytes exceeds max {}",
210            content.len(),
211            CORE_MEMORY_BLOCK_SIZE_BYTES_MAX
212        );
213
214        let size_bytes = content.len();
215        let id = MemoryBlockId::new();
216
217        Self {
218            id,
219            block_type,
220            label: Some(label),
221            content,
222            size_bytes,
223            created_at_ms: now_ms,
224            modified_at_ms: now_ms,
225        }
226    }
227
228    /// Get the block ID.
229    #[must_use]
230    pub fn id(&self) -> MemoryBlockId {
231        self.id
232    }
233
234    /// Get the block type.
235    #[must_use]
236    pub fn block_type(&self) -> MemoryBlockType {
237        self.block_type
238    }
239
240    /// Get the optional label.
241    #[must_use]
242    pub fn label(&self) -> Option<&str> {
243        self.label.as_deref()
244    }
245
246    /// Get the block content.
247    #[must_use]
248    pub fn content(&self) -> &str {
249        &self.content
250    }
251
252    /// Get the size in bytes (cached).
253    #[must_use]
254    pub fn size_bytes(&self) -> usize {
255        self.size_bytes
256    }
257
258    /// Get creation timestamp.
259    #[must_use]
260    pub fn created_at_ms(&self) -> u64 {
261        self.created_at_ms
262    }
263
264    /// Get modification timestamp.
265    #[must_use]
266    pub fn modified_at_ms(&self) -> u64 {
267        self.modified_at_ms
268    }
269
270    /// Update the content.
271    ///
272    /// # Panics
273    /// Panics if new content exceeds `CORE_MEMORY_BLOCK_SIZE_BYTES_MAX`.
274    pub fn set_content(&mut self, content: impl Into<String>, now_ms: u64) {
275        let content = content.into();
276
277        // Precondition
278        assert!(
279            content.len() <= CORE_MEMORY_BLOCK_SIZE_BYTES_MAX,
280            "block content {} bytes exceeds max {}",
281            content.len(),
282            CORE_MEMORY_BLOCK_SIZE_BYTES_MAX
283        );
284
285        self.size_bytes = content.len();
286        self.content = content;
287        self.modified_at_ms = now_ms;
288
289        // Postcondition
290        assert_eq!(
291            self.size_bytes,
292            self.content.len(),
293            "size must match content"
294        );
295    }
296
297    /// Render the block as XML for LLM context.
298    ///
299    /// TigerStyle: Deterministic, predictable output format.
300    #[must_use]
301    pub fn render(&self) -> String {
302        let type_attr = self.block_type.as_str();
303        match &self.label {
304            Some(label) => {
305                format!(
306                    "<block type=\"{}\" label=\"{}\">\n{}\n</block>",
307                    type_attr, label, self.content
308                )
309            }
310            None => {
311                format!("<block type=\"{}\">\n{}\n</block>", type_attr, self.content)
312            }
313        }
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_block_type_as_str() {
323        assert_eq!(MemoryBlockType::System.as_str(), "system");
324        assert_eq!(MemoryBlockType::Persona.as_str(), "persona");
325        assert_eq!(MemoryBlockType::Human.as_str(), "human");
326        assert_eq!(MemoryBlockType::Facts.as_str(), "facts");
327        assert_eq!(MemoryBlockType::Goals.as_str(), "goals");
328        assert_eq!(MemoryBlockType::Scratch.as_str(), "scratch");
329    }
330
331    #[test]
332    fn test_block_type_priority() {
333        assert_eq!(MemoryBlockType::System.priority(), 0);
334        assert_eq!(MemoryBlockType::Scratch.priority(), 5);
335    }
336
337    #[test]
338    fn test_block_type_all_ordered() {
339        let types = MemoryBlockType::all_ordered();
340        assert_eq!(types.len(), 6);
341        assert_eq!(types[0], MemoryBlockType::System);
342        assert_eq!(types[5], MemoryBlockType::Scratch);
343    }
344
345    #[test]
346    fn test_memory_block_new() {
347        let block = MemoryBlock::new(MemoryBlockType::System, "Hello world", 1000);
348
349        assert_eq!(block.block_type(), MemoryBlockType::System);
350        assert_eq!(block.content(), "Hello world");
351        assert_eq!(block.size_bytes(), 11);
352        assert!(block.label().is_none());
353        assert_eq!(block.created_at_ms(), 1000);
354        assert_eq!(block.modified_at_ms(), 1000);
355    }
356
357    #[test]
358    fn test_memory_block_with_label() {
359        let block = MemoryBlock::with_label(
360            MemoryBlockType::Facts,
361            "user_preferences",
362            "Likes cats",
363            2000,
364        );
365
366        assert_eq!(block.block_type(), MemoryBlockType::Facts);
367        assert_eq!(block.label(), Some("user_preferences"));
368        assert_eq!(block.content(), "Likes cats");
369        assert_eq!(block.size_bytes(), 10);
370    }
371
372    #[test]
373    fn test_memory_block_set_content() {
374        let mut block = MemoryBlock::new(MemoryBlockType::Scratch, "initial", 1000);
375        assert_eq!(block.size_bytes(), 7);
376
377        block.set_content("updated content here", 2000);
378
379        assert_eq!(block.content(), "updated content here");
380        assert_eq!(block.size_bytes(), 20);
381        assert_eq!(block.created_at_ms(), 1000);
382        assert_eq!(block.modified_at_ms(), 2000);
383    }
384
385    #[test]
386    fn test_memory_block_render() {
387        let block = MemoryBlock::new(MemoryBlockType::System, "You are helpful.", 1000);
388        let rendered = block.render();
389
390        assert!(rendered.contains("<block type=\"system\">"));
391        assert!(rendered.contains("You are helpful."));
392        assert!(rendered.contains("</block>"));
393    }
394
395    #[test]
396    fn test_memory_block_render_with_label() {
397        let block = MemoryBlock::with_label(MemoryBlockType::Human, "profile", "Name: Alice", 1000);
398        let rendered = block.render();
399
400        assert!(rendered.contains("type=\"human\""));
401        assert!(rendered.contains("label=\"profile\""));
402        assert!(rendered.contains("Name: Alice"));
403    }
404
405    #[test]
406    fn test_memory_block_id_unique() {
407        let id1 = MemoryBlockId::new();
408        let id2 = MemoryBlockId::new();
409        assert_ne!(id1, id2);
410    }
411
412    #[test]
413    #[should_panic(expected = "block content")]
414    fn test_memory_block_content_too_large() {
415        let large_content = "x".repeat(CORE_MEMORY_BLOCK_SIZE_BYTES_MAX + 1);
416        let _ = MemoryBlock::new(MemoryBlockType::System, large_content, 1000);
417    }
418
419    #[test]
420    #[should_panic(expected = "block label")]
421    fn test_memory_block_label_too_large() {
422        let large_label = "x".repeat(CORE_MEMORY_BLOCK_LABEL_BYTES_MAX + 1);
423        let _ = MemoryBlock::with_label(MemoryBlockType::System, large_label, "content", 1000);
424    }
425}