Skip to main content

lago_core/
cognitive.rs

1//! Cognitive memory types for the Lago Cognitive Storage layer.
2//!
3//! These types model agent memory as structured units (MemCubes) organized
4//! into cognitive tiers, following the MemOS research paradigm where each
5//! memory unit carries content, metadata, causal links, and lifecycle state.
6
7use serde::{Deserialize, Serialize};
8
9/// Memory tier classification — how the knowledge was formed.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum MemoryTier {
13    /// Working memory — current session context, ephemeral.
14    Working,
15    /// Episodic memory — specific experiences and conversations.
16    Episodic,
17    /// Semantic memory — extracted facts, patterns, knowledge.
18    Semantic,
19    /// Procedural memory — tested approaches, recipes, workflows.
20    Procedural,
21    /// Meta memory — EGRI evaluations, self-metrics, introspection.
22    Meta,
23}
24
25/// Cognition phase that produced this memory.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum CognitionKind {
29    /// Observation — raw sensory input or environment reading.
30    Perceive,
31    /// Deliberation — weighing options, reasoning.
32    Deliberate,
33    /// Decision — committing to a course of action.
34    Decide,
35    /// Action — executing a decision (tool use, code generation).
36    Act,
37    /// Verification — checking an outcome against expectations.
38    Verify,
39    /// Reflection — post-hoc analysis of what happened and why.
40    Reflect,
41    /// Consolidation — synthesizing multiple memories into a pattern.
42    Consolidate,
43    /// Governance — meta-level policy or strategy adjustment.
44    Govern,
45}
46
47/// A MemCube is the fundamental unit of cognitive memory.
48///
49/// Each MemCube carries natural-language content, cognition metadata,
50/// importance/confidence scores, causal links, and lifecycle state.
51/// MemCubes are storage-agnostic — they can be persisted in Lance,
52/// redb, or plain files.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct MemCube {
55    /// Unique identifier (typically a ULID).
56    pub id: String,
57
58    /// Memory tier classification.
59    pub tier: MemoryTier,
60
61    /// Cognition phase that produced this memory.
62    pub kind: CognitionKind,
63
64    /// Natural language content of the memory.
65    pub content: String,
66
67    /// Source identifier (e.g., "filesystem", "lance", "session:XYZ").
68    pub source: String,
69
70    /// Importance score (0.0 to 1.0), updated by access patterns.
71    pub importance: f32,
72
73    /// Confidence score (0.0 to 1.0), how certain the information is.
74    pub confidence: f32,
75
76    /// Decay rate — how fast relevance fades without access.
77    pub decay_rate: f32,
78
79    /// IDs of memories that caused this one.
80    pub caused_by: Vec<String>,
81
82    /// IDs of memories this one led to.
83    pub leads_to: Vec<String>,
84
85    /// IDs of decisions this memory serves as evidence for.
86    pub evidence_for: Vec<String>,
87
88    /// Creation timestamp (microseconds since epoch).
89    pub created_at: u64,
90
91    /// Last access timestamp (microseconds since epoch).
92    pub last_accessed: u64,
93
94    /// Number of times this memory has been accessed.
95    pub access_count: u32,
96
97    /// Session that created this memory (if applicable).
98    pub session_id: Option<String>,
99}
100
101impl MemCube {
102    /// Create a new MemCube with sensible defaults.
103    pub fn new(
104        tier: MemoryTier,
105        kind: CognitionKind,
106        content: impl Into<String>,
107        source: impl Into<String>,
108    ) -> Self {
109        let now = now_micros();
110        Self {
111            id: ulid::Ulid::new().to_string(),
112            tier,
113            kind,
114            content: content.into(),
115            source: source.into(),
116            importance: 0.5,
117            confidence: 0.5,
118            decay_rate: 0.01,
119            caused_by: Vec::new(),
120            leads_to: Vec::new(),
121            evidence_for: Vec::new(),
122            created_at: now,
123            last_accessed: now,
124            access_count: 0,
125            session_id: None,
126        }
127    }
128
129    /// Compute current relevance factoring importance and time decay.
130    ///
131    /// Returns a value in \[0, 1\] representing how relevant this memory
132    /// is right now, combining its importance with exponential time decay.
133    pub fn relevance(&self) -> f32 {
134        let now = now_micros();
135        let age_hours = (now.saturating_sub(self.last_accessed)) as f64 / 3_600_000_000.0;
136        let decay = (-self.decay_rate as f64 * age_hours).exp() as f32;
137        self.importance * decay
138    }
139
140    /// Record an access, updating last_accessed and access_count.
141    pub fn touch(&mut self) {
142        self.last_accessed = now_micros();
143        self.access_count = self.access_count.saturating_add(1);
144    }
145}
146
147fn now_micros() -> u64 {
148    std::time::SystemTime::now()
149        .duration_since(std::time::UNIX_EPOCH)
150        .unwrap_or_default()
151        .as_micros() as u64
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn memcube_new_defaults() {
160        let cube = MemCube::new(
161            MemoryTier::Semantic,
162            CognitionKind::Consolidate,
163            "Rust is a systems language",
164            "test",
165        );
166        assert_eq!(cube.tier, MemoryTier::Semantic);
167        assert_eq!(cube.kind, CognitionKind::Consolidate);
168        assert_eq!(cube.content, "Rust is a systems language");
169        assert!((cube.importance - 0.5).abs() < f32::EPSILON);
170        assert!((cube.confidence - 0.5).abs() < f32::EPSILON);
171        assert_eq!(cube.access_count, 0);
172        assert!(cube.caused_by.is_empty());
173    }
174
175    #[test]
176    fn memcube_relevance_fresh() {
177        let cube = MemCube::new(MemoryTier::Episodic, CognitionKind::Perceive, "obs", "t");
178        // Just created, relevance should be close to importance
179        let rel = cube.relevance();
180        assert!(
181            rel > 0.4,
182            "relevance should be near importance for fresh memory: {rel}"
183        );
184    }
185
186    #[test]
187    fn memcube_touch_increments() {
188        let mut cube = MemCube::new(MemoryTier::Procedural, CognitionKind::Act, "approach", "t");
189        let before = cube.last_accessed;
190        cube.touch();
191        assert_eq!(cube.access_count, 1);
192        assert!(cube.last_accessed >= before);
193    }
194
195    #[test]
196    fn memory_tier_serde_roundtrip() {
197        for tier in [
198            MemoryTier::Working,
199            MemoryTier::Episodic,
200            MemoryTier::Semantic,
201            MemoryTier::Procedural,
202            MemoryTier::Meta,
203        ] {
204            let json = serde_json::to_string(&tier).unwrap();
205            let back: MemoryTier = serde_json::from_str(&json).unwrap();
206            assert_eq!(tier, back);
207        }
208    }
209
210    #[test]
211    fn cognition_kind_serde_roundtrip() {
212        for kind in [
213            CognitionKind::Perceive,
214            CognitionKind::Deliberate,
215            CognitionKind::Decide,
216            CognitionKind::Act,
217            CognitionKind::Verify,
218            CognitionKind::Reflect,
219            CognitionKind::Consolidate,
220            CognitionKind::Govern,
221        ] {
222            let json = serde_json::to_string(&kind).unwrap();
223            let back: CognitionKind = serde_json::from_str(&json).unwrap();
224            assert_eq!(kind, back);
225        }
226    }
227
228    #[test]
229    fn memcube_full_serde_roundtrip() {
230        let mut cube = MemCube::new(
231            MemoryTier::Meta,
232            CognitionKind::Reflect,
233            "self-evaluation note",
234            "egri",
235        );
236        cube.caused_by = vec!["A".into()];
237        cube.leads_to = vec!["B".into()];
238        cube.session_id = Some("SESS001".into());
239
240        let json = serde_json::to_string(&cube).unwrap();
241        let back: MemCube = serde_json::from_str(&json).unwrap();
242        assert_eq!(back.tier, MemoryTier::Meta);
243        assert_eq!(back.kind, CognitionKind::Reflect);
244        assert_eq!(back.content, "self-evaluation note");
245        assert_eq!(back.caused_by, vec!["A"]);
246        assert_eq!(back.session_id, Some("SESS001".into()));
247    }
248
249    #[test]
250    fn unique_ids() {
251        let a = MemCube::new(MemoryTier::Working, CognitionKind::Perceive, "a", "t");
252        let b = MemCube::new(MemoryTier::Working, CognitionKind::Perceive, "b", "t");
253        assert_ne!(a.id, b.id);
254    }
255}