assistant-stream
Version:
Streaming utilities for AI assistants
231 lines (220 loc) • 6.78 kB
text/typescript
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { AssistantTransportDecoder } from "./AssistantTransport";
import { AssistantMessageAccumulator } from "../../accumulators/assistant-message-accumulator";
import type { AssistantStreamChunk } from "../../AssistantStreamChunk";
import type { AssistantMessage } from "../../utils/types";
// Fixture produced by the Python encoder; regenerate with
// `uv run python tests/generate_interop_fixture.py` in python/assistant-stream.
const fixture = readFileSync(
new URL("./__fixtures__/python-encoder.sse", import.meta.url),
);
const EXPECTED_CHUNKS: AssistantStreamChunk[] = [
{ type: "step-start", messageId: "msg_1", path: [] },
{ type: "part-start", part: { type: "reasoning" }, path: [] },
{ type: "text-delta", textDelta: "Let me check the weather.", path: [0] },
{ type: "part-finish", path: [0] },
{ type: "part-start", part: { type: "text" }, path: [] },
{ type: "text-delta", textDelta: "Checking", path: [1] },
{ type: "text-delta", textDelta: " now.", path: [1] },
{ type: "part-finish", path: [1] },
{
type: "part-start",
part: { type: "tool-call", toolCallId: "call_1", toolName: "get_weather" },
path: [],
},
{ type: "text-delta", textDelta: '{"city": ', path: [2] },
{ type: "text-delta", textDelta: '"NYC"}', path: [2] },
{
type: "update-state",
operations: [{ type: "set", path: ["status"], value: "running" }],
path: [],
},
{
type: "result",
result: { status: "working" },
isError: false,
isPreliminary: true,
path: [2],
},
{ type: "tool-call-args-text-finish", path: [2] },
{
type: "result",
result: { temp: 70 },
isError: false,
messages: [
{ role: "assistant", content: [{ type: "text", text: "Sunny" }] },
],
path: [2],
},
{ type: "part-finish", path: [2] },
{ type: "data", data: [{ progress: 1 }], path: [] },
{
type: "annotations",
annotations: [{ type: "citation", id: "a1" }],
path: [],
},
{
type: "part-start",
part: {
type: "source",
sourceType: "url",
id: "s1",
url: "https://example.com",
title: "Example",
},
path: [],
},
{ type: "part-finish", path: [3] },
{ type: "part-start", part: { type: "text" }, path: [] },
{ type: "text-delta", textDelta: "It is sunny.", path: [4] },
{ type: "part-finish", path: [4] },
{
type: "part-start",
part: { type: "file", data: "aGVsbG8=", mimeType: "image/png" },
path: [],
},
{ type: "part-finish", path: [5] },
{
type: "step-finish",
finishReason: "stop",
usage: { inputTokens: 12, outputTokens: 34 },
isContinued: false,
path: [],
},
{
type: "error",
error: "Forecast delayed",
code: "weather_delay",
severity: "warning",
path: [],
},
];
const decodeFixture = async (chunkSize: number) => {
const bytes = new Uint8Array(fixture);
const stream = new ReadableStream<Uint8Array<ArrayBuffer>>({
start(controller) {
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
controller.enqueue(
new Uint8Array(bytes.slice(offset, offset + chunkSize)),
);
}
controller.close();
},
});
const chunks: AssistantStreamChunk[] = [];
const reader = stream
.pipeThrough(new AssistantTransportDecoder())
.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
return chunks;
};
describe("Python encoder interop", () => {
it("decodes the captured Python encoder output into canonical chunks", async () => {
expect(await decodeFixture(fixture.length)).toEqual(EXPECTED_CHUNKS);
});
it("decodes the fixture identically under network fragmentation", async () => {
expect(await decodeFixture(5)).toEqual(EXPECTED_CHUNKS);
});
it("accumulates the decoded fixture into a canonical message", async () => {
const decoded = await decodeFixture(fixture.length);
const source = new ReadableStream<AssistantStreamChunk>({
start(controller) {
for (const chunk of decoded) {
controller.enqueue(chunk);
}
controller.close();
},
});
const messages: AssistantMessage[] = [];
await source.pipeThrough(new AssistantMessageAccumulator()).pipeTo(
new WritableStream({
write(message) {
messages.push(message);
},
}),
);
const interim = messages
.filter(
(message) =>
message.parts[2]?.type === "tool-call" &&
message.parts[2].isPreliminary,
)
.at(-1)!;
expect(interim.parts[2]).toMatchObject({
type: "tool-call",
state: "call",
args: { city: "NYC" },
result: { status: "working" },
isPreliminary: true,
status: { type: "running", isArgsComplete: true },
});
const message = messages.at(-1)!;
expect(message.parts).toHaveLength(6);
expect(message.parts[0]).toMatchObject({
type: "reasoning",
text: "Let me check the weather.",
});
expect(message.parts[1]).toMatchObject({
type: "text",
text: "Checking now.",
});
expect(message.parts[2]).toMatchObject({
type: "tool-call",
toolCallId: "call_1",
toolName: "get_weather",
state: "result",
argsText: '{"city": "NYC"}',
args: { city: "NYC" },
result: { temp: 70 },
isError: false,
messages: [
{ role: "assistant", content: [{ type: "text", text: "Sunny" }] },
],
});
expect(message.parts[2]).not.toHaveProperty("isPreliminary");
expect(message.parts[3]).toMatchObject({
type: "source",
sourceType: "url",
id: "s1",
url: "https://example.com",
title: "Example",
});
expect(message.parts[4]).toMatchObject({
type: "text",
text: "It is sunny.",
});
expect(message.parts[5]).toMatchObject({
type: "file",
mimeType: "image/png",
data: "aGVsbG8=",
});
expect(message.metadata.unstable_state).toEqual({ status: "running" });
expect(message.metadata.unstable_data).toEqual([{ progress: 1 }]);
expect(message.metadata.unstable_annotations).toEqual([
{ type: "citation", id: "a1" },
]);
expect(message.metadata.steps).toEqual([
{
state: "finished",
messageId: "msg_1",
finishReason: "stop",
usage: { inputTokens: 12, outputTokens: 34 },
isContinued: false,
},
]);
expect(message.status).toEqual({
type: "incomplete",
reason: "error",
error: {
code: "weather_delay",
message: "Forecast delayed",
severity: "warning",
},
});
});
});