feat: AG-UI client-to-server compliance#511
Conversation
…oles Add a pre-pass to convertMessagesToModelMessages that drops tool fan-out duplicates when a UIMessage anchor already covers the tool result, filters out AG-UI reasoning/activity messages with no ModelMessage equivalent, and collapses developer messages to the system role.
Implements a server-side helper that validates an incoming request body against RunAgentInputSchema from @ag-ui/core and returns a spread-friendly params object. Re-attaches stripped `parts` fields from raw messages to preserve UIMessage compatibility after AG-UI Zod strip-mode parsing.
…mRequestBody Replaces inline anonymous type with the Context type from @ag-ui/core to reuse the canonical shape instead of re-declaring it.
- Export uiMessagesToWire and WireMessage from @tanstack/ai
- Add RunAgentInputContext interface to connection-adapters.ts
- Extend ConnectConnectionAdapter.connect and SubscribeConnectionAdapter.send with optional runContext param
- fetchServerSentEvents and fetchHttpStream now POST AG-UI RunAgentInput shape (threadId, runId, state, messages, tools, context, forwardedProps) instead of bare {messages, data}
- stream() and rpcStream() accept _runContext for type consistency but pass through unchanged
- normalizeConnectionAdapter wrapper forwards runContext to connect()
- Add generateRunId() helper for fallback threadId/runId generation
- Make uiMessagesToWire defensive against ModelMessage-shaped inputs (no parts field)
- Update test assertions: body.data -> body.forwardedProps, body.model/provider -> body.forwardedProps.*
…tise client tools Adds threadId field to ChatClient (persisted across sends, configurable via ChatClientOptions.threadId) and builds a per-send runContext with a fresh runId and serialized clientTools advertised to the connection adapter.
…s-react-chat to RunAgentInput endpoint - Widen TextActivityOptions.messages to accept Array<UIMessage | ModelMessage | ConstrainedModelMessage> (function body already handled both at runtime) - Migrate examples/ts-react-chat to chatParamsFromRequestBody + mergeAgentTools
Migrate ts-solid-chat and ts-svelte-chat server endpoints to use chatParamsFromRequestBody and mergeAgentTools, accepting RunAgentInput wire format. Switches provider selection from data.provider to params.forwardedProps.provider. Passes threadId and runId through to chat(). ts-group-chat uses Cap'n Web RPC WebSockets (no HTTP body parsing) and is intentionally skipped.
…ance spec Switch api.chat.ts, api.tools-test.ts, and api.middleware-test.ts from destructuring body.data.* (legacy shape) to chatParamsFromRequestBody(), extracting provider/feature/scenario/middlewareMode from params.forwardedProps. Pass params.threadId and params.runId through to chat() calls. Add ag-ui-compliance.spec.ts to assert the wire body has all RunAgentInput fields (threadId, runId, state, messages, tools, context, forwardedProps), that threadId persists across sends within a session, runId is fresh per send, and that user/assistant messages carry their parts arrays.
📝 WalkthroughWalkthroughImplements AG-UI client-to-server protocol compliance: Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant AI_Client as `@tanstack/ai-client`
participant Adapter as Connection Adapter
participant Server as Server Endpoint
participant LLM as LLM Provider
Client->>AI_Client: send(messages)
AI_Client->>AI_Client: Build runContext<br/>(threadId, runId, clientTools,<br/>forwardedProps)
AI_Client->>Adapter: send(messages, data, signal,<br/>runContext)
Adapter->>Adapter: Convert UIMessages<br/>to wire format
Adapter->>Adapter: Merge forwardedProps<br/>+ requestBody fields
Adapter->>Server: POST /api/chat<br/>RunAgentInput{<br/> threadId, runId, state,<br/> messages, tools, context,<br/> forwardedProps<br/>}
Server->>Server: Parse via<br/>chatParamsFromRequestBody()
Server->>Server: Validate request<br/>or 400 Bad Request
Server->>Server: mergeAgentTools()<br/>(serverTools + params.tools)
Server->>LLM: chat(params.messages,<br/>mergedTools,<br/>threadId, runId)
LLM-->>Server: Stream responses
Server-->>Adapter: Stream back to client
Adapter-->>AI_Client: RUN_STARTED + chunks
AI_Client-->>Client: Streamed response
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~75 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Changeset Version Preview7 package(s) bumped directly, 26 bumped as dependents. 🟥 Major bumps
🟨 Minor bumps
🟩 Patch bumps
|
|
View your CI Pipeline Execution ↗ for commit 8b1cdb6
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-anthropic
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-devtools-core
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-openrouter
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/typescript/ai-client/src/connection-adapters.ts (1)
218-228:⚠️ Potential issue | 🟡 MinorSynthesized
RUN_FINISHEDshould reuserunContext.runIdso the terminal event correlates with the actual run.Today the wrapper fabricates a fresh
run-${Date.now()}runId, which won't match the runId the chat-client advertised for this send. Downstream consumers correlating byrunIdwill see a mismatched terminal event.♻️ Suggested fix
- if (!abortSignal?.aborted && !hasTerminalEvent) { - push({ - type: 'RUN_FINISHED', - runId: `run-${Date.now()}`, - model: 'connect-wrapper', - timestamp: Date.now(), - finishReason: 'stop', - } as unknown as StreamChunk) - } + if (!abortSignal?.aborted && !hasTerminalEvent) { + push({ + type: 'RUN_FINISHED', + runId: runContext?.runId ?? `run-${Date.now()}`, + model: 'connect-wrapper', + timestamp: Date.now(), + finishReason: 'stop', + } as unknown as StreamChunk) + }Same applies to the synthesized
RUN_ERRORpath — consider addingrunId: runContext?.runIdso error events carry the correlation as well.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai-client/src/connection-adapters.ts` around lines 218 - 228, The synthesized terminal events currently fabricate a new runId causing correlation breaks; update the code that pushes synthetic events (the push calls creating type: 'RUN_FINISHED' and the analogous 'RUN_ERROR' path) to set runId: runContext?.runId (or runContext.runId) instead of `run-${Date.now()}` so the synthetic RUN_FINISHED and RUN_ERROR events carry the original runContext.runId and properly correlate with the original run.
🧹 Nitpick comments (13)
packages/typescript/ai/src/activities/chat/messages.ts (1)
104-111: Minor: developer→system content type-narrowing.
(msg as { content: string }).contentassumes string content, but aModelMessage-typed input hascontent: string | null | Array<ContentPart>. AG-UI's developer role spec is text-only in practice, so this is unlikely to bite, but if a foreign client sends a developer message with array content it will be passed through as-is and the resulting "system"ModelMessagewill have a non-string content the downstream provider adapters may not handle gracefully.Consider preserving the original content type or coercing explicitly:
🛡️ Optional safer narrowing
- // AG-UI developer — collapse to system - if (role === 'developer') { - modelMessages.push({ - role: 'system' as ModelMessage['role'], - content: (msg as { content: string }).content, - } as ModelMessage) - continue - } + // AG-UI developer — collapse to system + if (role === 'developer') { + const content = (msg as { content: unknown }).content + modelMessages.push({ + role: 'system' as ModelMessage['role'], + content: typeof content === 'string' ? content : String(content ?? ''), + } as ModelMessage) + continue + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai/src/activities/chat/messages.ts` around lines 104 - 111, The current developer→system conversion casts msg to { content: string } and forces a string which can drop or mis-handle legitimate non-string ModelMessage.content (null or Array<ContentPart>); update the conversion in the developer branch (where role === 'developer' and you push into modelMessages) to preserve or safely coerce the original msg.content: use the actual msg.content as ModelMessage['content'] (no blind cast) or, if you must ensure a string, explicitly normalize by checking typeof msg.content and Array.isArray(msg.content> (transform ContentPart[] to a string via join or mapping) or pass through null/array unchanged so downstream provider adapters receive a valid ModelMessage['content'] type.packages/typescript/ai/src/utilities/chat-params.ts (2)
38-94: Optional: drop thePromisewrapper.
chatParamsFromRequestBodyperforms only synchronous work but returns a Promise viaPromise.reject/Promise.resolve. Either drop the wrapper (return the value/throw) or mark the functionasyncfor symmetry; thePromise<...>return type forces every caller intoawaitfor no real reason.♻️ Proposed simplification
-export function chatParamsFromRequestBody(body: unknown): Promise<{ +export function chatParamsFromRequestBody(body: unknown): { messages: Array<UIMessage | ModelMessage> threadId: string runId: string parentRunId?: string tools: Array<{ name: string; description: string; parameters: JSONSchema }> forwardedProps: Record<string, unknown> state: unknown context: Array<AGUIContext> -}> { +} { const parseResult = RunAgentInputSchema.safeParse(body) if (!parseResult.success) { - return Promise.reject( - new AGUIError( - `Request body is not a valid AG-UI RunAgentInput. ` + - `If you're upgrading from a previous `@tanstack/ai-client` release, ` + - `see docs/migration/ag-ui-compliance.md. ` + - `Validation errors: ${parseResult.error.message}`, - ), - ) + throw new AGUIError( + `Request body is not a valid AG-UI RunAgentInput. ` + + `If you're upgrading from a previous `@tanstack/ai-client` release, ` + + `see docs/migration/ag-ui-compliance.md. ` + + `Validation errors: ${parseResult.error.message}`, + ) } ... - return Promise.resolve({ + return { messages, ... - }) + } }If callers already
awaitit, leaving thePromise<>for backwards-compat may be intentional — feel free to ignore.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai/src/utilities/chat-params.ts` around lines 38 - 94, The function chatParamsFromRequestBody currently wraps synchronous logic in Promise.reject/Promise.resolve; change it to a synchronous function that returns the parsed object or throws on error: update the signature to return the plain result type (remove Promise<...>), replace Promise.reject(new AGUIError(...)) with throw new AGUIError(...), and replace Promise.resolve({...}) with a plain return {...}; keep the same validation logic and references to RunAgentInputSchema, AGUIError, isValidParts, and the parsed/messages handling so callers can be updated to remove unnecessary awaits (or keep them if backward compatibility is desired).
65-78: Consider the limitations of the suggested id-based refactor.While the concern about index-based alignment is valid in principle, the current implementation is safe because Zod's
.strip()only removes unknown properties and preserves array length and order. The suggested defensive refactor to use messageidhas a limitation:ModelMessagedoes not have anidfield, onlyUIMessagedoes (line 355 in types.ts). SincechatParamsFromRequestBodyreturnsArray<UIMessage | ModelMessage>, the refactored code would need to handle both cases—either by always including id in ModelMessage or by adding a fallback. If the goal is purely defensive against future schema changes that might filter/reorder messages, a more robust approach would ensure all message types carry an id or use a different keying strategy that works for both.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai/src/utilities/chat-params.ts` around lines 65 - 78, The review notes that switching from index-based alignment to id-based matching is tricky because ModelMessage lacks an id (UIMessage has id); update the merge logic in chatParamsFromRequestBody (the mapped block over parsed.messages using rawMessages, isValidParts, and parts) to be defensive: when raw message has an id and parsed message is a UIMessage with the same id, use raw.parts; otherwise fall back to the current index-based behavior so ModelMessage still works—alternatively add an optional id to ModelMessage across types.ts and handle both cases so the parts-matching logic works whether messages are UIMessage or ModelMessage.testing/e2e/tests/ag-ui-old-client-rejection.spec.ts (1)
17-19: Optional: tighten the error-message assertion.The current regex passes when any one of
AG-UI,RunAgentInput, ormigrationappears, so a 400 with an unrelated body mentioning "migration" would still pass. If the intent is to verify the migration-pointing error specifically, consider asserting both that a protocol identifier and a migration cue are present.♻️ Suggested tightening
- expect(body).toMatch(/AG-UI|RunAgentInput|migration/i) + expect(body).toMatch(/AG-UI|RunAgentInput/i) + expect(body).toMatch(/migration/i)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@testing/e2e/tests/ag-ui-old-client-rejection.spec.ts` around lines 17 - 19, The current assertion on the response body uses a single regex that matches any of "AG-UI", "RunAgentInput", or "migration", which can yield false positives; update the test to require both a protocol identifier and a migration cue by replacing the single expect(body).toMatch(/AG-UI|RunAgentInput|migration/i) check with two assertions (e.g., expect(body).toMatch(/AG-UI|RunAgentInput/) and expect(body).toMatch(/migration/i)) or a single regex with lookaheads to ensure both are present, targeting the existing body variable and the expect(body) call in this spec.packages/typescript/ai-client/src/types.ts (1)
310-313:ChatRequestBodyis stale and contradicts the wire format change.Per the PR,
@tanstack/ai-clientnow POSTs AG-UIRunAgentInput({threadId, runId, state, messages, tools, context, forwardedProps}), and the legacy shape ({messages, data}) is explicitly rejected with 400. This interface still documents the old shape, misleading any downstream consumer importing it.Remove it or replace it with a type alias to
RunAgentInput(if available from@ag-ui/core) to align the public API with the actual wire format.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai-client/src/types.ts` around lines 310 - 313, Update the stale ChatRequestBody interface to match the new wire format: remove the old ChatRequestBody definition and instead export a type alias to RunAgentInput from `@ag-ui/core` (e.g. export type ChatRequestBody = RunAgentInput) or delete the symbol entirely if RunAgentInput is unavailable; ensure you import RunAgentInput from '@ag-ui/core' and update any exported symbols so the public types reflect the actual POST payload rather than the legacy {messages, data} shape.testing/e2e/tests/ag-ui-foreign-client.spec.ts (1)
59-63: Strengthen the developer-role assertion to verify the run actually finishes.
response.ok()alone passes even if the body contains aRUN_ERROR(or no run events at all), so this test would not catch a regression where the developer→system collapse is correct but the run subsequently fails server-side. Mirror Test 1'sexpect(text).toContain('RUN_FINISHED')here.♻️ Proposed change
const response = await request.post('/api/chat', { data: body, headers: { 'Content-Type': 'application/json' }, }) expect(response.ok()).toBe(true) + const text = await response.text() + expect(text).toContain('RUN_FINISHED') })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@testing/e2e/tests/ag-ui-foreign-client.spec.ts` around lines 59 - 63, The test currently only checks response.ok() which can miss server-side run failures; after obtaining the response from request.post('/api/chat') (the response variable), read the response text (e.g., response.text()) and add an assertion like expect(text).toContain('RUN_FINISHED') to verify the developer→system run actually completed successfully, mirroring the assertion used in Test 1.packages/typescript/ai-client/src/chat-client.ts (1)
615-615: ReusegenerateUniqueId('run')for the runId.
threadId(L85) and other IDs usethis.generateUniqueId(prefix); this one inlines an only-slightly-different format (slice(2, 8)vs.substring(7)). Reusing the helper keeps ID generation consistent and centralizes any future changes (e.g., switching tocrypto.randomUUID).♻️ Proposed change
- runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + runId: this.generateUniqueId('run'),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai-client/src/chat-client.ts` at line 615, Replace the inline runId creation with the existing helper to ensure consistent ID format: change the runId assignment that currently uses `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` to call `this.generateUniqueId('run')` so it matches other IDs like `threadId` which use `this.generateUniqueId(prefix)`; update the runId assignment in the same block where `runId` is set to use `this.generateUniqueId('run')` and ensure no other callers rely on the old inline format.packages/typescript/ai/src/index.ts (1)
177-179: Comment understates the public surface area.Once
uiMessagesToWireandWireMessageare exported from the package barrel, they become public API for any consumer — calling them "used internally by@tanstack/ai-client" risks giving the impression they're private and free to break in patch releases. Consider either dropping the "internally" wording or moving these to a sub-path export if you want to keep them out of the stability contract.♻️ Proposed comment tweak
-// AG-UI wire serialization (used internally by `@tanstack/ai-client`) +// AG-UI wire serialization (used by `@tanstack/ai-client`; also part of the public API) export { uiMessagesToWire } from './utilities/ag-ui-wire' export type { WireMessage } from './utilities/ag-ui-wire'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai/src/index.ts` around lines 177 - 179, The comment currently labels uiMessagesToWire and WireMessage as "used internally by `@tanstack/ai-client`", understating that once exported from the package barrel they are public API; update the comment or the export to reflect intended stability: either remove or reword "used internally by `@tanstack/ai-client`" to indicate these are public exports (e.g., "public serialization utilities"), or instead move the exports uiMessagesToWire and WireMessage out of the barrel into a dedicated internal sub-path export so they are not part of the package root public surface; adjust the lines exporting uiMessagesToWire and WireMessage in index.ts accordingly.examples/ts-vue-chat/vite.config.ts (2)
215-219: Provider value isn't validated against the known Provider set.Unlike the Svelte handler (which checks
params.forwardedProps.provider in adapterConfig), this cast accepts any string and relies on thedefault:branch of theswitchat lines 227-245 for fallback. That works today, but a future refactor that drops thedefault:case would silently pass an invalid string through. Consider validating against a known set up-front for parity with the other examples.♻️ Suggested validation
- const fp = params.forwardedProps as Record<string, unknown> - const provider: Provider = - typeof fp.provider === 'string' - ? (fp.provider as Provider) - : 'openai' + const fp = params.forwardedProps as Record<string, unknown> + const knownProviders = ['openai', 'anthropic', 'gemini', 'ollama'] as const + const provider: Provider = + typeof fp.provider === 'string' && + (knownProviders as readonly string[]).includes(fp.provider) + ? (fp.provider as Provider) + : 'openai'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/ts-vue-chat/vite.config.ts` around lines 215 - 219, The provider string is being cast without validation; update the assignment that creates provider from fp (params.forwardedProps) so it validates the value against the known provider set (e.g., keys of adapterConfig or a local knownProviders collection) before casting—if fp.provider is a string and exists in that set use it, otherwise fall back to 'openai'; modify the logic around the provider variable creation (where fp and Provider are referenced) to perform this membership check to match the Svelte handler approach.
253-255: Inconsistent server tool registry vs. the Svelte/React examples.This handler only registers
getGuitarsandaddToCartToolServeras server tools, while the Svelte example registersgetGuitars,recommendGuitarToolDef,addToCartToolServer,addToWishListToolDef, andgetPersonalGuitarPreferenceToolDef, and the React example registers an even larger superset. Tool definitions without.server(...)are still useful inserverToolsbecausemergeAgentToolswill treat them as no-execute entries — the LLM still sees them, but the runtime emitsClientToolRequestevents. Unless the Vue client side is advertising all of these viaparams.tools, the model in this example will be missingrecommendGuitar,addToWishList, andgetPersonalGuitarPreference.Worth either making the example consistent with the others or adding a comment explaining why Vue diverges.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/ts-vue-chat/vite.config.ts` around lines 253 - 255, The serverTools registry currently only includes getGuitars and addToCartToolServer which omits recommendGuitarToolDef, addToWishListToolDef, and getPersonalGuitarPreferenceToolDef used in other examples; update the serverTools Object.fromEntries to include those tool defs (so mergeAgentTools treats them as no-execute entries) or add a clarifying comment explaining why Vue intentionally omits them and that the client must advertise missing tools via params.tools; reference the symbols getGuitars, addToCartToolServer, recommendGuitarToolDef, addToWishListToolDef, getPersonalGuitarPreferenceToolDef, and mergeAgentTools when making the change.examples/ts-svelte-chat/src/routes/api/chat/+server.ts (1)
109-117: Optional: avoid surfacing raw validator messages to clients in production handlers.Returning
error.messagedirectly is fine for an example, but it surfaces Zod's internal validation text (paths, expected types) to any caller. For production-bound copies of this handler, consider returning a stable, sanitized 400 body and logging the detailed error server-side.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/ts-svelte-chat/src/routes/api/chat/`+server.ts around lines 109 - 117, The handler currently returns raw validator messages from the catch block when calling chatParamsFromRequestBody; instead, sanitize responses for production by returning a fixed, non-revealing 400 body (e.g., "Invalid request payload") and log the detailed error server-side (including the caught error and context) so debugging info is preserved without exposing Zod internals to clients; update the catch around chatParamsFromRequestBody/params to perform logging and return the sanitized message.packages/typescript/ai/tests/chat-params.test.ts (1)
41-54: Optional: also assert the rejection error message points at the missing field for better debug ergonomics.These tests confirm that
chatParamsFromRequestBodyrejects whenthreadId/runId/messagesare absent, but they only assert that some error is thrown. IfRunAgentInputSchemaever changes to make one of these optional with a default, these tests would still pass. A regex assertion (e.g.,rejects.toThrow(/threadId/)) would tighten the contract.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai/tests/chat-params.test.ts` around lines 41 - 54, Update the three tests that currently call chatParamsFromRequestBody(rest) and only assert a rejection to also assert the rejection message references the missing field; for each test replace the generic rejects.toThrow() assertion with a regex check like rejects.toThrow(/threadId/), rejects.toThrow(/runId/), and rejects.toThrow(/messages/) respectively so the tests fail if RunAgentInputSchema changes to make a field optional or provide a default.packages/typescript/ai-client/src/connection-adapters.ts (1)
501-506: Stream and RPC adapters intentionally excluderunContextfrom the wire shape—this is by design.Both
stream()(line 501) andrpcStream()(line 532) drop therunContextparameter because their factory functions only acceptmessagesanddata. Unlike thefetch()andwebSocket()adapters, which embedthreadIdandrunIdin their wire payloads, direct server functions and RPC endpoints don't use the AG-UI wire format.If an RPC backend or direct server function needs session correlation, consider extending its factory signature to optionally accept
runContextand extractthreadId/runIdfrom there.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai-client/src/connection-adapters.ts` around lines 501 - 506, The adapters currently drop runContext (see async *connect(messages, data, _abortSignal, _runContext) and the rpcStream() adapter) which is intentional, but unclear in the code; either add a short clarifying comment above connect and rpcStream explaining that streamFactory and RPC factory functions accept only (messages, data) and therefore runContext/threadId/runId are not sent on the wire, or if session correlation is required, update the streamFactory and the RPC factory signatures to optionally accept runContext and then forward _runContext from async *connect (and the rpcStream connector) so threadId/runId can be extracted and embedded as needed. Ensure you reference the connect method, streamFactory, rpcStream, and runContext when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/ts-react-chat/src/routes/api.tanchat.ts`:
- Around line 154-158: The provider selection currently treats any non-empty
string as valid, so set provider must be validated against the known adapters
before using it; change the logic that computes provider (the expression using
params.forwardedProps.provider and Provider) to check membership in
adapterConfig (or a Set of adapterConfig keys) and only accept it if it exists,
otherwise default to 'openai'. Specifically, validate
params.forwardedProps.provider against adapterConfig keys prior to calling
adapterConfig[provider]() to prevent undefined lookups and return a 400 or fall
back to 'openai' when the provided key is not present.
In `@packages/typescript/ai-client/src/chat-client.ts`:
- Around line 613-624: The client tool parameter serialization is using raw
AnyClientTool.inputSchema (which may be a Standard Schema) instead of JSON
Schema; update the mapping that builds runContext.clientTools (the
Array.from(this.clientToolsRef.current.values()).map callback that sets
name/description/parameters) to call convertSchemaToJsonSchema(inputSchema) when
inputSchema exists, falling back to { type: 'object' } if conversion returns
falsy, so RunAgentInput.tools[].parameters are valid JSON Schema; reference
convertSchemaToJsonSchema and the runContext/clientTools mapping when making
this change.
In `@packages/typescript/ai-client/src/connection-adapters.ts`:
- Around line 313-329: Change the uiMessagesToWire signature to accept
Array<UIMessage | ModelMessage> (instead of only Array<UIMessage>) so callers no
longer need unsafe casts; update the function declaration for uiMessagesToWire
and any related type imports/exports, and adjust internal typing if necessary to
preserve existing defensive access (e.g., using msg.parts ?? msg.content) so
both ConnectConnectionAdapter.connect and SubscribeConnectionAdapter.send can
pass their messages: Array<UIMessage> | Array<ModelMessage> directly without
casting.
In `@packages/typescript/ai/docs/chat-architecture.md`:
- Line 86: The link text in packages/typescript/ai/docs/chat-architecture.md
references "docs/migration/ag-ui-compliance.md" which is a broken relative path
from this nested location; update that link to the correct repository-relative
path (either replace "docs/migration/ag-ui-compliance.md" with
"../../../../docs/migration/ag-ui-compliance.md" or use an absolute repo-root
path like "/docs/migration/ag-ui-compliance.md") so the link resolves correctly.
---
Outside diff comments:
In `@packages/typescript/ai-client/src/connection-adapters.ts`:
- Around line 218-228: The synthesized terminal events currently fabricate a new
runId causing correlation breaks; update the code that pushes synthetic events
(the push calls creating type: 'RUN_FINISHED' and the analogous 'RUN_ERROR'
path) to set runId: runContext?.runId (or runContext.runId) instead of
`run-${Date.now()}` so the synthetic RUN_FINISHED and RUN_ERROR events carry the
original runContext.runId and properly correlate with the original run.
---
Nitpick comments:
In `@examples/ts-svelte-chat/src/routes/api/chat/`+server.ts:
- Around line 109-117: The handler currently returns raw validator messages from
the catch block when calling chatParamsFromRequestBody; instead, sanitize
responses for production by returning a fixed, non-revealing 400 body (e.g.,
"Invalid request payload") and log the detailed error server-side (including the
caught error and context) so debugging info is preserved without exposing Zod
internals to clients; update the catch around chatParamsFromRequestBody/params
to perform logging and return the sanitized message.
In `@examples/ts-vue-chat/vite.config.ts`:
- Around line 215-219: The provider string is being cast without validation;
update the assignment that creates provider from fp (params.forwardedProps) so
it validates the value against the known provider set (e.g., keys of
adapterConfig or a local knownProviders collection) before casting—if
fp.provider is a string and exists in that set use it, otherwise fall back to
'openai'; modify the logic around the provider variable creation (where fp and
Provider are referenced) to perform this membership check to match the Svelte
handler approach.
- Around line 253-255: The serverTools registry currently only includes
getGuitars and addToCartToolServer which omits recommendGuitarToolDef,
addToWishListToolDef, and getPersonalGuitarPreferenceToolDef used in other
examples; update the serverTools Object.fromEntries to include those tool defs
(so mergeAgentTools treats them as no-execute entries) or add a clarifying
comment explaining why Vue intentionally omits them and that the client must
advertise missing tools via params.tools; reference the symbols getGuitars,
addToCartToolServer, recommendGuitarToolDef, addToWishListToolDef,
getPersonalGuitarPreferenceToolDef, and mergeAgentTools when making the change.
In `@packages/typescript/ai-client/src/chat-client.ts`:
- Line 615: Replace the inline runId creation with the existing helper to ensure
consistent ID format: change the runId assignment that currently uses
`run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` to call
`this.generateUniqueId('run')` so it matches other IDs like `threadId` which use
`this.generateUniqueId(prefix)`; update the runId assignment in the same block
where `runId` is set to use `this.generateUniqueId('run')` and ensure no other
callers rely on the old inline format.
In `@packages/typescript/ai-client/src/connection-adapters.ts`:
- Around line 501-506: The adapters currently drop runContext (see async
*connect(messages, data, _abortSignal, _runContext) and the rpcStream() adapter)
which is intentional, but unclear in the code; either add a short clarifying
comment above connect and rpcStream explaining that streamFactory and RPC
factory functions accept only (messages, data) and therefore
runContext/threadId/runId are not sent on the wire, or if session correlation is
required, update the streamFactory and the RPC factory signatures to optionally
accept runContext and then forward _runContext from async *connect (and the
rpcStream connector) so threadId/runId can be extracted and embedded as needed.
Ensure you reference the connect method, streamFactory, rpcStream, and
runContext when making the change.
In `@packages/typescript/ai-client/src/types.ts`:
- Around line 310-313: Update the stale ChatRequestBody interface to match the
new wire format: remove the old ChatRequestBody definition and instead export a
type alias to RunAgentInput from `@ag-ui/core` (e.g. export type ChatRequestBody =
RunAgentInput) or delete the symbol entirely if RunAgentInput is unavailable;
ensure you import RunAgentInput from '@ag-ui/core' and update any exported
symbols so the public types reflect the actual POST payload rather than the
legacy {messages, data} shape.
In `@packages/typescript/ai/src/activities/chat/messages.ts`:
- Around line 104-111: The current developer→system conversion casts msg to {
content: string } and forces a string which can drop or mis-handle legitimate
non-string ModelMessage.content (null or Array<ContentPart>); update the
conversion in the developer branch (where role === 'developer' and you push into
modelMessages) to preserve or safely coerce the original msg.content: use the
actual msg.content as ModelMessage['content'] (no blind cast) or, if you must
ensure a string, explicitly normalize by checking typeof msg.content and
Array.isArray(msg.content> (transform ContentPart[] to a string via join or
mapping) or pass through null/array unchanged so downstream provider adapters
receive a valid ModelMessage['content'] type.
In `@packages/typescript/ai/src/index.ts`:
- Around line 177-179: The comment currently labels uiMessagesToWire and
WireMessage as "used internally by `@tanstack/ai-client`", understating that once
exported from the package barrel they are public API; update the comment or the
export to reflect intended stability: either remove or reword "used internally
by `@tanstack/ai-client`" to indicate these are public exports (e.g., "public
serialization utilities"), or instead move the exports uiMessagesToWire and
WireMessage out of the barrel into a dedicated internal sub-path export so they
are not part of the package root public surface; adjust the lines exporting
uiMessagesToWire and WireMessage in index.ts accordingly.
In `@packages/typescript/ai/src/utilities/chat-params.ts`:
- Around line 38-94: The function chatParamsFromRequestBody currently wraps
synchronous logic in Promise.reject/Promise.resolve; change it to a synchronous
function that returns the parsed object or throws on error: update the signature
to return the plain result type (remove Promise<...>), replace
Promise.reject(new AGUIError(...)) with throw new AGUIError(...), and replace
Promise.resolve({...}) with a plain return {...}; keep the same validation logic
and references to RunAgentInputSchema, AGUIError, isValidParts, and the
parsed/messages handling so callers can be updated to remove unnecessary awaits
(or keep them if backward compatibility is desired).
- Around line 65-78: The review notes that switching from index-based alignment
to id-based matching is tricky because ModelMessage lacks an id (UIMessage has
id); update the merge logic in chatParamsFromRequestBody (the mapped block over
parsed.messages using rawMessages, isValidParts, and parts) to be defensive:
when raw message has an id and parsed message is a UIMessage with the same id,
use raw.parts; otherwise fall back to the current index-based behavior so
ModelMessage still works—alternatively add an optional id to ModelMessage across
types.ts and handle both cases so the parts-matching logic works whether
messages are UIMessage or ModelMessage.
In `@packages/typescript/ai/tests/chat-params.test.ts`:
- Around line 41-54: Update the three tests that currently call
chatParamsFromRequestBody(rest) and only assert a rejection to also assert the
rejection message references the missing field; for each test replace the
generic rejects.toThrow() assertion with a regex check like
rejects.toThrow(/threadId/), rejects.toThrow(/runId/), and
rejects.toThrow(/messages/) respectively so the tests fail if
RunAgentInputSchema changes to make a field optional or provide a default.
In `@testing/e2e/tests/ag-ui-foreign-client.spec.ts`:
- Around line 59-63: The test currently only checks response.ok() which can miss
server-side run failures; after obtaining the response from
request.post('/api/chat') (the response variable), read the response text (e.g.,
response.text()) and add an assertion like
expect(text).toContain('RUN_FINISHED') to verify the developer→system run
actually completed successfully, mirroring the assertion used in Test 1.
In `@testing/e2e/tests/ag-ui-old-client-rejection.spec.ts`:
- Around line 17-19: The current assertion on the response body uses a single
regex that matches any of "AG-UI", "RunAgentInput", or "migration", which can
yield false positives; update the test to require both a protocol identifier and
a migration cue by replacing the single
expect(body).toMatch(/AG-UI|RunAgentInput|migration/i) check with two assertions
(e.g., expect(body).toMatch(/AG-UI|RunAgentInput/) and
expect(body).toMatch(/migration/i)) or a single regex with lookaheads to ensure
both are present, targeting the existing body variable and the expect(body) call
in this spec.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9681e92e-f463-4cf2-b11b-43fe638ec324
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (36)
.changeset/ag-ui-client-compliance.mddocs/config.jsondocs/migration/ag-ui-compliance.mdexamples/ts-react-chat/src/routes/api.tanchat.tsexamples/ts-solid-chat/src/routes/api.chat.tsexamples/ts-svelte-chat/src/routes/api/chat/+server.tsexamples/ts-vue-chat/vite.config.tspackages/typescript/ai-anthropic/src/adapters/text.tspackages/typescript/ai-client/src/chat-client.tspackages/typescript/ai-client/src/connection-adapters.tspackages/typescript/ai-client/src/types.tspackages/typescript/ai-client/tests/connection-adapters.test.tspackages/typescript/ai-gemini/src/adapters/text.tspackages/typescript/ai-grok/src/adapters/text.tspackages/typescript/ai-groq/src/adapters/text.tspackages/typescript/ai-ollama/src/adapters/text.tspackages/typescript/ai-openai/src/adapters/text.tspackages/typescript/ai-openrouter/src/adapters/text.tspackages/typescript/ai/docs/chat-architecture.mdpackages/typescript/ai/package.jsonpackages/typescript/ai/skills/ai-core/ag-ui-protocol/SKILL.mdpackages/typescript/ai/src/activities/chat/index.tspackages/typescript/ai/src/activities/chat/messages.tspackages/typescript/ai/src/index.tspackages/typescript/ai/src/types.tspackages/typescript/ai/src/utilities/ag-ui-wire.tspackages/typescript/ai/src/utilities/chat-params.tspackages/typescript/ai/tests/ag-ui-wire.test.tspackages/typescript/ai/tests/chat-params.test.tspackages/typescript/ai/tests/messages.test.tstesting/e2e/src/routes/api.chat.tstesting/e2e/src/routes/api.middleware-test.tstesting/e2e/src/routes/api.tools-test.tstesting/e2e/tests/ag-ui-compliance.spec.tstesting/e2e/tests/ag-ui-foreign-client.spec.tstesting/e2e/tests/ag-ui-old-client-rejection.spec.ts
| const provider: Provider = | ||
| typeof params.forwardedProps.provider === 'string' && | ||
| (params.forwardedProps.provider as Provider) | ||
| ? (params.forwardedProps.provider as Provider) | ||
| : 'openai' |
There was a problem hiding this comment.
Provider check accepts any non-empty string and can crash the handler on bad input.
typeof params.forwardedProps.provider === 'string' &&
(params.forwardedProps.provider as Provider)
? (params.forwardedProps.provider as Provider)
: 'openai'
The second condition (params.forwardedProps.provider as Provider) only tests truthiness — every non-empty string passes. If a client sends forwardedProps.provider: 'unknown', provider becomes 'unknown', then adapterConfig[provider]() at line 223 throws because the entry is undefined. 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 adapterConfig for this.
🛡️ Suggested fix
- // Extract provider and model from forwardedProps (sent by the client)
- const provider: Provider =
- typeof params.forwardedProps.provider === 'string' &&
- (params.forwardedProps.provider as Provider)
- ? (params.forwardedProps.provider as Provider)
- : 'openai'
+ // Extract provider and model from forwardedProps (sent by the client)
+ const candidateProvider =
+ typeof params.forwardedProps.provider === 'string'
+ ? params.forwardedProps.provider
+ : ''
+ // Note: adapterConfig is defined below; defer until after it's in scope
+ // or move the validation after the adapterConfig declaration.Or, alternatively, hoist the adapterConfig keys into a Set and validate against it before defaulting to 'openai'.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/ts-react-chat/src/routes/api.tanchat.ts` around lines 154 - 158, The
provider selection currently treats any non-empty string as valid, so set
provider must be validated against the known adapters before using it; change
the logic that computes provider (the expression using
params.forwardedProps.provider and Provider) to check membership in
adapterConfig (or a Set of adapterConfig keys) and only accept it if it exists,
otherwise default to 'openai'. Specifically, validate
params.forwardedProps.provider against adapterConfig keys prior to calling
adapterConfig[provider]() to prevent undefined lookups and return a 400 or fall
back to 'openai' when the provided key is not present.
| const runContext = { | ||
| threadId: this.threadId, | ||
| runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, | ||
| clientTools: Array.from(this.clientToolsRef.current.values()).map( | ||
| (t) => ({ | ||
| name: t.name, | ||
| description: t.description, | ||
| parameters: t.inputSchema || { type: 'object' }, | ||
| }), | ||
| ), | ||
| forwardedProps: { ...mergedBody }, | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm AnyClientTool.inputSchema typing and that convertSchemaToJsonSchema is exported from `@tanstack/ai` for client use.
rg -nP --type=ts -C2 '\bAnyClientTool\b'
rg -nP --type=ts -C2 'inputSchema\??:' packages/typescript/ai-client/src
rg -nP --type=ts -C2 '\bconvertSchemaToJsonSchema\b'Repository: TanStack/ai
Length of output: 50369
🏁 Script executed:
#!/bin/bash
# Verify chat-client.ts at lines 613-624 and check imports
sed -n '605,630p' packages/typescript/ai-client/src/chat-client.ts | cat -n
echo "---"
echo "Checking imports in chat-client.ts:"
head -20 packages/typescript/ai-client/src/chat-client.ts | cat -n
echo "---"
echo "Searching for convertSchemaToJsonSchema in chat-client.ts:"
rg -n 'convertSchemaToJsonSchema' packages/typescript/ai-client/src/chat-client.tsRepository: TanStack/ai
Length of output: 1995
🏁 Script executed:
#!/bin/bash
# Check AnyClientTool definition to confirm inputSchema typing
sed -n '45,75p' packages/typescript/ai/src/activities/chat/tools/tool-definition.ts | cat -nRepository: TanStack/ai
Length of output: 1185
🏁 Script executed:
#!/bin/bash
# Check Tool base type definition for inputSchema
rg -n -A10 'interface Tool<' packages/typescript/ai/src/activities/chat/tools/tool-definition.ts | head -30Repository: TanStack/ai
Length of output: 37
🏁 Script executed:
#!/bin/bash
# Find Tool interface definition
rg -n 'interface Tool' packages/typescript/ai/src/activities/chat/tools/tool-definition.ts
# Also check what SchemaInput is
rg -n 'type SchemaInput' packages/typescript/ai/src/activities/chat/tools/tool-definition.ts
# And look at imports in tool-definition.ts
head -50 packages/typescript/ai/src/activities/chat/tools/tool-definition.ts | cat -nRepository: TanStack/ai
Length of output: 1801
🏁 Script executed:
#!/bin/bash
# Verify SchemaInput type definition
rg -n -A3 'type SchemaInput' packages/typescript/ai/src/types.ts | head -20
# Confirm convertSchemaToJsonSchema export in index.ts
grep -n 'convertSchemaToJsonSchema' packages/typescript/ai/src/index.tsRepository: TanStack/ai
Length of output: 278
Add schema conversion before serializing client tool parameters to wire
AnyClientTool.inputSchema follows the toolDefinition() pattern and can be a Standard Schema instance (Zod, etc.), not raw JSON Schema. The wire format expects RunAgentInput.tools[].parameters to be JSON Schema, and foreign AG-UI servers consuming these tool definitions will receive unusable parameter shapes if Standard Schemas are serialized directly.
The package already exports convertSchemaToJsonSchema and uses it for the same purpose in realtime-client.ts (lines 517–521). Apply the same conversion in chat-client.ts (line 616):
♻️ Proposed change
+import { convertSchemaToJsonSchema } from '@tanstack/ai'
+
const runContext = {
threadId: this.threadId,
runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
clientTools: Array.from(this.clientToolsRef.current.values()).map(
(t) => ({
name: t.name,
description: t.description,
parameters:
+ (t.inputSchema && convertSchemaToJsonSchema(t.inputSchema)) || {
+ type: 'object',
+ },
- parameters: t.inputSchema || { type: 'object' },
}),
),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const runContext = { | |
| threadId: this.threadId, | |
| runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, | |
| clientTools: Array.from(this.clientToolsRef.current.values()).map( | |
| (t) => ({ | |
| name: t.name, | |
| description: t.description, | |
| parameters: t.inputSchema || { type: 'object' }, | |
| }), | |
| ), | |
| forwardedProps: { ...mergedBody }, | |
| } | |
| import { convertSchemaToJsonSchema } from '@tanstack/ai' | |
| const runContext = { | |
| threadId: this.threadId, | |
| runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, | |
| clientTools: Array.from(this.clientToolsRef.current.values()).map( | |
| (t) => ({ | |
| name: t.name, | |
| description: t.description, | |
| parameters: (t.inputSchema && convertSchemaToJsonSchema(t.inputSchema)) || { type: 'object' }, | |
| }), | |
| ), | |
| forwardedProps: { ...mergedBody }, | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai-client/src/chat-client.ts` around lines 613 - 624, The
client tool parameter serialization is using raw AnyClientTool.inputSchema
(which may be a Standard Schema) instead of JSON Schema; update the mapping that
builds runContext.clientTools (the
Array.from(this.clientToolsRef.current.values()).map callback that sets
name/description/parameters) to call convertSchemaToJsonSchema(inputSchema) when
inputSchema exists, falling back to { type: 'object' } if conversion returns
falsy, so RunAgentInput.tools[].parameters are valid JSON Schema; reference
convertSchemaToJsonSchema and the runContext/clientTools mapping when making
this change.
| // Build AG-UI RunAgentInput payload | ||
| const wireMessages = uiMessagesToWire(messages as Array<UIMessage>) | ||
| const requestBody = { | ||
| messages, | ||
| data, | ||
| ...resolvedOptions.body, | ||
| threadId: runContext?.threadId ?? generateRunId('thread'), | ||
| runId: runContext?.runId ?? generateRunId('run'), | ||
| ...(runContext?.parentRunId !== undefined && { | ||
| parentRunId: runContext.parentRunId, | ||
| }), | ||
| state: {}, | ||
| messages: wireMessages, | ||
| tools: runContext?.clientTools ?? [], | ||
| context: [], | ||
| forwardedProps: { | ||
| ...(runContext?.forwardedProps ?? {}), | ||
| ...data, | ||
| ...resolvedOptions.body, | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm uiMessagesToWire's expected input shape and whether it tolerates ModelMessage.
fd -t f 'wire' packages/typescript/ai/src
rg -nP -C5 'export\s+function\s+uiMessagesToWire' packages/typescript/ai/src
echo '---'
# Find every connect()/send() caller in chat-client to confirm what it passes.
rg -nP -C3 '\.(connect|send)\s*\(' packages/typescript/ai-client/srcRepository: TanStack/ai
Length of output: 4889
🏁 Script executed:
# Read the specific lines in connection-adapters.ts to understand the context
sed -n '310,335p' packages/typescript/ai-client/src/connection-adapters.tsRepository: TanStack/ai
Length of output: 941
🏁 Script executed:
# Check the send() method signature to see if it declares union type
rg -nP -B15 'async send\(' packages/typescript/ai-client/src/connection-adapters.ts | head -40Repository: TanStack/ai
Length of output: 681
🏁 Script executed:
# Check line 429 for the second cast
sed -n '425,435p' packages/typescript/ai-client/src/connection-adapters.tsRepository: TanStack/ai
Length of output: 498
🏁 Script executed:
# Look for UIMessage and ModelMessage type definitions
rg -nP 'type\s+(UIMessage|ModelMessage)' packages/typescript/ai/srcRepository: TanStack/ai
Length of output: 37
🏁 Script executed:
# Get full send() method signature with types
sed -n '195,215p' packages/typescript/ai-client/src/connection-adapters.tsRepository: TanStack/ai
Length of output: 660
🏁 Script executed:
# Search for type definitions with different patterns
rg -nP '(export\s+(type|interface)\s+(UIMessage|ModelMessage))|(UIMessage|ModelMessage)\s*=' packages/typescript/ai/src | head -20Repository: TanStack/ai
Length of output: 635
🏁 Script executed:
# Find the fetchHttpStream function and line 429 area
rg -nP 'fetchHttpStream' packages/typescript/ai-client/src/connection-adapters.tsRepository: TanStack/ai
Length of output: 416
🏁 Script executed:
# Check what's around the second occurrence (if it exists)
sed -n '420,440p' packages/typescript/ai-client/src/connection-adapters.tsRepository: TanStack/ai
Length of output: 838
🏁 Script executed:
# Get full fetchHttpStream function signature
sed -n '410,425p' packages/typescript/ai-client/src/connection-adapters.tsRepository: TanStack/ai
Length of output: 707
🏁 Script executed:
# Check what interface/type contains the send() method
rg -nP -B30 'async send\(' packages/typescript/ai-client/src/connection-adapters.ts | grep -E '(interface|type|function|return|:.*\{)' | head -20Repository: TanStack/ai
Length of output: 246
🏁 Script executed:
# Read the UIMessage and ModelMessage interface definitions
sed -n '289,360p' packages/typescript/ai/src/types.tsRepository: TanStack/ai
Length of output: 1653
🏁 Script executed:
# Find ConnectConnectionAdapter interface definition
rg -nP 'interface\s+ConnectConnectionAdapter|type\s+ConnectConnectionAdapter' packages/typescript/ai-client/srcRepository: TanStack/ai
Length of output: 238
🏁 Script executed:
# Also search in types or definitions files
rg -nP 'ConnectConnectionAdapter' packages/typescript/ai-client/src --type tsRepository: TanStack/ai
Length of output: 1984
🏁 Script executed:
# Read the ConnectConnectionAdapter interface definition
sed -n '89,125p' packages/typescript/ai-client/src/connection-adapters.tsRepository: TanStack/ai
Length of output: 984
🏁 Script executed:
# Read the full uiMessagesToWire implementation
sed -n '47,150p' packages/typescript/ai/src/utilities/ag-ui-wire.tsRepository: TanStack/ai
Length of output: 2700
🏁 Script executed:
# Confirm the second occurrence at line 429
sed -n '415,440p' packages/typescript/ai-client/src/connection-adapters.tsRepository: TanStack/ai
Length of output: 1062
🏁 Script executed:
# Search for all instances of the uiMessagesToWire cast pattern
rg -n 'uiMessagesToWire.*as\s+Array<UIMessage>' packages/typescript/ai-client/srcRepository: TanStack/ai
Length of output: 325
🏁 Script executed:
# Check other adapter functions (webSocketStream, sseStream, customAdapter) for same pattern
grep -n "export function" packages/typescript/ai-client/src/connection-adapters.tsRepository: TanStack/ai
Length of output: 242
Update uiMessagesToWire to accept the declared union type instead of using an unsafe cast.
The ConnectConnectionAdapter.connect() and SubscribeConnectionAdapter.send() interfaces declare messages: Array<UIMessage> | Array<ModelMessage>, but implementations cast to Array<UIMessage> before calling uiMessagesToWire, which is typed to accept only Array<UIMessage>. While uiMessagesToWire is defensively implemented to handle both shapes (it safely accesses msg.parts with fallback to msg.content), the cast obscures the actual capability and violates type safety.
Change the uiMessagesToWire signature to accept Array<UIMessage | ModelMessage> instead of casting unsafely. This fixes both occurrences (lines 314 and 429).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai-client/src/connection-adapters.ts` around lines 313 -
329, Change the uiMessagesToWire signature to accept Array<UIMessage |
ModelMessage> (instead of only Array<UIMessage>) so callers no longer need
unsafe casts; update the function declaration for uiMessagesToWire and any
related type imports/exports, and adjust internal typing if necessary to
preserve existing defensive access (e.g., using msg.parts ?? msg.content) so
both ConnectConnectionAdapter.connect and SubscribeConnectionAdapter.send can
pass their messages: Array<UIMessage> | Array<ModelMessage> directly without
casting.
|
|
||
| On the server, `chatParamsFromRequestBody` validates the body and returns the parsed fields. `convertMessagesToModelMessages` (called inside `chat()`) handles dedup: when an anchor's `parts` already contain a `tool-result`, the matching fan-out tool message is dropped from the `ModelMessage[]` fed to the LLM. `reasoning` and `activity` messages are dropped (no `ModelMessage` equivalent today); `developer` messages collapse to `system`. | ||
|
|
||
| For the migration story when upgrading, see `docs/migration/ag-ui-compliance.md`. |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm location of the migration doc and the architecture doc to verify the relative path.
fd -t f 'ag-ui-compliance.md'
fd -t f 'chat-architecture.md'Repository: TanStack/ai
Length of output: 139
Fix broken relative link to migration documentation.
The reference to docs/migration/ag-ui-compliance.md will not resolve from this nested location. Use the correct relative path:
🔗 Suggested fix
-For the migration story when upgrading, see `docs/migration/ag-ui-compliance.md`.
+For the migration story when upgrading, see [`docs/migration/ag-ui-compliance.md`](../../../../docs/migration/ag-ui-compliance.md).📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| For the migration story when upgrading, see `docs/migration/ag-ui-compliance.md`. | |
| For the migration story when upgrading, see [`docs/migration/ag-ui-compliance.md`](../../../../docs/migration/ag-ui-compliance.md). |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai/docs/chat-architecture.md` at line 86, The link text
in packages/typescript/ai/docs/chat-architecture.md references
"docs/migration/ag-ui-compliance.md" which is a broken relative path from this
nested location; update that link to the correct repository-relative path
(either replace "docs/migration/ag-ui-compliance.md" with
"../../../../docs/migration/ag-ui-compliance.md" or use an absolute repo-root
path like "/docs/migration/ag-ui-compliance.md") so the link resolves correctly.
Summary
@tanstack/ai-clientnow POSTs AG-UIRunAgentInput({threadId, runId, state, messages, tools, context, forwardedProps}) instead of{messages, data}. New helperschatParamsFromRequestBodyandmergeAgentToolsexported from@tanstack/aifor server endpoints.chat()accepts optionalthreadId,runId,parentRunIdfor AG-UI run correlation;parentRunIdplumbed through every provider adapter intoRUN_STARTEDevents.ChatClientgenerates and persiststhreadIdper session, freshrunIdper send; client-side tools auto-advertised in the request payload'stoolsfield.convertMessagesToModelMessagesdedups fan-out tool messages already covered by anchorparts, dropsreasoning/activity, collapsesdeveloper→system. A pure AG-UI client can hit a TanStack server and have its history reassembled correctly.{messages, data}body shape is rejected with 400 + migration-pointing error. Coordinated minor bump for@tanstack/ai,@tanstack/ai-client,@tanstack/ai-react,@tanstack/ai-solid,@tanstack/ai-vue,@tanstack/ai-svelte,@tanstack/ai-react-ui.Migration guide:
docs/migration/ag-ui-compliance.mdTest Plan
ts-react-chatin a browser: send messages, trigger a server tool, trigger a client tool, verify streaming and threadId continuity (subagents could not visually verify the chat UI)ts-vue-chatin a browser to confirm the migrated vite middleware works end-to-endts-svelte-chatandts-solid-chatchat flows in a browserpnpm --filter @tanstack/ai-e2e test:e2e) — local subset showed 24 hard pass + 11 flaky-but-passing-on-retry; confirm CI behavior matchesforwardedPropsshould land before any reader is tempted to spread itRUN_STARTEDwithparentRunIdwhen suppliedSummary by CodeRabbit
Release Notes
New Features
threadIdfor conversation continuity across chat sessionsrunIdfor individual run trackingparentRunIdsupport for nested run correlation and improved observabilityDocumentation