AI & Development

TanStack AI 1.0 or Vercel AI SDK 7: Pick the Protocol First

TanStack AI hit 1.0 on October 22. The real stream of each SDK captured in a test, the weight of each client and why the protocol decides between them.

Published on October 29, 202615 min read

Monday morning, a team of four opens the ticket "assistant inside the app". The prototype has been running for three weeks on the Vercel AI SDK. Its "Approve" button hangs on one precise detail: a message part named tool-envoyerEmail, in the approval-requested state. That name comes out of Vercel's stream protocol. A TanStack AI server sends nothing like it. So the question of the day is concrete: keep what you know, or look at the SDK everyone has been talking about for a week.

That SDK is TanStack AI, which just went 1.0. @tanstack/ai@1.0.0 shipped on October 22, 2026, according to the repository's release list1 and its release notes2. The announcement post3 is dated the 23rd.

Both SDKs cover the same ground. What separates them is the stream protocol your components read: that choice is hard to undo, so protect it with a client-side adaptation layer. Client weight mostly depends on how you import tool definitions.

I built a small test outside the repository from the examples in each SDK's documentation, with a fake model and no network call, to compile the code and capture the real stream of each. The article reports on it, then says how to decide this week. It is the kind of call I make at the start of an engagement on LLM agents and workflows, before the interface depends on the stream.

One caveat up front: the test is minimal, a single support case, no call to Anthropic. I have not run TanStack AI in production and I found no third-party production report. The rest comes from TanStack's documentation, announcements and repository, and I flag it whenever a number is theirs.

Three words before we start

  • an adapter is the module that talks to a model provider. TanStack has one per activity and per provider. The 1.0 post counts 23 of them3;
  • a stream protocol is the format of the messages the server sends to the interface while the model answers: text chunks, tool calls, approval requests. AG-UI is an open protocol of this kind. The Vercel AI SDK has its own, called UI Message Stream;
  • an isomorphic tool is a tool defined once, with its schemas, then attached to an execution on the server or in the client.

The second one is what matters. Adapters and tools swap at the margins from one SDK to the other. The protocol runs through your code up to the components.

TanStack AI 1.0 commits the core APIs and leaves the harness experimental

1.0 adds no concept: TanStack removed the APIs that 0.x marked as deprecated, each replacement already existing3. The number commits the core APIs. The alpha, beta and candidate phases shipped as 0.x, the last being 0.71.0 on October 21, per the release list1.

If you come from 0.x, the breaking changes are in the release notes2 and the migration guide4:

  • threadId becomes mandatory on client and server, conversationId is renamed threadId and body becomes forwardedProps;
  • addToolApprovalResponse() is gone, and the approval interrupt is resolved with resolveInterrupt;
  • old wire formats are removed, including the SSE [DONE] line: a 1.0 client requires a 1.0 server.

Only @tanstack/ai-harness, the durable agent sessions layer published as 0.1.0, stays experimental: its documentation says it can change in a minor release5. The rest covers only the core.

Tools, route and approval fit in little code

The test case comes down to two tools, a route and a hook, and everything compiles in strict mode. It is the case from the Jev article: a support assistant that classifies a ticket, with chercherTicket running on the server and envoyerEmail requiring approval, on claude-sonnet-5-5. Each snippet starts from the official documentation. Versions: the @tanstack/* packages at 1.0.0, ai 7.0.156, @ai-sdk/react 4.0.160 and zod 4.6.7. Identifiers stay French because the test ran on the French case; strings and comments are translated.

The next snippet defines both tools. .server() gives a server-side execution and needsApproval: true suspends the tool until the user agrees6:

export const chercherTicketDef = toolDefinition({
  name: 'chercherTicket',
  description: 'Look up a support ticket by its identifier',
  inputSchema: z.object({ identifiant: z.string() }),
  outputSchema: z.object({
    identifiant: z.string(),
    objet: z.string(),
    urgence: z.enum(['urgent', 'normal', 'basse']),
  }),
})

export const envoyerEmailDef = toolDefinition({
  name: 'envoyerEmail',
  description: 'Send an email to the customer',
  inputSchema: z.object({
    destinataire: z.string(),
    objet: z.string(),
    corps: z.string(),
  }),
  outputSchema: z.object({ envoye: z.boolean() }),
  needsApproval: true,
})

// Runs on the server, only after the user approves
export const envoyerEmail = envoyerEmailDef.server(
  async ({ destinataire, objet }) => {
    console.log(`Sending "${objet}" to ${destinataire}`)
    return { envoye: true }
  },
)

The route calls chat() and returns the stream over SSE. The adapter is a parameter, which lets the test plug in a fake model:

import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { chercherTicket, envoyerEmail } from './tanstack-outils'

type AdaptateurTexte = Parameters<typeof chat>[0]['adapter']

export function creerRoute(adapter: AdaptateurTexte) {
  return async function POST(request: Request) {
    const params = await chatParamsFromRequest(request)

    const flux = chat({
      adapter,
      messages: params.messages,
      threadId: params.threadId,
      runId: params.runId,
      parentRunId: params.parentRunId,
      ...(params.resume ? { resume: params.resume } : {}),
      systemPrompts: ['You classify tickets and suggest a reply.'],
      tools: [chercherTicket, envoyerEmail],
    })

    return toServerSentEventsResponse(flux)
  }
}

export const POST = (request: Request) =>
  creerRoute(anthropicText('claude-sonnet-5-5'))(request)

On the interface side, the hook connects to the route and the approval arrives in interrupts7:

const { messages, sendMessage, interrupts, resuming } = useChat({
  connection: fetchServerSentEvents('/api/chat'),
  threadId: 'support-chat',
  tools: [envoyerEmailDef],
})

// …
{interrupts.map((interruption) =>
  interruption.kind === 'tool-approval' ? (
    <div key={interruption.id}>
      <p>Send this email to the customer?</p>
      <pre>{JSON.stringify(interruption.originalArgs, null, 2)}</pre>
      <button
        disabled={!interruption.canResolve || resuming}
        onClick={() => interruption.resolveInterrupt(true)}
      >
        Approve
      </button>
    </div>
  ) : null,
)}

Human validation takes one line of configuration and one button.

The fake models come from the documentation: fakeText() on the TanStack side8 and MockLanguageModelV4 on the Vercel side9. Both return a bit of text, a call to chercherTicket, then a closing sentence.

Two Vercel examples need a fix. The client on the approval page does not compile until useChat is typed with InferUITools10. And streamText() stops after one step by default11: my first capture ended on the executed tool, with no second answer, until I added stopWhen: isStepCount(5). ToolLoopAgent runs up to twenty steps according to this blog's agentic guide and chat() stops after five turns12: an agent that chains ten tool calls stops at the fifth with TanStack and runs to the end with ToolLoopAgent.

The client weight comes down to one import. I compiled each side's component with esbuild (minified bundle, react and react-dom external, browser platform). With the hook above, the TanStack client weighs 679,733 bytes, 154,170 gzipped, against 201,230 and 55,329 at Vercel. That 2.8-fold gap comes from import { envoyerEmailDef }, which pulls all of zod into the browser: 453,883 output bytes. Without that import, the TanStack client weighs 224,224 bytes, 60,665 gzipped, which is 5,336 more than the Vercel client.

Removing the import and tools: [envoyerEmailDef] breaks the typing: interrupts is no longer recognized as tool-approval. The documentation always passes the tools to useChat and describes no alternative. The package types offer one, which compiles and gives the same 60,665-byte bundle: a type-only import, passed as a type parameter.

import type { envoyerEmailDef } from './tanstack-outils'

const { messages, sendMessage, interrupts, resuming } = useChat<
  readonly [typeof envoyerEmailDef]
>({
  connection: fetchServerSentEvents('/api/chat'),
  threadId: 'support-chat',
})

I only compiled this variant, without running it against a real stream. So the weight is decided by how you share the tool definition with the client.

The stream protocol is the choice you cannot undo

The two SDKs emit incompatible streams: that is the deciding criterion, ahead of the feature list. TanStack's comparison page puts it this way: "TanStack AI streams the open AG-UI protocol."13 It adds that the Vercel AI SDK streams its own protocol and that TanStack AI neither reads nor writes that one.

Here is the wire, for the same answer from the fake model. I removed timestamps, metadata and token counts, shortened the text and spread events longer than 80 characters over several lines. The first excerpt is the SSE body from toServerSentEventsResponse, which follows the AG-UI events14:

{"type":"RUN_STARTED","runId":"run-1","threadId":"support-chat"}
{"type":"TEXT_MESSAGE_START","messageId":"run-1-text","role":"assistant"}
{"type":"TEXT_MESSAGE_CONTENT","messageId":"run-1-text","delta":"Look"}
: …
{"type":"TEXT_MESSAGE_END","messageId":"run-1-text"}
{"type":"TOOL_CALL_START","toolCallId":"c1","toolCallName":"chercherTicket"}
{
  "type": "TOOL_CALL_ARGS",
  "toolCallId": "c1",
  "delta": "{\"identifiant\":\"T-1042\"}"
}
{"type":"TOOL_CALL_END","toolCallId":"c1"}
{"type":"RUN_FINISHED","runId":"run-1","threadId":"support-chat"}
: TOOL_CALL_RESULT, then the final answer

The second is Vercel's stream response, described on the protocol page15:

{"type":"start"}
{"type":"start-step"}
{"type":"text-start","id":"t1"}
{"type":"text-delta","id":"t1","delta":"Looking at ticket "}
: …
{"type":"text-end","id":"t1"}
{
  "type": "tool-input-available",
  "toolCallId": "c1",
  "toolName": "chercherTicket",
  "input": {
    "identifiant": "T-1042"
  }
}
: tool-output-available, then the final answer
{"type":"finish-step"}

A TanStack component reads events that open and close by messageId or toolCallId. A Vercel component reads lowercase types and gets the tool input in one block. The approvals have nothing in common: Vercel sends a tool-approval-request event in the middle of the message, while TanStack ends the turn with a RUN_FINISHED whose outcome is interrupt, carrying reason: "tool_call" and the toolCallId concerned, as AG-UI's interrupts page describes16. Vercel closes its stream with data: [DONE] and an x-vercel-ai-ui-message-stream: v1 header, which TanStack does not send.

TanStack's stream departs from AG-UI's documentation on two points, observed with fakeText() and not with the Anthropic adapter: two RUN_FINISHED for one RUN_STARTED, and a messageId reused by the final answer. I do not know whether this comes from the fake adapter or from chat().

Components built on Vercel's protocol, AI Elements and the useChat hook from @ai-sdk/react, read the second stream only. If your app already shows a conversation with those pieces, moving to TanStack AI means rewriting the interface as well as the model call. TanStack advises staying on Vercel in that case13. It is the view of an interested maker, but the captured stream backs it up.

The rest of the migration is mechanical, per TanStack's mapping table13:

Vercel AI SDKTanStack AI
streamText()chat()
generateObject()chat({ outputSchema })
stopWhen: stepCountIs(n)agentLoopStrategy: maxIterations(n)
tool()toolDefinition() then .server() or .client()

The right choice depends on the interface you already have

For a new app, TanStack AI is a good candidate. For an app that already speaks Vercel's protocol, stay. My table mixes what TanStack says, what this blog's agentic guide says, what I ship and the measurements from the test.

SituationTanStack AI 1.0Vercel AI SDK 7Mastra
New app, interface to buildGood candidate, open protocolGood candidate, the most widespread UI ecosystemNo view
App already speaking the Vercel protocolInterface rewriteStayA separate decision, on the server side
Full framework: memory, RAG, evaluationTo assembleTo assemble with third partiesWhere it earns its place, see the Mastra 1.0 article
Client weight in the test (gzip)154,170 bytes with the tool definition, 60,665 without55,329 bytesNot measured

The guide rates the maturity of Vercel AI SDK 7 as high and Mastra's as medium: a version number does not put TanStack AI higher. Its advice to take Vercel for Next.js also stands, since my test did not touch Next.js.

TanStack announces a built-in MCP client and server, as the article on MCP and A2A describes, and sub-agents, which the multi-agent architecture with single-threaded writes piece covers.

What it does not solve

The test proves little: one component, two tools and a fake model. It says nothing about a real answer from Anthropic, a recovery after a network drop, a streamed structured output, MCP, sub-agents, decide(), Next.js or whether AI Elements works against a TanStack server. I also did not verify TanStack's claim about per-model typing, which Vercel would only have per provider.

The post describes a team of three maintainers and 143 contributors3: depending on so few people is a real risk when the SDK carries your core feature. The project has an MIT license and 3,275 GitHub stars17.

Adoption is much lower. Over the week of October 21 to 27, @tanstack/ai was downloaded 973,955 times on npm and Vercel's ai package 34,285,429 times, about 35 times more, according to npm-stat18. Those counters include mirrors and continuous integration, and the window contains the 1.0 release: only the order of magnitude counts.

The harness is still experimental, with resume limits its documentation flags5: if your product depends on it, plan to rewrite. TanStack's comparison table gives about 38 provider packages on the Vercel side13. It is written by TanStack, so it is one-sided, and I did not cross-check it. I found no performance benchmark published by a third-party team.

Isolate the protocol behind an adaptation layer this week

Whatever the SDK, isolate it behind an interface of your own, as the MVP playbook recommends: your business code calls answer(messages) and only a small module knows chat() or streamText(). For a new app, then try TanStack AI for two days on your most demanding screen: a tool with approval, a streamed structured output, a recovery after a network drop. If it holds without contortions, its open protocol is a real argument. For an app that already speaks Vercel's protocol, do not touch it just because 1.0 came out: rewriting the interface almost always costs more than you gain.

The stream protocol is the part that spills over, because it reaches the components. Confine it to one client-side layer: a single file that turns message parts into your own types, along the lines of the MoteurDeDecision contract in the Jev article. Here is the one from the test:

import type { UIMessage as MessageTanstack } from '@tanstack/ai-react'
import type { UIMessage as MessageVercel } from 'ai'

// The interface types: components only know these
type Appel = { identifiant: string; nom: string; entree: unknown }
export type PartieDeMessage =
  | { type: 'texte'; texte: string }
  | ({ type: 'outil'; etat: 'en-cours' | 'termine' } & Appel)
  | ({ type: 'approbation' } & Appel)

export function depuisTanstack(
  parties: MessageTanstack['parts'],
): PartieDeMessage[] {
  return parties.flatMap((p): PartieDeMessage[] => {
    if (p.type === 'text') return [{ type: 'texte', texte: p.content }]
    if (p.type !== 'tool-call') return []
    const appel = { identifiant: p.id, nom: p.name, entree: p.input }
    if (p.approval?.needsApproval && p.approval.approved === undefined)
      return [{ type: 'approbation', ...appel }]
    const etat = p.output === undefined ? 'en-cours' : 'termine'
    return [{ type: 'outil', etat, ...appel }]
  })
}

export function depuisVercel(
  parties: MessageVercel['parts'],
): PartieDeMessage[] {
  return parties.flatMap((p): PartieDeMessage[] => {
    if (p.type === 'text') return [{ type: 'texte', texte: p.text }]
    if (!p.type.startsWith('tool-')) return []
    const o = p as { toolCallId: string; state: string; input?: unknown }
    const nom = p.type.slice(5)
    const appel = { identifiant: o.toolCallId, nom, entree: o.input }
    if (o.state === 'approval-requested')
      return [{ type: 'approbation', ...appel }]
    const etat = o.state === 'output-available' ? 'termine' : 'en-cours'
    return [{ type: 'outil', etat, ...appel }]
  })
}

MessageTanstack and MessageVercel are each SDK's UIMessage. On hand-built parts, a text followed by an approval request, the two functions return the same list. The user's answer goes through resolveInterrupt on one side and addToolApprovalResponse on the other, and belongs in the same file.

What to remember

  • TanStack AI 1.0 adds no concept: it removes the 0.x deprecations and commits the core APIs, except the harness, which stays experimental. Client and server must both be on 1.0.
  • The choice between TanStack AI and Vercel AI SDK 7 is about the stream protocol: in the test, tool approval is an interrupt inside RUN_FINISHED on one side and a tool-approval-request event on the other.
  • In the test, the two clients weigh 60,665 and 55,329 bytes gzipped. Importing the tool definition on the client pushes TanStack to 154,170 bytes because it ships zod. A type-only import avoids that.
  • TanStack's figures, comparison table included, are TanStack's. I have not run the SDK in production or found third-party reports.
  • Isolate the SDK behind an interface of your own and confine the stream protocol to a single adaptation layer.

I make the SDK choice with clients at scoping time, before the interface is written. It carries on into building an AI-first web product.

Sources

  1. TanStack, TanStack AI release list, accessed 10/29/2026.
  2. TanStack, release notes 1.0.0, October 22, 2026.
  3. TanStack, post "TanStack AI 1.0", October 23, 2026.
  4. TanStack, migration guide, accessed 10/29/2026.
  5. TanStack, harness, accessed 10/29/2026.
  6. TanStack, tools, accessed 10/29/2026.
  7. TanStack, tool approval, accessed 10/29/2026.
  8. TanStack, testing with fakeText, accessed 10/29/2026.
  9. Vercel, AI SDK, testing, accessed 10/29/2026.
  10. Vercel, AI SDK, tools and approval, accessed 10/29/2026.
  11. Vercel, AI SDK, streamText reference, accessed 10/29/2026.
  12. TanStack, agentic cycle, accessed 10/29/2026.
  13. TanStack, comparison with Vercel, accessed 10/29/2026.
  14. AG-UI, events, accessed 10/29/2026.
  15. Vercel, AI SDK, stream protocol, accessed 10/29/2026.
  16. AG-UI, interrupts, accessed 10/29/2026.
  17. TanStack, GitHub repository, accessed 10/29/2026.
  18. npm-stat, npm downloads of @tanstack/ai and ai, October 21 to 27, 2026.