Skip to main content

ai_agent/memory/
types.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/utils/filePersistence/types.ts
2//! Memory type taxonomy and structures.
3//!
4//! Memories are constrained to four types capturing context NOT derivable
5//! from the current project state. Code patterns, architecture, git history,
6//! and file structure are derivable and should NOT be saved as memories.
7
8use serde::{Deserialize, Serialize};
9
10/// Memory types supported by the memory system
11pub const MEMORY_TYPES: &[&str] = &["user", "feedback", "project", "reference"];
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum MemoryType {
16    User,
17    Feedback,
18    Project,
19    Reference,
20}
21
22impl MemoryType {
23    /// Parse a string into a MemoryType
24    pub fn from_str(s: &str) -> Option<Self> {
25        match s.to_lowercase().as_str() {
26            "user" => Some(Self::User),
27            "feedback" => Some(Self::Feedback),
28            "project" => Some(Self::Project),
29            "reference" => Some(Self::Reference),
30            _ => None,
31        }
32    }
33
34    /// Get the type name as string
35    pub fn as_str(&self) -> &'static str {
36        match self {
37            Self::User => "user",
38            Self::Feedback => "feedback",
39            Self::Project => "project",
40            Self::Reference => "reference",
41        }
42    }
43}
44
45impl std::fmt::Display for MemoryType {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(f, "{}", self.as_str())
48    }
49}
50
51/// A single memory entry
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Memory {
54    pub name: String,
55    pub description: String,
56    #[serde(rename = "type")]
57    pub memory_type: MemoryType,
58    pub content: String,
59}
60
61/// Frontmatter for memory files
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct MemoryFrontmatter {
64    pub name: String,
65    pub description: String,
66    #[serde(rename = "type")]
67    pub memory_type: MemoryType,
68}
69
70/// Parse frontmatter from markdown content
71pub fn parse_frontmatter(content: &str) -> Option<MemoryFrontmatter> {
72    let trimmed = content.trim();
73
74    // Check for frontmatter delimiters
75    if !trimmed.starts_with("---") {
76        return None;
77    }
78
79    // Find the closing delimiter
80    let end_idx = trimmed[3..].find("---")? + 3;
81    let frontmatter = &trimmed[3..end_idx];
82
83    let mut name = String::new();
84    let mut description = String::new();
85    let mut memory_type = MemoryType::User; // Default
86
87    for line in frontmatter.lines() {
88        let line = line.trim();
89        if let Some((key, value)) = line.split_once(':') {
90            let key = key.trim();
91            let value = value.trim();
92
93            match key {
94                "name" => name = value.to_string(),
95                "description" => description = value.to_string(),
96                "type" => {
97                    if let Some(t) = MemoryType::from_str(value) {
98                        memory_type = t;
99                    }
100                }
101                _ => {}
102            }
103        }
104    }
105
106    if name.is_empty() {
107        return None;
108    }
109
110    Some(MemoryFrontmatter {
111        name,
112        description,
113        memory_type,
114    })
115}
116
117/// Extract content after frontmatter
118pub fn extract_content(content: &str) -> String {
119    let trimmed = content.trim();
120
121    if !trimmed.starts_with("---") {
122        return trimmed.to_string();
123    }
124
125    if let Some(end_idx) = trimmed[3..].find("---") {
126        let after_frontmatter = &trimmed[3 + end_idx + 3..];
127        after_frontmatter.trim().to_string()
128    } else {
129        trimmed.to_string()
130    }
131}
132
133/// Entrypoint truncation result
134#[derive(Debug, Clone)]
135pub struct EntrypointTruncation {
136    pub content: String,
137    pub line_count: usize,
138    pub byte_count: usize,
139    pub was_line_truncated: bool,
140    pub was_byte_truncated: bool,
141}
142
143/// Maximum lines in MEMORY.md entrypoint
144pub const MAX_ENTRYPOINT_LINES: usize = 200;
145/// Maximum bytes in MEMORY.md entrypoint (~125 chars/line * 200 lines)
146pub const MAX_ENTRYPOINT_BYTES: usize = 25_000;
147
148/// Truncate MEMORY.md content to line and byte caps
149pub fn truncate_entrypoint(raw: &str) -> EntrypointTruncation {
150    let trimmed = raw.trim();
151    let content_lines: Vec<&str> = trimmed.lines().collect();
152    let line_count = content_lines.len();
153    let byte_count = trimmed.len();
154
155    let was_line_truncated = line_count > MAX_ENTRYPOINT_LINES;
156    let was_byte_truncated = byte_count > MAX_ENTRYPOINT_BYTES;
157
158    if !was_line_truncated && !byte_count <= MAX_ENTRYPOINT_BYTES {
159        return EntrypointTruncation {
160            content: trimmed.to_string(),
161            line_count,
162            byte_count,
163            was_line_truncated: false,
164            was_byte_truncated: false,
165        };
166    }
167
168    let truncated = if was_line_truncated {
169        content_lines[..MAX_ENTRYPOINT_LINES].join("\n")
170    } else {
171        trimmed.to_string()
172    };
173
174    let truncated = if truncated.len() > MAX_ENTRYPOINT_BYTES {
175        if let Some(cut_at) = truncated.rfind('\n') {
176            if cut_at > MAX_ENTRYPOINT_BYTES {
177                truncated[..cut_at].to_string()
178            } else {
179                truncated[..MAX_ENTRYPOINT_BYTES].to_string()
180            }
181        } else {
182            truncated[..MAX_ENTRYPOINT_BYTES].to_string()
183        }
184    } else {
185        truncated
186    };
187
188    let reason = if was_byte_truncated && !was_line_truncated {
189        format!("{} (limit: {} bytes)", byte_count, MAX_ENTRYPOINT_BYTES)
190    } else if was_line_truncated && !was_byte_truncated {
191        format!("{} lines (limit: {})", line_count, MAX_ENTRYPOINT_LINES)
192    } else {
193        format!("{} lines and {} bytes", line_count, byte_count)
194    };
195
196    let content = format!(
197        "{}\n\n> WARNING: MEMORY.md is {}. Only part of it was loaded. Keep index entries to one line under ~200 chars; move detail into topic files.",
198        truncated, reason
199    );
200
201    EntrypointTruncation {
202        content,
203        line_count,
204        byte_count,
205        was_line_truncated,
206        was_byte_truncated,
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_memory_type_from_str() {
216        assert_eq!(MemoryType::from_str("user"), Some(MemoryType::User));
217        assert_eq!(MemoryType::from_str("feedback"), Some(MemoryType::Feedback));
218        assert_eq!(MemoryType::from_str("project"), Some(MemoryType::Project));
219        assert_eq!(
220            MemoryType::from_str("reference"),
221            Some(MemoryType::Reference)
222        );
223        assert_eq!(MemoryType::from_str("unknown"), None);
224    }
225
226    #[test]
227    fn test_parse_frontmatter() {
228        let content = r#"---
229name: test_memory
230description: A test memory
231type: user
232---
233
234This is the content."#;
235
236        let fm = parse_frontmatter(content).unwrap();
237        assert_eq!(fm.name, "test_memory");
238        assert_eq!(fm.description, "A test memory");
239        assert_eq!(fm.memory_type, MemoryType::User);
240    }
241
242    #[test]
243    fn test_extract_content() {
244        let content = r#"---
245name: test
246description: test
247type: user
248---
249
250This is the actual content."#;
251
252        let extracted = extract_content(content);
253        assert_eq!(extracted, "This is the actual content.");
254    }
255}