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