Runs

A Run is AI Transport's unit of work for one prompt-response cycle, with explicit identity, lifecycle, and end reason. Each conversation turn the user sees is implemented as a Run.

A Run is one unit of agent work, initiated in response to user intent. It encompasses everything that happens in service of that intent: the user's input, the agent's response, any tool calls the agent makes, any intermediate messages required to resolve those tool calls (approvals, tool outputs), the agent's continued work after resolution, and the final completion.

AI Transport implements each user prompt and LLM turn as a Run.

Diagram showing a Run as a bracketed group of channel messages with lifecycle events and end reason

Why Runs exist

An agent's work in response to a single user prompt isn't atomic. It can involve reasoning over time, gathering external information, executing tools, and waiting for humans or other systems to respond. Across that span, multiple participants (multiple devices, multiple browser tabs, a serverless function that restarts mid-execution) need to agree on which work is which, when it started, when it ended, and whether it was cancelled. The Run is the SDK primitive that carries this identity end-to-end.

The Run model

A Run has four invariants that the SDK enforces on every channel:

  • A unique runId.
  • A bracketing pair of lifecycle events on the channel: ai-run-start and ai-run-end.
  • An owner: the clientId of the Ably client that published ai-run-start.
  • A terminal RunEndReason once it ends: 'complete', 'cancelled', or 'error'. (A Run that pauses awaiting input takes RunInfo.status === 'suspended' via AgentRun.suspend(), which is non-terminal.)

Within those brackets, the Run owns a window of channel messages: the user input that triggered it, the agent's streamed outputs, any tool-call or tool-result messages. The conversation Tree groups those messages into a RunNode; the View surfaces projection-free RunInfo snapshots for the UI.

A Run is triggered by an Invocation: the trigger the client posts to the agent endpoint that says "create or continue a Run with these identifiers." The Invocation is a separate concept because the same Run can be re-triggered (a tool result, a regenerate request); each trigger is one Invocation.

What the Run layer requires

PropertyWhy it matters
Stable identityThe same runId must be readable to every participant: the client that started the Run, every other connected client, the agent process. Without it, cancellation has no target and observation has no scope.
Lifecycle bracketsai-run-start, ai-run-suspend, ai-run-resume, and ai-run-end mark the Run on the wire. A view filtering "active Runs" reads RunInfo.status === 'active'; 'suspended' and the terminal RunEndReason values cover the other states.
Cancel routingA cancel message names a runId. The agent's Run.abortSignal fires only for matching cancels, so unrelated Runs on the same session continue.
Durable executionAn agent can restart its process mid-Run (serverless cold start, container redeploy). The Run's identity plus the channel's persistence let the new agent rehydrate by draining its leaf-pinned run.view from channel history and continue without the client noticing.
Multiple participantsSeveral clients may observe the same Run (multi-device, support handover). They all see the lifecycle events; they all hold the same RunInfo.

Understand the Run lifecycle

A Run progresses from start to terminal end. Along the way it can pause to wait for external input and be resumed when that input arrives. Each phase is reflected on RunInfo.status:

  • 'active' while the agent is working. Set when the Run first starts and again whenever a paused Run is resumed.
  • 'suspended' while the Run is paused awaiting input (a tool approval, a human-in-the-loop response). The Run is not over; a later trigger re-activates it.
  • A terminal RunEndReason once the Run finishes:
    • 'complete' is the success path.
    • 'cancelled' is set when the Run is cancelled.
    • 'error' is set when reasoning, output streaming, or a tool execution fails unrecoverably.

A Run is the unit of cancellation. There is no user-facing cancel below the Run level. Internal failures (a stream dying, a model call retrying, a serverless cold start) fail the execution attempt, not the Run; the Run stays active and the SDK retries underneath.

Read a Run from either side

Both sides of a Run expose the same read-model, so the same accessor means the same thing on the client and the agent. A client's view.send() returns a ClientRun; an agent's createRun() returns an AgentRun. Each carries:

  • runId: the Run's identifier. Known synchronously on the agent; on the client it is empty until the agent's run-start is observed (await clientRun.started first).
  • status: the lifecycle status, one of 'active', 'suspended', 'complete', 'cancelled', or 'error', read live off the conversation tree.
  • error: the terminal error, present exactly when status is 'error'.
  • messages: all of the Run's messages, its triggering input followed by its streamed output (across any suspend and resume). This is the unit to persist; see database hydration.

Each side then adds its own verbs. The client's ClientRun adds started and cancel(); the agent's AgentRun adds located (resolves once the triggering input has been observed on the channel) and the lifecycle methods start(), pipe(), suspend(), and end().

Trigger a Run

A minimal client-side send returns a ClientRun that exposes the Run's identity and a per-Run cancel. Each send introduces at most one new message and triggers exactly one Run. The application then POSTs clientRun.toInvocation().toJSON() to its agent endpoint to wake the agent; see Invocations.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

const clientRun = await session.view.send(UIMessageCodec.createUserMessage({
  id: crypto.randomUUID(),
  role: 'user',
  parts: [{ type: 'text', text: 'Plan a 3-day trip to Lisbon.' }],
}));

// The agent mints the runId on the server, so clientRun.runId is empty until
// the agent's run-start is observed. Await `started`, then read the id.
await clientRun.started;
console.log('Run started:', clientRun.runId);

// Stop button: clientRun.cancel() works immediately, even before
// the run-start has been observed.
await clientRun.cancel();

On the agent side, the symmetric primitive is AgentSession.createRun(invocation, runtime?). The agent mints runId (for a fresh run) or reads the existing runId off the triggering input event (for a continuation), and stamps it on every event it publishes; the client reads it from clientRun.runId once clientRun.started resolves.

Run concurrent Runs

A session can hold multiple Runs in flight at the same time. They share the channel; they don't share state. See concurrent turns for the patterns that arise when more than one Run is 'active'.

  • Sessions: the shared conversation state Runs live within.
  • Invocations: the trigger that creates or continues a Run.
  • Connections: how ClientSession and AgentSession connect to a session and publish Runs.
  • Cancellation: control who cancels Runs and how cancel signals are routed.
  • Concurrent turns: multiple Runs in flight on the same session.