xz_memory_engine/layered/traits.rs
1//! Layered memory trait definitions.
2//!
3//! This module defines the five trait contracts that make up the **layered
4//! memory architecture**: [`ConversationMemory`], [`UserProfileMemory`],
5//! [`ProjectMemory`], [`FactMemory`], and [`SummaryMemory`].
6//!
7//! Each trait represents a distinct *memory layer* with its own access
8//! patterns, retention policies, and lifecycle — from short-lived
9//! conversational windows to durable, searchable facts.
10//!
11//! # Design principles
12//!
13//! - **Separation of concerns**: each layer has a single responsibility and
14//! can be backed by a different storage engine.
15//! - **Async-first**: every operation is `async` so backends can use
16//! remote stores, vector databases, or batched I/O without blocking.
17//! - **Swappable backends**: implementing a trait is the only contract;
18//! the layered engine composes them without coupling to any concrete type.
19//! - **`Send + Sync`**: all traits require `Send + Sync` so they can be
20//! shared across `tokio` tasks safely.
21
22use async_trait::async_trait;
23use std::collections::HashMap;
24use xz_memory_core::StoreError;
25
26// ---------------------------------------------------------------------------
27// ConversationMemory
28// ---------------------------------------------------------------------------
29
30/// Short-term conversational memory.
31///
32/// Models the sliding window of a single conversation session — messages are
33/// appended in chronological order, the most recent `n` can be retrieved,
34/// and the oldest messages can be evicted to bound memory usage.
35///
36/// # Typical backends
37///
38/// - In-memory ring buffer (fast, bounded).
39/// - Redis list with `LPUSH` / `LRANGE` / `LTRIM`.
40/// - SQLite table ordered by `recorded_at`.
41///
42/// # Examples
43///
44/// ```ignore
45/// use xz_memory_engine::layered::traits::ConversationMemory;
46/// use xz_memory_core::StoreError;
47///
48/// async fn example(store: &dyn ConversationMemory) -> Result<(), StoreError> {
49/// store.append("sess-1", "Hello, how can I help?").await?;
50/// store.append("sess-1", "What is the weather?").await?;
51/// let recent = store.recent("sess-1", 2).await?;
52/// assert_eq!(recent.len(), 2);
53/// let evicted = store.evict("sess-1", 1).await?;
54/// assert_eq!(evicted, 1);
55/// Ok(())
56/// }
57/// ```
58#[async_trait]
59pub trait ConversationMemory: Send + Sync {
60 /// Append a message to the session.
61 ///
62 /// Messages are stored in insertion order; the most recent message
63 /// is last in retrieval order.
64 async fn append(&self, session_id: &str, message: &str) -> Result<(), StoreError>;
65
66 /// Return the most recent `n` messages, newest last.
67 ///
68 /// If the session has fewer than `n` messages the returned vector
69 /// will be shorter. Use `n = 0` to retrieve all messages (behaviour
70 /// is backend-defined — some may return an empty vector).
71 async fn recent(&self, session_id: &str, n: usize) -> Result<Vec<String>, StoreError>;
72
73 /// Evict the oldest messages, keeping only the `keep` most recent.
74 ///
75 /// Returns the number of messages actually evicted.
76 async fn evict(&self, session_id: &str, keep: usize) -> Result<usize, StoreError>;
77}
78
79// ---------------------------------------------------------------------------
80// UserProfileMemory
81// ---------------------------------------------------------------------------
82
83/// Per-user profile / preference memory.
84///
85/// Stores key-value preference pairs keyed by `(user_id, key)`. This layer
86/// is designed for stable, low-volume data that persists across sessions:
87/// language preferences, notification settings, display name, etc.
88///
89/// # Typical backends
90///
91/// - SQLite `user_prefs` table.
92/// - Redis hash per user (`HSET` / `HGETALL` / `HDEL`).
93/// - Embedded key-value store (e.g. RocksDB, Sled).
94///
95/// # Examples
96///
97/// ```ignore
98/// use xz_memory_engine::layered::traits::UserProfileMemory;
99/// use xz_memory_core::StoreError;
100///
101/// async fn example(store: &dyn UserProfileMemory) -> Result<(), StoreError> {
102/// store.set_preference("user-42", "language", "zh-CN").await?;
103/// store.set_preference("user-42", "theme", "dark").await?;
104/// let prefs = store.get_preferences("user-42").await?;
105/// assert!(prefs.contains_key("language"));
106/// store.remove_preference("user-42", "theme").await?;
107/// Ok(())
108/// }
109/// ```
110#[async_trait]
111pub trait UserProfileMemory: Send + Sync {
112 /// Retrieve all preferences for a user as a flat key-value map.
113 async fn get_preferences(&self, user_id: &str) -> Result<HashMap<String, String>, StoreError>;
114
115 /// Set or update a single preference key.
116 ///
117 /// If the key already exists its value is overwritten.
118 async fn set_preference(&self, user_id: &str, key: &str, value: &str)
119 -> Result<(), StoreError>;
120
121 /// Remove a single preference key.
122 ///
123 /// Removing a non-existent key should succeed silently (no error).
124 async fn remove_preference(&self, user_id: &str, key: &str) -> Result<(), StoreError>;
125}
126
127// ---------------------------------------------------------------------------
128// ProjectMemory
129// ---------------------------------------------------------------------------
130
131/// Project-scoped key-value memory with fuzzy search.
132///
133/// Unlike [`UserProfileMemory`], this layer is designed for larger,
134/// project-associated datasets that benefit from content-based retrieval.
135/// Values are opaque strings and the `search` method enables approximate
136/// matching (e.g. for notes, code snippets, or documentation fragments).
137///
138/// # Typical backends
139///
140/// - Full-text search index (Tantivy, Meilisearch) backed by a document store.
141/// - Vector database (Qdrant, Milvus) with semantic search via embedding.
142/// - SQLite with FTS5 extension.
143///
144/// # Examples
145///
146/// ```ignore
147/// use xz_memory_engine::layered::traits::ProjectMemory;
148/// use xz_memory_core::StoreError;
149///
150/// async fn example(store: &dyn ProjectMemory) -> Result<(), StoreError> {
151/// store.put("proj-1", "note-1", "Remember to update deps").await?;
152/// let val = store.get("proj-1", "note-1").await?;
153/// assert_eq!(val, Some("Remember to update deps".into()));
154/// let results = store.search("proj-1", "update deps").await?;
155/// assert!(!results.is_empty());
156/// Ok(())
157/// }
158/// ```
159#[async_trait]
160pub trait ProjectMemory: Send + Sync {
161 /// Store a key-value pair in the project namespace.
162 async fn put(&self, project_id: &str, key: &str, value: &str) -> Result<(), StoreError>;
163
164 /// Retrieve a value by key.
165 ///
166 /// Returns `None` if the key does not exist.
167 async fn get(&self, project_id: &str, key: &str) -> Result<Option<String>, StoreError>;
168
169 /// List all keys in the project namespace.
170 async fn keys(&self, project_id: &str) -> Result<Vec<String>, StoreError>;
171
172 /// Search for entries matching the query string.
173 ///
174 /// Returns a vector of `(key, relevance_score)` pairs sorted by
175 /// descending relevance. The score is a float typically in `[0.0, 1.0]`
176 /// but the range is backend-defined.
177 async fn search(&self, project_id: &str, query: &str)
178 -> Result<Vec<(String, f32)>, StoreError>;
179}
180
181// ---------------------------------------------------------------------------
182// FactMemory
183// ---------------------------------------------------------------------------
184
185/// Semantic / tagged fact memory.
186///
187/// Stores discrete facts with optional tags and supports fuzzy retrieval
188/// by semantic similarity or keyword match. Designed for knowledge that
189/// is ingested, queried, and occasionally pruned — the agent's long-term
190/// general knowledge.
191///
192/// # Typical backends
193///
194/// - Vector database with embedding-based retrieval.
195/// - Hybrid index combining BM25 keyword search and dense embeddings.
196/// - Graph database with tag-based traversal.
197///
198/// # Examples
199///
200/// ```ignore
201/// use xz_memory_engine::layered::traits::FactMemory;
202/// use xz_memory_core::StoreError;
203///
204/// async fn example(store: &dyn FactMemory) -> Result<(), StoreError> {
205/// let id = store.remember("Tokyo is the capital of Japan", &["geography".into()]).await?;
206/// let results = store.recall("capital of Japan", 5).await?;
207/// assert!(!results.is_empty());
208/// store.forget(&id).await?;
209/// Ok(())
210/// }
211/// ```
212#[async_trait]
213pub trait FactMemory: Send + Sync {
214 /// Store a fact with optional tags and return a unique identifier.
215 ///
216 /// The returned `String` is the backend-assigned ID that can be used
217 /// to later [`forget`](FactMemory::forget) the fact.
218 async fn remember(&self, fact: &str, tags: &[String]) -> Result<String, StoreError>;
219
220 /// Recall facts matching the query.
221 ///
222 /// Returns a vector of `(fact_text, relevance_score, tags)` tuples
223 /// sorted by descending relevance. `limit` caps the number of results.
224 async fn recall(
225 &self,
226 query: &str,
227 limit: usize,
228 ) -> Result<Vec<(String, f32, Vec<String>)>, StoreError>;
229
230 /// Delete a fact by its unique ID.
231 ///
232 /// Removing a non-existent ID should succeed silently (no error).
233 async fn forget(&self, id: &str) -> Result<(), StoreError>;
234}
235
236// ---------------------------------------------------------------------------
237// SummaryMemory
238// ---------------------------------------------------------------------------
239
240/// Compressed / summary memory.
241///
242/// Stores timestamped summaries of conversations, projects, or other scopes.
243/// Each summary is associated with a `source` (e.g. the raw conversation ID
244/// that was summarised) and a monotonic timestamp. The latest summary can
245/// be retrieved quickly, and the full history can be inspected.
246///
247/// # Typical backends
248///
249/// - SQLite table with `(scope, summary, source, created_at)`.
250/// - Document database (MongoDB, CouchDB) with scope-based indexing.
251/// - Append-only log file.
252///
253/// # Examples
254///
255/// ```ignore
256/// use xz_memory_engine::layered::traits::SummaryMemory;
257/// use xz_memory_core::StoreError;
258///
259/// async fn example(store: &dyn SummaryMemory) -> Result<(), StoreError> {
260/// store.store("sess-1", "User asked about weather and travel.",
261/// "raw-sess-1").await?;
262/// let latest = store.get_latest("sess-1").await?;
263/// assert!(latest.is_some());
264/// let history = store.history("sess-1", 10).await?;
265/// assert!(!history.is_empty());
266/// Ok(())
267/// }
268/// ```
269#[async_trait]
270pub trait SummaryMemory: Send + Sync {
271 /// Retrieve the most recent summary for a scope.
272 ///
273 /// Returns `None` if no summary exists. The tuple is
274 /// `(summary_text, source_id)`.
275 async fn get_latest(&self, scope: &str) -> Result<Option<(String, String)>, StoreError>;
276
277 /// Store a new summary for a scope.
278 ///
279 /// Each call creates a new entry; previous summaries are preserved
280 /// and can be retrieved via [`history`](SummaryMemory::history).
281 async fn store(&self, scope: &str, summary: &str, source: &str) -> Result<(), StoreError>;
282
283 /// Return the summary history for a scope, newest first.
284 ///
285 /// Each entry is `(summary_text, source_id, timestamp)` where
286 /// the timestamp is a Unix epoch in **milliseconds**.
287 async fn history(
288 &self,
289 scope: &str,
290 limit: usize,
291 ) -> Result<Vec<(String, String, u64)>, StoreError>;
292}