/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.
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.
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:
//! 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:
//! 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:
//! 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:
//! 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:
//! 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:
//! 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:
//! 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:
//! 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:
//! 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:
//! 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:
//! 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:
//! 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:
//! 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:
//! 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>,
}