-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(core): support Valibot schemas for tools #1399
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,7 @@ | ||
| --- | ||
| "@voltagent/core": minor | ||
| --- | ||
|
|
||
| Let tools use Valibot (and other Standard Schema libraries) for their parameter and output schemas, not just Zod. | ||
|
|
||
| Tool schemas now accept any Standard Schema, so `createTool`/`new Tool` take a Valibot schema and infer the `execute` args from it. Zod keeps working exactly as before. Since Valibot doesn't ship a JSON Schema extension yet, VoltAgent converts Valibot schemas with `@valibot/to-json-schema` (install it alongside `valibot` to use Valibot schemas); Zod-only users don't need it and nothing extra loads for them. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import type { | |
| ToolContent, | ||
| UserContent, | ||
| } from "@ai-sdk/provider-utils"; | ||
| import type { StandardSchemaV1 } from "@standard-schema/spec"; | ||
| import type { AsyncIterableStream } from "@voltagent/internal/utils"; | ||
| import type { TextStreamPart } from "ai"; | ||
| import type { z } from "zod"; | ||
|
|
@@ -242,7 +243,26 @@ export type MessageRole = "user" | "assistant" | "system" | "tool"; | |
| export type BaseMessage = ModelMessage; | ||
|
|
||
| // Schema types | ||
| export type ToolSchema = z.ZodType; | ||
| /** | ||
| * Schema accepted for tool parameters and output. | ||
| * | ||
| * Any Standard Schema library works here (Valibot, ArkType, Effect Schema, ...), | ||
| * and Zod stays fully supported since Zod implements the Standard Schema interface. | ||
| * The AI SDK converts whatever it's given to JSON Schema for the model. | ||
| */ | ||
| export type ToolSchema = z.ZodType | StandardSchemaV1; | ||
|
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. P1: ToolSchema now accepts any StandardSchemaV1, but tool argument/output validation elsewhere (e.g. in agent.ts) still assumes a Zod-style Prompt for AI agents |
||
|
|
||
| /** | ||
| * Infer the parsed output type of a tool schema. | ||
| * | ||
| * Zod schemas keep going through `z.infer` for exact backward compatibility; | ||
| * other Standard Schema libraries resolve via their inferred output type. | ||
| */ | ||
| export type InferSchema<T> = T extends z.ZodType | ||
| ? z.infer<T> | ||
| : T extends StandardSchemaV1 | ||
| ? StandardSchemaV1.InferOutput<T> | ||
| : unknown; | ||
|
Comment on lines
+253
to
+265
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. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline packages/core/src/agent/agent.ts --match 'validateToolOutput|createToolRoutingCallTool'
rg -n -C 5 'safeParse|~standard|validateToolOutput|createToolRoutingCallTool' \
packages/core/src/agent/agent.ts \
packages/core/src/tool/standard-schema.tsRepository: VoltAgent/voltagent Length of output: 11936 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- imports and tool/schema definitions ---'
sed -n '1,140p' packages/core/src/agent/agent.ts
sed -n '245,275p' packages/core/src/agent/providers/base/types.ts
sed -n '1,150p' packages/core/src/tool/standard-schema.ts
printf '%s\n' '--- output validation implementation ---'
sed -n '6395,6435p' packages/core/src/agent/agent.ts
sed -n '6525,6555p' packages/core/src/agent/agent.ts
sed -n '6625,6720p' packages/core/src/agent/agent.ts
printf '%s\n' '--- routed argument validation implementation ---'
sed -n '7045,7185p' packages/core/src/agent/agent.ts
printf '%s\n' '--- schema preparation and tool construction call sites ---'
rg -n -C 8 'prepare.*Schema|standardSchema|convert.*Schema|parameters:|inputSchema|outputSchema|createTool\\(' \
packages/core/src/agent packages/core/src/tool packages/core/src/agent/providers/baseRepository: VoltAgent/voltagent Length of output: 20625 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- tool types and createTool ---'
rg -n -C 10 'export (type|interface) Tool|function createTool|const createTool|parameters\\??:|outputSchema' packages/core/src/tool packages/core/src/agent/providers/base/types.ts
fd -i 'tool' packages/core/src/tool --type f | head -80
printf '%s\n' '--- normalization call sites and prepared-tool flow ---'
rg -n -C 12 'normalizeToolSchemasForModel|toModelToolSchema|prepare.*Tool|prepared.*Tools|inputSchema' packages/core/src/agent packages/core/src/toolRepository: VoltAgent/voltagent Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- prepared tool creation and normalization ---'
rg -n 'normalizeToolSchemasForModel|getToolsFor|createTools|prepareTools|inputSchema' packages/core/src/agent/agent.ts packages/core/src/tool/manager/ToolManager.ts
sed -n '6180,6325p' packages/core/src/agent/agent.ts
sed -n '35,95p' packages/core/src/tool/manager/ToolManager.ts
printf '%s\n' '--- existing Standard Schema tests ---'
sed -n '1,260p' packages/core/src/tool/standard-schema.spec.ts
printf '%s\n' '--- Standard Schema dependency/type details ---'
rg -n -C 5 '`@standard-schema/spec`|StandardSchemaV1|~standard' packages/core package.json pnpm-lock.yamlRepository: VoltAgent/voltagent Length of output: 29471 Validate Standard Schema values in all tool execution paths.
Add a validation adapter for Zod 🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Tool execution context containing all tool-specific metadata. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import { asSchema } from "ai"; | ||
| import * as v from "valibot"; | ||
| import { describe, expect, expectTypeOf, it } from "vitest"; | ||
| import { z } from "zod"; | ||
| import { createTool } from "./index"; | ||
| import { normalizeToolSchemasForModel, toModelToolSchema } from "./standard-schema"; | ||
|
|
||
| describe("Valibot tool schemas", () => { | ||
| it("accepts a Valibot schema and infers execute args from it", async () => { | ||
| const executed: Array<{ city: string; days?: number }> = []; | ||
|
|
||
| const tool = createTool({ | ||
| name: "getWeather", | ||
| description: "Get the weather for a city", | ||
| parameters: v.object({ | ||
| city: v.string(), | ||
| days: v.optional(v.number()), | ||
| }), | ||
| execute: async (args) => { | ||
| // Types come from the Valibot schema, not `any`. | ||
| expectTypeOf(args.city).toEqualTypeOf<string>(); | ||
| expectTypeOf(args.days).toEqualTypeOf<number | undefined>(); | ||
| executed.push(args); | ||
| return { forecast: "sunny" }; | ||
| }, | ||
| }); | ||
|
|
||
| expect(tool.name).toBe("getWeather"); | ||
| expect(tool.parameters).toBeDefined(); | ||
|
|
||
| await tool.execute?.({ city: "Paris", days: 3 }); | ||
| expect(executed).toEqual([{ city: "Paris", days: 3 }]); | ||
| }); | ||
|
|
||
| it("converts a Valibot schema to the same JSON Schema as the Zod equivalent", async () => { | ||
| const valibotSchema = v.object({ | ||
| city: v.string(), | ||
| days: v.optional(v.number()), | ||
| }); | ||
| const zodSchema = z.object({ | ||
| city: z.string(), | ||
| days: z.number().optional(), | ||
| }); | ||
|
|
||
| const converted = await toModelToolSchema(valibotSchema); | ||
| const valibotJson = asSchema(converted).jsonSchema; | ||
| const zodJson = asSchema(zodSchema).jsonSchema; | ||
|
|
||
| expect(valibotJson.type).toBe("object"); | ||
| expect(valibotJson.properties).toEqual({ | ||
| city: { type: "string" }, | ||
| days: { type: "number" }, | ||
| }); | ||
| expect(valibotJson.required).toEqual(["city"]); | ||
| // Same shape the model would see for the Zod version. | ||
| expect(valibotJson.properties).toEqual(zodJson.properties); | ||
| expect(valibotJson.required).toEqual(zodJson.required); | ||
| }); | ||
|
|
||
| it("keeps Valibot validation on the converted schema", async () => { | ||
| const converted = await toModelToolSchema( | ||
| v.object({ city: v.string(), days: v.optional(v.number()) }), | ||
| ); | ||
| const schema = asSchema(converted); | ||
|
|
||
| const ok = await schema.validate?.({ city: "Paris", days: 3 }); | ||
| expect(ok).toEqual({ success: true, value: { city: "Paris", days: 3 } }); | ||
|
|
||
| const bad = await schema.validate?.({ city: 123 }); | ||
| expect(bad?.success).toBe(false); | ||
| }); | ||
|
|
||
| it("leaves Zod schemas untouched", async () => { | ||
| const zodSchema = z.object({ query: z.string() }); | ||
| const result = await toModelToolSchema(zodSchema); | ||
| expect(result).toBe(zodSchema); | ||
| }); | ||
|
|
||
| it("normalizes only Valibot entries in a prepared tool map", async () => { | ||
| const zodSchema = z.object({ query: z.string() }); | ||
| const valibotSchema = v.object({ query: v.string() }); | ||
|
|
||
| const tools: Record<string, { inputSchema?: unknown }> = { | ||
| zodTool: { inputSchema: zodSchema }, | ||
| valibotTool: { inputSchema: valibotSchema }, | ||
| providerTool: {}, | ||
| }; | ||
|
|
||
| await normalizeToolSchemasForModel(tools); | ||
|
|
||
| expect(tools.zodTool.inputSchema).toBe(zodSchema); | ||
| expect(tools.valibotTool.inputSchema).not.toBe(valibotSchema); | ||
| expect(asSchema(tools.valibotTool.inputSchema).jsonSchema.properties).toEqual({ | ||
| query: { type: "string" }, | ||
| }); | ||
| expect(tools.providerTool.inputSchema).toBeUndefined(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import type { StandardSchemaV1 } from "@standard-schema/spec"; | ||
| import { safeStringify } from "@voltagent/internal/utils"; | ||
| import { jsonSchema } from "ai"; | ||
|
|
||
| /** | ||
| * Tool schemas can come from any Standard Schema library, not just Zod. The AI SDK | ||
| * turns Zod schemas and any Standard Schema that ships a JSON Schema extension into | ||
| * JSON Schema on its own, so those pass straight through. | ||
| * | ||
| * Valibot is the exception: as of v1 its `~standard` entry only exposes `validate`, | ||
| * with no JSON Schema, so the model would never see the parameter shape. For those we | ||
| * convert to JSON Schema with `@valibot/to-json-schema` (an optional peer dependency) | ||
| * and keep Valibot's own validator for argument checking. | ||
| */ | ||
|
|
||
| type ToJsonSchema = (schema: unknown, config?: Record<string, unknown>) => Record<string, unknown>; | ||
|
|
||
| let converterPromise: Promise<ToJsonSchema> | undefined; | ||
|
|
||
| function isStandardSchema(schema: unknown): schema is StandardSchemaV1 { | ||
| return typeof schema === "object" && schema !== null && "~standard" in schema; | ||
| } | ||
|
|
||
| /** | ||
| * True when a schema needs VoltAgent to convert it before the AI SDK sees it. | ||
| * Zod and JSON-Schema-capable Standard Schemas are left untouched. | ||
| */ | ||
| function needsValibotConversion(schema: unknown): schema is StandardSchemaV1 { | ||
| if (!isStandardSchema(schema)) return false; | ||
| const standard = schema["~standard"] as StandardSchemaV1.Props & { jsonSchema?: unknown }; | ||
| if (standard.vendor === "zod") return false; | ||
| if ("jsonSchema" in standard) return false; | ||
| return standard.vendor === "valibot"; | ||
| } | ||
|
|
||
| async function loadValibotConverter(): Promise<ToJsonSchema> { | ||
| if (!converterPromise) { | ||
| converterPromise = import("@valibot/to-json-schema") | ||
| .then((mod) => mod.toJsonSchema as unknown as ToJsonSchema) | ||
| .catch(() => { | ||
| throw new Error( | ||
| "A Valibot schema was passed to a tool, but '@valibot/to-json-schema' is not installed. " + | ||
| "Install it to use Valibot schemas: `npm install @valibot/to-json-schema`.", | ||
| ); | ||
| }); | ||
| } | ||
| return converterPromise; | ||
| } | ||
|
|
||
| /** | ||
| * Normalize a tool schema into a form the AI SDK can hand to the model. Zod and other | ||
| * schemas pass through unchanged; Valibot schemas are converted to a JSON Schema that | ||
| * keeps Valibot's runtime validation. | ||
| */ | ||
| export async function toModelToolSchema(schema: unknown): Promise<unknown> { | ||
| if (!needsValibotConversion(schema)) { | ||
| return schema; | ||
| } | ||
|
|
||
| const toJsonSchema = await loadValibotConverter(); | ||
| const standard = schema["~standard"]; | ||
|
|
||
| return jsonSchema(toJsonSchema(schema, { errorMode: "ignore" }), { | ||
| validate: async (value) => { | ||
| const result = await standard.validate(value); | ||
| if (result.issues) { | ||
| return { | ||
| success: false, | ||
| error: new Error(`Tool argument validation failed: ${safeStringify(result.issues)}`), | ||
| }; | ||
| } | ||
| return { success: true, value: result.value }; | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Rewrite the `inputSchema` of every entry in a prepared tool map so Valibot-defined | ||
| * tools produce a JSON Schema for the model. Mutates in place; provider tools and | ||
| * tools without a schema are left alone. | ||
| */ | ||
| export async function normalizeToolSchemasForModel( | ||
| tools: Record<string, { inputSchema?: unknown }>, | ||
| ): Promise<void> { | ||
| await Promise.all( | ||
| Object.values(tools).map(async (tool) => { | ||
| if (tool && "inputSchema" in tool && needsValibotConversion(tool.inputSchema)) { | ||
|
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. P2: A Valibot Prompt for AI agents |
||
| tool.inputSchema = await toModelToolSchema(tool.inputSchema); | ||
| } | ||
| }), | ||
| ); | ||
| } | ||
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.
P2: Validate-only Standard Schema implementations are accepted by
ToolSchema, but the preparation path leaves non-Valibot schemas unchanged, so the AI SDK cannot derive their model shape. Qualify this as JSON-Schema-capable Standard Schema support or add normalization for every accepted schema.Prompt for AI agents