Skip to main content

echo_state/memory/
mod.rs

1//! Memory system
2//!
3//! Layered architecture, each with distinct responsibilities:
4//!
5//! | Layer | Implementation | Scope |
6//! |------|------|--------|
7//! | Short-term context | [`compression::ContextManager`] | Within a single `execute()` call |
8//! | Thread state | [`Checkpointer`] / [`FileCheckpointer`] | Cross-process recovery of the same thread |
9//! | Conversation history | [`ConversationStore`] / `SqliteConversationStore` | Transcript projection, history browsing, multi-user isolation |
10//! | Long-term memory | [`Store`] / [`FileStore`] / `SqliteStore` | Cross-session, cross-user sharing |
11//!
12//! ## Thread state persistence (Checkpointer)
13//!
14//! ```rust,no_run
15//! use echo_core::error::Result;
16//! use echo_state::memory::checkpointer::FileCheckpointer;
17//! use std::sync::Arc;
18//!
19//! # async fn example() -> Result<()> {
20//! let cp = Arc::new(FileCheckpointer::new("~/.echo-agent/checkpoints.json")?);
21//! // Wire `cp` into your own agent/runtime layer, or use it through the `echo_agent` façade.
22//! let _ = cp;
23//! # Ok(())
24//! # }
25//! ```
26//!
27//! ## Conversation persistence (ConversationStore)
28//!
29//! ```rust,no_run
30//! use echo_core::error::Result;
31//! use echo_state::memory::conversation::{ConversationStore, NewConversation};
32//! # async fn example(store: &dyn ConversationStore) -> Result<()> {
33//! let conv = store.create_conversation(NewConversation {
34//!     conversation_id: "conv-001".to_string(),
35//!     user_id: "default".to_string(),
36//!     agent_type: None,
37//!     title: Some("Rust discussion".to_string()),
38//! }).await?;
39//! store.save_messages("conv-001", &[/* messages */]).await?;
40//! let msgs = store.get_messages("conv-001").await?;
41//! # Ok(())
42//! # }
43//! ```
44//!
45//! ## Long-term KV storage (Store)
46//!
47//! ```rust,no_run
48//! use echo_core::error::Result;
49//! use echo_state::memory::store::{FileStore, Store};
50//! use std::sync::Arc;
51//!
52//! # async fn example() -> Result<()> {
53//! let store = Arc::new(FileStore::new("~/.echo-agent/store.json")?);
54//! store.put(&["alice", "memories"], "pref-001", serde_json::json!({
55//!     "content": "User prefers dark theme",
56//!     "importance": 8
57//! })).await?;
58//! let items = store.search(&["alice", "memories"], "theme", 3).await?;
59//! # Ok(())
60//! # }
61//! ```
62
63pub mod checkpointer;
64pub mod conversation;
65pub mod embedder;
66pub mod embedding_store;
67pub mod snapshot;
68#[cfg(feature = "sqlite")]
69pub mod sqlite_conversation;
70#[cfg(feature = "sqlite")]
71pub mod sqlite_store;
72pub mod store;
73
74pub use checkpointer::Checkpointer as ThreadStore;
75pub use checkpointer::{
76    Checkpoint, Checkpointer, FileCheckpointer, InMemoryCheckpointer, ThreadState,
77};
78pub use conversation::{
79    Conversation, ConversationFilter, ConversationMeta, ConversationStore, NewConversation,
80    StoredMessage, project_message, project_messages,
81};
82pub use embedder::{Embedder, HttpEmbedder};
83pub use embedding_store::EmbeddingStore;
84pub use snapshot::{SnapshotManager, SnapshotPolicy, StateSnapshot};
85#[cfg(feature = "sqlite")]
86pub use sqlite_conversation::SqliteConversationStore;
87#[cfg(feature = "sqlite")]
88pub use sqlite_store::SqliteStore;
89pub use store::{FileStore, InMemoryStore, SearchMode, SearchQuery, Store, StoreItem};
90#[cfg(test)]
91pub use test_utils::MockEmbedder;
92
93/// Test embedder (visible only in tests)
94#[cfg(test)]
95mod test_utils {
96    use crate::memory::embedder::Embedder;
97    use echo_core::error::Result;
98    use futures::future::BoxFuture;
99
100    pub struct MockEmbedder {
101        dimension: usize,
102    }
103
104    impl MockEmbedder {
105        pub fn new(dimension: usize) -> Self {
106            assert!(dimension > 0);
107            Self { dimension }
108        }
109    }
110
111    impl Embedder for MockEmbedder {
112        fn embed<'a>(&'a self, text: &'a str) -> BoxFuture<'a, Result<Vec<f32>>> {
113            Box::pin(async move {
114                let mut vec = vec![0.0f32; self.dimension];
115                for (i, b) in text.bytes().enumerate() {
116                    vec[i % self.dimension] += b as f32;
117                }
118                let norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
119                if norm > 0.0 {
120                    for v in &mut vec {
121                        *v /= norm;
122                    }
123                }
124                Ok(vec)
125            })
126        }
127    }
128}