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