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}