|
| 1 | +--- |
| 2 | +title: 'TanStack AI now fully speaks AG-UI' |
| 3 | +published: 2026-05-17 |
| 4 | +excerpt: 'Server-to-client AG-UI events already worked. TanStack AI now completes the round trip with client-to-server AG-UI compliance. Fully backward compatible.' |
| 5 | +library: ai |
| 6 | +authors: |
| 7 | + - Alem Tuzlak |
| 8 | +--- |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | +Half the protocol was already there. |
| 13 | + |
| 14 | +For a while now, endpoints built with `@tanstack/ai` have emitted [AG-UI](https://ag-ui.com) 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`. |
| 15 | + |
| 16 | +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. |
| 17 | + |
| 18 | +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. |
| 19 | + |
| 20 | +## Why this matters |
| 21 | + |
| 22 | +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. |
| 23 | + |
| 24 | +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. |
| 25 | + |
| 26 | +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. |
| 27 | + |
| 28 | +## What changed on the wire |
| 29 | + |
| 30 | +Before this release, `@tanstack/ai-client` POSTed: |
| 31 | + |
| 32 | +```json |
| 33 | +{ |
| 34 | + "messages": [...], |
| 35 | + "data": { ... } |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +After: |
| 40 | + |
| 41 | +```json |
| 42 | +{ |
| 43 | + "threadId": "thread-7f2a", |
| 44 | + "runId": "run-a91", |
| 45 | + "state": {}, |
| 46 | + "messages": [...], |
| 47 | + "tools": [...], |
| 48 | + "context": [], |
| 49 | + "forwardedProps": { ... }, |
| 50 | + "data": { ... } |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +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. |
| 55 | + |
| 56 | +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. |
| 57 | + |
| 58 | +## What changed in the API |
| 59 | + |
| 60 | +Three new things to know about, all opt-in. |
| 61 | + |
| 62 | +### `chat()` accepts `threadId`, `runId`, `parentRunId` |
| 63 | + |
| 64 | +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. |
| 65 | + |
| 66 | +```ts |
| 67 | +import { chat } from '@tanstack/ai' |
| 68 | +import { openaiText } from '@tanstack/ai-openai/adapters' |
| 69 | + |
| 70 | +const stream = chat({ |
| 71 | + adapter: openaiText('gpt-4o'), |
| 72 | + threadId: 'thread-7f2a', |
| 73 | + runId: 'run-a91', |
| 74 | + messages: [...], |
| 75 | +}) |
| 76 | +``` |
| 77 | + |
| 78 | +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. |
| 79 | + |
| 80 | +### `chatParamsFromRequest` for the server |
| 81 | + |
| 82 | +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. |
| 83 | + |
| 84 | +```ts |
| 85 | +import { |
| 86 | + chat, |
| 87 | + chatParamsFromRequest, |
| 88 | + toServerSentEventsResponse, |
| 89 | +} from '@tanstack/ai' |
| 90 | +import { openaiText } from '@tanstack/ai-openai/adapters' |
| 91 | + |
| 92 | +export async function POST(req: Request) { |
| 93 | + const params = await chatParamsFromRequest(req) |
| 94 | + const stream = chat({ |
| 95 | + adapter: openaiText('gpt-4o'), |
| 96 | + messages: params.messages, |
| 97 | + threadId: params.threadId, |
| 98 | + tools: serverTools, |
| 99 | + }) |
| 100 | + return toServerSentEventsResponse(stream) |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +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. |
| 105 | + |
| 106 | +### `forwardedProps` replaces `body` on the client |
| 107 | + |
| 108 | +`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: |
| 109 | + |
| 110 | +```bash |
| 111 | +npx jscodeshift \ |
| 112 | + --parser=tsx \ |
| 113 | + -t https://raw.githubusercontent.com/TanStack/ai/main/codemods/ag-ui-compliance/transform.ts \ |
| 114 | + "src/**/*.{ts,tsx}" |
| 115 | +``` |
| 116 | + |
| 117 | +It's import-source gated, so files that don't import from `@tanstack/ai*` are left alone. |
| 118 | + |
| 119 | +## Nothing breaks |
| 120 | + |
| 121 | +This is the part most "wire format change" releases get wrong. The upgrade ships three compatibility bridges so old code keeps running: |
| 122 | + |
| 123 | +| Surface | Legacy (still works) | Canonical | |
| 124 | +| ---------------------- | ---------------------------------------- | ------------------------- | |
| 125 | +| Client option | `body: { ... }` | `forwardedProps: { ... }` | |
| 126 | +| Server wire field | `body.data.X` (mirror of forwardedProps) | `body.forwardedProps.X` | |
| 127 | +| Server `chat()` option | `conversationId` | `threadId` | |
| 128 | + |
| 129 | +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. |
| 130 | + |
| 131 | +## Bidirectional interop in practice |
| 132 | + |
| 133 | +With both halves of the protocol compliant, the boundaries between AI SDKs get a lot blurrier. |
| 134 | + |
| 135 | +**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. |
| 136 | + |
| 137 | +**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. |
| 138 | + |
| 139 | +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. |
| 140 | + |
| 141 | +## What's not in this release |
| 142 | + |
| 143 | +A few things were intentionally left out: |
| 144 | + |
| 145 | +- **Reasoning replay to LLM providers.** TanStack still drops `ThinkingPart` at the `UIMessage` → `ModelMessage` boundary. Providers like Anthropic that require thinking blocks to be replayed for extended thinking continuation are a separate track. |
| 146 | +- **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. |
| 147 | +- **PHP and Python server packages.** No `chatParamsFromRequest` parity yet. Those examples temporarily lag on the old shape until the matching helpers ship. |
| 148 | + |
| 149 | +## Try it |
| 150 | + |
| 151 | +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. |
| 152 | + |
| 153 | +- [Migration guide](https://tanstack.com/ai/migration/ag-ui-compliance) walks through the three deprecation bridges and the codemod |
| 154 | +- [Star the repo](https://github.com/TanStack/ai) if this saved you an adapter |
| 155 | + |
| 156 | +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. |
0 commit comments