Skip to content
Interrupts

Migration

In 1.0, tool approval, a generic pause, and client-tool execution are AG-UI interrupts. Resolve each one on the item. addToolApprovalResponse and pendingInterrupts are removed.

There is no codemod. Upgrade the server and the client together. For the full list of 1.0 changes, see Upgrade to 1.0.

Quick migration

  1. Rename pendingInterrupts to interrupts.
  2. Replace each addToolApprovalResponse call with interrupt.resolveInterrupt(approved) on the matching item.
  3. Replace approval loops with resolveInterrupts(...).
  4. On the server, forward parentRunId and resume into chat().
  5. Finish or drop the runs that paused on 0.x. 1.0 cannot resume them.

The sections below show each step.

API mapping

0.x (removed in 1.0)1.0
pendingInterruptsinterrupts
ChatClient.getPendingInterrupts()ChatClient.getInterrupts()
addToolApprovalResponse({ id, approved })Find the bound tool-approval item and call interrupt.resolveInterrupt(approved). Removed from every framework and from ChatClient.
Raw resumeInterrupts(entries, state)Item methods, or root resolveInterrupts(...). Keep resumeInterruptsUnsafe for validated recovery tools.
interrupt.errorinterrupt.errors[0]
approval-requested custom eventRUN_FINISHED interrupt descriptor, reason tool_call
tool-input-available custom eventRUN_FINISHED interrupt descriptor, reason tanstack:client_tool_execution
Boolean denial treated as cancellationresolveInterrupt(false) for a denial. cancel() for a cancellation with no payload.

These stay the same:

  • addToolResult still sends client-tool results. It hands them to the matching native item.
  • needsApproval still turns on approval in the tool definition.

Single approval

ts
// 0.x
await addToolApprovalResponse({ id: approval.id, approved: true })

// 1.0
const interrupt = interrupts.find(
  (item) => item.kind === 'tool-approval' && item.toolName === 'transfer',
)
if (interrupt?.kind === 'tool-approval' && interrupt.toolName === 'transfer') {
  interrupt.resolveInterrupt(true)
}

A single valid item submits at once. For the full component, see Tool Approval.

Payloads and edits

In 0.x, a boolean approval had no typed data. In 1.0, add an approvalSchema to the tool, and send data under payload:

ts
interrupt.resolveInterrupt(true, {
  editedArgs: { amount: 12, recipient: 'Ada' }, // optional, approval only, full replacement
  payload: { note: 'Reviewed' },
})
interrupt.resolveInterrupt(false, { payload: { reason: 'Policy limit' } })
  • Rejection never accepts edits.
  • Custom fields at the top level are not valid.
  • A single approvalSchema (not an { approve, reject } map) applies to the selected decision.
  • With no schema, resolveInterrupt(true) and resolveInterrupt(false) stay valid.

Denial and cancellation

  • resolveInterrupt(false, ...) continues the model with an explicit rejected decision.
  • cancel() emits AG-UI status: 'cancelled'. It never validates or selects the reject branch.

If you called addToolApprovalResponse({ approved: false }), use resolveInterrupt(false). It is a denial, not a cancellation.

Batches

Native batches are all or nothing. Replace loops over approval ids with one of these:

  • Stage each item. The last valid item submits the batch.
  • Answer every item from one synchronous root callback:
ts
resolveInterrupts((interrupt) => {
  if (interrupt.kind === 'tool-approval') {
    interrupt.resolveInterrupt(true, { payload: { note: 'Batch review' } })
    return
  }
  interrupt.cancel()
})

Other root helpers:

  • resolveInterrupts(true) or resolveInterrupts(false): only for batches of approvals with no payload and no edits.
  • cancelInterrupts(): cancel every item, with no payload.
  • clearResolution(): drop the draft of one item.
  • retryInterrupts(): only when every item is still validly staged and the root error is retryable.

See Multiple Interrupts.

Generic responses

Do not derive a static type from a responseSchema that you received. Parse the value as unknown, convert the schema with z.fromJSONSchema, and then resolve with the validated value. For typed generic interrupts, use defineInterrupt. See Generic Interrupts.

Server events

A native server emits these events, in this order:

  1. MESSAGES_SNAPSHOT
  2. STATE_SNAPSHOT (optional)
  3. RUN_FINISHED with a nonempty interrupt outcome

A continuation has:

  • a new runId
  • the same threadId
  • the interrupted run as parentRunId
  • every pending id, exactly once

Interrupts are ephemeral. The server rebuilds and validates the expected batch from the submitted history and its current tool definitions. So a stateless route needs no persistence. But the batch comes from client input, so this mode does not give authoritative recovery, exactly-once delivery, replay protection, or restart recovery.

resumeInterruptsUnsafe is a low-level escape hatch. It sends validated raw resume entries directly. Do not use it for approval UI.

Old streams

1.0 does not read the approval-requested and tool-input-available custom events. An interrupt descriptor without a binding (only metadata.kind) shows as kind: 'unbound', and you cannot resume it. Upgrade the server and the client together, and finish or drop the runs that paused on 0.x.

Before you ship

The quick migration covers the client. Also do these:

  1. If your server wrote the 0.x custom events itself, replace those writers with the interrupt terminal.
  2. Test expired items and a failed transport.

When this is done, your approvals run on 1.0 interrupts. Start from the Overview for the current flow.