Skip to content

refactor(ai): remove zod from the dependency graph - #1038

Open
AlemTuzlak wants to merge 1 commit into
mainfrom
refactor/ai-drop-zod-dependency
Open

refactor(ai): remove zod from the dependency graph#1038
AlemTuzlak wants to merge 1 commit into
mainfrom
refactor/ai-drop-zod-dependency

Conversation

@AlemTuzlak

@AlemTuzlak AlemTuzlak commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

@tanstack/ai no longer pulls zod into consumers' dependency graphs.

Why it was there. @ag-ui/core listed zod as a runtime dependency, so every @tanstack/ai install got zod transitively. 0.1.1-canary.beta.0 is the only published version of @ag-ui/core that declares zod as an optional peer instead — all 59 others, including 0.0.57 (latest), 0.0.58-canary.beta.0 and 0.0.59-canary.*, hard-depend on it.

Why the bump alone isn't enough. chatParamsFromRequest / chatParamsFromRequestBody were the only zod consumers in this package — they validated request bodies with AG-UI's RunAgentInputSchema, which the canary moved to the @ag-ui/core/schemas subpath. Because that module is re-exported from the package root, importing the subpath (statically or lazily) keeps zod on the critical path for anyone doing import { chat } from '@tanstack/ai': a static import makes zod mandatory for every consumer, and an optional peer would let them install without it and crash at import time.

So both helpers now validate the same RunAgentInput contract structurally, transcribed from the upstream schema definitions — tools/context stay required arrays, parentRunId/state/forwardedProps/resume optional, messages discriminate on role per variant (assistant content optional, user content string-or-array, tool requires toolCallId, activity requires activityType + object content). The role switch is exhaustive rather than default-cased, and the role list is keyed Record<AGUIRole, true> so a role added upstream fails to compile until it's handled (verified: removing one yields TS2741).

Result: @tanstack/ai ships with no schema-validation runtime at all. Verified by importing the built entry under a resolver hook that blocks zod and @ag-ui/core/schemas — the root imports fine (98 exports) and request parsing still succeeds.

No API change

Both helpers keep their signatures, still reject non-conforming bodies with AGUIError (chatParamsFromRequest still throws a 400 Response with the original attached as cause), and still carry TanStack's canonical parts through on messages. Validation failures now name the offending field — messages[1].content must be a string instead of a zod issue dump.

zod remains fully supported for defining tools; it's simply no longer installed on your behalf. Projects that used zod without declaring it, relying on the transitive copy, need to add it — called out in the changeset and the migration guide.

Two deliberate behavior notes

  • forwardedProps is now validated as an object. Upstream typed it z.any(), so a non-object previously passed through and violated the declared Record<string, unknown> return type. Happy to relax this if the looser contract was intentional.
  • Unknown fields on individual messages now pass through instead of being stripped by z.object, which is more forward-compatible with newer AG-UI message fields. Unknown top-level fields are still dropped by construction, so the cursor behavior is unchanged and its test still passes.

Drive-by doc fix

The canary replaced zod-inferred event types with hand-written interfaces, which changes narrowing on the AGUIEvent union (StreamChunk = AGUIEvent). A middleware snippet in docs/comparison/vercel-ai-sdk.md used 'delta' in chunk, which no longer filters the union — every member is retained and intersected with Record<'delta', unknown>, typing delta as unknown (hence {} after !). It now discriminates on type, the real discriminant, which narrows delta to string with no non-null assertion. Treated as a correction, so its updatedAt isn't bumped; the migration guide's is.

Tests

25 new cases in chat-params.test.ts covering the accept/reject boundary this PR now owns: role validation, per-variant required fields, malformed tool/context/resume entries, parts dropping, state passthrough, optional parentRunId.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

Ran the test:pr target set directly rather than through the Nx wrapper — in a git worktree on this machine Nx dies with EISDIR: illegal operation on a directory, lstat 'F:' and reports phantom successes (it printed "Successfully ran target build for 50 projects" while writing no dist at all). Results:

  • test:lib — 1335 passed (71 files)
  • test:types — clean for packages/ai; 51 projects via Nx
  • test:kiira — 876/876 snippets, 133 files, 0 errors
  • test:oxlint — exit 0
  • test:sherif — no issues
  • test:docs — no broken links
  • test:build (publint) — all good
  • build — all packages, via pnpm -r
  • E2E — 386 passed, 12 flaky (TTS/image/video provider tests, unrelated), 0 failed

test:knip exits 1 on 13 pre-existing config hints (a packages/react-ai workspace entry, stale ignore/ignoreDependencies paths); knip --no-config-hints exits 0 and none of the hints relate to this change. Left alone as unrelated config drift.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

One caveat worth an explicit decision: this pins a canary (0.1.1-canary.beta.0) as an exact version, since ^ on a prerelease doesn't range as you'd expect. It's the only release with zod as a peer, so going zod-free today means shipping against the canary. If you'd rather wait for a stable 0.1.x, the structural-validation half of this PR stands on its own and can land against 0.0.57.

Summary by CodeRabbit

  • New Features
    • Added structural validation for agent requests with clearer errors identifying invalid fields.
    • Preserved supported request formats while filtering malformed content and unknown fields.
  • Bug Fixes
    • Improved handling of messages, tools, context, resume data, and forwarded properties.
    • Updated middleware event handling for more reliable text content processing.
  • Documentation
    • Added migration guidance for the optional Zod integration.
    • Updated AG-UI compatibility documentation and dependency information.

Bump `@ag-ui/core` to `0.1.1-canary.beta.0`, the only published version that
declares zod as an optional peer rather than a runtime dependency. Every prior
version (including `0.0.57`) hard-depends on zod, so it was installed
transitively for every `@tanstack/ai` consumer.

`chatParamsFromRequest` / `chatParamsFromRequestBody` were the sole zod
consumers here — they validated request bodies with AG-UI's
`RunAgentInputSchema`, which the canary moved to the `@ag-ui/core/schemas`
subpath. Since this module is re-exported from the package root, importing that
subpath (statically or lazily) would keep zod on the critical path for anyone
calling `import { chat } from '@tanstack/ai'`. Instead both helpers now validate
the same `RunAgentInput` contract structurally, transcribed from the upstream
schema definitions: `tools`/`context` stay required arrays, `parentRunId` /
`state` / `forwardedProps` / `resume` optional, and messages discriminate on
`role` per variant.

`@tanstack/ai` now ships with no schema-validation runtime at all and neither
requires nor suggests zod. zod remains fully supported for defining tools.

No API change: both helpers keep their signatures, still reject non-conforming
bodies with `AGUIError` (`chatParamsFromRequest` still throws a 400 `Response`),
and still carry TanStack's canonical `parts` through on messages. Validation
failures now name the offending field (`messages[1].content must be a string`)
instead of dumping zod issues.

Two deliberate behavior notes:

- `forwardedProps` is now validated as an object. Upstream typed it `z.any()`,
  so a non-object previously passed through and violated the declared
  `Record<string, unknown>` return type.
- Unknown fields on individual messages now pass through instead of being
  stripped by `z.object`, which is more forward-compatible with newer AG-UI
  message fields. Unknown top-level fields are still dropped by construction,
  so the `cursor` behavior is unchanged.

Also fixes a middleware snippet in the Vercel AI SDK comparison doc. The canary
replaced zod-inferred event types with hand-written interfaces, so
`'delta' in chunk` no longer filters the `AGUIEvent` union — every member is
retained and intersected with `Record<'delta', unknown>`, typing `delta` as
`unknown`. The snippet now discriminates on `type`, the real discriminant, which
narrows `delta` to `string` with no non-null assertion.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

chatParamsFromRequestBody now performs structural AG-UI request validation without runtime zod code. The package upgrades @ag-ui/core, adds field-specific validation tests, documents migration requirements, and updates typed middleware redaction.

Changes

AG-UI request validation

Layer / File(s) Summary
AG-UI dependency and migration contract
packages/ai/package.json, docs/migration/ag-ui-compliance.md, docs/config.json, .changeset/ag-ui-core-zod-free.md
The package pins @ag-ui/core to 0.1.1-canary.beta.0. Documentation describes structural validation and optional zod installation.
Structural request validation
packages/ai/src/utilities/chat-params.ts
chatParamsFromRequestBody validates request fields and nested AG-UI entries, filters invalid message parts, applies defaults, excludes unknown top-level fields, and returns an asynchronous result.
Validation behavior coverage
packages/ai/tests/chat-params.test.ts
Tests cover request shapes, role-specific messages, tools, context, resume entries, forwarded properties, state, and parentRunId.

Middleware redaction documentation

Layer / File(s) Summary
Typed middleware chunk handling
docs/comparison/vercel-ai-sdk.md
The middleware narrows chunks with EventType.TEXT_MESSAGE_CONTENT before accessing delta for SSN redaction.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: tombeckenham

Sequence Diagram(s)

sequenceDiagram
  participant RequestBody
  participant chatParamsFromRequestBody
  participant AGUIValidators
  participant AGUIError
  RequestBody->>chatParamsFromRequestBody: provide unknown request body
  chatParamsFromRequestBody->>AGUIValidators: validate fields and nested entries
  AGUIValidators-->>chatParamsFromRequestBody: return validated values
  AGUIValidators-->>AGUIError: report invalid field
  chatParamsFromRequestBody-->>RequestBody: return reconstructed chat parameters
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% 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: removing zod from the dependency graph.
Description check ✅ Passed The description covers the changes, motivation, testing, checklist, release impact, changeset, and known caveats in detail.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/ai-drop-zod-dependency

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.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

19 package(s) bumped directly, 32 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-angular 0.3.1 → 1.0.0 Changeset
@tanstack/ai-durable-stream 0.0.0 → 1.0.0 Changeset
@tanstack/ai-memory 0.0.0 → 1.0.0 Changeset
@tanstack/ai-openai 0.17.1 → 1.0.0 Changeset
@tanstack/ai-openrouter 0.15.10 → 1.0.0 Changeset
@tanstack/ai-persistence 0.0.0 → 1.0.0 Changeset
@tanstack/ai-preact 0.11.1 → 1.0.0 Changeset
@tanstack/ai-react 0.18.1 → 1.0.0 Changeset
@tanstack/ai-sandbox 0.2.4 → 1.0.0 Changeset
@tanstack/ai-solid 0.15.1 → 1.0.0 Changeset
@tanstack/ai-svelte 0.15.1 → 1.0.0 Changeset
@tanstack/ai-vue 0.15.1 → 1.0.0 Changeset
@tanstack/openai-base 0.9.9 → 1.0.0 Changeset
@tanstack/ai-acp 0.2.3 → 1.0.0 Dependent
@tanstack/ai-anthropic 0.16.3 → 1.0.0 Dependent
@tanstack/ai-bedrock 0.1.4 → 1.0.0 Dependent
@tanstack/ai-claude-code 0.2.3 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.3.8 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.3.11 → 1.0.0 Dependent
@tanstack/ai-codex 0.2.3 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.34 → 1.0.0 Dependent
@tanstack/ai-fal 0.9.12 → 1.0.0 Dependent
@tanstack/ai-gemini 0.20.1 → 1.0.0 Dependent
@tanstack/ai-grok 0.14.9 → 1.0.0 Dependent
@tanstack/ai-grok-build 0.2.3 → 1.0.0 Dependent
@tanstack/ai-groq 0.5.3 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.47 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.47 → 1.0.0 Dependent
@tanstack/ai-mistral 0.2.3 → 1.0.0 Dependent
@tanstack/ai-ollama 0.8.16 → 1.0.0 Dependent
@tanstack/ai-opencode 0.2.3 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.15 → 1.0.0 Dependent
@tanstack/ai-sandbox-cloudflare 0.2.4 → 1.0.0 Dependent
@tanstack/ai-sandbox-daytona 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-docker 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-local-process 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-sprites 0.2.1 → 1.0.0 Dependent
@tanstack/ai-sandbox-vercel 0.2.0 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.14 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.42.0 → 0.43.0 Changeset
@tanstack/ai-client 0.22.1 → 0.23.0 Changeset
@tanstack/ai-devtools-core 0.4.24 → 0.5.0 Changeset
@tanstack/ai-event-client 0.6.8 → 0.7.0 Changeset
@tanstack/ai-utils 0.3.1 → 0.4.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-mcp 0.2.5 → 0.2.6 Changeset
@tanstack/ai-isolate-cloudflare 0.2.38 → 0.2.39 Dependent
@tanstack/ai-vue-ui 0.2.34 → 0.2.35 Dependent
@tanstack/preact-ai-devtools 0.1.67 → 0.1.68 Dependent
@tanstack/react-ai-devtools 0.2.67 → 0.2.68 Dependent
@tanstack/solid-ai-devtools 0.2.67 → 0.2.68 Dependent
ag-ui 0.0.2 → 0.0.3 Dependent

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​ag-ui/​core@​0.1.1-canary.beta.01001009497100

View full report

@nx-cloud

nx-cloud Bot commented Jul 31, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 9887698

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

☁️ Nx Cloud last updated this comment at 2026-07-31 16:56:54 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 31, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

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

@tanstack/ai-acp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-acp@1038

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@1038

@tanstack/ai-anthropic

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

@tanstack/ai-bedrock

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-bedrock@1038

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@1038

@tanstack/ai-client

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

@tanstack/ai-code-mode

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

@tanstack/ai-code-mode-skills

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

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@1038

@tanstack/ai-devtools-core

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

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-durable-stream@1038

@tanstack/ai-elevenlabs

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

@tanstack/ai-event-client

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

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-grok-build

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok-build@1038

@tanstack/ai-groq

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

@tanstack/ai-isolate-cloudflare

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

@tanstack/ai-isolate-node

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

@tanstack/ai-isolate-quickjs

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

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@1038

@tanstack/ai-memory

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-memory@1038

@tanstack/ai-mistral

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mistral@1038

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@1038

@tanstack/ai-openrouter

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

@tanstack/ai-persistence

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence@1038

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@1038

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@1038

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-daytona@1038

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@1038

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@1038

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-sprites@1038

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-vercel@1038

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@1038

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@1038

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: 9887698

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
docs/migration/ag-ui-compliance.md (1)

383-385: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a language to the fenced block.

markdownlint reports MD040 for this block. Use text so the docs lint gate passes.

📝 Proposed fix
-```
+```text
 Request body is not a valid AG-UI RunAgentInput. ... Validation errors: messages[1].content must be a string
</details>

Based on the static analysis hint MD040 (fenced-code-language) at line 383.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/migration/ag-ui-compliance.md around lines 383 - 385, Update the fenced
code block in the AG-UI compliance documentation to specify the text language,
changing the opening fence to use text while preserving its existing contents.


</details>

<!-- cr-comment:v1:c69ec57803d9a17ea7ecda25 -->

_Source: Linters/SAST tools_

</blockquote></details>
<details>
<summary>packages/ai/tests/chat-params.test.ts (1)</summary><blockquote>

`218-230`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _⚡ Quick win_

**Add the complementary cases for `parts`, role lookup, and `toolCalls`.**

This test proves that unrecognized `parts` are dropped. Three boundaries stay uncovered:

- A message with recognized `parts` keeps them.
- `role: 'toString'` is rejected. It is currently accepted, because `isAGUIRole` uses `in`. See the comment on `packages/ai/src/utilities/chat-params.ts` lines 41-53.
- `toolCalls: [{}]` on an assistant message is rejected. See the comment on `packages/ai/src/utilities/chat-params.ts` lines 100-116.

Add these three cases so the accept/reject boundary is pinned after the fixes land.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @packages/ai/tests/chat-params.test.ts around lines 218 - 230, Add three
complementary test cases in the chatParamsFromRequestBody suite: verify
recognized parts are preserved, verify a message with role "toString" is
rejected, and verify an assistant message with toolCalls containing an empty
object is rejected. Keep the assertions aligned with the existing invalid-parts
test and use withMessages for request construction.


</details>

<!-- cr-comment:v1:2824d44c490e24adf724073d -->

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @docs/migration/ag-ui-compliance.md:

  • Around line 375-381: Update the earlier Tier 2 documentation statements that
    reference Zod or AG-UI’s RunAgentInputSchema, replacing them with structural
    validation against the RunAgentInput contract; keep the surrounding behavior and
    terminology consistent with the later chatParamsFromRequest and
    chatParamsFromRequestBody description.

In @packages/ai/src/utilities/chat-params.ts:

  • Around line 100-116: Update the assistant branch in the role validation switch
    to validate every element of value.toolCalls after requireArray, checking each
    expected tool-call property, including id and function.name/function.arguments,
    before modelMessageToUIMessage() can process it; malformed entries must produce
    the existing validation error rather than throwing during conversion.
  • Around line 41-53: Update isAGUIRole to use an own-property membership check
    with Object.hasOwn against AGUI_ROLES instead of the in operator, so inherited
    names such as toString, valueOf, and constructor are rejected.

Nitpick comments:
In @docs/migration/ag-ui-compliance.md:

  • Around line 383-385: Update the fenced code block in the AG-UI compliance
    documentation to specify the text language, changing the opening fence to use
    text while preserving its existing contents.

In @packages/ai/tests/chat-params.test.ts:

  • Around line 218-230: Add three complementary test cases in the
    chatParamsFromRequestBody suite: verify recognized parts are preserved, verify a
    message with role "toString" is rejected, and verify an assistant message with
    toolCalls containing an empty object is rejected. Keep the assertions aligned
    with the existing invalid-parts test and use withMessages for request
    construction.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro Plus

**Run ID**: `5ef51077-f12e-4023-97f7-b47d10d6253d`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 826cfed738ec19e8f416bc5373ad50de0c1824cc and 988769877e42c4d35a697686807875030ab5b088.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `.changeset/ag-ui-core-zod-free.md`
* `docs/comparison/vercel-ai-sdk.md`
* `docs/config.json`
* `docs/migration/ag-ui-compliance.md`
* `packages/ai/package.json`
* `packages/ai/src/utilities/chat-params.ts`
* `packages/ai/tests/chat-params.test.ts`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +375 to +381
`chatParamsFromRequest` / `chatParamsFromRequestBody` were the only zod
consumers: they validated the request body with AG-UI's `RunAgentInputSchema`.
They now validate the same `RunAgentInput` contract structurally. Their
signatures, their thrown types (`AGUIError`, and a 400 `Response` from
`chatParamsFromRequest`), and the `parts` passthrough on messages are all
unchanged. The one visible difference is friendlier failures — the error names
the offending field, e.g.:

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the earlier Tier 2 text, which still credits zod.

This section states that zod is no longer used. Lines 133 and 157 of the same document still describe "Zod validation against RunAgentInputSchema" and validation "against AG-UI RunAgentInputSchema". Readers see two different mechanisms in one page. Reword those two lines to describe structural validation of the RunAgentInput contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/migration/ag-ui-compliance.md` around lines 375 - 381, Update the
earlier Tier 2 documentation statements that reference Zod or AG-UI’s
RunAgentInputSchema, replacing them with structural validation against the
RunAgentInput contract; keep the surrounding behavior and terminology consistent
with the later chatParamsFromRequest and chatParamsFromRequestBody description.

Comment on lines +41 to +53
const AGUI_ROLES: Record<AGUIRole, true> = {
developer: true,
system: true,
assistant: true,
user: true,
tool: true,
activity: true,
reasoning: true,
}

function isAGUIRole(value: unknown): value is AGUIRole {
return typeof value === 'string' && value in AGUI_ROLES
}

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Show the AG-UI Role union and Message variants for the pinned canary.
set -euo pipefail

fd -t f -g '*.d.ts' . node_modules/@ag-ui/core 2>/dev/null | head -20 || true
rg -n -C3 'type Role|ToolCall|toolCalls' --glob '**/@ag-ui/core/**/*.d.ts' . 2>/dev/null | head -60 || \
  echo 'Package not installed in sandbox; inspect the published types instead.'

Repository: TanStack/ai

Length of output: 513


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate chat-params and relevant package files =="
fd -t f -g 'chat-params.ts' .
fd -t f -g 'package.json' packages/ai packages 2>/dev/null | head -40

echo "== chat-params outline =="
ast-grep outline packages/ai/src/utilities/chat-params.ts || true

echo "== chat-params relevant content =="
nl -ba packages/ai/src/utilities/chat-params.ts | sed -n '1,140p'

echo "== usage of assertAGUIMessage / AGUI roles =="
rg -n "assertAGUIMessage|AGUIRole|isAGUIRole|AGUI_ROLES|`@ag-ui/core`" packages/ai/src packages -g '*.{ts,tsx}' | head -100

Repository: TanStack/ai

Length of output: 2860


🌐 Web query:

@ag-ui/core 0.1.1-canary.beta.0 type Role AGUI Role

💡 Result:

In the @ag-ui/core library, the Role type defines the possible participants in the Agent User Interaction (AG-UI) Protocol [1][2]. It is implemented as a union of string literals in TypeScript [1] and a Literal type in Python [2]. The Role type includes the following values: - developer [1][2] - system [1][2] - assistant [1][2] - user [1][2] - tool [1][2] - activity [1][2] - reasoning [1][2] These roles act as a discriminator for message types within the protocol, determining the structure and permitted fields of a message [3][4]. For example, a message with the role "tool" requires a toolCallId field [3], while a "user" role may support multimodal content parts [5]. Regarding the version specified, @ag-ui/core 0.1.1-canary.beta.0 does not appear in the official npm registry version history as of July 31, 2026; the package has active development under the 0.0.x branch [6][7][8]. Users are advised to rely on the latest stable or canary releases documented in the official AG-UI protocol documentation [9][10].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== chat-params.ts relevant content =="
awk '{printf "%5d\t%s\n", NR, $0}' packages/ai/src/utilities/chat-params.ts | sed -n '1,160p'

echo "== usage of assertAGUIMessage / AGUI roles / package version =="
rg -n "assertAGUIMessage|AGUIRole|isAGUIRole|AGUI_ROLES|`@ag-ui/core`" packages/ai/src packages -g '*.{ts,tsx}' | head -120
awk '{printf "%5d\t%s\n", NR, $0}' packages/ai/package.json | sed -n '1,220p'

echo "== JavaScript prototype membership behavior =="
node - <<'JS'
const AGUI_ROLES = {
  developer: true,
  system: true,
  assistant: true,
  user: true,
  tool: true,
  activity: true,
  reasoning: true,
}
for (const value of ['toString', 'valueOf', 'constructor', 'activity', 'unknown']) {
  console.log(JSON.stringify(value), {
    typeof: typeof value,
    in_member: value in AGUI_ROLES,
    hasOwn_member: Object.hasOwn(AGUI_ROLES, value),
    ownKeys: Object.keys(AGUI_ROLES),
  })
}
JS

Repository: TanStack/ai

Length of output: 19770


Use Object.hasOwn for the role membership check.

in checks the prototype chain, so messages with role: 'toString', role: 'valueOf', or role: 'constructor' pass isAGUIRole(). Those values then fall through the switch without any role-specific validation.

🔒 Proposed fix
 function isAGUIRole(value: unknown): value is AGUIRole {
-  return typeof value === 'string' && value in AGUI_ROLES
+  return typeof value === 'string' && Object.hasOwn(AGUI_ROLES, value)
 }
📝 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 AGUI_ROLES: Record<AGUIRole, true> = {
developer: true,
system: true,
assistant: true,
user: true,
tool: true,
activity: true,
reasoning: true,
}
function isAGUIRole(value: unknown): value is AGUIRole {
return typeof value === 'string' && value in AGUI_ROLES
}
const AGUI_ROLES: Record<AGUIRole, true> = {
developer: true,
system: true,
assistant: true,
user: true,
tool: true,
activity: true,
reasoning: true,
}
function isAGUIRole(value: unknown): value is AGUIRole {
return typeof value === 'string' && Object.hasOwn(AGUI_ROLES, value)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/utilities/chat-params.ts` around lines 41 - 53, Update
isAGUIRole to use an own-property membership check with Object.hasOwn against
AGUI_ROLES instead of the in operator, so inherited names such as toString,
valueOf, and constructor are rejected.

Comment on lines +100 to +116
switch (role) {
case 'assistant':
// Both optional: a tool-calling turn carries no text content.
if (value.content !== undefined) {
requireString(value.content, `${at}.content`)
}
if (value.toolCalls !== undefined) {
requireArray(value.toolCalls, `${at}.toolCalls`)
}
break
case 'user':
if (typeof value.content !== 'string' && !Array.isArray(value.content)) {
invalidBody(
`${at}.content must be a string or an array of content parts`,
)
}
break

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find downstream reads of AG-UI toolCalls entries that assume a validated shape.
set -euo pipefail

rg -nP --type=ts -C3 'toolCalls\s*(\?\.|\[)|\.function\.(name|arguments)' packages/ai/src | head -80

Repository: TanStack/ai

Length of output: 6902


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file size/outline =="
wc -l packages/ai/src/utilities/chat-params.ts packages/ai/src/activities/chat/messages.ts
ast-grep outline packages/ai/src/utilities/chat-params.ts packages/ai/src/activities/chat/messages.ts --view compact 2>/dev/null | sed -n '1,180p'

echo "== chat-params relevant slice =="
cat -n packages/ai/src/utilities/chat-params.ts | sed -n '1,180p'

echo "== messages relevant slice =="
cat -n packages/ai/src/activities/chat/messages.ts | sed -n '380,475p'

echo "== search requireArray/isRecord definitions =="
rg -n --type=ts -C2 'function requireArray|const requireArray|requireArray\s*=|function isRecord|const isRecord|export function invalidBody|export function requireString' packages/ai/src

Repository: TanStack/ai

Length of output: 282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== chat-params relevant slice =="
cat -n packages/ai/src/utilities/chat-params.ts | sed -n '1,180p'

echo "== messages relevant slice =="
cat -n packages/ai/src/activities/chat/messages.ts | sed -n '380,475p'

echo "== search requireArray/isRecord definitions =="
rg -n --type=ts -C2 'function requireArray|const requireArray|requireArray\s*=|function isRecord|const isRecord|export function invalidBody|export function requireString' packages/ai/src || true

echo "== behavioral probe for current validation shape access =="
node - <<'JS'
function requireArray(value, at) {
  if (!Array.isArray(value)) throw new TypeError(`${at} must be an array`)
  return value
}
function requireString(value, at) {}

function assertAGUIMessage(value) {}

for (const v of [null, {}, [{id: 1}], [{function: {}}], [{function: {name: 'x'}}]]) {
  const role = 'assistant'
  try {
    if (value.content !== undefined) requireString(value.content, `${role}.content`)
    if (v.toolCalls !== undefined) requireArray(v.toolCalls, `${role}.toolCalls`)
  } catch (error) {
    console.log('validation rejects', JSON.stringify(v), 'error:', error.message)
    continue
  }

  // Downstream shape access modeled from packages/ai/src/activities/chat/messages.ts lines 452/459/460
  try {
    const toolCalls = v.toolCalls
    for (const toolCall of toolCalls) {
      JSON.parse(toolCall.function.arguments)
      toolCall.function.name
      toolCall.function.arguments
    }
    console.log('shape access succeeds', JSON.stringify(v))
  } catch (error) {
    console.log('shape access throws', JSON.stringify(v), 'name:', error.name, 'message:', error.message)
  }
}
JS

Repository: TanStack/ai

Length of output: 12025


Validate each toolCalls object before downstream conversion.

requireArray only checks the parent array, so toolCalls: [{}] or toolCalls: [{function: {}}] passes validation into modelMessageToUIMessage(). That code then reads toolCall.function.arguments, id, and function.name, which throws on malformed entries instead of returning the intended validation error. Add per-element checks for each expected property.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/utilities/chat-params.ts` around lines 100 - 116, Update the
assistant branch in the role validation switch to validate every element of
value.toolCalls after requireArray, checking each expected tool-call property,
including id and function.name/function.arguments, before
modelMessageToUIMessage() can process it; malformed entries must produce the
existing validation error rather than throwing during conversion.

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