79 lines
2.5 KiB
Rust
79 lines
2.5 KiB
Rust
//! Pure Memory entity — long-term agent memory with frontmatter metadata
|
|||
|
|
//! and free-form markdown content.
|
||
|
|
//!
|
||
|
|
//! # Architecture
|
||
|
|
//! This is a pure data structure with **no I/O logic**. Load/save
|
||
|
|
//! responsibilities live in [`MemoryRepository`](super::repository::MemoryRepository).
|
||
|
|
|
||
|
|
use std::path::{Path, PathBuf};
|
||
|
|
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
|
||
|
|
/// A single memory entry (lesson, reference, etc.) with frontmatter
|
||
|
|
/// metadata and free-form markdown content.
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct Memory {
|
||
|
|
pub name: String,
|
||
|
|
pub description: String,
|
||
|
|
pub content: String,
|
||
|
|
pub kind: String,
|
||
|
|
pub created_at: i64,
|
||
|
|
pub updated_at: i64,
|
||
|
|
pub outcome: Option<String>,
|
||
|
|
pub lifecycle: String,
|
||
|
|
pub scope: Option<String>,
|
||
|
|
pub before_snippet: Option<String>,
|
||
|
|
pub after_snippet: Option<String>,
|
||
|
|
pub provenances: Vec<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Memory {
|
||
|
|
/// Convert an arbitrary string into a filesystem-safe slug.
|
||
|
|
///
|
||
|
|
/// Flow: lowercase → replace non-alphanumeric chars with `-` →
|
||
|
|
/// collapse/trim repeated `-`.
|
||
|
|
///
|
||
|
|
/// Returns `None` if the result is empty or exceeds 80 characters.
|
||
|
|
pub fn slugify(s: &str) -> Option<String> {
|
||
|
|
let slug: String = s
|
||
|
|
.to_lowercase()
|
||
|
|
.chars()
|
||
|
|
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
|
||
|
|
.collect();
|
||
|
|
let slug: String = slug
|
||
|
|
.split('-')
|
||
|
|
.filter(|s| !s.is_empty())
|
||
|
|
.collect::<Vec<_>>()
|
||
|
|
.join("-");
|
||
|
|
if slug.is_empty() || slug.len() > 80 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
Some(slug)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Compute the on-disk path for a memory of the given name.
|
||
|
|
///
|
||
|
|
/// This is a **pure** computation: it takes `memory_dir` as a parameter
|
||
|
|
/// and performs no I/O itself.
|
||
|
|
///
|
||
|
|
/// Falls back to `"memory.md"` when `name` slugifies to nothing.
|
||
|
|
pub fn path(memory_dir: &Path, name: &str) -> PathBuf {
|
||
|
|
let slug = Self::slugify(name).unwrap_or_else(|| "memory".to_string());
|
||
|
|
let clean: String = format!("{slug}.md")
|
||
|
|
.chars()
|
||
|
|
.map(|c| {
|
||
|
|
if c.is_ascii_alphanumeric() || c == '.' || c == '-' {
|
||
|
|
c
|
||
|
|
} else {
|
||
|
|
'-'
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
let clean = clean.trim_start_matches('.').to_string();
|
||
|
|
memory_dir.join(if clean.is_empty() {
|
||
|
|
"memory.md".to_string()
|
||
|
|
} else {
|
||
|
|
clean
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|