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}