1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5pub struct MemoryRecord {
6 pub id: Uuid,
7 pub agent_id: String,
8 pub content: String,
9 pub memory_type: MemoryType,
10 pub scope: Scope,
11 pub importance: f32,
12 pub tags: Vec<String>,
13 pub metadata: serde_json::Value,
14 pub embedding: Option<Vec<f32>>,
15 pub content_hash: Vec<u8>,
16 pub prev_hash: Option<Vec<u8>>,
17 pub source_type: SourceType,
18 pub source_id: Option<String>,
19 pub consolidation_state: ConsolidationState,
20 pub access_count: u64,
21 pub org_id: Option<String>,
22 pub thread_id: Option<String>,
23 pub created_at: String,
24 pub updated_at: String,
25 pub last_accessed_at: Option<String>,
26 pub expires_at: Option<String>,
27 pub deleted_at: Option<String>,
28 pub decay_rate: Option<f32>,
29 pub created_by: Option<String>,
30 pub version: u32,
31 pub prev_version_id: Option<uuid::Uuid>,
32 pub quarantined: bool,
33 pub quarantine_reason: Option<String>,
34 pub decay_function: Option<String>,
35}
36
37impl MemoryRecord {
38 pub fn new(agent_id: String, content: String) -> Self {
41 let now = chrono::Utc::now().to_rfc3339();
42 let content_hash = crate::hash::compute_content_hash(&content, &agent_id, &now);
43 Self {
44 id: Uuid::now_v7(),
45 agent_id,
46 content,
47 memory_type: MemoryType::Episodic,
48 scope: Scope::Private,
49 importance: 0.5,
50 tags: vec![],
51 metadata: serde_json::json!({}),
52 embedding: None,
53 content_hash,
54 prev_hash: None,
55 source_type: SourceType::Agent,
56 source_id: None,
57 consolidation_state: ConsolidationState::Raw,
58 access_count: 0,
59 org_id: None,
60 thread_id: None,
61 created_at: now.clone(),
62 updated_at: now,
63 last_accessed_at: None,
64 expires_at: None,
65 deleted_at: None,
66 decay_rate: None,
67 created_by: None,
68 version: 1,
69 prev_version_id: None,
70 quarantined: false,
71 quarantine_reason: None,
72 decay_function: None,
73 }
74 }
75
76 #[allow(clippy::too_many_arguments)]
79 pub fn from_parts(
80 id: Uuid,
81 agent_id: String,
82 content: String,
83 memory_type: MemoryType,
84 scope: Scope,
85 importance: f32,
86 tags: Vec<String>,
87 metadata: serde_json::Value,
88 embedding: Option<Vec<f32>>,
89 content_hash: Vec<u8>,
90 prev_hash: Option<Vec<u8>>,
91 source_type: SourceType,
92 source_id: Option<String>,
93 consolidation_state: ConsolidationState,
94 access_count: u64,
95 org_id: Option<String>,
96 thread_id: Option<String>,
97 created_at: String,
98 updated_at: String,
99 last_accessed_at: Option<String>,
100 expires_at: Option<String>,
101 deleted_at: Option<String>,
102 decay_rate: Option<f32>,
103 created_by: Option<String>,
104 version: u32,
105 prev_version_id: Option<Uuid>,
106 quarantined: bool,
107 quarantine_reason: Option<String>,
108 decay_function: Option<String>,
109 ) -> Self {
110 Self {
111 id,
112 agent_id,
113 content,
114 memory_type,
115 scope,
116 importance,
117 tags,
118 metadata,
119 embedding,
120 content_hash,
121 prev_hash,
122 source_type,
123 source_id,
124 consolidation_state,
125 access_count,
126 org_id,
127 thread_id,
128 created_at,
129 updated_at,
130 last_accessed_at,
131 expires_at,
132 deleted_at,
133 decay_rate,
134 created_by,
135 version,
136 prev_version_id,
137 quarantined,
138 quarantine_reason,
139 decay_function,
140 }
141 }
142
143 pub fn is_deleted(&self) -> bool {
144 self.deleted_at.is_some()
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150pub enum MemoryType {
151 Episodic,
152 Semantic,
153 Procedural,
154 Working,
155}
156
157pub type MemoryTier = MemoryType;
174
175impl std::fmt::Display for MemoryType {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 match self {
178 MemoryType::Episodic => write!(f, "episodic"),
179 MemoryType::Semantic => write!(f, "semantic"),
180 MemoryType::Procedural => write!(f, "procedural"),
181 MemoryType::Working => write!(f, "working"),
182 }
183 }
184}
185
186impl std::str::FromStr for MemoryType {
187 type Err = crate::error::Error;
188 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
189 match s {
190 "episodic" => Ok(MemoryType::Episodic),
191 "semantic" => Ok(MemoryType::Semantic),
192 "procedural" => Ok(MemoryType::Procedural),
193 "working" => Ok(MemoryType::Working),
194 _ => Err(crate::error::Error::Validation(format!(
195 "invalid memory type: {s}"
196 ))),
197 }
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(rename_all = "snake_case")]
203pub enum Scope {
204 Private,
205 Shared,
206 Public,
207 Global,
208}
209
210impl std::fmt::Display for Scope {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 match self {
213 Scope::Private => write!(f, "private"),
214 Scope::Shared => write!(f, "shared"),
215 Scope::Public => write!(f, "public"),
216 Scope::Global => write!(f, "global"),
217 }
218 }
219}
220
221impl std::str::FromStr for Scope {
222 type Err = crate::error::Error;
223 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
224 match s {
225 "private" => Ok(Scope::Private),
226 "shared" => Ok(Scope::Shared),
227 "public" => Ok(Scope::Public),
228 "global" => Ok(Scope::Global),
229 _ => Err(crate::error::Error::Validation(format!(
230 "invalid scope: {s}"
231 ))),
232 }
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(rename_all = "snake_case")]
238pub enum ConsolidationState {
239 Raw,
240 Active,
241 Pending,
242 Consolidated,
243 Archived,
244 Forgotten,
245}
246
247impl std::fmt::Display for ConsolidationState {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 match self {
250 ConsolidationState::Raw => write!(f, "raw"),
251 ConsolidationState::Active => write!(f, "active"),
252 ConsolidationState::Pending => write!(f, "pending"),
253 ConsolidationState::Consolidated => write!(f, "consolidated"),
254 ConsolidationState::Archived => write!(f, "archived"),
255 ConsolidationState::Forgotten => write!(f, "forgotten"),
256 }
257 }
258}
259
260impl std::str::FromStr for ConsolidationState {
261 type Err = crate::error::Error;
262 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
263 match s {
264 "raw" => Ok(ConsolidationState::Raw),
265 "active" => Ok(ConsolidationState::Active),
266 "pending" => Ok(ConsolidationState::Pending),
267 "consolidated" => Ok(ConsolidationState::Consolidated),
268 "archived" => Ok(ConsolidationState::Archived),
269 "forgotten" => Ok(ConsolidationState::Forgotten),
270 _ => Err(crate::error::Error::Validation(format!(
271 "invalid consolidation state: {s}"
272 ))),
273 }
274 }
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278#[serde(rename_all = "snake_case")]
279pub enum SourceType {
280 Agent,
281 Human,
282 System,
283 UserInput,
284 ToolOutput,
285 ModelResponse,
286 Retrieval,
287 Consolidation,
288 Import,
289}
290
291impl std::fmt::Display for SourceType {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 match self {
294 SourceType::Agent => write!(f, "agent"),
295 SourceType::Human => write!(f, "human"),
296 SourceType::System => write!(f, "system"),
297 SourceType::UserInput => write!(f, "user_input"),
298 SourceType::ToolOutput => write!(f, "tool_output"),
299 SourceType::ModelResponse => write!(f, "model_response"),
300 SourceType::Retrieval => write!(f, "retrieval"),
301 SourceType::Consolidation => write!(f, "consolidation"),
302 SourceType::Import => write!(f, "import"),
303 }
304 }
305}
306
307impl std::str::FromStr for SourceType {
308 type Err = crate::error::Error;
309 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
310 match s {
311 "agent" => Ok(SourceType::Agent),
312 "human" => Ok(SourceType::Human),
313 "system" => Ok(SourceType::System),
314 "user_input" => Ok(SourceType::UserInput),
315 "tool_output" => Ok(SourceType::ToolOutput),
316 "model_response" => Ok(SourceType::ModelResponse),
317 "retrieval" => Ok(SourceType::Retrieval),
318 "consolidation" => Ok(SourceType::Consolidation),
319 "import" => Ok(SourceType::Import),
320 _ => Err(crate::error::Error::Validation(format!(
321 "invalid source type: {s}"
322 ))),
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use uuid::Uuid;
331
332 fn sample_record() -> MemoryRecord {
333 MemoryRecord {
334 id: Uuid::now_v7(),
335 agent_id: "agent-1".to_string(),
336 content: "The user prefers dark mode".to_string(),
337 memory_type: MemoryType::Semantic,
338 scope: Scope::Private,
339 importance: 0.8,
340 tags: vec!["preference".to_string(), "ui".to_string()],
341 metadata: serde_json::json!({"source": "conversation"}),
342 embedding: None,
343 content_hash: vec![1, 2, 3],
344 prev_hash: None,
345 source_type: SourceType::Agent,
346 source_id: None,
347 consolidation_state: ConsolidationState::Raw,
348 access_count: 0,
349 org_id: None,
350 thread_id: None,
351 created_at: "2025-01-01T00:00:00Z".to_string(),
352 updated_at: "2025-01-01T00:00:00Z".to_string(),
353 last_accessed_at: None,
354 expires_at: None,
355 deleted_at: None,
356 decay_rate: None,
357 created_by: None,
358 version: 1,
359 prev_version_id: None,
360 quarantined: false,
361 quarantine_reason: None,
362 decay_function: None,
363 }
364 }
365
366 #[test]
367 fn test_serde_roundtrip() {
368 let record = sample_record();
369 let json = serde_json::to_string(&record).unwrap();
370 let deserialized: MemoryRecord = serde_json::from_str(&json).unwrap();
371 assert_eq!(record, deserialized);
372 }
373
374 #[test]
375 fn test_enum_serde() {
376 assert_eq!(
377 serde_json::to_string(&MemoryType::Episodic).unwrap(),
378 "\"episodic\""
379 );
380 assert_eq!(
381 serde_json::to_string(&Scope::Private).unwrap(),
382 "\"private\""
383 );
384 assert_eq!(
385 serde_json::to_string(&SourceType::Agent).unwrap(),
386 "\"agent\""
387 );
388 assert_eq!(
389 serde_json::to_string(&ConsolidationState::Raw).unwrap(),
390 "\"raw\""
391 );
392 }
393
394 #[test]
395 fn test_is_deleted() {
396 let mut record = sample_record();
397 assert!(!record.is_deleted());
398 record.deleted_at = Some("2025-01-02T00:00:00Z".to_string());
399 assert!(record.is_deleted());
400 }
401
402 #[test]
403 fn test_enum_fromstr() {
404 assert_eq!(
405 "episodic".parse::<MemoryType>().unwrap(),
406 MemoryType::Episodic
407 );
408 assert_eq!(
409 "working".parse::<MemoryType>().unwrap(),
410 MemoryType::Working
411 );
412 assert_eq!("shared".parse::<Scope>().unwrap(), Scope::Shared);
413 assert_eq!("human".parse::<SourceType>().unwrap(), SourceType::Human);
414 assert_eq!(
415 "active".parse::<ConsolidationState>().unwrap(),
416 ConsolidationState::Active
417 );
418 assert_eq!(
419 "pending".parse::<ConsolidationState>().unwrap(),
420 ConsolidationState::Pending
421 );
422 assert_eq!(
423 "forgotten".parse::<ConsolidationState>().unwrap(),
424 ConsolidationState::Forgotten
425 );
426 assert!("invalid".parse::<MemoryType>().is_err());
427 }
428
429 #[test]
430 fn test_extended_enums_parse() {
431 assert_eq!(
433 "user_input".parse::<SourceType>().unwrap(),
434 SourceType::UserInput
435 );
436 assert_eq!(
437 "tool_output".parse::<SourceType>().unwrap(),
438 SourceType::ToolOutput
439 );
440 assert_eq!(
441 "model_response".parse::<SourceType>().unwrap(),
442 SourceType::ModelResponse
443 );
444 assert_eq!(
445 "retrieval".parse::<SourceType>().unwrap(),
446 SourceType::Retrieval
447 );
448 assert_eq!(
449 "consolidation".parse::<SourceType>().unwrap(),
450 SourceType::Consolidation
451 );
452 assert_eq!("import".parse::<SourceType>().unwrap(), SourceType::Import);
453
454 assert_eq!("global".parse::<Scope>().unwrap(), Scope::Global);
456
457 assert_eq!(SourceType::UserInput.to_string(), "user_input");
459 assert_eq!(SourceType::Import.to_string(), "import");
460 assert_eq!(Scope::Global.to_string(), "global");
461 }
462}