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.
| Header | Value |
|---|---|
Content-Type | application/json |
Accept | text/event-stream, application/json |
traceparent | W3C trace context |
X-Substructure-Signature | sha256=<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
}