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