xz-memory-engine 0.2.0

Reusable engine implementations for xz-memory-core: storage backends and layered memory
Documentation
//! Layered memory trait definitions.
//!
//! This module defines the five trait contracts that make up the **layered
//! memory architecture**: [`ConversationMemory`], [`UserProfileMemory`],
//! [`ProjectMemory`], [`FactMemory`], and [`SummaryMemory`].
//!
//! Each trait represents a distinct *memory layer* with its own access
//! patterns, retention policies, and lifecycle — from short-lived
//! conversational windows to durable, searchable facts.
//!
//! # Design principles
//!
//! - **Separation of concerns**: each layer has a single responsibility and
//!   can be backed by a different storage engine.
//! - **Async-first**: every operation is `async` so backends can use
//!   remote stores, vector databases, or batched I/O without blocking.
//! - **Swappable backends**: implementing a trait is the only contract;
//!   the layered engine composes them without coupling to any concrete type.
//! - **`Send + Sync`**: all traits require `Send + Sync` so they can be
//!   shared across `tokio` tasks safely.

use async_trait::async_trait;
use std::collections::HashMap;
use xz_memory_core::StoreError;

// ---------------------------------------------------------------------------
// ConversationMemory
// ---------------------------------------------------------------------------

/// Short-term conversational memory.
///
/// Models the sliding window of a single conversation session — messages are
/// appended in chronological order, the most recent `n` can be retrieved,
/// and the oldest messages can be evicted to bound memory usage.
///
/// # Typical backends
///
/// - In-memory ring buffer (fast, bounded).
/// - Redis list with `LPUSH` / `LRANGE` / `LTRIM`.
/// - SQLite table ordered by `recorded_at`.
///
/// # Examples
///
/// ```ignore
/// use xz_memory_engine::layered::traits::ConversationMemory;
/// use xz_memory_core::StoreError;
///
/// async fn example(store: &dyn ConversationMemory) -> Result<(), StoreError> {
///     store.append("sess-1", "Hello, how can I help?").await?;
///     store.append("sess-1", "What is the weather?").await?;
///     let recent = store.recent("sess-1", 2).await?;
///     assert_eq!(recent.len(), 2);
///     let evicted = store.evict("sess-1", 1).await?;
///     assert_eq!(evicted, 1);
///     Ok(())
/// }
/// ```
#[async_trait]
pub trait ConversationMemory: Send + Sync {
    /// Append a message to the session.
    ///
    /// Messages are stored in insertion order; the most recent message
    /// is last in retrieval order.
    async fn append(&self, session_id: &str, message: &str) -> Result<(), StoreError>;

    /// Return the most recent `n` messages, newest last.
    ///
    /// If the session has fewer than `n` messages the returned vector
    /// will be shorter. Use `n = 0` to retrieve all messages (behaviour
    /// is backend-defined — some may return an empty vector).
    async fn recent(&self, session_id: &str, n: usize) -> Result<Vec<String>, StoreError>;

    /// Evict the oldest messages, keeping only the `keep` most recent.
    ///
    /// Returns the number of messages actually evicted.
    async fn evict(&self, session_id: &str, keep: usize) -> Result<usize, StoreError>;
}

// ---------------------------------------------------------------------------
// UserProfileMemory
// ---------------------------------------------------------------------------

/// Per-user profile / preference memory.
///
/// Stores key-value preference pairs keyed by `(user_id, key)`. This layer
/// is designed for stable, low-volume data that persists across sessions:
/// language preferences, notification settings, display name, etc.
///
/// # Typical backends
///
/// - SQLite `user_prefs` table.
/// - Redis hash per user (`HSET` / `HGETALL` / `HDEL`).
/// - Embedded key-value store (e.g. RocksDB, Sled).
///
/// # Examples
///
/// ```ignore
/// use xz_memory_engine::layered::traits::UserProfileMemory;
/// use xz_memory_core::StoreError;
///
/// async fn example(store: &dyn UserProfileMemory) -> Result<(), StoreError> {
///     store.set_preference("user-42", "language", "zh-CN").await?;
///     store.set_preference("user-42", "theme", "dark").await?;
///     let prefs = store.get_preferences("user-42").await?;
///     assert!(prefs.contains_key("language"));
///     store.remove_preference("user-42", "theme").await?;
///     Ok(())
/// }
/// ```
#[async_trait]
pub trait UserProfileMemory: Send + Sync {
    /// Retrieve all preferences for a user as a flat key-value map.
    async fn get_preferences(&self, user_id: &str) -> Result<HashMap<String, String>, StoreError>;

    /// Set or update a single preference key.
    ///
    /// If the key already exists its value is overwritten.
    async fn set_preference(&self, user_id: &str, key: &str, value: &str)
    -> Result<(), StoreError>;

    /// Remove a single preference key.
    ///
    /// Removing a non-existent key should succeed silently (no error).
    async fn remove_preference(&self, user_id: &str, key: &str) -> Result<(), StoreError>;
}

// ---------------------------------------------------------------------------
// ProjectMemory
// ---------------------------------------------------------------------------

/// Project-scoped key-value memory with fuzzy search.
///
/// Unlike [`UserProfileMemory`], this layer is designed for larger,
/// project-associated datasets that benefit from content-based retrieval.
/// Values are opaque strings and the `search` method enables approximate
/// matching (e.g. for notes, code snippets, or documentation fragments).
///
/// # Typical backends
///
/// - Full-text search index (Tantivy, Meilisearch) backed by a document store.
/// - Vector database (Qdrant, Milvus) with semantic search via embedding.
/// - SQLite with FTS5 extension.
///
/// # Examples
///
/// ```ignore
/// use xz_memory_engine::layered::traits::ProjectMemory;
/// use xz_memory_core::StoreError;
///
/// async fn example(store: &dyn ProjectMemory) -> Result<(), StoreError> {
///     store.put("proj-1", "note-1", "Remember to update deps").await?;
///     let val = store.get("proj-1", "note-1").await?;
///     assert_eq!(val, Some("Remember to update deps".into()));
///     let results = store.search("proj-1", "update deps").await?;
///     assert!(!results.is_empty());
///     Ok(())
/// }
/// ```
#[async_trait]
pub trait ProjectMemory: Send + Sync {
    /// Store a key-value pair in the project namespace.
    async fn put(&self, project_id: &str, key: &str, value: &str) -> Result<(), StoreError>;

    /// Retrieve a value by key.
    ///
    /// Returns `None` if the key does not exist.
    async fn get(&self, project_id: &str, key: &str) -> Result<Option<String>, StoreError>;

    /// List all keys in the project namespace.
    async fn keys(&self, project_id: &str) -> Result<Vec<String>, StoreError>;

    /// Search for entries matching the query string.
    ///
    /// Returns a vector of `(key, relevance_score)` pairs sorted by
    /// descending relevance. The score is a float typically in `[0.0, 1.0]`
    /// but the range is backend-defined.
    async fn search(&self, project_id: &str, query: &str)
    -> Result<Vec<(String, f32)>, StoreError>;
}

// ---------------------------------------------------------------------------
// FactMemory
// ---------------------------------------------------------------------------

/// Semantic / tagged fact memory.
///
/// Stores discrete facts with optional tags and supports fuzzy retrieval
/// by semantic similarity or keyword match. Designed for knowledge that
/// is ingested, queried, and occasionally pruned — the agent's long-term
/// general knowledge.
///
/// # Typical backends
///
/// - Vector database with embedding-based retrieval.
/// - Hybrid index combining BM25 keyword search and dense embeddings.
/// - Graph database with tag-based traversal.
///
/// # Examples
///
/// ```ignore
/// use xz_memory_engine::layered::traits::FactMemory;
/// use xz_memory_core::StoreError;
///
/// async fn example(store: &dyn FactMemory) -> Result<(), StoreError> {
///     let id = store.remember("Tokyo is the capital of Japan", &["geography".into()]).await?;
///     let results = store.recall("capital of Japan", 5).await?;
///     assert!(!results.is_empty());
///     store.forget(&id).await?;
///     Ok(())
/// }
/// ```
#[async_trait]
pub trait FactMemory: Send + Sync {
    /// Store a fact with optional tags and return a unique identifier.
    ///
    /// The returned `String` is the backend-assigned ID that can be used
    /// to later [`forget`](FactMemory::forget) the fact.
    async fn remember(&self, fact: &str, tags: &[String]) -> Result<String, StoreError>;

    /// Recall facts matching the query.
    ///
    /// Returns a vector of `(fact_text, relevance_score, tags)` tuples
    /// sorted by descending relevance. `limit` caps the number of results.
    async fn recall(
        &self,
        query: &str,
        limit: usize,
    ) -> Result<Vec<(String, f32, Vec<String>)>, StoreError>;

    /// Delete a fact by its unique ID.
    ///
    /// Removing a non-existent ID should succeed silently (no error).
    async fn forget(&self, id: &str) -> Result<(), StoreError>;
}

// ---------------------------------------------------------------------------
// SummaryMemory
// ---------------------------------------------------------------------------

/// Compressed / summary memory.
///
/// Stores timestamped summaries of conversations, projects, or other scopes.
/// Each summary is associated with a `source` (e.g. the raw conversation ID
/// that was summarised) and a monotonic timestamp. The latest summary can
/// be retrieved quickly, and the full history can be inspected.
///
/// # Typical backends
///
/// - SQLite table with `(scope, summary, source, created_at)`.
/// - Document database (MongoDB, CouchDB) with scope-based indexing.
/// - Append-only log file.
///
/// # Examples
///
/// ```ignore
/// use xz_memory_engine::layered::traits::SummaryMemory;
/// use xz_memory_core::StoreError;
///
/// async fn example(store: &dyn SummaryMemory) -> Result<(), StoreError> {
///     store.store("sess-1", "User asked about weather and travel.",
///                 "raw-sess-1").await?;
///     let latest = store.get_latest("sess-1").await?;
///     assert!(latest.is_some());
///     let history = store.history("sess-1", 10).await?;
///     assert!(!history.is_empty());
///     Ok(())
/// }
/// ```
#[async_trait]
pub trait SummaryMemory: Send + Sync {
    /// Retrieve the most recent summary for a scope.
    ///
    /// Returns `None` if no summary exists. The tuple is
    /// `(summary_text, source_id)`.
    async fn get_latest(&self, scope: &str) -> Result<Option<(String, String)>, StoreError>;

    /// Store a new summary for a scope.
    ///
    /// Each call creates a new entry; previous summaries are preserved
    /// and can be retrieved via [`history`](SummaryMemory::history).
    async fn store(&self, scope: &str, summary: &str, source: &str) -> Result<(), StoreError>;

    /// Return the summary history for a scope, newest first.
    ///
    /// Each entry is `(summary_text, source_id, timestamp)` where
    /// the timestamp is a Unix epoch in **milliseconds**.
    async fn history(
        &self,
        scope: &str,
        limit: usize,
    ) -> Result<Vec<(String, String, u64)>, StoreError>;
}