Skip to main content

mnemo_core/query/
mod.rs

1pub mod branch;
2pub mod causality;
3pub mod checkpoint;
4pub mod conflict;
5pub mod consolidate;
6pub mod current_fact_resolver;
7pub mod event_builder;
8pub mod evidence;
9pub mod experience;
10pub mod forget;
11pub mod lifecycle;
12pub mod maturity;
13pub mod merge;
14pub mod orientation_cache;
15pub mod poisoning;
16pub mod recall;
17pub mod reflection;
18pub mod remember;
19pub mod replay;
20pub mod retained;
21pub mod retrieval;
22pub mod share;
23
24use std::sync::Arc;
25
26use crate::cache::MemoryCache;
27use crate::embedding::EmbeddingProvider;
28use crate::encryption::ContentEncryption;
29use crate::error::{Error, Result};
30use crate::index::VectorIndex;
31use crate::search::FullTextIndex;
32use crate::storage::StorageBackend;
33use crate::storage::cold::ColdStorage;
34
35const MAX_AGENT_ID_LEN: usize = 256;
36
37/// Maximum number of records returned by a single batch query.
38/// Prevents unbounded memory growth while supporting reasonable workloads.
39pub const MAX_BATCH_QUERY_LIMIT: usize = 10_000;
40
41/// Validate that an agent_id contains only safe characters and is within length limits.
42pub fn validate_agent_id(agent_id: &str) -> Result<()> {
43    if agent_id.is_empty() {
44        return Err(Error::Validation("agent_id cannot be empty".into()));
45    }
46    if agent_id.len() > MAX_AGENT_ID_LEN {
47        return Err(Error::Validation(format!(
48            "agent_id exceeds max length of {MAX_AGENT_ID_LEN}"
49        )));
50    }
51    if !agent_id
52        .chars()
53        .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.')
54    {
55        return Err(Error::Validation(
56            "agent_id must contain only alphanumeric characters, hyphens, underscores, or dots"
57                .into(),
58        ));
59    }
60    Ok(())
61}
62
63pub struct MnemoEngine {
64    pub storage: Arc<dyn StorageBackend>,
65    pub index: Arc<dyn VectorIndex>,
66    pub embedding: Arc<dyn EmbeddingProvider>,
67    pub full_text: Option<Arc<dyn FullTextIndex>>,
68    pub default_agent_id: String,
69    pub default_org_id: Option<String>,
70    pub encryption: Option<Arc<ContentEncryption>>,
71    pub cold_storage: Option<Arc<dyn ColdStorage>>,
72    pub cache: Option<Arc<MemoryCache>>,
73    pub embed_events: bool,
74    /// Default TTL applied to `Working`-tier memories whose `remember`
75    /// request does not supply an explicit `ttl_seconds`. Defaults to 1 hour.
76    pub ttl_working_seconds: u64,
77    /// Importance floor enforced on write for `Procedural`-tier memories.
78    /// Defaults to 0.8.
79    pub procedural_importance_floor: f32,
80    /// Poisoning policy read by `check_for_anomaly`. Defaults to the v0.3.2
81    /// behaviour (no z-score outlier gate). Override with
82    /// [`MnemoEngine::with_poisoning_policy`].
83    pub poisoning_policy: poisoning::PoisoningPolicy,
84    /// v0.4.0-rc3 (Task B1) — when set, every
85    /// `recall(req)` with `req.with_provenance == Some(true)` returns
86    /// an HMAC-signed [`ReadProvenance`](crate::provenance::ReadProvenance)
87    /// receipt. `None` keeps the recall hot-path overhead at zero.
88    /// Attach via [`MnemoEngine::with_provenance_signer`].
89    pub provenance_signer: Option<Arc<crate::provenance::ProvenanceSigner>>,
90    /// v0.4.8 — when set, every `recall(req)` with
91    /// `req.orientation_cache == Some(_)` updates this in-process,
92    /// namespace-scoped, constant-token "context map" and returns a
93    /// bounded rendering alongside the top-k. PEEK-anchored
94    /// (arXiv:2605.19932). `None` keeps the recall hot-path
95    /// overhead at zero. Attach via
96    /// [`MnemoEngine::with_orientation_cache_store`].
97    pub orientation_cache_store: Option<Arc<orientation_cache::OrientationCacheStore>>,
98    /// v0.4.10 — feedback-driven consolidation trigger metric. Default
99    /// [`maturity::ConsolidationPolicy::FixedSize`] preserves the
100    /// v0.4.x behaviour. Attach a
101    /// [`maturity::ConsolidationPolicy::MaturityDriven`] policy via
102    /// [`MnemoEngine::with_consolidation_policy`] to opt in to the
103    /// scalar maturity gate (recency / hit-success / edge-degree /
104    /// redundancy). Internal anchor: FluxMem (arXiv:2605.28773), prior
105    /// art only — mnemo's policy is a structural cousin, not a
106    /// reproduction.
107    pub consolidation_policy: maturity::ConsolidationPolicy,
108    /// v0.4.12 — optional answer-impact scorer for the cost-aware
109    /// evidence budget. When a recall sets
110    /// [`RecallRequest::evidence_budget`](recall::RecallRequest::evidence_budget)
111    /// with [`ScorerKind::Delta`](evidence::ScorerKind::Delta) AND this
112    /// is `Some`, the budget uses this scorer to decide sufficiency;
113    /// otherwise it falls back to [`evidence::CosineScorer`]. `None`
114    /// keeps the recall hot-path at zero overhead. Attach via
115    /// [`MnemoEngine::with_evidence_scorer`].
116    pub evidence_scorer: Option<Arc<dyn evidence::EvidenceScorer>>,
117    /// DocTrace (arXiv:2606.10921) — experience-memory tier gate. When
118    /// `false` (the default), `remember_plan` is a validation error and
119    /// `recall_plan` always misses, so default behaviour is unchanged.
120    /// Flip on via [`MnemoEngine::with_experience_memory`]. Plans are
121    /// stored as ordinary records (reserved tag + metadata), so the
122    /// tier is backend-agnostic and RBAC/consent-gated like everything
123    /// else. See [`experience`].
124    pub experience_memory_enabled: bool,
125}
126
127/// Default TTL (in seconds) applied to Working-tier memories.
128pub const DEFAULT_TTL_WORKING_SECONDS: u64 = 3600;
129
130/// Minimum importance floor applied to Procedural-tier memories on write.
131pub const DEFAULT_PROCEDURAL_IMPORTANCE_FLOOR: f32 = 0.8;
132
133impl MnemoEngine {
134    pub fn new(
135        storage: Arc<dyn StorageBackend>,
136        index: Arc<dyn VectorIndex>,
137        embedding: Arc<dyn EmbeddingProvider>,
138        default_agent_id: String,
139        default_org_id: Option<String>,
140    ) -> Self {
141        Self {
142            storage,
143            index,
144            embedding,
145            full_text: None,
146            default_agent_id,
147            default_org_id,
148            encryption: None,
149            cold_storage: None,
150            cache: None,
151            embed_events: false,
152            ttl_working_seconds: DEFAULT_TTL_WORKING_SECONDS,
153            procedural_importance_floor: DEFAULT_PROCEDURAL_IMPORTANCE_FLOOR,
154            poisoning_policy: poisoning::PoisoningPolicy::default(),
155            provenance_signer: None,
156            orientation_cache_store: None,
157            consolidation_policy: maturity::ConsolidationPolicy::default(),
158            evidence_scorer: None,
159            experience_memory_enabled: false,
160        }
161    }
162
163    /// Attach a [`provenance::ProvenanceSigner`](crate::provenance::ProvenanceSigner)
164    /// (Task B1) so callers can request signed read-receipts via
165    /// `RecallRequest.with_provenance = Some(true)`.
166    pub fn with_provenance_signer(
167        mut self,
168        signer: Arc<crate::provenance::ProvenanceSigner>,
169    ) -> Self {
170        self.provenance_signer = Some(signer);
171        self
172    }
173
174    /// Attach a [`poisoning::PoisoningPolicy`] to the engine. See
175    /// [`poisoning::PoisoningPolicy::with_outlier_threshold`] for the
176    /// v0.3.3 z-score outlier gate.
177    pub fn with_poisoning_policy(mut self, policy: poisoning::PoisoningPolicy) -> Self {
178        self.poisoning_policy = policy;
179        self
180    }
181
182    /// Override the default 1-hour TTL applied to `Working`-tier memories
183    /// when a caller does not supply an explicit `ttl_seconds`.
184    pub fn with_ttl_working_seconds(mut self, seconds: u64) -> Self {
185        self.ttl_working_seconds = seconds;
186        self
187    }
188
189    /// Override the default 0.8 importance floor applied to Procedural
190    /// memories on write.
191    pub fn with_procedural_importance_floor(mut self, floor: f32) -> Self {
192        self.procedural_importance_floor = floor.clamp(0.0, 1.0);
193        self
194    }
195
196    pub fn with_full_text(mut self, ft: Arc<dyn FullTextIndex>) -> Self {
197        self.full_text = Some(ft);
198        self
199    }
200
201    pub fn with_encryption(mut self, enc: Arc<ContentEncryption>) -> Self {
202        self.encryption = Some(enc);
203        self
204    }
205
206    pub fn with_cold_storage(mut self, cs: Arc<dyn ColdStorage>) -> Self {
207        self.cold_storage = Some(cs);
208        self
209    }
210
211    pub fn with_cache(mut self, c: Arc<MemoryCache>) -> Self {
212        self.cache = Some(c);
213        self
214    }
215
216    pub fn with_event_embeddings(mut self) -> Self {
217        self.embed_events = true;
218        self
219    }
220
221    /// v0.4.8 — attach a per-engine orientation-cache store. Recall
222    /// calls that set
223    /// [`RecallRequest::orientation_cache`][crate::query::recall::RecallRequest::orientation_cache]
224    /// will update + render the namespace-scoped, constant-token
225    /// context map. See
226    /// [`crate::query::orientation_cache`] for the contract +
227    /// the PEEK arXiv:2605.19932 anchor.
228    pub fn with_orientation_cache_store(
229        mut self,
230        store: Arc<orientation_cache::OrientationCacheStore>,
231    ) -> Self {
232        self.orientation_cache_store = Some(store);
233        self
234    }
235
236    /// v0.4.10 — attach a [`maturity::ConsolidationPolicy`]. The default
237    /// `FixedSize` policy preserves the legacy behaviour; pass
238    /// `MaturityDriven(MaturityPolicy::balanced())` to opt in to the
239    /// feedback-driven trigger metric. See
240    /// [`crate::query::maturity`] for the score contract.
241    pub fn with_consolidation_policy(mut self, policy: maturity::ConsolidationPolicy) -> Self {
242        self.consolidation_policy = policy;
243        self
244    }
245
246    /// v0.4.12 — attach an answer-impact [`evidence::EvidenceScorer`]
247    /// (typically a [`evidence::DeltaScorer`] wrapping an LLM callback)
248    /// used by the cost-aware evidence budget when a recall requests
249    /// [`ScorerKind::Delta`](evidence::ScorerKind::Delta). Without an
250    /// attached scorer, delta-mode budgets fall back to
251    /// [`evidence::CosineScorer`]. See [`crate::query::evidence`].
252    pub fn with_evidence_scorer(mut self, scorer: Arc<dyn evidence::EvidenceScorer>) -> Self {
253        self.evidence_scorer = Some(scorer);
254        self
255    }
256
257    /// DocTrace (arXiv:2606.10921) — enable the experience-memory tier so
258    /// [`remember_plan`](Self::remember_plan) caches successful plans and
259    /// [`recall_plan`](Self::recall_plan) replays them on
260    /// structurally-similar queries. Off by default (the two ops are
261    /// inert when disabled), so existing behaviour is unchanged. See
262    /// [`experience`].
263    pub fn with_experience_memory(mut self) -> Self {
264        self.experience_memory_enabled = true;
265        self
266    }
267
268    pub async fn remember(
269        &self,
270        request: remember::RememberRequest,
271    ) -> Result<remember::RememberResponse> {
272        remember::execute(self, request).await
273    }
274
275    pub async fn recall(&self, request: recall::RecallRequest) -> Result<recall::RecallResponse> {
276        recall::execute(self, request).await
277    }
278
279    pub async fn forget(&self, request: forget::ForgetRequest) -> Result<forget::ForgetResponse> {
280        forget::execute(self, request).await
281    }
282
283    /// Subject-scoped erasure for GDPR / DPDPA compliance.
284    /// See [`forget::forget_subject`] for strategy semantics.
285    pub async fn forget_subject(
286        &self,
287        request: forget::ForgetSubjectRequest,
288    ) -> Result<forget::ForgetSubjectResponse> {
289        forget::forget_subject(self, request).await
290    }
291
292    /// Hard-delete every memory whose `expires_at` is in the past and emit
293    /// one `MemoryExpired` audit event per deletion.
294    pub async fn run_ttl_sweep(&self) -> Result<lifecycle::TtlReport> {
295        lifecycle::run_ttl_sweep(self).await
296    }
297
298    /// Auto-Dream-compatible reflection pass: date absolutization, external
299    /// rewrite acceptance, semantic dedup, low-importance conflict
300    /// resolution, and stale archival. See [`reflection::run_reflection_pass`].
301    pub async fn run_reflection_pass(
302        &self,
303        agent_id: Option<String>,
304    ) -> Result<reflection::ReflectionReport> {
305        let agent_id = agent_id.unwrap_or_else(|| self.default_agent_id.clone());
306        reflection::run_reflection_pass(self, &agent_id).await
307    }
308
309    /// Reflection pass that honours the new `ReflectionMode` gate (v0.3.1).
310    /// Use `Coordinated` to avoid double-work when Auto Dream is also running.
311    pub async fn run_reflection_pass_with_mode(
312        &self,
313        agent_id: Option<String>,
314        mode: reflection::ReflectionMode,
315        force: bool,
316    ) -> Result<reflection::ReflectionReport> {
317        let agent_id = agent_id.unwrap_or_else(|| self.default_agent_id.clone());
318        reflection::run_reflection_pass_with_mode(self, &agent_id, mode, force).await
319    }
320
321    /// List quarantined memories for operator review. See
322    /// [`poisoning::replay_quarantine`].
323    pub async fn replay_quarantine(
324        &self,
325        agent_id: Option<String>,
326        since: Option<&str>,
327    ) -> Result<Vec<poisoning::QuarantineReplayEntry>> {
328        let agent_id = agent_id.unwrap_or_else(|| self.default_agent_id.clone());
329        poisoning::replay_quarantine(self, &agent_id, since).await
330    }
331
332    pub async fn share(&self, request: share::ShareRequest) -> Result<share::ShareResponse> {
333        share::execute(self, request).await
334    }
335
336    pub async fn checkpoint(
337        &self,
338        request: checkpoint::CheckpointRequest,
339    ) -> Result<checkpoint::CheckpointResponse> {
340        checkpoint::execute(self, request).await
341    }
342
343    pub async fn branch(&self, request: branch::BranchRequest) -> Result<branch::BranchResponse> {
344        branch::execute(self, request).await
345    }
346
347    pub async fn merge(&self, request: merge::MergeRequest) -> Result<merge::MergeResponse> {
348        merge::execute(self, request).await
349    }
350
351    pub async fn replay(&self, request: replay::ReplayRequest) -> Result<replay::ReplayResponse> {
352        replay::execute(self, request).await
353    }
354
355    /// `CONSOLIDATE` (Infini-Memory, arXiv:2606.10677) — group a caller-chosen
356    /// set of member memories into one revisable topic document, preserving
357    /// provenance and the hash-chained audit history. See [`consolidate`].
358    pub async fn consolidate(
359        &self,
360        request: consolidate::ConsolidateRequest,
361    ) -> Result<consolidate::ConsolidateResponse> {
362        consolidate::execute(self, request).await
363    }
364
365    /// `REMEMBER_PLAN` (DocTrace, arXiv:2606.10921) — cache a successful
366    /// retrieval/reasoning plan into the experience-memory tier. Inert
367    /// unless the engine was built with
368    /// [`with_experience_memory`](Self::with_experience_memory). See
369    /// [`experience`].
370    pub async fn remember_plan(
371        &self,
372        request: experience::RememberPlanRequest,
373    ) -> Result<experience::RememberPlanResponse> {
374        experience::execute_remember_plan(self, request).await
375    }
376
377    /// `RECALL_PLAN` (DocTrace, arXiv:2606.10921) — replay the best stored
378    /// plan whose query signature matches above the similarity threshold,
379    /// or return a miss. Always misses when the experience-memory mode is
380    /// disabled. See [`experience`].
381    pub async fn recall_plan(
382        &self,
383        request: experience::RecallPlanRequest,
384    ) -> Result<experience::RecallPlanResponse> {
385        experience::execute_recall_plan(self, request).await
386    }
387
388    pub async fn run_decay_pass(
389        &self,
390        agent_id: Option<String>,
391        archive_threshold: f32,
392        forget_threshold: f32,
393    ) -> Result<lifecycle::DecayPassResult> {
394        let agent_id = agent_id.unwrap_or_else(|| self.default_agent_id.clone());
395        lifecycle::run_decay_pass(self, &agent_id, archive_threshold, forget_threshold).await
396    }
397
398    pub async fn run_consolidation(
399        &self,
400        agent_id: Option<String>,
401        min_cluster_size: usize,
402    ) -> Result<lifecycle::ConsolidationResult> {
403        let agent_id = agent_id.unwrap_or_else(|| self.default_agent_id.clone());
404        lifecycle::run_consolidation(self, &agent_id, min_cluster_size).await
405    }
406
407    pub async fn verify_integrity(
408        &self,
409        agent_id: Option<String>,
410        thread_id: Option<&str>,
411    ) -> Result<crate::hash::ChainVerificationResult> {
412        let agent_id = agent_id.unwrap_or_else(|| self.default_agent_id.clone());
413        let records = self
414            .storage
415            .list_memories_by_agent_ordered(&agent_id, thread_id, 10000)
416            .await?;
417        Ok(crate::hash::verify_chain(&records))
418    }
419
420    pub async fn trace_causality(
421        &self,
422        event_id: uuid::Uuid,
423        max_depth: usize,
424    ) -> Result<causality::CausalChain> {
425        causality::trace_causality(
426            self,
427            event_id,
428            max_depth,
429            causality::TraceDirection::Down,
430            None,
431        )
432        .await
433    }
434
435    pub async fn trace_causality_with_options(
436        &self,
437        event_id: uuid::Uuid,
438        max_depth: usize,
439        direction: causality::TraceDirection,
440        event_type_filter: Option<crate::model::event::EventType>,
441    ) -> Result<causality::CausalChain> {
442        causality::trace_causality(self, event_id, max_depth, direction, event_type_filter).await
443    }
444
445    pub async fn verify_event_integrity(
446        &self,
447        agent_id: Option<String>,
448        thread_id: Option<&str>,
449    ) -> Result<crate::hash::ChainVerificationResult> {
450        let agent_id = agent_id.unwrap_or_else(|| self.default_agent_id.clone());
451        let events = if let Some(tid) = thread_id {
452            self.storage.get_events_by_thread(tid, 10000).await?
453        } else {
454            // list_events returns DESC order; reverse to chronological for chain verification
455            let mut evts = self.storage.list_events(&agent_id, 10000, 0).await?;
456            evts.reverse();
457            evts
458        };
459        Ok(crate::hash::verify_event_chain(&events))
460    }
461
462    pub async fn detect_conflicts(
463        &self,
464        agent_id: Option<String>,
465        threshold: f32,
466    ) -> Result<conflict::ConflictDetectionResult> {
467        let agent_id = agent_id.unwrap_or_else(|| self.default_agent_id.clone());
468        conflict::detect_conflicts(self, &agent_id, threshold).await
469    }
470
471    pub async fn resolve_conflict(
472        &self,
473        conflict_pair: &conflict::ConflictPair,
474        strategy: conflict::ResolutionStrategy,
475    ) -> Result<()> {
476        conflict::resolve_conflict(self, conflict_pair, strategy).await
477    }
478}