1use serde::{Deserialize, Serialize};
4
5use crate::types::*;
6use crate::types::{AgentId, MemoryId, SpaceId, UserId};
7
8fn default_user_id() -> UserId {
14 UserId::nil()
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub enum MemoryType {
20 Episodic,
22 Semantic,
24 Procedural,
26 AntiPattern,
28 Reasoning,
30 Correction,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct MemoryNode {
40 pub id: MemoryId,
42 pub agent_id: AgentId,
44 #[serde(default = "default_user_id")]
50 pub user_id: UserId,
51 pub memory_type: MemoryType,
53 pub embedding: Embedding,
55 pub content: String,
57 pub created_at: Timestamp,
59 pub accessed_at: Timestamp,
61 pub access_count: u32,
63 pub salience: Salience,
65 pub confidence: Confidence,
67 pub space_id: SpaceId,
69 pub attributes: std::collections::HashMap<String, AttributeValue>,
71 pub tags: Vec<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub valid_from: Option<Timestamp>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub valid_until: Option<Timestamp>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub context: Option<String>,
89}
90
91impl MemoryNode {
92 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 pub fn with_user_id(mut self, user_id: UserId) -> Self {
130 self.user_id = user_id;
131 self
132 }
133
134 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 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 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 pub fn invalidate(&mut self, at: Timestamp) {
166 self.valid_until = Some(at);
167 }
168
169 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 assert_eq!(n.content, "the migration plan");
191 assert_eq!(
193 n.indexed_text(),
194 "From a thread about billing\nthe migration plan"
195 );
196
197 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
271pub enum AttributeValue {
272 String(String),
274 Integer(i64),
276 Float(f64),
278 Boolean(bool),
280 Bytes(Vec<u8>),
282}