diff --git a/.changeset/curly-pugs-yell.md b/.changeset/curly-pugs-yell.md new file mode 100644 index 000000000..dade7bf94 --- /dev/null +++ b/.changeset/curly-pugs-yell.md @@ -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. diff --git a/docs/api/ai.md b/docs/api/ai.md index 1088e45da..e298c78b1 100644 --- a/docs/api/ai.md +++ b/docs/api/ai.md @@ -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"; diff --git a/docs/config.json b/docs/config.json index e3fc3b712..04c81e04f 100644 --- a/docs/config.json +++ b/docs/config.json @@ -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", @@ -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", diff --git a/docs/migration/ag-ui-compliance.md b/docs/migration/ag-ui-compliance.md index ec92e9804..b136f2c37 100644 --- a/docs/migration/ag-ui-compliance.md +++ b/docs/migration/ag-ui-compliance.md @@ -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) diff --git a/knip.json b/knip.json index a5e8a03e1..db637e2ec 100644 --- a/knip.json +++ b/knip.json @@ -1,6 +1,8 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", - "ignoreDependencies": ["@faker-js/faker"], + "ignoreDependencies": [ + "@faker-js/faker" + ], "ignoreWorkspaces": [ "examples/**", "testing/**", @@ -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" + ] } } } diff --git a/packages/ai/package.json b/packages/ai/package.json index 1ffd3b4f8..bc6b4b1ae 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -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": { diff --git a/packages/ai/skills/ai-core/ag-ui-protocol/SKILL.md b/packages/ai/skills/ai-core/ag-ui-protocol/SKILL.md index 3b798bcb5..d74f24da9 100644 --- a/packages/ai/skills/ai-core/ag-ui-protocol/SKILL.md +++ b/packages/ai/skills/ai-core/ag-ui-protocol/SKILL.md @@ -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. diff --git a/packages/ai/src/utilities/chat-params.ts b/packages/ai/src/utilities/chat-params.ts index fd70872da..04c40e1a1 100644 --- a/packages/ai/src/utilities/chat-params.ts +++ b/packages/ai/src/utilities/chat-params.ts @@ -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, @@ -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', @@ -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 { + 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`. * @@ -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 threadId: string runId: string @@ -56,15 +92,14 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{ context: Array aguiContext: Array }> { + 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}`, ) } @@ -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, @@ -103,7 +138,7 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{ state: parsed.state, context: aguiContext, aguiContext, - }) + } } /**