Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/curly-pugs-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@tanstack/ai': minor
---

Stop pulling Zod 3 into consumer dependency trees and type graphs (#520).

`@tanstack/ai` now depends on `@ag-ui/core@^0.1.0`, whose main entry is
type-only with zero runtime dependencies. Apps on Zod 4 (e.g. using
`@hookform/resolvers@5`) no longer hit `zod/v4/core` version-mismatch type
errors caused by the transitive Zod 3 that earlier `@ag-ui/core` versions
carried.

`chatParamsFromRequest` / `chatParamsFromRequestBody` now load
`RunAgentInputSchema` lazily from the `@ag-ui/core/schemas` subpath. That
subpath requires zod (`^3.24.0 || ^4.0.0` — whichever major your app already
has), so zod is now an optional peer dependency of `@tanstack/ai`: only
server code calling `chatParamsFromRequest*` needs it installed. Calling it
without zod rejects with an `AGUIError` explaining what to install. Wire
behavior is unchanged.
2 changes: 2 additions & 0 deletions docs/api/ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ A `Response` object suitable for HTTP endpoints with SSE headers (`Content-Type:

Reads an HTTP `Request`, parses its JSON body, and validates it against AG-UI `RunAgentInputSchema`. Returns parsed chat parameters ready to spread into `chat()`. On a malformed body, **throws a 400 `Response`** that frameworks like TanStack Start, SolidStart, Remix, and React Router 7 return to the client automatically.

> **zod requirement.** Validation loads `RunAgentInputSchema` lazily from `@ag-ui/core/schemas`, which needs zod (`^3.24.0 || ^4.0.0` — whichever major your app already uses). zod is an optional peer dependency of `@tanstack/ai`: only server code calling `chatParamsFromRequest` / `chatParamsFromRequestBody` needs it installed.

```typescript
import { chat, chatParamsFromRequest, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
Expand Down
6 changes: 4 additions & 2 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,8 @@
{
"label": "AG-UI Client Compliance",
"to": "migration/ag-ui-compliance",
"addedAt": "2026-05-16"
"addedAt": "2026-05-16",
"updatedAt": "2026-06-10"
},
{
"label": "Sampling → modelOptions",
Expand All @@ -360,7 +361,8 @@
{
"label": "@tanstack/ai",
"to": "api/ai",
"addedAt": "2026-04-15"
"addedAt": "2026-04-15",
"updatedAt": "2026-06-10"
},
{
"label": "@tanstack/ai-client",
Expand Down
17 changes: 16 additions & 1 deletion docs/migration/ag-ui-compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,22 @@ Pure AG-UI `RunAgentInput` payloads (no TanStack `parts` field) work end-to-end:

## `@ag-ui/core` bump

`@tanstack/ai` now depends on `@ag-ui/core@^0.0.52`. If your code imports types from `@tanstack/ai` that re-export AG-UI types, you may need minor type adjustments — see the changeset for specifics.
`@tanstack/ai` now depends on `@ag-ui/core@^0.1.0`, whose main entry is
type-only with **zero runtime dependencies** — it no longer pulls Zod 3 into
your dependency tree or type graph, so apps on Zod 4 type-check cleanly
alongside `@tanstack/ai`
([#520](https://github.com/TanStack/ai/issues/520)).

Request validation in `chatParamsFromRequest` / `chatParamsFromRequestBody`
now loads `RunAgentInputSchema` lazily from the `@ag-ui/core/schemas`
subpath, which requires zod (`^3.24.0 || ^4.0.0` — whichever major your app
already uses). zod is an **optional** peer dependency of `@tanstack/ai`: if
you never call `chatParamsFromRequest*`, you don't need zod installed at
all. Calling it without zod present rejects with an `AGUIError` telling you
to install zod.

If your code imports types from `@tanstack/ai` that re-export AG-UI types,
you may need minor type adjustments — see the changeset for specifics.

## Out of scope (existing behavior preserved)

Expand Down
33 changes: 25 additions & 8 deletions knip.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"ignoreDependencies": ["@faker-js/faker"],
"ignoreDependencies": [
"@faker-js/faker"
],
"ignoreWorkspaces": [
"examples/**",
"testing/**",
Expand All @@ -25,25 +27,40 @@
"ignore": []
},
"packages/ai": {
"ignoreDependencies": ["@opentelemetry/api"]
"ignoreDependencies": [
"@opentelemetry/api",
"zod"
]
},
"packages/ai-anthropic": {
"ignore": ["src/tools/**"]
"ignore": [
"src/tools/**"
]
},
"packages/ai-gemini": {
"ignore": ["src/tools/**"]
"ignore": [
"src/tools/**"
]
},
"packages/ai-openai": {
"ignore": ["src/tools/**"]
"ignore": [
"src/tools/**"
]
},
"packages/ai-client": {
"ignoreDependencies": ["@standard-schema/spec"]
"ignoreDependencies": [
"@standard-schema/spec"
]
},
"packages/ai-react-ui": {
"ignoreDependencies": ["react-dom"]
"ignoreDependencies": [
"react-dom"
]
},
"packages/ai-vue-ui": {
"ignore": ["src/use-chat-context.ts"]
"ignore": [
"src/use-chat-context.ts"
]
}
}
}
8 changes: 6 additions & 2 deletions packages/ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,21 @@
"tanstack-intent"
],
"dependencies": {
"@ag-ui/core": "^0.0.52",
"@ag-ui/core": "^0.1.0",
"@standard-schema/spec": "^1.1.0",
"@tanstack/ai-event-client": "workspace:*",
"partial-json": "^0.1.7"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.9.0"
"@opentelemetry/api": ">=1.9.0",
"zod": "^3.24.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"zod": {
"optional": true
}
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/ai/skills/ai-core/ag-ui-protocol/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function POST(req: Request) {
}
```

`chatParamsFromRequestBody` validates the body against `RunAgentInputSchema` from `@ag-ui/core`. `mergeAgentTools` merges the server's tool registry with client-declared tools (server wins on collision; client-only tools become no-execute stubs that flow through the runtime's `ClientToolRequest` path).
`chatParamsFromRequestBody` validates the body against `RunAgentInputSchema`, loaded lazily from `@ag-ui/core/schemas`. That subpath needs zod (`^3.24.0 || ^4.0.0`, an optional peer of `@tanstack/ai`) — server apps calling `chatParamsFromRequest*` must have zod installed; everything else in `@tanstack/ai` works without it. `mergeAgentTools` merges the server's tool registry with client-declared tools (server wins on collision; client-only tools become no-execute stubs that flow through the runtime's `ClientToolRequest` path).

`params.messages` is a mixed array of TanStack `UIMessage` anchors (with `parts`) and AG-UI fan-out duplicates (`{role:'tool',...}`, `{role:'reasoning',...}`). The existing `convertMessagesToModelMessages` (called inside `chat()`) handles dedup automatically.

Expand Down
61 changes: 48 additions & 13 deletions packages/ai/src/utilities/chat-params.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AGUIError, RunAgentInputSchema } from '@ag-ui/core'
import type { Context as AGUIContext } from '@ag-ui/core'
import { AGUIError } from '@ag-ui/core'
import type { Context as AGUIContext, RunAgentInput } from '@ag-ui/core'
import type {
JSONSchema,
ModelMessage,
Expand All @@ -8,6 +8,22 @@ import type {
UIMessage,
} from '../types'

/**
* The slice of `RunAgentInputSchema` this module relies on, expressed
* without referencing zod types. `@ag-ui/core/schemas` runs on zod 3.24+
* and zod 4, but its emitted declarations are pinned to one zod major —
* typing the import structurally keeps zod out of this package's
* compilation and out of its public `.d.ts` (the whole point of
* https://github.com/TanStack/ai/issues/520).
*/
interface RunAgentInputValidator {
safeParse: (
body: unknown,
) =>
| { success: true; data: RunAgentInput }
| { success: false; error: { message: string } }
}

const KNOWN_PART_TYPES = new Set([
'text',
'image',
Expand All @@ -29,6 +45,26 @@ function isValidParts(value: unknown): value is Array<{ type: string }> {
return true
}

/**
* Lazily load `RunAgentInputSchema` from the opt-in `@ag-ui/core/schemas`
* subpath. The subpath has zod as an optional peer dependency, so the import
* only happens when AG-UI request parsing is actually used — consumers that
* never call `chatParamsFromRequest*` don't need zod installed at all.
*/
async function loadRunAgentInputSchema(): Promise<RunAgentInputValidator> {
try {
const { RunAgentInputSchema } = await import('@ag-ui/core/schemas')
return RunAgentInputSchema as RunAgentInputValidator
} catch (cause) {
const error = new AGUIError(
`chatParamsFromRequestBody requires zod to validate AG-UI request ` +
`bodies. Install zod (^3.24.0 || ^4.0.0) alongside @tanstack/ai.`,
)
error.cause = cause
throw error
}
}

/**
* Parse and validate an HTTP request body as an AG-UI `RunAgentInput`.
*
Expand All @@ -38,10 +74,10 @@ function isValidParts(value: unknown): value is Array<{ type: string }> {
* reasoning/activity/developer-role normalization internally.
*
* @throws An error with a migration-pointing message when the body does
* not conform to AG-UI 0.0.52 `RunAgentInputSchema`. Surface this as a
* not conform to the AG-UI `RunAgentInputSchema`. Surface this as a
* 400 Bad Request to the client.
*/
export function chatParamsFromRequestBody(body: unknown): Promise<{
export async function chatParamsFromRequestBody(body: unknown): Promise<{
messages: Array<UIMessage | ModelMessage>
threadId: string
runId: string
Expand All @@ -56,15 +92,14 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{
context: Array<AGUIContext>
aguiContext: Array<AGUIContext>
}> {
const RunAgentInputSchema = await loadRunAgentInputSchema()
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}`,
)
}

Expand All @@ -89,7 +124,7 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{
return m as ModelMessage
})

return Promise.resolve({
return {
messages,
threadId: parsed.threadId,
runId: parsed.runId,
Expand All @@ -103,7 +138,7 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{
state: parsed.state,
context: aguiContext,
aguiContext,
})
}
}

/**
Expand Down
Loading