zeph_common/memory.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Shared memory interface types used by both `zeph-memory` (Layer 1) and
5//! `zeph-context` (Layer 1) without a cross-layer dependency.
6//!
7//! Moving these pure interface types here resolves the same-layer violation
8//! `zeph-context → zeph-memory` (issue #3665).
9
10use std::fmt;
11use std::str::FromStr;
12
13use serde::{Deserialize, Serialize};
14
15// ── MemoryRoute ───────────────────────────────────────────────────────────────
16
17/// Classification of which memory backend(s) to query.
18///
19/// Used in routing configuration and at runtime to dispatch memory operations.
20/// Serialises with `snake_case` names (`keyword`, `semantic`, `hybrid`, `graph`, `episodic`).
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
22#[serde(rename_all = "snake_case")]
23#[non_exhaustive]
24pub enum MemoryRoute {
25 /// Full-text search only (`SQLite` FTS5). Fast, good for keyword/exact queries.
26 Keyword,
27 /// Vector search only (Qdrant). Good for semantic/conceptual queries.
28 Semantic,
29 /// Both backends, results merged by reciprocal rank fusion.
30 #[default]
31 Hybrid,
32 /// Graph-based retrieval via BFS traversal.
33 Graph,
34 /// FTS5 search with a timestamp-range filter. Used for temporal/episodic queries.
35 Episodic,
36}
37
38/// Routing decision with confidence and optional LLM reasoning.
39#[derive(Debug, Clone)]
40pub struct RoutingDecision {
41 pub route: MemoryRoute,
42 /// Confidence in `[0, 1]`. `1.0` = certain, `0.5` = ambiguous.
43 pub confidence: f32,
44 /// Only populated when an LLM classifier was used.
45 pub reasoning: Option<String>,
46}
47
48/// Decides which memory backend(s) to query for a given input.
49pub trait MemoryRouter: Send + Sync {
50 /// Route a query to the appropriate backend(s).
51 fn route(&self, query: &str) -> MemoryRoute;
52
53 /// Route with a confidence signal. Default implementation wraps `route()` with confidence 1.0.
54 fn route_with_confidence(&self, query: &str) -> RoutingDecision {
55 RoutingDecision {
56 route: self.route(query),
57 confidence: 1.0,
58 reasoning: None,
59 }
60 }
61}
62
63/// Async extension for LLM-capable routers.
64pub trait AsyncMemoryRouter: MemoryRouter {
65 fn route_async<'a>(
66 &'a self,
67 query: &'a str,
68 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = RoutingDecision> + Send + 'a>>;
69}
70
71// ── RecallView ────────────────────────────────────────────────────────────────
72
73/// Enrichment level for view-aware graph recall.
74///
75/// # Examples
76///
77/// ```
78/// use zeph_common::memory::RecallView;
79///
80/// assert_eq!(RecallView::default(), RecallView::Head);
81/// ```
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
83#[non_exhaustive]
84pub enum RecallView {
85 /// Standard retrieval — no enrichment beyond what the base method provides.
86 #[default]
87 Head,
88 /// Retrieval + source-message provenance.
89 ZoomIn,
90 /// Retrieval + 1-hop neighbor expansion.
91 ZoomOut,
92}
93
94// ── CompressionLevel ─────────────────────────────────────────────────────────
95
96/// The three abstraction levels in the compression spectrum.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
98#[non_exhaustive]
99pub enum CompressionLevel {
100 /// Raw episodic messages — full fidelity, high token cost.
101 Episodic,
102 /// Abstracted procedural knowledge (how-to, tool patterns).
103 Procedural,
104 /// Stable declarative facts and reference material.
105 Declarative,
106}
107
108impl CompressionLevel {
109 /// A relative token-cost factor for budgeting purposes.
110 ///
111 /// `Episodic = 1.0` (baseline), `Procedural = 0.6`, `Declarative = 0.3`.
112 #[must_use]
113 pub const fn cost_factor(self) -> f32 {
114 match self {
115 Self::Episodic => 1.0,
116 Self::Procedural => 0.6,
117 Self::Declarative => 0.3,
118 }
119 }
120}
121
122// ── AnchoredSummary ───────────────────────────────────────────────────────────
123
124/// Structured compaction summary with anchored sections.
125///
126/// Produced by the structured summarization path during hard compaction.
127/// Replaces the free-form 9-section prose when `[memory] structured_summaries = true`.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
130pub struct AnchoredSummary {
131 /// What the user is ultimately trying to accomplish in this session.
132 pub session_intent: String,
133 /// File paths, function names, structs/enums touched or referenced.
134 pub files_modified: Vec<String>,
135 /// Architectural or implementation decisions made, with rationale.
136 pub decisions_made: Vec<String>,
137 /// Unresolved questions, ambiguities, or blocked items.
138 pub open_questions: Vec<String>,
139 /// Concrete next actions the agent should take immediately.
140 pub next_steps: Vec<String>,
141}
142
143impl AnchoredSummary {
144 /// Returns true if the mandatory sections (`session_intent`, `next_steps`) are populated.
145 #[must_use]
146 pub fn is_complete(&self) -> bool {
147 !self.session_intent.trim().is_empty() && !self.next_steps.is_empty()
148 }
149
150 /// Render as Markdown for context injection into the LLM.
151 #[must_use]
152 pub fn to_markdown(&self) -> String {
153 let mut out = String::with_capacity(512);
154 out.push_str("[anchored summary]\n");
155 out.push_str("## Session Intent\n");
156 out.push_str(&self.session_intent);
157 out.push('\n');
158
159 if !self.files_modified.is_empty() {
160 out.push_str("\n## Files Modified\n");
161 for entry in &self.files_modified {
162 let clean = entry.trim_start_matches("- ");
163 out.push_str("- ");
164 out.push_str(clean);
165 out.push('\n');
166 }
167 }
168
169 if !self.decisions_made.is_empty() {
170 out.push_str("\n## Decisions Made\n");
171 for entry in &self.decisions_made {
172 let clean = entry.trim_start_matches("- ");
173 out.push_str("- ");
174 out.push_str(clean);
175 out.push('\n');
176 }
177 }
178
179 if !self.open_questions.is_empty() {
180 out.push_str("\n## Open Questions\n");
181 for entry in &self.open_questions {
182 let clean = entry.trim_start_matches("- ");
183 out.push_str("- ");
184 out.push_str(clean);
185 out.push('\n');
186 }
187 }
188
189 if !self.next_steps.is_empty() {
190 out.push_str("\n## Next Steps\n");
191 for entry in &self.next_steps {
192 let clean = entry.trim_start_matches("- ");
193 out.push_str("- ");
194 out.push_str(clean);
195 out.push('\n');
196 }
197 }
198
199 out
200 }
201
202 /// Validate per-field length limits to guard against bloated LLM output.
203 ///
204 /// # Errors
205 ///
206 /// Returns `Err` with a descriptive message if any field exceeds its limit.
207 pub fn validate(&self) -> Result<(), String> {
208 const MAX_INTENT: usize = 2_000;
209 const MAX_ENTRY: usize = 500;
210 const MAX_VEC_LEN: usize = 50;
211
212 if self.session_intent.len() > MAX_INTENT {
213 return Err(format!(
214 "session_intent exceeds {MAX_INTENT} chars (got {})",
215 self.session_intent.len()
216 ));
217 }
218 for (field, entries) in [
219 ("files_modified", &self.files_modified),
220 ("decisions_made", &self.decisions_made),
221 ("open_questions", &self.open_questions),
222 ("next_steps", &self.next_steps),
223 ] {
224 if entries.len() > MAX_VEC_LEN {
225 return Err(format!(
226 "{field} has {} entries (max {MAX_VEC_LEN})",
227 entries.len()
228 ));
229 }
230 for entry in entries {
231 if entry.len() > MAX_ENTRY {
232 return Err(format!(
233 "{field} entry exceeds {MAX_ENTRY} chars (got {})",
234 entry.len()
235 ));
236 }
237 }
238 }
239 Ok(())
240 }
241
242 /// Serialize to JSON for storage in `summaries.content`.
243 ///
244 /// # Panics
245 ///
246 /// Panics if serialization fails. Since all fields are `String`/`Vec<String>`,
247 /// serialization is infallible in practice.
248 #[must_use]
249 pub fn to_json(&self) -> String {
250 serde_json::to_string(self).expect("AnchoredSummary serialization is infallible")
251 }
252}
253
254// ── SpreadingActivationParams ─────────────────────────────────────────────────
255
256/// Parameters for spreading activation graph retrieval.
257#[derive(Debug, Clone)]
258pub struct SpreadingActivationParams {
259 pub decay_lambda: f32,
260 pub max_hops: u32,
261 pub activation_threshold: f32,
262 pub inhibition_threshold: f32,
263 pub max_activated_nodes: usize,
264 pub temporal_decay_rate: f64,
265 /// Weight of structural score in hybrid seed ranking. Range: `[0.0, 1.0]`. Default: `0.4`.
266 pub seed_structural_weight: f32,
267 /// Maximum seeds per community ID. `0` = unlimited. Default: `3`.
268 pub seed_community_cap: usize,
269 /// SYNAPSE blend coefficient for Benna-Fusi fast/slow variables (#3709).
270 ///
271 /// Blends `confidence_fast` and `confidence_slow` for edge weight in spreading activation:
272 /// `blended = alpha * fast + (1 - alpha) * slow`.
273 /// Range: `[0.0, 1.0]`. Default: `0.3` (favors the stable slow variable).
274 pub alpha: f32,
275}
276
277// ── EdgeType ──────────────────────────────────────────────────────────────────
278
279/// MAGMA edge type: the semantic category of a relationship between two entities.
280#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
281#[serde(rename_all = "snake_case")]
282#[non_exhaustive]
283pub enum EdgeType {
284 #[default]
285 Semantic,
286 Temporal,
287 Causal,
288 Entity,
289}
290
291impl EdgeType {
292 /// Return the canonical lowercase string for this edge type.
293 ///
294 /// # Examples
295 ///
296 /// ```
297 /// use zeph_common::memory::EdgeType;
298 ///
299 /// assert_eq!(EdgeType::Causal.as_str(), "causal");
300 /// ```
301 #[must_use]
302 pub const fn as_str(self) -> &'static str {
303 match self {
304 Self::Semantic => "semantic",
305 Self::Temporal => "temporal",
306 Self::Causal => "causal",
307 Self::Entity => "entity",
308 }
309 }
310}
311
312impl fmt::Display for EdgeType {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 f.write_str(self.as_str())
315 }
316}
317
318impl FromStr for EdgeType {
319 type Err = String;
320
321 fn from_str(s: &str) -> Result<Self, Self::Err> {
322 match s {
323 "semantic" => Ok(Self::Semantic),
324 "temporal" => Ok(Self::Temporal),
325 "causal" => Ok(Self::Causal),
326 "entity" => Ok(Self::Entity),
327 other => Err(format!("unknown edge type: {other}")),
328 }
329 }
330}
331
332// ── Marker constants ──────────────────────────────────────────────────────────
333
334/// MAGMA causal edge markers used by `classify_graph_subgraph`.
335pub const CAUSAL_MARKERS: &[&str] = &[
336 "why",
337 "because",
338 "caused",
339 "cause",
340 "reason",
341 "result",
342 "led to",
343 "consequence",
344 "trigger",
345 "effect",
346 "blame",
347 "fault",
348];
349
350/// MAGMA temporal edge markers for subgraph classification.
351pub const TEMPORAL_MARKERS: &[&str] = &[
352 "before", "after", "first", "then", "timeline", "sequence", "preceded", "followed", "started",
353 "ended", "during", "prior",
354];
355
356/// MAGMA entity/structural markers.
357pub const ENTITY_MARKERS: &[&str] = &[
358 "is a",
359 "type of",
360 "kind of",
361 "part of",
362 "instance",
363 "same as",
364 "alias",
365 "subtype",
366 "subclass",
367 "belongs to",
368];
369
370/// Single-word temporal tokens that require word-boundary checking.
371pub const WORD_BOUNDARY_TEMPORAL: &[&str] = &["ago"];
372
373/// Classify a query into MAGMA edge types to use for subgraph-scoped BFS retrieval.
374///
375/// Pure heuristic, zero latency — no LLM call. Returns a prioritised list of [`EdgeType`]s.
376///
377/// # Example
378///
379/// ```
380/// use zeph_common::memory::{classify_graph_subgraph, EdgeType};
381///
382/// let types = classify_graph_subgraph("why did X happen");
383/// assert!(types.contains(&EdgeType::Causal));
384/// assert!(types.contains(&EdgeType::Semantic));
385/// ```
386#[must_use]
387pub fn classify_graph_subgraph(query: &str) -> Vec<EdgeType> {
388 let lower = query.to_ascii_lowercase();
389 let mut types: Vec<EdgeType> = Vec::new();
390
391 if CAUSAL_MARKERS.iter().any(|m| lower.contains(m)) {
392 types.push(EdgeType::Causal);
393 }
394 if TEMPORAL_MARKERS.iter().any(|m| lower.contains(m)) {
395 types.push(EdgeType::Temporal);
396 }
397 if ENTITY_MARKERS.iter().any(|m| lower.contains(m)) {
398 types.push(EdgeType::Entity);
399 }
400
401 if !types.contains(&EdgeType::Semantic) {
402 types.push(EdgeType::Semantic);
403 }
404
405 types
406}
407
408/// Parse a route name string into a [`MemoryRoute`], falling back to `fallback` on unknown values.
409///
410/// # Examples
411///
412/// ```
413/// use zeph_common::memory::{parse_route_str, MemoryRoute};
414///
415/// assert_eq!(parse_route_str("semantic", MemoryRoute::Hybrid), MemoryRoute::Semantic);
416/// assert_eq!(parse_route_str("unknown", MemoryRoute::Hybrid), MemoryRoute::Hybrid);
417/// ```
418#[must_use]
419pub fn parse_route_str(s: &str, fallback: MemoryRoute) -> MemoryRoute {
420 match s {
421 "keyword" => MemoryRoute::Keyword,
422 "semantic" => MemoryRoute::Semantic,
423 "hybrid" => MemoryRoute::Hybrid,
424 "graph" => MemoryRoute::Graph,
425 "episodic" => MemoryRoute::Episodic,
426 _ => fallback,
427 }
428}
429
430// ── TokenCounting trait ───────────────────────────────────────────────────────
431
432/// Minimal token-counting interface used by `zeph-context` for budget enforcement.
433///
434/// Defined here in Layer 0 so `zeph-context` can accept a `&dyn TokenCounting`
435/// without importing `zeph-memory`. `zeph-memory::TokenCounter` implements this trait.
436pub trait TokenCounting: Send + Sync {
437 /// Count tokens in a plain text string.
438 fn count_tokens(&self, text: &str) -> usize;
439 /// Count tokens for a JSON schema value (tool definitions).
440 fn count_tool_schema_tokens(&self, schema: &serde_json::Value) -> usize;
441}
442
443// ── Context memory DTOs ───────────────────────────────────────────────────────
444//
445// Plain data-transfer structs used by `ContextMemoryBackend`. They mirror the
446// fields that `zeph-context::assembler` actually reads from `zeph-memory` row
447// types. Keeping them here (Layer 0) allows `zeph-context` (Layer 1) to depend
448// only on `zeph-common` rather than `zeph-memory`.
449
450/// A persona fact row projection used by context assembly.
451#[derive(Debug, Clone)]
452pub struct MemPersonaFact {
453 /// Fact category label (e.g. `"preference"`, `"domain"`).
454 pub category: String,
455 /// Fact content injected into the system prompt.
456 pub content: String,
457}
458
459/// A memory tree node projection used by context assembly.
460#[derive(Debug, Clone)]
461pub struct MemTreeNode {
462 /// Node content injected into the system prompt.
463 pub content: String,
464}
465
466/// A conversation summary projection used by context assembly.
467#[derive(Debug, Clone)]
468pub struct MemSummary {
469 /// Row ID of the first message covered by this summary, if known.
470 pub first_message_id: Option<i64>,
471 /// Row ID of the last message covered by this summary, if known.
472 pub last_message_id: Option<i64>,
473 /// Summary text.
474 pub content: String,
475}
476
477/// A reasoning strategy projection used by context assembly.
478#[derive(Debug, Clone)]
479pub struct MemReasoningStrategy {
480 /// Unique strategy identifier (used by `mark_reasoning_used`).
481 pub id: String,
482 /// Outcome label (e.g. `"success"`, `"failure"`).
483 pub outcome: String,
484 /// Distilled strategy summary injected into the system prompt.
485 pub summary: String,
486}
487
488/// A user correction projection used by context assembly.
489#[derive(Debug, Clone)]
490pub struct MemCorrection {
491 /// The correction text to inject into the system prompt.
492 pub correction_text: String,
493}
494
495/// A recalled message projection used by context assembly.
496#[derive(Debug, Clone)]
497pub struct MemRecalledMessage {
498 /// Message role: `"user"`, `"assistant"`, or `"system"`.
499 pub role: String,
500 /// Message content.
501 pub content: String,
502 /// Similarity score in `[0, 1]`.
503 pub score: f32,
504}
505
506/// A neighbor fact in a graph recall result.
507#[derive(Debug, Clone)]
508pub struct MemGraphNeighbor {
509 /// Neighbor fact text.
510 pub fact: String,
511 /// Confidence score in `[0, 1]`.
512 pub confidence: f32,
513}
514
515/// A graph fact projection used by context assembly.
516#[derive(Debug, Clone)]
517pub struct MemGraphFact {
518 /// Fact text.
519 pub fact: String,
520 /// Confidence score in `[0, 1]`.
521 pub confidence: f32,
522 /// Spreading-activation score, if applicable.
523 pub activation_score: Option<f32>,
524 /// `ZoomOut` 1-hop neighbors, if view-aware expansion was requested.
525 pub neighbors: Vec<MemGraphNeighbor>,
526 /// `ZoomIn` provenance snippet, if view-aware provenance was requested.
527 pub provenance_snippet: Option<String>,
528}
529
530/// A cross-session summary search result used by context assembly.
531#[derive(Debug, Clone)]
532pub struct MemSessionSummary {
533 /// Summary text from the matched session.
534 pub summary_text: String,
535 /// Similarity score in `[0, 1]`.
536 pub score: f32,
537}
538
539/// A document chunk search result used by context assembly.
540#[derive(Debug, Clone)]
541pub struct MemDocumentChunk {
542 /// Chunk text extracted from the `"text"` payload key.
543 pub text: String,
544}
545
546/// A trajectory entry projection used by context assembly.
547#[derive(Debug, Clone)]
548pub struct MemTrajectoryEntry {
549 /// Intent description for the trajectory entry.
550 pub intent: String,
551 /// Outcome description.
552 pub outcome: String,
553 /// Confidence score in `[0, 1]`.
554 pub confidence: f64,
555}
556
557// ── GraphRecallParams ─────────────────────────────────────────────────────────
558
559/// Parameters for a graph-view recall call, used by [`ContextMemoryBackend::recall_graph_facts`].
560#[derive(Debug)]
561pub struct GraphRecallParams<'a> {
562 /// Maximum number of graph facts to return.
563 pub limit: usize,
564 /// Enrichment view (head, zoom-in, zoom-out).
565 pub view: RecallView,
566 /// Cap on `ZoomOut` neighbor expansion.
567 pub zoom_out_neighbor_cap: usize,
568 /// Maximum BFS hops during graph traversal.
569 pub max_hops: u32,
570 /// Rate at which older facts are downweighted.
571 pub temporal_decay_rate: f64,
572 /// Edge type filters for subgraph-scoped BFS.
573 pub edge_types: &'a [EdgeType],
574 /// Spreading activation parameters. `None` disables spreading activation.
575 pub spreading_activation: Option<SpreadingActivationParams>,
576}
577
578// ── ContextMemoryBackend trait ────────────────────────────────────────────────
579
580/// Abstraction over `SemanticMemory` that `zeph-context` uses for all memory
581/// operations during context assembly.
582///
583/// Defined in Layer 0 (`zeph-common`) so that `zeph-context` (Layer 1) can hold
584/// `Option<Arc<dyn ContextMemoryBackend>>` without importing `zeph-memory`.
585/// `zeph-core` (Layer 4) provides the concrete implementation that wraps
586/// `SemanticMemory`.
587///
588/// All async methods use `Pin<Box<dyn Future<...>>>` for dyn-compatibility.
589#[allow(clippy::type_complexity)]
590pub trait ContextMemoryBackend: Send + Sync {
591 /// Load persona facts with at least `min_confidence`.
592 fn load_persona_facts<'a>(
593 &'a self,
594 min_confidence: f64,
595 ) -> std::pin::Pin<
596 Box<
597 dyn std::future::Future<
598 Output = Result<Vec<MemPersonaFact>, Box<dyn std::error::Error + Send + Sync>>,
599 > + Send
600 + 'a,
601 >,
602 >;
603
604 /// Load `top_k` trajectory entries for the given `tier` filter (e.g. `"procedural"`).
605 fn load_trajectory_entries<'a>(
606 &'a self,
607 tier: Option<&'a str>,
608 top_k: usize,
609 ) -> std::pin::Pin<
610 Box<
611 dyn std::future::Future<
612 Output = Result<
613 Vec<MemTrajectoryEntry>,
614 Box<dyn std::error::Error + Send + Sync>,
615 >,
616 > + Send
617 + 'a,
618 >,
619 >;
620
621 /// Load `top_k` memory tree nodes at the given level.
622 fn load_tree_nodes<'a>(
623 &'a self,
624 level: u32,
625 top_k: usize,
626 ) -> std::pin::Pin<
627 Box<
628 dyn std::future::Future<
629 Output = Result<Vec<MemTreeNode>, Box<dyn std::error::Error + Send + Sync>>,
630 > + Send
631 + 'a,
632 >,
633 >;
634
635 /// Load all summaries for the given conversation (raw row ID).
636 fn load_summaries<'a>(
637 &'a self,
638 conversation_id: i64,
639 ) -> std::pin::Pin<
640 Box<
641 dyn std::future::Future<
642 Output = Result<Vec<MemSummary>, Box<dyn std::error::Error + Send + Sync>>,
643 > + Send
644 + 'a,
645 >,
646 >;
647
648 /// Retrieve the top-`top_k` reasoning strategies for `query`.
649 fn retrieve_reasoning_strategies<'a>(
650 &'a self,
651 query: &'a str,
652 top_k: usize,
653 ) -> std::pin::Pin<
654 Box<
655 dyn std::future::Future<
656 Output = Result<
657 Vec<MemReasoningStrategy>,
658 Box<dyn std::error::Error + Send + Sync>,
659 >,
660 > + Send
661 + 'a,
662 >,
663 >;
664
665 /// Mark reasoning strategies as used (fire-and-forget; best-effort).
666 fn mark_reasoning_used<'a>(
667 &'a self,
668 ids: &'a [String],
669 ) -> std::pin::Pin<
670 Box<
671 dyn std::future::Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>>
672 + Send
673 + 'a,
674 >,
675 >;
676
677 /// Retrieve corrections similar to `query`, up to `limit` with `min_score`.
678 fn retrieve_corrections<'a>(
679 &'a self,
680 query: &'a str,
681 limit: usize,
682 min_score: f32,
683 ) -> std::pin::Pin<
684 Box<
685 dyn std::future::Future<
686 Output = Result<Vec<MemCorrection>, Box<dyn std::error::Error + Send + Sync>>,
687 > + Send
688 + 'a,
689 >,
690 >;
691
692 /// Recall semantically similar messages for `query`, up to `limit`.
693 fn recall<'a>(
694 &'a self,
695 query: &'a str,
696 limit: usize,
697 router: Option<&'a dyn AsyncMemoryRouter>,
698 ) -> std::pin::Pin<
699 Box<
700 dyn std::future::Future<
701 Output = Result<
702 Vec<MemRecalledMessage>,
703 Box<dyn std::error::Error + Send + Sync>,
704 >,
705 > + Send
706 + 'a,
707 >,
708 >;
709
710 /// Recall graph facts for `query` with view-aware enrichment.
711 fn recall_graph_facts<'a>(
712 &'a self,
713 query: &'a str,
714 params: GraphRecallParams<'a>,
715 ) -> std::pin::Pin<
716 Box<
717 dyn std::future::Future<
718 Output = Result<Vec<MemGraphFact>, Box<dyn std::error::Error + Send + Sync>>,
719 > + Send
720 + 'a,
721 >,
722 >;
723
724 /// Search cross-session summaries for `query`, excluding `current_conversation_id`.
725 fn search_session_summaries<'a>(
726 &'a self,
727 query: &'a str,
728 limit: usize,
729 current_conversation_id: Option<i64>,
730 ) -> std::pin::Pin<
731 Box<
732 dyn std::future::Future<
733 Output = Result<
734 Vec<MemSessionSummary>,
735 Box<dyn std::error::Error + Send + Sync>,
736 >,
737 > + Send
738 + 'a,
739 >,
740 >;
741
742 /// Search a named document collection for `query`, returning `top_k` chunks.
743 fn search_document_collection<'a>(
744 &'a self,
745 collection: &'a str,
746 query: &'a str,
747 top_k: usize,
748 ) -> std::pin::Pin<
749 Box<
750 dyn std::future::Future<
751 Output = Result<
752 Vec<MemDocumentChunk>,
753 Box<dyn std::error::Error + Send + Sync>,
754 >,
755 > + Send
756 + 'a,
757 >,
758 >;
759}
760
761#[cfg(test)]
762mod tests {
763 use super::MemoryRoute;
764
765 #[test]
766 fn memory_route_serde_roundtrip() {
767 let cases = [
768 ("\"keyword\"", MemoryRoute::Keyword),
769 ("\"semantic\"", MemoryRoute::Semantic),
770 ("\"hybrid\"", MemoryRoute::Hybrid),
771 ("\"graph\"", MemoryRoute::Graph),
772 ("\"episodic\"", MemoryRoute::Episodic),
773 ];
774 for (json_str, expected) in cases {
775 let got: MemoryRoute = serde_json::from_str(json_str).unwrap();
776 assert_eq!(got, expected);
777 let serialized = serde_json::to_string(&got).unwrap();
778 let roundtrip: MemoryRoute = serde_json::from_str(&serialized).unwrap();
779 assert_eq!(roundtrip, expected);
780 }
781 }
782
783 #[test]
784 fn memory_route_default_is_hybrid() {
785 assert_eq!(MemoryRoute::default(), MemoryRoute::Hybrid);
786 }
787}