# /agent/run

> A prompt, and the loop that runs on it: the proxy sends zero or more chunks and closes the connection, or sends one error as its last message; the first message states whether a loop ran.

Canonical: https://provider.diverge.network/2.3.0/proxy/agent/run/
Specification revision: 2.3.0

The server opens `/agent/run` and sends exactly one message, the
request defined below as JSON, carrying the prompt. The proxy sends
the loop as messages and closes the connection.

```text
server → proxy:   [request JSON]                          once
proxy → server:   [0][chunk JSON] … [0][chunk JSON]       the loop
                  [1][error JSON]                         last, when there was no loop or it did not finish
```

- **The first message decides.** An error as the first message states
  that no loop ran. A chunk as the first message states that a loop
  ran. An error after one or more chunks states that the loop ended
  without finishing, and it is the last message the proxy sends.
- **A chunk is output.** A notification chunk marked fatal is the
  last word of the loop. It is a chunk and not an error, and the
  close follows it.
- **One loop at a time.** A loop asked for while a loop runs is
  answered with an error as the first message.
- **No repetition.** An abrupt end of the path is a loop cut short.
  No party retries it.

The request is defined by
`diverge-provider-sdk/src/shared/containers/run_loop/request/request.rs`:

```rust
//! What one loop is asked.

use serde::{Deserialize, Serialize};
use serde_json::Error;

use crate::decode::Decode;
use crate::encode::{Encode, Writer};

/// Run one loop on this prompt.
///
/// The agent is not here: it was on the request that made the
/// container, registered once, and it never changes. The prompt is
/// each loop's, because a container runs loops one after another —
/// each resuming the conversation the last one left — and every one
/// is asked something.
///
/// POST-TRANSFORM: the result of whatever built the request — a
/// system prompt applied, a history folded in — so a provider never
/// rewrites what it was given.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct Request {
    /// What the loop is asked.
    pub prompt: String,
}

/// Its JSON, and nothing in front of it. The tag that says which
/// request this is belongs to whichever frame carries it.
impl Encode for Request {
    /// The ordinary JSON failure.
    type Error = Error;

    fn encode(&self, out: &mut Writer<'_>) -> Result<(), Error> {
        serde_json::to_writer(out, self)
    }
}

impl Decode<'_> for Request {
    /// The ordinary JSON failure.
    type Error = Error;

    fn decode(bytes: &[u8]) -> Result<Self, Error> {
        serde_json::from_slice(bytes)
    }
}
```

The messages are defined by
`diverge-provider-sdk/src/shared/containers/run_loop/response/frame.rs`:

```rust
//! What an agentic loop channel carries back.

use std::fmt;

use super::AgenticLoopChunk;
use crate::decode::Decode;
use crate::encode::{Encode, Writer};
use crate::shared::error::Error;

/// A piece of the loop, or the news that there will not be one.
///
/// A payload leads with one byte saying which — `0` for
/// [`Chunk`](Self::Chunk), `1` for [`Error`](Self::Error) — and the
/// rest is that variant's own JSON.
///
/// # 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. Leaving the two to be distinguished by their JSON
/// would mean a provider's error text could be read as a chunk, and
/// the failure would look like output.
///
/// # This is not [`NotificationChunk`](super::NotificationChunk)
///
/// They are both failures and they are not the same failure. A
/// [`NotificationChunk`](super::NotificationChunk) with
/// [`is_fatal`](super::NotificationChunk::is_fatal) set is part of the
/// loop's OUTPUT: it arrives as a [`Chunk`](Self::Chunk) like any
/// other, and exists because a loop can fail after producing output —
/// ending the stream silently would leave a caller unable to tell a
/// partial result from a complete one. An [`Error`](Self::Error) is
/// not part of the loop. It is what a provider sends when there is no
/// loop to report on — the agent the image would not take, the
/// container gone — and it carries a bare JSON value because this
/// specification does not describe what providers can go wrong with.
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
    /// One chunk of the loop. Tag `0`.
    Chunk(AgenticLoopChunk),
    /// A failure. Tag `1`.
    ///
    /// See [`shared::error::Error`](crate::shared::error::Error) for
    /// why it says so little.
    Error(Error),
}

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

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

impl Encode for Frame {
    /// The ordinary JSON failure, from either half.
    type Error = serde_json::Error;

    // 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<(), serde_json::Error> {
        match self {
            Frame::Chunk(chunk) => {
                out.extend_from_slice(&[CHUNK]);
                serde_json::to_writer(out, chunk)
            }
            Frame::Error(error) => {
                out.extend_from_slice(&[ERROR]);
                error.encode(out)
            }
        }
    }
}

impl Decode<'_> for Frame {
    /// Four ways to fail, and each names which half 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 {
            CHUNK => serde_json::from_slice(rest)
                .map(Frame::Chunk)
                .map_err(FrameError::Chunk),
            ERROR => {
                Error::decode(rest).map(Frame::Error).map_err(FrameError::Error)
            }
            tag => Err(FrameError::UnknownTag(tag)),
        }
    }
}

/// An agentic loop 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 neither of this frame's two.
    UnknownTag(u8),
    /// The chunk did not parse.
    Chunk(serde_json::Error),
    /// The error did not parse.
    ///
    /// Which is its own small joke and its own real problem: a
    /// provider whose failure report is malformed has told a caller
    /// that something went wrong and nothing else.
    Error(serde_json::Error),
}

impl fmt::Display for FrameError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameError::Empty => {
                f.write_str("agentic loop frame is empty")
            }
            FrameError::UnknownTag(tag) => {
                write!(f, "unknown agentic loop frame tag {tag}")
            }
            FrameError::Chunk(error) => {
                write!(f, "agentic loop chunk did not parse: {error}")
            }
            FrameError::Error(error) => {
                write!(f, "agentic loop error did not parse: {error}")
            }
        }
    }
}

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

The chunks that follow the first byte `0` are defined by the
following files of `diverge-provider-sdk/src/shared/containers/run_loop/response/`:

`diverge-provider-sdk/src/shared/containers/run_loop/response/chunk.rs`:

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

use serde::{Deserialize, Serialize};

use super::{
    AssistantAudioContentChunk, AssistantImageContentChunk,
    AssistantReasoningChunk, AssistantRefusalChunk,
    AssistantTextContentChunk, AssistantToolCallChunk, NotificationChunk,
    ToolResponseChunk, UsageChunk, UserChunk,
};

/// 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.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[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),
    /// An enqueued message entering the conversation. See
    /// [`UserChunk`].
    User(UserChunk),
    /// Token usage so far. See [`UsageChunk`].
    Usage(UsageChunk),
    /// Something about the run itself. See [`NotificationChunk`].
    Notification(NotificationChunk),
}
```

`diverge-provider-sdk/src/shared/containers/run_loop/response/user_chunk.rs`:

```rust
//! The user chunk.

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

/// An enqueued message entering the conversation.
///
/// Emitted 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. The stream's order is the
/// only statement of WHERE; this chunk is the statement of THAT, and
/// of WHICH.
///
/// # It carries the prompt itself
///
/// The delivered message's text, verbatim — so the chunk stands on
/// its own in the response stream and in any history built from it,
/// and a caller with several enqueues in flight tells them apart by
/// content. The
/// [`Delivered`](crate::shared::containers::enqueue::response::Frame::Delivered)
/// answer on the enqueue's own channel says the same event from the
/// channel's side.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UserChunk {
    /// The discriminator. Fixed, and the reason
    /// [`AgenticLoopChunk`](super::AgenticLoopChunk) can be untagged:
    /// serde has no tag of its own to read, so each variant's payload
    /// carries a `type` no other variant can match.
    pub r#type: UserChunkType,
    /// The delivered message's text, exactly as enqueued.
    pub prompt: String,
    /// 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.
    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
    pub meta: Option<MetaObject>,
}

/// [`UserChunk`]'s discriminator.
///
/// One variant, so the field can hold exactly one value. A type rather
/// than a bare `String` because a wrong value then fails to
/// deserialize instead of arriving as data nobody checks.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum UserChunkType {
    #[serde(rename = "user")]
    #[default]
    User,
}
```

`diverge-provider-sdk/src/shared/containers/run_loop/response/assistant_text_content_chunk.rs`:

```rust
//! 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)]
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)]
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),
            _ => {}
        }
    }
}
```

`diverge-provider-sdk/src/shared/containers/run_loop/response/assistant_reasoning_chunk.rs`:

```rust
//! 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)]
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)]
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),
            _ => {}
        }
    }
}
```

`diverge-provider-sdk/src/shared/containers/run_loop/response/assistant_refusal_chunk.rs`:

```rust
//! 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)]
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)]
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),
            _ => {}
        }
    }
}
```

`diverge-provider-sdk/src/shared/containers/run_loop/response/assistant_tool_call_chunk.rs`:

```rust
//! 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)]
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)]
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),
            }
        }
    }
}
```

`diverge-provider-sdk/src/shared/containers/run_loop/response/assistant_image_content_chunk.rs`:

```rust
//! 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)]
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)]
pub enum AssistantImageContentChunkType {
    #[serde(rename = "assistant_image_content")]
    #[default]
    AssistantImageContent,
}
```

`diverge-provider-sdk/src/shared/containers/run_loop/response/assistant_audio_content_chunk.rs`:

```rust
//! 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)]
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)]
pub enum AssistantAudioContentChunkType {
    #[serde(rename = "assistant_audio_content")]
    #[default]
    AssistantAudioContent,
}
```

`diverge-provider-sdk/src/shared/containers/run_loop/response/tool_response_chunk.rs`:

```rust
//! 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)]
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)]
pub enum ToolResponseChunkType {
    #[serde(rename = "tool_response")]
    #[default]
    ToolResponse,
}
```

`diverge-provider-sdk/src/shared/containers/run_loop/response/notification_chunk.rs`:

```rust
//! 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)]
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)]
pub enum NotificationChunkType {
    #[serde(rename = "notification")]
    #[default]
    Notification,
}
```

`diverge-provider-sdk/src/shared/containers/run_loop/response/usage_chunk.rs`:

```rust
//! 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)]
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)]
pub enum UsageChunkType {
    #[serde(rename = "usage")]
    #[default]
    Usage,
}
```

`diverge-provider-sdk/src/shared/containers/run_loop/response/logprobs.rs`:

```rust
//! 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)]
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)]
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>,
}
```
