Merge pull request #2372 from code-yeongyu/fix/issue-2314
fix(plugin): preserve cross-zod tool arg metadata
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { afterEach, describe, expect, it } from "bun:test"
|
||||
import { cpSync, mkdtempSync, rmSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { dirname, join } from "node:path"
|
||||
import { pathToFileURL } from "node:url"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import { normalizeToolArgSchemas } from "./normalize-tool-arg-schemas"
|
||||
|
||||
const tempDirectories: string[] = []
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null
|
||||
}
|
||||
|
||||
function getNestedRecord(record: Record<string, unknown>, key: string): Record<string, unknown> | undefined {
|
||||
const value = record[key]
|
||||
return isRecord(value) ? value : undefined
|
||||
}
|
||||
|
||||
async function loadSeparateHostZodModule(): Promise<typeof import("zod")> {
|
||||
const pluginPackageDirectory = dirname(Bun.resolveSync("@opencode-ai/plugin/package.json", import.meta.dir))
|
||||
const sourceZodDirectory = join(pluginPackageDirectory, "node_modules", "zod")
|
||||
const tempDirectory = mkdtempSync(join(tmpdir(), "omo-host-zod-"))
|
||||
const copiedZodDirectory = join(tempDirectory, "zod")
|
||||
|
||||
cpSync(sourceZodDirectory, copiedZodDirectory, { recursive: true })
|
||||
tempDirectories.push(tempDirectory)
|
||||
|
||||
return await import(pathToFileURL(join(copiedZodDirectory, "index.js")).href)
|
||||
}
|
||||
|
||||
function serializeWithHostZod(
|
||||
hostZod: typeof import("zod"),
|
||||
args: Record<string, object>,
|
||||
): Record<string, unknown> {
|
||||
return hostZod.z.toJSONSchema(Reflect.apply(hostZod.z.object, hostZod.z, [args]))
|
||||
}
|
||||
|
||||
describe("normalizeToolArgSchemas", () => {
|
||||
afterEach(() => {
|
||||
for (const tempDirectory of tempDirectories.splice(0)) {
|
||||
rmSync(tempDirectory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("preserves nested descriptions and metadata across zod instances", async () => {
|
||||
// given
|
||||
const hostZod = await loadSeparateHostZodModule()
|
||||
const toolDefinition = tool({
|
||||
description: "Search tool",
|
||||
args: {
|
||||
filters: tool.schema
|
||||
.object({
|
||||
query: tool.schema
|
||||
.string()
|
||||
.describe("Free-text search query")
|
||||
.meta({ title: "Query", examples: ["issue 2314"] }),
|
||||
})
|
||||
.describe("Filter options")
|
||||
.meta({ title: "Filters" }),
|
||||
},
|
||||
async execute(): Promise<string> {
|
||||
return "ok"
|
||||
},
|
||||
})
|
||||
|
||||
// when
|
||||
const beforeSchema = serializeWithHostZod(hostZod, toolDefinition.args)
|
||||
const beforeProperties = getNestedRecord(beforeSchema, "properties")
|
||||
const beforeFilters = beforeProperties ? getNestedRecord(beforeProperties, "filters") : undefined
|
||||
const beforeFilterProperties = beforeFilters ? getNestedRecord(beforeFilters, "properties") : undefined
|
||||
const beforeQuery = beforeFilterProperties ? getNestedRecord(beforeFilterProperties, "query") : undefined
|
||||
|
||||
normalizeToolArgSchemas(toolDefinition)
|
||||
|
||||
const afterSchema = serializeWithHostZod(hostZod, toolDefinition.args)
|
||||
const afterProperties = getNestedRecord(afterSchema, "properties")
|
||||
const afterFilters = afterProperties ? getNestedRecord(afterProperties, "filters") : undefined
|
||||
const afterFilterProperties = afterFilters ? getNestedRecord(afterFilters, "properties") : undefined
|
||||
const afterQuery = afterFilterProperties ? getNestedRecord(afterFilterProperties, "query") : undefined
|
||||
|
||||
// then
|
||||
expect(beforeFilters?.description).toBeUndefined()
|
||||
expect(beforeFilters?.title).toBeUndefined()
|
||||
expect(beforeQuery?.description).toBeUndefined()
|
||||
expect(beforeQuery?.title).toBeUndefined()
|
||||
expect(beforeQuery?.examples).toBeUndefined()
|
||||
|
||||
expect(afterFilters?.description).toBe("Filter options")
|
||||
expect(afterFilters?.title).toBe("Filters")
|
||||
expect(afterQuery?.description).toBe("Free-text search query")
|
||||
expect(afterQuery?.title).toBe("Query")
|
||||
expect(afterQuery?.examples).toEqual(["issue 2314"])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import type { ToolDefinition } from "@opencode-ai/plugin"
|
||||
|
||||
type ToolArgSchema = ToolDefinition["args"][string]
|
||||
|
||||
type SchemaWithJsonSchemaOverride = ToolArgSchema & {
|
||||
_zod: ToolArgSchema["_zod"] & {
|
||||
toJSONSchema?: () => unknown
|
||||
}
|
||||
}
|
||||
|
||||
function stripRootJsonSchemaFields(jsonSchema: Record<string, unknown>): Record<string, unknown> {
|
||||
const { $schema: _schema, ...rest } = jsonSchema
|
||||
return rest
|
||||
}
|
||||
|
||||
function attachJsonSchemaOverride(schema: SchemaWithJsonSchemaOverride): void {
|
||||
if (schema._zod.toJSONSchema) {
|
||||
return
|
||||
}
|
||||
|
||||
schema._zod.toJSONSchema = (): Record<string, unknown> => {
|
||||
const originalOverride = schema._zod.toJSONSchema
|
||||
delete schema._zod.toJSONSchema
|
||||
|
||||
try {
|
||||
return stripRootJsonSchemaFields(tool.schema.toJSONSchema(schema))
|
||||
} finally {
|
||||
schema._zod.toJSONSchema = originalOverride
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeToolArgSchemas<TDefinition extends Pick<ToolDefinition, "args">>(
|
||||
toolDefinition: TDefinition,
|
||||
): TDefinition {
|
||||
for (const schema of Object.values(toolDefinition.args)) {
|
||||
attachJsonSchemaOverride(schema)
|
||||
}
|
||||
|
||||
return toolDefinition
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import { log } from "../shared"
|
||||
|
||||
import type { Managers } from "../create-managers"
|
||||
import type { SkillContext } from "./skill-context"
|
||||
import { normalizeToolArgSchemas } from "./normalize-tool-arg-schemas"
|
||||
|
||||
export type ToolRegistryResult = {
|
||||
filteredTools: ToolsRecord
|
||||
@@ -145,6 +146,10 @@ export function createToolRegistry(args: {
|
||||
...hashlineToolsRecord,
|
||||
}
|
||||
|
||||
for (const toolDefinition of Object.values(allTools)) {
|
||||
normalizeToolArgSchemas(toolDefinition)
|
||||
}
|
||||
|
||||
const filteredTools = filterDisabledTools(allTools, pluginConfig.disabled_tools)
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user