feat(tui): implement status bar with connection and turn state indicators feat(tui): create workflow panel for agent status and progress visualization feat(web): introduce web frontend interface with static file serving feat(ws): add WebSocket interface for real-time communication and session management
110 lines
3.9 KiB
Rust
110 lines
3.9 KiB
Rust
//! Pure domain entity for long-term agent memory.
|
|
//!
|
|
//! A `Memory` entry stores a named, kinded piece of information (lesson,
|
|
//! reference, fact) with frontmatter metadata and free-form markdown
|
|
//! content. Memories are persisted as individual `.md` files with YAML
|
|
//! frontmatter.
|
|
//!
|
|
//! # Architecture
|
|
//! This is a pure data structure with **no I/O logic**. Load/save
|
|
//! responsibilities live in `MemoryRepository` (domain::repository).
|
|
//!
|
|
//! ## Utility Functions
|
|
//! - `slugify()` — converts a name string into a filesystem-safe slug
|
|
//! - `path()` — computes the on-disk path for a given memory name
|
|
//!
|
|
//! Both are pure computations that take parameters and perform no I/O.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// A single memory entry with frontmatter metadata and markdown content.
|
|
///
|
|
/// ## Fields
|
|
/// - `name` — unique identifier / title for this memory
|
|
/// - `description` — short summary of what this memory contains
|
|
/// - `content` — free-form markdown body
|
|
/// - `kind` — category/tag (e.g. "lesson", "reference", "fact")
|
|
/// - `created_at` — Unix timestamp of creation
|
|
/// - `updated_at` — Unix timestamp of last modification
|
|
/// - `outcome` — optional outcome of applying this memory
|
|
/// - `lifecycle` — lifecycle stage (e.g. "active", "archived")
|
|
/// - `scope` — optional scope qualifier (which session/context this applies to)
|
|
/// - `before_snippet` — optional context snapshot before memory was applied
|
|
/// - `after_snippet` — optional context snapshot after memory was applied
|
|
/// - `provenances` — list of origin identifiers that created or confirmed this memory
|
|
#[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> {
|
|
// Phase 1: replace every non-alphanumeric character with '-'
|
|
let slug: String = s
|
|
.to_lowercase()
|
|
.chars()
|
|
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
|
|
.collect();
|
|
// Phase 2: collapse consecutive '-' separators
|
|
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.
|
|
///
|
|
/// ## Parameters
|
|
/// - `memory_dir` — the base directory for memory storage
|
|
/// - `name` — the memory name (will be slugified internally)
|
|
///
|
|
/// Falls back to `"memory.md"` when the name slugifies to an empty
|
|
/// or invalid string.
|
|
///
|
|
/// ## Pure Computation
|
|
/// This function performs **no I/O** — it only computes a path.
|
|
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
|
|
})
|
|
}
|
|
}
|