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