Skip to main content

cel_memory/
provider.rs

1//! The [`MemoryProvider`] trait — the durable cross-turn memory contract.
2//!
3//! This is the single trait every memory backend implements and every caller
4//! depends on. Agents, CLIs, servers, desktop apps, and evaluation harnesses can
5//! compile against this trait while swapping persistence backends underneath.
6//! [`crate::BasicMemoryProvider`] is the in-crate reference backend; a full
7//! storage backend (e.g. the [`cel-memory-sqlite`](https://crates.io/crates/cel-memory-sqlite) crate)
8//! drops in behind the same surface without caller churn. Other engines belong
9//! in separate crates — see
10//! [BACKENDS.md](https://github.com/dimpagk92/cel-memory/blob/main/BACKENDS.md).
11
12use async_trait::async_trait;
13use chrono::NaiveDate;
14
15use crate::chunk::{MemoryChunk, NewMemoryChunk};
16use crate::error::Result;
17use crate::ops::{
18    AgingReport, ExportBundle, ExportFilter, MemoryStats, PurgeReport, ReEmbedReport,
19};
20use crate::query::{MemoryPredicate, MemoryQuery};
21use crate::session::{MemorySession, NewMemorySession, SessionFilter, SessionOutcome};
22
23use crate::ops::EvictionReason;
24
25/// The full memory provider surface.
26///
27/// All methods are async. All return `Result<T, MemoryError>`. The in-crate
28/// [`crate::BasicMemoryProvider`] backs the read/write/session/export surface
29/// (some methods return [`crate::MemoryError::NotImplemented`]); a full storage
30/// backend (e.g. the `cel-memory-sqlite` crate) implements every method.
31#[async_trait]
32pub trait MemoryProvider: Send + Sync {
33    // ─────────────── Reads ───────────────
34
35    /// Hybrid retrieval over the memory store. The provider applies the
36    /// query's profile, scope, and filters and returns up to `query.k`
37    /// chunks in descending relevance order.
38    async fn retrieve(&self, query: MemoryQuery) -> Result<Vec<MemoryChunk>>;
39
40    /// Fetch a chunk by ID.
41    async fn get(&self, chunk_id: &str) -> Result<Option<MemoryChunk>>;
42
43    /// Fetch a session by ID.
44    async fn get_session(&self, session_id: &str) -> Result<Option<MemorySession>>;
45
46    /// List sessions matching the filter, most-recent-first.
47    async fn list_sessions(&self, filter: SessionFilter) -> Result<Vec<MemorySession>>;
48
49    // ─────────────── Writes ───────────────
50
51    /// Persist a single new chunk. The provider assigns `id`, `created_at`,
52    /// `tier`, and embedding-model metadata.
53    async fn write(&self, chunk: NewMemoryChunk) -> Result<MemoryChunk>;
54
55    /// Persist many chunks in one call. The provider may batch embeddings;
56    /// the v1 stub processes them one at a time.
57    async fn write_batch(&self, chunks: Vec<NewMemoryChunk>) -> Result<Vec<MemoryChunk>>;
58
59    /// Open a new session. Returns the session record.
60    async fn open_session(&self, init: NewMemorySession) -> Result<MemorySession>;
61
62    /// Close an open session. Triggers session summarization where supported.
63    async fn close_session(&self, session_id: &str, outcome: SessionOutcome) -> Result<()>;
64
65    /// Rename a session's display title. Returns `Err(NotFound)` if the session
66    /// does not exist. The v1 `BasicMemoryProvider` implements this in-memory;
67    /// `SqliteMemoryProvider` updates the `memory_sessions.title` column.
68    async fn rename_session(&self, session_id: &str, title: &str) -> Result<()>;
69
70    // ─────────────── Updates ───────────────
71
72    /// Set or clear the pin flag on a chunk.
73    async fn pin(&self, chunk_id: &str, pinned: bool) -> Result<()>;
74
75    /// Set the importance score on a chunk. Out-of-range values are clamped
76    /// to `[0.0, 1.0]`. The v1 stub no-ops (importance is not scored).
77    async fn update_importance(&self, chunk_id: &str, importance: f32) -> Result<()>;
78
79    /// Mark `old_id` as superseded by `new_id`. Both chunks are retained;
80    /// retrieval surfaces the superseder. The v1 stub no-ops.
81    async fn supersede(&self, old_id: &str, new_id: &str) -> Result<()>;
82
83    /// Record that a chunk was returned by a retrieval call. `used=true`
84    /// signals the consumer cited or acted on it (drives importance bumps
85    /// in the full impl).
86    async fn record_access(&self, chunk_id: &str, retrieved_by: &str, used: bool) -> Result<()>;
87
88    // ─────────────── Deletes ───────────────
89
90    /// Hard-delete a single chunk, logging the reason.
91    async fn delete(&self, chunk_id: &str, reason: EvictionReason) -> Result<()>;
92
93    /// Hard-delete all chunks matching the predicate. Returns the count.
94    ///
95    /// As a footgun-prevention measure, an empty predicate is a no-op (see
96    /// [`MemoryPredicate::is_empty`]). Use [`purge_all`] to delete everything.
97    ///
98    /// [`MemoryPredicate::is_empty`]: crate::MemoryPredicate::is_empty
99    /// [`purge_all`]: MemoryProvider::purge_all
100    async fn delete_matching(
101        &self,
102        predicate: MemoryPredicate,
103        reason: EvictionReason,
104    ) -> Result<usize>;
105
106    /// Hard-delete every chunk, session, access-log row, and eviction-log
107    /// row. The "forget everything" flow.
108    async fn purge_all(&self) -> Result<PurgeReport>;
109
110    // ─────────────── Summarization ───────────────
111
112    /// Produce a `JobSummary` chunk synthesizing the named session. Inserted
113    /// into the store; returned to the caller. v1 stub returns
114    /// [`crate::MemoryError::NotImplemented`].
115    async fn summarize_session(&self, session_id: &str) -> Result<MemoryChunk>;
116
117    /// Produce one or more `Rollup` chunks for the named day. v1 stub returns
118    /// [`crate::MemoryError::NotImplemented`].
119    ///
120    /// Skips the day if a rollup already exists for it (idempotent;
121    /// cron sweeper can safely re-run). Use [`Self::rollup_day_forced`]
122    /// to force re-summarization.
123    async fn rollup_day(&self, date: NaiveDate) -> Result<Vec<MemoryChunk>>;
124
125    /// Force-produce one or more `Rollup` chunks for the named day even
126    /// if a prior rollup exists. Default impl delegates to
127    /// [`Self::rollup_day`] for backward compatibility; the SQLite
128    /// provider overrides to actually honour the force flag.
129    async fn rollup_day_forced(&self, date: NaiveDate) -> Result<Vec<MemoryChunk>> {
130        self.rollup_day(date).await
131    }
132
133    /// Produce a `Rollup` chunk for the named rule across the named week.
134    /// v1 stub returns [`crate::MemoryError::NotImplemented`].
135    ///
136    /// Skips the (rule, week) pair if a rollup already exists for it
137    /// (idempotent). Use [`Self::rollup_rule_week_forced`] to force
138    /// re-summarization.
139    async fn rollup_rule_week(&self, rule_id: &str, week_start: NaiveDate) -> Result<MemoryChunk>;
140
141    /// Force-produce a `Rollup` chunk for the named rule + week even if
142    /// a prior rollup exists. Default impl delegates to
143    /// [`Self::rollup_rule_week`] for backward compatibility; the SQLite
144    /// provider overrides to honour the force flag.
145    async fn rollup_rule_week_forced(
146        &self,
147        rule_id: &str,
148        week_start: NaiveDate,
149    ) -> Result<MemoryChunk> {
150        self.rollup_rule_week(rule_id, week_start).await
151    }
152
153    // ─────────────── Maintenance ───────────────
154
155    /// Run the aging sweeper: tier transitions, retention-horizon deletes,
156    /// importance-based eviction. Idempotent.
157    async fn run_aging_sweep(&self) -> Result<AgingReport>;
158
159    /// Re-embed every chunk against the target model. Long-running. Resumable.
160    /// v1 stub returns [`crate::MemoryError::NotImplemented`].
161    async fn re_embed_all(&self, target_model: &str) -> Result<ReEmbedReport>;
162
163    /// Export memory matching the filter as a self-contained bundle.
164    async fn export(&self, filter: ExportFilter) -> Result<ExportBundle>;
165
166    /// Summary statistics for the dashboard / doctor check.
167    async fn stats(&self) -> Result<MemoryStats>;
168}