Cancellation

Your users can stop an agent mid-response without breaking the session. AI Transport sends cancel as a signal on the channel, so other turns continue and the session stays open.

Cancellation is a turn-level operation. The client publishes a cancel signal on the Ably channel; the agent receives it on the run with the matching runId and fires its abort signal. Unlike closing an HTTP connection, cancellation is an explicit signal: the session remains intact, other runs continue, and both sides handle cleanup gracefully.

Diagram showing a cancel signal stopping the in-progress Run

A minimal cancel:

JavaScript

1

await activeRun.cancel();

How it works

Sessions are bidirectional, so a cancel is just a signal on the channel. The client publishes a cancel message keyed on the triggering input's codec-message-id (the synchronous handle the client owns from send time). Once the agent has resolved the cancel to a registered Run, that Run's abortSignal fires. The LLM stream stops, the run ends with reason 'cancelled', and every subscriber receives the lifecycle update. A cancel published before the agent has minted the run-id is still honoured: the agent buffers it and fires once the input-event lookup resolves.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

// Client: cancel the current run.
// activeRun.cancel() works immediately, even before the runId
// promise on activeRun has resolved.
await activeRun.cancel();

// Or, when you already have a resolved runId (for example from a RunInfo
// in view.runs()):
await session.cancel(someRunInfo.runId);

// Server: abort signal fires automatically
const result = streamText({
  abortSignal: run.abortSignal,
});

Cancel one run, several, or all

activeRun.cancel() targets the run the client just kicked off. To cancel several runs, iterate the visible Runs and cancel each by id:

JavaScript

1

2

3

// Cancel all active runs in the visible view (Stop button)
const active = session.view.runs().filter((r) => r.status === 'active');
await Promise.all(active.map((r) => session.cancel(r.runId)));

Selecting which runs to cancel is application logic. RunInfo.clientId tells you the run owner, so you can scope a cancel to runs started by the current client, a specific user, or all visible runs.

Server-side handling

Abort signal

Every run exposes an abortSignal that fires when the run is cancelled. Pass it to your LLM call:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

const run = session.createRun(invocation, { signal: req.signal });
await run.start();

const result = streamText({
  model: anthropic('claude-sonnet-4-20250514'),
  messages: run.messages,
  abortSignal: run.abortSignal,
});

const { reason } = await run.pipe(result.toUIMessageStream());
await run.end({ reason });

reason is 'cancelled' when the abort fires.

Authorise the cancel

The onCancel hook on RunRuntime authorises or rejects cancel requests:

JavaScript

1

2

3

4

5

6

const userId = await authenticateUser(req);

const run = session.createRun(invocation, {
  signal: req.signal,
  onCancel: async (request) => request.message.clientId === userId,
});

CancelRequest carries the raw cancel message (with the requester's clientId) and the runId it targets. Resolve the authorised identity from the inbound HTTP request and compare against request.message.clientId; the Ably service verifies the publisher's clientId before the cancel reaches the agent, so the value is trustworthy. Return false to reject; the run continues. If onCancel is not provided, all cancel requests are accepted.

Publish a final note before cancelling

The onCancelled hook runs when the abort signal fires, giving you a chance to publish final events before the stream closes:

JavaScript

1

2

3

4

5

6

const run = session.createRun(invocation, {
  signal: req.signal,
  onCancelled: async (write) => {
    await write({ type: 'text-delta', id: 'cancel-note', delta: '\n[Response cancelled]' });
  },
});

Cancel on close

ClientSession.close() is local-state-only: it does not cancel runs on the wire. To stop in-progress runs before closing, cancel them explicitly:

JavaScript

1

2

3

const active = session.view.runs().filter((r) => r.status === 'active');
await Promise.all(active.map((r) => session.cancel(r.runId)));
await session.close();

Edge cases and unhappy paths

  • Cancellation is asynchronous. A few more tokens arrive after cancel() returns and before the server's abortSignal fires. Render them on the cancelled turn.
  • The server is responsible for honouring the abort signal. A tool invocation that does not check the signal continues to run until it completes.
  • Cancel signals from a client without the channel publish capability will silently fail. Verify capabilities on the authentication endpoint.
  • An onCancel that returns false does not notify the requesting client. Surface the rejection through your own application protocol if the user needs to know.
  • A cancel sent before the turn starts is delivered to the channel and accumulated; the server applies it as soon as the turn is created.

FAQ

Why use cancel signals instead of closing the connection?

Closing the connection disconnects a client from the session. The session and connection are distinct and not coupled. A cancel signal notifies the agent to stop the stream but leaves the session intact, so the next message starts a new turn immediately, on every connected device. See reconnection and recovery for how clients that disconnect mid-stream can reconnect and resume.

Can a user on another device cancel my turn?

Yes, if your onCancel hook authorises it. The default accepts all cancel requests. See the authorisation pattern above to scope it to the turn owner.

What happens if multiple cancel signals match the same turn?

The turn cancels once. Subsequent matching signals are no-ops; the abort signal does not refire.

How do I tell a cancelled run apart from one that finished normally?

run.end(reason) reports the reason on the channel. Clients receive it through the view's run lifecycle event. The reason is 'cancelled' for a cancel and 'complete' for a normal finish.

Does cancel cost a message?

The cancel signal is a published message on the channel. See pricing for current rates.