use serde::{Deserialize, Serialize};
pub const MEMORY_TYPES: &[&str] = &["user", "feedback", "project", "reference"];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MemoryType {
User,
Feedback,
Project,
Reference,
}
impl MemoryType {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"user" => Some(Self::User),
"feedback" => Some(Self::Feedback),
"project" => Some(Self::Project),
"reference" => Some(Self::Reference),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::User => "user",
Self::Feedback => "feedback",
Self::Project => "project",
Self::Reference => "reference",
}
}
}
impl std::fmt::Display for MemoryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
pub name: String,
pub description: String,
#[serde(rename = "type")]
pub memory_type: MemoryType,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryFrontmatter {
pub name: String,
pub description: String,
#[serde(rename = "type")]
pub memory_type: MemoryType,
}
pub fn parse_frontmatter(content: &str) -> Option<MemoryFrontmatter> {
let trimmed = content.trim();
if !trimmed.starts_with("---") {
return None;
}
let end_idx = trimmed[3..].find("---")? + 3;
let frontmatter = &trimmed[3..end_idx];
let mut name = String::new();
let mut description = String::new();
let mut memory_type = MemoryType::User;
for line in frontmatter.lines() {
let line = line.trim();
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
match key {
"name" => name = value.to_string(),
"description" => description = value.to_string(),
"type" => {
if let Some(t) = MemoryType::from_str(value) {
memory_type = t;
}
}
_ => {}
}
}
}
if name.is_empty() {
return None;
}
Some(MemoryFrontmatter {
name,
description,
memory_type,
})
}
pub fn extract_content(content: &str) -> String {
let trimmed = content.trim();
if !trimmed.starts_with("---") {
return trimmed.to_string();
}
if let Some(end_idx) = trimmed[3..].find("---") {
let after_frontmatter = &trimmed[3 + end_idx + 3..];
after_frontmatter.trim().to_string()
} else {
trimmed.to_string()
}
}
#[derive(Debug, Clone)]
pub struct EntrypointTruncation {
pub content: String,
pub line_count: usize,
pub byte_count: usize,
pub was_line_truncated: bool,
pub was_byte_truncated: bool,
}
pub const MAX_ENTRYPOINT_LINES: usize = 200;
pub const MAX_ENTRYPOINT_BYTES: usize = 25_000;
pub fn truncate_entrypoint(raw: &str) -> EntrypointTruncation {
let trimmed = raw.trim();
let content_lines: Vec<&str> = trimmed.lines().collect();
let line_count = content_lines.len();
let byte_count = trimmed.len();
let was_line_truncated = line_count > MAX_ENTRYPOINT_LINES;
let was_byte_truncated = byte_count > MAX_ENTRYPOINT_BYTES;
if !was_line_truncated && !byte_count <= MAX_ENTRYPOINT_BYTES {
return EntrypointTruncation {
content: trimmed.to_string(),
line_count,
byte_count,
was_line_truncated: false,
was_byte_truncated: false,
};
}
let truncated = if was_line_truncated {
content_lines[..MAX_ENTRYPOINT_LINES].join("\n")
} else {
trimmed.to_string()
};
let truncated = if truncated.len() > MAX_ENTRYPOINT_BYTES {
if let Some(cut_at) = truncated.rfind('\n') {
if cut_at > MAX_ENTRYPOINT_BYTES {
truncated[..cut_at].to_string()
} else {
truncated[..MAX_ENTRYPOINT_BYTES].to_string()
}
} else {
truncated[..MAX_ENTRYPOINT_BYTES].to_string()
}
} else {
truncated
};
let reason = if was_byte_truncated && !was_line_truncated {
format!("{} (limit: {} bytes)", byte_count, MAX_ENTRYPOINT_BYTES)
} else if was_line_truncated && !was_byte_truncated {
format!("{} lines (limit: {})", line_count, MAX_ENTRYPOINT_LINES)
} else {
format!("{} lines and {} bytes", line_count, byte_count)
};
let content = format!(
"{}\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.",
truncated, reason
);
EntrypointTruncation {
content,
line_count,
byte_count,
was_line_truncated,
was_byte_truncated,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_type_from_str() {
assert_eq!(MemoryType::from_str("user"), Some(MemoryType::User));
assert_eq!(MemoryType::from_str("feedback"), Some(MemoryType::Feedback));
assert_eq!(MemoryType::from_str("project"), Some(MemoryType::Project));
assert_eq!(MemoryType::from_str("reference"), Some(MemoryType::Reference));
assert_eq!(MemoryType::from_str("unknown"), None);
}
#[test]
fn test_parse_frontmatter() {
let content = r#"---
name: test_memory
description: A test memory
type: user
---
This is the content."#;
let fm = parse_frontmatter(content).unwrap();
assert_eq!(fm.name, "test_memory");
assert_eq!(fm.description, "A test memory");
assert_eq!(fm.memory_type, MemoryType::User);
}
#[test]
fn test_extract_content() {
let content = r#"---
name: test
description: test
type: user
---
This is the actual content."#;
let extracted = extract_content(content);
assert_eq!(extracted, "This is the actual content.");
}
}