# Cancellation Your users can stop an agent mid-response without breaking the session. AI Transport sends cancel as a signal on the session, so other turns continue and the session stays open. Cancellation is a turn-level operation. The client publishes a cancel signal on the session; the agent receives it on the run with the matching `runId` and fires its abort signal. Cancellation is an explicit signal, so the session remains intact and other runs continue while both sides clean up gracefully. ![Diagram showing a cancel signal stopping the in-progress run](https://raw.githubusercontent.com/ably/docs/main/src/images/content/diagrams/ait-cancellation.png) On the client, a minimal cancel: #### Javascript ``` await activeRun.cancel(); ``` ## How it works Sessions are bidirectional, so a cancel is just a signal on the session. 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 assigned the run-id is still honoured: the agent buffers it and fires once the input-event lookup resolves. ### Javascript ``` // 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 started. To cancel several runs, iterate the visible runs on the client and cancel each by id: ### Javascript ``` // 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 ``` const run = session.createRun(invocation, {}, { signal: req.signal }); // Drain run.view for the full conversation to feed the model. run.messages // is only this run's own turn. while (run.view.hasOlder()) await run.view.loadOlder(); await run.start(); const conversation = run.view.getMessages().map(({ message }) => message); const result = streamText({ model: anthropic('claude-sonnet-4-20250514'), messages: await convertToModelMessages(conversation), 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 `RunHooks` authorises or rejects cancel requests: #### Javascript ``` 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 ``` 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. On the client, cancel in-progress runs explicitly before closing: ### Javascript ``` 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](https://ably.com/docs/auth/capabilities.md#capability-operations) will silently fail. Verify capabilities on the [authentication](https://ably.com/docs/ai-transport/getting-started/authentication.md) 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 session and accumulated; the server applies it as soon as the turn is created. - `onCancel` authorises cancel messages only. An abort arriving through the run's `signal`, such as a request abort or a serverless function timeout, cancels the run without consulting it, so cleanup that must happen either way belongs in `onCancelled`. - A run suspended awaiting a client tool result is not terminal, so it still appears in `runs()`. Code that filters on `status === 'active'` alone skips it, and the run stays open until something resolves or cancels it. ## 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. Clients that disconnect mid-stream [reconnect and resume](https://ably.com/docs/ai-transport/features/reconnection-and-recovery.md). ### 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 distinguish a cancelled run from one that finished normally? `run.end(reason)` reports the reason on the session. 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, billed at the current [message rates](https://ably.com/docs/platform/pricing.md). ## Related features - [Interruption and steering](https://ably.com/docs/ai-transport/features/interruption-and-steering.md): steer the active run, cancel and re-prompt, or send alongside. - [Concurrent turns](https://ably.com/docs/ai-transport/features/concurrent-turns.md): multiple turns with independent cancel handles. - [Token streaming](https://ably.com/docs/ai-transport/features/token-streaming.md): what gets cancelled when the abort fires. ## Related Topics - [Agent presence](https://ably.com/docs/ai-transport/features/agent-presence.md): Show agent status in your AI application with Ably Presence. Display streaming, thinking, idle, and offline states in realtime. - [Branching, edit, and regenerate](https://ably.com/docs/ai-transport/features/branching.md): Edit user messages, regenerate AI responses, and navigate branches with Ably AI Transport. The full history is preserved in the conversation tree. - [Chain of thought](https://ably.com/docs/ai-transport/features/chain-of-thought.md): Stream reasoning and thinking content alongside responses with Ably AI Transport. Display chain-of-thought in realtime. - [Concurrent turns](https://ably.com/docs/ai-transport/features/concurrent-turns.md): Run multiple AI turns simultaneously with Ably AI Transport. Independent streams, scoped cancellation, and multi-agent support. - [Database hydration](https://ably.com/docs/ai-transport/features/database-hydration.md): Hydrate an AI conversation from your own database with AI Transport and reconcile it with the live Ably channel, with no gap and no duplicate. - [Double texting](https://ably.com/docs/ai-transport/features/double-texting.md): Handle users sending multiple messages while the AI is streaming with Ably AI Transport. Queue or run messages concurrently. - [Durable execution](https://ably.com/docs/ai-transport/features/durable-execution.md): Run AI Transport agents inside a durable workflow engine. Adopt an in-flight run from a fresh process, retry a failed step under a stable stepId, and let the retry supersede the failed attempt on the channel. - [History and replay](https://ably.com/docs/ai-transport/features/history.md): Load conversation history from Ably channels with AI Transport. Paginated history, gapless continuity, and scroll-back patterns. - [Human-in-the-loop](https://ably.com/docs/ai-transport/features/human-in-the-loop.md): Add human approval gates to AI agent workflows with Ably AI Transport. Approve tool executions and provide input across devices. - [Interruption and steering](https://ably.com/docs/ai-transport/features/interruption-and-steering.md): Let users change direction mid-response in Ably AI Transport. Three patterns: steer the active run with a follow-up prompt, cancel and re-prompt, or send alongside as a concurrent run. - [LiveObjects state](https://ably.com/docs/ai-transport/features/liveobjects.md): Give an AI agent live awareness of what the user is doing, and the user live awareness of what the agent is doing, with shared state on the AI Transport session channel via Ably LiveObjects. - [Multi-device sessions](https://ably.com/docs/ai-transport/features/multi-device.md): Share AI conversations across tabs, phones, and laptops with Ably AI Transport. All devices see the same session in realtime. - [Optimistic updates](https://ably.com/docs/ai-transport/features/optimistic-updates.md): User messages appear instantly in Ably AI Transport. Optimistic insertion with automatic reconciliation when the server confirms. - [Push notifications](https://ably.com/docs/ai-transport/features/push-notifications.md): Notify users when AI agents complete background tasks with Ably Push Notifications. Reach users even when they're offline. - [Reconnection and recovery](https://ably.com/docs/ai-transport/features/reconnection-and-recovery.md): AI Transport streams survive connection drops automatically. Clients reconnect and resume from where they left off with no lost tokens. - [Token streaming](https://ably.com/docs/ai-transport/features/token-streaming.md): Stream AI-generated tokens to clients in realtime using AI Transport. Tokens are appended to a single durable message, and the full response is served to clients that join later. - [Tool calling](https://ably.com/docs/ai-transport/features/tool-calling.md): Stream tool invocations and results through Ably AI Transport. Server-executed and client-executed tools with persistent state. ## Documentation Index To discover additional Ably documentation: 1. Fetch [llms.txt](https://ably.com/llms.txt) for the canonical list of available pages. 2. Identify relevant URLs from that index. 3. Fetch target pages as needed. Avoid using assumed or outdated documentation paths.