Skip to main content

recall_echo/
lib.rs

1//! recall-echo — Persistent memory system with knowledge graph.
2//!
3//! A general-purpose persistent memory system for any LLM tool — Claude Code,
4//! Ollama, or any provider. Features a four-layer memory architecture with
5//! a knowledge graph (SurrealDB + fastembed) as Layer 0.
6//!
7//! # Architecture
8//!
9//! ```text
10//! Input adapters (JSONL transcripts, pulse-null Messages)
11//!     → Conversation (universal internal format)
12//!     → Archive pipeline (markdown + index + ephemeral + graph)
13//! ```
14//!
15//! # Features
16//!
17//! - `pulse-null` — Plugin integration for pulse-null entities
18//! - `llm` — HTTP-based LLM provider for entity extraction
19
20pub mod archive;
21pub mod checkpoint;
22pub mod config;
23pub mod config_cli;
24pub mod consume;
25pub mod conversation;
26pub mod dashboard;
27pub mod distill;
28pub mod ephemeral;
29pub mod error;
30pub mod frontmatter;
31pub mod init;
32pub mod jsonl;
33pub mod paths;
34pub mod search;
35pub mod status;
36pub mod summarize;
37pub mod tags;
38
39pub mod graph;
40pub mod graph_bridge;
41pub mod graph_cli;
42#[cfg(feature = "llm")]
43pub mod llm_provider;
44
45#[cfg(feature = "pulse-null")]
46pub mod pulse_null;
47
48#[cfg(feature = "bench")]
49pub mod bench;
50
51use std::fs;
52use std::path::{Path, PathBuf};
53
54pub use archive::SessionMetadata;
55pub use summarize::ConversationSummary;
56
57/// The recall-echo memory system.
58///
59/// All paths are derived from entity_root:
60/// ```text
61/// {entity_root}/memory/
62/// ├── MEMORY.md
63/// ├── EPHEMERAL.md
64/// ├── ARCHIVE.md
65/// ├── conversations/
66/// └── graph/ (knowledge graph store)
67/// ```
68pub struct RecallEcho {
69    entity_root: PathBuf,
70}
71
72impl RecallEcho {
73    /// Create a new RecallEcho instance with a specific entity root directory.
74    #[must_use]
75    pub fn new(entity_root: PathBuf) -> Self {
76        Self { entity_root }
77    }
78
79    /// Create a RecallEcho using the default path resolution
80    /// (RECALL_ECHO_HOME env var or current working directory).
81    pub fn from_default() -> Result<Self, error::RecallError> {
82        Ok(Self::new(paths::entity_root()?))
83    }
84
85    /// Entity root directory.
86    #[must_use]
87    pub fn entity_root(&self) -> &Path {
88        &self.entity_root
89    }
90
91    /// Memory directory: {entity_root}/memory/
92    #[must_use]
93    pub fn memory_dir(&self) -> PathBuf {
94        self.entity_root.join("memory")
95    }
96
97    /// Path to MEMORY.md.
98    #[must_use]
99    pub fn memory_file(&self) -> PathBuf {
100        self.memory_dir().join("MEMORY.md")
101    }
102
103    /// Path to EPHEMERAL.md.
104    #[must_use]
105    pub fn ephemeral_file(&self) -> PathBuf {
106        self.memory_dir().join("EPHEMERAL.md")
107    }
108
109    /// Path to conversations directory.
110    #[must_use]
111    pub fn conversations_dir(&self) -> PathBuf {
112        self.memory_dir().join("conversations")
113    }
114
115    /// Path to ARCHIVE.md index.
116    #[must_use]
117    pub fn archive_index(&self) -> PathBuf {
118        self.memory_dir().join("ARCHIVE.md")
119    }
120
121    // ── Core operations ──────────────────────────────────────────────
122
123    /// Read EPHEMERAL.md content without clearing it.
124    /// Returns None if the file doesn't exist or is empty.
125    pub fn consume_content(&self) -> Result<Option<String>, error::RecallError> {
126        consume::consume(&self.ephemeral_file())
127    }
128
129    /// Check if the memory system has been initialized.
130    #[must_use]
131    pub fn is_initialized(&self) -> bool {
132        self.memory_dir().exists() && self.conversations_dir().exists()
133    }
134
135    /// Number of lines in MEMORY.md.
136    #[must_use]
137    pub fn memory_line_count(&self) -> usize {
138        let path = self.memory_file();
139        if !path.exists() {
140            return 0;
141        }
142        fs::read_to_string(&path)
143            .unwrap_or_default()
144            .lines()
145            .count()
146    }
147}
148
149// ---------------------------------------------------------------------------
150// Pulse-null plugin implementation — behind feature flag
151// ---------------------------------------------------------------------------
152
153#[cfg(feature = "pulse-null")]
154mod plugin_impl {
155    use super::*;
156    use std::any::Any;
157    use std::future::Future;
158    use std::pin::Pin;
159
160    use pulse_system_types::plugin::{Plugin, PluginContext, PluginResult, PluginRole};
161    use pulse_system_types::{HealthStatus, PluginMeta, SetupPrompt};
162
163    impl RecallEcho {
164        fn health_check(&self) -> HealthStatus {
165            if !self.memory_dir().exists() {
166                return HealthStatus::Down("memory directory not found".into());
167            }
168            if !self.memory_file().exists() {
169                return HealthStatus::Degraded("MEMORY.md not found".into());
170            }
171            if !self.conversations_dir().exists() {
172                return HealthStatus::Degraded("conversations directory not found".into());
173            }
174            HealthStatus::Healthy
175        }
176
177        fn get_setup_prompts() -> Vec<SetupPrompt> {
178            vec![SetupPrompt {
179                key: "entity_root".into(),
180                question: "Entity root directory:".into(),
181                required: true,
182                secret: false,
183                default: None,
184            }]
185        }
186    }
187
188    /// Factory function — creates a fully initialized recall-echo plugin.
189    pub async fn create(
190        config: &serde_json::Value,
191        ctx: &PluginContext,
192    ) -> Result<Box<dyn Plugin>, Box<dyn std::error::Error + Send + Sync>> {
193        let entity_root = config
194            .get("entity_root")
195            .and_then(|v| v.as_str())
196            .map(PathBuf::from)
197            .unwrap_or_else(|| ctx.entity_root.clone());
198
199        Ok(Box::new(RecallEcho::new(entity_root)))
200    }
201
202    impl Plugin for RecallEcho {
203        fn meta(&self) -> PluginMeta {
204            PluginMeta {
205                name: "recall-echo".into(),
206                version: env!("CARGO_PKG_VERSION").into(),
207                description: "Persistent memory system with knowledge graph".into(),
208            }
209        }
210
211        fn role(&self) -> PluginRole {
212            PluginRole::Memory
213        }
214
215        fn start(&mut self) -> PluginResult<'_> {
216            Box::pin(async { Ok(()) })
217        }
218
219        fn stop(&mut self) -> PluginResult<'_> {
220            Box::pin(async { Ok(()) })
221        }
222
223        fn health(&self) -> Pin<Box<dyn Future<Output = HealthStatus> + Send + '_>> {
224            Box::pin(async move { self.health_check() })
225        }
226
227        fn setup_prompts(&self) -> Vec<SetupPrompt> {
228            Self::get_setup_prompts()
229        }
230
231        fn as_any(&self) -> &dyn Any {
232            self
233        }
234    }
235}
236
237#[cfg(feature = "pulse-null")]
238pub use plugin_impl::create;