Running and streaming

A defined agent does nothing until a conversation drives it. You drive it over HTTP — create a thread, post a message, follow the run — and stream the run live over a websocket.

Threads, runs, and messages

  • A thread is one conversation with one agent, owned by a principal. It has a status (active or archived) and an ordered list of messages.
  • A run is one turn. Posting a user message to a thread triggers a run.
  • A message has a role (system, user, assistant) and structured parts: text, reasoning, step-start, tool-call, and file. The assistant message is persisted once the run finishes.

HTTP API

MethodPathPurpose
GET/api/agentsList agents you can run.
GET/api/agents/:agentIdGet one agent.
GET/api/agent-threadsList threads (filter by agentId, status).
POST/api/agent-threadsCreate a thread ({ agentId, title?, threadId? }).
GET/api/agent-threads/:threadIdGet a thread.
GET/api/agent-threads/:threadId/messagesList a thread's messages.
POST/api/agent-threads/:threadId/messagesPost a user message — triggers a run.
GET/api/agent-threads/:threadId/runsList a thread's runs.
POST/api/agent-threads/:threadId/runs/:runId/retryRetry a failed run without adding another user message.
POST/api/agent-threads/:threadId/cancelCancel the thread's queued or running run ({ runId }).
GET/api/agent-runs/:runIdGet a run's status.
GET/api/agent-threads/:threadId/messages/:messageId/files/contentDownload a file attached to a message.

Trigger a run

There is no separate run-trigger endpoint — posting a message is the trigger. The 202 response returns the canonical durable run:

JSON
// POST /api/agent-threads/:threadId/messages   { "text": "Which invoices are overdue?" }
{
  "run": {
    "id": "run_...",
    "threadId": "thr_...",
    "agentId": "accounts",
    "triggerMessageId": "msg_...",
    "requestedBy": { "type": "user", "id": "usr_..." },
    "status": "queued",
    "attempt": 0,
    "streamId": "agents.runs.run_...",
    "createdAt": "2026-07-11T20:00:00.000Z"
  }
}

The run and user message are durable before this response returns, so you can immediately read the run or subscribe to its stream even when queue publication is temporarily unavailable. requestedBy is resolved from the run's immutable execution record; it is omitted for automatic executions that have no requesting principal. A thread runs one turn at a time — posting while a run is active returns 409; wait for it to finish first.

Attachments

A user message can carry files: pass attachments — an array of FileRefs, the same blob references objects use — alongside text.

JSON
// POST /api/agent-threads/:threadId/messages
{ "text": "Summarize this contract", "attachments": [ /* FileRef */ ] }

Attachments — and any files the agent produces — appear as file parts on the stored message ({ "type": "file", "fileRef": … }). Download the bytes from GET /api/agent-threads/:threadId/messages/:messageId/files/content.

Run status

GET /api/agent-runs/:runId returns the run record.

StatusMeaning
queuedThe request is durable and waiting to start.
runningThe turn is in progress.
succeededThe turn completed and the reply was persisted.
failedA model/tool error or the turn timeout ended it (error has details).
cancelledThe run was aborted.

A finished run also carries finishReason (stop, length, tool-calls, content-filter, timeout, error, other, unknown), provider-neutral usage, and modelId. Usage is read from the durable model-call ledger and includes input/output totals plus any reported cache, text, and reasoning breakdowns. It summarizes every completed provider call, including calls completed before a later failure or cancellation.

When the wall-clock budget is reached, the run ends as failed with finishReason: "timeout" and the configured duration in error.details.timeoutMs. Any coherent text and completed tool work that already streamed is finalized as an assistant message in the same transaction as the run. A client can therefore offer Continue when that message exists; when no coherent progress exists, it can offer the failed-run retry instead. Retrying a timeout that has saved progress is discouraged because completed tools may already have had side effects.

Cancel a queued or running run with POST /api/agent-threads/:threadId/cancel (body { runId }); it ends as cancelled.

Retrying a failed run creates a new queued run that points to the failed run's existing triggerMessageId. The original user message is not appended again.

Stream a run

Connect to /ws/agents and send JSON commands; the server replies with JSON events.

CommandFieldsPurpose
subscriberunId, afterCursor?Follow a run live.
replayrunId, afterCursor?, limit?Read past records once.
unsubscriberunId?Stop the subscription.
JSON
// follow from the start
{ "type": "subscribe", "runId": "run_..." }

// resume after a disconnect, from the last cursor you saw
{ "type": "subscribe", "runId": "run_...", "afterCursor": "..." }

After replaying retained records, the server sends a run.snapshot frame containing the current durable run. A new run therefore streams status: "queued" before any worker lifecycle record, and a reconnect can recover terminal state even if no live record was observed.

Each record frame carries an AgentRunStreamEvent:

EventWhenFields
agent.run.startedThe turn began.modelId
agent.ui.chunkLive output as the model streams.chunkIndex, chunk
agent.message.finalizedThe assistant message was persisted.messageId
agent.run.finishedThe run ended.status, finishReason, error

Records carry a cursor. Persist the last one you saw and pass it as afterCursor to resume where you left off — a client that reconnects mid-run replays the gap. A cursor older than the stream's retention is rejected rather than quietly skipped, so a client that was away long enough for its resume point to be trimmed is resubscribed from the oldest retained record and told so by a subscribed frame carrying afterCursor: null. agent.ui.chunk events are live; the durable copy of the turn is the persisted assistant message, read back with GET /api/agent-threads/:threadId/messages.

A ready-made chat UI

You rarely need to wire this HTTP + WebSocket flow by hand. @sixb/agent-ui ships a turnkey React chat: AgentChat (a full thread with composer and live transcript), AgentsHome (an agent picker), and the Composer, Transcript, and streaming hooks as building blocks. For a React-Router app, @sixb/agent-ui/react-router exposes a drop-in AgentChatPage:

TSX
import { AgentChatPage } from "@sixb/agent-ui/react-router"

export default function Agents() {
  return <AgentChatPage routeBase="/agents" />
}

Import @sixb/agent-ui/globals.css once for styling. These components call the same /api/agent-* routes and /ws/agents stream documented above, so anything they do is reachable from your own client too.

Search docs

Search the documentation