# REST API

The engine serves four HTTP surfaces on one host. Requests and responses are
JSON; streaming endpoints return `text/event-stream`. Every shape used in a body
is defined under [Objects](#objects), so this page stands on its own.

| Surface | Prefix | Auth | For |
| --- | --- | --- | --- |
| Client | `/api/client` | Client JWT | End-user clients (browser, app backend). |
| Machine | `/api/machine` | API key | Your backend and worker callbacks. |
| Admin | `/api/admin` | API key | Self-hosted session inspection. |
| Cloud | `/api/v1` | API key | The hosted control plane the `subs` cloud commands wrap. |

## Base URL

Prepend your engine's host to every path.

- **Self-hosted:** your `subs serve` address, such as `http://localhost:8080`.
  All four surfaces share it.
- **Cloud:** the control plane (`/api/v1`) is `https://api.substructure.ai`.
  The client and machine surfaces are served at your app's own engine URL, shown
  by `subs open` and handed to clients alongside their token.

## Headers

| Header | When |
| --- | --- |
| `Authorization: Bearer <token>` | Every request. Which token depends on the surface (see each section). |
| `Content-Type: application/json` | Any request with a body. |
| `Accept: text/event-stream` | The streaming and AG-UI endpoints. |

## Errors

Failures return a status and a JSON body. Most surfaces use the first shape;
the machine `submit` and `settle` endpoints use the second.

```json
{ "error": "session not found" }
```
```json
{ "ok": false, "error": "effect is not pending" }
```

| Status | Meaning |
| --- | --- |
| 202 | Input accepted. |
| 400 | Malformed request or a validation failure. |
| 401 | Missing or invalid credential. |
| 403 | Wrong tenant, not the session owner, or wrong caller for the effect. |
| 404 | Unknown session or effect. |
| 409 | Conflict: session interrupted, effect not pending, or a completed turn. |
| 422 | Unrecognized JSON body. |

## Streaming

The `.../events/stream` endpoints return `text/event-stream`. Each frame carries
`id` (the event's stream version), `event` (the event type), and `data` (an
[`Event`](#events) envelope). Resume after a drop with
`?after_stream_version=<n>`; the id of the last frame you saw is that number.

```
id: 7
event: message.new
data: {"global_position":42,"aggregate_id":"0193a1","stream_version":7,"occurred_at":"2026-07-14T10:00:03Z","payload":{"type":"message.new","message":{"id":"m2","role":"assistant","content":"Hi"}}}

event: llm.token.delta
data: {"type":"llm.token.delta","session_id":"0193a1","call_id":"llm-1","seq":3,"text":"Hi"}
```

`llm.token.delta` frames stream partial assistant output between events; see
[StreamDelta](#events). The `event` value is one of:

| Event | Payload highlights |
| --- | --- |
| `message.new` | A message was added to the tree: `{ message: Message, parent_id? }`. |
| `turn.started` / `turn.completed` | Turn boundaries, `{ turn_id, … }`; `turn.completed` closes a turn-scoped stream. |
| `tool.call.requested` / `.completed` / `.errored` | `{ tool_call_id, name, result? / error? }`. |
| `llm.call.requested` / `.completed` / `.errored` | An LLM call's lifecycle. |
| `sub_agent.requested` / `.started` / `.turn_completed` / `.errored` | A child session's lifecycle. |
| `session.created` / `.done` / `.cancelled` | Session lifecycle. |
| `session.interrupted` / `.interrupt_resumed` | `{ interrupt_id, origin, reason, payload, anchor? }`; `anchor` is the head the interrupt parks (absent = every branch). |
| `worker.state.updated` / `agent.updated` | State or config was rewritten. |
| `worker.decision.requested` / `.completed` / `.errored`, `decision_request.queued` / `.dropped`, `call.voided`, `session.message_requested` | Engine bookkeeping, useful for observability. |

The AG-UI endpoints emit AG-UI protocol events instead; see
[AG-UI events](#ag-ui-events).

## Client API

For end-user clients: a browser, or your app's frontend.

**Authentication.** `Authorization: Bearer <client-jwt>`. A client token is
short-lived and minted by your backend through the machine API's
`client-tokens` endpoint; never ship an API key to a browser. The token fixes
the tenant and owner, so a client acts only as itself. This surface answers CORS
preflight, so it is safe to call from a browser.

```sh
curl $BASE/api/client/sessions/input \
    -H "Authorization: Bearer $CLIENT_JWT" \
    -H "Content-Type: application/json" \
    -d '{"input":{"type":"client.message","agent_id":"my-agent","message":{"role":"user","content":"hi"}}}'
```

### `POST /api/client/sessions/input`

Submit a [`ClientInput`](#client-input): a message, action, interrupt resume, or
client-tool settle. `session_id` is minted when omitted; a completed `turn_id`
returns the existing turn instead of re-running it.

**Headers** `Authorization: Bearer <client-jwt>`, `Content-Type: application/json`

**Request body**

```json
{
  "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "input": {
    "type": "client.message",
    "agent_id": "my-agent",
    "message": { "role": "user", "content": "what time is it?" }
  }
}
```

**Response** `202`

```json
{
  "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "turn_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a60"
}
```

### `GET /api/client/sessions/{session_id}`

The session resource: head-resolved status, every open interrupt, `messages` —
the active conversation (root→head lineage), ready to render — and the full
[message tree](./70-conversations.md) for clients that show branches. Enough
for a client to rehydrate a conversation on page load.

**Headers** `Authorization: Bearer <client-jwt>`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `session_id` | The session to fetch. |

**Response** `200`

```json
{
  "session_id": "sess-1",
  "agent_id": "my-agent",
  "status": "idle",
  "interrupts": [],
  "messages": [
    { "id": "m1", "role": "user", "content": "hi" },
    { "id": "m2", "role": "assistant", "content": "Hello!" }
  ],
  "message_tree": {
    "head_id": "m2",
    "nodes": [
      { "message": { "id": "m1", "role": "user", "content": "hi" } },
      { "parent_id": "m1", "message": { "id": "m2", "role": "assistant", "content": "Hello!" } }
    ]
  }
}
```

`status` is `idle`, `interrupted`, or `done`, resolved at the active branch: it
reads `interrupted` only when an open interrupt parks the head path.
`interrupts` lists every open interrupt on any branch as
`{ interrupt_id, origin, reason, anchor? }` — `anchor` is the message the
interrupt is parked at (absent = parks every branch). A session that has never
received input is `404`.

### `GET /api/client/sessions/{session_id}/events/stream`

Stream a session's events.

**Headers** `Authorization: Bearer <client-jwt>`, `Accept: text/event-stream`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `session_id` | The session to stream. |

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `turn_id` | string | Optional. Scope to one turn; the stream closes when it completes. |
| `after_stream_version` | integer | Optional. Resume after this stream version. |

**Response** `text/event-stream`. See [Streaming](#streaming).

### `POST /api/client/sessions/{session_id}/interrupt`

Pause the session's active branch. The interrupt anchors at the current head;
other branches stay live (see [Interrupts](./140-interrupts.md)).
`interrupt_id` is minted when omitted.

**Headers** `Authorization: Bearer <client-jwt>`, `Content-Type: application/json`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `session_id` | The session to interrupt. |

**Request body**

```json
{
  "reason": "confirm",
  "payload": { "message": "Send the email?" }
}
```

**Response** `200`

```json
{ "ok": true, "interrupt_id": "int-1" }
```

### `POST /api/client/ag-ui/agents/{agent_id}/run`

Run a turn from an AG-UI client.

**Headers** `Authorization: Bearer <client-jwt>`, `Content-Type: application/json`, `Accept: text/event-stream`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `agent_id` | The agent to run. |

**Request body:** a [`RunAgentInput`](#runagentinput).

```json
{
  "threadId": "sess-1",
  "runId": "turn-1",
  "messages": [{ "id": "m1", "role": "user", "content": "hi" }],
  "tools": [],
  "context": [],
  "state": {},
  "resume": []
}
```

**Response** `text/event-stream` of [AG-UI events](#ag-ui-events).

```
event: RUN_STARTED
data: {"type":"RUN_STARTED","threadId":"sess-1","runId":"turn-1"}

event: TEXT_MESSAGE_CONTENT
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"m2","delta":"Hi"}

event: RUN_FINISHED
data: {"type":"RUN_FINISHED","threadId":"sess-1","runId":"turn-1"}
```

### `POST /api/client/ag-ui/agents/{agent_id}/connect`

Replay the session as a one-shot AG-UI snapshot, for reconnecting.

**Headers** `Authorization: Bearer <client-jwt>`, `Content-Type: application/json`, `Accept: text/event-stream`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `agent_id` | Present for symmetry; only `threadId` in the body is used. |

**Request body:** `threadId` addresses the session; `runId` is optional and
labels the snapshot frames.

```json
{ "threadId": "sess-1" }
```

**Response** `text/event-stream`: `RUN_STARTED`, `MESSAGES_SNAPSHOT`,
`RUN_FINISHED`, then close.

## Machine API

For your own backend and for worker callbacks. Server-side only.

**Authentication.** `Authorization: Bearer <api-key>`, the app's API key. It
carries full tenant privileges, so keep it on your server and never expose it to
a browser. This surface has no CORS. It is where you mint client tokens, submit
on a user's behalf, and settle deferred calls.

```sh
curl $BASE/api/machine/client-tokens \
    -H "Authorization: Bearer $API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"identity":{"id":"user_42"}}'
```

### `POST /api/machine/client-tokens`

Mint a client JWT for a browser session.

**Headers** `Authorization: Bearer <api-key>`, `Content-Type: application/json`

**Request body.** `identity.id` is required; `ttl_seconds` defaults to 600.

```json
{
  "identity": { "id": "user_42", "metadata": { "plan": "pro" } },
  "ttl_seconds": 600
}
```

**Response** `200`. `expires_at` is Unix seconds.

```json
{ "token": "eyJhbGciOiJIUzI1NiJ9...", "expires_at": 1784000000 }
```

### `POST /api/machine/sessions/submit`

Submit a [`ClientPayload`](#client-payload) from your backend; the owner is
attested from your key and the request `identity`.

**Headers** `Authorization: Bearer <api-key>`, `Content-Type: application/json`

**Request body.** `agent_id` and `identity.id` are required.

```json
{
  "agent_id": "my-agent",
  "identity": { "id": "user_42" },
  "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "payload": {
    "type": "client.message",
    "message": { "role": "user", "content": "hi" }
  }
}
```

**Response** `202`

```json
{
  "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "turn_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a60"
}
```

### `POST /api/machine/sessions/{session_id}/calls/settle`

Settle a worker-handled tool or LLM call out of band, by effect `id`. This is
how a [deferred](./130-deferred-tools.md) call reports its result later.

**Headers** `Authorization: Bearer <api-key>`, `Content-Type: application/json`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `session_id` | The session the call belongs to. |

**Request body.** One of four variants (`response` is an
[`LlmResponse`](#llm-response), `code` an [`ErrorCode`](#retries-and-errors)):

```jsonc
{ "type": "tool.result", "id": "call_abc", "result": "sunny" }
{ "type": "tool.error",  "id": "call_abc", "error": "upstream 503", "retryable": true, "code": "provider_error" }
{ "type": "llm.result",  "id": "llm_abc",  "response": { "model": "gpt-4", "content": "…" } }
{ "type": "llm.error",   "id": "llm_abc",  "error": "rate limited", "retryable": true }
```

**Response** `200`

```json
{ "ok": true }
```

### `POST /api/machine/workers/submit`

Push a worker-authored [`DecisionResponse`](#decision-response) into the engine
out of band, keyed by `session_id` and `decision_id`. Every settle action must
name its `id` and every `llm.call` its `model`, since no trigger is in scope to
fill them.

**Headers** `Authorization: Bearer <api-key>`, `Content-Type: application/json`

**Request body**

```json
{
  "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "decision_id": "dec_abc",
  "actions": [{ "type": "tool.result", "id": "call_abc", "result": "sunny" }]
}
```

**Response** `200`

```json
{ "ok": true }
```

### `GET /api/machine/sessions/{session_id}/events/stream`

The session event stream under API-key auth.

**Headers** `Authorization: Bearer <api-key>`, `Accept: text/event-stream`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `session_id` | The session to stream. |

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `turn_id` | string | Optional. Scope to one turn. |
| `after_stream_version` | integer | Optional. Resume after this stream version. |

**Response** `text/event-stream`. See [Streaming](#streaming).

## Admin API

Read-only session inspection on a self-hosted server.

**Authentication.** `Authorization: Bearer <api-key>`, the same key as the
machine surface. Results are scoped to the key's tenant. These routes exist only
on a `subs serve` host, not in the cloud.

```sh
curl $BASE/api/admin/sessions -H "Authorization: Bearer $API_KEY"
```

### `GET /api/admin/sessions`

List sessions, newest first.

**Headers** `Authorization: Bearer <api-key>`

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `top_level` | boolean | Default true. Exclude sub-agent sessions. |
| `sort` | string | `last_event_desc` (default), `first_event_asc`, `first_event_desc`, or `wake_at_asc`. |
| `limit` | integer | Page size. |
| `cursor` | string | Page cursor; echo `next_cursor` from the previous page. |
| `session_id` | string | Filter to one session. |
| `agent_id` | string | Filter by agent. |

**Response** `200`

```json
{
  "items": [
    {
      "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "tenant_id": "default",
      "stream_version": 12,
      "first_event_at": "2026-07-14T10:00:00Z",
      "last_event_at": "2026-07-14T10:03:00Z",
      "wake_at": null,
      "top_level": true,
      "agent_id": "my-agent",
      "cost": "0.0012",
      "sub_agent_cost": "0",
      "status": "idle",
      "turn_id": null
    }
  ],
  "next_cursor": null
}
```

### `GET /api/admin/sessions/{session_id}`

Fetch one session's current state. `state` is the internal session aggregate
(owner, status, running cost, message tree, open calls). Treat it as read-only
diagnostics; it may grow fields.

**Headers** `Authorization: Bearer <api-key>`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `session_id` | The session. |

**Response** `200`

```json
{
  "stream_version": 12,
  "first_event_at": "2026-07-14T10:00:00Z",
  "last_event_at": "2026-07-14T10:03:00Z",
  "state": {
    "status": "idle",
    "owner": { "tenant_id": "default", "id": "user_42" },
    "cost": "0.0012",
    "message_tree": { "nodes": [], "head_id": "m2" }
  }
}
```

### `GET /api/admin/sessions/{session_id}/events`

Fetch stored [`Event`](#events)s.

**Headers** `Authorization: Bearer <api-key>`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `session_id` | The session. |

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `after_stream_version` | integer | Return events after this version. |
| `limit` | integer | Page size. |

**Response** `200`

```json
[
  {
    "global_position": 128,
    "id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a70",
    "tenant_id": "default",
    "aggregate_type": "session",
    "aggregate_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
    "stream_version": 3,
    "occurred_at": "2026-07-14T10:00:03Z",
    "payload": {
      "type": "message.new",
      "message": { "id": "m2", "role": "assistant", "content": "Hi" }
    }
  }
]
```

### `GET /api/admin/sessions/{session_id}/events/stream`

The same events as SSE. `after_stream_version` defaults to 0, replaying full
history.

**Headers** `Authorization: Bearer <api-key>`, `Accept: text/event-stream`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `session_id` | The session to stream. |

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `after_stream_version` | integer | Default 0. Resume after this version. |
| `limit` | integer | Optional cap on replayed history. |

**Response** `text/event-stream`. See [Streaming](#streaming).

### `POST /api/admin/sessions/{session_id}/ag-ui/connect`

Replay the session as an AG-UI snapshot.

**Headers** `Authorization: Bearer <api-key>`, `Content-Type: application/json`, `Accept: text/event-stream`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `session_id` | The session to replay. |

**Request body:** a [`RunAgentInput`](#runagentinput).

**Response** `text/event-stream` of [AG-UI events](#ag-ui-events).

## Cloud API

The hosted control plane at `https://api.substructure.ai`, wrapped by the `subs`
cloud commands (see [CLI](./160-cli.md)).

**Authentication.** `Authorization: Bearer <token>`: your `subs login` token
against the cloud, or an app API key against a local server. A local `subs serve`
re-serves the same shape as a single tenant, so the `{org}` and `{app}` segments
are ignored, and the mutating endpoints return `400`:

```json
{ "error": { "code": "unsupported_on_local", "message": "this operation is not supported on a local server; it only exists in the hosted cloud" } }
```

```sh
curl https://api.substructure.ai/api/v1/orgs -H "Authorization: Bearer $TOKEN"
```

### `GET /api/v1/orgs`

List the orgs you belong to.

**Headers** `Authorization: Bearer <token>`

**Response** `200`

```json
[{ "id": "org_1", "name": "Acme", "role": "owner" }]
```

### `GET /api/v1/orgs/{org}/apps`

List an org's apps. Returns an array of [`App`](#app).

**Headers** `Authorization: Bearer <token>`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `org` | The org id. |

### `GET /api/v1/apps/{app}`

Fetch one app.

**Headers** `Authorization: Bearer <token>`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `app` | The app id. |

**Response** `200`. An [`App`](#app). `balanceUsd` and `sessionCount` are
omitted on a local server.

```json
{
  "id": "app_abc",
  "organizationId": "org_1",
  "name": "my-bot",
  "createdAt": "2026-07-01T00:00:00Z",
  "balanceUsd": "42.50",
  "sessionCount": 1280
}
```

### `POST /api/v1/orgs/{org}/apps`

Create an app. Hosted only.

**Headers** `Authorization: Bearer <token>`, `Content-Type: application/json`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `org` | The org to create the app in. |

**Request body**

```json
{ "name": "my-bot" }
```

**Response** `200`

```json
{
  "app": { "id": "app_abc", "organizationId": "org_1", "name": "my-bot" },
  "signing_secret": "9f3c1a…"
}
```

### `PATCH /api/v1/apps/{app}`

Rename an app. Hosted only.

**Headers** `Authorization: Bearer <token>`, `Content-Type: application/json`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `app` | The app id. |

**Request body:** `{ "name": "new-name" }`. **Response** `200`: the updated
[`App`](#app).

### `DELETE /api/v1/apps/{app}`

Delete an app. Hosted only.

**Headers** `Authorization: Bearer <token>`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `app` | The app id. |

### `GET /api/v1/apps/{app}/worker`

Read the webhook worker.

**Headers** `Authorization: Bearer <token>`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `app` | The app id. |

**Response** `200`

```json
{
  "endpoint_url": "https://my-worker.example.com/agent",
  "state": "enabled",
  "signing_secret": "9f3c1a…"
}
```

### `PUT /api/v1/apps/{app}/worker`

Set the webhook worker. Enabling reuses or mints the signing secret.

**Headers** `Authorization: Bearer <token>`, `Content-Type: application/json`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `app` | The app id. |

**Request body.** `state` is `"enabled"` or `"disabled"`.

```json
{ "endpointUrl": "https://my-worker.example.com/agent", "state": "enabled" }
```

**Response** `200`. The worker config, as `GET` returns.

### `POST /api/v1/apps/{app}/worker/rotate-secret`

Rotate the signing secret. No body; returns the worker config with the new
secret.

**Headers** `Authorization: Bearer <token>`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `app` | The app id. |

### `GET /api/v1/apps/{app}/api-keys`

List client API keys. Hosted only.

**Headers** `Authorization: Bearer <token>`

**Response** `200`

```json
[{ "key_id": "key_1", "label": "web-frontend", "created_at": "…", "last_used_at": null }]
```

### `POST /api/v1/apps/{app}/api-keys`

Create a client API key. Hosted only.

**Headers** `Authorization: Bearer <token>`, `Content-Type: application/json`

**Request body:** `{ "label": "web-frontend" }`.

**Response** `200`. The key is shown once.

```json
{ "api_key": "sk_live_…" }
```

### `DELETE /api/v1/apps/{app}/api-keys/{key_id}`

Revoke a key. Hosted only.

**Headers** `Authorization: Bearer <token>`

**Path parameters**

| Parameter | Description |
| --- | --- |
| `app` | The app id. |
| `key_id` | The key to revoke. |

### `GET /api/v1/apps/{app}/sessions…`

Session reads, identical to the [Admin API](#admin-api) with the `{app}` segment
ignored on a local server: `/sessions`, `/sessions/{id}`, `/sessions/{id}/events`,
and `/sessions/{id}/events/stream`.

## Objects

The types used in the bodies above. `?` marks an optional field; `unknown` is
any JSON value. These mirror the machine-readable
[`schemas/protocol.schema.json`](../schemas/protocol.schema.json); see
[Typed bindings](./40-typed-bindings.md) to generate them.

### Messages

```typescript
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 message; the id is optional when you submit one (the engine assigns it).
type Message = {
    id?: string
    role: Role
    content?: Content
    tool_calls?: ToolCall[]
    tool_call_id?: string
    name?: string
}
```

### Client input

The body of `POST /api/client/sessions/input` is `{ session_id?, input: ClientInput }`.

```typescript
type ClientInput =
    | { type: "client.message"; agent_id: string; turn_id?: string; message: Message; stream?: boolean }
    | { type: "client.messages"; agent_id: string; turn_id?: string; messages: Message[]; 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 }

// Browser-contributed tools and context, layered onto the agent for this turn.
type ClientContext = {
    tools?: AgentTool[]
    context?: unknown[]
    state?: unknown
    forwarded_props?: unknown
}
type AgentTool = {
    name: string
    description?: string
    input?: unknown             // JSON Schema for arguments
    output?: unknown            // JSON Schema the result must satisfy
    handler?: "worker" | "client"
}
```

`turn_id` is an idempotency key; resubmitting a completed one returns the
existing turn.

### Client payload

The `payload` of `POST /api/machine/sessions/submit`: a client input without the
addressing fields, which the machine supplies separately.

```typescript
type ClientPayload =
    | { type: "client.message"; message: Message; stream?: boolean }
    | { type: "client.messages"; messages: Message[]; stream?: boolean; client?: ClientContext }
    | { type: "client.action"; name: string; args?: unknown }
```

### Decision response

The body of `POST /api/machine/workers/submit`.

```typescript
type DecisionResponse = {
    messages?: Message[]        // 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
}

type Action =
    | { type: "llm.call"; id?: string; model?: string; messages?: Message[]; tools?: LlmTool[]; temperature?: number; max_completion_tokens?: number; reasoning?: ReasoningConfig; stream?: boolean; retry?: RetryPolicy; handler?: "server" | "worker" }
    | { type: "tool.call"; id?: string; name: string; arguments: unknown; handler?: "worker" | "client"; retry?: RetryPolicy }
    | { type: "tool.result"; id?: string; attempt?: number; result: unknown }
    | { type: "tool.error"; id?: string; attempt?: number; error: string; retryable?: boolean; code?: ErrorCode; detail?: unknown }
    | { type: "llm.result"; id?: string; attempt?: number; response: LlmResponse }
    | { type: "llm.error"; id?: string; attempt?: number; error: string; retryable?: boolean; code?: ErrorCode; detail?: unknown }
    | { type: "sub_agent.spawn"; session_id: string; agent_id: string; tool_call_id: string; retry?: RetryPolicy }
    | { type: "message.send"; session_id: string; message: Message }
    | { type: "interrupt"; interrupt_id?: string; reason: string; payload?: unknown }
    | { type: "done"; data?: unknown }

type AgentConfig = {
    model: string
    system?: string
    stream?: boolean
    handler?: "server" | "worker"
    format?: "openai" | "anthropic"
    retry?: RetryPolicy
    tools?: AgentTool[]
    sub_agents?: { id: string; description?: string }[]
}
type LlmTool = { name: string; description: string; input?: unknown; output?: unknown }
type ReasoningConfig = { effort?: "xhigh" | "high" | "medium" | "low" | "minimal" | "none"; max_tokens?: number; exclude?: boolean; enabled?: boolean }
```

### LLM response

Used in the `llm.result` settle and action.

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

### Retries and errors

```typescript
type RetryPolicy = {
    timeout_secs: number | null   // deadline per attempt; null waits forever
    max_retries: number           // cap on total attempts
    backoff_base_secs: number
    backoff_max_secs: number
}
type ErrorCode = "provider_error" | "rate_limited" | "refused" | "budget_exceeded" | "deadline_exceeded"
```

### Events

A stored event, from the event-stream and `.../events` endpoints. `payload` is
tagged by `type` (see the [Streaming](#streaming) table).

```typescript
type Event = {
    global_position: number
    id: string
    tenant_id: string
    aggregate_type: string       // "session"
    aggregate_id: string         // the session id
    stream_version: number       // the SSE frame id
    occurred_at: string          // RFC 3339
    payload: { type: string; [field: string]: unknown }
}

type StreamDelta = {             // the llm.token.delta data
    type: "llm.token.delta"
    session_id: string
    call_id: string
    seq: number
    text?: string
    reasoning?: string
    tool_calls?: { id: string; name?: string; arguments?: string }[]
    finish_reason?: string
}
```

### App

The Cloud `App` resource. `balanceUsd` and `sessionCount` are omitted on a local
server.

```typescript
type App = {
    id: string
    organizationId: string
    name: string
    createdAt?: string
    balanceUsd?: string          // decimal string
    sessionCount?: number
}
```

### RunAgentInput

The AG-UI request body (camelCase).

```typescript
type RunAgentInput = {
    threadId: string             // the session id
    runId: string                // the turn id
    messages: { id?: string; role: string; content?: string; toolCallId?: string; toolCalls?: unknown[] }[]
    state?: unknown
    tools?: { name: string; description?: string; parameters?: unknown }[]
    context?: unknown[]
    forwardedProps?: unknown
    resume?: { interruptId: string; status: "resolved" | "cancelled"; payload?: unknown }[]
}
```

### AG-UI events

The AG-UI `run` and `connect` streams emit these, each a frame whose `event` is
the type and `data` is a camelCase JSON object.

`RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR`, `MESSAGES_SNAPSHOT`,
`TEXT_MESSAGE_START`, `TEXT_MESSAGE_CONTENT`, `TEXT_MESSAGE_END`,
`TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_END`, `TOOL_CALL_RESULT`,
`REASONING_START`, `REASONING_MESSAGE_START`, `REASONING_MESSAGE_CONTENT`,
`REASONING_MESSAGE_END`, `REASONING_END`. `RUN_FINISHED` carries an interrupt
outcome when the agent paused. See [AG-UI](./100-ag-ui.md).

## Next

- [Authentication](./180-auth.md): tokens, keys, and identity.
- [CLI](./160-cli.md): the commands that drive the cloud API.
- [Typed bindings](./40-typed-bindings.md): generate these objects in your language.
