Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
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#
- Rename pendingInterrupts to interrupts.
- Replace each addToolApprovalResponse call with interrupt.resolveInterrupt(approved) on the matching item.
- Replace approval loops with resolveInterrupts(...).
- On the server, forward parentRunId and resume into chat().
- 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 |
|---|---|
| pendingInterrupts | interrupts |
| 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.error | interrupt.errors[0] |
| 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 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#
// 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:
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:
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:
- MESSAGES_SNAPSHOT
- STATE_SNAPSHOT (optional)
- 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:
- If your server wrote the 0.x custom events itself, replace those writers with the interrupt terminal.
- 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.