fix(tool): bridge custom tool zod metadata (#27770)

This commit is contained in:
Aiden Cline
2026-05-15 14:50:21 -05:00
committed by GitHub
parent 0df2f5b45f
commit 48122b31cc
3 changed files with 31 additions and 18 deletions

View File

@@ -145,14 +145,7 @@ export const layer: Layer.Layer<
const entries = Object.entries(def.args)
const allZod = entries.every((entry) => isZodType(entry[1]))
const zodParams = allZod ? z.object(def.args) : undefined
// Newer @opencode-ai/plugin versions precompute JSON Schema with the
// Zod instance that owns arg metadata. Fall back for older/manual
// custom tools that only expose raw Zod args.
const jsonSchema = zodParams
? isJsonSchemaDefinition(def.jsonSchema)
? (def.jsonSchema as JSONSchema7)
: zodJsonSchema(zodParams)
: legacyJsonSchema(entries)
const jsonSchema = zodParams ? zodJsonSchema(zodParams) : legacyJsonSchema(entries)
const parameters = zodParams
? Schema.declare<unknown>((u): u is unknown => zodParams.safeParse(u).success)
: Schema.Unknown
@@ -424,7 +417,7 @@ function legacyJsonSchema(entries: [string, unknown][]): JSONSchema7 {
}
function zodJsonSchema(schema: z.ZodType): JSONSchema7 {
const result = normalizeZodJsonSchema(z.toJSONSchema(schema, { io: "input" }))
const result = normalizeZodJsonSchema(z.toJSONSchema(schema, { io: "input", metadata: zodMetadataRegistry(schema) }))
if (!isJsonSchemaObject(result)) throw new Error("plugin tool Zod schema produced a non-object JSON Schema")
const { $defs, ...rest } = result
return (
@@ -432,6 +425,32 @@ function zodJsonSchema(schema: z.ZodType): JSONSchema7 {
) as JSONSchema7
}
function zodMetadataRegistry(schema: z.ZodType) {
const registry = z.registry<Record<string, unknown>>()
const seen = new WeakSet<object>()
const collect = (value: unknown) => {
if (typeof value !== "object" || value === null) return
if (seen.has(value)) return
seen.add(value)
if (isZodType(value)) {
const metadata = typeof value.meta === "function" ? value.meta() : undefined
const description = typeof value.description === "string" ? value.description : undefined
const merged = {
...(metadata && typeof metadata === "object" ? metadata : {}),
...(description ? { description } : {}),
}
if (Object.keys(merged).length) registry.add(value, merged)
collect(value._zod.def)
return
}
for (const item of Object.values(value)) collect(item)
}
collect(schema)
return registry
}
function normalizeZodJsonSchema(value: unknown): unknown {
if (Array.isArray(value)) return value.map((item) => normalizeZodJsonSchema(item))
if (typeof value !== "object" || value === null) return value

View File

@@ -264,7 +264,7 @@ describe("tool.registry", () => {
)
it.instance(
"preserves Zod arg descriptions from config-scoped plugin packages",
"preserves Zod arg descriptions from older config-scoped plugin packages",
() =>
Effect.gen(function* () {
const test = yield* TestInstance
@@ -291,7 +291,7 @@ describe("tool.registry", () => {
[
"import { z } from 'zod'",
"export function tool(input) {",
" return { ...input, jsonSchema: z.toJSONSchema(z.object(input.args), { target: 'draft-7', io: 'input' }) }",
" return input",
"}",
"tool.schema = z",
"",

View File

@@ -48,13 +48,7 @@ export function tool<Args extends z.ZodRawShape>(input: {
args: Args
execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<ToolResult>
}) {
return {
...input,
// Generate JSON Schema here with the same Zod instance that created
// `tool.schema` args. Zod metadata such as `.describe()` is stored in a
// module-local registry, so converting later from opencode can lose it.
jsonSchema: z.toJSONSchema(z.object(input.args), { target: "draft-7", io: "input" }),
}
return input
}
tool.schema = z