Преглед на файлове

fix(plugin): preserve tool attachments (#27157)

Aiden Cline преди 3 месеца
родител
ревизия
cb511f78ff
променени са 3 файла, в които са добавени 68 реда и са изтрити 2 реда
  1. 3 1
      packages/opencode/src/tool/registry.ts
  2. 50 0
      packages/opencode/test/tool/registry.test.ts
  3. 15 1
      packages/plugin/src/tool.ts

+ 3 - 1
packages/opencode/src/tool/registry.ts

@@ -160,11 +160,13 @@ export const layer: Layer.Layer<
                 const result = yield* Effect.promise(() => def.execute(args as any, pluginCtx))
                 const output = typeof result === "string" ? result : result.output
                 const metadata = typeof result === "string" ? {} : (result.metadata ?? {})
+                const attachments = typeof result === "string" ? undefined : result.attachments
                 const info = yield* agent.get(toolCtx.agent)
                 const out = yield* truncate.output(output, {}, info)
                 return {
-                  title: "",
+                  title: typeof result === "string" ? "" : (result.title ?? ""),
                   output: out.truncated ? out.content : output,
+                  attachments,
                   metadata: {
                     ...metadata,
                     truncated: out.truncated,

+ 50 - 0
packages/opencode/test/tool/registry.test.ts

@@ -5,6 +5,7 @@ import { pathToFileURL } from "url"
 import { Effect, Layer, Result, Schema } from "effect"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { ToolRegistry } from "@/tool/registry"
+import { Tool } from "@/tool/tool"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { disposeAllInstances, TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
@@ -29,6 +30,7 @@ import { InstanceState } from "@/effect/instance-state"
 import { Reference } from "@/reference/reference"
 import { ProviderID, ModelID } from "@/provider/schema"
 import { ToolJsonSchema } from "@/tool/json-schema"
+import { MessageID, SessionID } from "@/session/schema"
 
 const node = CrossSpawnSpawner.defaultLayer
 const originalExperimentalScout = Flag.OPENCODE_EXPERIMENTAL_SCOUT
@@ -193,6 +195,54 @@ describe("tool.registry", () => {
     }),
   )
 
+  it.instance("preserves attachments from structured custom tool results", () =>
+    Effect.gen(function* () {
+      const test = yield* TestInstance
+      const customTools = path.join(test.directory, ".opencode", "tools")
+      const pluginTool = pathToFileURL(path.resolve(import.meta.dir, "../../../plugin/src/tool.ts")).href
+      yield* Effect.promise(() => fs.mkdir(customTools, { recursive: true }))
+      yield* Effect.promise(() =>
+        Bun.write(
+          path.join(customTools, "image.ts"),
+          [
+            `import { tool } from ${JSON.stringify(pluginTool)}`,
+            "export default tool({",
+            "  description: 'image tool',",
+            "  args: {},",
+            "  execute: async () => ({",
+            "    output: 'here is an image',",
+            "    attachments: [{ type: 'file', mime: 'image/png', filename: 'picture.png', url: 'data:image/png;base64,AAAA' }],",
+            "  }),",
+            "})",
+            "",
+          ].join("\n"),
+        ),
+      )
+
+      const registry = yield* ToolRegistry.Service
+      const loaded = (yield* registry.all()).find((tool) => tool.id === "image")
+      if (!loaded) throw new Error("custom image tool was not loaded")
+      const agents = yield* Agent.Service
+      const result = yield* loaded.execute(
+        {},
+        {
+          sessionID: SessionID.make("ses_test"),
+          messageID: MessageID.make("msg_test"),
+          agent: (yield* agents.defaultInfo()).name,
+          abort: new AbortController().signal,
+          messages: [],
+          metadata: () => Effect.void,
+          ask: () => Effect.void,
+        } satisfies Tool.Context,
+      )
+
+      expect(result.output).toBe("here is an image")
+      expect(result.attachments).toEqual([
+        { type: "file", mime: "image/png", filename: "picture.png", url: "data:image/png;base64,AAAA" },
+      ])
+    }),
+  )
+
   it.instance("loads legacy JSON-schema-shaped custom tools with wire schema", () =>
     Effect.gen(function* () {
       const test = yield* TestInstance

+ 15 - 1
packages/plugin/src/tool.ts

@@ -27,7 +27,21 @@ type AskInput = {
   metadata: { [key: string]: any }
 }
 
-export type ToolResult = string | { output: string; metadata?: { [key: string]: any } }
+export type ToolAttachment = {
+  type: "file"
+  mime: string
+  url: string
+  filename?: string
+}
+
+export type ToolResult =
+  | string
+  | {
+      title?: string
+      output: string
+      metadata?: { [key: string]: any }
+      attachments?: ToolAttachment[]
+    }
 
 export function tool<Args extends z.ZodRawShape>(input: {
   description: string