Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 16 additions & 22 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { BusEvent } from "@/bus/bus-event"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
import {
APICallError,
convertToModelMessages,
LoadAPIKeyError,
type ModelMessage,
type ToolSet,
type UIMessage,
} from "ai"
import { Identifier } from "../id/id"
import { LSP } from "../lsp"
import { Snapshot } from "@/snapshot"
Expand Down Expand Up @@ -432,7 +439,7 @@ export namespace MessageV2 {
})
export type WithParts = z.infer<typeof WithParts>

export function toModelMessage(input: WithParts[]): ModelMessage[] {
export function toModelMessage(input: WithParts[], options?: { tools?: ToolSet }): ModelMessage[] {
const result: UIMessage[] = []

for (const msg of input) {
Expand Down Expand Up @@ -503,30 +510,14 @@ 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 ? "[Old tool result content cleared]" : part.state.output,
output: part.state.time.compacted
? { output: "[Old tool result content cleared]" }
: { output: part.state.output, attachments: part.state.attachments },
callProviderMetadata: part.metadata,
})
}
Expand Down Expand Up @@ -565,7 +556,10 @@ export namespace MessageV2 {
}
}

return convertToModelMessages(result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")))
return convertToModelMessages(
result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")),
{ tools: options?.tools },
)
}

export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
Expand Down
38 changes: 33 additions & 5 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ export namespace SessionPrompt {
sessionID,
system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())],
messages: [
...MessageV2.toModelMessage(sessionMessages),
...MessageV2.toModelMessage(sessionMessages, { tools }),
...(isLastStep
? [
{
Expand Down Expand Up @@ -718,8 +718,22 @@ export namespace SessionPrompt {
},
toModelOutput(result) {
return {
type: "text",
value: result.output,
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,
}
}) ?? []),
],
}
},
})
Expand Down Expand Up @@ -808,8 +822,22 @@ export namespace SessionPrompt {
}
item.toModelOutput = (result) => {
return {
type: "text",
value: result.output,
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,
}
}) ?? []),
],
}
}
tools[key] = item
Expand Down
56 changes: 40 additions & 16 deletions packages/opencode/test/session/message-v2.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
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,
Expand Down Expand Up @@ -259,23 +286,11 @@ describe("session.message-v2.toModelMessage", () => {
},
]

expect(MessageV2.toModelMessage(input)).toStrictEqual([
expect(MessageV2.toModelMessage(input, { tools: createMockTools() })).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: [
Expand All @@ -297,7 +312,13 @@ describe("session.message-v2.toModelMessage", () => {
type: "tool-result",
toolCallId: "call-1",
toolName: "bash",
output: { type: "text", value: "ok" },
output: {
type: "content",
value: [
{ type: "text", text: "ok" },
{ type: "media", data: "https://example.com/attachment.png", mediaType: "image/png" },
],
},
providerOptions: { openai: { tool: "meta" } },
},
],
Expand Down Expand Up @@ -341,7 +362,7 @@ describe("session.message-v2.toModelMessage", () => {
},
]

expect(MessageV2.toModelMessage(input)).toStrictEqual([
expect(MessageV2.toModelMessage(input, { tools: createMockTools() })).toStrictEqual([
{
role: "user",
content: [{ type: "text", text: "run tool" }],
Expand All @@ -365,7 +386,10 @@ describe("session.message-v2.toModelMessage", () => {
type: "tool-result",
toolCallId: "call-1",
toolName: "bash",
output: { type: "text", value: "[Old tool result content cleared]" },
output: {
type: "content",
value: [{ type: "text", text: "[Old tool result content cleared]" }],
},
},
],
},
Expand Down