diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index dad9914a289..f9e5557a75d 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -68,11 +68,22 @@ export namespace ToolRegistry { description: def.description, execute: async (args, ctx) => { const result = await def.execute(args as any, ctx) - const out = await Truncate.output(result, {}, initCtx?.agent) + + const isString = typeof result === 'string' + const output = isString ? result : result.output + + const truncatedOut = await Truncate.output(output, {}, initCtx?.agent) + const title = isString ? "" : result.title ?? "" + const metadata = { + ...(isString ? {} : (result.metadata ?? {})), + truncated: truncatedOut.truncated, + outputPath: truncatedOut.truncated ? truncatedOut.outputPath : undefined, + } + return { - title: "", - output: out.truncated ? out.content : result, - metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined }, + title, + metadata, + output: truncatedOut.truncated ? truncatedOut.content : output, } }, }), diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index aea8b7088f4..b7a3ed11b74 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -4,6 +4,8 @@ import fs from "fs/promises" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { ToolRegistry } from "../../src/tool/registry" +import { Tool } from "../../src/tool/tool" +import { Provider } from "../../src/provider/provider" describe("tool.registry", () => { test("loads tools from .opencode/tool (singular)", async () => { @@ -28,14 +30,61 @@ describe("tool.registry", () => { "", ].join("\n"), ) + + await Bun.write( + path.join(toolDir, "goodbye.ts"), + [ + "export default {", + " description: 'goodbye tool',", + " args: {},", + " execute: async () => {", + " return {", + " title: 'goodbye title',", + " output: 'goodbye world',", + " metadata: { nihilism: true },", + " }", + " },", + "}", + "", + ].join("\n"), + ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { - const ids = await ToolRegistry.ids() - expect(ids).toContain("hello") + const tools = await ToolRegistry.tools(await Provider.defaultModel()) + const hello = tools.find((t) => t.id === "hello") + const goodbye = tools.find((t) => t.id === "goodbye") + + expect(hello).toBeDefined() + expect(goodbye).toBeDefined() + + const helloResult = await hello?.execute({}, {} as Tool.Context) + const goodbyeResult = await goodbye?.execute({}, {} as Tool.Context) + + expect(helloResult).toMatchInlineSnapshot(` + { + "metadata": { + "outputPath": undefined, + "truncated": false, + }, + "output": "hello world", + "title": "", + } + `) + expect(goodbyeResult).toMatchInlineSnapshot(` + { + "metadata": { + "nihilism": true, + "outputPath": undefined, + "truncated": false, + }, + "output": "goodbye world", + "title": "goodbye title", + } + `) }, }) }) diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/tool.ts index f759c07d2b5..c4c74aacab7 100644 --- a/packages/plugin/src/tool.ts +++ b/packages/plugin/src/tool.ts @@ -1,25 +1,40 @@ import { z } from "zod" +import type { FilePart } from "@opencode-ai/sdk" -export type ToolContext = { +export type Metadata = { + [key: string]: any +} + +export type ToolContext = { sessionID: string messageID: string agent: string abort: AbortSignal - metadata(input: { title?: string; metadata?: { [key: string]: any } }): void - ask(input: AskInput): Promise + callID?: string + extra?: M + metadata(input: { title?: string; metadata?: M }): void + ask(input: AskInput): Promise } -type AskInput = { +export type AskInput = { permission: string patterns: string[] always: string[] - metadata: { [key: string]: any } + metadata: M +} + +export type ExecuteResult = { + title: string + metadata: M + output: string + attachments?: FilePart[] } -export function tool(input: { +export function tool(input: { description: string args: Args - execute(args: z.infer>, context: ToolContext): Promise + execute(args: z.infer>, context: ToolContext): Promise> + formatValidationError?(error: z.ZodError): string }) { return input }