Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -424,14 +417,40 @@ 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 (
$defs && isJsonSchemaObject($defs) ? { ...rest, definitions: $defs as JSONSchema7["definitions"] } : rest
) 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
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/test/tool/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
"",
Expand Down
8 changes: 1 addition & 7 deletions packages/plugin/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading