From aee2429aaa4ab4e1c5a6f8196f5099177477bbc6 Mon Sep 17 00:00:00 2001 From: Saatvik Arya Date: Thu, 1 Jan 2026 02:23:04 +0530 Subject: [PATCH 1/5] fix: preserve plugin tool metadata on completion --- packages/opencode/src/tool/registry.ts | 21 +++++++++++++++++++++ packages/opencode/src/tool/tool.ts | 18 +++++++++--------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index eb76681ded4..2abac082bcb 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -58,6 +58,15 @@ export namespace ToolRegistry { return { custom } }) + function isStructuredResult(result: unknown): result is Partial & { output: string } { + return ( + typeof result === "object" && + result !== null && + "output" in result && + typeof (result as Record).output === "string" + ) + } + function fromPlugin(id: string, def: ToolDefinition): Tool.Info { return { id, @@ -66,6 +75,18 @@ export namespace ToolRegistry { description: def.description, execute: async (args, ctx) => { const result = await def.execute(args as any, ctx) + if (isStructuredResult(result)) { + const out = await Truncate.output(result.output, {}, initCtx?.agent) + return { + title: result.title ?? "", + metadata: { + ...result.metadata, + truncated: out.truncated, + outputPath: out.truncated ? out.outputPath : undefined, + }, + output: out.truncated ? out.content : result.output, + } + } const out = await Truncate.output(result, {}, initCtx?.agent) return { title: "", 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 }> } From a4b2aafdcd4698265b1ed74fc9cf0c78632f62b3 Mon Sep 17 00:00:00 2001 From: Saatvik Arya Date: Thu, 1 Jan 2026 05:23:54 +0530 Subject: [PATCH 2/5] feat(plugin): add ToolResult type for structured tool responses - Add ToolResult interface to @opencode-ai/plugin - Update execute signature to Promise - Enables plugin tools to return {title, metadata, output} --- packages/plugin/src/tool.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/tool.ts index 37e802ac408..fa83e397e58 100644 --- a/packages/plugin/src/tool.ts +++ b/packages/plugin/src/tool.ts @@ -7,10 +7,25 @@ export type ToolContext = { abort: AbortSignal } +/** + * 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 +} + export function tool(input: { description: string args: Args - execute(args: z.infer>, context: ToolContext): Promise + execute(args: z.infer>, context: ToolContext): Promise }) { return input } From f316888778605db256da4dac23f799af569829e0 Mon Sep 17 00:00:00 2001 From: Saatvik Arya Date: Thu, 1 Jan 2026 05:28:57 +0530 Subject: [PATCH 3/5] fix: add type assertion for string result in fromPlugin --- packages/opencode/src/tool/registry.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 2abac082bcb..0234c8194a3 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -87,10 +87,10 @@ export namespace ToolRegistry { output: out.truncated ? out.content : result.output, } } - const out = await Truncate.output(result, {}, initCtx?.agent) + const out = await Truncate.output(result as string, {}, initCtx?.agent) return { title: "", - output: out.truncated ? out.content : result, + output: out.truncated ? out.content : (result as string), metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined }, } }, From b26525baba41f89eb573485f1e46f4931c342928 Mon Sep 17 00:00:00 2001 From: Saatvik Arya Date: Thu, 1 Jan 2026 06:22:32 +0530 Subject: [PATCH 4/5] feat(plugin): support structured tool results with metadata - Add ToolResult interface to @opencode-ai/plugin for structured tool responses - Update execute signature to accept string | ToolResult - Add runtime detection in fromPlugin to preserve title and metadata - Extract Tool.Result interface for clarity in opencode package - Maintain backward compatibility with string-only results --- packages/opencode/src/tool/registry.ts | 38 ++++++++++---------------- packages/plugin/src/tool.ts | 3 ++ 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 0234c8194a3..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" @@ -58,15 +58,6 @@ export namespace ToolRegistry { return { custom } }) - function isStructuredResult(result: unknown): result is Partial & { output: string } { - return ( - typeof result === "object" && - result !== null && - "output" in result && - typeof (result as Record).output === "string" - ) - } - function fromPlugin(id: string, def: ToolDefinition): Tool.Info { return { id, @@ -75,23 +66,24 @@ export namespace ToolRegistry { description: def.description, execute: async (args, ctx) => { const result = await def.execute(args as any, ctx) - if (isStructuredResult(result)) { - const out = await Truncate.output(result.output, {}, initCtx?.agent) + if (typeof result === "string") { + const out = await Truncate.output(result, {}, initCtx?.agent) return { - title: result.title ?? "", - metadata: { - ...result.metadata, - truncated: out.truncated, - outputPath: out.truncated ? out.outputPath : undefined, - }, - output: out.truncated ? out.content : result.output, + title: "", + output: out.truncated ? out.content : result, + metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined }, } } - const out = await Truncate.output(result as string, {}, initCtx?.agent) + const out = await Truncate.output(result.output, {}, initCtx?.agent) return { - title: "", - output: out.truncated ? out.content : (result as string), - 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/plugin/src/tool.ts b/packages/plugin/src/tool.ts index fa83e397e58..e7d5e80c233 100644 --- a/packages/plugin/src/tool.ts +++ b/packages/plugin/src/tool.ts @@ -1,4 +1,5 @@ import { z } from "zod" +import type { FilePart } from "@opencode-ai/sdk" export type ToolContext = { sessionID: string @@ -20,6 +21,8 @@ export interface ToolResult { 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: { From f35c3da1ec6c48ee096b3592e37edcb6eef72a7e Mon Sep 17 00:00:00 2001 From: Saatvik Arya Date: Sat, 10 Jan 2026 12:13:38 +0530 Subject: [PATCH 5/5] feat(plugin): add metadata method to ToolContext for enhanced tool functionality --- packages/plugin/src/tool.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/tool.ts index e7d5e80c233..242b8794c2e 100644 --- a/packages/plugin/src/tool.ts +++ b/packages/plugin/src/tool.ts @@ -6,6 +6,7 @@ export type ToolContext = { messageID: string agent: string abort: AbortSignal + metadata(input: { title?: string; metadata?: Record }): void } /**