Skip to main content

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}