diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index da714c437..9f2e0ba06 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -1,14 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import z from "zod" import { NamedError } from "@opencode-ai/util/error" -import { - APICallError, - convertToModelMessages, - LoadAPIKeyError, - type ModelMessage, - type ToolSet, - type UIMessage, -} from "ai" +import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai" import { Identifier } from "../id/id" import { LSP } from "../lsp" import { Snapshot } from "@/snapshot" @@ -439,7 +432,7 @@ export namespace MessageV2 { }) export type WithParts = z.infer - export function toModelMessage(input: WithParts[], options?: { tools?: ToolSet }): ModelMessage[] { + export function toModelMessage(input: WithParts[]): ModelMessage[] { const result: UIMessage[] = [] for (const msg of input) { @@ -510,14 +503,30 @@ export namespace MessageV2 { }) if (part.type === "tool") { if (part.state.status === "completed") { + if (part.state.attachments?.length) { + result.push({ + id: Identifier.ascending("message"), + role: "user", + parts: [ + { + type: "text", + text: `Tool ${part.tool} returned an attachment:`, + }, + ...part.state.attachments.map((attachment) => ({ + type: "file" as const, + url: attachment.url, + mediaType: attachment.mime, + filename: attachment.filename, + })), + ], + }) + } assistantMessage.parts.push({ type: ("tool-" + part.tool) as `tool-${string}`, state: "output-available", toolCallId: part.callID, input: part.state.input, - output: part.state.time.compacted - ? { output: "[Old tool result content cleared]" } - : { output: part.state.output, attachments: part.state.attachments }, + output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output, callProviderMetadata: part.metadata, }) } @@ -556,10 +565,7 @@ export namespace MessageV2 { } } - return convertToModelMessages( - result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")), - { tools: options?.tools }, - ) + return convertToModelMessages(result.filter((msg) => msg.parts.some((part) => part.type !== "step-start"))) } export const stream = fn(Identifier.schema("session"), async function* (sessionID) { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 151b2d62f..8327698fd 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -597,7 +597,7 @@ export namespace SessionPrompt { sessionID, system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())], messages: [ - ...MessageV2.toModelMessage(sessionMessages, { tools }), + ...MessageV2.toModelMessage(sessionMessages), ...(isLastStep ? [ { @@ -718,22 +718,8 @@ export namespace SessionPrompt { }, toModelOutput(result) { return { - type: "content", - value: [ - { - type: "text", - text: result.output, - }, - ...(result.attachments?.map((attachment: MessageV2.FilePart) => { - const base64 = attachment.url.startsWith("data:") ? attachment.url.split(",", 2)[1] : attachment.url - - return { - type: "media", - data: base64, - mediaType: attachment.mime, - } - }) ?? []), - ], + type: "text", + value: result.output, } }, }) @@ -822,22 +808,8 @@ export namespace SessionPrompt { } item.toModelOutput = (result) => { return { - type: "content", - value: [ - { - type: "text", - text: result.output, - }, - ...(result.attachments?.map((attachment: MessageV2.FilePart) => { - const base64 = attachment.url.startsWith("data:") ? attachment.url.split(",", 2)[1] : attachment.url - - return { - type: "media", - data: base64, - mediaType: attachment.mime, - } - }) ?? []), - ], + type: "text", + value: result.output, } } tools[key] = item diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 376c189ba..f069f6ba6 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -1,35 +1,8 @@ import { describe, expect, test } from "bun:test" import { MessageV2 } from "../../src/session/message-v2" -import type { ToolSet } from "ai" const sessionID = "session" -// Mock tool that transforms output to content format with media support -function createMockTools(): ToolSet { - return { - bash: { - description: "mock bash tool", - inputSchema: { type: "object", properties: {} } as any, - toModelOutput(result: { output: string; attachments?: MessageV2.FilePart[] }) { - return { - type: "content" as const, - value: [ - { type: "text" as const, text: result.output }, - ...(result.attachments?.map((attachment) => { - const base64 = attachment.url.startsWith("data:") ? attachment.url.split(",", 2)[1] : attachment.url - return { - type: "media" as const, - data: base64, - mediaType: attachment.mime, - } - }) ?? []), - ], - } - }, - }, - } as ToolSet -} - function userInfo(id: string): MessageV2.User { return { id, @@ -286,11 +259,23 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessage(input, { tools: createMockTools() })).toStrictEqual([ + expect(MessageV2.toModelMessage(input)).toStrictEqual([ { role: "user", content: [{ type: "text", text: "run tool" }], }, + { + role: "user", + content: [ + { type: "text", text: "Tool bash returned an attachment:" }, + { + type: "file", + mediaType: "image/png", + filename: "attachment.png", + data: "https://example.com/attachment.png", + }, + ], + }, { role: "assistant", content: [ @@ -312,13 +297,7 @@ describe("session.message-v2.toModelMessage", () => { type: "tool-result", toolCallId: "call-1", toolName: "bash", - output: { - type: "content", - value: [ - { type: "text", text: "ok" }, - { type: "media", data: "https://example.com/attachment.png", mediaType: "image/png" }, - ], - }, + output: { type: "text", value: "ok" }, providerOptions: { openai: { tool: "meta" } }, }, ], @@ -362,7 +341,7 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessage(input, { tools: createMockTools() })).toStrictEqual([ + expect(MessageV2.toModelMessage(input)).toStrictEqual([ { role: "user", content: [{ type: "text", text: "run tool" }], @@ -386,10 +365,7 @@ describe("session.message-v2.toModelMessage", () => { type: "tool-result", toolCallId: "call-1", toolName: "bash", - output: { - type: "content", - value: [{ type: "text", text: "[Old tool result content cleared]" }], - }, + output: { type: "text", value: "[Old tool result content cleared]" }, }, ], },