Skip to main content

rig_core/effect/
mod.rs

1//! Serializable effect requests, handler descriptions, outcomes, and recording data.
2//! [`HandlerKey`] identifies a destination; [`EffectKind`] and [`Answered`] describe
3//! the exchange. Host-defined operations use [`CustomEffect`].
4//!
5//! ```
6//! use rig_core::effect::{EffectFamily, EffectRow, model_key};
7//!
8//! let mut row = EffectRow::new();
9//! row.insert(model_key("primary"), EffectFamily::Completion);
10//! assert_eq!(row.len(), 1);
11//! ```
12
13use std::{borrow::Cow, fmt, sync::Arc};
14
15use serde::{Deserialize, Serialize};
16
17use crate::{
18    completion::{CompletionRequest, CompletionResponse, ModelRef, ProviderCapabilities},
19    embeddings::EmbeddingResponse,
20    error::ErrorReport,
21    operation::RerankRequest,
22    rerank::RerankResponse,
23    streaming::StreamRecord,
24    tool::ToolResult,
25    vector_store::request::{Filter, VectorSearchRequest},
26    wasm_compat::WasmCompatSend,
27};
28
29/// The identity of one dispatch, minted by the dispatcher.
30///
31/// It links an [`EffectRecord`] to a host's own bookkeeping (a host driver
32/// maps it to its own handle). Ids are unique per dispatcher and strictly increasing.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
34#[serde(transparent)]
35pub struct EffectId(u64);
36
37impl EffectId {
38    /// Build an id from its raw value (a replayer restoring a log does this).
39    pub const fn from_raw(raw: u64) -> Self {
40        Self(raw)
41    }
42
43    /// The raw value.
44    pub const fn as_u64(self) -> u64 {
45        self.0
46    }
47}
48
49impl fmt::Display for EffectId {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(f, "effect:{}", self.0)
52    }
53}
54
55/// String identifier for a registered handler, serializable without a live handle.
56#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
57#[serde(transparent)]
58pub struct HandlerKey(Arc<str>);
59
60impl HandlerKey {
61    /// Build a key from any string-like value.
62    pub fn new(key: impl Into<Arc<str>>) -> Self {
63        Self(key.into())
64    }
65
66    /// The key as a string slice.
67    pub fn as_str(&self) -> &str {
68        &self.0
69    }
70
71    /// Parses the key into [`KeyParts`] without failure. Formatting the parts
72    /// reproduces the original key.
73    pub fn parts(&self) -> KeyParts {
74        KeyParts::parse(&self.0)
75    }
76}
77
78/// A key's parts, by the grammar every key the tree mints follows:
79///
80/// ```text
81/// [<owner>/]<name>[#<generation>]     where <name> is [<kind>:]<label>
82/// ```
83///
84/// `golden/tool:add#2` is owner `golden`, kind `tool`, label `add`,
85/// generation `2`; `host/note` is owner `host`, no kind, label `note`;
86/// `model:fast` is no owner, kind `model`, label `fast`. The owner is the
87/// text before the first `/`, the kind the text before the first `:` of
88/// what follows, the generation the digits after the last `#`; a label
89/// holds anything else, so a key whose label contains one of those
90/// separators is read as its grammar says, not as its author meant. The
91/// string is the wire form; this is how a reader takes it apart without
92/// a hand parser of its own.
93#[derive(Debug, Clone, PartialEq, Eq, Hash)]
94pub struct KeyParts {
95    /// The program or host the key belongs to (`golden`, `host`).
96    pub owner: Option<Arc<str>>,
97    /// The family-shaped prefix of the name (`model`, `tool`, `retrieve`).
98    pub kind: Option<Arc<str>>,
99    /// The name proper: a model label, a tool name, `retrieve`, `note`.
100    pub label: Arc<str>,
101    /// The disambiguating generation (`#2`): the same tool registered
102    /// again under a fresh key. Only canonical `u64` decimal suffixes are
103    /// generations; leading zeroes and overflow remain literal label text.
104    pub generation: Option<u64>,
105}
106
107impl KeyParts {
108    /// Read `key` by the grammar.
109    pub fn parse(key: &str) -> Self {
110        let (owner, rest) = match key.split_once('/') {
111            Some((owner, rest)) if !owner.is_empty() && !rest.is_empty() => {
112                (Some(Arc::from(owner)), rest)
113            }
114            _ => (None, key),
115        };
116        let (rest, generation) = match rest.rsplit_once('#') {
117            Some((head, digits))
118                if !digits.is_empty()
119                    && (digits.len() == 1 || !digits.starts_with('0'))
120                    && digits.bytes().all(|b| b.is_ascii_digit()) =>
121            {
122                match digits.parse::<u64>() {
123                    Ok(generation) => (head, Some(generation)),
124                    Err(_) => (rest, None),
125                }
126            }
127            _ => (rest, None),
128        };
129        let (kind, label) = match rest.split_once(':') {
130            Some((kind, label)) if !kind.is_empty() && !label.is_empty() => {
131                (Some(Arc::from(kind)), label)
132            }
133            _ => (None, rest),
134        };
135        Self {
136            owner,
137            kind,
138            label: Arc::from(label),
139            generation,
140        }
141    }
142}
143
144impl fmt::Display for KeyParts {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        if let Some(owner) = &self.owner {
147            write!(f, "{owner}/")?;
148        }
149        if let Some(kind) = &self.kind {
150            write!(f, "{kind}:")?;
151        }
152        f.write_str(&self.label)?;
153        if let Some(generation) = self.generation {
154            write!(f, "#{generation}")?;
155        }
156        Ok(())
157    }
158}
159
160impl fmt::Debug for HandlerKey {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        write!(f, "HandlerKey({:?})", &*self.0)
163    }
164}
165
166impl fmt::Display for HandlerKey {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        f.write_str(&self.0)
169    }
170}
171
172impl From<&str> for HandlerKey {
173    fn from(key: &str) -> Self {
174        Self(Arc::from(key))
175    }
176}
177
178impl From<String> for HandlerKey {
179    fn from(key: String) -> Self {
180        Self(Arc::from(key))
181    }
182}
183
184impl AsRef<str> for HandlerKey {
185    fn as_ref(&self) -> &str {
186        &self.0
187    }
188}
189
190/// Effect classification used by typed binding and recording.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
192#[serde(rename_all = "snake_case")]
193pub enum EffectFamily {
194    /// A completion request to a model.
195    Completion,
196    /// A tool call.
197    Tool,
198    /// A text or image embedding request.
199    Embed,
200    /// A reranking request.
201    Rerank,
202    /// A vector-store retrieval.
203    Retrieve,
204    /// An out-of-tree kind.
205    Custom,
206}
207
208impl EffectFamily {
209    /// The family's stable label.
210    pub const fn name(self) -> &'static str {
211        match self {
212            Self::Completion => "completion",
213            Self::Tool => "tool_call",
214            Self::Embed => "embed",
215            Self::Rerank => "rerank",
216            Self::Retrieve => "retrieve",
217            Self::Custom => "custom",
218        }
219    }
220}
221
222impl fmt::Display for EffectFamily {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        f.write_str(self.name())
225    }
226}
227
228/// Sealed mapping between typed requests and answers and the wire's
229/// [`EffectKind`] and [`Answered`]. Hosts extend the protocol through
230/// [`CustomEffect`] and [`family::Custom`], not new family implementations.
231pub trait Family: sealed::Sealed + Clone + Copy + Send + Sync + 'static {
232    /// The family this marker names.
233    const FAMILY: EffectFamily;
234    /// What a typed dispatch of this family takes.
235    type Request: WasmCompatSend + 'static;
236    /// What it resolves to.
237    type Answer: WasmCompatSend + 'static;
238    /// The wire form of a request, or the report for a request that has
239    /// none (a [`CustomEffect`] whose `Serialize` fails). The in-tree
240    /// families always have one; a typed dispatch of a request without one
241    /// is pre-failed and never reaches a handler or a log.
242    fn wrap(request: Self::Request) -> Result<EffectKind, ErrorReport>;
243    /// The typed answer, or the report for an outcome of another family.
244    fn unwrap(outcome: Answered) -> Result<Self::Answer, ErrorReport>;
245    /// The report [`Family::unwrap`] gives for an outcome of another family.
246    fn mismatch(outcome: &Answered) -> ErrorReport {
247        ErrorReport::new(
248            crate::error::ErrorKind::Internal,
249            format!(
250                "expected a {} outcome, the handler answered {}",
251                Self::FAMILY,
252                outcome.family()
253            ),
254        )
255    }
256}
257
258mod sealed {
259    pub trait Sealed {}
260}
261
262/// Sealed handler-family declaration. Typed families expose `Some(F::FAMILY)`;
263/// [`family::Dynamic`] exposes `None` and requires runtime family checks.
264pub trait Served: sealed::Sealed + 'static {
265    /// The family, when the handler has one.
266    const SERVED: Option<EffectFamily>;
267}
268
269// `Served` is sealed; a handler's family is a `Family` marker or `Dynamic`,
270// and an error about it should say so rather than suggest this impl.
271#[diagnostic::do_not_recommend]
272impl<F: Family> Served for F {
273    const SERVED: Option<EffectFamily> = Some(F::FAMILY);
274}
275
276/// A tool call as a typed request: the name and the raw JSON arguments.
277/// The context the tool runs with is not part of the request: it travels
278/// beside the dispatch, attached by the driver to the handler's sink
279/// (`Dispatch::scope::<ToolContext>()`), and what the tool publishes
280/// comes back the same way ([`crate::tool::PublishedContext`]).
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
282pub struct ToolCallRequest {
283    /// The tool's name (the name the model calls it by).
284    pub name: String,
285    /// The arguments as a JSON string.
286    pub args: String,
287}
288
289/// A host's own effect, typed the way a [`ToolContext`](crate::tool::ToolContext) value is: a
290/// declared kind label and a declared answer type, both serde. The wire
291/// form is [`EffectKind::Custom`] / [`Answered::Custom`]; the type never
292/// crosses it.
293pub trait CustomEffect: Serialize + serde::de::DeserializeOwned + WasmCompatSend + 'static {
294    /// The kind label this effect dispatches under; a handler's
295    /// [`FamilyDescriptor::Custom`] must name the same label.
296    const KIND: &'static str;
297    /// What the handler answers.
298    type Answer: Serialize + serde::de::DeserializeOwned + WasmCompatSend + 'static;
299}
300
301mod key;
302
303pub use key::Key;
304
305/// A key for a model handler: `model:<label>`.
306pub fn model_key(label: &str) -> HandlerKey {
307    HandlerKey::from(format!("model:{label}"))
308}
309
310/// A key for a tool handler: `tool:<name>`.
311pub fn tool_key(name: &str) -> HandlerKey {
312    HandlerKey::from(format!("tool:{name}"))
313}
314
315/// A key for an embedding handler: `embed:<label>`.
316pub fn embed_key(label: &str) -> HandlerKey {
317    HandlerKey::from(format!("embed:{label}"))
318}
319
320/// A key for a rerank handler: `rerank:<label>`.
321pub fn rerank_key(label: &str) -> HandlerKey {
322    HandlerKey::from(format!("rerank:{label}"))
323}
324
325/// A key for a labelled retrieval handler: `retrieve:<label>`. An agent's
326/// own index is registered under the bare `retrieve` key.
327pub fn retrieve_key(label: &str) -> HandlerKey {
328    HandlerKey::from(format!("retrieve:{label}"))
329}
330
331/// Type-level markers for built-in and host-defined effect families.
332///
333/// ```
334/// use rig_core::effect::{family, Family, EffectFamily};
335/// assert_eq!(family::Completion::FAMILY, EffectFamily::Completion);
336/// ```
337pub mod family {
338    use std::marker::PhantomData;
339
340    use super::{
341        Answered, CustomEffect, EffectFamily, EffectKind, EmbedInputs, EmbedOutputs, Family,
342        RetrieveQuery, RetrievedDocuments, ToolCallRequest, sealed::Sealed,
343    };
344    use crate::{
345        completion::{CompletionRequest, CompletionResponse},
346        error::{ErrorKind, ErrorReport},
347        operation::RerankRequest,
348        rerank::RerankResponse,
349        tool::ToolResult,
350    };
351
352    macro_rules! marker {
353        ($($(#[$doc:meta])* $name:ident => $family:ident, $request:ty, $answer:ty, $wrap:expr, $unwrap:expr;)+) => {$(
354            $(#[$doc])*
355            #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
356            pub struct $name;
357
358            impl Sealed for $name {}
359
360            impl Family for $name {
361                const FAMILY: EffectFamily = EffectFamily::$family;
362                type Request = $request;
363                type Answer = $answer;
364
365                fn wrap(request: Self::Request) -> Result<EffectKind, ErrorReport> {
366                    let wrap: fn(Self::Request) -> EffectKind = $wrap;
367                    Ok(wrap(request))
368                }
369
370                fn unwrap(outcome: Answered) -> Result<Self::Answer, ErrorReport> {
371                    let unwrap: fn(Answered) -> Result<Self::Answer, ErrorReport> = $unwrap;
372                    unwrap(outcome)
373                }
374            }
375        )+};
376    }
377
378    marker! {
379        /// The completion family: a unary completion (a streaming dispatch is
380        /// `ModelHandle::stream`, not a typed request).
381        Completion => Completion, CompletionRequest, CompletionResponse,
382            |request| EffectKind::Completion { request: std::sync::Arc::new(request), stream: false },
383            |outcome| match outcome {
384                Answered::Completion(response) => Ok(std::sync::Arc::unwrap_or_clone(response)),
385                other => Err(Completion::mismatch(&other)),
386            };
387        /// The tool family.
388        Tool => Tool, ToolCallRequest, ToolResult,
389            |request| EffectKind::ToolCall { name: request.name, args: request.args },
390            |outcome| match outcome {
391                Answered::ToolResult { result } => Ok(result),
392                other => Err(Tool::mismatch(&other)),
393            };
394        /// The embedding family.
395        Embed => Embed, EmbedInputs, EmbedOutputs,
396            |inputs| EffectKind::Embed { inputs },
397            |outcome| match outcome {
398                Answered::Embeddings(outputs) => Ok(outputs),
399                other => Err(Embed::mismatch(&other)),
400            };
401        /// The reranking family.
402        Rerank => Rerank, RerankRequest, RerankResponse,
403            |request| EffectKind::Rerank { request },
404            |outcome| match outcome {
405                Answered::Reranked(response) => Ok(response),
406                other => Err(Rerank::mismatch(&other)),
407            };
408        /// The retrieval family.
409        Retrieve => Retrieve, RetrieveQuery, RetrievedDocuments,
410            |query| EffectKind::Retrieve { query },
411            |outcome| match outcome {
412                Answered::Documents(documents) => Ok(documents),
413                other => Err(Retrieve::mismatch(&other)),
414            };
415    }
416
417    /// A handler with no one family: a replayer answering whatever its log
418    /// holds, or an erased handler forwarding to whatever it wraps.
419    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
420    pub struct Dynamic;
421
422    impl Sealed for Dynamic {}
423
424    impl super::Served for Dynamic {
425        const SERVED: Option<EffectFamily> = None;
426    }
427
428    /// The family of one host-defined effect `E`: dispatches
429    /// [`EffectKind::Custom`] under `E::KIND` and answers `E::Answer`.
430    pub struct Custom<E: CustomEffect>(PhantomData<fn() -> E>);
431
432    impl<E: CustomEffect> Custom<E> {
433        /// The marker.
434        pub const fn new() -> Self {
435            Self(PhantomData)
436        }
437    }
438
439    // Written by hand: a derive would demand `E: Clone` (and friends), and
440    // the marker must be `Copy` for every `E`.
441    impl<E: CustomEffect> Clone for Custom<E> {
442        fn clone(&self) -> Self {
443            *self
444        }
445    }
446    impl<E: CustomEffect> Copy for Custom<E> {}
447    impl<E: CustomEffect> Default for Custom<E> {
448        fn default() -> Self {
449            Self::new()
450        }
451    }
452    impl<E: CustomEffect> std::fmt::Debug for Custom<E> {
453        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454            write!(f, "Custom<{}>", E::KIND)
455        }
456    }
457    impl<E: CustomEffect> PartialEq for Custom<E> {
458        fn eq(&self, _: &Self) -> bool {
459            true
460        }
461    }
462    impl<E: CustomEffect> Eq for Custom<E> {}
463    impl<E: CustomEffect> std::hash::Hash for Custom<E> {
464        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
465            E::KIND.hash(state);
466        }
467    }
468
469    impl<E: CustomEffect> Sealed for Custom<E> {}
470
471    impl<E: CustomEffect> Family for Custom<E> {
472        const FAMILY: EffectFamily = EffectFamily::Custom;
473        type Request = E;
474        type Answer = E::Answer;
475
476        fn wrap(request: E) -> Result<EffectKind, ErrorReport> {
477            // Reject unencodable requests before dispatch so no handler or log
478            // receives an effect without a wire representation.
479            serde_json::to_value(&request)
480                .map(|payload| EffectKind::Custom {
481                    kind: std::sync::Arc::from(E::KIND),
482                    payload,
483                })
484                .map_err(|error| {
485                    ErrorReport::new(
486                        ErrorKind::Request,
487                        format!("the `{}` effect did not serialize: {error}", E::KIND),
488                    )
489                })
490        }
491
492        fn unwrap(outcome: Answered) -> Result<E::Answer, ErrorReport> {
493            match outcome {
494                Answered::Custom { payload: value } => {
495                    serde_json::from_value(value).map_err(|error| {
496                        ErrorReport::new(
497                            ErrorKind::Internal,
498                            format!(
499                                "the answer to the `{}` effect did not deserialize: {error}",
500                                E::KIND
501                            ),
502                        )
503                    })
504                }
505                other => Err(Self::mismatch(&other)),
506            }
507        }
508    }
509}
510
511/// What a registered handler is: its key and its family-specific description.
512#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
513pub struct HandlerDescriptor {
514    /// The key the handler is registered under.
515    pub key: HandlerKey,
516    /// The family and its advertised metadata.
517    pub family: FamilyDescriptor,
518    /// Interceptor names in outermost-first order, used to validate the layer
519    /// stack during replay. Empty for a handler without layers.
520    #[serde(default, skip_serializing_if = "Vec::is_empty")]
521    pub layers: Vec<String>,
522}
523
524impl HandlerDescriptor {
525    /// A tool's handler, under [`tool_key`] of its `name`, with no layers
526    /// and no embedding.
527    pub fn tool(name: &str, description: impl Into<String>, parameters: serde_json::Value) -> Self {
528        Self {
529            key: tool_key(name),
530            family: FamilyDescriptor::Tool {
531                name: name.to_owned(),
532                description: description.into(),
533                parameters,
534                embedding: None,
535            },
536            layers: Vec::new(),
537        }
538    }
539}
540
541/// The family-keyed description of a handler. The variant *is* the family:
542/// binding a typed view compares [`Family::FAMILY`] against
543/// [`FamilyDescriptor::family`].
544#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
545#[serde(tag = "family", rename_all = "snake_case")]
546pub enum FamilyDescriptor {
547    /// A completion model.
548    Completion {
549        /// The model's label.
550        model: ModelRef,
551        /// The capability snapshot a runtime prepares requests against.
552        capabilities: ProviderCapabilities,
553    },
554    /// A tool.
555    Tool {
556        /// The tool's name (the name the model calls it by).
557        name: String,
558        /// The tool's description.
559        description: String,
560        /// The JSON schema of the tool's arguments.
561        parameters: serde_json::Value,
562        /// Present when the tool is retrievable by embedding.
563        #[serde(default, skip_serializing_if = "Option::is_none")]
564        embedding: Option<ToolEmbeddingDescriptor>,
565    },
566    /// An embedding model.
567    Embed {
568        /// The model's label.
569        model: String,
570        /// The vector dimension, when the model declares one.
571        #[serde(default, skip_serializing_if = "Option::is_none")]
572        dims: Option<usize>,
573        /// The largest batch the model accepts.
574        max_documents: usize,
575        /// Whether the model embeds text or images.
576        modality: EmbedModality,
577    },
578    /// A reranking model.
579    Rerank {
580        /// The model's label.
581        model: String,
582        /// The largest batch the model accepts.
583        max_documents: usize,
584    },
585    /// A vector-store index.
586    Retrieve {},
587    /// A handler for an out-of-tree kind: the label it serves.
588    Custom {
589        /// The [`EffectKind::Custom`] kind label this handler answers.
590        kind: String,
591    },
592}
593
594impl FamilyDescriptor {
595    /// The family this descriptor belongs to.
596    pub const fn family(&self) -> EffectFamily {
597        match self {
598            Self::Completion { .. } => EffectFamily::Completion,
599            Self::Tool { .. } => EffectFamily::Tool,
600            Self::Embed { .. } => EffectFamily::Embed,
601            Self::Rerank { .. } => EffectFamily::Rerank,
602            Self::Retrieve {} => EffectFamily::Retrieve,
603            Self::Custom { .. } => EffectFamily::Custom,
604        }
605    }
606}
607
608/// The embedding context of a retrievable tool: what
609/// [`ToolEmbedding`](crate::tool::ToolEmbedding) advertises, as data.
610#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
611pub struct ToolEmbeddingDescriptor {
612    /// The tool's serialized context.
613    pub context: serde_json::Value,
614    /// The documents the tool is retrieved by.
615    pub embedding_docs: Vec<String>,
616}
617
618/// Which modality an embedding handler serves.
619#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
620#[serde(rename_all = "snake_case")]
621pub enum EmbedModality {
622    /// Text documents.
623    Text,
624    /// Image bytes.
625    Image,
626}
627
628/// One effect: what a handler is asked to do.
629#[derive(Debug, Clone, Serialize, Deserialize)]
630#[serde(tag = "effect", rename_all = "snake_case")]
631pub enum EffectKind {
632    /// A completion request.
633    Completion {
634        /// The prepared request, shared with the recorder and the log.
635        request: Arc<CompletionRequest>,
636        /// Whether the response streams (`dispatch_stream`) or is unary.
637        stream: bool,
638    },
639    /// A tool call. The context the tool runs with is not on the wire:
640    /// see [`ToolCallRequest`].
641    ToolCall {
642        /// The tool's name.
643        name: String,
644        /// The raw JSON arguments as the model produced them.
645        args: String,
646    },
647    /// An embedding request.
648    Embed {
649        /// The inputs to embed.
650        inputs: EmbedInputs,
651    },
652    /// A reranking request.
653    Rerank {
654        /// The query and the documents.
655        request: RerankRequest,
656    },
657    /// A retrieval.
658    Retrieve {
659        /// The query.
660        query: RetrieveQuery,
661    },
662    /// An out-of-tree effect.
663    Custom {
664        /// The host-defined kind label.
665        kind: Arc<str>,
666        /// The host-defined payload.
667        payload: serde_json::Value,
668    },
669}
670
671/// The effect a model's tool call is: the tool's name and the call's raw
672/// arguments, as a tool handler is dispatched it.
673///
674/// ```
675/// use rig_core::effect::EffectKind;
676/// use rig_core::message::{ToolCall, ToolFunction, ToolName};
677///
678/// let read = ToolFunction::new(ToolName::new("read")?, serde_json::json!({ "path": "a.rs" }));
679/// let kind = EffectKind::from(&ToolCall::from_wire("call-1", read));
680/// assert!(matches!(kind, EffectKind::ToolCall { ref name, .. } if name == "read"));
681/// # Ok::<(), Box<dyn std::error::Error>>(())
682/// ```
683impl From<&crate::message::ToolCall> for EffectKind {
684    fn from(call: &crate::message::ToolCall) -> Self {
685        Self::ToolCall {
686            name: call.function.name.as_str().to_owned(),
687            args: call.function.raw_arguments(),
688        }
689    }
690}
691
692impl EffectKind {
693    /// The family of this effect.
694    pub const fn family(&self) -> EffectFamily {
695        match self {
696            Self::Completion { .. } => EffectFamily::Completion,
697            Self::ToolCall { .. } => EffectFamily::Tool,
698            Self::Embed { .. } => EffectFamily::Embed,
699            Self::Rerank { .. } => EffectFamily::Rerank,
700            Self::Retrieve { .. } => EffectFamily::Retrieve,
701            Self::Custom { .. } => EffectFamily::Custom,
702        }
703    }
704
705    /// Classification label without payload data. Custom effects return their
706    /// host-defined kind label.
707    pub fn name(&self) -> &str {
708        match self {
709            Self::Custom { kind, .. } => kind,
710            Self::Completion { .. }
711            | Self::ToolCall { .. }
712            | Self::Embed { .. }
713            | Self::Rerank { .. }
714            | Self::Retrieve { .. } => self.family().name(),
715        }
716    }
717
718    /// Whether this effect answers as a stream. Only a streaming completion
719    /// does; every other kind is unary.
720    pub const fn streams(&self) -> bool {
721        match self {
722            Self::Completion { stream, .. } => *stream,
723            Self::ToolCall { .. }
724            | Self::Embed { .. }
725            | Self::Rerank { .. }
726            | Self::Retrieve { .. }
727            | Self::Custom { .. } => false,
728        }
729    }
730}
731
732/// The inputs of an embedding request: the texts or images an embedding
733/// model's call embeds.
734#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
735#[serde(tag = "modality", content = "inputs", rename_all = "snake_case")]
736pub enum EmbedInputs {
737    /// Text documents.
738    Texts(Vec<String>),
739    /// Image bytes.
740    Images(Vec<Vec<u8>>),
741}
742
743/// A retrieval: the transcription of
744/// [`VectorStoreIndex`](crate::vector_store::VectorStoreIndex) over the
745/// dynamic filter. The typed result parameter of `top_n<T>` stays on the
746/// client side.
747#[derive(Debug, Clone, Serialize, Deserialize)]
748#[serde(tag = "query", rename_all = "snake_case")]
749pub enum RetrieveQuery {
750    /// Scored documents.
751    TopN {
752        /// The search.
753        req: VectorSearchRequest<Filter<serde_json::Value>>,
754    },
755    /// Scored ids only.
756    TopNIds {
757        /// The search.
758        req: VectorSearchRequest<Filter<serde_json::Value>>,
759    },
760}
761
762/// What a handler answered.
763#[derive(Debug, Clone, Serialize, Deserialize)]
764#[serde(tag = "outcome", rename_all = "snake_case")]
765pub enum Answered {
766    /// A unary completion, shared with the records and layers that read it.
767    Completion(Arc<CompletionResponse>),
768    /// A tool call's result. The values the tool published into its
769    /// context are not on the wire: they come back beside the sink
770    /// ([`crate::tool::PublishedContext`]).
771    ToolResult {
772        /// The result.
773        result: ToolResult,
774    },
775    /// Embeddings.
776    Embeddings(EmbedOutputs),
777    /// A reranking.
778    Reranked(RerankResponse),
779    /// Retrieved documents.
780    Documents(RetrievedDocuments),
781    /// An out-of-tree answer.
782    Custom {
783        /// The answer, nested so every JSON shape is representable.
784        payload: serde_json::Value,
785    },
786}
787
788impl Answered {
789    /// The family this outcome answers.
790    pub const fn family(&self) -> EffectFamily {
791        match self {
792            Self::Completion(_) => EffectFamily::Completion,
793            Self::ToolResult { .. } => EffectFamily::Tool,
794            Self::Embeddings(_) => EffectFamily::Embed,
795            Self::Reranked(_) => EffectFamily::Rerank,
796            Self::Documents(_) => EffectFamily::Retrieve,
797            Self::Custom { .. } => EffectFamily::Custom,
798        }
799    }
800}
801
802/// The answer to an [`EmbedInputs`].
803#[derive(Debug, Clone, Serialize, Deserialize)]
804#[serde(tag = "modality", content = "response", rename_all = "snake_case")]
805pub enum EmbedOutputs {
806    /// Text embeddings.
807    Texts(EmbeddingResponse),
808    /// Image embeddings.
809    Images(EmbeddingResponse),
810}
811
812/// The answer to a [`RetrieveQuery`].
813#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
814#[serde(tag = "retrieved", content = "results", rename_all = "snake_case")]
815pub enum RetrievedDocuments {
816    /// Scored documents: `(score, id, document)`.
817    Scored(Vec<(f64, String, serde_json::Value)>),
818    /// Scored ids: `(score, id)`.
819    Ids(Vec<(f64, String)>),
820}
821
822/// One recorded exchange: the effect, who served it, and the answer.
823#[derive(Debug, Clone, Serialize, Deserialize)]
824pub struct EffectRecord {
825    /// Explicitly published result metadata, including on errors: a
826    /// tool's, or the facts any layer of the dispatch published
827    /// ([`PublishedContext`](crate::tool::PublishedContext)). Never
828    /// inbound authentication values or runtime scopes. `None` means nothing
829    /// was published before resolving; the wire field is required, and
830    /// `null` records that nothing was published.
831    #[serde(rename = "tool_output", deserialize_with = "Option::deserialize")]
832    pub published: Option<crate::tool::ToolResultContext>,
833    /// The dispatch's id.
834    pub id: EffectId,
835    /// The handler the effect was routed to.
836    pub key: HandlerKey,
837    /// The effect.
838    pub kind: EffectKind,
839    /// The answer.
840    pub outcome: Result<Answered, ErrorReport>,
841    /// A streamed dispatch's items, verbatim, when the recorder was asked
842    /// to keep them (`EffectLogRecorder::keeping_stream_events`); `None`
843    /// otherwise, and the answer is the fold. A replayer re-emits these
844    /// when present, so a replayed consumer sees the original fragment
845    /// boundaries. They read back through [`StreamRecord::parse_prefix`].
846    #[serde(default, skip_serializing_if = "Option::is_none")]
847    pub events: Option<StreamRecord>,
848    /// Who a streamed dispatch's reply was from, as its relay stated it
849    /// before the first item, when the items are kept. A replayer sends it
850    /// first, so a replayed stream cut short knows its origin.
851    #[serde(default, skip_serializing_if = "Option::is_none")]
852    pub stream_origin: Option<crate::message::Origin>,
853    /// Parent dispatch for a nested handler call, or `None` for a root dispatch.
854    #[serde(default, skip_serializing_if = "Option::is_none")]
855    pub parent: Option<EffectId>,
856    /// Stable identifier of the dispatching program or run, not a runtime handle.
857    /// `None` when the dispatcher supplied no scope.
858    #[serde(default, skip_serializing_if = "Option::is_none")]
859    pub scope: Option<std::sync::Arc<str>>,
860    /// What the dispatcher made the effect for, such as `"turn"` or
861    /// `"compaction"`, from its [`DispatchContext`](crate::serve::DispatchContext). `None` when
862    /// it named no purpose.
863    #[serde(default, skip_serializing_if = "Option::is_none")]
864    pub purpose: Option<std::borrow::Cow<'static, str>>,
865    /// The turn of its dispatcher's conversation it was made in, as the
866    /// host numbers turns (the first is `0`). `None` when the host keeps no
867    /// turns or it belongs to none, such as a call made between turns.
868    #[serde(default, skip_serializing_if = "Option::is_none")]
869    pub turn: Option<u64>,
870    /// When a handler started serving it, in milliseconds since the Unix
871    /// epoch. `None` when no handler started it (it was answered before it
872    /// started), or when the recorder keeps no times.
873    #[serde(default, skip_serializing_if = "Option::is_none")]
874    pub started_ms: Option<u64>,
875    /// When its outcome was known, in milliseconds since the Unix epoch.
876    /// `None` when the recorder keeps no times.
877    #[serde(default, skip_serializing_if = "Option::is_none")]
878    pub finished_ms: Option<u64>,
879    /// The outcome a reviewer put in place of [`Self::outcome`], which stays
880    /// the handler's (or the early answer's) own. `None` when nothing
881    /// replaced it. A replayer answers with [`Self::outcome`] and lets the
882    /// reviewers run again, as it reruns layers.
883    #[serde(default, skip_serializing_if = "Option::is_none")]
884    pub reviewed: Option<Result<Answered, ErrorReport>>,
885    /// How the outcome came about.
886    #[serde(default, skip_serializing_if = "Disposition::is_ran")]
887    pub disposition: Disposition,
888    /// Who decided the answer before it ran, when a decision answered it.
889    /// `None` when its handler or its owner answered it: an effect no gate
890    /// decided ran, and an allow is not a decision.
891    #[serde(default, skip_serializing_if = "Option::is_none")]
892    pub decided_by: Option<Decider>,
893}
894
895/// Who decided an effect's answer before it ran ([`EffectRecord::decided_by`]).
896#[non_exhaustive]
897#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
898#[serde(rename_all = "snake_case")]
899pub enum Decider {
900    /// A person, through a front: an answer to a question, a line they typed.
901    Person,
902    /// A plugin, by its name, such as `print` or `protected-paths`.
903    Plugin(Cow<'static, str>),
904}
905
906impl Decider {
907    /// The plugin called `name`.
908    pub fn plugin(name: impl Into<Cow<'static, str>>) -> Self {
909        Self::Plugin(name.into())
910    }
911}
912
913/// How an [`EffectRecord`]'s outcome came about. A recorder of handler
914/// dispatches records [`Ran`](Self::Ran); a host that answers, replaces or
915/// cancels effects itself records the others.
916#[non_exhaustive]
917#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
918#[serde(rename_all = "snake_case")]
919pub enum Disposition {
920    /// A handler served it, and the outcome is its answer.
921    #[default]
922    Ran,
923    /// Answered before any handler started it, such as by a policy that
924    /// refused it or a replayer.
925    AnsweredBeforeStart,
926    /// Answered from outside while its handler ran; the handler was
927    /// cancelled.
928    AnsweredWhileRunning,
929    /// Another effect replaced it, and its outcome was never applied.
930    Superseded,
931    /// Cancelled before it had an outcome.
932    Cancelled,
933}
934
935impl EffectRecord {
936    /// The record of effect `id`, `kind`, routed to `key` and answered with
937    /// `outcome`, and nothing else known: no parent, scope, purpose, turn, times,
938    /// tool output, kept items, review or decider, and [`Disposition::Ran`]. A host
939    /// that answers or cancels an effect itself builds its record from this
940    /// and sets what it knows.
941    pub fn new(
942        id: EffectId,
943        key: HandlerKey,
944        kind: EffectKind,
945        outcome: Result<Answered, ErrorReport>,
946    ) -> Self {
947        Self {
948            published: None,
949            id,
950            key,
951            kind,
952            outcome,
953            events: None,
954            stream_origin: None,
955            parent: None,
956            scope: None,
957            purpose: None,
958            turn: None,
959            started_ms: None,
960            finished_ms: None,
961            reviewed: None,
962            disposition: Disposition::Ran,
963            decided_by: None,
964        }
965    }
966}
967
968impl Disposition {
969    /// Whether this is [`Self::Ran`], the default a record leaves out.
970    fn is_ran(&self) -> bool {
971        *self == Self::Ran
972    }
973}
974
975/// Required handler keys and their effect families, ordered by key.
976/// [`Self::is_subset_of`] validates the requirements against handler descriptors.
977#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
978#[serde(transparent)]
979pub struct EffectRow(std::collections::BTreeMap<HandlerKey, EffectFamily>);
980
981/// The first entry of a row a set of handlers does not serve: the key,
982/// the family the row needs, and what serves the key instead, if anything.
983#[derive(Debug, Clone, PartialEq, Eq)]
984pub struct RowGap {
985    /// The key the row names.
986    pub key: HandlerKey,
987    /// The family the row needs it as.
988    pub needed: EffectFamily,
989    /// The family a handler serves it as, or `None` when nothing serves it.
990    pub served: Option<EffectFamily>,
991}
992
993impl std::fmt::Display for RowGap {
994    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
995        match self.served {
996            Some(served) => write!(
997                f,
998                "`{}` is needed as {} but served as {served}",
999                self.key, self.needed
1000            ),
1001            None => write!(f, "`{}` ({}) is not served", self.key, self.needed),
1002        }
1003    }
1004}
1005
1006/// One difference between two rows.
1007#[derive(Debug, Clone, PartialEq, Eq)]
1008pub enum RowDiff {
1009    /// The other row lacks this entry.
1010    Missing {
1011        /// The key this row names.
1012        key: HandlerKey,
1013        /// The family this row names it as.
1014        family: EffectFamily,
1015    },
1016    /// The other row has an entry this one lacks.
1017    Extra {
1018        /// The key the other row names.
1019        key: HandlerKey,
1020        /// The family the other row names it as.
1021        family: EffectFamily,
1022    },
1023    /// Both rows name the key, as different families.
1024    Family {
1025        /// The key both name.
1026        key: HandlerKey,
1027        /// This row's family for it.
1028        this: EffectFamily,
1029        other: EffectFamily,
1030    },
1031}
1032
1033impl std::fmt::Display for RowDiff {
1034    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1035        match self {
1036            Self::Missing { key, family } => write!(f, "`{key}` ({family}) is missing"),
1037            Self::Extra { key, family } => write!(f, "`{key}` ({family}) is extra"),
1038            Self::Family { key, this, other } => {
1039                write!(f, "`{key}` is {this} here and {other} there")
1040            }
1041        }
1042    }
1043}
1044
1045impl EffectRow {
1046    /// An empty row.
1047    pub fn new() -> Self {
1048        Self::default()
1049    }
1050
1051    /// Name `key` as `family`; a key already named is re-named.
1052    pub fn insert(&mut self, key: HandlerKey, family: EffectFamily) -> Option<EffectFamily> {
1053        self.0.insert(key, family)
1054    }
1055
1056    /// Name `key` as `family` unless the row already names it.
1057    pub fn insert_if_absent(&mut self, key: HandlerKey, family: EffectFamily) {
1058        self.0.entry(key).or_insert(family);
1059    }
1060
1061    /// Remove `key` from the row; the family it named, if it was there.
1062    pub fn remove(&mut self, key: &HandlerKey) -> Option<EffectFamily> {
1063        self.0.remove(key)
1064    }
1065
1066    /// The family the row names `key` as.
1067    pub fn get(&self, key: &HandlerKey) -> Option<&EffectFamily> {
1068        self.0.get(key)
1069    }
1070
1071    /// Whether the row names `key`.
1072    pub fn contains_key(&self, key: &HandlerKey) -> bool {
1073        self.0.contains_key(key)
1074    }
1075
1076    /// The keys, in order.
1077    pub fn keys(&self) -> impl Iterator<Item = &HandlerKey> {
1078        self.0.keys()
1079    }
1080
1081    /// The entries, in key order.
1082    pub fn iter(&self) -> impl Iterator<Item = (&HandlerKey, &EffectFamily)> {
1083        self.0.iter()
1084    }
1085
1086    /// How many keys the row names.
1087    pub fn len(&self) -> usize {
1088        self.0.len()
1089    }
1090
1091    /// Whether the row names no key.
1092    pub fn is_empty(&self) -> bool {
1093        self.0.is_empty()
1094    }
1095
1096    /// Validates that every key is served with the required family, returning
1097    /// the first gap in key order. Uses the first descriptor matching each key.
1098    pub fn is_subset_of(&self, handlers: &[HandlerDescriptor]) -> Result<(), RowGap> {
1099        for (key, needed) in &self.0 {
1100            let served = handlers
1101                .iter()
1102                .find(|descriptor| &descriptor.key == key)
1103                .map(|descriptor| descriptor.family.family());
1104            match served {
1105                Some(served) if served == *needed => {}
1106                served => {
1107                    return Err(RowGap {
1108                        key: key.clone(),
1109                        needed: *needed,
1110                        served,
1111                    });
1112                }
1113            }
1114        }
1115        Ok(())
1116    }
1117
1118    /// Returns missing and mismatched entries in this row's key order, followed
1119    /// by extra entries in the other row's key order.
1120    pub fn diff(&self, other: &EffectRow) -> Vec<RowDiff> {
1121        let mut diffs = Vec::new();
1122        for (key, family) in &self.0 {
1123            match other.0.get(key) {
1124                None => diffs.push(RowDiff::Missing {
1125                    key: key.clone(),
1126                    family: *family,
1127                }),
1128                Some(theirs) if theirs != family => diffs.push(RowDiff::Family {
1129                    key: key.clone(),
1130                    this: *family,
1131                    other: *theirs,
1132                }),
1133                Some(_) => {}
1134            }
1135        }
1136        for (key, family) in &other.0 {
1137            if !self.0.contains_key(key) {
1138                diffs.push(RowDiff::Extra {
1139                    key: key.clone(),
1140                    family: *family,
1141                });
1142            }
1143        }
1144        diffs
1145    }
1146}
1147
1148impl FromIterator<(HandlerKey, EffectFamily)> for EffectRow {
1149    fn from_iter<I: IntoIterator<Item = (HandlerKey, EffectFamily)>>(iter: I) -> Self {
1150        Self(iter.into_iter().collect())
1151    }
1152}
1153
1154impl<'a> IntoIterator for &'a EffectRow {
1155    type Item = (&'a HandlerKey, &'a EffectFamily);
1156    type IntoIter = std::collections::btree_map::Iter<'a, HandlerKey, EffectFamily>;
1157
1158    fn into_iter(self) -> Self::IntoIter {
1159        self.0.iter()
1160    }
1161}
1162
1163impl IntoIterator for EffectRow {
1164    type Item = (HandlerKey, EffectFamily);
1165    type IntoIter = std::collections::btree_map::IntoIter<HandlerKey, EffectFamily>;
1166
1167    fn into_iter(self) -> Self::IntoIter {
1168        self.0.into_iter()
1169    }
1170}
1171
1172// The protocol crosses threads and serializes on every target.
1173const _: fn() = || {
1174    fn assert_wire<T: Clone + Send + Sync + 'static + Serialize + serde::de::DeserializeOwned>() {}
1175    assert_wire::<EffectId>();
1176    assert_wire::<HandlerKey>();
1177    assert_wire::<EffectFamily>();
1178    assert_wire::<HandlerDescriptor>();
1179    assert_wire::<FamilyDescriptor>();
1180    assert_wire::<ToolEmbeddingDescriptor>();
1181    assert_wire::<EmbedModality>();
1182    assert_wire::<EffectKind>();
1183    assert_wire::<EmbedInputs>();
1184    assert_wire::<RerankRequest>();
1185    assert_wire::<RetrieveQuery>();
1186    assert_wire::<Answered>();
1187    assert_wire::<EmbedOutputs>();
1188    assert_wire::<RetrievedDocuments>();
1189    assert_wire::<EffectRecord>();
1190    assert_wire::<Decider>();
1191    assert_wire::<EffectRow>();
1192    assert_wire::<StreamRecord>();
1193};
1194
1195#[cfg(test)]
1196mod tests;