Skip to content

feat: AG-UI client-to-server compliance#511

Open
AlemTuzlak wants to merge 19 commits intomainfrom
feat/ag-ui-client-compliance
Open

feat: AG-UI client-to-server compliance#511
AlemTuzlak wants to merge 19 commits intomainfrom
feat/ag-ui-client-compliance

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Contributor

@AlemTuzlak AlemTuzlak commented Apr 27, 2026

Summary

  • Wire format: @tanstack/ai-client now POSTs AG-UI RunAgentInput ({threadId, runId, state, messages, tools, context, forwardedProps}) instead of {messages, data}. New helpers chatParamsFromRequestBody and mergeAgentTools exported from @tanstack/ai for server endpoints.
  • chat() accepts optional threadId, runId, parentRunId for AG-UI run correlation; parentRunId plumbed through every provider adapter into RUN_STARTED events.
  • ChatClient generates and persists threadId per session, fresh runId per send; client-side tools auto-advertised in the request payload's tools field.
  • Foreign AG-UI client compatibility: convertMessagesToModelMessages dedups fan-out tool messages already covered by anchor parts, drops reasoning/activity, collapses developersystem. A pure AG-UI client can hit a TanStack server and have its history reassembled correctly.
  • Breaking: legacy {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.md

Test Plan

  • Manually exercise ts-react-chat in 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)
  • Manually exercise ts-vue-chat in a browser to confirm the migrated vite middleware works end-to-end
  • Verify ts-svelte-chat and ts-solid-chat chat flows in a browser
  • Run full E2E suite in CI (pnpm --filter @tanstack/ai-e2e test:e2e) — local subset showed 24 hard pass + 11 flaky-but-passing-on-retry; confirm CI behavior matches
  • Review the migration guide for clarity from a fresh-eyes perspective; the security note on forwardedProps should land before any reader is tempted to spread it
  • Confirm the changeset's framework package list is what we want released together (currently bumps all 7 framework packages even where their source didn't change, to keep clients and servers in lockstep)
  • Sanity check: all 7 provider adapters (openai, anthropic, gemini, ollama, groq, grok, openrouter) emit RUN_STARTED with parentRunId when supplied

Summary by CodeRabbit

Release Notes

  • New Features

    • Added persistent thread tracking with threadId for conversation continuity across chat sessions
    • Each message now generates a unique runId for individual run tracking
    • Added parentRunId support for nested run correlation and improved observability
    • Implemented AG-UI client-to-server compliance protocol with standardized request formatting
  • Documentation

    • Added AG-UI compliance migration guide with updated endpoint handling examples
    • Updated all example applications to reflect new request/response format

…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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Implements AG-UI client-to-server protocol compliance: @tanstack/ai-client now POSTs structured RunAgentInput bodies (containing threadId, runId, messages, tools, forwardedProps) instead of flat { messages, data } shapes. Adds server utilities for parsing, tool merging, and wire serialization; updates all example endpoints and test routes; extends adapters to propagate parentRunId; includes comprehensive E2E tests.

Changes

Cohort / File(s) Summary
Documentation & Configuration
.changeset/ag-ui-client-compliance.md, docs/config.json, docs/migration/ag-ui-compliance.md, packages/typescript/ai/docs/chat-architecture.md, packages/typescript/ai/skills/ai-core/ag-ui-protocol/SKILL.md
Changeset, migration guide, and architectural docs documenting the new AG-UI request body format, request/response parsing, tool merging semantics, and compatibility notes for foreign clients. Config updated to link migration guide.
Package Dependencies
packages/typescript/ai/package.json
Updated @ag-ui/core from 0.0.49 to ^0.0.52 to support new compliance features.
AI Client Core
packages/typescript/ai-client/src/chat-client.ts, packages/typescript/ai-client/src/types.ts, packages/typescript/ai-client/src/connection-adapters.ts
Added threadId member initialization and per-send runContext construction with threadId, runId, clientTools metadata, and forwardedProps. Extended connection adapter APIs with RunAgentInputContext parameter. Updated adapters to serialize AG-UI request payload including messages-to-wire conversion.
Chat Activity & Message Conversion
packages/typescript/ai/src/activities/chat/index.ts, packages/typescript/ai/src/activities/chat/messages.ts, packages/typescript/ai/src/types.ts
Added parentRunId option threading through TextActivity. Enhanced message conversion to perform AG-UI deduplication pre-scan, omit reasoning/activity roles, collapse developer roles to system, and preserve parts arrays.
Chat Utilities
packages/typescript/ai/src/utilities/chat-params.ts, packages/typescript/ai/src/utilities/ag-ui-wire.ts, packages/typescript/ai/src/index.ts
Introduced chatParamsFromRequestBody for request body validation/normalization and mergeAgentTools for combining server/client tool registries. Added uiMessagesToWire to convert UIMessages to AG-UI wire format with fan-out for reasoning/tool messages. Re-exported utilities from package root.
Provider Adapters
packages/typescript/ai-anthropic/src/adapters/text.ts, packages/typescript/ai-gemini/src/adapters/text.ts, packages/typescript/ai-grok/src/adapters/text.ts, packages/typescript/ai-groq/src/adapters/text.ts, packages/typescript/ai-ollama/src/adapters/text.ts, packages/typescript/ai-openai/src/adapters/text.ts, packages/typescript/ai-openrouter/src/adapters/text.ts
Propagated parentRunId from options.parentRunId into RUN_STARTED stream events for nested run correlation.
Example Application Routes
examples/ts-react-chat/src/routes/api.tanchat.ts, examples/ts-solid-chat/src/routes/api.chat.ts, examples/ts-svelte-chat/src/routes/api/chat/+server.ts, examples/ts-vue-chat/vite.config.ts
Refactored all endpoints to parse request bodies via chatParamsFromRequestBody, validate with 400 error handling, derive provider/model from params.forwardedProps, and merge tools using mergeAgentTools before invoking chat() with threadId/runId.
E2E Test Routes
testing/e2e/src/routes/api.chat.ts, testing/e2e/src/routes/api.middleware-test.ts, testing/e2e/src/routes/api.tools-test.ts
Updated all test routes to use chatParamsFromRequestBody for request parsing, extract config from params.forwardedProps, add 400 error handling, and supply threadId/runId to chat() invocations.
E2E Tests
testing/e2e/tests/ag-ui-compliance.spec.ts, testing/e2e/tests/ag-ui-foreign-client.spec.ts, testing/e2e/tests/ag-ui-old-client-rejection.spec.ts
New Playwright test suites validating AG-UI compliance: request payload structure with required fields, threadId persistence and runId freshness across messages, foreign client acceptance, and legacy format rejection.
Test Suites
packages/typescript/ai-client/tests/connection-adapters.test.ts, packages/typescript/ai/tests/ag-ui-wire.test.ts, packages/typescript/ai/tests/chat-params.test.ts, packages/typescript/ai/tests/messages.test.ts
Updated connection adapter tests to validate forwardedProps serialization. Added new test files covering wire message conversion (multimodal content, fan-out reasoning/tool messages), chat params validation (required fields, legacy rejection, parts reattachment), and message conversion (AG-UI deduplication, role filtering).

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
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~75 minutes


🐰 From whiskers twitched with glee,
New protocols flow seamlessly—
ThreadIds persist, runIds bloom,
AG-UI compliance fills the room!
With merged tools and wired grace,
The chat now finds its proper place.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: implementing AG-UI client-to-server compliance with a new wire format.
Description check ✅ Passed The PR description provides comprehensive coverage of changes, testing plan, and migration guidance. All key aspects are documented with technical detail and context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ag-ui-client-compliance

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 27, 2026

🚀 Changeset Version Preview

7 package(s) bumped directly, 26 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-react 0.8.0 → 1.0.0 Changeset
@tanstack/ai-react-ui 0.6.2 → 1.0.0 Changeset
@tanstack/ai-solid 0.7.0 → 1.0.0 Changeset
@tanstack/ai-svelte 0.7.0 → 1.0.0 Changeset
@tanstack/ai-vue 0.7.0 → 1.0.0 Changeset
@tanstack/ai-anthropic 0.8.2 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.1.8 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.1.8 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.1.8 → 1.0.0 Dependent
@tanstack/ai-event-client 0.2.8 → 1.0.0 Dependent
@tanstack/ai-fal 0.7.0 → 1.0.0 Dependent
@tanstack/ai-gemini 0.10.0 → 1.0.0 Dependent
@tanstack/ai-grok 0.7.0 → 1.0.0 Dependent
@tanstack/ai-groq 0.1.8 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.8 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.8 → 1.0.0 Dependent
@tanstack/ai-ollama 0.6.10 → 1.0.0 Dependent
@tanstack/ai-openai 0.8.2 → 1.0.0 Dependent
@tanstack/ai-openrouter 0.8.2 → 1.0.0 Dependent
@tanstack/ai-preact 0.6.20 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.6.2 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.14.0 → 0.15.0 Changeset
@tanstack/ai-client 0.8.0 → 0.9.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-code-mode-models-eval 0.0.11 → 0.0.12 Dependent
@tanstack/ai-devtools-core 0.3.25 → 0.3.26 Dependent
@tanstack/ai-isolate-cloudflare 0.1.8 → 0.1.9 Dependent
@tanstack/ai-vue-ui 0.1.31 → 0.1.32 Dependent
@tanstack/preact-ai-devtools 0.1.29 → 0.1.30 Dependent
@tanstack/react-ai-devtools 0.2.29 → 0.2.30 Dependent
@tanstack/solid-ai-devtools 0.2.29 → 0.2.30 Dependent
ts-svelte-chat 0.1.37 → 0.1.38 Dependent
ts-vue-chat 0.1.37 → 0.1.38 Dependent
vanilla-chat 0.0.35 → 0.0.36 Dependent

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 27, 2026

View your CI Pipeline Execution ↗ for commit 8b1cdb6

Command Status Duration Result
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 1m 24s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-27 17:46:54 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 27, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@511

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@511

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@511

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@511

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@511

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@511

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@511

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@511

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@511

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@511

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@511

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@511

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@511

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@511

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@511

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@511

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@511

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@511

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@511

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@511

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@511

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@511

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@511

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@511

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@511

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@511

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@511

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@511

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@511

commit: 8b1cdb6

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Synthesized RUN_FINISHED should reuse runContext.runId so 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 by runId will 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_ERROR path — consider adding runId: runContext?.runId so 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 }).content assumes string content, but a ModelMessage-typed input has content: 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" ModelMessage will 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 the Promise wrapper.

chatParamsFromRequestBody performs only synchronous work but returns a Promise via Promise.reject/Promise.resolve. Either drop the wrapper (return the value/throw) or mark the function async for symmetry; the Promise<...> return type forces every caller into await for 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 await it, leaving the Promise<> 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 message id has a limitation: ModelMessage does not have an id field, only UIMessage does (line 355 in types.ts). Since chatParamsFromRequestBody returns Array<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, or migration appears, 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: ChatRequestBody is stale and contradicts the wire format change.

Per the PR, @tanstack/ai-client now POSTs AG-UI RunAgentInput ({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 a RUN_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's expect(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: Reuse generateUniqueId('run') for the runId.

threadId (L85) and other IDs use this.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 to crypto.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 uiMessagesToWire and WireMessage are 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 the default: branch of the switch at lines 227-245 for fallback. That works today, but a future refactor that drops the default: 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 getGuitars and addToCartToolServer as server tools, while the Svelte example registers getGuitars, recommendGuitarToolDef, addToCartToolServer, addToWishListToolDef, and getPersonalGuitarPreferenceToolDef, and the React example registers an even larger superset. Tool definitions without .server(...) are still useful in serverTools because mergeAgentTools will treat them as no-execute entries — the LLM still sees them, but the runtime emits ClientToolRequest events. Unless the Vue client side is advertising all of these via params.tools, the model in this example will be missing recommendGuitar, addToWishList, and getPersonalGuitarPreference.

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.message directly 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 chatParamsFromRequestBody rejects when threadId/runId/messages are absent, but they only assert that some error is thrown. If RunAgentInputSchema ever 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 exclude runContext from the wire shape—this is by design.

Both stream() (line 501) and rpcStream() (line 532) drop the runContext parameter because their factory functions only accept messages and data. Unlike the fetch() and webSocket() adapters, which embed threadId and runId in 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 runContext and extract threadId/runId from 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

📥 Commits

Reviewing files that changed from the base of the PR and between ff33855 and 8b1cdb6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (36)
  • .changeset/ag-ui-client-compliance.md
  • docs/config.json
  • docs/migration/ag-ui-compliance.md
  • examples/ts-react-chat/src/routes/api.tanchat.ts
  • examples/ts-solid-chat/src/routes/api.chat.ts
  • examples/ts-svelte-chat/src/routes/api/chat/+server.ts
  • examples/ts-vue-chat/vite.config.ts
  • packages/typescript/ai-anthropic/src/adapters/text.ts
  • packages/typescript/ai-client/src/chat-client.ts
  • packages/typescript/ai-client/src/connection-adapters.ts
  • packages/typescript/ai-client/src/types.ts
  • packages/typescript/ai-client/tests/connection-adapters.test.ts
  • packages/typescript/ai-gemini/src/adapters/text.ts
  • packages/typescript/ai-grok/src/adapters/text.ts
  • packages/typescript/ai-groq/src/adapters/text.ts
  • packages/typescript/ai-ollama/src/adapters/text.ts
  • packages/typescript/ai-openai/src/adapters/text.ts
  • packages/typescript/ai-openrouter/src/adapters/text.ts
  • packages/typescript/ai/docs/chat-architecture.md
  • packages/typescript/ai/package.json
  • packages/typescript/ai/skills/ai-core/ag-ui-protocol/SKILL.md
  • packages/typescript/ai/src/activities/chat/index.ts
  • packages/typescript/ai/src/activities/chat/messages.ts
  • packages/typescript/ai/src/index.ts
  • packages/typescript/ai/src/types.ts
  • packages/typescript/ai/src/utilities/ag-ui-wire.ts
  • packages/typescript/ai/src/utilities/chat-params.ts
  • packages/typescript/ai/tests/ag-ui-wire.test.ts
  • packages/typescript/ai/tests/chat-params.test.ts
  • packages/typescript/ai/tests/messages.test.ts
  • testing/e2e/src/routes/api.chat.ts
  • testing/e2e/src/routes/api.middleware-test.ts
  • testing/e2e/src/routes/api.tools-test.ts
  • testing/e2e/tests/ag-ui-compliance.spec.ts
  • testing/e2e/tests/ag-ui-foreign-client.spec.ts
  • testing/e2e/tests/ag-ui-old-client-rejection.spec.ts

Comment on lines +154 to +158
const provider: Provider =
typeof params.forwardedProps.provider === 'string' &&
(params.forwardedProps.provider as Provider)
? (params.forwardedProps.provider as Provider)
: 'openai'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +613 to +624
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 },
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.ts

Repository: 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 -n

Repository: 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 -30

Repository: 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 -n

Repository: 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.ts

Repository: 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.

Suggested change
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.

Comment on lines +313 to +329
// 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,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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/src

Repository: 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.ts

Repository: 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 -40

Repository: 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.ts

Repository: TanStack/ai

Length of output: 498


🏁 Script executed:

# Look for UIMessage and ModelMessage type definitions
rg -nP 'type\s+(UIMessage|ModelMessage)' packages/typescript/ai/src

Repository: 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.ts

Repository: 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 -20

Repository: 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.ts

Repository: 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.ts

Repository: TanStack/ai

Length of output: 838


🏁 Script executed:

# Get full fetchHttpStream function signature
sed -n '410,425p' packages/typescript/ai-client/src/connection-adapters.ts

Repository: 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 -20

Repository: TanStack/ai

Length of output: 246


🏁 Script executed:

# Read the UIMessage and ModelMessage interface definitions
sed -n '289,360p' packages/typescript/ai/src/types.ts

Repository: TanStack/ai

Length of output: 1653


🏁 Script executed:

# Find ConnectConnectionAdapter interface definition
rg -nP 'interface\s+ConnectConnectionAdapter|type\s+ConnectConnectionAdapter' packages/typescript/ai-client/src

Repository: TanStack/ai

Length of output: 238


🏁 Script executed:

# Also search in types or definitions files
rg -nP 'ConnectConnectionAdapter' packages/typescript/ai-client/src --type ts

Repository: TanStack/ai

Length of output: 1984


🏁 Script executed:

# Read the ConnectConnectionAdapter interface definition
sed -n '89,125p' packages/typescript/ai-client/src/connection-adapters.ts

Repository: 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.ts

Repository: 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.ts

Repository: 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/src

Repository: 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.ts

Repository: 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`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant