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<T: EmbedderSend> Embedder for T, so
  Box<dyn Embedder> / Arc<dyn Embedder> 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<dyn Embedder> cast). cargo build
--workspace --release green. cargo clippy --workspace --features
embeddings,vector-search -- -D warnings clean. grep -rn async_trait crates/
returns zero.
This commit is contained in:
Jan De Landtsheer 2026-05-27 16:07:25 +02:00 committed by Sam Valladares
parent a4a6e877c5
commit 194fc6e4c0
3 changed files with 72 additions and 12 deletions

View file

@ -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<Vec<f32>> {
#[cfg(feature = "embeddings")]
{

View file

@ -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<T> = std::result::Result<T, EmbedderError>;
/// Boxed Send future returning an `EmbedderResult<T>`, 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<Box<dyn Future<Output = EmbedderResult<T>> + 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<Box<dyn Future + Send>>`, which is required for `Box<dyn Embedder>`
/// and `Arc<dyn Embedder>` 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<Box<dyn Future + Send + '_>>`.
#[trait_variant::make(EmbedderSend: Send)]
pub trait LocalEmbedder: Sync + 'static {
async fn embed(&self, text: &str) -> EmbedderResult<Vec<f32>>;
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<Box<dyn Future + Send + '_>>` so `Box<dyn Embedder>`
/// and `Arc<dyn Embedder>` work for the cognitive module surface and
/// the Phase 1 integration tests.
///
/// Implementations should not target this trait directly; the blanket
/// `impl<T: EmbedderSend> 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<f32>>;
fn embed_batch<'a>(
&'a self,
texts: &'a [&'a str],
) -> BoxedEmbedderFuture<'a, Vec<Vec<f32>>>;
fn model_name(&self) -> &str;
fn dimension(&self) -> usize;
fn model_hash(&self) -> String;
fn signature(&self) -> crate::storage::ModelSignature;
}
impl<T> Embedder for T
where
T: EmbedderSend,
{
fn embed<'a>(&'a self, text: &'a str) -> BoxedEmbedderFuture<'a, Vec<f32>> {
Box::pin(<T as EmbedderSend>::embed(self, text))
}
fn embed_batch<'a>(
&'a self,
texts: &'a [&'a str],
) -> BoxedEmbedderFuture<'a, Vec<Vec<f32>>> {
Box::pin(<T as EmbedderSend>::embed_batch(self, texts))
}
fn model_name(&self) -> &str {
<T as EmbedderSend>::model_name(self)
}
fn dimension(&self) -> usize {
<T as EmbedderSend>::dimension(self)
}
fn model_hash(&self) -> String {
<T as EmbedderSend>::model_hash(self)
}
fn signature(&self) -> crate::storage::ModelSignature {
<T as EmbedderSend>::signature(self)
}
}

View file

@ -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;