Skip to content

fromPlugin wrapper discards custom metadata set by plugin tools via context.metadata() #12527

Closed

Description

@orgito

Description

Plugin tools that call context.metadata() during execution have their custom metadata silently discarded. The completed tool result in the API response (POST /session/:id/message) only contains { truncated, outputPath } regardless of what the tool set.

Cause

fromPlugin in packages/opencode/src/tool/registry.ts (lines 60-82) wraps plugin tool execution. It calls def.execute(), which returns a string, then constructs a hardcoded metadata object:

function fromPlugin(id: string, def: ToolDefinition): Tool.Info {
  return {
    id,
    init: async (initCtx) => ({
      parameters: z.object(def.args),
      description: def.description,
      execute: async (args, ctx) => {
        const pluginCtx = {
          ...ctx,
          directory: Instance.directory,
          worktree: Instance.worktree,
        } as unknown as PluginToolContext;
        const result = await def.execute(args as any, pluginCtx);
        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,
          },
          // ^^^^^^^^ hardcoded — custom metadata from context.metadata() is lost
        };
      },
    }),
  };
}

Two things go wrong:

  1. context.metadata() updates the running-state ToolPart, but when the tool completes, processor.ts replaces the entire state with a new completed state using the metadata from the execute return value — overwriting whatever was set during execution.

  2. The return metadata is hardcoded to { truncated, outputPath }. There's no mechanism to capture what context.metadata() set and merge it into the final result.

Suggested Fix

Intercept context.metadata() calls in the fromPlugin wrapper, accumulate custom metadata, and merge it into the final return value:

execute: async (args, ctx) => {
  let customMetadata: Record<string, any> = {};
  const pluginCtx = {
    ...ctx,
    directory: Instance.directory,
    worktree: Instance.worktree,
    metadata(input: { title?: string; metadata?: Record<string, any> }) {
      if (input.metadata)
        customMetadata = { ...customMetadata, ...input.metadata };
      ctx.metadata(input); // still update live TUI state
    },
  } as unknown as PluginToolContext;
  const result = await def.execute(args as any, pluginCtx);
  const out = await Truncate.output(result, {}, initCtx?.agent);
  return {
    title: "",
    output: out.truncated ? out.content : result,
    metadata: {
      ...customMetadata,
      truncated: out.truncated,
      ...(out.truncated && { outputPath: out.outputPath }),
    },
  };
};

The original ctx.metadata() is still called so the TUI gets live updates during execution.

Plugins

Custom tools defined in .opencode/tools/ using @opencode-ai/plugin

OpenCode version

1.1.53

Steps to reproduce

  1. Create a plugin tool in .opencode/tools/my_tool.ts:
import { tool } from "@opencode-ai/plugin";

export default tool({
  description: "A tool that sets custom metadata",
  args: {
    input: tool.schema.string(),
  },
  async execute(args, context) {
    context.metadata({
      metadata: { foo: "bar", custom_field: 123 },
    });
    return "done";
  },
});
  1. Invoke the tool via POST /session/:id/message
  2. Inspect the response — the tool part's state.metadata is { truncated: false }, not { foo: "bar", custom_field: 123 }

Screenshot and/or share link

N/A

Operating System

Linux (Docker container)

Terminal

N/A (API-only usage via HTTP server mode)

Activity

  1. github-actions commented on Feb 6, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of or related to existing issues. Please check:

    Feel free to ignore if this doesn't address your specific case.

  2. gotgenes commented on Mar 30, 2026

    @gotgenes

    Subscribing — ran into this from the other side. The related issue #18585 covers the TUI rendering gap where state.title set by context.metadata() is ignored by GenericTool/InlineTool. This issue covers the completion-time metadata being discarded by fromPlugin. Together they mean plugin tool metadata is effectively invisible in the TUI both during execution (title not rendered) and after completion (metadata overwritten).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions