Skip to main content

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