Skip to main content

rustyclaw_core/mnemo/
mod.rs

1//! Native memory coprocessor for persistent recall across sessions.
2//!
3//! Inspired by Mnemo Cortex, this module provides:
4//! - SQLite + FTS5 storage for messages and summaries
5//! - Background compaction via LLM summarization
6//! - Context injection at agent bootstrap
7//! - DAG-based summary lineage for expansion
8//!
9//! ## Trait Architecture
10//!
11//! Follows existing RustyClaw patterns:
12//! - `MemoryStore` — storage abstraction (SQLite, in-memory, remote)
13//! - `Summarizer` — summarization backend (LLM, deterministic)
14
15mod compaction;
16mod config;
17mod schema;
18mod sqlite_store;
19mod summarizer;
20mod traits;
21#[cfg(test)]
22mod tests;
23
24pub use compaction::run_compaction;
25pub use config::{MnemoConfig, SummarizationConfig};
26pub use sqlite_store::SqliteMemoryStore;
27pub use summarizer::{DeterministicSummarizer, LlmSummarizer};
28pub use traits::{CompactionStats, MemoryEntry, MemoryHit, MemoryStore, SummaryKind, Summarizer};
29
30use anyhow::Result;
31use std::path::Path;
32use std::sync::Arc;
33
34/// Shared mnemo store for concurrent access.
35pub type SharedMemoryStore = Arc<dyn MemoryStore>;
36
37/// Create a shared memory store from configuration.
38pub async fn create_memory_store(
39    config: &MnemoConfig,
40    settings_dir: &Path,
41) -> Result<SharedMemoryStore> {
42    let db_path = config.db_path.clone().unwrap_or_else(|| {
43        settings_dir.join("mnemo.sqlite3")
44    });
45    
46    let store = SqliteMemoryStore::open(&db_path, config.clone()).await?;
47    Ok(Arc::new(store))
48}
49
50/// Create a summarizer based on configuration.
51pub fn create_summarizer(config: &MnemoConfig) -> Arc<dyn Summarizer> {
52    if config.summarization.use_main_model {
53        // Will be wired to main provider later
54        // For now, fall back to deterministic
55        Arc::new(DeterministicSummarizer::new(
56            config.summarization.truncate_chars,
57            config.summarization.truncate_total,
58        ))
59    } else if let (Some(base_url), Some(model)) = (
60        config.summarization.provider.as_ref(),
61        config.summarization.model.as_ref(),
62    ) {
63        Arc::new(LlmSummarizer::new(
64            base_url.clone(),
65            model.clone(),
66            None, // API key resolved elsewhere
67        ))
68    } else {
69        Arc::new(DeterministicSummarizer::new(
70            config.summarization.truncate_chars,
71            config.summarization.truncate_total,
72        ))
73    }
74}
75
76/// Estimate token count for a string (rough heuristic: ~4 chars per token).
77pub fn estimate_tokens(text: &str) -> usize {
78    (text.len() as f64 / 4.0).ceil() as usize
79}
80
81/// Generate context markdown for bootstrap injection.
82pub fn generate_context_md(entries: &[MemoryEntry]) -> String {
83    let mut lines = Vec::new();
84    lines.push("# MNEMO CONTEXT".to_string());
85    lines.push(String::new());
86    
87    for entry in entries {
88        if entry.depth == 0 {
89            lines.push(format!("## {} (msg #{})", entry.role, entry.id));
90        } else {
91            lines.push(format!("## Summary d{} #{}", entry.depth, entry.id));
92        }
93        lines.push(entry.content.clone());
94        lines.push(String::new());
95    }
96    
97    lines.join("\n")
98}