Skip to main content

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}