mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-17 18:12:42 +00:00
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 allZod = entries.every((entry) => isZodType(entry[1]))
|
||||
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
|
||||
? Schema.declare<unknown>((u): u is unknown => zodParams.safeParse(u).success)
|
||||
: Schema.Unknown
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { pathToFileURL } from "url"
|
||||
import { fileURLToPath, pathToFileURL } from "url"
|
||||
import { Effect, Layer, Result, Schema } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
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", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
|
||||
@@ -48,7 +48,13 @@ export function tool<Args extends z.ZodRawShape>(input: {
|
||||
args: Args
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user