Response

The server sends Begun once, carrying the dependencies the container declared and the secrets it takes, or an error and the finish; after Begun it sends Active before each loop, every chunk of that loop as a response, and Inactive carrying the rotating secrets after it, and the finish when the server ends or after the one error it sends when it cannot collect them.

[0][registration JSON …]    begun: the container holds its arguments, and these are its dependencies and its secrets
[1][error JSON …]           not begun: the connection had begun, or the arguments were refused; or, after begun, the rotating secrets could not be collected
[2][chunk JSON …]           one chunk of the conversation
[3]                         active: a loop began
[4][values JSON …]          inactive: that loop ended, and these are its rotating secrets
  • The sequence. Exactly one response whose first byte is 0, followed by zero or more loops, followed by the response finish when the server ends; or exactly one response whose first byte is 1, followed by the response finish. A loop is exactly one response whose payload is the byte 3 and nothing else, followed by zero or more responses whose first byte is 2, in the order the container produced them, followed by exactly one response whose first byte is 4, or, where the server cannot collect the loop’s rotating secrets, by exactly one response whose first byte is 1 and the response finish. The server sends no response whose first byte is 2, 3 or 4 before Begun, none whose first byte is 2 outside a loop, and none after the finish.
  • Active and inactive. The server sends the byte 3 when the agent’s server answers the loop’s POST /run with 2xx, before the first chunk of that loop, and the byte 4 when the event stream of that answer ends, after the last chunk of that loop, whether the stream ended cleanly or by failing, and after the program has handed back the loop’s rotating secrets. The server produces both itself and relays neither from the agent’s server: every event of the agent’s server is sent under the byte 2, so no event is read as either. The byte 3 carries nothing after it. The server sends no byte 3 while a loop is open, and no byte 4 while none is.
  • The registration. The bytes after the byte 0 are a JSON object of two members, each absent when empty: dependencies, an array of the dependency tool templates the container’s program answered its registration with, each as the registration channel of the run defines one, in the order the program declared them; and secrets, an object whose every member is a secret’s name, as the container’s template declares it, to the mode the program takes it in, the string fixed or rotating, in the order the program declared them. The server sends both exactly as the program produced them and reads neither.
  • The values. The bytes after the byte 4 are a JSON object whose every member is the name of a secret the 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 collects them from the program after the loop’s stream ends, and sends them as the program handed them back. From the byte 4 the program holds no values, and the server delivers no message to a program that declared a secret until the next secrets channel is answered Fed.
  • The chunk. The bytes after the byte 2 are one chunk as the containers::agents::run response defines it, as JSON, which the server sends as the agent’s server produced it. The first chunks of a loop are the user parts of the message it started on — one chunk per content block, each carrying the message’s key — or of every message it started on, in the order sent.
  • _meta. The server sets nothing under _meta. Every member of _meta, and every other member of the chunk, is sent as the agent’s server produced it.
  • The error. After the byte 1, exactly one JSON value in the form defined on the volumes::list response page. Refused arguments carry the container’s own words. After Begun, the byte 1 states that the program did not hand back every rotating secret the server asked for at a loop’s end; the error names the secret and carries no value, and the finish follows it. A value the program did not hand back is not written anywhere by the server.
  • A finish with nothing. A response finish that no response precedes states that the server could not read the request.

The response is defined by diverge-sdk-rs/src/container_proxy/outside/endpoints/agents/begin/server/response/frame.rs:

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

use std::fmt;

use super::Registered;
use crate::wire::decode::Decode;
use crate::wire::encode::{Encode, Writer};
use crate::provider::endpoints::containers::agents::run::server::response::AgenticLoopChunk;
use crate::shared::containers::secrets::Values;
use crate::shared::error::Error;

/// A begin's answer: that the connection has begun, then the agent's
/// conversation for as long as the connection lives, each loop of it
/// bracketed by the proxy's own word that it began and that it ended
/// — or a failure.
///
/// A payload leads with one byte saying which — `0` for
/// [`Begun`](Self::Begun), `1` for [`Error`](Self::Error), `2` for
/// [`Chunk`](Self::Chunk), `3` for [`Active`](Self::Active), `4` for
/// [`Inactive`](Self::Inactive) — and the rest is that variant's own
/// JSON, or nothing: the one bare word carries nothing after the
/// byte.
///
/// # Begun, then the conversation
///
/// | the scope | means |
/// |-----------|-------|
/// | a begun, then quiet, and stays open | the container holds its arguments, what its program declared is known, and no loop has run |
/// | a begun, an active, then chunks, and stays open | 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 collected; the agent is quiet until the next message |
/// | an error, then a finish | it has not begun — this connection had already begun, or the arguments were refused; or, after `Begun`, a loop's secrets could not be collected |
/// | a finish, with no error | the proxy is ending |
///
/// # The conversation is this stream
///
/// What the agent says arrives here, chunk by chunk, in the order
/// the agent's server streams them, and the server carries each one
/// on to the caller as the run scope's own
/// [`Chunk`](crate::provider::endpoints::containers::agents::run::server::response::Frame::Chunk). Nothing opens a
/// loop: an [`Enqueue`](super::super::super::client::channel_request::Frame::Enqueue)
/// with no loop running starts one on its message, and one while a
/// loop runs joins the queue. The tools family's stream carries the
/// two words and nothing else after `Begun`.
///
/// # The proxy marks the loop, and only the proxy can
///
/// A loop is one `POST /run` on the agent's server: it begins when
/// that call answers `2xx`, and it 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
/// here as [`Active`](Self::Active) before the loop's first chunk and
/// [`Inactive`](Self::Inactive) after its last — the latter once the
/// program has handed back its rotating secrets, which ride it. The
/// program's stream is chunks and only chunks, every one of them a
/// `Chunk` here, so nothing a program writes is read as either word:
/// see
/// [the run's frame](crate::provider::endpoints::containers::agents::run::server::response::Frame)
/// for why the byte in front is what keeps that so. Between an
/// `Inactive` and the next `Active` the agent says nothing; quiet
/// between an `Active` and its `Inactive` is an agent still working.
///
/// # Nothing is delivered before a feed
///
/// After `Begun`, and after every `Inactive`, the proxy delivers
/// nothing to a program that declared a secret until the server has
/// fed the values on a
/// [`Secrets`](super::super::super::client::channel_request::Frame::Secrets)
/// channel and the proxy has answered it `Fed`: a message enqueued
/// meanwhile waits. A program that declared none is never gated.
///
/// The asks the container makes, and the family's other exchanges,
/// are channels, not this stream. It carries no readiness signal
/// beyond `Begun`: the proxy is here, the arguments are held, and
/// channels may be opened.
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
    /// The connection has begun, the container holds its arguments
    /// for its life, and this is what its program declared. Tag `0`.
    ///
    /// Arrives once, when the container's server has taken the
    /// arguments and answered its registration — as JSON, a
    /// [`Registered`]: the dependencies, a list, empty for a program
    /// that needs none, and the secrets it takes, a map, empty for a
    /// program that takes none; see
    /// [`registration`](crate::shared::containers::registration) for
    /// what the provider does with both. A channel on this scope is
    /// opened only after it.
    Begun(Registered),
    /// A failure. Tag `1`.
    ///
    /// A second begin on a connection that had one, or arguments the
    /// container refused — the container's server's own words: a
    /// value the image will not take — or, after `Begun`, a loop's
    /// rotating secrets the program would not hand back, named and
    /// never valued. 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.
    Error(Error),
    /// One chunk of the agent's conversation. Tag `2`.
    ///
    /// After `Begun`, zero or more, for as long as the connection
    /// lives, each as the agent's server streamed it — the proxy sets
    /// nothing under its `_meta`; the daemon that keeps it does, see
    /// [`shared::mcp`](crate::shared::mcp); never before
    /// `Begun`, 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 `3`, and nothing after it.
    ///
    /// The agent's server answered `POST /run` with `2xx`: a loop is
    /// running, and the chunks that follow until the matching
    /// [`Inactive`](Self::Inactive) are its output. Sent by the proxy,
    /// never relayed from the program.
    Active,
    /// A loop ended, and these are its rotating secrets. Tag `4`.
    ///
    /// The event stream of the loop's `POST /run` ended — cleanly, or
    /// by dying; a loop that failed said so in a fatal notification
    /// chunk before this — and the program handed back every
    /// `rotating` secret it declared, each by name to the value it
    /// leaves in it, as JSON after the byte: the [`Values`] the
    /// caller writes back. Empty for a program that declared none
    /// rotating. The agent is quiet until the next message starts the
    /// next loop. Sent by the proxy, never relayed from the program.
    Inactive(Values),
}

/// Tag for [`Frame::Begun`].
const BEGUN: u8 = 0;

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

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

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

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

/// A tag, and that variant's own JSON — or the tag alone.
impl Encode for Frame {
    /// The ordinary JSON failure.
    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::Begun(registered) => {
                out.extend_from_slice(&[BEGUN]);
                serde_json::to_writer(out, registered)
            }
            Frame::Error(error) => {
                out.extend_from_slice(&[ERROR]);
                error.encode(out)
            }
            Frame::Chunk(chunk) => {
                out.extend_from_slice(&[CHUNK]);
                serde_json::to_writer(out, chunk)
            }
            Frame::Active => {
                out.extend_from_slice(&[ACTIVE]);
                Ok(())
            }
            Frame::Inactive(values) => {
                out.extend_from_slice(&[INACTIVE]);
                serde_json::to_writer(out, values)
            }
        }
    }
}

impl Decode<'_> for Frame {
    /// Six ways to fail, and four of them are JSON.
    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 {
            BEGUN => serde_json::from_slice(rest).map(Frame::Begun).map_err(FrameError::Begun),
            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 agents begin 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 five.
    ///
    /// What a proxy newer than its server 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),
    /// What the program declared did not parse.
    Begun(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 begin response frame is empty")
            }
            FrameError::UnknownTag(tag) => {
                write!(f, "unknown agents begin response frame tag {tag}")
            }
            FrameError::Begun(error) => {
                write!(f, "agents begin registration did not parse: {error}")
            }
            FrameError::Error(error) => {
                write!(f, "agents begin error did not parse: {error}")
            }
            FrameError::Chunk(error) => {
                write!(f, "agents begin chunk did not parse: {error}")
            }
            FrameError::Inactive(error) => {
                write!(f, "agents begin inactive secrets did not parse: {error}")
            }
        }
    }
}

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

and by diverge-sdk-rs/src/container_proxy/outside/endpoints/agents/begin/server/response/registered.rs:

//! What the container's program declared when it registered.

use serde::{Deserialize, Serialize};

use crate::shared::containers::dependencies::Template;
use crate::shared::containers::secrets::Declaration;

/// The program's answer to its registration, as `Begun` carries it:
/// the dependencies it declared, each a tool container the caller
/// deploys, and the secrets it takes, each by name to its mode. Both
/// are the container's for its life. The provider carries them to
/// the caller in one
/// [`registration`](crate::shared::containers::registration) ask when
/// either is non-empty, and reads neither.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Registered {
    /// The dependencies, in the order the program declared them: see
    /// [`dependencies`](crate::shared::containers::dependencies).
    /// Absent when empty.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub dependencies: Vec<Template>,
    /// The secrets the program takes, each by name to its mode, in
    /// the order the program declared them: see
    /// [`secrets`](crate::shared::containers::secrets). Absent when
    /// empty.
    #[serde(default, skip_serializing_if = "Declaration::is_empty")]
    pub secrets: Declaration,
}

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

//! Which secrets a program takes, and how.

use indexmap::IndexMap;

use super::Mode;

/// The program's answer about its secrets: each name it takes, as
/// its template declares it, to the [`Mode`] it takes it in, in the
/// order the program names them. A name the template does not
/// declare is the registration's error; a name the template declares
/// and this omits is a secret the program does not want, and is never
/// fed. Empty, the program takes none: nothing is ever withheld from
/// it for want of a feed, and nothing is collected from it.
pub type Declaration = IndexMap<String, Mode>;

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

//! How a program takes one of its secrets.

use serde::{Deserialize, Serialize};

/// What a program does with a secret, declared per name when it
/// registers: read it, or hold and change it. Snake case on the
/// wire: `"fixed"`, `"rotating"`.
///
/// The mode is the program's to declare and the caller's to honour:
/// a template names the secret, a record binds the name to a vault
/// key, and this says whether the value the container is handed is
/// one of the key's to read or one of the key's to keep.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
    /// Read, never locked: at every activation the caller hands the
    /// container a random value among the key's, and takes nothing
    /// back at the inactive. Several containers may read the same
    /// value at once.
    Fixed,
    /// Held and written back: at every activation the caller locks
    /// one free value of the key's to the container and hands it
    /// over; at the inactive the caller collects what the program
    /// made of it, writes that into the same place and drops the
    /// lock. No two containers hold one value at once, and a key
    /// with every value held makes the next activation fail.
    Rotating,
}

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>;