diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 8d4dd5440a..879855c366 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -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((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>() + const seen = new WeakSet() + 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 diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index ce7f89b35b..c426704295 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -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", "", diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/tool.ts index daf6b0bbd7..b8a634c796 100644 --- a/packages/plugin/src/tool.ts +++ b/packages/plugin/src/tool.ts @@ -48,13 +48,7 @@ export function tool(input: { args: Args execute(args: z.infer>, context: ToolContext): Promise }) { - 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