# Migration

# Migration

TanStack AI now models approvals, generic pauses, and client-tool execution as
AG-UI interrupt descriptors. Native runs end with
`RUN_FINISHED.outcome.type === 'interrupt'`, and the continuation is a new run
whose `parentRunId` is the interrupted run.

There is no codemod. Migrate the server lifecycle and client rendering together.
Legacy readers stay temporarily for old streams but cannot provide the full
native contract. Start from [Overview](./overview).

## API mapping

| Deprecated / legacy | Current |
| --- | --- |
| `pendingInterrupts` | `interrupts` (`pendingInterrupts` is a deprecated alias of the same array) |
| `ChatClient.getPendingInterrupts()` | `ChatClient.getInterrupts()` |
| `addToolApprovalResponse({ id, approved })` | Find the bound `tool-approval` item, call `interrupt.resolveInterrupt(approved)` |
| Raw `resumeInterrupts(entries, state)` | Bound item methods or root `resolveInterrupts(...)`; reserve `resumeInterruptsUnsafe` for validated recovery tooling |
| `approval-requested` custom event | `RUN_FINISHED` interrupt descriptor, reason `tool_call` |
| `tool-input-available` custom event | `RUN_FINISHED` interrupt descriptor, reason `tanstack:client_tool_execution` |
| Boolean denial treated as cancellation | `resolveInterrupt(false)` for denial; `cancel()` for payloadless cancellation |

`addToolResult` is **not** removed. It still handles client-tool results and
delegates to a matching native item. `needsApproval` remains the tool-definition
switch for approvals.

## Single approval

```ts ignore
// Before
await addToolApprovalResponse({ id: approval.id, approved: true })

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

A valid singleton submits automatically. For the full render/resolve component
see [Tool Approval](./tool-approval).

## Branch payloads and edits

Legacy boolean approvals couldn't carry typed data. Add `approvalSchema` and
resolve the selected branch with data under `payload`:

```ts ignore
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; top-level custom fields are invalid. A single
`approvalSchema` (not `{ approve, reject }`) applies to the selected decision;
with no schema the boolean shorthand stays valid.

## Denial vs cancellation

`resolveInterrupt(false, ...)` continues the model with an explicit rejected
decision. `cancel()` emits AG-UI `status: 'cancelled'` and never validates or
selects the reject branch. Deprecated `addToolApprovalResponse({ approved: false })`
maps to denial, not cancellation.

## Batches

Native batches are all-or-nothing. Replace approval-ID loops with staged items
(the last valid item auto-submits) or one synchronous root callback:

```ts ignore
resolveInterrupts((interrupt) => {
  if (interrupt.kind === 'tool-approval') {
    interrupt.resolveInterrupt(true, { payload: { note: 'Batch review' } })
    return
  }
  interrupt.cancel()
})
```

`resolveInterrupts(true|false)` is shorthand only for all-approval batches with
no payload/edits. Use `cancelInterrupts()` for payloadless all-items cancel,
`clearResolution()` to drop one draft, `retryInterrupts()` only when every item
is still validly staged and the root error is retryable. See
[Multiple Interrupts](./multiple).

## Generic responses

Don't derive a static type from a received `responseSchema`. Parse as
`unknown`, convert with `z.fromJSONSchema`, then resolve the validated value.
Full form example in [Generic Interrupts](./generic).

## Server events

A native server emits, in order: `MESSAGES_SNAPSHOT` → optional
`STATE_SNAPSHOT` → `RUN_FINISHED` with a nonempty interrupt outcome.
Continuations use a fresh `runId`, the same `threadId`, and the interrupted run
as `parentRunId`, with every pending ID present exactly once.

Interrupts run **ephemerally**: the server reconstructs and validates the
expected batch from the submitted history and its current tool definitions, so
a stateless route needs no persistence. Because the batch is rebuilt from
client-provided input, this mode does not provide authoritative recovery,
exactly-once, replay protection, or restart recovery.

`resumeInterruptsUnsafe` is a low-level escape hatch for submitting validated
raw resume entries directly, not the normal target for approval UI.

## Legacy limits

Deprecated readers recognize well-formed historical `approval-requested` and
`tool-input-available` events and convert a fully-covered legacy batch into one
cloned-history follow-up. They do **not** support edited arguments, custom
approval payloads, generic responses, payloadless cancellation, or expiry/
schema-hash reconciliation; those fail with `legacy-unsupported`. Native and
legacy items cannot mix in one batch; a failed legacy transport keeps staged
decisions and reports `legacy-submit-failed`.

## Checklist

1. Replace native custom-event writers with the interrupt terminal.
2. Render bound `interrupts` instead of `pendingInterrupts`.
3. Replace boolean approval helpers with `resolveInterrupt` + explicit
   denial/cancellation.
4. Replace approval loops with atomic batch staging or root `resolveInterrupts`.
5. Keep `addToolResult` for client-tool results where useful.
6. Test expired items and failed transport before removing legacy support.
