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}