cel_memory/query.rs
1//! Query, profile, scope, and predicate types.
2//!
3//! [`MemoryQuery`] is the input to [`crate::MemoryProvider::retrieve`].
4//! [`RetrievalProfile`] selects a tuned set of hybrid weights and kind filters
5//! per caller path; [`CallerScope`] enforces multi-agent isolation.
6//! [`MemoryPredicate`] describes the criteria for
7//! [`crate::MemoryProvider::delete_matching`].
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12use crate::chunk::ChunkKind;
13
14/// A retrieval query. See [`cellar-memory-manager.md`] §8.1.
15///
16/// [`cellar-memory-manager.md`]: file:///Users/dimitriospagkratis/.claude/plans/cellar-memory-manager.md
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub struct MemoryQuery {
19 /// Free-text query. Embedded for vector search and tokenized for FTS.
20 pub text: String,
21 /// Optional kind filter. `None` means all kinds.
22 #[serde(default)]
23 pub kinds: Option<Vec<ChunkKind>>,
24 /// Optional lower bound on `created_at`.
25 #[serde(default)]
26 pub since: Option<DateTime<Utc>>,
27 /// Optional upper bound on `created_at`.
28 #[serde(default)]
29 pub until: Option<DateTime<Utc>>,
30 /// Restrict to chunks in this session.
31 #[serde(default)]
32 pub session_id: Option<String>,
33 /// Multi-agent visibility scope.
34 #[serde(default)]
35 pub caller_scope: CallerScope,
36 /// Restrict to chunks with a `project_root` prefix matching this string.
37 #[serde(default)]
38 pub project_root_prefix: Option<String>,
39 /// Top-k results. Default 8.
40 #[serde(default = "default_k")]
41 pub k: usize,
42 /// Include rollup chunks (`ChunkKind::Rollup`). Default true.
43 #[serde(default = "default_true")]
44 pub include_rollups: bool,
45 /// Minimum importance to include. `None` means no floor.
46 #[serde(default)]
47 pub min_importance: Option<f32>,
48 /// Retrieval profile selecting hybrid weights and per-caller defaults.
49 #[serde(default)]
50 pub profile: RetrievalProfile,
51 /// Identifier of the caller performing the retrieval — used to enforce
52 /// [`CallerScope::Own`] and to log access for relevance feedback.
53 pub caller_id: String,
54}
55
56fn default_k() -> usize {
57 8
58}
59
60fn default_true() -> bool {
61 true
62}
63
64/// Multi-agent visibility scope for a query.
65///
66/// See [`cellar-memory-manager.md`] §13. Default scope for every external MCP
67/// client is [`Own`]; the embedded agent gets [`OwnPlusShared`]; the Memory
68/// tab UI and the audit timeline get [`Global`].
69///
70/// [`Own`]: CallerScope::Own
71/// [`OwnPlusShared`]: CallerScope::OwnPlusShared
72/// [`Global`]: CallerScope::Global
73/// [`cellar-memory-manager.md`]: file:///Users/dimitriospagkratis/.claude/plans/cellar-memory-manager.md
74#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
75#[serde(rename_all = "snake_case")]
76pub enum CallerScope {
77 /// Caller sees only chunks where `caller_id == self`.
78 #[default]
79 Own,
80 /// Caller sees own chunks plus any chunk tagged shareable.
81 OwnPlusShared,
82 /// Caller sees everything. Privileged — granted to user surfaces only.
83 Global,
84}
85
86/// Named retrieval profiles. Each profile sets a tuned set of hybrid weights
87/// (vector / FTS / recency) and kind filters appropriate to the caller path.
88///
89/// The v1 [`crate::BasicMemoryProvider`] accepts the profile but does not
90/// honor it (it always performs a single lexical match). The full Memory &
91/// Context Manager implements per-profile tuning per
92/// [`cellar-memory-manager.md`] §8.3.
93///
94/// [`cellar-memory-manager.md`]: file:///Users/dimitriospagkratis/.claude/plans/cellar-memory-manager.md
95#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
96#[serde(rename_all = "snake_case")]
97pub enum RetrievalProfile {
98 /// Embedded agent's per-turn retrieval. Semantic-heavy, short recency
99 /// half-life. The default for any call without a profile.
100 #[default]
101 AgentChatTurn,
102 /// Embedded agent's delegated-job context. Heavier on long-term tier and
103 /// job-summary chunks.
104 AgentDelegatedJob,
105 /// NL rule compiler authoring a new rule — wants similar prior rules.
106 NLCompilerSimilarRules,
107 /// NL rule compiler authoring a new rule — wants similar prior fires
108 /// to a draft rule's match.
109 NLCompilerSimilarFires,
110 /// Audit / Activity tab — wide window, keyword-dominant.
111 AuditTimeline,
112 /// User free-text search in the Memory tab.
113 UserSearch,
114}
115
116/// Criteria for [`crate::MemoryProvider::delete_matching`] (and reused as the
117/// filter inside [`crate::ExportFilter`]).
118///
119/// Empty predicate matches nothing — `delete_matching(MemoryPredicate::default())`
120/// is a safe no-op rather than a "delete everything" footgun. Use
121/// [`crate::MemoryProvider::purge_all`] for that.
122#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
123pub struct MemoryPredicate {
124 /// Match chunks whose kind is in this set.
125 #[serde(default)]
126 pub kinds: Option<Vec<ChunkKind>>,
127 /// Match chunks written by callers in this set.
128 #[serde(default)]
129 pub callers: Option<Vec<String>>,
130 /// Match chunks belonging to these sessions.
131 #[serde(default)]
132 pub session_ids: Option<Vec<String>>,
133 /// Match chunks whose `project_root` begins with this prefix.
134 #[serde(default)]
135 pub project_root_prefix: Option<String>,
136 /// Match chunks created before this instant.
137 #[serde(default)]
138 pub before: Option<DateTime<Utc>>,
139 /// Match chunks created after this instant.
140 #[serde(default)]
141 pub after: Option<DateTime<Utc>>,
142 /// If `Some(true)`, only pinned chunks; if `Some(false)`, only unpinned;
143 /// `None` ignores pinning.
144 #[serde(default)]
145 pub pinned: Option<bool>,
146 /// Match chunks with importance strictly less than this.
147 #[serde(default)]
148 pub importance_below: Option<f32>,
149 /// Match chunks whose content contains this substring (case-insensitive
150 /// in v1).
151 #[serde(default)]
152 pub content_contains: Option<String>,
153}
154
155impl MemoryPredicate {
156 /// True if the predicate has no constraints — i.e. would match every
157 /// chunk. [`crate::MemoryProvider::delete_matching`] short-circuits to a
158 /// no-op when this returns true.
159 pub fn is_empty(&self) -> bool {
160 self.kinds.is_none()
161 && self.callers.is_none()
162 && self.session_ids.is_none()
163 && self.project_root_prefix.is_none()
164 && self.before.is_none()
165 && self.after.is_none()
166 && self.pinned.is_none()
167 && self.importance_below.is_none()
168 && self.content_contains.is_none()
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use serde_json::json;
176
177 #[test]
178 fn caller_scope_default_is_own() {
179 assert_eq!(CallerScope::default(), CallerScope::Own);
180 }
181
182 #[test]
183 fn retrieval_profile_default_is_agent_chat_turn() {
184 assert_eq!(RetrievalProfile::default(), RetrievalProfile::AgentChatTurn);
185 }
186
187 #[test]
188 fn empty_predicate_is_empty() {
189 assert!(MemoryPredicate::default().is_empty());
190 let p = MemoryPredicate {
191 content_contains: Some("hi".into()),
192 ..Default::default()
193 };
194 assert!(!p.is_empty());
195 }
196
197 #[test]
198 fn query_round_trip() {
199 let q = MemoryQuery {
200 text: "Q4 report".into(),
201 kinds: Some(vec![ChunkKind::Chat, ChunkKind::Action]),
202 since: None,
203 until: None,
204 session_id: Some("sess_abc".into()),
205 caller_scope: CallerScope::OwnPlusShared,
206 project_root_prefix: Some("/Users/dim/Workspace".into()),
207 k: 8,
208 include_rollups: true,
209 min_importance: Some(0.3),
210 profile: RetrievalProfile::AgentChatTurn,
211 caller_id: "embedded".into(),
212 };
213 let s = serde_json::to_string(&q).unwrap();
214 let back: MemoryQuery = serde_json::from_str(&s).unwrap();
215 assert_eq!(q, back);
216 }
217
218 #[test]
219 fn query_defaults_apply_on_deserialize() {
220 let raw = json!({
221 "text": "Q4 report",
222 "caller_id": "embedded"
223 })
224 .to_string();
225 let q: MemoryQuery = serde_json::from_str(&raw).unwrap();
226 assert_eq!(q.k, 8);
227 assert!(q.include_rollups);
228 assert_eq!(q.profile, RetrievalProfile::AgentChatTurn);
229 assert_eq!(q.caller_scope, CallerScope::Own);
230 }
231}