Skip to main content

telos_agent/knowledge/memory/
format.rs

1use serde::{Deserialize, Serialize};
2
3/// Category of a memory entry — determines storage subdirectory.
4#[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/// Lifecycle status of a memory entry.
15#[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/// A single memory entry — stored as a markdown file with YAML frontmatter.
26#[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    /// Body markdown — NOT serialized in YAML, stored after the frontmatter.
46    #[serde(skip)]
47    pub body: String,
48}
49
50/// Parses and serializes memory entries in YAML frontmatter + markdown format.
51pub struct MemoryFormat;
52
53impl MemoryFormat {
54    /// Parse a memory file content into a MemoryEntry.
55    /// Returns None if frontmatter is missing, malformed, or missing required fields.
56    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    /// Serialize a MemoryEntry to a string suitable for writing to a file.
67    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}