Skip to main content

rig_core/serve/
adapters.rs

1//! Effect handlers adapting model, tool, and retrieval traits.
2//! Each adapter translates its effect family into trait calls and returns
3//! outcomes or streaming events.
4//!
5//! ```
6//! use rig_core::message::ToolName;
7//! use rig_core::serve::Serve;
8//! use rig_core::tool::{DynamicTool, ToolContext, ToolOutput};
9//! use rig_core::wasm_compat::WasmBoxedFuture;
10//!
11//! let handler = DynamicTool::new_with_context(
12//!     ToolName::new("echo")?,
13//!     "Echo the arguments",
14//!     serde_json::json!({ "type": "object" }),
15//!     |_context: &mut ToolContext, args: serde_json::Value| {
16//!         Box::pin(async move { Ok(ToolOutput::text(args.to_string())) })
17//!             as WasmBoxedFuture<'_, _>
18//!     },
19//! );
20//! assert_eq!(handler.descriptor().key, rig_core::effect::tool_key("echo"));
21//! # Ok::<(), rig_core::message::EmptyToolName>(())
22//! ```
23
24use std::sync::{Arc, Mutex};
25
26use crate::{
27    completion::{CompletionRequest, ModelRef, UnsupportedOption},
28    driver::{DynModel, Sending, without_messages},
29    effect::{
30        Answered, EffectFamily, EffectId, EffectKind, EmbedInputs, EmbedModality, EmbedOutputs,
31        FamilyDescriptor, HandlerDescriptor, HandlerKey, RetrieveQuery, RetrievedDocuments,
32        ToolEmbeddingDescriptor,
33    },
34    error::{ErrorKind, ErrorReport},
35    operation::{Completion, Embedding, Rerank},
36    providers::registry::ProviderId,
37    tool::{Tool, ToolEmbedding, ToolExecutionError},
38    vector_store::{VectorStoreError, VectorStoreIndex, request::DynamicSearchFilter},
39    wasm_compat::{WasmBoxedFuture, WasmCompatSend, WasmCompatSync},
40    wire::Operation,
41};
42
43use super::{Dispatch, Reply, Serve};
44use crate::effect::family;
45
46fn wrong_family(handler: EffectFamily, kind: &EffectKind) -> ErrorReport {
47    ErrorReport::new(
48        ErrorKind::HandlerUnavailable,
49        format!(
50            "a {handler} handler cannot serve a `{}` effect",
51            kind.name()
52        ),
53    )
54}
55
56/// A model as a handler under `label`. Completion, text-embedding and
57/// rerank models serve their effect families. The model is erased to its
58/// operation, so one adapter type serves every wire and transport.
59pub struct ModelAdapter<Op: Operation> {
60    label: ModelRef,
61    model: DynModel<Op>,
62    /// The last completion request [`Serve::check`] answered, as it was
63    /// checked (without its messages), and what the model refused of it.
64    checked: Mutex<Option<(CompletionRequest, Vec<UnsupportedOption>)>>,
65}
66
67impl<Op: Operation> ModelAdapter<Op> {
68    /// Wrap `model` under `label`.
69    pub fn new(label: impl Into<ModelRef>, model: impl Into<DynModel<Op>>) -> Self {
70        Self {
71            label: label.into(),
72            model: model.into(),
73            checked: Mutex::new(None),
74        }
75    }
76
77    /// How the model sends for `dispatch` under `handler`: its HTTP request
78    /// carries the [`Serving`], observed under `observation`.
79    fn sending(
80        &self,
81        handler: HandlerKey,
82        dispatch: &Dispatch,
83        observation: Option<crate::observe::AdapterContext>,
84    ) -> Sending {
85        let mut extensions = http::Extensions::new();
86        extensions.insert(Serving {
87            effect: dispatch.id(),
88            handler,
89            provider: self.model.spec().map(|spec| spec.provider),
90        });
91        Sending {
92            observation,
93            extensions,
94        }
95    }
96}
97
98/// What an outgoing HTTP request to a model serves, in its
99/// [extensions](http::Request::extensions): the effect it answers and the
100/// handler it was dispatched to. A model adapter sets it on every request
101/// it sends, so a [transport wrapper](crate::catalog::Connector::wrap_http)
102/// can tell one model call's requests from another's and from those of no
103/// call, such as a model listing. The request's headers and body are the
104/// wrapper's to read, such as rig-cassette's traffic log, which records them.
105///
106/// ```
107/// use rig_core::http_client::Request;
108/// use rig_core::serve::Serving;
109///
110/// fn served(request: &Request<Vec<u8>>) -> Option<u64> {
111///     request
112///         .extensions()
113///         .get::<Serving>()
114///         .map(|serving| serving.effect.as_u64())
115/// }
116/// assert_eq!(served(&Request::new(Vec::new())), None);
117/// ```
118#[derive(Clone, Debug, PartialEq, Eq)]
119#[non_exhaustive]
120pub struct Serving {
121    /// The effect the request answers, such as an ECS effect's id.
122    pub effect: EffectId,
123    /// The handler it was dispatched to, such as `model:openai/gpt-5`.
124    pub handler: HandlerKey,
125    /// The provider the model is reached through, when the model was
126    /// connected from a catalog spec.
127    pub provider: Option<ProviderId>,
128}
129
130impl Serving {
131    /// What a request answering `effect` through `handler` serves, reached
132    /// through no provider a catalog names.
133    pub fn new(effect: EffectId, handler: HandlerKey) -> Self {
134        Self {
135            effect,
136            handler,
137            provider: None,
138        }
139    }
140}
141
142/// Unary and streaming completions both route here; the descriptor carries
143/// the model's label and capability snapshot.
144impl Serve for ModelAdapter<Completion> {
145    type Family = family::Completion;
146
147    fn descriptor(&self) -> HandlerDescriptor {
148        HandlerDescriptor {
149            key: crate::effect::model_key(self.label.as_str()),
150            family: FamilyDescriptor::Completion {
151                model: self.label.clone(),
152                capabilities: self.model.capabilities().completion,
153            },
154            layers: Vec::new(),
155        }
156    }
157
158    /// What the model's wire or the model leaves out of a completion's
159    /// request ([`DynModel::check`]); nothing for a request that cannot be
160    /// built at all, which sending reports, or for another family. The
161    /// check reads no messages but the system prompt, so the answer for
162    /// the last request is kept for the next that differs only in its
163    /// messages. A request with provider options is always checked: two
164    /// equal entries can still refuse differently.
165    fn check(&self, kind: &EffectKind) -> Vec<UnsupportedOption> {
166        let EffectKind::Completion { request, .. } = kind else {
167            return Vec::new();
168        };
169        let memo = request.provider_options.is_empty();
170        let checked = without_messages(request);
171        if memo
172            && let Ok(last) = self.checked.lock()
173            && let Some((last, refused)) = last.as_ref()
174            && *last == checked
175        {
176            return refused.clone();
177        }
178        let refused = match self.model.check(request) {
179            Err(crate::completion::CheckError::Unsupported(refused)) => refused,
180            Ok(()) | Err(crate::completion::CheckError::Invalid(_)) => Vec::new(),
181        };
182        if memo && let Ok(mut last) = self.checked.lock() {
183            *last = Some((checked, refused.clone()));
184        }
185        refused
186    }
187
188    async fn serve(&self, kind: EffectKind, dispatch: Dispatch) -> Reply {
189        let model = &self.model;
190        let sending = self.sending(
191            crate::effect::model_key(self.label.as_str()),
192            &dispatch,
193            dispatch.adapter_context(),
194        );
195        match kind {
196            EffectKind::Completion {
197                request,
198                stream: false,
199            } => {
200                let result = model.finished(Arc::unwrap_or_clone(request), sending).await;
201                let result = result.map(Arc::new);
202                Reply::Outcome(result.map(Answered::Completion).map_err(ErrorReport::from))
203            }
204            EffectKind::Completion {
205                request,
206                stream: true,
207            } => match model.opened(Arc::unwrap_or_clone(request), sending) {
208                Ok(stream) => Reply::Stream(stream.into_relay()),
209                Err(error) => Reply::Outcome(Err(ErrorReport::from(error))),
210            },
211            other @ (EffectKind::ToolCall { .. }
212            | EffectKind::Embed { .. }
213            | EffectKind::Retrieve { .. }
214            | EffectKind::Rerank { .. }
215            | EffectKind::Custom { .. }) => {
216                Reply::Outcome(Err(wrong_family(EffectFamily::Completion, &other)))
217            }
218        }
219    }
220}
221
222/// A text embedding model embeds texts and refuses images.
223impl Serve for ModelAdapter<Embedding> {
224    type Family = family::Embed;
225
226    fn descriptor(&self) -> HandlerDescriptor {
227        let capabilities = self.model.capabilities();
228        HandlerDescriptor {
229            key: crate::effect::embed_key(self.label.as_str()),
230            family: FamilyDescriptor::Embed {
231                model: self.label.to_string(),
232                dims: Some(capabilities.ndims),
233                max_documents: capabilities.max_documents,
234                modality: EmbedModality::Text,
235            },
236            layers: Vec::new(),
237        }
238    }
239
240    async fn serve(&self, kind: EffectKind, dispatch: Dispatch) -> Reply {
241        let model = &self.model;
242        let sending = self.sending(
243            crate::effect::embed_key(self.label.as_str()),
244            &dispatch,
245            None,
246        );
247        match kind {
248            EffectKind::Embed {
249                inputs: EmbedInputs::Texts(texts),
250            } => Reply::Outcome(
251                model
252                    .finished(texts, sending)
253                    .await
254                    .map(|response| Answered::Embeddings(EmbedOutputs::Texts(response)))
255                    .map_err(ErrorReport::from),
256            ),
257            EffectKind::Embed {
258                inputs: EmbedInputs::Images(_),
259            } => Reply::Outcome(Err(ErrorReport::new(
260                ErrorKind::HandlerUnavailable,
261                "a text embedding handler cannot embed images",
262            ))),
263            other @ (EffectKind::Completion { .. }
264            | EffectKind::ToolCall { .. }
265            | EffectKind::Retrieve { .. }
266            | EffectKind::Rerank { .. }
267            | EffectKind::Custom { .. }) => {
268                Reply::Outcome(Err(wrong_family(EffectFamily::Embed, &other)))
269            }
270        }
271    }
272}
273
274/// A reranking model orders documents for a query.
275impl Serve for ModelAdapter<Rerank> {
276    type Family = family::Rerank;
277
278    fn descriptor(&self) -> HandlerDescriptor {
279        HandlerDescriptor {
280            key: crate::effect::rerank_key(self.label.as_str()),
281            family: FamilyDescriptor::Rerank {
282                model: self.label.to_string(),
283                max_documents: self.model.capabilities().max_documents,
284            },
285            layers: Vec::new(),
286        }
287    }
288
289    async fn serve(&self, kind: EffectKind, dispatch: Dispatch) -> Reply {
290        let model = &self.model;
291        let sending = self.sending(
292            crate::effect::rerank_key(self.label.as_str()),
293            &dispatch,
294            None,
295        );
296        match kind {
297            EffectKind::Rerank { request } => Reply::Outcome(
298                model
299                    .finished(request, sending)
300                    .await
301                    .map(Answered::Reranked)
302                    .map_err(ErrorReport::from),
303            ),
304            other @ (EffectKind::Completion { .. }
305            | EffectKind::ToolCall { .. }
306            | EffectKind::Embed { .. }
307            | EffectKind::Retrieve { .. }
308            | EffectKind::Custom { .. }) => {
309                Reply::Outcome(Err(wrong_family(EffectFamily::Rerank, &other)))
310            }
311        }
312    }
313}
314
315/// Serve one tool call: `args` parsed and handed to `call`, with the
316/// driver's inbound values from the dispatch's scope (`ToolContext`, as
317/// `for_dispatch`), else an empty context, and every scope of the dispatch
318/// attached, so the tool reaches its runtime by type for the length of the
319/// call. What the tool published goes back beside the dispatch when the
320/// driver asked for it ([`PublishedContext`](crate::tool::PublishedContext)
321/// in the dispatch's scope); the result carries data only. A call to
322/// another tool than `bound`, when given, is refused, so key routing never
323/// runs a different tool than the bound target.
324pub(crate) async fn call_tool<A, O>(
325    kind: EffectKind,
326    dispatch: Dispatch,
327    bound: Option<&str>,
328    call: impl AsyncFnOnce(&mut crate::tool::ToolContext, A) -> Result<O, ToolExecutionError>,
329) -> Reply
330where
331    A: serde::de::DeserializeOwned,
332    O: crate::tool::IntoToolOutput,
333{
334    let args = match (kind, bound) {
335        (EffectKind::ToolCall { name, .. }, Some(bound)) if bound != name => {
336            return Reply::Outcome(Err(ErrorReport::new(
337                ErrorKind::Internal,
338                format!("tool handler `{bound}` asked to run `{name}`"),
339            )));
340        }
341        (EffectKind::ToolCall { args, .. }, _) => args,
342        (
343            other @ (EffectKind::Completion { .. }
344            | EffectKind::Embed { .. }
345            | EffectKind::Retrieve { .. }
346            | EffectKind::Rerank { .. }
347            | EffectKind::Custom { .. }),
348            _,
349        ) => return Reply::Outcome(Err(wrong_family(EffectFamily::Tool, &other))),
350    };
351    let result = run_tool(&args, &dispatch, call).await;
352    Reply::Outcome(Ok(Answered::ToolResult { result }))
353}
354
355/// The body of [`call_tool`] once the call is known to be the tool's: `args`
356/// parsed and handed to `call` in the dispatch's context, and what the tool
357/// published handed back.
358pub(crate) async fn run_tool<A, O>(
359    args: &str,
360    dispatch: &Dispatch,
361    call: impl AsyncFnOnce(&mut crate::tool::ToolContext, A) -> Result<O, ToolExecutionError>,
362) -> crate::tool::ToolResult
363where
364    A: serde::de::DeserializeOwned,
365    O: crate::tool::IntoToolOutput,
366{
367    let mut context = dispatch
368        .scope::<crate::tool::ToolContext>()
369        .map(|inbound| inbound.for_dispatch())
370        .unwrap_or_default()
371        .with_scopes(dispatch.scopes());
372    let result = run_in(args, &mut context, call).await;
373    if let Some(published) = dispatch.scope::<crate::tool::PublishedContext>() {
374        published.publish(context);
375    }
376    result
377}
378
379/// A direct call of a tool, outside any dispatch: [`run_in`] over the
380/// caller's inbound values (as `for_dispatch`), with what the tool published
381/// handed back into `context` (as `accept_dispatch_result`).
382pub(crate) async fn run_direct<A, O>(
383    context: &mut crate::tool::ToolContext,
384    args: serde_json::Value,
385    call: impl AsyncFnOnce(&mut crate::tool::ToolContext, A) -> Result<O, ToolExecutionError>,
386) -> Result<crate::tool::ToolOutput, ToolExecutionError>
387where
388    A: serde::de::DeserializeOwned,
389    O: crate::tool::IntoToolOutput,
390{
391    let mut dispatched = context.for_dispatch();
392    let result = run_in(&args.to_string(), &mut dispatched, call).await;
393    context.accept_dispatch_result(dispatched);
394    result.into_result()
395}
396
397/// Every tool call's body: `args` parsed and handed to `call` in `context`,
398/// and its output converted.
399async fn run_in<A, O>(
400    args: &str,
401    context: &mut crate::tool::ToolContext,
402    call: impl AsyncFnOnce(&mut crate::tool::ToolContext, A) -> Result<O, ToolExecutionError>,
403) -> crate::tool::ToolResult
404where
405    A: serde::de::DeserializeOwned,
406    O: crate::tool::IntoToolOutput,
407{
408    match crate::tool::contextual::parse_tool_args::<A>(args) {
409        Ok(args) => crate::tool::contextual::tool_result_from(call(context, args).await),
410        Err(error) => crate::tool::ToolResult::failed(error),
411    }
412}
413
414/// A [`Tool`] as a handler, keyed by its name.
415pub struct ToolAdapter<T> {
416    tool: T,
417    embedding: Option<ToolEmbeddingDescriptor>,
418}
419
420impl<T: Tool> ToolAdapter<T> {
421    /// Wrap a static tool.
422    pub fn new(tool: T) -> Self {
423        Self {
424            tool,
425            embedding: None,
426        }
427    }
428
429    /// Wraps a tool with embedding descriptions and serialized reconstruction
430    /// context. Returns an error if context serialization fails.
431    pub fn retrievable(tool: T) -> Result<Self, serde_json::Error>
432    where
433        T: ToolEmbedding,
434    {
435        let embedding = ToolEmbeddingDescriptor {
436            context: serde_json::to_value(tool.context())?,
437            embedding_docs: tool.embedding_docs(),
438        };
439        Ok(Self {
440            tool,
441            embedding: Some(embedding),
442        })
443    }
444
445    /// The wrapped tool.
446    pub fn tool(&self) -> &T {
447        &self.tool
448    }
449
450    /// The retrieval descriptor of a [`Self::retrievable`] adapter.
451    pub fn embedding(&self) -> Option<&ToolEmbeddingDescriptor> {
452        self.embedding.as_ref()
453    }
454
455    /// Runs the tool on JSON `args` with an empty [`ToolContext`], as
456    /// [`Self::run_with`] does.
457    ///
458    /// [`ToolContext`]: crate::tool::ToolContext
459    pub async fn run(
460        &self,
461        args: serde_json::Value,
462    ) -> Result<crate::tool::ToolOutput, ToolExecutionError> {
463        self.run_with(&mut crate::tool::ToolContext::new(), args)
464            .await
465    }
466
467    /// Runs the tool on JSON `args` as a dispatch of its call runs it:
468    /// arguments parsed, errors normalized by [`Tool::map_error`], output
469    /// converted. The tool sees `context`'s inbound values, and what it
470    /// publishes with `insert_result` lands in `context`. No registry or
471    /// agent is involved.
472    ///
473    /// ```
474    /// use rig_core::{serve::adapters::ToolAdapter, tool::{ContextValue, Tool, ToolContext}};
475    /// use serde_json::json;
476    ///
477    /// #[derive(serde::Serialize, serde::Deserialize)]
478    /// struct Calls(u32);
479    /// impl ContextValue for Calls { const KEY: &'static str = "calls"; }
480    ///
481    /// struct Double;
482    ///
483    /// impl Tool for Double {
484    ///     const NAME: &'static str = "double";
485    ///     type Args = i64;
486    ///     type Output = i64;
487    ///     type Error = rig_core::tool::ToolContextError;
488    ///
489    ///     fn description(&self) -> String {
490    ///         "Doubles a number.".to_string()
491    ///     }
492    ///
493    ///     fn parameters(&self) -> serde_json::Value {
494    ///         json!({ "type": "integer" })
495    ///     }
496    ///
497    ///     async fn call(&self, context: &mut ToolContext, n: i64) -> Result<i64, Self::Error> {
498    ///         context.insert_result(Calls(1))?;
499    ///         Ok(n * 2)
500    ///     }
501    /// }
502    ///
503    /// let double = ToolAdapter::new(Double);
504    /// let mut context = ToolContext::new();
505    /// let output = futures::executor::block_on(double.run_with(&mut context, json!(21)))?;
506    /// assert_eq!(output.as_json(), Some(&json!(42)));
507    /// assert_eq!(context.result::<Calls>()?.map(|calls| calls.0), Some(1));
508    /// assert!(futures::executor::block_on(double.run(json!("21"))).is_err());
509    /// # Ok::<(), Box<dyn std::error::Error>>(())
510    /// ```
511    pub async fn run_with(
512        &self,
513        context: &mut crate::tool::ToolContext,
514        args: serde_json::Value,
515    ) -> Result<crate::tool::ToolOutput, ToolExecutionError> {
516        run_direct(context, args, async |context, args| {
517            self.call(context, args).await
518        })
519        .await
520    }
521
522    /// The tool's call with its error normalized, as every run makes it.
523    async fn call(
524        &self,
525        context: &mut crate::tool::ToolContext,
526        args: T::Args,
527    ) -> Result<T::Output, ToolExecutionError> {
528        self.tool
529            .call(context, args)
530            .await
531            .map_err(|error| self.tool.map_error(error))
532    }
533}
534
535impl<T> Serve for ToolAdapter<T>
536where
537    T: Tool + 'static,
538{
539    type Family = family::Tool;
540
541    fn descriptor(&self) -> HandlerDescriptor {
542        HandlerDescriptor {
543            key: crate::effect::tool_key(T::NAME),
544            family: FamilyDescriptor::Tool {
545                name: T::NAME.to_owned(),
546                description: self.tool.description(),
547                parameters: self.tool.parameters(),
548                embedding: self.embedding.clone(),
549            },
550            layers: Vec::new(),
551        }
552    }
553
554    fn key(&self) -> HandlerKey {
555        crate::effect::tool_key(T::NAME)
556    }
557
558    fn footprint(&self, kind: &EffectKind) -> Option<crate::tool::Touches> {
559        match kind {
560            EffectKind::ToolCall { name, args } if name == T::NAME => {
561                let args = crate::tool::contextual::parse_tool_args::<T::Args>(args).ok()?;
562                Some(self.tool.footprint(&args))
563            }
564            _ => None,
565        }
566    }
567
568    async fn serve(&self, kind: EffectKind, dispatch: Dispatch) -> Reply {
569        call_tool(kind, dispatch, Some(T::NAME), async |context, args| {
570            self.call(context, args).await
571        })
572        .await
573    }
574}
575
576/// Contextual tool callback accepting JSON arguments and returning canonical
577/// output or a tool execution error.
578pub trait ToolCallback:
579    for<'a> Fn(
580        &'a mut crate::tool::ToolContext,
581        serde_json::Value,
582    ) -> WasmBoxedFuture<
583        'a,
584        Result<crate::tool::ToolOutput, crate::tool::ToolExecutionError>,
585    > + WasmCompatSend
586    + WasmCompatSync
587{
588}
589
590impl<F> ToolCallback for F where
591    F: for<'a> Fn(
592            &'a mut crate::tool::ToolContext,
593            serde_json::Value,
594        ) -> WasmBoxedFuture<
595            'a,
596            Result<crate::tool::ToolOutput, crate::tool::ToolExecutionError>,
597        > + WasmCompatSend
598        + WasmCompatSync
599{
600}
601
602/// A [`VectorStoreIndex`] as a handler. The index's filter type is rebuilt
603/// from the dynamic filter on the wire; documents come back as JSON, and the
604/// typed view deserialises on the client side.
605pub struct RetrieveAdapter<I> {
606    index: I,
607    label: Option<String>,
608}
609
610impl<I> RetrieveAdapter<I> {
611    /// Wrap `index` as an agent's own index, under the bare `retrieve` key.
612    pub fn new(index: I) -> Self {
613        Self { index, label: None }
614    }
615
616    /// Wrap `index` under `retrieve:<label>`, so a host can serve several
617    /// indexes in one table and dispatch to each by
618    /// [`retrieve_key`](crate::effect::retrieve_key).
619    pub fn labelled(label: impl Into<String>, index: I) -> Self {
620        Self {
621            index,
622            label: Some(label.into()),
623        }
624    }
625
626    /// The wrapped index.
627    pub fn index(&self) -> &I {
628        &self.index
629    }
630}
631
632impl<I, F> Serve for RetrieveAdapter<I>
633where
634    I: VectorStoreIndex<Filter = F> + 'static,
635    F: DynamicSearchFilter + WasmCompatSend + WasmCompatSync + 'static,
636{
637    type Family = family::Retrieve;
638
639    fn descriptor(&self) -> HandlerDescriptor {
640        HandlerDescriptor {
641            key: self
642                .label
643                .as_deref()
644                .map_or_else(|| HandlerKey::from("retrieve"), crate::effect::retrieve_key),
645            family: FamilyDescriptor::Retrieve {},
646            layers: Vec::new(),
647        }
648    }
649
650    async fn serve(&self, kind: EffectKind, _dispatch: Dispatch) -> Reply {
651        match kind {
652            EffectKind::Retrieve { query } => {
653                let outcome = match query {
654                    RetrieveQuery::TopN { req } => {
655                        match req.try_map_filter(F::from_dynamic_filter) {
656                            Ok(req) => self
657                                .index
658                                .top_n::<serde_json::Value>(req)
659                                .await
660                                .map(|results| {
661                                    Answered::Documents(RetrievedDocuments::Scored(
662                                        results
663                                            .into_iter()
664                                            .map(|result| {
665                                                (
666                                                    result.score,
667                                                    result.id,
668                                                    F::normalize_dynamic_document(result.document),
669                                                )
670                                            })
671                                            .collect(),
672                                    ))
673                                })
674                                .map_err(ErrorReport::from),
675                            Err(error) => Err(ErrorReport::from(VectorStoreError::from(error))),
676                        }
677                    }
678                    RetrieveQuery::TopNIds { req } => {
679                        match req.try_map_filter(F::from_dynamic_filter) {
680                            Ok(req) => self
681                                .index
682                                .top_n_ids(req)
683                                .await
684                                .map(|results| {
685                                    Answered::Documents(RetrievedDocuments::Ids(
686                                        results
687                                            .into_iter()
688                                            .map(|result| (result.score, result.id))
689                                            .collect(),
690                                    ))
691                                })
692                                .map_err(ErrorReport::from),
693                            Err(error) => Err(ErrorReport::from(VectorStoreError::from(error))),
694                        }
695                    }
696                };
697                Reply::Outcome(outcome)
698            }
699            other @ (EffectKind::Completion { .. }
700            | EffectKind::ToolCall { .. }
701            | EffectKind::Embed { .. }
702            | EffectKind::Rerank { .. }
703            | EffectKind::Custom { .. }) => {
704                Reply::Outcome(Err(wrong_family(EffectFamily::Retrieve, &other)))
705            }
706        }
707    }
708}