fix(tool): preserve custom tool arg descriptions (#27750)
Co-authored-by: khimaros <231498+khimaros@users.noreply.github.com>
This commit is contained in:
@@ -145,7 +145,14 @@ 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
|
||||||
const jsonSchema = zodParams ? zodJsonSchema(zodParams) : legacyJsonSchema(entries)
|
// 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 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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { afterEach, describe, expect } from "bun:test"
|
import { afterEach, describe, expect } from "bun:test"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { pathToFileURL } from "url"
|
import { fileURLToPath, pathToFileURL } from "url"
|
||||||
import { Effect, Layer, Result, Schema } from "effect"
|
import { Effect, Layer, Result, Schema } from "effect"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { ToolRegistry } from "@/tool/registry"
|
import { ToolRegistry } from "@/tool/registry"
|
||||||
@@ -263,6 +263,71 @@ describe("tool.registry", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.instance("preserves Zod arg descriptions from config-scoped plugin packages", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const test = yield* TestInstance
|
||||||
|
const opencode = path.join(test.directory, ".opencode")
|
||||||
|
const customTools = path.join(opencode, "tools")
|
||||||
|
const plugin = path.join(opencode, "node_modules", "@opencode-ai", "plugin")
|
||||||
|
yield* Effect.promise(() => fs.mkdir(path.join(plugin, "dist"), { recursive: true }))
|
||||||
|
yield* Effect.promise(() => fs.mkdir(customTools, { recursive: true }))
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
fs.cp(path.dirname(fileURLToPath(import.meta.resolve("zod"))), path.join(opencode, "node_modules", "zod"), {
|
||||||
|
dereference: true,
|
||||||
|
recursive: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
Bun.write(
|
||||||
|
path.join(plugin, "package.json"),
|
||||||
|
JSON.stringify({ name: "@opencode-ai/plugin", type: "module", exports: { ".": "./dist/index.js" } }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
Bun.write(
|
||||||
|
path.join(plugin, "dist", "index.js"),
|
||||||
|
[
|
||||||
|
"import { z } from 'zod'",
|
||||||
|
"export function tool(input) {",
|
||||||
|
" return { ...input, jsonSchema: z.toJSONSchema(z.object(input.args), { target: 'draft-7', io: 'input' }) }",
|
||||||
|
"}",
|
||||||
|
"tool.schema = z",
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
Bun.write(
|
||||||
|
path.join(customTools, "addition.ts"),
|
||||||
|
[
|
||||||
|
'import { tool } from "@opencode-ai/plugin"',
|
||||||
|
"export default tool({",
|
||||||
|
" description: 'Use this tool to add two numbers and return their sum.',",
|
||||||
|
" args: {",
|
||||||
|
" left: tool.schema.number().describe('The first number to add'),",
|
||||||
|
" right: tool.schema.number().describe('The second number to add'),",
|
||||||
|
" },",
|
||||||
|
" execute: async (args) => `${args.left} + ${args.right} = ${args.left + args.right}`,",
|
||||||
|
"})",
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const loaded = (yield* registry.all()).find((tool) => tool.id === "addition")
|
||||||
|
if (!loaded) throw new Error("custom addition tool was not loaded")
|
||||||
|
|
||||||
|
expect(ToolJsonSchema.fromTool(loaded)).toMatchObject({
|
||||||
|
properties: {
|
||||||
|
left: { type: "number", description: "The first number to add" },
|
||||||
|
right: { type: "number", description: "The second number to add" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
20_000,
|
||||||
|
)
|
||||||
|
|
||||||
it.instance("preserves attachments from structured custom tool results", () =>
|
it.instance("preserves attachments from structured custom tool results", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
|
|||||||
@@ -48,7 +48,13 @@ 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 input
|
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" }),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tool.schema = z
|
tool.schema = z
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user