Interruption
Your users can change direction mid-response. AI Transport's session layer lets a client cancel the in-progress Run and start a new one without breaking the stream.
Interruption is the pattern where a user sends a new message while the agent is still streaming a response. The new message creates a new turn on the session; whether the existing turn is cancelled first or runs in parallel is up to you.
A minimal interruption cancels the active Run and sends a new message:
1
2
3
4
5
6
7
8
const active = session.view.runs().filter((run) => run.status === 'active');
await Promise.all(active.map((run) => session.cancel(run.runId)));
await session.view.send(UIMessageCodec.createUserMessage({
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text: 'Wait, make it 5 days.' }],
}));How it works
Each turn becomes a Run on the session. The session is bidirectional, so a client can publish a cancel signal at any time, even while the response is mid-flight. A new send doesn't have to wait for the previous Run to finish.
Two patterns:
| Pattern | Behaviour | When to use it |
|---|---|---|
| Cancel-then-send | Cancel the active Run, then send the new message. | A Stop button followed by a new prompt. |
| Send-alongside | Send the new message while the active Run continues. | Quick follow-up that doesn't invalidate the in-flight response. |
With cancel-then-send, the active Run aborts before the new message dispatches. The agent stops generating, ends the Run with reason 'cancelled', and the new Run starts clean. With send-alongside, both Runs run concurrently (each with its own stream and its own cancel handle). See concurrent turns for the multiplexing model.
Implement cancel-then-send
Detect whether a Run is active, cancel it, then send the new message:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useClientSession, useView } from '@ably/ai-transport/react';
function Chat() {
const { session } = useClientSession();
const view = useView();
const handleSend = async (text) => {
const active = view.runs().filter((run) => run.status === 'active');
await Promise.all(active.map((run) => session.cancel(run.runId)));
await view.send(UIMessageCodec.createUserMessage({
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text }],
}));
};
}session.cancel(runId) publishes a cancel signal. The agent's Run.abortSignal fires, the LLM stream stops, and the Run ends with reason 'cancelled'. The new message starts a clean Run.
Implement send-alongside
Send a new message without cancelling. Both Runs stream concurrently; each ActiveRun carries its own runId and cancel(), and both Runs' outputs land on the same channel where they fold into the conversation tree by runId:
1
2
3
4
5
const followUp = await session.view.send(UIMessageCodec.createUserMessage({
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text: 'Also include vegan options.' }],
}));Cancel them independently with session.cancel(specificRunId) or activeRun.cancel().
Drive the Stop / Send toggle
session.view.runs() returns the visible RunInfo snapshots. Filter by status === 'active' to drive the Send / Stop toggle:
1
2
3
4
5
6
const { session } = useClientSession();
const isStreaming = session.view.runs().some((run) => run.status === 'active');
return isStreaming
? <StopButton onClick={() => cancelAll()} />
: <SendButton onClick={handleSend} />;Edge cases and unhappy paths
- Cancel is asynchronous. A small tail of tokens arrives after
session.cancel(runId)returns and before the agent'sabortSignalfires. The view emits them on the cancelled Run; treat them as belonging to that Run, not the new one. - A Run cancelled while awaiting a tool result ends with
'cancelled'. Tool calls triggered before the cancel may still run on your server unless your handler honours the sameAbortSignal. - A network drop on the client cancels nothing. The agent keeps streaming into the session. When the client reconnects, the response is still there. Check
session.view.runs()on reconnect to decide whether to show Stop. - Send-alongside is rate-limited by your channel and any server-side concurrency you enforce. See concurrent turns.
'cancelled'is reported on the Run's terminalRunEndReason. Don't infer cancellation from the absence of further tokens; subscribe toview.on('run', ...)and read the lifecycle event.
FAQ
What happens to tokens already in flight when I cancel?
The agent keeps publishing until its abortSignal fires. A small tail of tokens arrives after cancel() returns. The view reflects them on the cancelled Run; the new Run is unaffected.
Can I cancel one of several concurrent Runs without touching the others?
Yes. session.cancel(runId) only cancels the matching Run. Pull runIds from session.view.runs() to target the right one.
Does cancel work from a different device?
Yes. Any client connected to the same session can publish a cancel. The agent receives it through its own subscription regardless of which client started the Run. See cancellation for the authorisation model.
What if the agent endpoint is down when I send the follow-up?
The Ably channel publishes still happen (the cancel and the new user message both land on the channel). The new POST retries the agent endpoint; the new Run starts as soon as the endpoint is reachable. The session and existing messages are unaffected.
How is interruption different from concurrent turns?
Interruption is the user-facing pattern (Stop and re-prompt, or quick follow-up). Concurrent turns is the underlying mechanism that makes both possible: multiple Runs coexisting on the same session.
Related features
- Cancellation: cancel signals and agent-side authorisation.
- Concurrent turns: multiple Runs in flight on the same session.
- Double texting: handling multiple user messages in quick succession.