summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-01-16 15:49:39 -0600
committerAiden Cline <[email protected]>2026-01-16 15:50:24 -0600
commitf5a6a4af7fe9ef8aa203c94c74764d1e674c598c (patch)
treef820ceb705256bc9f37a1d38c91b1a4b8660ccf9
parent6e00348bd7771847ea60cc182c14c6a4ccad5fe1 (diff)
downloadopencode-f5a6a4af7fe9ef8aa203c94c74764d1e674c598c.tar.gz
opencode-f5a6a4af7fe9ef8aa203c94c74764d1e674c598c.zip
Revert "fix: ensure that tool attachments arent sent as user messages (#8944)"
This reverts commit 8fd1b92e6e95ec8570e13987e73d42093661aa59.
-rw-r--r--packages/opencode/src/session/message-v2.ts38
-rw-r--r--packages/opencode/src/session/prompt.ts38
-rw-r--r--packages/opencode/test/session/message-v2.test.ts56
3 files changed, 43 insertions, 89 deletions
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<typeof WithParts>
- 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,12 +259,24 @@ 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: [
{ type: "text", text: "done", providerOptions: { openai: { assistant: "meta" } } },
@@ -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]" },
},
],
},