telos_agent/knowledge/memory/
format.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5#[serde(rename_all = "snake_case")]
6pub enum MemoryCategory {
7 Script,
8 Command,
9 Pattern,
10 Fact,
11 Workflow,
12}
13
14#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
16#[serde(rename_all = "snake_case")]
17pub enum MemoryStatus {
18 #[default]
19 Working,
20 #[serde(rename = "needs_fix")]
21 NeedsFix,
22 Deprecated,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct MemoryEntry {
28 pub name: String,
29 pub description: String,
30 pub category: MemoryCategory,
31 #[serde(default)]
32 pub tags: Vec<String>,
33 pub created: String,
34 pub updated: String,
35 #[serde(default)]
36 pub status: MemoryStatus,
37 #[serde(default)]
38 pub times_used: u32,
39 #[serde(default)]
40 pub confidence: Option<String>,
41 #[serde(default)]
42 pub related: Vec<String>,
43 #[serde(default)]
44 pub source_session: Option<String>,
45 #[serde(skip)]
47 pub body: String,
48}
49
50pub struct MemoryFormat;
52
53impl MemoryFormat {
54 pub fn parse(content: &str) -> Option<MemoryEntry> {
57 let content = content.trim();
58 let rest = content.strip_prefix("---")?;
59 let (frontmatter, body) = rest.split_once("\n---")?;
60 let body = body.trim().to_string();
61 let mut entry: MemoryEntry = serde_yaml::from_str(frontmatter).ok()?;
62 entry.body = body;
63 Some(entry)
64 }
65
66 pub fn serialize(entry: &MemoryEntry) -> String {
68 let frontmatter = serde_yaml::to_string(entry).unwrap_or_default();
69 format!("---\n{}---\n\n{}", frontmatter, entry.body)
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn parse_valid_memory_entry() {
79 let content = r#"---
80name: deploy-staging
81description: Deploy to staging
82category: script
83tags: [deploy, staging]
84created: "2026-06-18"
85updated: "2026-06-18"
86status: working
87times_used: 3
88related: [docker-setup]
89---
90# Deploy script
91Some body text.
92"#;
93 let entry = MemoryFormat::parse(content).unwrap();
94 assert_eq!(entry.name, "deploy-staging");
95 assert_eq!(entry.category, MemoryCategory::Script);
96 assert_eq!(entry.tags, vec!["deploy", "staging"]);
97 assert_eq!(entry.times_used, 3);
98 assert_eq!(entry.status, MemoryStatus::Working);
99 assert!(entry.body.contains("Some body text"));
100 }
101
102 #[test]
103 fn parse_missing_frontmatter_returns_none() {
104 assert!(MemoryFormat::parse("no frontmatter here").is_none());
105 }
106
107 #[test]
108 fn serialize_roundtrip() {
109 let content = r#"---
110name: test
111description: Test memory
112category: fact
113tags: []
114created: "2026-06-18"
115updated: "2026-06-18"
116status: working
117times_used: 0
118related: []
119---
120Body text.
121"#;
122 let entry = MemoryFormat::parse(content).unwrap();
123 let serialized = MemoryFormat::serialize(&entry);
124 let reparse = MemoryFormat::parse(&serialized).unwrap();
125 assert_eq!(entry.name, reparse.name);
126 assert_eq!(entry.body, reparse.body);
127 assert_eq!(entry.category, reparse.category);
128 }
129
130 #[test]
131 fn default_status_is_working() {
132 assert_eq!(MemoryStatus::default(), MemoryStatus::Working);
133 }
134}