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