Cancellation

Open in

Cancellation of an agent invocation is supported in AI Transport. Cancellation is a turn-level operation. The client publishes a cancel signal on the Ably channel, the server receives it and aborts the matching turns, but the session remains intact, other turns continue, and both sides handle cleanup gracefully. Unlike closing an HTTP connection, cancellation is an explicit signal.

How it works

Since sessions are bidirectional, cancel is just a signal on the channel. The client publishes a cancel message with a filter specifying which turns to cancel. The server's transport matches the filter against active turns and fires their abort signals. The LLM stream stops, the turn ends with reason 'cancelled', and all subscribers are notified.

JavaScript

1

2

3

4

5

6

7

// Client: cancel the current turn
await turn.cancel()

// Server: abort signal fires automatically
const result = streamText({
  abortSignal: turn.abortSignal,  // wired to cancel
})

Cancel filters

Cancel signals are scoped. You control exactly what gets cancelled:

FilterEffectUse case
{ own: true } (default)Cancel all turns started by this clientStop button
{ turnId: 'abc' }Cancel one specific turnCancel a specific generation
{ clientId: 'user-1' }Cancel all turns by a specific clientAdmin cancellation
{ all: true }Cancel all turns on the channelEmergency stop
JavaScript

1

2

3

4

5

6

7

8

// Cancel your own turns (default)
await transport.cancel()

// Cancel a specific turn
await transport.cancel({ turnId: activeTurn.turnId })

// Cancel all turns on the channel
await transport.cancel({ all: true })

Server-side handling

Abort signal

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

JavaScript

1

2

3

4

5

6

7

8

9

10

11

const turn = transport.newTurn({ turnId, clientId })
await turn.start()

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

const { reason } = await turn.streamResponse(result.toUIMessageStream())
await turn.end(reason)  // reason is 'cancelled' if abort fired

Cancel authorization

The onCancel hook lets you authorize or reject cancel requests:

JavaScript

1

2

3

4

5

6

7

8

9

const turn = transport.newTurn({
  turnId,
  clientId,
  onCancel: async (request) => {
    // Only allow the turn owner to cancel
    const owner = request.turnOwners.get(request.filter.turnId)
    return owner === request.message.clientId
  },
})

The CancelRequest includes message (the raw cancel message with clientId), filter (parsed scope), matchedTurnIds, and turnOwners (map of turn ID to owner client ID). Return false to reject.

Abort hook

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

JavaScript

1

2

3

4

5

6

7

const turn = transport.newTurn({
  turnId,
  clientId,
  onAbort: async (write) => {
    await write({ type: 'text-delta', textDelta: '\n[Response cancelled]' })
  },
})

Cancel on close

When a client transport closes, it can optionally cancel its own turns:

JavaScript

1

await transport.close({ cancel: { own: true } })