384 lines
13 KiB
TypeScript
384 lines
13 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { Message, Model } from "@opencode-ai/llm"
|
|
import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
|
|
import { ModelV2 } from "@opencode-ai/core/model"
|
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
|
import { AgentAttachment, FileAttachment, ReferenceAttachment } from "@opencode-ai/core/session/prompt"
|
|
import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message"
|
|
import { SessionV2 } from "@opencode-ai/core/session"
|
|
import { ToolOutput } from "@opencode-ai/core/tool-output"
|
|
import { DateTime } from "effect"
|
|
|
|
const created = DateTime.makeUnsafe(0)
|
|
const id = (value: string) => SessionMessage.ID.make(`msg_${value}`)
|
|
const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.route })
|
|
|
|
describe("toLLMMessages", () => {
|
|
test("maps every top-level V2 Session message type", () => {
|
|
const file = new FileAttachment({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" })
|
|
const reference = new ReferenceAttachment({ name: "docs", kind: "local", uri: "file:///docs" })
|
|
const messages = toLLMMessages(
|
|
[
|
|
new SessionMessage.AgentSwitched({
|
|
id: id("agent"),
|
|
type: "agent-switched",
|
|
agent: "build",
|
|
time: { created },
|
|
}),
|
|
new SessionMessage.ModelSwitched({
|
|
id: id("model"),
|
|
type: "model-switched",
|
|
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
|
|
time: { created },
|
|
}),
|
|
new SessionMessage.System({
|
|
id: id("system"),
|
|
type: "system",
|
|
text: "Updated context\n\nOther context",
|
|
time: { created },
|
|
}),
|
|
new SessionMessage.User({
|
|
id: id("user"),
|
|
type: "user",
|
|
text: "Inspect this image",
|
|
files: [file],
|
|
agents: [new AgentAttachment({ name: "build" })],
|
|
references: [reference],
|
|
time: { created },
|
|
}),
|
|
new SessionMessage.Synthetic({
|
|
id: id("synthetic"),
|
|
type: "synthetic",
|
|
sessionID: SessionV2.ID.make("ses_translate"),
|
|
text: "Synthetic context",
|
|
time: { created },
|
|
}),
|
|
new SessionMessage.Shell({
|
|
id: id("shell"),
|
|
type: "shell",
|
|
callID: "shell-1",
|
|
command: "pwd",
|
|
output: "/project",
|
|
time: { created, completed: created },
|
|
}),
|
|
new SessionMessage.Compaction({
|
|
id: id("compaction"),
|
|
type: "compaction",
|
|
reason: "auto",
|
|
summary: "Earlier work",
|
|
time: { created },
|
|
}),
|
|
],
|
|
model,
|
|
)
|
|
|
|
expect(messages.map((message) => message.role)).toEqual(["system", "user", "user", "user", "user"])
|
|
expect(messages[0]).toEqual(Message.system("Updated context\n\nOther context"))
|
|
expect(messages[1]).toEqual(
|
|
Message.make({
|
|
id: id("user"),
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "Inspect this image" },
|
|
{ type: "media", mediaType: "image/png", data: "data:image/png;base64,aGVsbG8=", filename: "hello.png" },
|
|
],
|
|
metadata: { agents: [{ name: "build" }], references: [reference] },
|
|
}),
|
|
)
|
|
expect(messages.slice(2).map((message) => message.content)).toEqual([
|
|
[{ type: "text", text: "Synthetic context" }],
|
|
[{ type: "text", text: "Shell command: pwd\n\n/project" }],
|
|
[{ type: "text", text: "Summary of earlier conversation:\nEarlier work" }],
|
|
])
|
|
})
|
|
|
|
test("expands assistant tool calls and settled outcomes into canonical tool messages", () => {
|
|
const messages = toLLMMessages(
|
|
[
|
|
new SessionMessage.Assistant({
|
|
id: id("assistant"),
|
|
type: "assistant",
|
|
agent: "build",
|
|
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
|
|
content: [
|
|
new SessionMessage.AssistantText({ type: "text", id: "text-1", text: "Checking" }),
|
|
new SessionMessage.AssistantReasoning({
|
|
type: "reasoning",
|
|
id: "reasoning-1",
|
|
text: "Think",
|
|
providerMetadata: { anthropic: { signature: "sig_1" } },
|
|
}),
|
|
new SessionMessage.AssistantTool({
|
|
type: "tool",
|
|
id: "pending",
|
|
name: "read",
|
|
state: new SessionMessage.ToolStatePending({ status: "pending", input: '{"path":"README.md"}' }),
|
|
time: { created },
|
|
}),
|
|
new SessionMessage.AssistantTool({
|
|
type: "tool",
|
|
id: "running",
|
|
name: "read",
|
|
state: new SessionMessage.ToolStateRunning({
|
|
status: "running",
|
|
input: { path: "README.md" },
|
|
content: [],
|
|
structured: {},
|
|
}),
|
|
time: { created },
|
|
}),
|
|
new SessionMessage.AssistantTool({
|
|
type: "tool",
|
|
id: "completed",
|
|
name: "read",
|
|
state: new SessionMessage.ToolStateCompleted({
|
|
status: "completed",
|
|
input: { path: "README.md" },
|
|
content: [
|
|
new ToolOutput.TextContent({ type: "text", text: "Hello" }),
|
|
new ToolOutput.FileContent({
|
|
type: "file",
|
|
source: { type: "data", data: "aGVsbG8=" },
|
|
mime: "image/png",
|
|
name: "hello.png",
|
|
}),
|
|
],
|
|
structured: {},
|
|
}),
|
|
time: { created, completed: created },
|
|
}),
|
|
new SessionMessage.AssistantTool({
|
|
type: "tool",
|
|
id: "hosted",
|
|
name: "web_search",
|
|
provider: {
|
|
executed: true,
|
|
metadata: { fake: { continuation: "hosted-call" } },
|
|
resultMetadata: { fake: { continuation: "hosted-result" } },
|
|
},
|
|
state: new SessionMessage.ToolStateCompleted({
|
|
status: "completed",
|
|
input: { query: "Effect" },
|
|
content: [new ToolOutput.TextContent({ type: "text", text: "Found it" })],
|
|
structured: {},
|
|
}),
|
|
time: { created, completed: created },
|
|
}),
|
|
new SessionMessage.AssistantTool({
|
|
type: "tool",
|
|
id: "hosted-failed",
|
|
name: "write",
|
|
provider: { executed: true, metadata: { fake: { continuation: "failed" } } },
|
|
state: new SessionMessage.ToolStateError({
|
|
status: "error",
|
|
input: { path: "README.md" },
|
|
content: [],
|
|
structured: {},
|
|
error: { type: "unknown", message: "Denied" },
|
|
}),
|
|
time: { created, completed: created },
|
|
}),
|
|
],
|
|
time: { created, completed: created },
|
|
}),
|
|
],
|
|
model,
|
|
)
|
|
|
|
expect(messages.map((message) => message.role)).toEqual(["assistant", "tool"])
|
|
expect(messages[0]?.content).toEqual([
|
|
{ type: "text", text: "Checking" },
|
|
{ type: "reasoning", text: "Think", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
|
{ type: "tool-call", id: "pending", name: "read", input: { path: "README.md" } },
|
|
{ type: "tool-call", id: "running", name: "read", input: { path: "README.md" } },
|
|
{
|
|
type: "tool-call",
|
|
id: "completed",
|
|
name: "read",
|
|
input: { path: "README.md" },
|
|
},
|
|
{
|
|
type: "tool-call",
|
|
id: "hosted",
|
|
name: "web_search",
|
|
input: { query: "Effect" },
|
|
providerExecuted: true,
|
|
providerMetadata: { fake: { continuation: "hosted-call" } },
|
|
},
|
|
{
|
|
type: "tool-result",
|
|
id: "hosted",
|
|
name: "web_search",
|
|
providerExecuted: true,
|
|
providerMetadata: { fake: { continuation: "hosted-result" } },
|
|
result: { type: "text", value: "Found it" },
|
|
},
|
|
{
|
|
type: "tool-call",
|
|
id: "hosted-failed",
|
|
name: "write",
|
|
input: { path: "README.md" },
|
|
providerExecuted: true,
|
|
providerMetadata: { fake: { continuation: "failed" } },
|
|
},
|
|
{
|
|
type: "tool-result",
|
|
id: "hosted-failed",
|
|
name: "write",
|
|
providerExecuted: true,
|
|
providerMetadata: { fake: { continuation: "failed" } },
|
|
result: {
|
|
type: "error",
|
|
value: { error: { type: "unknown", message: "Denied" }, content: [], structured: {} },
|
|
},
|
|
},
|
|
])
|
|
expect(messages[1]?.content).toEqual([
|
|
{
|
|
type: "tool-result",
|
|
id: "completed",
|
|
name: "read",
|
|
result: {
|
|
type: "content",
|
|
value: [
|
|
{ type: "text", text: "Hello" },
|
|
{ type: "media", mediaType: "image/png", data: "aGVsbG8=", filename: "hello.png" },
|
|
],
|
|
},
|
|
},
|
|
])
|
|
})
|
|
|
|
test("restores OpenAI encrypted reasoning metadata", () => {
|
|
const messages = toLLMMessages(
|
|
[
|
|
new SessionMessage.Assistant({
|
|
id: id("assistant-openai-reasoning"),
|
|
type: "assistant",
|
|
agent: "build",
|
|
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
|
|
content: [
|
|
new SessionMessage.AssistantReasoning({
|
|
type: "reasoning",
|
|
id: "reasoning-openai",
|
|
text: "Think",
|
|
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
|
}),
|
|
],
|
|
time: { created, completed: created },
|
|
}),
|
|
],
|
|
model,
|
|
)
|
|
|
|
expect(messages[0]?.content).toEqual([
|
|
{
|
|
type: "reasoning",
|
|
text: "Think",
|
|
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
|
},
|
|
])
|
|
})
|
|
|
|
test("drops provider-native continuation metadata after a model switch", () => {
|
|
const messages = toLLMMessages(
|
|
[
|
|
new SessionMessage.Assistant({
|
|
id: id("assistant-old-model"),
|
|
type: "assistant",
|
|
agent: "build",
|
|
model: { id: ModelV2.ID.make("old-model"), providerID: ProviderV2.ID.make("provider") },
|
|
content: [
|
|
new SessionMessage.AssistantReasoning({
|
|
type: "reasoning",
|
|
id: "reasoning-old-model",
|
|
text: "Visible thought",
|
|
providerMetadata: { anthropic: { signature: "sig_old" } },
|
|
}),
|
|
new SessionMessage.AssistantTool({
|
|
type: "tool",
|
|
id: "hosted-old-model",
|
|
name: "web_search",
|
|
provider: {
|
|
executed: true,
|
|
metadata: { openai: { itemId: "hosted-old-model" } },
|
|
resultMetadata: { openai: { itemId: "hosted-old-model" } },
|
|
},
|
|
state: new SessionMessage.ToolStateCompleted({
|
|
status: "completed",
|
|
input: { query: "Effect" },
|
|
content: [],
|
|
structured: {},
|
|
result: { type: "json", value: { status: "completed" } },
|
|
}),
|
|
time: { created, completed: created },
|
|
}),
|
|
new SessionMessage.AssistantTool({
|
|
type: "tool",
|
|
id: "local-old-model",
|
|
name: "read",
|
|
provider: {
|
|
executed: false,
|
|
metadata: { fake: { call: "old" } },
|
|
resultMetadata: { fake: { result: "old" } },
|
|
},
|
|
state: new SessionMessage.ToolStateCompleted({
|
|
status: "completed",
|
|
input: { path: "README.md" },
|
|
content: [],
|
|
structured: { text: "Hello" },
|
|
}),
|
|
time: { created, completed: created },
|
|
}),
|
|
],
|
|
time: { created, completed: created },
|
|
}),
|
|
],
|
|
model,
|
|
)
|
|
|
|
expect(messages[0]?.content).toEqual([
|
|
{ type: "text", text: "Visible thought" },
|
|
{
|
|
type: "tool-call",
|
|
id: "hosted-old-model",
|
|
name: "web_search",
|
|
input: { query: "Effect" },
|
|
providerExecuted: true,
|
|
providerMetadata: undefined,
|
|
},
|
|
{
|
|
type: "tool-result",
|
|
id: "hosted-old-model",
|
|
name: "web_search",
|
|
result: { type: "json", value: { status: "completed" } },
|
|
providerExecuted: true,
|
|
cache: undefined,
|
|
metadata: undefined,
|
|
providerMetadata: undefined,
|
|
},
|
|
{
|
|
type: "tool-call",
|
|
id: "local-old-model",
|
|
name: "read",
|
|
input: { path: "README.md" },
|
|
providerExecuted: false,
|
|
providerMetadata: undefined,
|
|
},
|
|
])
|
|
expect(messages[1]?.content).toEqual([
|
|
{
|
|
type: "tool-result",
|
|
id: "local-old-model",
|
|
name: "read",
|
|
result: { type: "json", value: { text: "Hello" } },
|
|
providerExecuted: false,
|
|
cache: undefined,
|
|
metadata: undefined,
|
|
providerMetadata: undefined,
|
|
},
|
|
])
|
|
})
|
|
})
|