Response

The server sends the id, one of five refusals, or an error, as the first response; after the id it sends active before each loop, the agent’s chunks as they come, and inactive carrying the rotating secrets after the loop, and nothing else until the finish but the proxy’s error, relayed, when the secrets could not be collected; the finish carries no error.

The server sends the id; or one of five refusals — the volume that refused the run, named, and why; or an error; and, after the id, zero or more loops: for each, the word that it began, the chunks the agent says in it, and the word that it ended, carrying the rotating secrets the program handed back, for the life of the run. The response finish ends the scope. No frame bearing the scope follows the finish.

[0][id JSON …]                          once, when the container is running
[1][volume-held JSON …]                 once, when an operation holds a named volume
[2][volume-mounted JSON …]              once, when a running container holds a named volume and the server mounts a volume of its mode once
[3][volume-fuse-served JSON …]          once, when a FUSE serve holds a named volume and the server does not mount a volume of its mode beside one
[4][volume-mount-unsupported JSON …]    once, when the server does not mount a volume of a named volume's mode
[5][volume-mode JSON …]                 once, when a named volume is not in the mode its mount names
[6][error JSON …]                       once, when it is not running for another reason, or after the id when the proxy ends the begin with an error
[7][chunk JSON …]                       zero or more, within a loop, for the life of the run
[8]                                     active: a loop began
[9][values JSON …]                      inactive: that loop ended, and these are its rotating secrets
  • The sequence. Before the id, the server sends no response. The first response is the id, one of the five refusals, or an error. A refusal and an error before the id are each followed by the response finish, and the container is not running and will not be. After the id, the server sends zero or more loops, and no other response until the finish but, at most once, the error with which the proxy ended the begin scope, the byte 6 followed by it, which the response finish follows. A loop is exactly one response whose payload is the byte 8 and nothing else, followed by zero or more chunks, each as one response, in the order the proxy sent them, followed by exactly one response whose first byte is 9, or, where the proxy could not collect the loop’s rotating secrets, by the error. The finish carries no error: a run that ended after its id ended by the client’s stop, by the container’s own end, by the proxy’s error, or by the client’s connection ending. A response finish that no response precedes states that the request was not served, as Endpoints provides, or that the run ended before the server had an id to send.
  • The id. A payload whose first byte is 0 carries the id after that byte as JSON, an object with the member id, a string. The server sends it after every FUSE mount is complete and after the container is registered and, where its program declared a secret, fed, as containers::agents::run states.
  • The refusals, and their order. The server decides the bytes 1 to 5 for each distinct volume_name the request names, in the order first named, in the order Capabilities states, the first rule that applies deciding; a refusal of any volume gives back every volume held for the request before it. Every refusal is sent before the server fetches or deploys anything, the finish follows it, and the container was not started.
  • Volume held. A payload whose first byte is 1 carries, after that byte as JSON, an object with the member name, a string: the volume_name of a volume the request named that is under a volumes::stat, volumes::read, volumes::write, volumes::filetree, volumes::edit or volumes::delete at the time of the request.
  • Volume mounted. A payload whose first byte is 2 carries, after that byte as JSON, an object with the member name, a string: the volume_name of a volume the request named that a running container of the client holds at the time of the request, on a server whose mode object for the volume’s mode has mount_many false. A volume a running container holds does not refuse a run where mount_many is true.
  • Volume FUSE served. A payload whose first byte is 3 carries, after that byte as JSON, an object with the member name, a string: the volume_name of a volume the request named that a volumes::fuse_serve of the client holds at the time of the request, on a server whose mode object for the volume’s mode has mount_and_fuse_serve false. A volume a FUSE serve holds does not refuse a run where mount_and_fuse_serve is true.
  • Volume mount unsupported. A payload whose first byte is 4 carries, after that byte as JSON, an object with the member name, a string: the volume_name of a volume the request named whose mode the server holds out no mode object for, or whose mode object has mount false. The volume is as it was.
  • Volume mode. A payload whose first byte is 5 carries, after that byte as JSON, an object with the members name, a string, and mode, a string: the volume_name of a volume the request named under a mode the volume is not in, and the mode the volume is in, as the client’s listing reports it, one of persistent, ephemeral and read_only. The volume is as it was. A request naming one volume under two modes is answered the same way, with the mode the volume is in.
  • The error. A payload whose first byte is 6 carries an error after that byte, exactly one JSON value in the form defined on the volumes::list response page. Before the id, this revision prescribes nothing about its content. After the id, the server sends it only when the proxy ends the begin scope with an error, as the begin response states, and the bytes after the byte 6 are the proxy’s error verbatim: it names a secret and carries no value.
  • The chunk. A payload whose first byte is 7 carries, after that byte as JSON, one chunk of the agent’s conversation, exactly as the proxy sent it; the server does not read it. The server sends a chunk only after the id. A chunk is one of the forms the included files below define, told apart by its type member. A message of the client’s lands as its user parts: one chunk per content block, its type the block’s kind — user_text_content, user_image_content, user_audio_content, user_resource, user_resource_link — each carrying the message’s key as the enqueue gave it, consecutive in the message’s order. The first chunks the server sends after the id are the user parts of the message the loop started on, or of every message it started on, in the order sent. A message’s user parts mark it landing, and a notification chunk whose is_fatal is true marks a loop that ended in failure. No chunk marks the end of a loop: the byte 9 does.
  • Active and inactive. A payload that is the byte 8 and nothing else states that a loop began: the agent’s server answered the loop’s POST /run with 2xx. A payload whose first byte is 9 states that the loop ended: the event stream of that answer ended, cleanly or by failing, and the program handed back its rotating secrets. The server sends each exactly as the proxy sent it on the begin response, where the proxy produces both and relays neither from the agent’s server. The server sends no byte 8 before the id, none while a loop is open, and no byte 9 while none is. An agent is quiet between a 9 and the next 8; quiet between an 8 and its 9 is an agent still working.
  • The values. The bytes after the byte 9 are a JSON object whose every member is the name of a secret the container’s program declared rotating, to the value the program leaves in it, any JSON value: every rotating name the program declared, and no other; the empty object for a program that declared none rotating. The server sends the object exactly as the proxy sent it and reads none of it. From the byte 9 the container holds no values, and the proxy delivers no message to a program that declared a secret until the client feeds them again on the secrets channel.
  • Malformed. A payload with no byte, a payload whose first byte is none of 0 to 9, a payload whose bytes after 0 do not parse as the id, a payload whose bytes after 1, 2, 3, 4 or 5 do not parse as that refusal, a payload whose bytes after 7 do not parse as a chunk, a payload whose bytes after 9 do not parse as the values, and a payload whose bytes after 6 are not one JSON value are malformed. A payload whose first byte is 8 is read by that byte alone; bytes after it are not read.

The response is defined by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/frame.rs:

//! What a server's response frame carries for an agent container run.

use std::fmt;

use super::AgenticLoopChunk;
use crate::wire::decode::Decode;
use crate::wire::encode::{Encode, Writer};
use crate::shared::containers::response::{
    Id, VolumeFuseServed, VolumeHeld, VolumeMode, VolumeMountUnsupported, VolumeMounted,
};
use crate::shared::containers::secrets::Values;
use crate::shared::error::Error;

/// A run's answer: the container's id, then the agent's conversation
/// for as long as the scope lives, each loop of it bracketed by the
/// proxy's own word that it began and that it ended — or the volume
/// that refused it, or a failure.
///
/// A payload leads with one byte saying which — `0` for
/// [`Id`](Self::Id), `1` for [`VolumeHeld`](Self::VolumeHeld), `2`
/// for [`VolumeMounted`](Self::VolumeMounted), `3` for
/// [`VolumeFuseServed`](Self::VolumeFuseServed), `4` for
/// [`VolumeMountUnsupported`](Self::VolumeMountUnsupported), `5` for
/// [`VolumeMode`](Self::VolumeMode), `6` for
/// [`Error`](Self::Error), `7` for [`Chunk`](Self::Chunk), `8` for
/// [`Active`](Self::Active), `9` for [`Inactive`](Self::Inactive) —
/// and the rest is that variant's own JSON, or nothing: the one
/// bare word carries nothing after the byte.
///
/// # The id, then the conversation
///
/// | the scope | means |
/// |-----------|-------|
/// | an id, then quiet, and stays open | the container is running, and no loop has run |
/// | an id, an active, then chunks, and stays open | the container is running, a loop is running, and the agent is speaking |
/// | …an active, chunks, an inactive, and stays open | that loop ran and ended, its rotating secrets handed back; the agent is quiet until the next message |
/// | a volume held, then a finish | it never started: that volume is under a stat, a read, a write, a filetree, an edit or a delete |
/// | a volume mounted, then a finish | it never started: a running container has that volume, and the provider mounts a volume of its mode once |
/// | a volume FUSE served, then a finish | it never started: a FUSE serve has that volume, and the provider does not mount a volume of its mode beside one |
/// | a volume mount unsupported, then a finish | it never started: the provider does not mount a volume of that volume's mode |
/// | a volume mode, then a finish | it never started: that volume is not in the mode its mount names |
/// | FUSE mounts unsupported, then a finish | it never started: the request names a FUSE mount, and the provider mounts none |
/// | an error, then a finish | it never came up, or it is gone |
/// | a finish, with no error | the run is over — a stop, or the container's own end |
///
/// # One mode, and who may hold it
///
/// A mount states the mode it means the volume to have, and a volume
/// in another mode refuses the run,
/// [`VolumeMode`](Self::VolumeMode), with the mode it is in — before
/// anything is fetched or deployed, and after the hold is taken and
/// given back, so an edit cannot slip between the look and the
/// answer. Who else may hold the volume meanwhile is the provider's
/// capability for the volume's mode, stated in its
/// [`info`](crate::provider::endpoints::info): a running container
/// that has it refuses the run, [`VolumeMounted`](Self::VolumeMounted),
/// on a provider whose `mount_many` for the mode is `false`; a FUSE
/// serve that has it refuses the run,
/// [`VolumeFuseServed`](Self::VolumeFuseServed), where
/// `mount_and_fuse_serve` is `false`; a mode the provider does not
/// mount at all refuses it,
/// [`VolumeMountUnsupported`](Self::VolumeMountUnsupported); and no
/// volume is mounted while a stat, a read, a write, a filetree, an
/// edit or a delete has it to itself: a request that names one so
/// held is answered [`VolumeHeld`](Self::VolumeHeld). Each
/// refusal is its own variant, because a caller acts on it
/// differently from a failure: edit the volume or the mount, stop the
/// container or the FUSE serve, wait for the operation, name another
/// volume or ask another provider, and ask again. The hold is the
/// volume's `mount`, taken by the run handler and given back on every
/// ending.
///
/// # The conversation is this stream
///
/// An agent container is one conversation, and this is where it is
/// read. Nothing opens a loop: an
/// [`enqueue`](crate::shared::containers::enqueue) with no loop
/// running starts one on its message, an enqueue while one runs joins
/// the queue, and either way what the agent says arrives here, chunk
/// by chunk, in order, as the proxy sent it. A message's user parts —
/// see [`user_parts`](super::user_parts) — mark it landing, and a
/// [`NotificationChunk`](super::NotificationChunk) with
/// [`is_fatal`](super::NotificationChunk::is_fatal) set marks a loop
/// that died. The tools family's stream carries the two words after
/// the id and nothing else: its own exchange answers on channels.
///
/// # The proxy marks the loop, and only the proxy can
///
/// A loop is one `POST /run` on the agent's server inside the
/// container: it begins when that call answers `2xx` and ends when the
/// event stream of that answer ends, however it ends. The proxy makes
/// the call, so the proxy is the one party that knows both moments,
/// and it says them on its begin stream — see
/// [the begin's frame](crate::container_proxy::outside::endpoints::agents::begin::server::response::Frame)
/// — which the provider relays here as [`Active`](Self::Active) before
/// the loop's first chunk and [`Inactive`](Self::Inactive) after its
/// last, the latter carrying the rotating secrets the program handed
/// back, for the caller to write into the places they were locked
/// from. Between an `Inactive` and the next `Active` the agent says
/// nothing; quiet between an `Active` and its `Inactive` is an agent
/// still working. So a caller knows whether the agent is busy from
/// the stream alone, and never guesses it from silence; and a caller
/// that fed a secret knows when it has it back. Before the next
/// `Active` the caller feeds the values again, on its own
/// [`Secrets`](super::super::super::client::channel_request::Frame::Secrets)
/// channel, and the proxy delivers nothing to the program until it
/// has.
///
/// The tree and the files are still channels the caller opens, so a
/// caller that wants none of them pays for none of them. This stream
/// carries no readiness signal: a provider knows when a CONTAINER has
/// started, and that is not the same fact as the thing inside it
/// having bound its port. The channels find out, one exchange at a
/// time. The three facts are three things — the container up, which
/// is the id; the program reachable, which the channels learn; and a
/// loop running, which is `Active` — and none stands in for another.
///
/// # The tag is not decoration
///
/// [`AgenticLoopChunk`] is untagged and tells its own variants apart
/// by a `type` constant inside each one, while an [`Error`] is an
/// arbitrary JSON value — including, legitimately, an object with a
/// `type` field. The byte in front is what keeps a provider's error
/// text from being read as a chunk, and a failure from looking like
/// output. It is also what keeps `Active` and `Inactive` the proxy's
/// alone: a program's output is chunks, every one of them under the
/// chunk's byte, so no chunk a program writes, whatever its `type`,
/// is read as the proxy's word about the loop.
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
    /// The container's id. Tag `0`.
    ///
    /// Arrives once, whenever the provider has it. See [`Id`].
    Id(Id),
    /// The run was refused: a volume it names is under a stat, a
    /// read, a write, a filetree, an edit or a delete. Tag `1`.
    ///
    /// The first and only response of its scope; the finish follows.
    /// Nothing was fetched and nothing was deployed.
    VolumeHeld(VolumeHeld),
    /// The run was refused: a running container has a volume it
    /// names, and the provider mounts a volume of that mode once.
    /// Tag `2`.
    ///
    /// The first and only response of its scope; the finish follows.
    /// Nothing was fetched and nothing was deployed.
    VolumeMounted(VolumeMounted),
    /// The run was refused: a FUSE serve has a volume it names, and
    /// the provider does not mount a volume of that mode beside one.
    /// Tag `3`.
    ///
    /// The first and only response of its scope; the finish follows.
    /// Nothing was fetched and nothing was deployed.
    VolumeFuseServed(VolumeFuseServed),
    /// The run was refused: the provider does not mount a volume of
    /// the mode a volume it names is in. Tag `4`.
    ///
    /// The first and only response of its scope; the finish follows.
    /// Nothing was fetched and nothing was deployed.
    VolumeMountUnsupported(VolumeMountUnsupported),
    /// The run was refused: a volume it names is not in the mode the
    /// mount means it to have. Tag `5`.
    ///
    /// The first and only response of its scope; the finish follows.
    /// Nothing was fetched and nothing was deployed, and the volume is
    /// as it was.
    VolumeMode(VolumeMode),
    /// A failure. Tag `6`.
    ///
    /// The container is not running and will not be — the image
    /// would not pull, the container would not start, whatever the
    /// provider knows. It is the one variant that ends the scope
    /// rather than adding to it. See
    /// [`shared::error::Error`](crate::shared::error::Error) for why
    /// it says so little. This is not a
    /// [`NotificationChunk`](super::NotificationChunk) with
    /// [`is_fatal`](super::NotificationChunk::is_fatal) set: that is
    /// part of the agent's OUTPUT, a loop saying it is over and why,
    /// and the scope goes on.
    Error(Error),
    /// One chunk of the agent's conversation. Tag `7`.
    ///
    /// After the id, zero or more, for as long as the scope lives, in
    /// the order the agent produced them and exactly as the proxy
    /// sent them; never before the id, never outside an
    /// [`Active`](Self::Active) and its [`Inactive`](Self::Inactive),
    /// and never after a finish. See [`AgenticLoopChunk`] for what
    /// one is.
    Chunk(AgenticLoopChunk),
    /// A loop began. Tag `8`, and nothing after it.
    ///
    /// The proxy's word that the agent's server took a message and is
    /// working: every chunk until the matching
    /// [`Inactive`](Self::Inactive) is this loop's. After the id,
    /// never twice without an `Inactive` between.
    Active,
    /// A loop ended, and these are its rotating secrets. Tag `9`.
    ///
    /// The proxy's word that the loop's stream ended — cleanly, or by
    /// dying; a loop that failed said so in a fatal notification chunk
    /// before this — and the [`Values`] it collected from the program
    /// after, every `rotating` secret the program declared, each by
    /// name to the value it leaves in it, as JSON after the byte;
    /// empty for a program that declared none rotating. The caller
    /// writes each into the place it was locked from. The agent is
    /// quiet until the next message starts the next loop. Never
    /// without an [`Active`](Self::Active) before it.
    Inactive(Values),
}

/// Tag for [`Frame::Id`].
const ID: u8 = 0;

/// Tag for [`Frame::VolumeHeld`].
const VOLUME_HELD: u8 = 1;

/// Tag for [`Frame::VolumeMounted`].
const VOLUME_MOUNTED: u8 = 2;

/// Tag for [`Frame::VolumeFuseServed`].
const VOLUME_FUSE_SERVED: u8 = 3;

/// Tag for [`Frame::VolumeMountUnsupported`].
const VOLUME_MOUNT_UNSUPPORTED: u8 = 4;

/// Tag for [`Frame::VolumeMode`].
const VOLUME_MODE: u8 = 5;

/// Tag for [`Frame::Error`].
const ERROR: u8 = 6;

/// Tag for [`Frame::Chunk`]. Public, so a relay that carries a
/// chunk's JSON without reading it can frame it.
pub const CHUNK: u8 = 7;

/// Tag for [`Frame::Active`]. Public, so a relay writes the one byte
/// that is the whole frame.
pub const ACTIVE: u8 = 8;

/// Tag for [`Frame::Inactive`]. Public, so a relay that carries the
/// values' JSON without reading it can frame it.
pub const INACTIVE: u8 = 9;

impl Encode for Frame {
    /// One failure per variant that carries JSON; the one bare word
    /// cannot fail.
    type Error = FrameEncodeError;

    // Spelled out rather than `Self::Error`: this enum has a variant
    // called `Error`, so the associated type is ambiguous by that name.
    fn encode(
        &self,
        out: &mut Writer<'_>,
    ) -> Result<(), FrameEncodeError> {
        match self {
            Frame::Id(id) => {
                out.extend_from_slice(&[ID]);
                serde_json::to_writer(out, id).map_err(FrameEncodeError::Id)
            }
            Frame::VolumeHeld(refused) => {
                out.extend_from_slice(&[VOLUME_HELD]);
                serde_json::to_writer(out, refused).map_err(FrameEncodeError::VolumeHeld)
            }
            Frame::VolumeMounted(refused) => {
                out.extend_from_slice(&[VOLUME_MOUNTED]);
                serde_json::to_writer(out, refused).map_err(FrameEncodeError::VolumeMounted)
            }
            Frame::VolumeFuseServed(refused) => {
                out.extend_from_slice(&[VOLUME_FUSE_SERVED]);
                serde_json::to_writer(out, refused).map_err(FrameEncodeError::VolumeFuseServed)
            }
            Frame::VolumeMountUnsupported(refused) => {
                out.extend_from_slice(&[VOLUME_MOUNT_UNSUPPORTED]);
                serde_json::to_writer(out, refused).map_err(FrameEncodeError::VolumeMountUnsupported)
            }
            Frame::VolumeMode(refused) => {
                out.extend_from_slice(&[VOLUME_MODE]);
                serde_json::to_writer(out, refused).map_err(FrameEncodeError::VolumeMode)
            }
            Frame::Error(error) => {
                out.extend_from_slice(&[ERROR]);
                error.encode(out).map_err(FrameEncodeError::Error)
            }
            Frame::Chunk(chunk) => {
                out.extend_from_slice(&[CHUNK]);
                serde_json::to_writer(out, chunk).map_err(FrameEncodeError::Chunk)
            }
            Frame::Active => {
                out.extend_from_slice(&[ACTIVE]);
                Ok(())
            }
            Frame::Inactive(values) => {
                out.extend_from_slice(&[INACTIVE]);
                serde_json::to_writer(out, values).map_err(FrameEncodeError::Inactive)
            }
        }
    }
}

/// An agent container run response that could not be written.
#[derive(Debug)]
pub enum FrameEncodeError {
    /// The id did not serialize.
    Id(serde_json::Error),
    /// The held refusal did not serialize.
    VolumeHeld(serde_json::Error),
    /// The mounted refusal did not serialize.
    VolumeMounted(serde_json::Error),
    /// The FUSE-served refusal did not serialize.
    VolumeFuseServed(serde_json::Error),
    /// The mount-unsupported refusal did not serialize.
    VolumeMountUnsupported(serde_json::Error),
    /// The mode refusal did not serialize.
    VolumeMode(serde_json::Error),
    /// The error did not serialize.
    Error(serde_json::Error),
    /// The chunk did not serialize.
    Chunk(serde_json::Error),
    /// The rotating secrets did not serialize. The message is the
    /// serializer's, which quotes no value.
    Inactive(serde_json::Error),
}

impl fmt::Display for FrameEncodeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameEncodeError::Id(error) => {
                write!(f, "container id did not serialize: {error}")
            }
            FrameEncodeError::VolumeHeld(error) => {
                write!(f, "volume-held refusal did not serialize: {error}")
            }
            FrameEncodeError::VolumeMounted(error) => {
                write!(f, "volume-mounted refusal did not serialize: {error}")
            }
            FrameEncodeError::VolumeFuseServed(error) => {
                write!(f, "volume-fuse-served refusal did not serialize: {error}")
            }
            FrameEncodeError::VolumeMountUnsupported(error) => {
                write!(f, "volume-mount-unsupported refusal did not serialize: {error}")
            }
            FrameEncodeError::VolumeMode(error) => {
                write!(f, "volume-mode refusal did not serialize: {error}")
            }
            FrameEncodeError::Error(error) => {
                write!(f, "agents run error did not serialize: {error}")
            }
            FrameEncodeError::Chunk(error) => {
                write!(f, "agents run chunk did not serialize: {error}")
            }
            FrameEncodeError::Inactive(error) => {
                write!(f, "agents run inactive secrets did not serialize: {error}")
            }
        }
    }
}

impl std::error::Error for FrameEncodeError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            FrameEncodeError::Id(error) => Some(error),
            FrameEncodeError::VolumeHeld(error) => Some(error),
            FrameEncodeError::VolumeMounted(error) => Some(error),
            FrameEncodeError::VolumeFuseServed(error) => Some(error),
            FrameEncodeError::VolumeMountUnsupported(error) => Some(error),
            FrameEncodeError::VolumeMode(error) => Some(error),
            FrameEncodeError::Error(error) => Some(error),
            FrameEncodeError::Chunk(error) => Some(error),
            FrameEncodeError::Inactive(error) => Some(error),
        }
    }
}

impl Decode<'_> for Frame {
    /// Eleven ways to fail, and each names which variant failed.
    type Error = FrameError;

    // Spelled out for the same reason as `encode` above.
    fn decode(bytes: &[u8]) -> Result<Self, FrameError> {
        let (tag, rest) = bytes.split_first().ok_or(FrameError::Empty)?;
        match *tag {
            ID => serde_json::from_slice(rest)
                .map(Frame::Id)
                .map_err(FrameError::Id),
            VOLUME_HELD => serde_json::from_slice(rest)
                .map(Frame::VolumeHeld)
                .map_err(FrameError::VolumeHeld),
            VOLUME_MOUNTED => serde_json::from_slice(rest)
                .map(Frame::VolumeMounted)
                .map_err(FrameError::VolumeMounted),
            VOLUME_FUSE_SERVED => serde_json::from_slice(rest)
                .map(Frame::VolumeFuseServed)
                .map_err(FrameError::VolumeFuseServed),
            VOLUME_MOUNT_UNSUPPORTED => serde_json::from_slice(rest)
                .map(Frame::VolumeMountUnsupported)
                .map_err(FrameError::VolumeMountUnsupported),
            VOLUME_MODE => serde_json::from_slice(rest)
                .map(Frame::VolumeMode)
                .map_err(FrameError::VolumeMode),
            ERROR => Error::decode(rest)
                .map(Frame::Error)
                .map_err(FrameError::Error),
            CHUNK => serde_json::from_slice(rest)
                .map(Frame::Chunk)
                .map_err(FrameError::Chunk),
            ACTIVE => Ok(Frame::Active),
            INACTIVE => serde_json::from_slice(rest)
                .map(Frame::Inactive)
                .map_err(FrameError::Inactive),
            tag => Err(FrameError::UnknownTag(tag)),
        }
    }
}

/// An agent container run response frame that could not be read.
#[derive(Debug)]
pub enum FrameError {
    /// No bytes at all, so not even a tag.
    Empty,
    /// A tag that is none of this frame's ten.
    ///
    /// What a provider newer than its caller produces, which is the
    /// case the tag exists to make survivable: a reader that does not
    /// know a variant says so, rather than reading somebody else's
    /// bytes as its own.
    UnknownTag(u8),
    /// The id did not parse.
    Id(serde_json::Error),
    /// The held refusal did not parse.
    VolumeHeld(serde_json::Error),
    /// The mounted refusal did not parse.
    VolumeMounted(serde_json::Error),
    /// The FUSE-served refusal did not parse.
    VolumeFuseServed(serde_json::Error),
    /// The mount-unsupported refusal did not parse.
    VolumeMountUnsupported(serde_json::Error),
    /// The mode refusal did not parse.
    VolumeMode(serde_json::Error),
    /// The error did not parse.
    Error(serde_json::Error),
    /// The chunk did not parse.
    Chunk(serde_json::Error),
    /// The rotating secrets did not parse. The message is the
    /// parser's, which quotes no value.
    Inactive(serde_json::Error),
}

impl fmt::Display for FrameError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameError::Empty => {
                f.write_str("agents run response frame is empty")
            }
            FrameError::UnknownTag(tag) => {
                write!(f, "unknown agents run response frame tag {tag}")
            }
            FrameError::Id(error) => {
                write!(f, "container id did not parse: {error}")
            }
            FrameError::VolumeHeld(error) => {
                write!(f, "volume-held refusal did not parse: {error}")
            }
            FrameError::VolumeMounted(error) => {
                write!(f, "volume-mounted refusal did not parse: {error}")
            }
            FrameError::VolumeFuseServed(error) => {
                write!(f, "volume-fuse-served refusal did not parse: {error}")
            }
            FrameError::VolumeMountUnsupported(error) => {
                write!(f, "volume-mount-unsupported refusal did not parse: {error}")
            }
            FrameError::VolumeMode(error) => {
                write!(f, "volume-mode refusal did not parse: {error}")
            }
            FrameError::Error(error) => {
                write!(f, "agents run error did not parse: {error}")
            }
            FrameError::Chunk(error) => {
                write!(f, "agents run chunk did not parse: {error}")
            }
            FrameError::Inactive(error) => {
                write!(f, "agents run inactive secrets did not parse: {error}")
            }
        }
    }
}

impl std::error::Error for FrameError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            FrameError::Id(error) => Some(error),
            FrameError::VolumeHeld(error) => Some(error),
            FrameError::VolumeMounted(error) => Some(error),
            FrameError::VolumeFuseServed(error) => Some(error),
            FrameError::VolumeMountUnsupported(error) => Some(error),
            FrameError::VolumeMode(error) => Some(error),
            FrameError::Error(error) => Some(error),
            FrameError::Chunk(error) => Some(error),
            FrameError::Inactive(error) => Some(error),
            FrameError::Empty | FrameError::UnknownTag(_) => None,
        }
    }
}

and by diverge-sdk-rs/src/shared/containers/response/id.rs:

//! The container's id.

use serde::{Deserialize, Serialize};

/// What the provider decided to call this container.
///
/// An object rather than a bare string, so a provider that later has
/// something else to say about the container's identity has somewhere
/// to say it. A JSON string is a shape that can only ever be one
/// field.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct Id {
    /// The id itself.
    ///
    /// Opaque, and the provider's to mint. A caller that wants to name
    /// this container anywhere else — a
    /// [`transfer`](crate::shared::containers::transfer) into it, a
    /// [`fuse_serve`](crate::provider::endpoints::containers::fuse_serve) of it —
    /// has this and nothing else to name it with. It is a capability:
    /// holding it is what lets its runner name it, so it has to be
    /// unguessable, and nothing a caller could choose would be.
    ///
    /// Nothing here constrains its shape. It means nothing to anyone
    /// who was not given it, and nothing outside the provider that
    /// minted it.
    pub id: String,
}

and by diverge-sdk-rs/src/shared/containers/response/volume_held.rs:

//! A run refused because a volume it names is held to itself by an
//! operation: a stat, a read, a write, a filetree, an edit or a
//! delete.

use serde::{Deserialize, Serialize};

/// The volume a run request named that is held exclusively at the
/// time — under a stat, a read, a write, a filetree, an edit or a
/// delete of the same caller — and so refused the run.
///
/// No volume is mounted while something examines, reads, writes,
/// walks, resizes or removes it, whatever its
/// [`Mode`](crate::provider::endpoints::volumes::Mode). A provider
/// holds every volume a run names, shared, from the moment it accepts
/// the request until the run ends, and a request naming one that is
/// held exclusively is answered with this — the first and only
/// response of its scope, then the finish — with nothing fetched and
/// nothing deployed; a caller waits for the operation to end and asks
/// again. A volume another RUN or a FUSE serve has is not this: that
/// is [`VolumeMounted`](super::VolumeMounted) or
/// [`VolumeFuseServed`](super::VolumeFuseServed), each where the
/// provider's capability for the mode refuses a second holder. On the
/// server half the hold is the volume's `mount`, which the run
/// handler takes before anything else and gives back on every ending.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VolumeHeld {
    /// The `volume_name` of the volume, as the request named it.
    pub name: String,
}

and by diverge-sdk-rs/src/shared/containers/response/volume_mounted.rs:

//! A run refused because a running container has a volume it names,
//! on a provider that mounts a volume of that mode once.

use serde::{Deserialize, Serialize};

/// The volume a run request named that a running container has
/// mounted at the time, on a provider whose `mount_many` for the
/// volume's mode is `false` — see
/// [`VolumeCapabilities`](crate::provider::endpoints::info::server::response::VolumeCapabilities)
/// — and so refused the run.
///
/// The first and only response of its scope, then the finish, with
/// nothing fetched and nothing deployed. The volume and the container
/// that has it are as they were; a caller stops that container, names
/// another volume, or asks another provider, and asks again. Every
/// [`Mode`](crate::provider::endpoints::volumes::Mode) allows more
/// than one running container at once; which modes this provider
/// mounts more than once is its capability for each.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VolumeMounted {
    /// The `volume_name` of the volume, as the request named it.
    pub name: String,
}

and by diverge-sdk-rs/src/shared/containers/response/volume_fuse_served.rs:

//! A run refused because a FUSE serve has a volume it names, on a
//! provider that does not mount a volume of that mode beside one.

use serde::{Deserialize, Serialize};

/// The volume a run request named that a
/// [`fuse_serve`](crate::provider::endpoints::volumes::fuse_serve)
/// holds at the time, on a provider whose `mount_and_fuse_serve` for
/// the volume's mode is `false` — see
/// [`VolumeCapabilities`](crate::provider::endpoints::info::server::response::VolumeCapabilities)
/// — and so refused the run.
///
/// The first and only response of its scope, then the finish, with
/// nothing fetched and nothing deployed. The volume and the FUSE serve
/// that has it are as they were; a caller stops that FUSE serve, names
/// another volume, or asks another provider, and asks again.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VolumeFuseServed {
    /// The `volume_name` of the volume, as the request named it.
    pub name: String,
}

and by diverge-sdk-rs/src/shared/containers/response/volume_mount_unsupported.rs:

//! A run refused because the provider does not mount a volume of the
//! mode a volume it names is in.

use serde::{Deserialize, Serialize};

/// The volume a run request named whose
/// [`Mode`](crate::provider::endpoints::volumes::Mode) the provider
/// does not mount into containers at all: its `mount` for the mode is
/// `false` — see
/// [`VolumeCapabilities`](crate::provider::endpoints::info::server::response::VolumeCapabilities)
/// — and so refused the run.
///
/// The first and only response of its scope, then the finish, with
/// nothing fetched and nothing deployed. Nothing about the volume is
/// the cause: the same volume in a mode the provider mounts, after an
/// [`edit`](crate::provider::endpoints::volumes::edit), or another
/// provider, is what a caller asks next. A volume in a mode the
/// provider has no capability for at all is never listed, so a
/// request cannot name one.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VolumeMountUnsupported {
    /// The `volume_name` of the volume, as the request named it.
    pub name: String,
}

and by diverge-sdk-rs/src/shared/containers/response/volume_mode.rs:

//! A run, or a FUSE serve, refused because a volume it names is not in
//! the mode it meant.

use serde::{Deserialize, Serialize};

use crate::provider::endpoints::volumes::Mode;

/// The volume a request named under one mode that is in another, and
/// so refused the request: a run, whose every mount states the mode it
/// means the volume to have, or a FUSE serve, whose request does.
///
/// Nothing changed: the volume is in the mode reported here, as its
/// listing reports it, and only an
/// [`edit`](crate::provider::endpoints::volumes::edit) changes it. The
/// provider answers this after it has held the volume and given the
/// hold back, so that an edit cannot land between the look and the
/// answer — the first and only response of its scope, then the
/// finish — with nothing fetched and nothing deployed for it. The
/// mode the request meant is the request's to read back.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VolumeMode {
    /// The `volume_name` of the volume, as the request named it.
    pub name: String,
    /// The mode the volume is in.
    pub mode: Mode,
}

and by diverge-sdk-rs/src/shared/containers/secrets/values.rs:

//! The secrets' values, by name.

use indexmap::IndexMap;
use serde_json::Value;

/// Secrets by name to value, any JSON: what the caller feeds a
/// container, and what the program hands back. Fed, it carries every
/// name the program declared, in the order declared — a `fixed` one
/// a value read from its key, a `rotating` one the value locked to
/// the container. Collected at an inactive, it carries every
/// `rotating` name the program declared, each with the value the
/// program leaves in it; a `fixed` name in it is ignored, and a
/// `rotating` name missing from it is the collect's error, never a
/// write-back. A value is never read by anything between the vault
/// and the program.
pub type Values = IndexMap<String, Value>;

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/chunk.rs:

//! The agentic loop chunk — the unit of a streaming response.

use rmcp::model::MetaObject;
use serde::{Deserialize, Serialize};

use super::{
    AssistantAudioContentChunk, AssistantImageContentChunk,
    AssistantReasoningChunk, AssistantRefusalChunk,
    AssistantTextContentChunk, AssistantToolCallChunk, NotificationChunk,
    ToolResponseChunk, UsageChunk, UserAudioContentChunk, UserImageContentChunk, UserResourceChunk,
    UserResourceLinkChunk, UserTextContentChunk,
};
use crate::shared::mcp::attest;
use crate::shared::mcp::attestation::Caller;

/// One chunk of a streaming agentic loop.
///
/// Each chunk is ONE event, not a struct of mostly-absent optionals.
/// A consumer learns what happened by matching, rather than by
/// inspecting which fields happen to be set.
///
/// **Untagged, discriminated by payload.** serde adds no tag of its
/// own; instead every variant's payload carries a `type` field whose
/// value no other variant can produce — each `type` is its own
/// single-variant enum, so a wrong value fails to deserialize instead
/// of arriving as data nobody checks. So the wire shape is the event
/// itself rather than a wrapper around one, and deserialization is
/// still unambiguous — the `type` constants do the work a tag would,
/// without a level of nesting.
///
/// That also means variant order here is not load-bearing. Untagged
/// deserialization takes the first variant that matches, and with
/// distinct `type` constants at most one ever can.
///
/// # Sub-agents
///
/// An upstream that delegates to sub-agents produces chunks on more
/// than one thread. The six assistant chunks and the tool response
/// carry `parent_tool_call_id` for that: absent, the chunk is the
/// main thread's; present, it is the id of the tool call whose
/// sub-agent produced it, so a caller sees a sub-agent's work as
/// children of the call that made it — its own calls answered by
/// its own responses, attributed alike. A nested sub-agent names
/// its IMMEDIATE spawner, so depth is a chain of ids. The other
/// chunks are the run's, not any thread's, and carry nothing.
///
/// # User parts
///
/// A message of the caller's is not one chunk but its parts: one
/// chunk per content block, in the message's order, each carrying
/// the message's `key` — the five user kinds, one per kind MCP's
/// content has, mirroring the assistant's but for a key in place of
/// a thread and whole in place of a delta. A message is its run of
/// parts under one key; the first chunks of every run are the parts
/// of the message it started on; see [`user_parts`](super::user_parts).
///
/// # Who spoke, under `_meta`
///
/// Every kind of chunk has a `_meta` at its top level — MCP's own on
/// the flattened content and results, this crate's on the rest — and
/// the daemon sets the key of [`shared::mcp`](crate::shared::mcp)
/// on each as it keeps the stream, by [`attest`](Self::attest): the
/// speaking agent, with its image. The proxy and the
/// provider relay each chunk as the agent's server produced it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
#[serde(untagged)]
pub enum AgenticLoopChunk {
    /// The model's reasoning. See [`AssistantReasoningChunk`].
    AssistantReasoning(AssistantReasoningChunk),
    /// Text from the model. See [`AssistantTextContentChunk`].
    AssistantTextContent(AssistantTextContentChunk),
    /// An image from the model. See [`AssistantImageContentChunk`].
    AssistantImageContent(AssistantImageContentChunk),
    /// Audio from the model. See [`AssistantAudioContentChunk`].
    AssistantAudioContent(AssistantAudioContentChunk),
    /// The model calling a tool. See [`AssistantToolCallChunk`].
    AssistantToolCall(AssistantToolCallChunk),
    /// The model declining. See [`AssistantRefusalChunk`].
    AssistantRefusal(AssistantRefusalChunk),
    /// A tool's result. See [`ToolResponseChunk`].
    ToolResponse(ToolResponseChunk),
    /// Text of a message of the caller's. See [`UserTextContentChunk`].
    UserTextContent(UserTextContentChunk),
    /// An image of a message of the caller's. See
    /// [`UserImageContentChunk`].
    UserImageContent(UserImageContentChunk),
    /// Audio of a message of the caller's. See
    /// [`UserAudioContentChunk`].
    UserAudioContent(UserAudioContentChunk),
    /// An embedded resource of a message of the caller's. See
    /// [`UserResourceChunk`].
    UserResource(UserResourceChunk),
    /// A resource link of a message of the caller's. See
    /// [`UserResourceLinkChunk`].
    UserResourceLink(UserResourceLinkChunk),
    /// Token usage so far. See [`UsageChunk`].
    Usage(UsageChunk),
    /// Something about the run itself. See [`NotificationChunk`].
    Notification(NotificationChunk),
}

impl AgenticLoopChunk {
    /// Set the key of [`shared::mcp`](crate::shared::mcp) on this
    /// chunk's `_meta`, whichever kind it is — made when the chunk
    /// carried none: the speaking agent, as the caller it is, with its
    /// image, in place of whatever was under the key and beside
    /// everything else.
    pub fn attest(&mut self, agent: &Caller) {
        let meta: &mut MetaObject = match self {
            AgenticLoopChunk::AssistantReasoning(chunk) => chunk.inner.meta.get_or_insert_default(),
            AgenticLoopChunk::AssistantTextContent(chunk) => chunk.inner.meta.get_or_insert_default(),
            AgenticLoopChunk::AssistantImageContent(chunk) => chunk.inner.meta.get_or_insert_default(),
            AgenticLoopChunk::AssistantAudioContent(chunk) => chunk.inner.meta.get_or_insert_default(),
            AgenticLoopChunk::AssistantToolCall(chunk) => &mut **chunk.meta.get_or_insert_default(),
            AgenticLoopChunk::AssistantRefusal(chunk) => chunk.inner.meta.get_or_insert_default(),
            AgenticLoopChunk::ToolResponse(chunk) => chunk.inner.meta.get_or_insert_default(),
            AgenticLoopChunk::UserTextContent(chunk) => chunk.inner.meta.get_or_insert_default(),
            AgenticLoopChunk::UserImageContent(chunk) => chunk.inner.meta.get_or_insert_default(),
            AgenticLoopChunk::UserAudioContent(chunk) => chunk.inner.meta.get_or_insert_default(),
            AgenticLoopChunk::UserResource(chunk) => chunk.inner.meta.get_or_insert_default(),
            AgenticLoopChunk::UserResourceLink(chunk) => chunk.inner.meta.get_or_insert_default(),
            AgenticLoopChunk::Usage(chunk) => chunk.meta.get_or_insert_default(),
            AgenticLoopChunk::Notification(chunk) => chunk.meta.get_or_insert_default(),
        };
        attest(meta, agent);
    }
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/user_parts.rs:

//! A delivered message as its parts.

use rmcp::model::ContentBlock;

use super::{
    AgenticLoopChunk, UserAudioContentChunk, UserImageContentChunk, UserResourceChunk, UserResourceLinkChunk,
    UserTextContentChunk,
};

/// One chunk per block of a delivered message, in the message's
/// order, each under the message's `key`: what every image yields
/// at the position a message landed, and first of all for the
/// message a run started on.
///
/// A block of a kind this crate does not know — MCP's content is
/// open-ended — yields nothing: the image refused the message before
/// it was taken, so none reaches here.
pub fn user_parts(key: &str, content: Vec<ContentBlock>) -> Vec<AgenticLoopChunk> {
    content
        .into_iter()
        .filter_map(|block| match block {
            ContentBlock::Text(inner) => Some(AgenticLoopChunk::UserTextContent(UserTextContentChunk {
                r#type: Default::default(),
                key: key.to_string(),
                inner,
            })),
            ContentBlock::Image(inner) => Some(AgenticLoopChunk::UserImageContent(UserImageContentChunk {
                r#type: Default::default(),
                key: key.to_string(),
                inner,
            })),
            ContentBlock::Audio(inner) => Some(AgenticLoopChunk::UserAudioContent(UserAudioContentChunk {
                r#type: Default::default(),
                key: key.to_string(),
                inner,
            })),
            ContentBlock::Resource(inner) => Some(AgenticLoopChunk::UserResource(UserResourceChunk {
                r#type: Default::default(),
                key: key.to_string(),
                inner,
            })),
            ContentBlock::ResourceLink(inner) => Some(AgenticLoopChunk::UserResourceLink(UserResourceLinkChunk {
                r#type: Default::default(),
                key: key.to_string(),
                inner,
            })),
            _ => None,
        })
        .collect()
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/user_text_content_chunk.rs:

//! The user text content chunk.

use rmcp::model::TextContent;
use serde::{Deserialize, Serialize};

/// Text of a delivered message.
///
/// One block of the message, whole, at the position the message
/// landed: between the tool responses it was folded in behind, or
/// opening the next turn when the assistant had already finished, or
/// first of all, for the message a run started on. A message's parts
/// arrive consecutively, in the message's order, each under its
/// [`key`](Self::key); a message is its run of parts under one key.
/// The stream's order is the only statement of WHERE; a part is the
/// statement of THAT, and of WHICH.
///
/// The payload is MCP's own [`TextContent`], flattened, so what the
/// caller said is expressed in the same vocabulary a tool would use
/// to return it and the model would use to say it — one content
/// model across the whole loop rather than one per direction.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct UserTextContentChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: UserTextContentChunkType,
    /// The message's key, as its enqueue gave it: what tells one
    /// message's parts from another's when several are in flight, and
    /// what a dequeue names to withdraw the message.
    pub key: String,
    /// The block itself.
    #[serde(flatten)]
    pub inner: TextContent,
}

/// [`UserTextContentChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum UserTextContentChunkType {
    #[serde(rename = "user_text_content")]
    #[default]
    UserTextContent,
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/user_image_content_chunk.rs:

//! The user image content chunk.

use rmcp::model::ImageContent;
use serde::{Deserialize, Serialize};

/// An image of a delivered message.
///
/// One block of the message, whole, at the position the message
/// landed: between the tool responses it was folded in behind, or
/// opening the next turn when the assistant had already finished, or
/// first of all, for the message a run started on. A message's parts
/// arrive consecutively, in the message's order, each under its
/// [`key`](Self::key); a message is its run of parts under one key.
/// The stream's order is the only statement of WHERE; a part is the
/// statement of THAT, and of WHICH.
///
/// The payload is MCP's own [`ImageContent`], flattened, so what the
/// caller said is expressed in the same vocabulary a tool would use
/// to return it and the model would use to say it — one content
/// model across the whole loop rather than one per direction.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct UserImageContentChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: UserImageContentChunkType,
    /// The message's key, as its enqueue gave it: what tells one
    /// message's parts from another's when several are in flight, and
    /// what a dequeue names to withdraw the message.
    pub key: String,
    /// The block itself.
    #[serde(flatten)]
    pub inner: ImageContent,
}

/// [`UserImageContentChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum UserImageContentChunkType {
    #[serde(rename = "user_image_content")]
    #[default]
    UserImageContent,
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/user_audio_content_chunk.rs:

//! The user audio content chunk.

use rmcp::model::AudioContent;
use serde::{Deserialize, Serialize};

/// Audio of a delivered message.
///
/// One block of the message, whole, at the position the message
/// landed: between the tool responses it was folded in behind, or
/// opening the next turn when the assistant had already finished, or
/// first of all, for the message a run started on. A message's parts
/// arrive consecutively, in the message's order, each under its
/// [`key`](Self::key); a message is its run of parts under one key.
/// The stream's order is the only statement of WHERE; a part is the
/// statement of THAT, and of WHICH.
///
/// The payload is MCP's own [`AudioContent`], flattened, so what the
/// caller said is expressed in the same vocabulary a tool would use
/// to return it and the model would use to say it — one content
/// model across the whole loop rather than one per direction.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct UserAudioContentChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: UserAudioContentChunkType,
    /// The message's key, as its enqueue gave it: what tells one
    /// message's parts from another's when several are in flight, and
    /// what a dequeue names to withdraw the message.
    pub key: String,
    /// The block itself.
    #[serde(flatten)]
    pub inner: AudioContent,
}

/// [`UserAudioContentChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum UserAudioContentChunkType {
    #[serde(rename = "user_audio_content")]
    #[default]
    UserAudioContent,
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/user_resource_chunk.rs:

//! The user resource chunk.

use rmcp::model::EmbeddedResource;
use serde::{Deserialize, Serialize};

/// An embedded resource of a delivered message.
///
/// One block of the message, whole, at the position the message
/// landed: between the tool responses it was folded in behind, or
/// opening the next turn when the assistant had already finished, or
/// first of all, for the message a run started on. A message's parts
/// arrive consecutively, in the message's order, each under its
/// [`key`](Self::key); a message is its run of parts under one key.
/// The stream's order is the only statement of WHERE; a part is the
/// statement of THAT, and of WHICH.
///
/// The payload is MCP's own [`EmbeddedResource`], flattened, so what the
/// caller said is expressed in the same vocabulary a tool would use
/// to return it and the model would use to say it — one content
/// model across the whole loop rather than one per direction.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct UserResourceChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: UserResourceChunkType,
    /// The message's key, as its enqueue gave it: what tells one
    /// message's parts from another's when several are in flight, and
    /// what a dequeue names to withdraw the message.
    pub key: String,
    /// The block itself.
    #[serde(flatten)]
    pub inner: EmbeddedResource,
}

/// [`UserResourceChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum UserResourceChunkType {
    #[serde(rename = "user_resource")]
    #[default]
    UserResource,
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/user_resource_link_chunk.rs:

//! The user resource link chunk.

use rmcp::model::Resource;
use serde::{Deserialize, Serialize};

/// A resource link of a delivered message.
///
/// One block of the message, whole, at the position the message
/// landed: between the tool responses it was folded in behind, or
/// opening the next turn when the assistant had already finished, or
/// first of all, for the message a run started on. A message's parts
/// arrive consecutively, in the message's order, each under its
/// [`key`](Self::key); a message is its run of parts under one key.
/// The stream's order is the only statement of WHERE; a part is the
/// statement of THAT, and of WHICH.
///
/// The payload is MCP's own [`Resource`], flattened, so what the
/// caller said is expressed in the same vocabulary a tool would use
/// to return it and the model would use to say it — one content
/// model across the whole loop rather than one per direction.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct UserResourceLinkChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: UserResourceLinkChunkType,
    /// The message's key, as its enqueue gave it: what tells one
    /// message's parts from another's when several are in flight, and
    /// what a dequeue names to withdraw the message.
    pub key: String,
    /// The block itself.
    #[serde(flatten)]
    pub inner: Resource,
}

/// [`UserResourceLinkChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum UserResourceLinkChunkType {
    #[serde(rename = "user_resource_link")]
    #[default]
    UserResourceLink,
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/assistant_text_content_chunk.rs:

//! The assistant text content chunk.

use rmcp::model::TextContent;
use serde::{Deserialize, Serialize};

use super::Logprob;

/// Text from the model.
///
/// A DELTA: text arrives in fragments, and a caller
/// concatenates them. Unlike the image and audio chunks, one of
/// these is rarely a whole anything.
///
/// The payload is MCP's own [`TextContent`], flattened, so the
/// content the model produced is expressed in the same vocabulary a
/// tool would use to return it — one content model across the whole
/// loop rather than one per direction.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct AssistantTextContentChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: AssistantTextContentChunkType,
    /// The tool call whose sub-agent produced this chunk; absent on
    /// the main thread. A nested sub-agent names its IMMEDIATE
    /// spawning call, so depth is a chain of ids a caller can follow.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_tool_call_id: Option<String>,
    /// Per-token log probabilities for this fragment, when requested.
    ///
    /// Scoped to THIS chunk's tokens, not the turn's — each delta
    /// carries the probabilities for the text it delivers, so a caller
    /// that concatenates the text can concatenate these alongside it
    /// and keep them aligned.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub logprobs: Option<Vec<Logprob>>,
    /// The content itself.
    #[serde(flatten)]
    pub inner: TextContent,
}

/// [`AssistantTextContentChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum AssistantTextContentChunkType {
    #[serde(rename = "assistant_text_content")]
    #[default]
    AssistantTextContent,
}

impl AssistantTextContentChunk {
    /// Merge the fragment that arrived directly behind this one: the
    /// text concatenates, the log probabilities append. What else the
    /// other fragment carried — its `_meta`, its annotations — is
    /// dropped in favour of this chunk's own; fragments of one run
    /// say the same things there.
    pub fn push(&mut self, other: Self) {
        self.inner.text.push_str(&other.inner.text);
        match (&mut self.logprobs, other.logprobs) {
            (Some(logprobs), Some(other)) => logprobs.extend(other),
            (None, Some(other)) => self.logprobs = Some(other),
            _ => {}
        }
    }
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/assistant_reasoning_chunk.rs:

//! The assistant reasoning chunk.

use rmcp::model::TextContent;
use serde::{Deserialize, Serialize};

use super::Logprob;

/// The model's reasoning.
///
/// A DELTA, like [`AssistantTextContentChunk`](super::AssistantTextContentChunk):
/// fragments arrive and a caller concatenates them.
///
/// The payload is MCP's [`TextContent`], not a bare `String`. Same
/// vocabulary as ordinary content, so nothing here needs its own
/// handling — and `_meta` and `annotations` come along, which a
/// `String` has nowhere to put.
///
/// Structurally identical to the text and refusal chunks, and that is
/// fine precisely because the `type` constants differ: the untagged
/// enum decides on the discriminator, never on shape, so payloads may
/// coincide without becoming ambiguous.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct AssistantReasoningChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: AssistantReasoningChunkType,
    /// The tool call whose sub-agent produced this chunk; absent on
    /// the main thread. A nested sub-agent names its IMMEDIATE
    /// spawning call, so depth is a chain of ids a caller can follow.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_tool_call_id: Option<String>,
    /// Per-token log probabilities for this fragment, when requested.
    ///
    /// Scoped to THIS chunk's tokens, not the turn's — each delta
    /// carries the probabilities for the text it delivers, so a caller
    /// that concatenates the text can concatenate these alongside it
    /// and keep them aligned.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub logprobs: Option<Vec<Logprob>>,
    /// The reasoning itself.
    #[serde(flatten)]
    pub inner: TextContent,
}

/// [`AssistantReasoningChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum AssistantReasoningChunkType {
    #[serde(rename = "assistant_reasoning")]
    #[default]
    AssistantReasoning,
}

impl AssistantReasoningChunk {
    /// Merge the fragment that arrived directly behind this one: the
    /// text concatenates, the log probabilities append. What else the
    /// other fragment carried — its `_meta`, its annotations — is
    /// dropped in favour of this chunk's own; fragments of one run
    /// say the same things there.
    pub fn push(&mut self, other: Self) {
        self.inner.text.push_str(&other.inner.text);
        match (&mut self.logprobs, other.logprobs) {
            (Some(logprobs), Some(other)) => logprobs.extend(other),
            (None, Some(other)) => self.logprobs = Some(other),
            _ => {}
        }
    }
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/assistant_refusal_chunk.rs:

//! The assistant refusal chunk.

use rmcp::model::TextContent;
use serde::{Deserialize, Serialize};

use super::Logprob;

/// The model declining to answer.
///
/// A DELTA, like [`AssistantTextContentChunk`](super::AssistantTextContentChunk):
/// fragments arrive and a caller concatenates them.
///
/// The payload is MCP's [`TextContent`], not a bare `String`. Same
/// vocabulary as ordinary content, so nothing here needs its own
/// handling — and `_meta` and `annotations` come along, which a
/// `String` has nowhere to put.
///
/// Structurally identical to the text and reasoning chunks, and that is
/// fine precisely because the `type` constants differ: the untagged
/// enum decides on the discriminator, never on shape, so payloads may
/// coincide without becoming ambiguous.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct AssistantRefusalChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: AssistantRefusalChunkType,
    /// The tool call whose sub-agent produced this chunk; absent on
    /// the main thread. A nested sub-agent names its IMMEDIATE
    /// spawning call, so depth is a chain of ids a caller can follow.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_tool_call_id: Option<String>,
    /// Per-token log probabilities for this fragment, when requested.
    ///
    /// Scoped to THIS chunk's tokens, not the turn's — each delta
    /// carries the probabilities for the text it delivers, so a caller
    /// that concatenates the text can concatenate these alongside it
    /// and keep them aligned.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub logprobs: Option<Vec<Logprob>>,
    /// The refusal itself.
    #[serde(flatten)]
    pub inner: TextContent,
}

/// [`AssistantRefusalChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum AssistantRefusalChunkType {
    #[serde(rename = "assistant_refusal")]
    #[default]
    AssistantRefusal,
}

impl AssistantRefusalChunk {
    /// Merge the fragment that arrived directly behind this one: the
    /// text concatenates, the log probabilities append. What else the
    /// other fragment carried — its `_meta`, its annotations — is
    /// dropped in favour of this chunk's own; fragments of one run
    /// say the same things there.
    pub fn push(&mut self, other: Self) {
        self.inner.text.push_str(&other.inner.text);
        match (&mut self.logprobs, other.logprobs) {
            (Some(logprobs), Some(other)) => logprobs.extend(other),
            (None, Some(other)) => self.logprobs = Some(other),
            _ => {}
        }
    }
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/assistant_tool_call_chunk.rs:

//! The assistant tool call chunk.

use rmcp::model::RequestMetaObject;
use serde::{Deserialize, Serialize};

/// The model calling a tool.
///
/// A delta, like the text chunks around it. Providers stream tool
/// arguments in fragments, and this chunk carries them as they come:
/// consecutive tool call chunks bearing the same `id` continue one
/// call, their `arguments` concatenating in the order sent; a chunk
/// with a new `id` is a new call. The concatenation is the call's
/// arguments as JSON text, complete only when the last fragment has
/// arrived.
///
/// The fields are this chunk's own, spelled bare — they no longer
/// ride MCP's `CallToolRequestParams`, which is what made a delta
/// form possible: a fragment of a JSON object is not a JSON object,
/// but a fragment of a string is a string.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct AssistantToolCallChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: AssistantToolCallChunkType,
    /// The tool call whose sub-agent produced this chunk; absent on
    /// the main thread. A nested sub-agent names its IMMEDIATE
    /// spawning call, so depth is a chain of ids a caller can follow.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_tool_call_id: Option<String>,
    /// This call's id, which its
    /// [`ToolResponseChunk`](super::ToolResponseChunk) echoes back.
    ///
    /// Ours, not MCP's: in MCP the JSON-RPC envelope correlates a
    /// request with its response, and a stream has no envelope.
    pub id: String,
    /// Protocol-level metadata for the call.
    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
    pub meta: Option<RequestMetaObject>,
    /// The name of the tool to call.
    pub name: String,
    /// One fragment of the call's arguments: JSON text, complete
    /// only once every fragment with this `id` has been
    /// concatenated. Matches the tool's input schema when whole.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub arguments: Option<String>,
}

/// [`AssistantToolCallChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum AssistantToolCallChunkType {
    #[serde(rename = "assistant_tool_call")]
    #[default]
    AssistantToolCall,
}

impl AssistantToolCallChunk {
    /// Merge the fragment that continued this call — same `id`, the
    /// caller checks — by appending its piece of the arguments. What
    /// else the fragment carried says nothing new about a call it is
    /// the continuation of, and is dropped.
    pub fn push(&mut self, other: Self) {
        if let Some(arguments) = other.arguments {
            match &mut self.arguments {
                Some(existing) => existing.push_str(&arguments),
                None => self.arguments = Some(arguments),
            }
        }
    }
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/assistant_image_content_chunk.rs:

//! The assistant image content chunk.

use rmcp::model::ImageContent;
use serde::{Deserialize, Serialize};

/// An image from the model.
///
/// Whole, not a delta — the payload is base64 data with a MIME
/// type, and half of a base64 image is not an image.
///
/// The payload is MCP's own [`ImageContent`], flattened, so the
/// content the model produced is expressed in the same vocabulary a
/// tool would use to return it — one content model across the whole
/// loop rather than one per direction.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct AssistantImageContentChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: AssistantImageContentChunkType,
    /// The tool call whose sub-agent produced this chunk; absent on
    /// the main thread. A nested sub-agent names its IMMEDIATE
    /// spawning call, so depth is a chain of ids a caller can follow.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_tool_call_id: Option<String>,
    /// The content itself.
    #[serde(flatten)]
    pub inner: ImageContent,
}

/// [`AssistantImageContentChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum AssistantImageContentChunkType {
    #[serde(rename = "assistant_image_content")]
    #[default]
    AssistantImageContent,
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/assistant_audio_content_chunk.rs:

//! The assistant audio content chunk.

use rmcp::model::AudioContent;
use serde::{Deserialize, Serialize};

/// Audio from the model.
///
/// Whole, not a delta — as with images, the payload is base64
/// data with a MIME type.
///
/// The payload is MCP's own [`AudioContent`], flattened, so the
/// content the model produced is expressed in the same vocabulary a
/// tool would use to return it — one content model across the whole
/// loop rather than one per direction.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct AssistantAudioContentChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: AssistantAudioContentChunkType,
    /// The tool call whose sub-agent produced this chunk; absent on
    /// the main thread. A nested sub-agent names its IMMEDIATE
    /// spawning call, so depth is a chain of ids a caller can follow.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_tool_call_id: Option<String>,
    /// The content itself.
    #[serde(flatten)]
    pub inner: AudioContent,
}

/// [`AssistantAudioContentChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum AssistantAudioContentChunkType {
    #[serde(rename = "assistant_audio_content")]
    #[default]
    AssistantAudioContent,
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/tool_response_chunk.rs:

//! The tool response chunk.

use rmcp::model::CallToolResult;
use serde::{Deserialize, Serialize};

/// The result of one tool call.
///
/// Arrives whole, unlike the assistant chunks: a tool either returned
/// or it did not, so there is nothing to stream in pieces.
///
/// The result is MCP's own [`CallToolResult`], flattened, so what an
/// MCP server returned passes through verbatim — content blocks,
/// structured content, `isError` and `_meta` included — rather than
/// being re-encoded into a shape of ours that would lose some of it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct ToolResponseChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: ToolResponseChunkType,
    /// The tool call whose sub-agent produced this chunk; absent on
    /// the main thread. A nested sub-agent names its IMMEDIATE
    /// spawning call, so depth is a chain of ids a caller can follow.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_tool_call_id: Option<String>,
    /// The call this answers.
    ///
    /// Ours, not MCP's: a [`CallToolResult`] carries no id at all,
    /// because in MCP it is the payload of a JSON-RPC response and the
    /// request id does the correlating from the envelope. A stream has
    /// no envelope, and results may arrive in a different order than
    /// the calls were made, so the link has to be here.
    pub id: String,
    /// The result itself.
    #[serde(flatten)]
    pub inner: CallToolResult,
}

/// [`ToolResponseChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum ToolResponseChunkType {
    #[serde(rename = "tool_response")]
    #[default]
    ToolResponse,
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/notification_chunk.rs:

//! The notification chunk.

use rmcp::model::MetaObject;
use serde::{Deserialize, Serialize};

/// Something the loop has to say that is not part of its output.
///
/// A warning, a retry, a degraded mode, a failure — anything a
/// provider wants a caller to know about the run itself rather than
/// about what the agent produced.
///
/// In-band rather than a transport signal, because a loop keeps going
/// after most of these and can fail after producing output. Ending the
/// stream without saying why would leave a caller holding a partial
/// result and no way to tell it apart from a complete one.
///
/// # Whether it is fatal is a field, not a type
///
/// [`is_fatal`](Self::is_fatal) says which. A caller that only cares
/// whether the run survived reads one boolean; a caller that wants the
/// whole commentary reads every one of these and decides for itself.
///
/// The alternative was two chunk variants with the same three fields,
/// which would have made "the loop warned me" and "the loop failed"
/// different shapes to parse rather than different values in one.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct NotificationChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: NotificationChunkType,
    /// Whether the run ends here.
    ///
    /// `true` is the loop saying it is over and this is why. `false`
    /// is it saying something and carrying on — a warning, a retry, a
    /// degraded mode, a failure it recovered from.
    ///
    /// Fatal rather than merely wrong, because "wrong" is not a
    /// question a caller can act on and "over" is. A provider that
    /// hits an error and retries past it has not failed, and a caller
    /// told otherwise would abandon a run that was still going.
    ///
    /// Nothing here says what a provider must send either way. What is
    /// worth mentioning, and what it can recover from, is the
    /// provider's to decide.
    pub is_fatal: bool,
    /// The message or details, as an arbitrary JSON value — providers
    /// report failures in shapes we do not get to dictate, and
    /// flattening one into a string would discard the structure a
    /// caller needs to act on it.
    pub message: serde_json::Value,
    /// Arbitrary protocol-level metadata, MCP's `_meta` extension bag.
    ///
    /// Same key and same type as the chunks that flatten rmcp types
    /// carry, so a trace id attached to a content chunk can be
    /// attached here too — these three are ours rather than MCP's, but
    /// that is no reason for them to be the one place a trace stops.
    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
    pub meta: Option<MetaObject>,
}

/// [`NotificationChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum NotificationChunkType {
    #[serde(rename = "notification")]
    #[default]
    Notification,
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/usage_chunk.rs:

//! The usage chunk.

use rmcp::model::MetaObject;
use serde::{Deserialize, Serialize};

/// Token usage.
///
/// Emitted as the loop goes rather than once at the end, so a caller
/// watches consumption grow instead of learning it after the fact.
/// Each chunk is a DELTA — every field is additive, so a caller that
/// wants a running total sums them.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub struct UsageChunk {
    /// The discriminator. See [`AgenticLoopChunk`](super::AgenticLoopChunk).
    pub r#type: UsageChunkType,
    /// Tokens generated.
    pub completion_tokens: u64,
    /// Prompt tokens consumed.
    pub prompt_tokens: u64,
    /// The two above, summed.
    pub total_tokens: u64,
    /// Arbitrary protocol-level metadata, MCP's `_meta` extension bag.
    ///
    /// Same key and same type as the chunks that flatten rmcp types
    /// carry, so a trace id attached to a content chunk can be
    /// attached here too — these three are ours rather than MCP's, but
    /// that is no reason for them to be the one place a trace stops.
    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
    pub meta: Option<MetaObject>,
}

/// [`UsageChunk`]'s discriminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub enum UsageChunkType {
    #[serde(rename = "usage")]
    #[default]
    Usage,
}

and by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/response/logprobs.rs:

//! Per-token log probabilities.
//!
//! There is no `Logprobs` wrapper. The old shape had one so a single
//! chunk could carry `content` and `refusal` token lists side by side;
//! here the chunk's own `type` already says which it is, so a bare
//! list of tokens is the whole of it.

use serde::{Deserialize, Serialize};

/// One token, and what the model thought of it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub struct Logprob {
    /// The token as text.
    pub token: String,
    /// The token's raw bytes, for tokens that are not valid UTF-8 on
    /// their own — a multi-byte character split across two tokens
    /// leaves each half unrepresentable as text.
    pub bytes: Option<Vec<u8>>,
    /// The log probability the model assigned it.
    ///
    /// A decimal, not a float: these round-trip through JSON, and
    /// binary floating point loses exactly the low-order digits that
    /// make two providers' numbers comparable.
    pub logprob: rust_decimal::Decimal,
    /// What the model nearly chose instead, at this position. Empty
    /// when alternatives were not requested.
    pub top_logprobs: Vec<TopLogprob>,
}

/// An alternative the model considered at one position.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub struct TopLogprob {
    /// The token as text.
    pub token: String,
    /// The token's raw bytes. See [`Logprob::bytes`].
    pub bytes: Option<Vec<u8>>,
    /// Its log probability, when the provider reports one.
    pub logprob: Option<rust_decimal::Decimal>,
}