fix(tool): bridge custom tool zod metadata (#27770)
This commit is contained in:
@@ -145,14 +145,7 @@ export const layer: Layer.Layer<
|
|||||||
const entries = Object.entries(def.args)
|
const entries = Object.entries(def.args)
|
||||||
const allZod = entries.every((entry) => isZodType(entry[1]))
|
const allZod = entries.every((entry) => isZodType(entry[1]))
|
||||||
const zodParams = allZod ? z.object(def.args) : undefined
|
const zodParams = allZod ? z.object(def.args) : undefined
|
||||||
// Newer @opencode-ai/plugin versions precompute JSON Schema with the
|
const jsonSchema = zodParams ? zodJsonSchema(zodParams) : legacyJsonSchema(entries)
|
||||||
// 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 parameters = zodParams
|
const parameters = zodParams
|
||||||
? Schema.declare<unknown>((u): u is unknown => zodParams.safeParse(u).success)
|
? Schema.declare<unknown>((u): u is unknown => zodParams.safeParse(u).success)
|
||||||
: Schema.Unknown
|
: Schema.Unknown
|
||||||
@@ -424,7 +417,7 @@ function legacyJsonSchema(entries: [string, unknown][]): JSONSchema7 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function zodJsonSchema(schema: z.ZodType): 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")
|
if (!isJsonSchemaObject(result)) throw new Error("plugin tool Zod schema produced a non-object JSON Schema")
|
||||||
const { $defs, ...rest } = result
|
const { $defs, ...rest } = result
|
||||||
return (
|
return (
|
||||||
@@ -432,6 +425,32 @@ function zodJsonSchema(schema: z.ZodType): JSONSchema7 {
|
|||||||
) as 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 {
|
function normalizeZodJsonSchema(value: unknown): unknown {
|
||||||
if (Array.isArray(value)) return value.map((item) => normalizeZodJsonSchema(item))
|
if (Array.isArray(value)) return value.map((item) => normalizeZodJsonSchema(item))
|
||||||
if (typeof value !== "object" || value === null) return value
|
if (typeof value !== "object" || value === null) return value
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ describe("tool.registry", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"preserves Zod arg descriptions from config-scoped plugin packages",
|
"preserves Zod arg descriptions from older config-scoped plugin packages",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
@@ -291,7 +291,7 @@ describe("tool.registry", () => {
|
|||||||
[
|
[
|
||||||
"import { z } from 'zod'",
|
"import { z } from 'zod'",
|
||||||
"export function tool(input) {",
|
"export function tool(input) {",
|
||||||
" return { ...input, jsonSchema: z.toJSONSchema(z.object(input.args), { target: 'draft-7', io: 'input' }) }",
|
" return input",
|
||||||
"}",
|
"}",
|
||||||
"tool.schema = z",
|
"tool.schema = z",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -48,13 +48,7 @@ export function tool<Args extends z.ZodRawShape>(input: {
|
|||||||
args: Args
|
args: Args
|
||||||
execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<ToolResult>
|
execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<ToolResult>
|
||||||
}) {
|
}) {
|
||||||
return {
|
return input
|
||||||
...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" }),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tool.schema = z
|
tool.schema = z
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user