- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Upgrade to 1.0
1.0 removes the APIs that 0.x marked as deprecated. Each replacement already shipped in 0.x. Apply this page, then change the version.
Upgrade in 5 steps#
- Run the codemod. It does the most common renames for you.
- Update every @tanstack/ai* package to 1.0 at the same time. A 1.0 client needs a 1.0 server. If you use @tanstack/ai-react-ui, @tanstack/ai-solid-ui, or @tanstack/ai-vue-ui, remove it. These packages have no 1.0 release.
- Run your type checker. Fix each error with the sections In the browser and On the server.
- If you keep data from 0.x (chats, runs, files), do the steps in Saved data before you deploy.
- Do the done check.
Run the codemod#
npx jscodeshift \
--parser=tsx \
-t https://raw.githubusercontent.com/TanStack/ai/main/codemods/ag-ui-compliance/transform.ts \
"src/**/*.{ts,tsx}"To see the changes before the codemod writes them, add --dry --print.
The codemod does these renames:
| Before | After |
|---|---|
| useChat({ body }) | useChat({ forwardedProps }) |
| new ChatClient({ body }) | new ChatClient({ forwardedProps }) |
| client.updateOptions({ body }) | client.updateOptions({ forwardedProps }) |
| Svelte chat.updateBody(x) | chat.updateForwardedProps(x) |
| chat({ conversationId: x }) | chat({ threadId: x }) |
The codemod does not do these changes. Do them by hand:
- Server code that reads body.data. See Read forwardedProps, not data.
- The value of threadId after the rename. The codemod changes the key only. See conversationId is now threadId.
- body in createChatHook, in useAppChat overrides, and in Angular injectChat.
The codemod changes a file only when the file imports the hook or class from a TanStack AI package. A call through a re-export (a barrel file) does not change.
In the browser#
This section is for code that uses useChat, ChatClient, a chat UI factory, or any other client package.
body is now forwardedProps#
The chat-level body option is gone from these places:
- ChatClient and updateOptions.
- Every chat hook, and Angular injectChat.
- createChatHook and useAppChat overrides.
Use forwardedProps:
useChat({
connection: fetchServerSentEvents('/api/chat'),
- body: { provider: 'openai' },
+ forwardedProps: { provider: 'openai' },
})On Svelte, updateBody is now updateForwardedProps.
These options keep the name body, because each one is a different option:
- The connection adapter option, for example fetchServerSentEvents(url, { body }).
- The per-call option, sendMessage(content, { body }).
- The generation hooks, for example useGenerateImage({ body }) and its updateBody.
Interrupts#
Read interrupts. Resolve each item with its own methods:
- const { pendingInterrupts, resumeInterrupts } = useChat(chatOptions)
+ const { interrupts } = useChat(chatOptions)
- const approval = pendingInterrupts.find((item) => item.kind === 'tool-approval')
- await resumeInterrupts(resumeItems)
+ const approval = interrupts.find((item) => item.kind === 'tool-approval')
+ if (approval?.kind === 'tool-approval') approval.resolveInterrupt(true)
- interrupt.error?.message
+ interrupt.errors[0]?.message| Removed | Use instead |
|---|---|
| pendingInterrupts | interrupts |
| ChatClient.getPendingInterrupts() | ChatClient.getInterrupts() |
| resumeInterrupts(items) | interrupt.resolveInterrupt(...), interrupt.cancel(), or resolveInterrupts(...). For raw recovery tools, resumeInterruptsUnsafe(items). |
| interrupt.error | interrupt.errors[0] |
| addToolApprovalResponse() (all frameworks and ChatClient) | resolveInterrupt on the bound tool-approval interrupt |
An interrupt from a 0.x server that has no binding now shows as kind: 'unbound'. You cannot resume it. The error codes legacy-unsupported and legacy-submit-failed are gone. For a full approval screen, see Tool Approval.
Client tools#
A client tool resumes only when the tool on the server and the .client() tool in the browser have the same schemas, outputSchema included. If the schemas are different, the client does not run the tool. The pause shows as a generic interrupt, and it waits until something resolves it. addToolResult with the same toolCallId resolves that generic interrupt.
To keep the schemas the same, define the tool one time with toolDefinition(), and share that definition between the server and the client:
// tools/show-notification.ts: import this file on the server and in the browser
export const showNotificationDef = toolDefinition({
name: 'showNotification',
description: 'Show a toast notification in the browser',
inputSchema: z.object({ message: z.string() }),
outputSchema: z.object({ success: z.boolean() }),
})
// Browser
const showNotification = showNotificationDef.client(({ message }) => {
toast(message)
return { success: true }
})See Client Tools.
Chat UI components#
These ready-made components are gone from @tanstack/ai-react/ui, @tanstack/ai-solid/ui, @tanstack/ai-vue/ui, and @tanstack/ai-remix/ui:
- Chat, ChatMessages, ChatMessage, and ChatInput.
- ToolApproval.
- The old useChatContext export.
Their prop types are gone too. Build the UI with createChatHook. You supply each component, and the factory types them from your chat options:
- import { Chat, ChatMessages, ChatInput } from '@tanstack/ai-react-ui'
+ import { createChatHook } from '@tanstack/ai-react/ui'
+ const { useAppChat } = createChatHook({
+ options: chatOptions,
+ components: {
+ layout: ({ Messages, Input }) => (
+ <main>
+ <Messages />
+ <Input />
+ </main>
+ ),
+ message: ({ message, Parts }) => <article data-role={message.role}><Parts /></article>,
+ input: ChatForm,
+ },
+ partsComponents: { text: ({ part }) => <p>{part.content}</p>, fallback: () => null },
+ })
- export function ChatScreen() {
+ export function ChatScreen({ threadId }: { threadId: string }) {
- return (
- <Chat connection={connection}>
- <ChatMessages />
- <ChatInput />
- </Chat>
- )
+ const chat = useAppChat({ threadId })
+ return <chat.AppChat />
}TextPart and ThinkingPart stay. Import them from the same /ui subpath.
For a complete, runnable version, start with A chat box with no tools. Then open the guide for your framework: React, Solid, Vue, or Remix. If you build your own framework adapter, use createChatUI. See Custom adapters.
Pass a threadId to every chat and generation hook#
threadId is now required. The client does not make one for you. This applies to:
- useChat and the other chat hooks, and Angular injectChat.
- The hook that createChatHook returns. The factory options no longer take threadId. Pass it on each call, for example useAppChat({ threadId }).
- ChatClient.
- The generation hooks, for example useGenerateImage and useGeneration. On Octane, the generation hooks take threadId instead of id. A generation fetcher gets the threadId and runId in its second argument: fetcher: (input, { threadId, signal }) => ....
- useChat({ connection })
+ useChat({ connection, threadId: chatId })Use a stable id for each conversation. For example, use the id from your route or your database. Do not make a new random id on each render: a new id starts a new conversation.
The codemod cannot add threadId, because it does not know your ids.
If you call a connection adapter yourself, pass a run context with a threadId as the fourth argument of connect() or send(). The types require it, and the call fails without it.
Other client renames#
| Removed | Use instead |
|---|---|
| RealtimeAdapter, RealtimeConnection from @tanstack/ai-client | Import them from @tanstack/ai |
| useWebMCPTools (React, Preact, Solid, Vue, Octane) | useRegisterWebMCPTools |
| injectWebMCPTools (Angular) | injectRegisterWebMCPTools |
| createWebMCPTools (Svelte, Remix) | createRegisterWebMCPTools |
| Client persistence getItem that returns UIMessage[] | Return { messages } (see Saved data) |
Each WebMCP options type follows its function. For example, UseWebMCPToolsOptions is now UseRegisterWebMCPToolsOptions.
On the server#
This section is for these parts of your app:
- Your chat endpoint.
- Middleware and custom adapters.
- A server that is not TanStack AI, but streams to a TanStack AI client.
conversationId is now threadId#
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
- conversationId: params.forwardedProps.conversationId,
+ threadId: params.threadId,
})The client sends threadId as a top-level field, not in forwardedProps. After the codemod, change the value to params.threadId. Do not remove the option: threadId is now required. See chat() and the generation functions need a threadId.
Middleware reads ctx.threadId. ctx.conversationId is gone.
chat() and the generation functions need a threadId#
threadId is now required on chat(), summarize(), and the generation functions, for example generateImage(). The server does not make one for you.
- chat({ adapter, messages: params.messages })
+ chat({ adapter, messages: params.messages, threadId: params.threadId })- In a chat route, use params.threadId from chatParamsFromRequest().
- In a generation route, use threadId and runId from generationParamsFromRequest(). It throws when the body has no threadId or runId, so a body that holds only the input, with no AG-UI envelope, is rejected. The generation hooks always send the envelope.
- In a subagent run function, ctx.chat() and the other ctx functions fill threadId from the child run. A plain chat() call in run needs threadId: ctx.threadId.
- createChatOptions() does not need a threadId. Pass it on the chat() call that uses the options.
These also need a threadId now:
- codeModeWithSnippets() and selectRelevantSnippets() from @tanstack/ai-code-mode-snippets.
- CompactionContext.threadId and SummarizeInput.threadId in @tanstack/ai-compaction. This is the conversation id. Give a summary model call its own id, for example ${threadId}:compaction.
- createToolBridgeCore(), startHostToolBridge(), a custom ToolBridgeProvisioner, and executeHostTool() from @tanstack/ai-sandbox. The bridged tools get this threadId.
Read forwardedProps, not data#
The client sends the extra fields only as forwardedProps. The data copy is gone.
const params = await chatParamsFromRequest(request)
- const provider = params.forwardedProps.provider ?? body.data?.provider
+ const provider = params.forwardedProps.providerchatParamsFromRequestBody(body).context is now aguiContext.
The generation hooks (for example useGenerateImage) do the same. They send the input only in forwardedProps of the request body. If a custom generation route reads body.data, change it to read body.forwardedProps:
const body = await request.json()
- const { prompt } = body.data
+ const { prompt } = body.forwardedPropsCustom servers that stream to a TanStack AI client#
If your server does not use chat() and toServerSentEventsResponse(), make these changes:
- Stop sending data: [DONE]. End the run with a RUN_FINISHED event. The client cannot parse a [DONE] line, and it reports an error.
- Set message on RUN_ERROR. The client reads only the top-level message and code. It ignores a nested error.message.
- Send thinking as REASONING_* events. The client does not read STEP_STARTED or STEP_FINISHED for thinking text.
- Send approvals and client tools as interrupts. End the run with RUN_FINISHED and an interrupt outcome. The client does not read the CUSTOM events approval-requested and tool-input-available. See Interrupts.
The TanStack AI adapters no longer send STEP_STARTED or STEP_FINISHED for reasoning. If your middleware or devtools read those events for thinking, read the REASONING_* events.
Video adapters#
getVideo() is now required, and getVideoUrl() is gone from VideoAdapter and BaseVideoAdapter:
class MyVideoAdapter extends BaseVideoAdapter {
- async getVideoUrl(jobId: string) {
+ async getVideo(jobId: string) {
return { jobId, url: await this.fetchUrl(jobId) }
}
}In app code, call getVideoJobStatus({ adapter, jobId }), not adapter.getVideoUrl(jobId). See Video Generation.
Usage numbers#
TokenUsage.durationSeconds and TokenUsage.unitsBilled are gone. Read billed. It holds the quantity and its unit:
- const seconds = result.usage?.durationSeconds
+ const billed = result.usage?.billed
+ const seconds = billed?.unit === 'seconds' ? billed.quantity : undefinedThe OpenTelemetry attributes follow the same change:
- tanstack.ai.usage.duration_seconds and tanstack.ai.usage.units_billed are gone.
- Read tanstack.ai.usage.billed_quantity and tanstack.ai.usage.billed_unit.
See OpenTelemetry.
promptTokens on Anthropic, Bedrock, and Claude Code#
In 1.0, usage.promptTokens is the full input on every adapter: the uncached tokens, plus the cache reads, plus the cache writes. totalTokens uses this value.
Older 0.x releases of these adapters counted only the uncached tokens in promptTokens:
- @tanstack/ai-anthropic before 0.21.0.
- @tanstack/ai-bedrock (Converse) before 0.5.0.
- @tanstack/ai-claude-code before 0.8.0.
If your code adds the cache counts to promptTokens, remove that addition. If you keep it, you count the cache two times:
const details = usage.promptTokensDetails
- const input = usage.promptTokens + (details?.cachedTokens ?? 0) + (details?.cacheWriteTokens ?? 0)
+ const input = usage.promptTokensTo get only the uncached tokens, subtract cachedTokens and cacheWriteTokens from promptTokens. See Token usage.
Renamed types and helpers#
| Removed | Use instead |
|---|---|
| AGUIEventType, StreamChunkType | The EventType enum, or `${EventType}` for the string union |
| UsageTotals, ImageUsage | TokenUsage |
| OtelSpanKind | OtelSpanScope (from @tanstack/ai/middlewares/otel) |
| StreamProcessor.startAssistantMessage() | StreamProcessor.prepareAssistantMessage() |
| ApprovalRequestedEvent, ToolInputAvailableEvent | The interrupt outcome on RUN_FINISHED |
| toolName on TOOL_CALL_START chunks | toolCallName |
| resolveDurableThreadId, DurableThreadIdRequiredError (from @tanstack/ai-sandbox) | Nothing. threadId is always set, so pass the run record's threadId to chat() |
prepareAssistantMessage() returns nothing. The processor makes the assistant message when the first content arrives.
In @tanstack/ai-event-client, CompactionAppliedEvent and the 'compaction:applied' event are now CompactionStateEvent and 'compaction:state'.
Provider adapters#
Each provider tool type alias is gone. Use the Config name. For example, WebSearchTool is now WebSearchToolConfig:
| Package | Removed aliases |
|---|---|
| @tanstack/ai-anthropic | BashTool, CodeExecutionTool, ComputerUseTool, CustomTool, MemoryTool, TextEditorTool, WebFetchTool, WebSearchTool |
| @tanstack/ai-gemini | CodeExecutionTool, ComputerUseTool, FileSearchTool, GoogleMapsTool, GoogleSearchRetrievalTool, GoogleSearchTool, UrlContextTool |
| @tanstack/ai-openai, @tanstack/openai-base | ApplyPatchTool, CodeInterpreterTool, ComputerUseTool, CustomTool, FileSearchTool, FunctionTool, ImageGenerationTool, LocalShellTool, MCPTool, ShellTool, WebSearchPreviewTool, WebSearchTool |
| @tanstack/ai-openrouter | WebSearchTool |
Other provider changes:
| Provider | Change |
|---|---|
| Anthropic skills | Move modelOptions.container.skills to the code execution tool: codeExecutionTool(config, { skills }). |
| Anthropic media type | Delete mediaType from image and document metadata. The adapter reads the type from source.mimeType. The types AnthropicImageMediaType and AnthropicDocumentMediaType are gone. |
| Gemini image models | Google shut down gemini-3.1-flash-image-preview, gemini-3-pro-image-preview, and gemini-2.5-flash-image. Use gemini-3.1-flash-image, gemini-3-pro-image, or gemini-3.1-flash-lite-image. The type Gemini25FlashImageSize is gone. |
| Grok voice | grok-voice-think-fast-1.0 is now grok-voice-think-fast-2.0. |
Saved data#
1.0 does not read some formats that 0.x saved. Do these steps before you deploy:
- Finish or clear the runs in progress. @tanstack/ai-sandbox-cloudflare does not read run-log records in the 0.x layout (status: 'done' | 'error' and createdAt). Finish or delete the runs in progress, then clear the old rec: records. migrateStoredRunRecord is gone.
- Resolve or drop the pending interrupts. @tanstack/ai-persistence resumes only bound interrupts. A record saved before interrupt bindings existed does not resume.
- Add blobKey to artifact records. ArtifactRecord.blobKey is now required, and a record without it does not load. A 0.x record without blobKey used the key artifacts/<runId>/<artifactId>. Write that value into those records. A custom store must save blobKey. resolveArtifactBlobKey() is gone, so read record.blobKey.
- Return { messages } from client persistence. getItem must return { messages, resume? }. A bare UIMessage[] does not load. Wrap old values in getItem:
getItem: (id) => {
const saved = JSON.parse(localStorage.getItem(id) ?? 'null')
- return saved
+ return Array.isArray(saved) ? { messages: saved } : saved
},Two sandbox renames go with these steps:
- In @tanstack/ai-sandbox, FileEvent is now SandboxFileEvent.
- SandboxSnapshotBundle.resolveArtifactBlobKey is gone, because each record has blobKey.
See Client Persistence and Keep generated files.
Done#
You are on 1.0 when these three things are true:
Your type checker passes.
A search for the removed names finds nothing:
shellgrep -rnE "pendingInterrupts|resumeInterrupts\(|addToolApprovalResponse|conversationId|getVideoUrl|durationSeconds|unitsBilled|body\.data" srcA chat message streams a reply, and a saved chat loads after a page reload.
Do you come from an early 0.x release, with openai() adapters or providerOptions? Read the Migration Guide first. For the wire format, read AG-UI Client Compliance. For interrupts in depth, read Migrate to AG-UI interrupts.