From 194fc6e4c0c85d345ad32e268de88c930ebcec26 Mon Sep 17 00:00:00 2001 From: Jan De Landtsheer Date: Wed, 27 May 2026 16:07:25 +0200 Subject: [PATCH] feat(embedder): swap async-trait for trait_variant + dyn adapter (0001c) Mirror of the 0001a pattern for the Embedder side. - embedder/mod.rs: LocalEmbedder is the source trait declared with native async-fn-in-trait. #[trait_variant::make(EmbedderSend: Send)] derives the Send-bounded variant that backends implement. A hand-written Embedder trait wraps each async method in BoxedEmbedderFuture<'a, T> and forwards sync methods through a blanket impl Embedder for T, so Box / Arc stay dyn-safe -- trait_variant 0.1 alone does NOT produce a dyn-safe variant (RPITIT), so the hand-written adapter is required. - embedder/fastembed.rs: drop the #[async_trait::async_trait] attribute and retarget the impl block to EmbedderSend. Adjust the top-level use to bring EmbedderSend into scope (also keeps fastembed::tests' use super::* trait lookups working). - lib.rs: export EmbedderSend alongside the existing Embedder / LocalEmbedder re-exports. The async-trait Cargo dependency is dropped in a follow-up commit so the manifest change stays visible on its own. Verification: cargo test -p vestige-core --features embeddings,vector-search (428) and --no-default-features (370) both green. cargo test --test embedder_trait green (2/2 including Box cast). cargo build --workspace --release green. cargo clippy --workspace --features embeddings,vector-search -- -D warnings clean. grep -rn async_trait crates/ returns zero. --- crates/vestige-core/src/embedder/fastembed.rs | 5 +- crates/vestige-core/src/embedder/mod.rs | 75 +++++++++++++++++-- crates/vestige-core/src/lib.rs | 4 +- 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/crates/vestige-core/src/embedder/fastembed.rs b/crates/vestige-core/src/embedder/fastembed.rs index a4cd87b..a6ac120 100644 --- a/crates/vestige-core/src/embedder/fastembed.rs +++ b/crates/vestige-core/src/embedder/fastembed.rs @@ -4,7 +4,7 @@ #[cfg(feature = "embeddings")] use crate::embeddings::{EMBEDDING_DIMENSIONS, EmbeddingService}; -use super::{EmbedderError, EmbedderResult, LocalEmbedder}; +use super::{EmbedderError, EmbedderResult, EmbedderSend}; pub struct FastembedEmbedder { #[cfg(feature = "embeddings")] @@ -41,8 +41,7 @@ impl Default for FastembedEmbedder { } } -#[async_trait::async_trait] -impl LocalEmbedder for FastembedEmbedder { +impl EmbedderSend for FastembedEmbedder { async fn embed(&self, text: &str) -> EmbedderResult> { #[cfg(feature = "embeddings")] { diff --git a/crates/vestige-core/src/embedder/mod.rs b/crates/vestige-core/src/embedder/mod.rs index 9d43d0d..e8e654a 100644 --- a/crates/vestige-core/src/embedder/mod.rs +++ b/crates/vestige-core/src/embedder/mod.rs @@ -1,5 +1,8 @@ //! Text-to-vector encoding trait. Pluggable per-install. +use std::future::Future; +use std::pin::Pin; + mod fastembed; pub use fastembed::FastembedEmbedder; @@ -18,14 +21,23 @@ pub enum EmbedderError { pub type EmbedderResult = std::result::Result; +/// Boxed Send future returning an `EmbedderResult`, bound to the lifetime +/// of the borrows captured by the call. Used as the return type of every +/// async method on the dyn-compatible `Embedder` trait below. +pub type BoxedEmbedderFuture<'a, T> = + Pin> + Send + 'a>>; + /// Pluggable embedder. The storage layer NEVER calls fastembed directly; /// callers compute vectors via this trait and pass them into `MemoryStore`. /// -/// `#[async_trait::async_trait]` makes every `async fn` return a -/// `Pin>`, which is required for `Box` -/// and `Arc` to be dyn-compatible. -#[async_trait::async_trait] -pub trait LocalEmbedder: Send + Sync + 'static { +/// `LocalEmbedder` is the source-of-truth trait declared with native +/// async-fn-in-trait. `#[trait_variant::make(EmbedderSend: Send)]` derives +/// a Send-bounded variant that backends actually implement (the +/// trait_variant 0.1.x blanket goes variant -> source). The dyn-compatible +/// public surface is the `Embedder` trait declared below, which wraps every +/// async method in `Pin>`. +#[trait_variant::make(EmbedderSend: Send)] +pub trait LocalEmbedder: Sync + 'static { async fn embed(&self, text: &str) -> EmbedderResult>; fn model_name(&self) -> &str; @@ -52,6 +64,53 @@ pub trait LocalEmbedder: Send + Sync + 'static { } } -/// Type alias: `Embedder` is the dyn-compatible, Send+Sync variant. -/// Both names refer to the same `async_trait`-annotated trait. -pub use LocalEmbedder as Embedder; +/// Dyn-compatible embedder trait. +/// +/// `EmbedderSend` above is the trait users implement; it uses native +/// async-fn-in-trait return types (RPITIT), which gives zero-allocation +/// static dispatch but is not dyn-safe. This trait wraps every async +/// method in `Pin>` so `Box` +/// and `Arc` work for the cognitive module surface and +/// the Phase 1 integration tests. +/// +/// Implementations should not target this trait directly; the blanket +/// `impl Embedder for T` adapts every Send-variant +/// implementation automatically. +pub trait Embedder: Send + Sync + 'static { + fn embed<'a>(&'a self, text: &'a str) -> BoxedEmbedderFuture<'a, Vec>; + fn embed_batch<'a>( + &'a self, + texts: &'a [&'a str], + ) -> BoxedEmbedderFuture<'a, Vec>>; + fn model_name(&self) -> &str; + fn dimension(&self) -> usize; + fn model_hash(&self) -> String; + fn signature(&self) -> crate::storage::ModelSignature; +} + +impl Embedder for T +where + T: EmbedderSend, +{ + fn embed<'a>(&'a self, text: &'a str) -> BoxedEmbedderFuture<'a, Vec> { + Box::pin(::embed(self, text)) + } + fn embed_batch<'a>( + &'a self, + texts: &'a [&'a str], + ) -> BoxedEmbedderFuture<'a, Vec>> { + Box::pin(::embed_batch(self, texts)) + } + fn model_name(&self) -> &str { + ::model_name(self) + } + fn dimension(&self) -> usize { + ::dimension(self) + } + fn model_hash(&self) -> String { + ::model_hash(self) + } + fn signature(&self) -> crate::storage::ModelSignature { + ::signature(self) + } +} diff --git a/crates/vestige-core/src/lib.rs b/crates/vestige-core/src/lib.rs index f8a35d6..15dbdbf 100644 --- a/crates/vestige-core/src/lib.rs +++ b/crates/vestige-core/src/lib.rs @@ -198,7 +198,9 @@ pub use storage::{ }; // Embedder trait and implementations -pub use embedder::{Embedder, EmbedderError, EmbedderResult, FastembedEmbedder, LocalEmbedder}; +pub use embedder::{ + Embedder, EmbedderError, EmbedderResult, EmbedderSend, FastembedEmbedder, LocalEmbedder, +}; // Consolidation (sleep-inspired memory processing) pub use consolidation::SleepConsolidation;