Blog

TanStack AI now fully speaks AG-UI

by Alem Tuzlak on May 17, 2026.

TanStack AI now fully speaks AG-UI

Half the protocol was already there.

For a while now, endpoints built with @tanstack/ai have emitted AG-UI events on the wire going out. The streaming side of the conversation (RUN_STARTED, tool-call events, run finish, errors) was already a compliant AG-UI event stream. The piece that was still proprietary was the other direction: the request body going from client to server. The TanStack client POSTed { messages, data }, not AG-UI's RunAgentInput.

That last half is what this release fixes. TanStack AI is now fully AG-UI compliant in both directions. Server-to-client events were AG-UI before. Client-to-server requests are AG-UI now. The round trip is done.

The same @tanstack/ai-client can hit any AG-UI server. Any AG-UI client can hit an endpoint built with @tanstack/ai, wherever you host it (TanStack Start, Next.js, Hono, raw Node, Bun, anywhere). And nothing about your existing code breaks.

Why this matters

AG-UI is an open protocol for agent-to-frontend communication. It defines a single wire format, RunAgentInput, that carries the conversation, the tools, the thread and run IDs, and arbitrary forwarded properties. Servers that speak AG-UI can be addressed by any compliant client. Clients that emit AG-UI can talk to any compliant server.

With server-to-client AG-UI already in place, a @tanstack/ai endpoint could stream to a compliant client. But the client-to-server side was a one-way mirror: only the TanStack client could send requests that endpoint understood. The asymmetry meant true cross-vendor interop was still gated on rewriting your request layer.

Closing that gap is what this release does. The whole ecosystem (CopilotKit, CrewAI, LangGraph adapters, and now TanStack AI) gets to share the same plumbing in both directions.

What changed on the wire

Before this release, @tanstack/ai-client POSTed:

json
{
  "messages": [...],
  "data": { ... }
}
{
  "messages": [...],
  "data": { ... }
}

After:

json
{
  "threadId": "thread-7f2a",
  "runId": "run-a91",
  "state": {},
  "messages": [...],
  "tools": [...],
  "context": [],
  "forwardedProps": { ... },
  "data": { ... }
}
{
  "threadId": "thread-7f2a",
  "runId": "run-a91",
  "state": {},
  "messages": [...],
  "tools": [...],
  "context": [],
  "forwardedProps": { ... },
  "data": { ... }
}

The new envelope is the full AG-UI RunAgentInput. The old data field is still emitted as a mirror of forwardedProps so legacy servers reading body.data.X keep working unchanged. threadId persists per session, runId is fresh per send, and tools carries the client's clientTools declarations so the server can dispatch tool calls without a static registry.

Server-to-client events haven't changed shape, because they were already AG-UI compliant. They just now carry the matching threadId and runId you sent in.

What changed in the API

Three new things to know about, all opt-in.

chat() accepts threadId, runId, parentRunId

These were always part of the AG-UI event semantics on the way out. They're now first-class options on chat() and flow through every provider adapter into RUN_STARTED events for observability and run correlation.

ts
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai/adapters'

const stream = chat({
  adapter: openaiText('gpt-4o'),
  threadId: 'thread-7f2a',
  runId: 'run-a91',
  messages: [...],
})
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai/adapters'

const stream = chat({
  adapter: openaiText('gpt-4o'),
  threadId: 'thread-7f2a',
  runId: 'run-a91',
  messages: [...],
})

If you don't pass them, the runtime auto-generates a stable threadId per request and a fresh runId per call. Existing code that didn't know about them keeps working.

chatParamsFromRequest for the server

A one-import helper that reads req.json(), validates the body against the AG-UI RunAgentInputSchema, and gives you a clean params object. On invalid input it throws a 400 Response that frameworks like TanStack Start, SolidStart, Remix, and React Router 7 return to the client automatically.

ts
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai/adapters'

export async function POST(req: Request) {
  const params = await chatParamsFromRequest(req)
  const stream = chat({
    adapter: openaiText('gpt-4o'),
    messages: params.messages,
    threadId: params.threadId,
    tools: serverTools,
  })
  return toServerSentEventsResponse(stream)
}
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai/adapters'

export async function POST(req: Request) {
  const params = await chatParamsFromRequest(req)
  const stream = chat({
    adapter: openaiText('gpt-4o'),
    messages: params.messages,
    threadId: params.threadId,
    tools: serverTools,
  })
  return toServerSentEventsResponse(stream)
}

That's the whole server. No body shape to remember, no manual validation, and a typed params.forwardedProps if you want client-driven options like provider, model, or temperature.

forwardedProps replaces body on the client

useChat({ body: {...} }) still works, but body is now @deprecated. The canonical name is forwardedProps, which is what the new wire format calls the field. A jscodeshift codemod ships in the repo to flip every site:

sh
npx jscodeshift \
  --parser=tsx \
  -t https://raw.githubusercontent.com/TanStack/ai/main/codemods/ag-ui-compliance/transform.ts \
  "src/**/*.{ts,tsx}"
npx jscodeshift \
  --parser=tsx \
  -t https://raw.githubusercontent.com/TanStack/ai/main/codemods/ag-ui-compliance/transform.ts \
  "src/**/*.{ts,tsx}"

It's import-source gated, so files that don't import from @tanstack/ai* are left alone.

Nothing breaks

This is the part most "wire format change" releases get wrong. The upgrade ships three compatibility bridges so old code keeps running:

SurfaceLegacy (still works)Canonical
Client optionbody: { ... }forwardedProps: { ... }
Server wire fieldbody.data.X (mirror of forwardedProps)body.forwardedProps.X
Server chat() optionconversationIdthreadId

An existing endpoint reading body.data.provider keeps reading body.data.provider because the client emits both data and forwardedProps with the same content. A chat({ conversationId }) call keeps working because conversationId is now a deprecated alias of threadId. Mix old and new freely. The bridges will be removed in the next major release, so migrate at your convenience.

Bidirectional interop in practice

With both halves of the protocol compliant, the boundaries between AI SDKs get a lot blurrier.

A pure AG-UI client (no TanStack code) hitting a @tanstack/ai endpoint works end-to-end. Tool messages pass through as ModelMessage entries with role: 'tool'. AG-UI reasoning and activity messages with no TanStack equivalent are dropped at the boundary. developer messages collapse to system role. The outbound event stream was already AG-UI, so the foreign client renders it natively.

A TanStack client hitting a foreign AG-UI server works for the common cases. Single-turn user messages mirror to AG-UI's content field. Server-emitted events stream and render. Multi-turn history with tool results from prior turns survives because the client sends AG-UI fan-out duplicates alongside the TanStack anchor messages.

The practical upshot: if you've been waiting to try a different inference provider, a different framework's agent runtime, or a different orchestrator, the wire is no longer the thing standing in your way. Both directions speak the same language.

What's not in this release

A few things were intentionally left out:

  • Reasoning replay to LLM providers. TanStack still drops ThinkingPart at the UIMessageModelMessage boundary. Providers like Anthropic that require thinking blocks to be replayed for extended thinking continuation are a separate track.
  • AG-UI state and context fields. Surfaced on the params object but not yet wired into chat(). They're available for your endpoint to inspect or forward.
  • PHP and Python server packages. No chatParamsFromRequest parity yet. Those examples temporarily lag on the old shape until the matching helpers ship.

Try it

Upgrade @tanstack/ai and @tanstack/ai-client to the latest. If you're using one of the framework wrappers (@tanstack/ai-react, -vue, -svelte, -solid, -preact), bump those too so the client wire stays in lockstep.

The AI stack is supposed to be the part you compose, not the part that locks you in. AG-UI is how that starts being true across vendors. With this release, TanStack AI is the first SDK to ship full bidirectional client-to-server and server-to-client compliance against the AG-UI 0.0.52 spec. The next agent runtime you adopt should not be the one that finally forces you to rewrite your wire layer.