-
-
Notifications
You must be signed in to change notification settings - Fork 293
refactor(ai): remove zod from the dependency graph #1038
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| --- | ||
| '@tanstack/ai': minor | ||
| --- | ||
|
|
||
| Remove zod from `@tanstack/ai`'s dependency graph entirely. | ||
|
|
||
| `@ag-ui/core` is bumped to `0.1.1-canary.beta.0`, which drops zod from its | ||
| runtime dependencies and declares it as an optional peer instead. Previously | ||
| every `@tanstack/ai` install pulled zod in transitively through it. | ||
|
|
||
| `chatParamsFromRequest` / `chatParamsFromRequestBody` were the only zod | ||
| consumers in this package β they validated request bodies with AG-UI's | ||
| `RunAgentInputSchema`. They now validate the same `RunAgentInput` contract | ||
| structurally, so `@tanstack/ai` ships with no schema-validation runtime at all | ||
| and neither requires nor suggests zod. | ||
|
|
||
| No API change: both helpers keep their signatures, still reject non-conforming | ||
| bodies with a migration-pointing `AGUIError` (`chatParamsFromRequest` still | ||
| throws a 400 `Response`), and still carry TanStack's canonical `parts` field | ||
| through on messages. Validation errors 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 is simply no longer | ||
| installed on your behalf. If you relied on getting zod transitively without | ||
| declaring it, add it explicitly: `npm install zod`. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,10 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Message as AGUIMessage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ResumeEntry as AGUIResumeEntry, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Role as AGUIRole, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '@ag-ui/core' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AnyTool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSONSchema, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -29,6 +34,162 @@ function isValidParts(value: unknown): value is Array<{ type: string }> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Keyed by `AGUIRole` so a role added upstream fails to compile here until it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * is handled, rather than silently falling through as an unknown role. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 -100Repository: TanStack/ai Length of output: 2860 π Web query:
π‘ Result: In the 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),
})
}
JSRepository: TanStack/ai Length of output: 19770 Use
π 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
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isRecord(value: unknown): value is Record<string, unknown> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return typeof value === 'object' && value !== null && !Array.isArray(value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Reject the request body, pointing at the migration guide. Mirrors the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * message the previous `RunAgentInputSchema.safeParse` failure produced. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function invalidBody(reason: string): never { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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: ${reason}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function requireString(value: unknown, at: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof value !== 'string') invalidBody(`${at} must be a string`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function requireArray(value: unknown, at: string): Array<unknown> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!Array.isArray(value)) invalidBody(`${at} must be an array`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Assert one AG-UI `Message`, discriminating on `role` exactly as the upstream | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `MessageSchema` discriminated union does. The record view is retained on the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * asserted type so callers can still inspect non-AG-UI extras like `parts`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function assertAGUIMessage( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: Record<string, unknown>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| at: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): asserts value is Record<string, unknown> & AGUIMessage { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireString(value.id, `${at}.id`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const role = value.role | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isAGUIRole(role)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| invalidBody( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `${at}.role must be one of ${Object.keys(AGUI_ROLES).join(' | ')}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+100
to
+116
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 -80Repository: 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/srcRepository: 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)
}
}
JSRepository: TanStack/ai Length of output: 12025 Validate each
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'tool': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireString(value.content, `${at}.content`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireString(value.toolCallId, `${at}.toolCallId`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'activity': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireString(value.activityType, `${at}.activityType`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isRecord(value.content)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| invalidBody(`${at}.content must be an object`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'developer': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'system': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'reasoning': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireString(value.content, `${at}.content`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function validateMessage(value: unknown, index: number): AGUIMessage { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const at = `messages[${index}]` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isRecord(value)) invalidBody(`${at} must be an object`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertAGUIMessage(value, at) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // `parts` is TanStack's canonical extra, carried through so the UIMessage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // path inside `chat()` can use it. Keep it only when it holds recognized | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // part types β the previous schema-based path dropped `parts` during parse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // and re-attached it from the raw body behind this same check. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ('parts' in value && !isValidParts(value.parts)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const withoutParts = { ...value } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Reflect.deleteProperty(withoutParts, 'parts') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return withoutParts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function validateTool( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: unknown, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index: number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): { name: string; description: string; parameters: JSONSchema } { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const at = `tools[${index}]` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isRecord(value)) invalidBody(`${at} must be an object`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: requireString(value.name, `${at}.name`), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: requireString(value.description, `${at}.description`), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Upstream `ToolSchema` types this as optional `any`; it reaches the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // provider as a raw JSON Schema either way. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parameters: value.parameters as JSONSchema, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function validateContext(value: unknown, index: number): AGUIContext { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const at = `context[${index}]` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isRecord(value)) invalidBody(`${at} must be an object`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: requireString(value.description, `${at}.description`), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: requireString(value.value, `${at}.value`), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function validateResumeEntry(value: unknown, index: number): AGUIResumeEntry { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const at = `resume[${index}]` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isRecord(value)) invalidBody(`${at} must be an object`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const status = value.status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (status !== 'resolved' && status !== 'cancelled') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| invalidBody(`${at}.status must be "resolved" or "cancelled"`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const entry: AGUIResumeEntry = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interruptId: requireString(value.interruptId, `${at}.interruptId`), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Omit the key entirely when absent, matching the optional-field shape the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // schema produced. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value.payload !== undefined) entry.payload = value.payload | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return entry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Parse and validate an HTTP request body as an AG-UI `RunAgentInput`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -37,11 +198,14 @@ function isValidParts(value: unknown): value is Array<{ type: string }> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `convertMessagesToModelMessages` handles AG-UI fan-out dedup and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * reasoning/activity/developer-role normalization internally. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Validated structurally against the AG-UI `RunAgentInput` contract without a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * schema library, so this package pulls in no validation runtime of its own. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @throws An error with a migration-pointing message when the body does | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * not conform to AG-UI `RunAgentInputSchema`. Surface this as a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * not conform to AG-UI `RunAgentInput`. 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -57,55 +221,41 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| context: Array<AGUIContext> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aguiContext: 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}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isRecord(body)) invalidBody('body must be a JSON object') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const threadId = requireString(body.threadId, 'threadId') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const runId = requireString(body.runId, 'runId') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parentRunId = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body.parentRunId === undefined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? undefined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : requireString(body.parentRunId, 'parentRunId') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const messages = requireArray(body.messages, 'messages').map(validateMessage) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tools = requireArray(body.tools, 'tools').map(validateTool) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const aguiContext = requireArray(body.context, 'context').map(validateContext) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resume = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body.resume === undefined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? undefined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : requireArray(body.resume, 'resume').map(validateResumeEntry) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (body.forwardedProps !== undefined && !isRecord(body.forwardedProps)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| invalidBody('forwardedProps must be an object') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = parseResult.data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const aguiContext = parsed.context | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // AG-UI Zod uses `.strip()` so extra fields like `parts` on messages are | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // dropped during parse. We re-attach them from the original body so the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // existing UIMessage path inside `chat()` can use them directly. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rawMessages = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (body as { messages?: Array<Record<string, unknown>> }).messages ?? [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const messages = parsed.messages.map((m, i) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const raw = rawMessages[i] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raw && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof raw === 'object' && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'parts' in raw && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isValidParts(raw.parts) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { ...m, parts: raw.parts } as UIMessage | ModelMessage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return m as ModelMessage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Promise.resolve({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| threadId: parsed.threadId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runId: parsed.runId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parentRunId: parsed.parentRunId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tools: parsed.tools as Array<{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parameters: JSONSchema | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| forwardedProps: (parsed.forwardedProps ?? {}) as Record<string, unknown>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state: parsed.state, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resume: parsed.resume, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Unknown top-level fields (e.g. a legacy `cursor`) are dropped by | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // construction: only the fields below are copied onto the result. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages: messages as Array<UIMessage | ModelMessage>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| threadId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parentRunId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tools, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| forwardedProps: (body.forwardedProps ?? {}) as Record<string, unknown>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state: body.state, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resume: resume as Array<RunAgentResumeItem> | undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| context: aguiContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aguiContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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-UIRunAgentInputSchema". Readers see two different mechanisms in one page. Reword those two lines to describe structural validation of theRunAgentInputcontract.π€ Prompt for AI Agents