Skip to main content

mentedb_core/
memory.rs

1//! MemoryNode: the fundamental unit of storage in MenteDB.
2
3use serde::{Deserialize, Serialize};
4
5use crate::types::*;
6use crate::types::{AgentId, MemoryId, SpaceId, UserId};
7
8/// Default `user_id` for memories deserialized from data written before the
9/// `user_id` axis existed: the nil (shared/global) user. This must be an
10/// explicit function, not `#[serde(default)]`, because `UserId::default()`
11/// mints a fresh random id, which would make old memories look owned by a
12/// random user instead of shared.
13fn default_user_id() -> UserId {
14    UserId::nil()
15}
16
17/// The type classification of a memory.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub enum MemoryType {
20    /// A specific event or interaction.
21    Episodic,
22    /// A general fact or concept.
23    Semantic,
24    /// A learned skill or procedure.
25    Procedural,
26    /// What NOT to do: learned from failures.
27    AntiPattern,
28    /// A pre-computed reasoning chain or decision template.
29    Reasoning,
30    /// A correction record: "was X, actually Y because Z".
31    Correction,
32}
33
34/// A memory node: the atomic unit of knowledge in MenteDB.
35///
36/// Combines vector embeddings, graph connections, temporal properties,
37/// and flexible attributes in a single primitive.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct MemoryNode {
40    /// Unique identifier.
41    pub id: MemoryId,
42    /// The agent that owns this memory.
43    pub agent_id: AgentId,
44    /// The end user that owns this memory (orthogonal to `agent_id`).
45    ///
46    /// A memory belongs to both a user and an agent; a scoped query sees it
47    /// only when it is visible on both axes. Defaults to the nil (shared)
48    /// user, both for `new()` and for data written before this field existed.
49    #[serde(default = "default_user_id")]
50    pub user_id: UserId,
51    /// Memory type classification.
52    pub memory_type: MemoryType,
53    /// Embedding vector for semantic similarity search.
54    pub embedding: Embedding,
55    /// Human-readable content.
56    pub content: String,
57    /// When this memory was created.
58    pub created_at: Timestamp,
59    /// When this memory was last accessed.
60    pub accessed_at: Timestamp,
61    /// How many times this memory has been accessed.
62    pub access_count: u32,
63    /// Current salience score (decays over time).
64    pub salience: Salience,
65    /// Confidence in this memory's accuracy.
66    pub confidence: Confidence,
67    /// The memory space this belongs to.
68    pub space_id: SpaceId,
69    /// Flexible key-value attributes.
70    pub attributes: std::collections::HashMap<String, AttributeValue>,
71    /// Tags for bitmap indexing.
72    pub tags: Vec<String>,
73    /// When this fact became true in the real world.
74    /// None means valid since creation.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub valid_from: Option<Timestamp>,
77    /// When this fact stopped being true.
78    /// None means still valid.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub valid_until: Option<Timestamp>,
81    /// Optional retrieval context, prepended to the content when the memory is
82    /// indexed (BM25) and embedded, but NOT stored as part of the content the
83    /// caller reads back. This is the contextual-retrieval hook: a short
84    /// situating blurb the caller generates (for example "From a thread about the
85    /// billing migration") that makes an otherwise-ambiguous memory findable by
86    /// terms it never literally contains. None indexes the content as-is.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub context: Option<String>,
89}
90
91impl MemoryNode {
92    /// Create a new memory node with the given content and embedding.
93    pub fn new(
94        agent_id: AgentId,
95        memory_type: MemoryType,
96        content: String,
97        embedding: Embedding,
98    ) -> Self {
99        let now = std::time::SystemTime::now()
100            .duration_since(std::time::UNIX_EPOCH)
101            .unwrap_or_default()
102            .as_micros() as u64;
103
104        Self {
105            id: MemoryId::new(),
106            agent_id,
107            user_id: UserId::nil(),
108            memory_type,
109            embedding,
110            content,
111            created_at: now,
112            accessed_at: now,
113            access_count: 0,
114            salience: 1.0,
115            confidence: 1.0,
116            space_id: SpaceId::nil(),
117            attributes: std::collections::HashMap::new(),
118            tags: Vec::new(),
119            valid_from: None,
120            valid_until: None,
121            context: None,
122        }
123    }
124
125    /// Set the owning end user, returning the node (builder style).
126    ///
127    /// Parallels the `agent_id` constructor argument on the orthogonal user
128    /// axis. Leave unset (nil) for shared/global memories.
129    pub fn with_user_id(mut self, user_id: UserId) -> Self {
130        self.user_id = user_id;
131        self
132    }
133
134    /// Attach retrieval context (builder style). See [`MemoryNode::context`].
135    pub fn with_context(mut self, context: impl Into<String>) -> Self {
136        let ctx = context.into();
137        self.context = if ctx.is_empty() { None } else { Some(ctx) };
138        self
139    }
140
141    /// The text to index and embed: the context (when present) prefixed to the
142    /// content, so retrieval matches on both while the stored `content` stays
143    /// exactly what the caller wrote.
144    pub fn indexed_text(&self) -> std::borrow::Cow<'_, str> {
145        match &self.context {
146            Some(ctx) if !ctx.is_empty() => {
147                std::borrow::Cow::Owned(format!("{ctx}\n{}", self.content))
148            }
149            _ => std::borrow::Cow::Borrowed(&self.content),
150        }
151    }
152}
153
154impl MemoryNode {
155    /// Returns true if this memory is temporally valid at the given timestamp.
156    pub fn is_valid_at(&self, at: Timestamp) -> bool {
157        let from = self.valid_from.unwrap_or(0);
158        match self.valid_until {
159            Some(until) => at >= from && at < until,
160            None => at >= from,
161        }
162    }
163
164    /// Mark this memory as no longer valid.
165    pub fn invalidate(&mut self, at: Timestamp) {
166        self.valid_until = Some(at);
167    }
168
169    /// Returns true if this memory has been invalidated.
170    pub fn is_invalidated(&self) -> bool {
171        self.valid_until.is_some()
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::types::{AgentId, UserId};
179
180    #[test]
181    fn context_prefixes_indexed_text_but_not_content() {
182        let n = MemoryNode::new(
183            AgentId::new(),
184            MemoryType::Semantic,
185            "the migration plan".to_string(),
186            vec![1.0],
187        )
188        .with_context("From a thread about billing");
189        // The stored content the caller reads back is untouched.
190        assert_eq!(n.content, "the migration plan");
191        // What gets indexed/embedded carries the context prefix.
192        assert_eq!(
193            n.indexed_text(),
194            "From a thread about billing\nthe migration plan"
195        );
196
197        // No context (and empty context) indexes the content as-is.
198        let plain = MemoryNode::new(
199            AgentId::new(),
200            MemoryType::Semantic,
201            "x".to_string(),
202            vec![1.0],
203        );
204        assert_eq!(plain.indexed_text(), "x");
205        assert_eq!(plain.context, None);
206        assert_eq!(plain.with_context("").context, None);
207    }
208
209    #[test]
210    fn new_memory_has_no_temporal_bounds() {
211        let node = MemoryNode::new(
212            AgentId::new(),
213            MemoryType::Semantic,
214            "test".to_string(),
215            vec![1.0],
216        );
217        assert_eq!(node.valid_from, None);
218        assert_eq!(node.valid_until, None);
219        assert!(!node.is_invalidated());
220    }
221
222    #[test]
223    fn invalidate_memory() {
224        let mut node = MemoryNode::new(
225            AgentId::new(),
226            MemoryType::Semantic,
227            "Alice works at Acme".to_string(),
228            vec![1.0],
229        );
230        assert!(node.is_valid_at(node.created_at));
231
232        node.invalidate(node.created_at + 1_000_000);
233        assert!(node.is_invalidated());
234        assert!(node.is_valid_at(node.created_at));
235        assert!(!node.is_valid_at(node.created_at + 1_000_000));
236    }
237
238    #[test]
239    fn serde_backward_compatible() {
240        let json = r#"{"id":"00000000-0000-0000-0000-000000000001","agent_id":"00000000-0000-0000-0000-000000000002","memory_type":"Semantic","embedding":[1.0],"content":"test","created_at":1000,"accessed_at":1000,"access_count":0,"salience":1.0,"confidence":1.0,"space_id":"00000000-0000-0000-0000-000000000000","attributes":{},"tags":[]}"#;
241        let node: MemoryNode = serde_json::from_str(json).unwrap();
242        assert_eq!(node.valid_from, None);
243        assert_eq!(node.valid_until, None);
244        assert!(node.is_valid_at(5000));
245        // Data written before the user_id axis existed must deserialize as the
246        // nil (shared) user, never a random one. A random default would make
247        // legacy memories look privately owned and vanish from every scoped
248        // query.
249        assert!(
250            node.user_id.is_nil(),
251            "legacy memory (no user_id) must default to the nil user"
252        );
253    }
254
255    #[test]
256    fn with_user_id_sets_owner() {
257        let user = UserId::new();
258        let node = MemoryNode::new(
259            AgentId::new(),
260            MemoryType::Semantic,
261            "test".to_string(),
262            vec![1.0],
263        )
264        .with_user_id(user);
265        assert_eq!(node.user_id, user);
266    }
267}
268
269/// Flexible attribute values for memory metadata.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub enum AttributeValue {
272    /// A string value.
273    String(String),
274    /// An integer value.
275    Integer(i64),
276    /// A floating-point value.
277    Float(f64),
278    /// A boolean value.
279    Boolean(bool),
280    /// Raw bytes.
281    Bytes(Vec<u8>),
282}