context_forge/traits.rs
1use async_trait::async_trait;
2
3use crate::entry::{ContextEntry, ScoredEntry};
4use crate::error::Error;
5
6/// Result type alias for crate operations.
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// Trait for persisting and retrieving context entries.
10///
11/// Implementations must be thread-safe (`Send + Sync`) to support
12/// concurrent access from multiple worker threads.
13#[async_trait]
14pub trait ContextStorage: Send + Sync {
15 /// Persist a single entry.
16 ///
17 /// # Security
18 ///
19 /// Implementations persist `entry.content` verbatim — secret scrubbing
20 /// happens only in [`ContextForge::save`](crate::ContextForge::save).
21 /// Callers writing through this trait directly are responsible for
22 /// scrubbing first (see [`scrub_secrets`](crate::scrub_secrets)).
23 ///
24 /// # Errors
25 ///
26 /// Returns an error if the underlying storage write fails.
27 async fn save(&self, entry: &ContextEntry) -> Result<()>;
28
29 /// Return the top-k entries (most recent or highest priority).
30 ///
31 /// # Errors
32 ///
33 /// Returns an error if the underlying storage read fails.
34 async fn get_top_k(&self, k: usize) -> Result<Vec<ContextEntry>>;
35
36 /// Return every stored entry.
37 ///
38 /// # Errors
39 ///
40 /// Returns an error if the underlying storage read fails.
41 async fn get_all(&self) -> Result<Vec<ContextEntry>>;
42
43 /// Delete an entry by id. Returns `true` if an entry was removed.
44 ///
45 /// # Errors
46 ///
47 /// Returns an error if the underlying storage delete fails.
48 async fn delete(&self, id: &str) -> Result<bool>;
49
50 /// Remove all entries. Returns the number of entries removed.
51 ///
52 /// # Errors
53 ///
54 /// Returns an error if the underlying storage delete fails.
55 async fn clear(&self) -> Result<usize>;
56
57 /// Remove all entries within a given scope. Returns the number of entries removed.
58 ///
59 /// # Errors
60 ///
61 /// Returns an error if the underlying storage delete fails.
62 async fn clear_scope(&self, scope: &str) -> Result<usize>;
63
64 /// Return the total number of stored entries.
65 ///
66 /// # Errors
67 ///
68 /// Returns an error if the underlying storage read fails.
69 async fn count(&self) -> Result<usize>;
70
71 /// Persist a dense embedding vector for an already-saved entry.
72 ///
73 /// The default no-op is used by storage backends that do not support
74 /// vector search. `TursoStorage` overrides this to write into the
75 /// `embedding` column via `vector32(?)`.
76 ///
77 /// # Errors
78 ///
79 /// Returns an error if the underlying write fails.
80 async fn save_embedding(&self, id: &str, embedding: &[f32]) -> Result<()> {
81 tracing::trace!(id = %id, dims = embedding.len(), "save_embedding: no-op");
82 let _ = (id, embedding);
83 Ok(())
84 }
85
86 /// Return entries that do not yet have an embedding stored.
87 ///
88 /// Used by the backfill path. The default falls back to `get_all()`;
89 /// `TursoStorage` overrides with an efficient `WHERE embedding IS NULL` query.
90 ///
91 /// # Errors
92 ///
93 /// Returns an error if the underlying storage read fails.
94 async fn get_unembedded(&self, limit: usize) -> Result<Vec<ContextEntry>> {
95 tracing::trace!(limit = %limit, "get_unembedded: using get_all fallback");
96 let all = self.get_all().await?;
97 Ok(all.into_iter().take(limit).collect())
98 }
99}
100
101/// Trait for searching context entries by relevance.
102///
103/// Implementations must be thread-safe (`Send + Sync`) to support
104/// concurrent access from multiple worker threads.
105#[async_trait]
106pub trait Searcher: Send + Sync {
107 /// Search entries by cosine similarity to `embedding`, returning at most
108 /// `limit` results ordered by descending similarity score.
109 ///
110 /// The default no-op returns an empty list and is used by searcher backends
111 /// that do not support vector search. `TursoSearcher` overrides with a
112 /// `vector_distance_cos` query against the `embedding` column.
113 ///
114 /// # Errors
115 ///
116 /// Returns an error if the underlying vector search fails.
117 async fn search_semantic(
118 &self,
119 embedding: &[f32],
120 scope: Option<&str>,
121 limit: usize,
122 ) -> Result<Vec<ScoredEntry>> {
123 tracing::trace!(dims = embedding.len(), "search_semantic: no-op");
124 let _ = (embedding, scope, limit);
125 Ok(Vec::new())
126 }
127
128 /// Search for entries matching `query`, optionally restricted to `scope`,
129 /// returning at most `limit` results ordered by descending relevance score.
130 ///
131 /// `scope = None` searches every entry regardless of scope (global recall).
132 /// `scope = Some(s)` restricts results to entries whose `scope` equals `s`.
133 ///
134 /// `query` is treated as natural-language text: implementations should
135 /// split it into terms and OR-match them (e.g. via FTS5 bm25 ranking)
136 /// rather than requiring every term to match. Query-language operator
137 /// syntax (`AND`, `OR`, `NEAR`, prefix `*`, quoted phrases, column
138 /// filters, etc.) must **not** be interpreted from `query` — operator
139 /// characters are treated as term separators, so arbitrary text never
140 /// produces a syntax error. A query with no usable terms (empty or
141 /// punctuation-only) returns an empty result set, not an error. The
142 /// special value [`crate::engine::MATCH_ALL_QUERY`] (`"*"`) matches every
143 /// entry.
144 ///
145 /// # Errors
146 ///
147 /// Returns an error if the underlying search fails.
148 async fn search(
149 &self,
150 query: &str,
151 scope: Option<&str>,
152 limit: usize,
153 ) -> Result<Vec<ScoredEntry>>;
154}