Protocol

The wire reference. The engine and your worker exchange JSON over HTTP.

Types are shown in TypeScript notation. ? marks a field that may be omitted. unknown is any JSON value. Timestamps are RFC 3339 strings. Decimal money values are strings.

The machine-readable source of truth is schemas/protocol.schema.json (all types) and schemas/worker.openapi.json (the worker endpoint). To generate bindings instead of writing them, see Typed bindings.

Delivery

The engine POSTs a DecisionRequest to your worker's endpoint.

HeaderValue
Content-Typeapplication/json
Accepttext/event-stream, application/json
traceparentW3C trace context
X-Substructure-Signaturesha256=<hex HMAC-SHA256 of the body>, when a signing secret is configured

Respond with application/json (a DecisionResponse), or text/event-stream to stream (see Streaming).

Decision request

type DecisionRequest = {
    session_id: string
    decision_id: string
    agent_id: string
    identity: WorkerIdentity
    trigger: Trigger
    proposed: DecisionResponse      // empty when the engine has no default
    state: unknown              // your agent state, stored verbatim
    agent: AgentConfig | null
    calls: Call[]               // in-flight calls
    pending_calls: number       // in-flight tool and sub-agent calls
    messages: Message[]         // the active conversation path
    message_tree: MessageTree
    ancestry: string[]          // ancestor session ids, for sub-agents
    attempts: number            // delivery attempts for this decision
    deadline: string | null
    turn_id: string | null      // the turn's idempotency id
}

type WorkerIdentity = {   // the session owner, without the tenant
    id?: string
    metadata?: Record<string, string>
}

type Call = {
    id: string
    kind: "tool_call" | "llm_call" | "sub_agent"
    status: "pending" | "completed" | "failed" | "retry_scheduled" | "queued"
    attempt: number
    deadline?: string
    anchor?: string             // the tree node the call was requested at
    name?: string               // tool calls
    arguments?: string          // tool calls
    handler?: Handler
    stream?: boolean            // llm calls
    agent_id?: string           // sub-agents
    session_id?: string         // sub-agents
}

type Handler = "server" | "worker" | "client"

proposed is the engine's default continuation. Accept it by echoing it as the decision. It is null when only the worker knows what to do, such as running one of its own tools.

Decision response

type DecisionResponse = {
    messages?: DraftMessage[]   // messages to record
    actions?: Action[]          // what the engine should do next
    state?: unknown             // omitted or null keeps current state
    agent?: AgentConfig         // omitted keeps current config
}

To clear state, write a non-null empty value such as {}.

Triggers

type Trigger =
    | { type: "session.start" }
    | {
          type: "client.messages"
          messages: DraftMessage[]  // the client's full conversation view
          new_from: number          // index of the first new message
          client: ClientContext
      }
    | { type: "client.action"; name: string; args?: unknown }
    | {
          type: "tool.execute"
          id: string
          name: string
          arguments: string         // the raw argument string
          input: ToolInput          // the engine's validation of arguments
          attempt: number
          deadline?: string
      }
    | {
          type: "tool.finished"
          id: string
          ok: boolean
          name: string
          result?: string
          error?: string
      }
    | {
          type: "llm.execute"
          id: string
          request: unknown          // LlmRequest, or provider-native when format is set
          format?: "openai" | "anthropic"
          stream: boolean
          attempt: number
          deadline?: string
      }
    | {
          type: "llm.finished"
          id: string
          ok: boolean
          message?: DraftMessage
          truncated: boolean
          usage?: unknown
          cost?: string
          error?: string
          code?: ErrorCode
          detail?: unknown
      }
    | {
          type: "sub_agent.finished"
          id: string
          ok: boolean
          session_id: string
          agent_id: string
          result?: string
          error?: string
      }
    | { type: "interrupt.resumed"; interrupt_id: string; payload?: unknown }

type ToolInput =
    | { status: "valid"; value: unknown }     // parsed, conforms to the tool's input schema
    | { status: "invalid"; value: unknown; error: string }
    | { status: "malformed"; error: string }  // not a JSON object

type ErrorCode =
    | "provider_error"
    | "rate_limited"
    | "refused"
    | "budget_exceeded"
    | "deadline_exceeded"

type ClientContext = {
    tools?: AgentTool[]         // client-executed tools, layered onto the proposed config
    context?: unknown[]
    state?: unknown
    forwarded_props?: unknown
}

Answer tool.execute with tool.result or tool.error. Answer llm.execute with llm.result or llm.error, or stream.

Actions

type Action =
    | {
          type: "llm.call"        // all fields optional; omitted fields fill
          id?: string             // from the agent config, then engine defaults
          model?: string
          messages?: DraftMessage[]  // explicit messages suppress system-prompt injection
          tools?: LlmTool[]
          temperature?: number
          max_completion_tokens?: number
          reasoning?: ReasoningConfig
          stream?: boolean
          retry?: RetryPolicy
          handler?: "server" | "worker"  // default server
      }
    | {
          type: "tool.call"
          id?: string             // omitted: the engine mints one
          name: string
          arguments: unknown
          handler?: "worker" | "client"  // default worker
          retry?: RetryPolicy     // default: no retry
      }
    | {
          type: "tool.result"
          id?: string             // id and attempt omitted: taken from the
          attempt?: number        // answering tool.execute trigger
          result: unknown
      }
    | {
          type: "tool.error"
          id?: string
          attempt?: number
          error: string
          retryable?: boolean     // default false: terminal
          code?: ErrorCode
          detail?: unknown
      }
    | {
          type: "llm.result"
          id?: string
          attempt?: number
          response: unknown       // LlmResponse, or provider-native when the
      }                           // answered llm.execute carried a format
    | {
          type: "llm.error"
          id?: string
          attempt?: number
          error: string
          retryable?: boolean     // default false: terminal
          code?: ErrorCode
          detail?: unknown
      }
    | {
          type: "sub_agent.spawn"
          session_id: string
          agent_id: string
          tool_call_id: string    // the model tool call this delegation answers
          retry?: RetryPolicy
      }
    | { type: "message.send"; session_id: string; message: DraftMessage }
    | {
          type: "interrupt"
          interrupt_id?: string   // omitted: the engine mints one
          reason: string
          payload?: unknown
      }
    | { type: "done"; data?: unknown }

A bare { "type": "llm.call" } prompts per the agent's identity over the current conversation.

Agent config

type AgentConfig = {
    model: string               // the only required field
    system?: string
    stream?: boolean            // default false
    handler?: "server" | "worker"  // where LLM calls run; default server
    format?: "openai" | "anthropic"  // wire format for worker-handled LLM
                                     // calls; requires handler worker
    retry?: RetryPolicy
    tools?: AgentTool[]
    sub_agents?: SubAgent[]
}

type AgentTool = {
    name: string
    description?: string
    input?: unknown             // JSON Schema for arguments; omitted: no arguments
    output?: unknown            // JSON Schema results must satisfy; a violating
                                // result settles as a terminal error
    handler?: "worker" | "client"  // default worker
}

type SubAgent = {
    id: string                  // the agent to spawn, and the tool name the model sees
    description?: string
}

type RetryPolicy = {
    timeout_secs: number | null
    max_retries: number
    backoff_base_secs: number
    backoff_max_secs: number
}

Messages

type Role = "system" | "user" | "assistant" | "tool"

type Content = string | ContentPart[]

type ContentPart =
    | { type: "text"; text: string }
    | { type: "image_url"; image_url: { url: string } }
    | { type: "file"; file: { filename: string; file_data: string } }
    | { type: "input_audio"; input_audio: { data: string; format: string } }
    | { type: "video_url"; video_url: { url: string } }

type ToolCall = {
    id: string
    type: string
    function: { name: string; arguments: string }
}

// A recorded message. DraftMessage is the same shape with an optional id,
// used wherever a message is not yet recorded.
type Message = {
    id: string
    role: Role
    content?: Content
    tool_calls?: ToolCall[]
    tool_call_id?: string
    name?: string
}

type MessageTree = {
    nodes: { message: Message; parent_id?: string }[]
    head_id?: string
}

LLM requests and responses

The neutral shapes used when the agent config sets no format.

type LlmRequest = {
    model: string
    messages: DraftMessage[]
    tools?: LlmTool[]
    temperature?: number
    max_completion_tokens?: number
    reasoning?: ReasoningConfig
}

type LlmTool = {
    name: string
    description: string
    input?: unknown             // JSON Schema; omitted: no arguments
    output?: unknown
}

type ReasoningConfig = {
    effort?: "xhigh" | "high" | "medium" | "low" | "minimal" | "none"
    max_tokens?: number
    exclude?: boolean
    enabled?: boolean
}

type LlmResponse = {
    model: string
    content?: string
    tool_calls?: ToolCall[]
    finish_reason?: string
    usage?: unknown
    cost?: string               // dollars, decimal string
    images?: { url: string }[]
}

Client inputs

What clients submit. The target session rides the envelope: the CLI's --session, or the client API's request body.

type ClientInput =
    | {
          type: "client.message"
          agent_id: string
          turn_id?: string        // idempotency key
          message: DraftMessage
          stream?: boolean
      }
    | {
          type: "client.messages"
          agent_id: string
          turn_id?: string
          messages: DraftMessage[]  // the client's full conversation view
          stream?: boolean
          client?: ClientContext
      }
    | {
          type: "client.action"
          agent_id: string
          turn_id?: string
          name: string
          args?: unknown
      }
    | { type: "interrupt.resume"; interrupt_id: string; payload?: unknown }
    | { type: "tool.result"; id: string; attempt?: number; result?: unknown }
    | {
          type: "tool.error"
          id: string
          error: string
          retryable: boolean
          attempt?: number
      }

agent_id routes the turn and creates the session if new. Resubmitting a completed turn_id returns the existing turn instead of running it again.

Streaming

A worker answering an llm.execute with stream: true may respond with text/event-stream instead of JSON:

event: llm.token.delta
data: { "text": "hel" }

event: llm.token.delta
data: { "text": "lo" }

event: decision.result
data: { "actions": [{ "type": "llm.result", "response": … }] }

llm.token.delta frames carry a StreamDelta, or the provider's native stream events when the llm.execute carried a format. The stream must end with one decision.result frame (a DecisionResponse), or decision.error with message and retryable (default true).

type StreamDelta = {
    text?: string
    reasoning?: string
    tool_calls?: { id: string; name?: string; arguments?: string }[]
    finish_reason?: string
}