Skip to main content

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    /// Persist multiple entries, committing any derived search index **once**
30    /// for the whole batch.
31    ///
32    /// Semantically equivalent to calling [`save`](Self::save) for each entry,
33    /// but implementations with a separate index (e.g. a tantivy BM25 index)
34    /// should override this to avoid a per-entry index commit. The default
35    /// implementation simply loops [`save`](Self::save).
36    ///
37    /// # Errors
38    ///
39    /// Returns an error if any underlying storage write fails. On error, entries
40    /// already written before the failure may remain persisted.
41    async fn save_batch(&self, entries: &[ContextEntry]) -> Result<()> {
42        for entry in entries {
43            self.save(entry).await?;
44        }
45        Ok(())
46    }
47
48    /// Return the top-k entries (most recent or highest priority).
49    ///
50    /// # Errors
51    ///
52    /// Returns an error if the underlying storage read fails.
53    async fn get_top_k(&self, k: usize) -> Result<Vec<ContextEntry>>;
54
55    /// Return every stored entry.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the underlying storage read fails.
60    async fn get_all(&self) -> Result<Vec<ContextEntry>>;
61
62    /// Delete an entry by id. Returns `true` if an entry was removed.
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if the underlying storage delete fails.
67    async fn delete(&self, id: &str) -> Result<bool>;
68
69    /// Remove all entries. Returns the number of entries removed.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the underlying storage delete fails.
74    async fn clear(&self) -> Result<usize>;
75
76    /// Remove all entries within a given scope. Returns the number of entries removed.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if the underlying storage delete fails.
81    async fn clear_scope(&self, scope: &str) -> Result<usize>;
82
83    /// Return the total number of stored entries.
84    ///
85    /// # Errors
86    ///
87    /// Returns an error if the underlying storage read fails.
88    async fn count(&self) -> Result<usize>;
89
90    /// Persist a dense embedding vector for an already-saved entry.
91    ///
92    /// The default no-op is used by storage backends that do not support
93    /// vector search. `TursoStorage` overrides this to write into the
94    /// `embedding` column via `vector32(?)`.
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if the underlying write fails.
99    async fn save_embedding(&self, id: &str, embedding: &[f32]) -> Result<()> {
100        tracing::trace!(id = %id, dims = embedding.len(), "save_embedding: no-op");
101        let _ = (id, embedding);
102        Ok(())
103    }
104
105    /// Return entries that do not yet have an embedding stored.
106    ///
107    /// Used by the backfill path. The default falls back to `get_all()`;
108    /// `TursoStorage` overrides with an efficient `WHERE embedding IS NULL` query.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if the underlying storage read fails.
113    async fn get_unembedded(&self, limit: usize) -> Result<Vec<ContextEntry>> {
114        tracing::trace!(limit = %limit, "get_unembedded: using get_all fallback");
115        let all = self.get_all().await?;
116        Ok(all.into_iter().take(limit).collect())
117    }
118}
119
120/// Trait for searching context entries by relevance.
121///
122/// Implementations must be thread-safe (`Send + Sync`) to support
123/// concurrent access from multiple worker threads.
124#[async_trait]
125pub trait Searcher: Send + Sync {
126    /// Search entries by cosine similarity to `embedding`, returning at most
127    /// `limit` results ordered by descending similarity score.
128    ///
129    /// The default no-op returns an empty list and is used by searcher backends
130    /// that do not support vector search. `TursoSearcher` overrides with a
131    /// `vector_distance_cos` query against the `embedding` column.
132    ///
133    /// # Errors
134    ///
135    /// Returns an error if the underlying vector search fails.
136    async fn search_semantic(
137        &self,
138        embedding: &[f32],
139        scope: Option<&str>,
140        limit: usize,
141    ) -> Result<Vec<ScoredEntry>> {
142        tracing::trace!(dims = embedding.len(), "search_semantic: no-op");
143        let _ = (embedding, scope, limit);
144        Ok(Vec::new())
145    }
146
147    /// Search for entries matching `query`, optionally restricted to `scope`,
148    /// returning at most `limit` results ordered by descending relevance score.
149    ///
150    /// `scope = None` searches every entry regardless of scope (global recall).
151    /// `scope = Some(s)` restricts results to entries whose `scope` equals `s`.
152    ///
153    /// `query` is treated as natural-language text: implementations should
154    /// split it into terms and OR-match them (e.g. via FTS5 bm25 ranking)
155    /// rather than requiring every term to match. Query-language operator
156    /// syntax (`AND`, `OR`, `NEAR`, prefix `*`, quoted phrases, column
157    /// filters, etc.) must **not** be interpreted from `query` — operator
158    /// characters are treated as term separators, so arbitrary text never
159    /// produces a syntax error. A query with no usable terms (empty or
160    /// punctuation-only) returns an empty result set, not an error. The
161    /// special value [`crate::engine::MATCH_ALL_QUERY`] (`"*"`) matches every
162    /// entry.
163    ///
164    /// # Errors
165    ///
166    /// Returns an error if the underlying search fails.
167    async fn search(
168        &self,
169        query: &str,
170        scope: Option<&str>,
171        limit: usize,
172    ) -> Result<Vec<ScoredEntry>>;
173}