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}