diff --git a/src/plugin-sdk/provider-stream-shared.test.ts b/src/plugin-sdk/provider-stream-shared.test.ts index a82a1c3aac28..44eb341758cf 100644 --- a/src/plugin-sdk/provider-stream-shared.test.ts +++ b/src/plugin-sdk/provider-stream-shared.test.ts @@ -21,6 +21,41 @@ import { type StreamEvent = { type: string } & Record; +type AssistantContent = string | Array>; + +function textBlock(text: string) { + return { type: "text", text }; +} + +function textDelta(delta: string, contentIndex = 0, partial?: Record) { + return { + type: "text_delta", + contentIndex, + delta, + ...(partial ? { partial } : {}), + }; +} + +function textEnd(content: string, contentIndex = 0) { + return { type: "text_end", contentIndex, content }; +} + +function doneEvent(content: AssistantContent, reason = "stop") { + return { + type: "done", + reason, + message: { role: "assistant", content, stopReason: reason }, + }; +} + +function doneWithoutStopReason(content: string) { + return { type: "done", reason: "stop", message: { role: "assistant", content } }; +} + +function errorEvent(error: Record, partial?: Record) { + return { type: "error", ...(partial ? { partial } : {}), error }; +} + const lmstudioBinaryModel = { api: "openai-completions", provider: "lmstudio", @@ -49,6 +84,10 @@ function requireRecord(value: unknown, label: string): Record { return value as Record; } +function messageOf(event: unknown) { + return requireRecord(requireRecord(event, "done event").message, "done message"); +} + function createEventStream(events: unknown[]): ReturnType { const output = createAssistantMessageEventStream(); const stream = output as unknown as { push(event: unknown): void; end(): void }; @@ -96,10 +135,11 @@ function createByteOverCapZeroArgumentXmlCall(name: string): string { async function collectPlainTextToolCallCompatEventsFromStream( baseStreamFn: StreamFn, + toolNames = ["read"], ): Promise { const wrapped = createPlainTextToolCallCompatWrapper(baseStreamFn); const stream = await resolveStream( - wrapped({} as never, { tools: [{ name: "read" }] } as never, {}), + wrapped({} as never, { tools: toolNames.map((name) => ({ name })) } as never, {}), ); const output: StreamEvent[] = []; for await (const event of stream as AsyncIterable) { @@ -108,8 +148,40 @@ async function collectPlainTextToolCallCompatEventsFromStream( return output; } -async function collectPlainTextToolCallCompatEvents(events: unknown[]): Promise { - return collectPlainTextToolCallCompatEventsFromStream(() => createEventStream(events)); +async function collectPlainTextToolCallCompatEvents( + events: unknown[], + toolNames?: string[], +): Promise { + return collectPlainTextToolCallCompatEventsFromStream(() => createEventStream(events), toolNames); +} + +async function collectTextDoneEvents(deltas: string[], rawText: string, includeTextEnd = false) { + return collectPlainTextToolCallCompatEvents([ + ...deltas.map((delta) => textDelta(delta)), + ...(includeTextEnd ? [textEnd(rawText)] : []), + doneEvent([textBlock(rawText)]), + ]); +} + +async function collectPlainTextToolCallCompatEventsAndResult(events: unknown[]) { + const { source, stream } = createControlledPlainTextToolCallCompatStream(); + const output = await resolveStream(stream); + const resultPromise = output.result(); + const eventsPromise = (async () => { + const outputEvents: unknown[] = []; + for await (const event of output as AsyncIterable) { + outputEvents.push(event); + } + return outputEvents; + })(); + for (const event of events) { + source.push(event as never); + } + source.end(); + return { + events: await eventsPromise, + result: requireRecord(await resultPromise, "result message"), + }; } async function resolveStream(stream: ReturnType) { @@ -472,21 +544,12 @@ describe("createOpenAICompatibleCompletionsThinkingOffWrapper", () => { describe("createPlainTextToolCallCompatWrapper", () => { it("promotes standalone text tool calls into tool-call stream events", async () => { - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_start", content: "" }, - { type: "text_delta", delta: '[tool:read] {"path":"/tmp/file.txt"}' }, - { type: "text_end" }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: '[tool:read] {"path":"/tmp/file.txt"}', - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + { type: "text_start", content: "" }, + { type: "text_delta", delta: '[tool:read] {"path":"/tmp/file.txt"}' }, + { type: "text_end" }, + doneWithoutStopReason('[tool:read] {"path":"/tmp/file.txt"}'), + ]); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "start", @@ -508,19 +571,7 @@ describe("createPlainTextToolCallCompatWrapper", () => { it("does not promote complete-looking text tool calls after a length stop", async () => { const rawToolText = '[tool:read] {"path":"/tmp/file.txt"}'; - const baseStreamFn: StreamFn = () => - createEventStream([ - { - type: "done", - reason: "length", - message: { - role: "assistant", - content: rawToolText, - stopReason: "length", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([doneEvent(rawToolText, "length")]); expect(events.map((event) => (event as { type?: string }).type)).toEqual(["done"]); const done = events.at(-1) as { @@ -532,19 +583,10 @@ describe("createPlainTextToolCallCompatWrapper", () => { }); it("passes through bracketed text when no configured tool names match", async () => { - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", delta: "[note] keep streaming" }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: "[note] keep streaming", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + { type: "text_delta", delta: "[note] keep streaming" }, + doneWithoutStopReason("[note] keep streaming"), + ]); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "text_delta", @@ -553,28 +595,12 @@ describe("createPlainTextToolCallCompatWrapper", () => { }); it("converts standalone plain-text tool calls for result consumers", async () => { - const { source, stream } = createControlledPlainTextToolCallCompatStream(); - const resultPromise = (await resolveStream(stream)).result(); const rawToolText = '[tool:read] {"path":"src/index.ts"}'; - - source.push({ type: "start", partial: { content: [] } } as never); - source.push({ - type: "text_delta", - contentIndex: 0, - delta: rawToolText, - } as never); - source.push({ - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawToolText }], - stopReason: "stop", - }, - } as never); - source.end(); - - const message = requireRecord(await resultPromise, "result message"); + const { result: message } = await collectPlainTextToolCallCompatEventsAndResult([ + { type: "start", partial: { content: [] } }, + textDelta(rawToolText), + doneEvent([textBlock(rawToolText)]), + ]); expect(message.stopReason).toBe("toolUse"); expect(requireRecord((message.content as unknown[])[0], "tool call")).toMatchObject({ type: "toolCall", @@ -584,8 +610,6 @@ describe("createPlainTextToolCallCompatWrapper", () => { }); it("promotes serialized tool calls split across adjacent text blocks", async () => { - const { source, stream } = createControlledPlainTextToolCallCompatStream(); - const resultPromise = (await resolveStream(stream)).result(); const rawToolText = [ "[tool:read]", "", @@ -593,28 +617,14 @@ describe("createPlainTextToolCallCompatWrapper", () => { "", "", ].join("\n"); - - source.push({ type: "start", partial: { content: [] } } as never); - source.push({ - type: "text_delta", - contentIndex: 0, - delta: rawToolText, - } as never); - source.push({ - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [ - { type: "text", text: "[tool:read]\n" }, - { type: "text", text: "src/index.ts\n\n" }, - ], - stopReason: "stop", - }, - } as never); - source.end(); - - const message = requireRecord(await resultPromise, "result message"); + const { result: message } = await collectPlainTextToolCallCompatEventsAndResult([ + { type: "start", partial: { content: [] } }, + textDelta(rawToolText), + doneEvent([ + textBlock("[tool:read]\n"), + textBlock("src/index.ts\n\n"), + ]), + ]); expect(message.stopReason).toBe("toolUse"); expect(requireRecord((message.content as unknown[])[0], "tool call")).toMatchObject({ type: "toolCall", @@ -624,24 +634,12 @@ describe("createPlainTextToolCallCompatWrapper", () => { }); it("preserves exact text block adjacency inside promoted arguments", async () => { - const { source, stream } = createControlledPlainTextToolCallCompatStream(); - const resultPromise = (await resolveStream(stream)).result(); - - source.push({ - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [ - { type: "text", text: "[tool:read]\n\nsrc/ind" }, - { type: "text", text: "ex.ts\n\n" }, - ], - stopReason: "stop", - }, - } as never); - source.end(); - - const message = requireRecord(await resultPromise, "result message"); + const { result: message } = await collectPlainTextToolCallCompatEventsAndResult([ + doneEvent([ + textBlock("[tool:read]\n\nsrc/ind"), + textBlock("ex.ts\n\n"), + ]), + ]); expect(requireRecord((message.content as unknown[])[0], "tool call")).toMatchObject({ type: "toolCall", name: "read", @@ -650,24 +648,9 @@ describe("createPlainTextToolCallCompatWrapper", () => { }); it("repairs bracketed tool-call block boundaries when providers split header text", async () => { - const { source, stream } = createControlledPlainTextToolCallCompatStream(); - const resultPromise = (await resolveStream(stream)).result(); - - source.push({ - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [ - { type: "text", text: "[read]" }, - { type: "text", text: '{"path":"src/index.ts"}\n[END_TOOL_REQUEST]' }, - ], - stopReason: "stop", - }, - } as never); - source.end(); - - const message = requireRecord(await resultPromise, "result message"); + const { result: message } = await collectPlainTextToolCallCompatEventsAndResult([ + doneEvent([textBlock("[read]"), textBlock('{"path":"src/index.ts"}\n[END_TOOL_REQUEST]')]), + ]); expect(requireRecord((message.content as unknown[])[0], "tool call")).toMatchObject({ type: "toolCall", name: "read", @@ -683,42 +666,21 @@ describe("createPlainTextToolCallCompatWrapper", () => { "", "", ].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 1, delta: rawToolText }, - { - type: "thinking_delta", - contentIndex: 0, - delta: "Need file contents.", - partial: { - content: [ - { type: "thinking", thinking: "Need file contents." }, - { type: "text", text: rawToolText }, - ], - }, + const events = await collectPlainTextToolCallCompatEvents([ + { type: "text_delta", contentIndex: 1, delta: rawToolText }, + { + type: "thinking_delta", + contentIndex: 0, + delta: "Need file contents.", + partial: { + content: [ + { type: "thinking", thinking: "Need file contents." }, + { type: "text", text: rawToolText }, + ], }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [ - { type: "thinking", thinking: "Need file contents." }, - { type: "text", text: rawToolText }, - ], - stopReason: "stop", - }, - }, - ]); - const wrapped = createPlainTextToolCallCompatWrapper(baseStreamFn); - const stream = await resolveStream( - wrapped({} as never, { tools: [{ name: "read" }] } as never, {}), - ); - const events: unknown[] = []; - - for await (const event of stream as AsyncIterable) { - events.push(event); - } + }, + doneEvent([{ type: "thinking", thinking: "Need file contents." }, textBlock(rawToolText)]), + ]); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "start", @@ -744,42 +706,21 @@ describe("createPlainTextToolCallCompatWrapper", () => { "", "", ].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: rawToolText }, - { - type: "thinking_delta", - contentIndex: 1, - delta: "Need file contents.", - partial: { - content: [ - { type: "text", text: rawToolText }, - { type: "thinking", thinking: "Need file contents." }, - ], - }, + const events = await collectPlainTextToolCallCompatEvents([ + { type: "text_delta", contentIndex: 0, delta: rawToolText }, + { + type: "thinking_delta", + contentIndex: 1, + delta: "Need file contents.", + partial: { + content: [ + { type: "text", text: rawToolText }, + { type: "thinking", thinking: "Need file contents." }, + ], }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [ - { type: "text", text: rawToolText }, - { type: "thinking", thinking: "Need file contents." }, - ], - stopReason: "stop", - }, - }, - ]); - const wrapped = createPlainTextToolCallCompatWrapper(baseStreamFn); - const stream = await resolveStream( - wrapped({} as never, { tools: [{ name: "read" }] } as never, {}), - ); - const events: unknown[] = []; - - for await (const event of stream as AsyncIterable) { - events.push(event); - } + }, + doneEvent([textBlock(rawToolText), { type: "thinking", thinking: "Need file contents." }]), + ]); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "start", @@ -801,43 +742,25 @@ describe("createPlainTextToolCallCompatWrapper", () => { it("flushes false-positive buffered prefixes around interleaved events in source order", async () => { const firstText = "[tool:re"; const secondText = " not a call"; - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: firstText }, - { - type: "thinking_delta", - contentIndex: 1, - delta: "Need file contents.", - partial: { - content: [ - { type: "text", text: firstText }, - { type: "thinking", thinking: "Need file contents." }, - ], - }, + const events = await collectPlainTextToolCallCompatEvents([ + { type: "text_delta", contentIndex: 0, delta: firstText }, + { + type: "thinking_delta", + contentIndex: 1, + delta: "Need file contents.", + partial: { + content: [ + { type: "text", text: firstText }, + { type: "thinking", thinking: "Need file contents." }, + ], }, - { type: "text_delta", contentIndex: 0, delta: secondText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [ - { type: "text", text: `${firstText}${secondText}` }, - { type: "thinking", thinking: "Need file contents." }, - ], - stopReason: "stop", - }, - }, - ]); - const wrapped = createPlainTextToolCallCompatWrapper(baseStreamFn); - const stream = await resolveStream( - wrapped({} as never, { tools: [{ name: "read" }] } as never, {}), - ); - const events: unknown[] = []; - - for await (const event of stream as AsyncIterable) { - events.push(event); - } + }, + { type: "text_delta", contentIndex: 0, delta: secondText }, + doneEvent([ + textBlock(`${firstText}${secondText}`), + { type: "thinking", thinking: "Need file contents." }, + ]), + ]); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "text_delta", @@ -877,15 +800,7 @@ describe("createPlainTextToolCallCompatWrapper", () => { source.push({ type: "start", partial: { content: [] } } as never); expect((await nextEvent(iterator, "start")).type).toBe("start"); source.push({ type: "text_delta", contentIndex: 0, delta: rawToolText } as never); - source.push({ - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawToolText }], - stopReason: "stop", - }, - } as never); + source.push(doneEvent([textBlock(rawToolText)]) as never); expect((await nextEvent(iterator, `converted ${name}`)).type).toBe("toolcall_start"); } finally { @@ -939,34 +854,24 @@ describe("createPlainTextToolCallCompatWrapper", () => { const payload = rawToolText.slice(marker.length, payloadEnd); expect(new TextEncoder().encode(payload).byteLength).toBe(256_002); } - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "start", partial: { content: [] } }, - { type: "text_start", contentIndex: 0, content: "" }, - { type: "text_delta", contentIndex: 0, delta: rawToolText }, - { - type: "thinking_delta", - contentIndex: 1, - delta: "checking", - partial: { - content: [ - { type: "text", text: rawToolText }, - { type: "thinking", thinking: "checking" }, - ], - }, + const events = await collectPlainTextToolCallCompatEvents([ + { type: "start", partial: { content: [] } }, + { type: "text_start", contentIndex: 0, content: "" }, + textDelta(rawToolText), + { + type: "thinking_delta", + contentIndex: 1, + delta: "checking", + partial: { + content: [ + { type: "text", text: rawToolText }, + { type: "thinking", thinking: "checking" }, + ], }, - { type: "text_end", contentIndex: 0, content: rawToolText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawToolText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + }, + textEnd(rawToolText), + doneEvent([textBlock(rawToolText)]), + ]); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "start", @@ -978,9 +883,9 @@ describe("createPlainTextToolCallCompatWrapper", () => { { type: "text", text: "" }, { type: "thinking", thinking: "checking" }, ]); - const doneEvent = requireRecord(events[2], "done event"); - expect(doneEvent.reason).toBe("stop"); - expect(doneEvent.message).toMatchObject({ + const terminalEvent = requireRecord(events[2], "done event"); + expect(terminalEvent.reason).toBe("stop"); + expect(terminalEvent.message).toMatchObject({ role: "assistant", content: [], stopReason: "stop", @@ -993,20 +898,7 @@ describe("createPlainTextToolCallCompatWrapper", () => { const visibleText = "Visible answer"; const rawText = `${createByteOverCapZeroArgumentXmlCall("read")}\n${visibleText}`; expect(rawText.length).toBeLessThan(256_000); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: rawText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectTextDoneEvents([rawText], rawText); expect(events.map((event) => requireRecord(event, "event").type)).toEqual([ "text_delta", @@ -1034,22 +926,16 @@ describe("createPlainTextToolCallCompatWrapper", () => { { type: "thinking", thinking: "checking" }, { type: "text", text: secondChunk }, ]; - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: firstChunk }, - { - type: "text_delta", - contentIndex: 2, - delta: secondChunk, - partial: { role: "assistant", content }, - }, - { - type: "done", - reason: "stop", - message: { role: "assistant", content, stopReason: "stop" }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + textDelta(firstChunk), + { + type: "text_delta", + contentIndex: 2, + delta: secondChunk, + partial: { role: "assistant", content }, + }, + doneEvent(content), + ]); expect(events.map((event) => requireRecord(event, "event").type)).toEqual([ "text_delta", @@ -1076,23 +962,21 @@ describe("createPlainTextToolCallCompatWrapper", () => { const visibleText = "Visible answer"; const firstChunk = `${marker}${"\u00a0".repeat(100_000)}`; const secondChunk = `${"\u00a0".repeat(28_001)}\n${visibleText}`; - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: firstChunk }, - { - type: "text_delta", - contentIndex: 1, - delta: secondChunk, - partial: { - role: "assistant", - content: [ - { type: "text", text: firstChunk }, - { type: "text", text: secondChunk }, - ], - }, + const events = await collectPlainTextToolCallCompatEvents([ + textDelta(firstChunk), + { + type: "text_delta", + contentIndex: 1, + delta: secondChunk, + partial: { + role: "assistant", + content: [ + { type: "text", text: firstChunk }, + { type: "text", text: secondChunk }, + ], }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + }, + ]); expect(events.map((event) => requireRecord(event, "event").type)).toEqual(["text_delta"]); expect(requireRecord(events[0], "text event")).toMatchObject({ @@ -1115,25 +999,19 @@ describe("createPlainTextToolCallCompatWrapper", () => { { type: "text", text: firstChunk }, { type: "text", text: secondChunk }, ]; - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: firstChunk }, - { type: "text_delta", contentIndex: 1, delta: secondChunk }, - { - type: "error", - partial: { role: "assistant", content }, - error: { content, errorMessage: "stream failed" }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + textDelta(firstChunk), + textDelta(secondChunk, 1), + errorEvent({ content, errorMessage: "stream failed" }, { role: "assistant", content }), + ]); expect(events.map((event) => requireRecord(event, "event").type)).toEqual(["error"]); - const errorEvent = requireRecord(events[0], "error event"); - expect(requireRecord(errorEvent.partial, "error partial").content).toEqual([ + const terminalError = requireRecord(events[0], "error event"); + expect(requireRecord(terminalError.partial, "error partial").content).toEqual([ { type: "text", text: "" }, { type: "text", text: "" }, ]); - expect(requireRecord(errorEvent.error, "error body").content).toEqual([]); + expect(requireRecord(terminalError.error, "error body").content).toEqual([]); expect(JSON.stringify(events)).not.toContain(marker); }); @@ -1149,40 +1027,29 @@ describe("createPlainTextToolCallCompatWrapper", () => { rawToolText: createByteOverCapZeroArgumentXmlCall("read"), }, ])("scrubs $label from terminal error partials", async ({ marker, rawToolText }) => { - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: rawToolText }, + const events = await collectPlainTextToolCallCompatEvents([ + textDelta(rawToolText), + errorEvent( + { content: [textBlock(rawToolText)], errorMessage: "stream failed" }, { - type: "error", - partial: { - content: [ - { type: "text", text: rawToolText }, - { type: "thinking", thinking: "checking" }, - ], - }, - error: { - content: [{ type: "text", text: rawToolText }], - errorMessage: "stream failed", - }, + content: [textBlock(rawToolText), { type: "thinking", thinking: "checking" }], }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + ), + ]); expect(events.map((event) => (event as { type?: string }).type)).toEqual(["error"]); - const errorEvent = requireRecord(events[0], "error event"); - expect(requireRecord(errorEvent.partial, "error partial").content).toEqual([ + const terminalError = requireRecord(events[0], "error event"); + expect(requireRecord(terminalError.partial, "error partial").content).toEqual([ { type: "text", text: "" }, { type: "thinking", thinking: "checking" }, ]); - expect(requireRecord(errorEvent.error, "error body").content).toEqual([]); + expect(requireRecord(terminalError.error, "error body").content).toEqual([]); expect(JSON.stringify(events)).not.toContain(marker); }); it("does not flush a byte-over-cap XML call when the stream ends without a terminal event", async () => { const rawToolText = createByteOverCapZeroArgumentXmlCall("read"); - const baseStreamFn: StreamFn = () => - createEventStream([{ type: "text_delta", contentIndex: 0, delta: rawToolText }]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([textDelta(rawToolText)]); expect(events).toEqual([]); }); @@ -1196,18 +1063,14 @@ describe("createPlainTextToolCallCompatWrapper", () => { { type: "text_end", contentIndex: 0, content: rawToolText }, ...(terminal === "error" ? [ - { - type: "error", - partial: { + errorEvent( + { role: "assistant", - content: [{ type: "text", text: rawToolText }], - }, - error: { - role: "assistant", - content: [{ type: "text", text: rawToolText }], + content: [textBlock(rawToolText)], errorMessage: "stream failed", }, - }, + { role: "assistant", content: [textBlock(rawToolText)] }, + ), ] : []), ]); @@ -1217,11 +1080,11 @@ describe("createPlainTextToolCallCompatWrapper", () => { return; } expect(events.map((event) => event.type)).toEqual(["error"]); - const errorEvent = requireRecord(events[0], "error event"); - expect(requireRecord(errorEvent.partial, "error partial").content).toEqual([ + const terminalError = requireRecord(events[0], "error event"); + expect(requireRecord(terminalError.partial, "error partial").content).toEqual([ { type: "text", text: "" }, ]); - expect(requireRecord(errorEvent.error, "error body").content).toEqual([]); + expect(requireRecord(terminalError.error, "error body").content).toEqual([]); expect(JSON.stringify(events)).not.toContain(""); }, ); @@ -1229,21 +1092,20 @@ describe("createPlainTextToolCallCompatWrapper", () => { it("scrubs byte-over-cap XML from error-only terminal snapshots", async () => { const rawToolText = createByteOverCapZeroArgumentXmlCall("read"); const events = await collectPlainTextToolCallCompatEvents([ - { - type: "error", - partial: { role: "assistant", content: rawToolText }, - error: { + errorEvent( + { role: "assistant", - content: [{ type: "text", text: rawToolText }], + content: [textBlock(rawToolText)], errorMessage: "stream failed", }, - }, + { role: "assistant", content: rawToolText }, + ), ]); expect(events.map((event) => event.type)).toEqual(["error"]); - const errorEvent = requireRecord(events[0], "error event"); - expect(requireRecord(errorEvent.partial, "error partial").content).toBe(""); - expect(requireRecord(errorEvent.error, "error body")).toMatchObject({ + const terminalError = requireRecord(events[0], "error event"); + expect(requireRecord(terminalError.partial, "error partial").content).toBe(""); + expect(requireRecord(terminalError.error, "error body")).toMatchObject({ content: [], errorMessage: "stream failed", }); @@ -1254,15 +1116,7 @@ describe("createPlainTextToolCallCompatWrapper", () => { const body = "\u00a0".repeat(128_001); const parts = ["${body}"]; const events = await collectPlainTextToolCallCompatEvents([ - { - type: "done", - reason: "length", - message: { - role: "assistant", - content: parts.map((text) => ({ type: "text", text })), - stopReason: "length", - }, - }, + doneEvent(parts.map(textBlock), "length"), ]); expect(requireRecord(events[0], "done event")).toMatchObject({ @@ -1280,24 +1134,19 @@ describe("createPlainTextToolCallCompatWrapper", () => { const image = { type: "image", data: "aW1n", mimeType: "image/png" }; const thinkingAfter = { type: "thinking", thinking: "After suffix." }; const events = await collectPlainTextToolCallCompatEvents([ - { - type: "done", - reason: "length", - message: { - role: "assistant", - content: [ - thinkingBefore, - { type: "text", text: `${"\u00a0".repeat(128_001)}` }, - image, - { type: "text", text: `\n${visibleText}` }, - thinkingAfter, - ], - stopReason: "length", - }, - }, + doneEvent( + [ + thinkingBefore, + textBlock(`${"\u00a0".repeat(128_001)}`), + image, + textBlock(`\n${visibleText}`), + thinkingAfter, + ], + "length", + ), ]); - const doneMessage = requireRecord(requireRecord(events[0], "done event").message, "message"); + const doneMessage = messageOf(events[0]); expect(doneMessage.content).toEqual([ thinkingBefore, image, @@ -1315,20 +1164,10 @@ describe("createPlainTextToolCallCompatWrapper", () => { visibleText, ].join("\n"); const events = await collectPlainTextToolCallCompatEvents([ - { - type: "done", - reason: "length", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "length", - }, - }, + doneEvent([textBlock(rawText)], "length"), ]); - expect( - requireRecord(requireRecord(events[0], "done event").message, "message").content, - ).toEqual([{ type: "text", text: visibleText }]); + expect(messageOf(events[0]).content).toEqual([{ type: "text", text: visibleText }]); expect(JSON.stringify(events)).not.toContain(""); }); @@ -1336,25 +1175,12 @@ describe("createPlainTextToolCallCompatWrapper", () => { const visibleText = "Visible after compacted XML"; const chunks = ["${" ".repeat(330_001)}\n${visibleText}`]; const rawText = chunks.join(""); - const events = await collectPlainTextToolCallCompatEvents([ - ...chunks.map((delta) => ({ type: "text_delta", contentIndex: 0, delta })), - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); + const events = await collectTextDoneEvents(chunks, rawText); expect(rawText.length).toBeGreaterThan(320_000); expect(events.map((event) => event.type)).toEqual(["text_delta", "done"]); expect(events[0]).toMatchObject({ delta: visibleText }); - expect( - requireRecord(requireRecord(events[1], "done event").message, "message").content, - ).toEqual([{ type: "text", text: visibleText }]); + expect(messageOf(events[1]).content).toEqual([{ type: "text", text: visibleText }]); expect(JSON.stringify(events)).not.toContain(" { const firstChunk = `${toolPrefix}${"x".repeat(emojiIndex - toolPrefix.length)}😀${"y".repeat(70_000)}`; const secondChunk = "z".repeat(70_000); const rawToolText = firstChunk + secondChunk; - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: firstChunk }, - { type: "text_delta", contentIndex: 0, delta: secondChunk }, - { - type: "error", - partial: { content: [{ type: "text", text: rawToolText }] }, - error: { - content: [{ type: "text", text: rawToolText }], - errorMessage: "stream failed", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + textDelta(firstChunk), + textDelta(secondChunk), + errorEvent( + { content: [textBlock(rawToolText)], errorMessage: "stream failed" }, + { content: [textBlock(rawToolText)] }, + ), + ]); expect(events.map((event) => (event as { type?: string }).type)).toEqual(["error"]); - const errorEvent = requireRecord(events[0], "error event"); - expect(requireRecord(errorEvent.partial, "error partial").content).toEqual([ + const terminalError = requireRecord(events[0], "error event"); + expect(requireRecord(terminalError.partial, "error partial").content).toEqual([ { type: "text", text: "" }, ]); - expect(requireRecord(errorEvent.error, "error body").content).toEqual([]); + expect(requireRecord(terminalError.error, "error body").content).toEqual([]); expect(JSON.stringify(events)).not.toContain("[tool:read]"); }); @@ -1396,24 +1216,14 @@ describe("createPlainTextToolCallCompatWrapper", () => { "", "", ].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawToolText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + doneEvent([textBlock(rawToolText)]), + ]); expect(events.map((event) => (event as { type?: string }).type)).toEqual(["done"]); - const doneEvent = requireRecord(events[0], "done event"); - expect(doneEvent.reason).toBe("stop"); - expect(doneEvent.message).toMatchObject({ + const terminalEvent = requireRecord(events[0], "done event"); + expect(terminalEvent.reason).toBe("stop"); + expect(terminalEvent.message).toMatchObject({ role: "assistant", content: [], stopReason: "stop", @@ -1422,16 +1232,6 @@ describe("createPlainTextToolCallCompatWrapper", () => { }); it("scrubs over-cap bracketed XML parameter text from length terminal messages", async () => { - const { source, stream } = createControlledPlainTextToolCallCompatStream(); - const output = await resolveStream(stream); - const resultPromise = output.result(); - const eventsPromise = (async () => { - const events: unknown[] = []; - for await (const event of output as AsyncIterable) { - events.push(event); - } - return events; - })(); const rawToolText = [ "[tool:read]", "", @@ -1439,20 +1239,9 @@ describe("createPlainTextToolCallCompatWrapper", () => { "", "", ].join("\n"); - - source.push({ - type: "done", - reason: "length", - message: { - role: "assistant", - content: [{ type: "text", text: rawToolText }], - stopReason: "length", - }, - } as never); - source.end(); - - const events = await eventsPromise; - const result = requireRecord(await resultPromise, "result message"); + const { events, result } = await collectPlainTextToolCallCompatEventsAndResult([ + doneEvent([textBlock(rawToolText)], "length"), + ]); expect(requireRecord(events[0], "done event")).toMatchObject({ reason: "length", @@ -1467,7 +1256,6 @@ describe("createPlainTextToolCallCompatWrapper", () => { const overCapXml = ["[tool:read]", "", overCapPath].join("\n"); const closingXml = ["", ""].join("\n"); const visibleAfterTool = "Visible text after the tool-looking blocks."; - const textBlock = (text: string) => ({ type: "text", text }); const thinkingBlock = { type: "thinking", thinking: "Checking path." }; const completeTool = '[tool:read] {"path":"src/index.ts"}'; const unallowedTool = '[tool:write] {"path":"keep-visible"}'; @@ -1567,13 +1355,7 @@ describe("createPlainTextToolCallCompatWrapper", () => { absent: ["[tool:read]"], }, ])("$name", async ({ content, expected, absent }) => { - const events = await collectPlainTextToolCallCompatEvents([ - { - type: "done", - reason: "stop", - message: { role: "assistant", content, stopReason: "stop" }, - }, - ]); + const events = await collectPlainTextToolCallCompatEvents([doneEvent(content)]); expect(events.map((event) => event.type)).toEqual(["done"]); expect(requireRecord(events[0], "done event")).toMatchObject({ @@ -1593,29 +1375,10 @@ describe("createPlainTextToolCallCompatWrapper", () => { "", "", ].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: rawToolText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawToolText }], - stopReason: "stop", - }, - }, - ]); - const wrapped = createPlainTextToolCallCompatWrapper(baseStreamFn); - const events: unknown[] = []; - - for await (const event of wrapped( - {} as never, - { tools: [{ name: "read_file" }] } as never, - {}, - ) as AsyncIterable) { - events.push(event); - } + const events = await collectPlainTextToolCallCompatEvents( + [textDelta(rawToolText), doneEvent([textBlock(rawToolText)])], + ["read_file"], + ); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "text_delta", @@ -1626,20 +1389,7 @@ describe("createPlainTextToolCallCompatWrapper", () => { it("flushes long mixed text after a complete serialized tool-call prefix", async () => { const rawText = ['[tool:read] {"path":"src/index.ts"}', "A".repeat(256_001)].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: rawText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectTextDoneEvents([rawText], rawText); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "text_delta", @@ -1653,33 +1403,10 @@ describe("createPlainTextToolCallCompatWrapper", () => { const rawCall = ""; const visibleText = "Visible answer after the leaked call."; const rawText = `${rawCall}\n${visibleText}`; - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: rawText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [ - { type: "text", text: rawCall }, - { type: "text", text: visibleText }, - ], - stopReason: "stop", - }, - }, - ]); - const wrapped = createPlainTextToolCallCompatWrapper(baseStreamFn); - const output = await resolveStream( - wrapped({} as never, { tools: [{ name: "read" }] } as never, {}), - ); - const resultPromise = output.result(); - const events: unknown[] = []; - - for await (const event of output as AsyncIterable) { - events.push(event); - } - const result = requireRecord(await resultPromise, "result message"); + const { events, result } = await collectPlainTextToolCallCompatEventsAndResult([ + textDelta(rawText), + doneEvent([textBlock(rawCall), textBlock(visibleText)]), + ]); const expectedContent = [{ type: "text", text: visibleText }]; expect(events.map((event) => requireRecord(event, "event").type)).toEqual([ @@ -1687,9 +1414,7 @@ describe("createPlainTextToolCallCompatWrapper", () => { "done", ]); expect(requireRecord(events[0], "text event").delta).toBe(visibleText); - expect( - requireRecord(requireRecord(events[1], "done event").message, "done message").content, - ).toEqual(expectedContent); + expect(messageOf(events[1]).content).toEqual(expectedContent); expect(result.content).toEqual(expectedContent); expect(JSON.stringify({ events, result })).not.toContain(""); }); @@ -1698,22 +1423,14 @@ describe("createPlainTextToolCallCompatWrapper", () => { const rawCall = ""; const visibleText = "Visible answer before the stream error."; const rawText = `${rawCall}\n${visibleText}`; - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: rawText }, - { - type: "error", - error: { - role: "assistant", - content: [ - { type: "text", text: rawCall }, - { type: "text", text: visibleText }, - ], - message: "stream failed", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + textDelta(rawText), + errorEvent({ + role: "assistant", + content: [textBlock(rawCall), textBlock(visibleText)], + message: "stream failed", + }), + ]); expect(events.map((event) => requireRecord(event, "event").type)).toEqual([ "text_delta", @@ -1729,20 +1446,7 @@ describe("createPlainTextToolCallCompatWrapper", () => { it("preserves visible suffix text after an over-cap JSON tool payload", async () => { const visibleSuffix = "Visible answer after oversized JSON."; const rawText = [`[tool:read] {"path":"${"x".repeat(256_001)}"}`, visibleSuffix].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: rawText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectTextDoneEvents([rawText], rawText); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "text_delta", @@ -1760,26 +1464,11 @@ describe("createPlainTextToolCallCompatWrapper", () => { const toolPrefix = ["[tool:read]", "", "x".repeat(256_001)].join("\n"); const visibleSuffix = "Visible answer after the tool-looking prefix."; const rawText = [toolPrefix, "", "", visibleSuffix].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: toolPrefix }, - { - type: "text_delta", - contentIndex: 0, - delta: ["", "", visibleSuffix].join("\n"), - }, - { type: "text_end", contentIndex: 0, content: rawText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectTextDoneEvents( + [toolPrefix, ["", "", visibleSuffix].join("\n")], + rawText, + true, + ); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "text_delta", @@ -1793,26 +1482,11 @@ describe("createPlainTextToolCallCompatWrapper", () => { const toolPrefix = ["[tool:read]", "", `${"x".repeat(256_001)}İ`].join("\n"); const visibleSuffix = "Visible suffix after Unicode payload."; const rawText = [toolPrefix, "", "", visibleSuffix].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: toolPrefix }, - { - type: "text_delta", - contentIndex: 0, - delta: ["", "", visibleSuffix].join("\n"), - }, - { type: "text_end", contentIndex: 0, content: rawText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectTextDoneEvents( + [toolPrefix, ["", "", visibleSuffix].join("\n")], + rawText, + true, + ); expect(String(requireRecord(events[0], "text event").delta)).toBe(visibleSuffix); expect(JSON.stringify(events)).not.toContain(""); @@ -1823,24 +1497,14 @@ describe("createPlainTextToolCallCompatWrapper", () => { const toolPrefix = ["[tool:read]", "", "x".repeat(256_001)].join("\n"); const visibleSuffix = "Visible answer before the stream error."; const rawText = [toolPrefix, "", "", visibleSuffix].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: toolPrefix }, - { - type: "text_delta", - contentIndex: 0, - delta: ["", "", visibleSuffix].join("\n"), - }, - { - type: "error", - partial: { content: [{ type: "text", text: rawText }] }, - error: { - content: [{ type: "text", text: rawText }], - message: "stream failed", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + textDelta(toolPrefix), + textDelta(["", "", visibleSuffix].join("\n")), + errorEvent( + { content: [textBlock(rawText)], message: "stream failed" }, + { content: [textBlock(rawText)] }, + ), + ]); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "text_delta", @@ -1860,25 +1524,10 @@ describe("createPlainTextToolCallCompatWrapper", () => { const toolPrefix = ["[tool:read]", "", "x".repeat(400_000)].join("\n"); const visibleSuffix = "Visible answer after a very large tool-looking prefix."; const rawText = [toolPrefix, "", "", visibleSuffix].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: toolPrefix }, - { - type: "text_delta", - contentIndex: 0, - delta: ["", "", visibleSuffix].join("\n"), - }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectTextDoneEvents( + [toolPrefix, ["", "", visibleSuffix].join("\n")], + rawText, + ); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "text_delta", @@ -1893,30 +1542,14 @@ describe("createPlainTextToolCallCompatWrapper", () => { const visibleSuffix = "Visible answer after the incomplete tool-looking block."; const tail = ["", visibleSuffix].join("\n"); const rawText = [toolPrefix, tail].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: toolPrefix }, - { type: "text_delta", contentIndex: 0, delta: tail }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectTextDoneEvents([toolPrefix, tail], rawText); expect(events.map((event) => requireRecord(event, "event").type)).toEqual([ "text_delta", "done", ]); expect(requireRecord(events[0], "text event").delta).toBe(visibleSuffix); - expect( - requireRecord(requireRecord(events[1], "done event").message, "done message").content, - ).toEqual([{ type: "text", text: visibleSuffix }]); + expect(messageOf(events[1]).content).toEqual([{ type: "text", text: visibleSuffix }]); expect(JSON.stringify(events)).not.toContain("[read]"); expect(JSON.stringify(events)).not.toContain(""); }); @@ -1925,26 +1558,10 @@ describe("createPlainTextToolCallCompatWrapper", () => { const toolPrefix = ["[tool:read]", "", "x".repeat(400_000)].join("\n"); const visibleSuffix = "Visible answer after a split terminator."; const rawText = [toolPrefix, "", "", visibleSuffix].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: toolPrefix }, - { type: "text_delta", contentIndex: 0, delta: "", "", visibleSuffix].join("\n"), - }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectTextDoneEvents( + [toolPrefix, "", "", visibleSuffix].join("\n")], + rawText, + ); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "text_delta", @@ -1958,25 +1575,10 @@ describe("createPlainTextToolCallCompatWrapper", () => { const toolPrefix = ["[tool:read]", "", "x".repeat(400_000)].join("\n"); const visibleSuffix = `Visible answer ${"y".repeat(70_000)}`; const rawText = [toolPrefix, "", "", visibleSuffix].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: toolPrefix }, - { - type: "text_delta", - contentIndex: 0, - delta: ["", "", visibleSuffix].join("\n"), - }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectTextDoneEvents( + [toolPrefix, ["", "", visibleSuffix].join("\n")], + rawText, + ); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "text_delta", @@ -1993,21 +1595,11 @@ describe("createPlainTextToolCallCompatWrapper", () => { ])("does not duplicate visible suffix text when %s", async (_name, deltaIndex, endIndex) => { const visibleSuffix = "Visible answer from a mixed-index stream."; const rawText = [`[tool:read] {"path":"${"x".repeat(256_001)}"}`, visibleSuffix].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", ...deltaIndex, delta: rawText }, - { type: "text_end", ...endIndex, content: rawText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + { type: "text_delta", ...deltaIndex, delta: rawText }, + { type: "text_end", ...endIndex, content: rawText }, + doneEvent([textBlock(rawText)]), + ]); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ "text_delta", @@ -2023,18 +1615,10 @@ describe("createPlainTextToolCallCompatWrapper", () => { const second = `TWO\n${call}THREE`; const rawText = first + second; const events = await collectPlainTextToolCallCompatEvents([ - { type: "text_delta", contentIndex: 0, delta: first }, - { type: "text_delta", contentIndex: 0, delta: second }, - { type: "text_end", contentIndex: 0, content: rawText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, + textDelta(first), + textDelta(second), + textEnd(rawText), + doneEvent([textBlock(rawText)]), ]); expect(events.map((event) => event.type)).toEqual([ @@ -2044,9 +1628,7 @@ describe("createPlainTextToolCallCompatWrapper", () => { "done", ]); expect(events.slice(0, 3).map((event) => event.delta)).toEqual(["ONE\n", "TWO\n", "THREE"]); - expect( - requireRecord(requireRecord(events.at(-1), "done event").message, "done message").content, - ).toEqual([{ type: "text", text: "ONE\nTWO\nTHREE" }]); + expect(messageOf(events.at(-1)).content).toEqual([{ type: "text", text: "ONE\nTWO\nTHREE" }]); }); it("keeps partial snapshots current for multi-delta visible suffix text", async () => { @@ -2055,27 +1637,12 @@ describe("createPlainTextToolCallCompatWrapper", () => { const rawPrefix = `[tool:read] {"path":"${"x".repeat(256_001)}"}`; const firstChunk = [rawPrefix, firstVisible].join("\n"); const rawText = `${firstChunk}${secondVisible}`; - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: firstChunk }, - { - type: "text_delta", - contentIndex: 0, - delta: secondVisible, - partial: { content: [{ type: "text", text: rawText }] }, - }, - { type: "text_end", contentIndex: 0, content: rawText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawText }], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + textDelta(firstChunk), + textDelta(secondVisible, 0, { content: [textBlock(rawText)] }), + textEnd(rawText), + doneEvent([textBlock(rawText)]), + ]); const secondEvent = requireRecord(events[1], "second text event"); expect(events.map((event) => (event as { type?: string }).type)).toEqual([ @@ -2094,30 +1661,14 @@ describe("createPlainTextToolCallCompatWrapper", () => { const introText = "Intro text before the reclassified block."; const visibleSuffix = "Visible suffix from the reclassified block."; const rawToolText = [`[tool:read] {"path":"${"x".repeat(256_001)}"}`, visibleSuffix].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", contentIndex: 0, delta: introText }, - { type: "text_delta", contentIndex: 1, delta: rawToolText }, - { type: "text_end", contentIndex: 1, content: rawToolText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [ - { type: "text", text: introText }, - { type: "text", text: rawToolText }, - ], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + textDelta(introText), + textDelta(rawToolText, 1), + textEnd(rawToolText, 1), + doneEvent([textBlock(introText), textBlock(rawToolText)]), + ]); - const doneMessage = requireRecord( - requireRecord(events.at(-1), "done event").message, - "done message", - ); + const doneMessage = messageOf(events.at(-1)); expect(doneMessage.content).toEqual([ { type: "text", text: introText }, { type: "text", text: visibleSuffix }, @@ -2129,28 +1680,12 @@ describe("createPlainTextToolCallCompatWrapper", () => { const visibleSuffix = "Visible suffix from the reclassified block."; const laterText = "Additional visible answer text."; const rawToolText = [`[tool:read] {"path":"${"x".repeat(256_001)}"}`, visibleSuffix].join("\n"); - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", delta: rawToolText }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [ - { type: "text", text: rawToolText }, - { type: "text", text: laterText }, - ], - stopReason: "stop", - }, - }, - ]); - const events = await collectPlainTextToolCallCompatEventsFromStream(baseStreamFn); + const events = await collectPlainTextToolCallCompatEvents([ + { type: "text_delta", delta: rawToolText }, + doneEvent([textBlock(rawToolText), textBlock(laterText)]), + ]); - const doneMessage = requireRecord( - requireRecord(events.at(-1), "done event").message, - "done message", - ); + const doneMessage = messageOf(events.at(-1)); expect(doneMessage.content).toEqual([ { type: "text", text: visibleSuffix }, { type: "text", text: laterText }, @@ -2176,15 +1711,7 @@ describe("createPlainTextToolCallCompatWrapper", () => { source.push({ type: "start", partial: { content: [] } } as never); expect((await nextEvent(iterator, "start")).type).toBe("start"); source.push({ type: "text_delta", contentIndex: 0, delta: rawToolText } as never); - source.push({ - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawToolText }], - stopReason: "stop", - }, - } as never); + source.push(doneEvent([textBlock(rawToolText)]) as never); expect((await nextEvent(iterator, `converted ${name}`)).type).toBe("toolcall_start"); } finally { source.end(); @@ -2214,15 +1741,7 @@ describe("createPlainTextToolCallCompatWrapper", () => { }, } as never); } - source.push({ - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawToolText }], - stopReason: "stop", - }, - } as never); + source.push(doneEvent([textBlock(rawToolText)]) as never); const events = [ await nextEvent(iterator, "zero-argument tool-call start"),