diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index eb76681ded4..fa48afab716 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -16,7 +16,7 @@ import { Tool } from "./tool" import { Instance } from "../project/instance" import { Config } from "../config/config" import path from "path" -import { type ToolDefinition } from "@opencode-ai/plugin" +import { type ToolDefinition, type ToolResult } from "@opencode-ai/plugin" import z from "zod" import { Plugin } from "../plugin" import { WebSearchTool } from "./websearch" @@ -66,11 +66,24 @@ 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) + if (typeof result === "string") { + const out = await Truncate.output(result, {}, initCtx?.agent) + return { + title: "", + output: out.truncated ? out.content : result, + metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined }, + } + } + const out = await Truncate.output(result.output, {}, initCtx?.agent) return { - title: "", - output: out.truncated ? out.content : result, - metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined }, + title: result.title ?? "", + metadata: { + ...result.metadata, + truncated: out.truncated, + outputPath: out.truncated ? out.outputPath : undefined, + }, + output: out.truncated ? out.content : result.output, + attachments: result.attachments, } }, }), diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 78ab325af41..2dfbe541d86 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -23,20 +23,20 @@ export namespace Tool { metadata(input: { title?: string; metadata?: M }): void ask(input: Omit): Promise } + + export interface Result { + title: string + metadata: M + output: string + attachments?: MessageV2.FilePart[] + } + export interface Info { id: string init: (ctx?: InitContext) => Promise<{ description: string parameters: Parameters - execute( - args: z.infer, - ctx: Context, - ): Promise<{ - title: string - metadata: M - output: string - attachments?: MessageV2.FilePart[] - }> + execute(args: z.infer, ctx: Context): Promise> formatValidationError?(error: z.ZodError): string }> } diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/tool.ts index 37e802ac408..242b8794c2e 100644 --- a/packages/plugin/src/tool.ts +++ b/packages/plugin/src/tool.ts @@ -1,16 +1,35 @@ import { z } from "zod" +import type { FilePart } from "@opencode-ai/sdk" export type ToolContext = { sessionID: string messageID: string agent: string abort: AbortSignal + metadata(input: { title?: string; metadata?: Record }): void +} + +/** + * Structured result for plugin tools. + * + * Return this instead of a plain string to provide rich metadata + * that integrates with streaming updates. + */ +export interface ToolResult { + /** Title displayed in the UI */ + title: string + /** Arbitrary metadata passed to tool.execute.after hooks */ + metadata: Record + /** The text output returned to the model */ + output: string + /** Optional file attachments to include with the result */ + attachments?: FilePart[] } export function tool(input: { description: string args: Args - execute(args: z.infer>, context: ToolContext): Promise + execute(args: z.infer>, context: ToolContext): Promise }) { return input }