-
-
Notifications
You must be signed in to change notification settings - Fork 197
feat: AG-UI client-to-server compliance #511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AlemTuzlak
wants to merge
19
commits into
main
Choose a base branch
from
feat/ag-ui-client-compliance
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
8b1c396
chore(ai): bump @ag-ui/core to ^0.0.52
AlemTuzlak c2f7405
feat(ai): accept parentRunId in TextOptions for AG-UI run correlation
AlemTuzlak 6fde945
feat(ai): dedup AG-UI fan-out + handle reasoning/activity/developer r…
AlemTuzlak dc8c172
feat(ai): add chatParamsFromRequestBody for AG-UI server endpoints
AlemTuzlak d9f7408
refactor(ai): use AGUIContext type for context field in chatParamsFro…
AlemTuzlak 83c168d
feat(ai): add mergeAgentTools helper (server wins on collision)
AlemTuzlak 2a58cdb
feat(ai): export chatParamsFromRequestBody and mergeAgentTools
AlemTuzlak 7f865d0
feat(ai): add uiMessagesToWire serializer for AG-UI request body
AlemTuzlak 622bb80
feat(ai-client): connection adapters POST AG-UI RunAgentInput payload
AlemTuzlak 6680b40
feat(ai-client): generate threadId per session, runId per send, adver…
AlemTuzlak a2febe2
feat(ai): accept UIMessage/ModelMessage in chat() messages; migrate t…
AlemTuzlak 16ae255
chore(examples): migrate chat endpoints to AG-UI RunAgentInput
AlemTuzlak 7631d00
test(e2e): migrate test endpoints to AG-UI RunAgentInput + add compli…
AlemTuzlak 4949038
test(e2e): add foreign AG-UI client and legacy wire shape rejection s…
AlemTuzlak 21a0f90
docs: add AG-UI client compliance migration guide
AlemTuzlak e8e865a
docs(ai): document client-to-server AG-UI compliance and wire format
AlemTuzlak 2c3cf39
changeset: AG-UI client-to-server compliance (breaking)
AlemTuzlak 154d97c
fix(ai): migrate vue example, validate parts shape, refresh skill ten…
AlemTuzlak 8b1cdb6
ci: apply automated fixes
autofix-ci[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| --- | ||
| '@tanstack/ai': minor | ||
| '@tanstack/ai-client': minor | ||
| '@tanstack/ai-react': minor | ||
| '@tanstack/ai-solid': minor | ||
| '@tanstack/ai-vue': minor | ||
| '@tanstack/ai-svelte': minor | ||
| '@tanstack/ai-react-ui': minor | ||
| --- | ||
|
|
||
| **Breaking:** AG-UI client-to-server compliance. | ||
|
|
||
| `@tanstack/ai-client` now POSTs an AG-UI `RunAgentInput` request body and `@tanstack/ai` server endpoints must use the new `chatParamsFromRequestBody` + `mergeAgentTools` helpers. Upgrade both packages together. | ||
|
|
||
| Highlights: | ||
|
|
||
| - **Wire format**: `{threadId, runId, state, messages, tools, context, forwardedProps}` (per AG-UI 0.0.52 `RunAgentInputSchema`) instead of `{messages, data}`. | ||
| - **New server helpers** exported from `@tanstack/ai`: `chatParamsFromRequestBody`, `mergeAgentTools`. | ||
| - **`chat()` accepts `threadId`, `runId`, `parentRunId`** as optional fields for AG-UI run correlation. | ||
| - **`ChatClient` accepts `threadId`** option; auto-generates and persists per session if omitted; fresh `runId` per send. | ||
| - **Client tools auto-advertised** to the server via `RunAgentInput.tools`. | ||
| - **Foreign AG-UI clients** can hit a TanStack server: `developer` collapses to `system`, `reasoning`/`activity` drop. | ||
|
|
||
| See `docs/migration/ag-ui-compliance.md` for full migration steps. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| --- | ||
| title: Migrating to AG-UI Client-to-Server Compliance | ||
| --- | ||
|
|
||
| # Migrating to AG-UI Client-to-Server Compliance | ||
|
|
||
| > **TL;DR:** Upgrade `@tanstack/ai` and `@tanstack/ai-client` together. The HTTP wire format changed to AG-UI `RunAgentInput`. Server endpoints must use `chatParamsFromRequestBody` and `mergeAgentTools`. Clients have nothing to do — `useChat` and the connection adapters handle the new format internally. | ||
|
|
||
| ## What changed | ||
|
|
||
| `@tanstack/ai-client` now POSTs an AG-UI 0.0.52 `RunAgentInput` request body. The previous shape (`{ messages, data, ...optionsBody }`) is no longer accepted by `@tanstack/ai`'s server helpers. | ||
|
|
||
| ### Old wire shape | ||
|
|
||
| ```json | ||
| { | ||
| "messages": [...], | ||
| "data": {...} | ||
| } | ||
| ``` | ||
|
|
||
| ### New wire shape | ||
|
|
||
| ```json | ||
| { | ||
| "threadId": "thread-...", | ||
| "runId": "run-...", | ||
| "state": {}, | ||
| "messages": [...], | ||
| "tools": [...], | ||
| "context": [], | ||
| "forwardedProps": {...} | ||
| } | ||
| ``` | ||
|
|
||
| The `messages` array carries TanStack `UIMessage` anchors with `parts` intact, plus AG-UI mirror fields (`content`, `toolCalls`) so strict AG-UI servers can parse it. Tool results and thinking parts are additionally emitted as separate `{role:'tool',...}` and `{role:'reasoning',...}` fan-out messages alongside the anchors. | ||
|
|
||
| ## Server endpoint upgrade | ||
|
|
||
| ### Before | ||
|
|
||
| ```ts | ||
| import { chat, toServerSentEventsResponse } from '@tanstack/ai' | ||
| import { openaiText } from '@tanstack/ai-openai/adapters' | ||
|
|
||
| export async function POST(req: Request) { | ||
| const { messages } = await req.json() | ||
| const stream = chat({ | ||
| adapter: openaiText('gpt-4o'), | ||
| messages, | ||
| tools: serverTools, | ||
| }) | ||
| return toServerSentEventsResponse(stream) | ||
| } | ||
| ``` | ||
|
|
||
| ### After | ||
|
|
||
| ```ts | ||
| import { | ||
| chat, | ||
| chatParamsFromRequestBody, | ||
| mergeAgentTools, | ||
| toServerSentEventsResponse, | ||
| } from '@tanstack/ai' | ||
| import { openaiText } from '@tanstack/ai-openai/adapters' | ||
|
|
||
| export async function POST(req: Request) { | ||
| let params | ||
| try { | ||
| params = await chatParamsFromRequestBody(await req.json()) | ||
| } catch (error) { | ||
| return new Response( | ||
| error instanceof Error ? error.message : 'Bad request', | ||
| { status: 400 }, | ||
| ) | ||
| } | ||
|
|
||
| const stream = chat({ | ||
| adapter: openaiText('gpt-4o'), | ||
| messages: params.messages, | ||
| tools: mergeAgentTools(serverTools, params.tools), | ||
| threadId: params.threadId, | ||
| runId: params.runId, | ||
|
|
||
| // Explicitly allowlist forwardedProps — see security note below | ||
| temperature: | ||
| typeof params.forwardedProps.temperature === 'number' | ||
| ? params.forwardedProps.temperature | ||
| : undefined, | ||
| maxTokens: | ||
| typeof params.forwardedProps.maxTokens === 'number' | ||
| ? params.forwardedProps.maxTokens | ||
| : undefined, | ||
| }) | ||
|
|
||
| return toServerSentEventsResponse(stream) | ||
| } | ||
| ``` | ||
|
|
||
| `chatParamsFromRequestBody` validates the body against `RunAgentInputSchema` from `@ag-ui/core` and throws an `AGUIError` on a malformed shape. Surface this as HTTP 400. | ||
|
|
||
| ## `forwardedProps` security | ||
|
|
||
| `forwardedProps` is arbitrary client-controlled JSON. **Do not** spread it directly into `chat({...})`: | ||
|
|
||
| ```ts | ||
| // 🚫 UNSAFE — a client could override `adapter`, `model`, `tools`, system prompts, anything | ||
| chat({ | ||
| adapter: openaiText('gpt-4o'), | ||
| ...params, | ||
| ...params.forwardedProps, | ||
| }) | ||
| ``` | ||
|
|
||
| Always destructure the specific fields you intend to forward: | ||
|
|
||
| ```ts | ||
| // ✅ SAFE — explicit allowlist | ||
| chat({ | ||
| adapter: openaiText('gpt-4o'), | ||
| messages: params.messages, | ||
| tools: mergeAgentTools(serverTools, params.tools), | ||
| threadId: params.threadId, | ||
| runId: params.runId, | ||
| temperature: typeof params.forwardedProps.temperature === 'number' | ||
| ? params.forwardedProps.temperature | ||
| : undefined, | ||
| }) | ||
| ``` | ||
|
|
||
| ## Client-side: nothing to do | ||
|
|
||
| `useChat` and the connection adapters (`fetchServerSentEvents`, `fetchHttpStream`) handle the new wire format internally. Existing `UIMessage` state is unchanged. `clientTools(...)` declarations are now automatically advertised to the server in the request payload. | ||
|
|
||
| If you instantiated a `ChatClient` directly and want to control the thread identifier, pass `threadId` via the constructor options: | ||
|
|
||
| ```ts | ||
| const client = new ChatClient({ | ||
| threadId: 'persistent-thread-from-storage', | ||
| connection: fetchServerSentEvents('/api/chat'), | ||
| tools: [/* clientTools */], | ||
| }) | ||
| ``` | ||
|
|
||
| If you don't pass `threadId`, one is generated automatically and persists for the lifetime of the `ChatClient` instance. A fresh `runId` is generated for every send. | ||
|
|
||
| ## Tool-merge semantics | ||
|
|
||
| - **Server tools win on name collision.** A tool registered server-side via `toolDefinition().server(...)` always executes server-side. | ||
| - **Client-only tools become no-execute stubs** in `chat()`. The runtime emits a `ClientToolRequest` event back to the client; the client's registered handler (via `clientTools(...)`) executes locally and posts the result. | ||
| - **Dual-handler (both have it):** server executes, then `chat-client.ts`'s `onToolCall` fires the client's handler as a UI side-effect when the streamed tool result event arrives. The server's result is authoritative for the conversation. | ||
|
|
||
| ## Talking to a foreign AG-UI server | ||
|
|
||
| A `@tanstack/ai-client` request hitting a foreign AG-UI server: | ||
|
|
||
| - ✅ Single-turn user messages work — content is mirrored to AG-UI's `content` field. | ||
| - ✅ Server-emitted events stream and render correctly. | ||
| - ✅ Multi-turn history that includes tool results from prior turns: the foreign server reads them via the AG-UI fan-out duplicates we send (separate `{role:'tool',...}` messages). | ||
| - ⚠️ Client-only tools are sent in the AG-UI `tools` field; whether the foreign server actually invokes them depends on its tool-calling logic. | ||
|
|
||
| ## Talking to a TanStack server from a foreign AG-UI client | ||
|
|
||
| Pure AG-UI `RunAgentInput` payloads (no TanStack `parts` field) work end-to-end: | ||
|
|
||
| - Tool messages pass through as `ModelMessage` entries with `role: 'tool'`. | ||
| - `reasoning` messages are dropped (no LLM-replay equivalent today). | ||
| - `activity` messages are dropped (no TanStack equivalent). | ||
| - `developer` messages are collapsed to `system` role. | ||
|
|
||
| ## `@ag-ui/core` bump | ||
|
|
||
| `@tanstack/ai` now depends on `@ag-ui/core@^0.0.52`. If your code imports types from `@tanstack/ai` that re-export AG-UI types, you may need minor type adjustments — see the changeset for specifics. | ||
|
|
||
| ## Out of scope (existing behavior preserved) | ||
|
|
||
| - **Reasoning replay to LLM providers.** TanStack still drops `ThinkingPart` at the `UIMessage`→`ModelMessage` boundary (pre-existing behavior). Providers like Anthropic that require thinking blocks to be replayed for extended thinking continuation remain a separate concern, tracked outside this migration. | ||
| - **AG-UI `state` and `context` fields.** Surfaced on `chatParamsFromRequestBody`'s return value but not yet wired into `chat()`. They're available for your endpoint to inspect/forward, but the runtime ignores them. | ||
| - **PHP and Python server packages.** No `chatParamsFromRequestBody` parity yet. Their examples temporarily lag on the old shape until the matching helpers ship. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Provider check accepts any non-empty string and can crash the handler on bad input.
The second condition
(params.forwardedProps.provider as Provider)only tests truthiness — every non-empty string passes. If a client sendsforwardedProps.provider: 'unknown',providerbecomes'unknown', thenadapterConfig[provider]()at line 223 throws because the entry isundefined. The error is caught and returned as a 500, but it should be a 400 (or just fall back to'openai').The Svelte example correctly uses
params.forwardedProps.provider in adapterConfigfor this.🛡️ Suggested fix
Or, alternatively, hoist the
adapterConfigkeys into aSetand validate against it before defaulting to'openai'.🤖 Prompt for AI Agents