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.

Inside those brackets, the Run is composed of one or more Steps. Each Step is a re-attemptable unit of work with its own ai-step-start / ai-step-end bracket. A retry of a Step under the same stepId supersedes the failed attempt on the channel rather than appending, which is what makes a Run safe to run inside a durable workflow engine.

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 new process rehydrates by calling session.adoptRun, publishing further output as a Step with a stable stepId so a retry supersedes the failed attempt. See Durable execution.
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(createUIMessageCodec().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.

Steer a Run

While a Run is active, a client can send a follow-up message into it. This is called steering the Run. The agent picks up the new message on the next loop iteration; no new Run starts.

The follow-up has the same shape as the input that started the Run. It joins the conversation tree under the same RunNode and every connected client sees it. Unlike a fresh view.send, it carries the active Run's runId, which tells the agent to add it to the existing Run.

Steering messages don't change how a Run ends. The terminal RunEndReason reports how the Run finished. Each steering message also gets its own outcome, so the client can tell which ones the agent saw before the Run ended and which it didn't.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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

const { published, outcome } = activeRun.steer(createUIMessageCodec().createUserMessage({
  id: crypto.randomUUID(),
  role: 'user',
  parts: [{ type: 'text', text: 'Make it 5 days, and keep it under £800.' }],
}));

await published; // The steering message has been published to the channel.

const { consumed } = await outcome; // Resolves when the Run ends.
console.log('Reached the agent:', consumed);

See interruption and steering for how to call steer(), what outcome contains, and how to write the agent's loop.

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.
  • Steps: the re-attemptable unit of work within a Run.
  • 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.
  • Durable execution: run a Run across process boundaries and workflow-engine retries.
  • Concurrent turns: multiple Runs in flight on the same session.