| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786 |
- import { describe, expect, test } from "bun:test"
- import { MessageV2 } from "../../src/session/message-v2"
- import type { Provider } from "../../src/provider/provider"
- const sessionID = "session"
- const model: Provider.Model = {
- id: "test-model",
- providerID: "test",
- api: {
- id: "test-model",
- url: "https://example.com",
- npm: "@ai-sdk/openai",
- },
- name: "Test Model",
- capabilities: {
- temperature: true,
- reasoning: false,
- attachment: false,
- toolcall: true,
- input: {
- text: true,
- audio: false,
- image: false,
- video: false,
- pdf: false,
- },
- output: {
- text: true,
- audio: false,
- image: false,
- video: false,
- pdf: false,
- },
- interleaved: false,
- },
- cost: {
- input: 0,
- output: 0,
- cache: {
- read: 0,
- write: 0,
- },
- },
- limit: {
- context: 0,
- input: 0,
- output: 0,
- },
- status: "active",
- options: {},
- headers: {},
- release_date: "2026-01-01",
- }
- function userInfo(id: string): MessageV2.User {
- return {
- id,
- sessionID,
- role: "user",
- time: { created: 0 },
- agent: "user",
- model: { providerID: "test", modelID: "test" },
- tools: {},
- mode: "",
- } as unknown as MessageV2.User
- }
- function assistantInfo(
- id: string,
- parentID: string,
- error?: MessageV2.Assistant["error"],
- meta?: { providerID: string; modelID: string },
- ): MessageV2.Assistant {
- const infoModel = meta ?? { providerID: model.providerID, modelID: model.api.id }
- return {
- id,
- sessionID,
- role: "assistant",
- time: { created: 0 },
- error,
- parentID,
- modelID: infoModel.modelID,
- providerID: infoModel.providerID,
- mode: "",
- agent: "agent",
- path: { cwd: "/", root: "/" },
- cost: 0,
- tokens: {
- input: 0,
- output: 0,
- reasoning: 0,
- cache: { read: 0, write: 0 },
- },
- } as unknown as MessageV2.Assistant
- }
- function basePart(messageID: string, id: string) {
- return {
- id,
- sessionID,
- messageID,
- }
- }
- describe("session.message-v2.toModelMessage", () => {
- test("filters out messages with no parts", () => {
- const input: MessageV2.WithParts[] = [
- {
- info: userInfo("m-empty"),
- parts: [],
- },
- {
- info: userInfo("m-user"),
- parts: [
- {
- ...basePart("m-user", "p1"),
- type: "text",
- text: "hello",
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
- {
- role: "user",
- content: [{ type: "text", text: "hello" }],
- },
- ])
- })
- test("filters out messages with only ignored parts", () => {
- const messageID = "m-user"
- const input: MessageV2.WithParts[] = [
- {
- info: userInfo(messageID),
- parts: [
- {
- ...basePart(messageID, "p1"),
- type: "text",
- text: "ignored",
- ignored: true,
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
- })
- test("includes synthetic text parts", () => {
- const messageID = "m-user"
- const input: MessageV2.WithParts[] = [
- {
- info: userInfo(messageID),
- parts: [
- {
- ...basePart(messageID, "p1"),
- type: "text",
- text: "hello",
- synthetic: true,
- },
- ] as MessageV2.Part[],
- },
- {
- info: assistantInfo("m-assistant", messageID),
- parts: [
- {
- ...basePart("m-assistant", "a1"),
- type: "text",
- text: "assistant",
- synthetic: true,
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
- {
- role: "user",
- content: [{ type: "text", text: "hello" }],
- },
- {
- role: "assistant",
- content: [{ type: "text", text: "assistant" }],
- },
- ])
- })
- test("converts user text/file parts and injects compaction/subtask prompts", () => {
- const messageID = "m-user"
- const input: MessageV2.WithParts[] = [
- {
- info: userInfo(messageID),
- parts: [
- {
- ...basePart(messageID, "p1"),
- type: "text",
- text: "hello",
- },
- {
- ...basePart(messageID, "p2"),
- type: "text",
- text: "ignored",
- ignored: true,
- },
- {
- ...basePart(messageID, "p3"),
- type: "file",
- mime: "image/png",
- filename: "img.png",
- url: "https://example.com/img.png",
- },
- {
- ...basePart(messageID, "p4"),
- type: "file",
- mime: "text/plain",
- filename: "note.txt",
- url: "https://example.com/note.txt",
- },
- {
- ...basePart(messageID, "p5"),
- type: "file",
- mime: "application/x-directory",
- filename: "dir",
- url: "https://example.com/dir",
- },
- {
- ...basePart(messageID, "p6"),
- type: "compaction",
- auto: true,
- },
- {
- ...basePart(messageID, "p7"),
- type: "subtask",
- prompt: "prompt",
- description: "desc",
- agent: "agent",
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
- {
- role: "user",
- content: [
- { type: "text", text: "hello" },
- {
- type: "file",
- mediaType: "image/png",
- filename: "img.png",
- data: "https://example.com/img.png",
- },
- { type: "text", text: "What did we do so far?" },
- { type: "text", text: "The following tool was executed by the user" },
- ],
- },
- ])
- })
- test("converts assistant tool completion into tool-call + tool-result messages with attachments", () => {
- const userID = "m-user"
- const assistantID = "m-assistant"
- const input: MessageV2.WithParts[] = [
- {
- info: userInfo(userID),
- parts: [
- {
- ...basePart(userID, "u1"),
- type: "text",
- text: "run tool",
- },
- ] as MessageV2.Part[],
- },
- {
- info: assistantInfo(assistantID, userID),
- parts: [
- {
- ...basePart(assistantID, "a1"),
- type: "text",
- text: "done",
- metadata: { openai: { assistant: "meta" } },
- },
- {
- ...basePart(assistantID, "a2"),
- type: "tool",
- callID: "call-1",
- tool: "bash",
- state: {
- status: "completed",
- input: { cmd: "ls" },
- output: "ok",
- title: "Bash",
- metadata: {},
- time: { start: 0, end: 1 },
- attachments: [
- {
- ...basePart(assistantID, "file-1"),
- type: "file",
- mime: "image/png",
- filename: "attachment.png",
- url: "data:image/png;base64,Zm9v",
- },
- ],
- },
- metadata: { openai: { tool: "meta" } },
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
- {
- role: "user",
- content: [{ type: "text", text: "run tool" }],
- },
- {
- role: "assistant",
- content: [
- { type: "text", text: "done", providerOptions: { openai: { assistant: "meta" } } },
- {
- type: "tool-call",
- toolCallId: "call-1",
- toolName: "bash",
- input: { cmd: "ls" },
- providerExecuted: undefined,
- providerOptions: { openai: { tool: "meta" } },
- },
- ],
- },
- {
- role: "tool",
- content: [
- {
- type: "tool-result",
- toolCallId: "call-1",
- toolName: "bash",
- output: {
- type: "content",
- value: [
- { type: "text", text: "ok" },
- { type: "media", mediaType: "image/png", data: "Zm9v" },
- ],
- },
- providerOptions: { openai: { tool: "meta" } },
- },
- ],
- },
- ])
- })
- test("omits provider metadata when assistant model differs", () => {
- const userID = "m-user"
- const assistantID = "m-assistant"
- const input: MessageV2.WithParts[] = [
- {
- info: userInfo(userID),
- parts: [
- {
- ...basePart(userID, "u1"),
- type: "text",
- text: "run tool",
- },
- ] as MessageV2.Part[],
- },
- {
- info: assistantInfo(assistantID, userID, undefined, { providerID: "other", modelID: "other" }),
- parts: [
- {
- ...basePart(assistantID, "a1"),
- type: "text",
- text: "done",
- metadata: { openai: { assistant: "meta" } },
- },
- {
- ...basePart(assistantID, "a2"),
- type: "tool",
- callID: "call-1",
- tool: "bash",
- state: {
- status: "completed",
- input: { cmd: "ls" },
- output: "ok",
- title: "Bash",
- metadata: {},
- time: { start: 0, end: 1 },
- },
- metadata: { openai: { tool: "meta" } },
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
- {
- role: "user",
- content: [{ type: "text", text: "run tool" }],
- },
- {
- role: "assistant",
- content: [
- { type: "text", text: "done" },
- {
- type: "tool-call",
- toolCallId: "call-1",
- toolName: "bash",
- input: { cmd: "ls" },
- providerExecuted: undefined,
- },
- ],
- },
- {
- role: "tool",
- content: [
- {
- type: "tool-result",
- toolCallId: "call-1",
- toolName: "bash",
- output: { type: "text", value: "ok" },
- },
- ],
- },
- ])
- })
- test("replaces compacted tool output with placeholder", () => {
- const userID = "m-user"
- const assistantID = "m-assistant"
- const input: MessageV2.WithParts[] = [
- {
- info: userInfo(userID),
- parts: [
- {
- ...basePart(userID, "u1"),
- type: "text",
- text: "run tool",
- },
- ] as MessageV2.Part[],
- },
- {
- info: assistantInfo(assistantID, userID),
- parts: [
- {
- ...basePart(assistantID, "a1"),
- type: "tool",
- callID: "call-1",
- tool: "bash",
- state: {
- status: "completed",
- input: { cmd: "ls" },
- output: "this should be cleared",
- title: "Bash",
- metadata: {},
- time: { start: 0, end: 1, compacted: 1 },
- },
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
- {
- role: "user",
- content: [{ type: "text", text: "run tool" }],
- },
- {
- role: "assistant",
- content: [
- {
- type: "tool-call",
- toolCallId: "call-1",
- toolName: "bash",
- input: { cmd: "ls" },
- providerExecuted: undefined,
- },
- ],
- },
- {
- role: "tool",
- content: [
- {
- type: "tool-result",
- toolCallId: "call-1",
- toolName: "bash",
- output: { type: "text", value: "[Old tool result content cleared]" },
- },
- ],
- },
- ])
- })
- test("converts assistant tool error into error-text tool result", () => {
- const userID = "m-user"
- const assistantID = "m-assistant"
- const input: MessageV2.WithParts[] = [
- {
- info: userInfo(userID),
- parts: [
- {
- ...basePart(userID, "u1"),
- type: "text",
- text: "run tool",
- },
- ] as MessageV2.Part[],
- },
- {
- info: assistantInfo(assistantID, userID),
- parts: [
- {
- ...basePart(assistantID, "a1"),
- type: "tool",
- callID: "call-1",
- tool: "bash",
- state: {
- status: "error",
- input: { cmd: "ls" },
- error: "nope",
- time: { start: 0, end: 1 },
- metadata: {},
- },
- metadata: { openai: { tool: "meta" } },
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
- {
- role: "user",
- content: [{ type: "text", text: "run tool" }],
- },
- {
- role: "assistant",
- content: [
- {
- type: "tool-call",
- toolCallId: "call-1",
- toolName: "bash",
- input: { cmd: "ls" },
- providerExecuted: undefined,
- providerOptions: { openai: { tool: "meta" } },
- },
- ],
- },
- {
- role: "tool",
- content: [
- {
- type: "tool-result",
- toolCallId: "call-1",
- toolName: "bash",
- output: { type: "error-text", value: "nope" },
- providerOptions: { openai: { tool: "meta" } },
- },
- ],
- },
- ])
- })
- test("filters assistant messages with non-abort errors", () => {
- const assistantID = "m-assistant"
- const input: MessageV2.WithParts[] = [
- {
- info: assistantInfo(
- assistantID,
- "m-parent",
- new MessageV2.APIError({ message: "boom", isRetryable: true }).toObject() as MessageV2.APIError,
- ),
- parts: [
- {
- ...basePart(assistantID, "a1"),
- type: "text",
- text: "should not render",
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
- })
- test("includes aborted assistant messages only when they have non-step-start/reasoning content", () => {
- const assistantID1 = "m-assistant-1"
- const assistantID2 = "m-assistant-2"
- const aborted = new MessageV2.AbortedError({ message: "aborted" }).toObject() as MessageV2.Assistant["error"]
- const input: MessageV2.WithParts[] = [
- {
- info: assistantInfo(assistantID1, "m-parent", aborted),
- parts: [
- {
- ...basePart(assistantID1, "a1"),
- type: "reasoning",
- text: "thinking",
- time: { start: 0 },
- },
- {
- ...basePart(assistantID1, "a2"),
- type: "text",
- text: "partial answer",
- },
- ] as MessageV2.Part[],
- },
- {
- info: assistantInfo(assistantID2, "m-parent", aborted),
- parts: [
- {
- ...basePart(assistantID2, "b1"),
- type: "step-start",
- },
- {
- ...basePart(assistantID2, "b2"),
- type: "reasoning",
- text: "thinking",
- time: { start: 0 },
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
- {
- role: "assistant",
- content: [
- { type: "reasoning", text: "thinking", providerOptions: undefined },
- { type: "text", text: "partial answer" },
- ],
- },
- ])
- })
- test("splits assistant messages on step-start boundaries", () => {
- const assistantID = "m-assistant"
- const input: MessageV2.WithParts[] = [
- {
- info: assistantInfo(assistantID, "m-parent"),
- parts: [
- {
- ...basePart(assistantID, "p1"),
- type: "text",
- text: "first",
- },
- {
- ...basePart(assistantID, "p2"),
- type: "step-start",
- },
- {
- ...basePart(assistantID, "p3"),
- type: "text",
- text: "second",
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
- {
- role: "assistant",
- content: [{ type: "text", text: "first" }],
- },
- {
- role: "assistant",
- content: [{ type: "text", text: "second" }],
- },
- ])
- })
- test("drops messages that only contain step-start parts", () => {
- const assistantID = "m-assistant"
- const input: MessageV2.WithParts[] = [
- {
- info: assistantInfo(assistantID, "m-parent"),
- parts: [
- {
- ...basePart(assistantID, "p1"),
- type: "step-start",
- },
- ] as MessageV2.Part[],
- },
- ]
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
- })
- test("converts pending/running tool calls to error results to prevent dangling tool_use", () => {
- const userID = "m-user"
- const assistantID = "m-assistant"
- const input: MessageV2.WithParts[] = [
- {
- info: userInfo(userID),
- parts: [
- {
- ...basePart(userID, "u1"),
- type: "text",
- text: "run tool",
- },
- ] as MessageV2.Part[],
- },
- {
- info: assistantInfo(assistantID, userID),
- parts: [
- {
- ...basePart(assistantID, "a1"),
- type: "tool",
- callID: "call-pending",
- tool: "bash",
- state: {
- status: "pending",
- input: { cmd: "ls" },
- raw: "",
- },
- },
- {
- ...basePart(assistantID, "a2"),
- type: "tool",
- callID: "call-running",
- tool: "read",
- state: {
- status: "running",
- input: { path: "/tmp" },
- time: { start: 0 },
- },
- },
- ] as MessageV2.Part[],
- },
- ]
- const result = MessageV2.toModelMessages(input, model)
- expect(result).toStrictEqual([
- {
- role: "user",
- content: [{ type: "text", text: "run tool" }],
- },
- {
- role: "assistant",
- content: [
- {
- type: "tool-call",
- toolCallId: "call-pending",
- toolName: "bash",
- input: { cmd: "ls" },
- providerExecuted: undefined,
- },
- {
- type: "tool-call",
- toolCallId: "call-running",
- toolName: "read",
- input: { path: "/tmp" },
- providerExecuted: undefined,
- },
- ],
- },
- {
- role: "tool",
- content: [
- {
- type: "tool-result",
- toolCallId: "call-pending",
- toolName: "bash",
- output: { type: "error-text", value: "[Tool execution was interrupted]" },
- },
- {
- type: "tool-result",
- toolCallId: "call-running",
- toolName: "read",
- output: { type: "error-text", value: "[Tool execution was interrupted]" },
- },
- ],
- },
- ])
- })
- })
|