assistant-stream 0.2.37 → 0.2.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts +19 -0
- package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts.map +1 -0
- package/dist/core/serialization/assistant-transport/AssistantTransport.js +117 -0
- package/dist/core/serialization/assistant-transport/AssistantTransport.js.map +1 -0
- package/dist/core/serialization/assistant-transport/AssistantTransport.test.d.ts +2 -0
- package/dist/core/serialization/assistant-transport/AssistantTransport.test.d.ts.map +1 -0
- package/package.json +3 -5
- package/src/core/index.ts +4 -0
- package/src/core/serialization/assistant-transport/AssistantTransport.test.ts +185 -0
- package/src/core/serialization/assistant-transport/AssistantTransport.ts +149 -0
package/dist/core/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type { AssistantStreamController } from "./modules/assistant-stream";
|
|
|
5
5
|
export type { AssistantStreamChunk } from "./AssistantStreamChunk";
|
|
6
6
|
export { DataStreamDecoder, DataStreamEncoder, } from "./serialization/data-stream/DataStream";
|
|
7
7
|
export { PlainTextDecoder, PlainTextEncoder } from "./serialization/PlainText";
|
|
8
|
+
export { AssistantTransportDecoder, AssistantTransportEncoder, } from "./serialization/assistant-transport/AssistantTransport";
|
|
8
9
|
export { AssistantMessageStream } from "./accumulators/AssistantMessageStream";
|
|
9
10
|
export type { AssistantMessage } from "./utils/types";
|
|
10
11
|
export * from "./tool";
|
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,6BAA6B,EAC7B,+BAA+B,GAChC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,IAAI,6BAA6B,GACtD,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAC5E,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EACL,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,cAAc,QAAQ,CAAC;AACvB,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,YAAY,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,6BAA6B,EAC7B,+BAA+B,GAChC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,IAAI,6BAA6B,GACtD,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAC5E,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EACL,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC/E,OAAO,EACL,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,wDAAwD,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,cAAc,QAAQ,CAAC;AACvB,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,YAAY,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/core/index.js
CHANGED
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
DataStreamEncoder
|
|
15
15
|
} from "./serialization/data-stream/DataStream.js";
|
|
16
16
|
import { PlainTextDecoder, PlainTextEncoder } from "./serialization/PlainText.js";
|
|
17
|
+
import {
|
|
18
|
+
AssistantTransportDecoder,
|
|
19
|
+
AssistantTransportEncoder
|
|
20
|
+
} from "./serialization/assistant-transport/AssistantTransport.js";
|
|
17
21
|
import { AssistantMessageStream } from "./accumulators/AssistantMessageStream.js";
|
|
18
22
|
export * from "./tool/index.js";
|
|
19
23
|
import { createObjectStream } from "./object/createObjectStream.js";
|
|
@@ -25,6 +29,8 @@ export {
|
|
|
25
29
|
AssistantMessageAccumulator,
|
|
26
30
|
AssistantMessageStream,
|
|
27
31
|
AssistantStream,
|
|
32
|
+
AssistantTransportDecoder,
|
|
33
|
+
AssistantTransportEncoder,
|
|
28
34
|
DataStreamDecoder,
|
|
29
35
|
DataStreamEncoder,
|
|
30
36
|
ObjectStreamResponse,
|
package/dist/core/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/index.ts"],"sourcesContent":["export {\n createAssistantStream,\n createAssistantStreamResponse,\n createAssistantStreamController,\n} from \"./modules/assistant-stream\";\nexport {\n AssistantMessageAccumulator,\n createInitialMessage as unstable_createInitialMessage,\n} from \"./accumulators/assistant-message-accumulator\";\nexport { AssistantStream } from \"./AssistantStream\";\nexport type { AssistantStreamController } from \"./modules/assistant-stream\";\nexport type { AssistantStreamChunk } from \"./AssistantStreamChunk\";\nexport {\n DataStreamDecoder,\n DataStreamEncoder,\n} from \"./serialization/data-stream/DataStream\";\nexport { PlainTextDecoder, PlainTextEncoder } from \"./serialization/PlainText\";\nexport { AssistantMessageStream } from \"./accumulators/AssistantMessageStream\";\nexport type { AssistantMessage } from \"./utils/types\";\n\nexport * from \"./tool\";\nexport type { TextStreamController } from \"./modules/text\";\nexport type { ToolCallStreamController } from \"./modules/tool-call\";\n\nexport { createObjectStream } from \"./object/createObjectStream\";\nexport {\n ObjectStreamResponse,\n fromObjectStreamResponse,\n} from \"./object/ObjectStreamResponse\";\nexport type { ObjectStreamChunk } from \"./object/types\";\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACwB;AAAA,OACnB;AACP,SAAS,uBAAuB;AAGhC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB,wBAAwB;AACnD,SAAS,8BAA8B;AAGvC,cAAc;AAId,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/index.ts"],"sourcesContent":["export {\n createAssistantStream,\n createAssistantStreamResponse,\n createAssistantStreamController,\n} from \"./modules/assistant-stream\";\nexport {\n AssistantMessageAccumulator,\n createInitialMessage as unstable_createInitialMessage,\n} from \"./accumulators/assistant-message-accumulator\";\nexport { AssistantStream } from \"./AssistantStream\";\nexport type { AssistantStreamController } from \"./modules/assistant-stream\";\nexport type { AssistantStreamChunk } from \"./AssistantStreamChunk\";\nexport {\n DataStreamDecoder,\n DataStreamEncoder,\n} from \"./serialization/data-stream/DataStream\";\nexport { PlainTextDecoder, PlainTextEncoder } from \"./serialization/PlainText\";\nexport {\n AssistantTransportDecoder,\n AssistantTransportEncoder,\n} from \"./serialization/assistant-transport/AssistantTransport\";\nexport { AssistantMessageStream } from \"./accumulators/AssistantMessageStream\";\nexport type { AssistantMessage } from \"./utils/types\";\n\nexport * from \"./tool\";\nexport type { TextStreamController } from \"./modules/text\";\nexport type { ToolCallStreamController } from \"./modules/tool-call\";\n\nexport { createObjectStream } from \"./object/createObjectStream\";\nexport {\n ObjectStreamResponse,\n fromObjectStreamResponse,\n} from \"./object/ObjectStreamResponse\";\nexport type { ObjectStreamChunk } from \"./object/types\";\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACwB;AAAA,OACnB;AACP,SAAS,uBAAuB;AAGhC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB,wBAAwB;AACnD;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,8BAA8B;AAGvC,cAAc;AAId,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AssistantStreamChunk } from "../../AssistantStreamChunk";
|
|
2
|
+
import { PipeableTransformStream } from "../../utils/stream/PipeableTransformStream";
|
|
3
|
+
import { AssistantStreamEncoder } from "../../AssistantStream";
|
|
4
|
+
/**
|
|
5
|
+
* AssistantTransportEncoder encodes AssistantStreamChunks into SSE format
|
|
6
|
+
* and emits [DONE] when the stream completes.
|
|
7
|
+
*/
|
|
8
|
+
export declare class AssistantTransportEncoder extends PipeableTransformStream<AssistantStreamChunk, Uint8Array<ArrayBuffer>> implements AssistantStreamEncoder {
|
|
9
|
+
headers: Headers;
|
|
10
|
+
constructor();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* AssistantTransportDecoder decodes SSE format into AssistantStreamChunks.
|
|
14
|
+
* It stops decoding when it encounters [DONE].
|
|
15
|
+
*/
|
|
16
|
+
export declare class AssistantTransportDecoder extends PipeableTransformStream<Uint8Array<ArrayBuffer>, AssistantStreamChunk> {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=AssistantTransport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssistantTransport.d.ts","sourceRoot":"","sources":["../../../../src/core/serialization/assistant-transport/AssistantTransport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,4CAA4C,CAAC;AAErF,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D;;;GAGG;AACH,qBAAa,yBACX,SAAQ,uBAAuB,CAAC,oBAAoB,EAAE,UAAU,CAAC,WAAW,CAAC,CAC7E,YAAW,sBAAsB;IAEjC,OAAO,UAIJ;;CAkBJ;AAoED;;;GAGG;AACH,qBAAa,yBAA0B,SAAQ,uBAAuB,CACpE,UAAU,CAAC,WAAW,CAAC,EACvB,oBAAoB,CACrB;;CAsCA"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// src/core/serialization/assistant-transport/AssistantTransport.ts
|
|
2
|
+
import { PipeableTransformStream } from "../../utils/stream/PipeableTransformStream.js";
|
|
3
|
+
import { LineDecoderStream } from "../../utils/stream/LineDecoderStream.js";
|
|
4
|
+
var AssistantTransportEncoder = class extends PipeableTransformStream {
|
|
5
|
+
headers = new Headers({
|
|
6
|
+
"Content-Type": "text/event-stream",
|
|
7
|
+
"Cache-Control": "no-cache",
|
|
8
|
+
Connection: "keep-alive"
|
|
9
|
+
});
|
|
10
|
+
constructor() {
|
|
11
|
+
super((readable) => {
|
|
12
|
+
return readable.pipeThrough(
|
|
13
|
+
new TransformStream({
|
|
14
|
+
transform(chunk, controller) {
|
|
15
|
+
controller.enqueue(`data: ${JSON.stringify(chunk)}
|
|
16
|
+
|
|
17
|
+
`);
|
|
18
|
+
},
|
|
19
|
+
flush(controller) {
|
|
20
|
+
controller.enqueue("data: [DONE]\n\n");
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
).pipeThrough(new TextEncoderStream());
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var SSEEventStream = class extends TransformStream {
|
|
28
|
+
constructor() {
|
|
29
|
+
let eventBuffer = {};
|
|
30
|
+
let dataLines = [];
|
|
31
|
+
super({
|
|
32
|
+
start() {
|
|
33
|
+
eventBuffer = {};
|
|
34
|
+
dataLines = [];
|
|
35
|
+
},
|
|
36
|
+
transform(line, controller) {
|
|
37
|
+
if (line.startsWith(":")) return;
|
|
38
|
+
if (line === "") {
|
|
39
|
+
if (dataLines.length > 0) {
|
|
40
|
+
controller.enqueue({
|
|
41
|
+
event: eventBuffer.event || "message",
|
|
42
|
+
data: dataLines.join("\n"),
|
|
43
|
+
id: eventBuffer.id,
|
|
44
|
+
retry: eventBuffer.retry
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
eventBuffer = {};
|
|
48
|
+
dataLines = [];
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const [field, ...rest] = line.split(":");
|
|
52
|
+
const value = rest.join(":").trimStart();
|
|
53
|
+
switch (field) {
|
|
54
|
+
case "event":
|
|
55
|
+
eventBuffer.event = value;
|
|
56
|
+
break;
|
|
57
|
+
case "data":
|
|
58
|
+
dataLines.push(value);
|
|
59
|
+
break;
|
|
60
|
+
case "id":
|
|
61
|
+
eventBuffer.id = value;
|
|
62
|
+
break;
|
|
63
|
+
case "retry":
|
|
64
|
+
eventBuffer.retry = Number(value);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
flush(controller) {
|
|
69
|
+
if (dataLines.length > 0) {
|
|
70
|
+
controller.enqueue({
|
|
71
|
+
event: eventBuffer.event || "message",
|
|
72
|
+
data: dataLines.join("\n"),
|
|
73
|
+
id: eventBuffer.id,
|
|
74
|
+
retry: eventBuffer.retry
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var AssistantTransportDecoder = class extends PipeableTransformStream {
|
|
82
|
+
constructor() {
|
|
83
|
+
super((readable) => {
|
|
84
|
+
let receivedDone = false;
|
|
85
|
+
return readable.pipeThrough(new TextDecoderStream()).pipeThrough(new LineDecoderStream()).pipeThrough(new SSEEventStream()).pipeThrough(
|
|
86
|
+
new TransformStream({
|
|
87
|
+
transform(event, controller) {
|
|
88
|
+
switch (event.event) {
|
|
89
|
+
case "message":
|
|
90
|
+
if (event.data === "[DONE]") {
|
|
91
|
+
receivedDone = true;
|
|
92
|
+
controller.terminate();
|
|
93
|
+
} else {
|
|
94
|
+
controller.enqueue(JSON.parse(event.data));
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`Unknown SSE event type: ${event.event}`);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
flush() {
|
|
102
|
+
if (!receivedDone) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
"Stream ended abruptly without receiving [DONE] marker"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
export {
|
|
114
|
+
AssistantTransportDecoder,
|
|
115
|
+
AssistantTransportEncoder
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=AssistantTransport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/core/serialization/assistant-transport/AssistantTransport.ts"],"sourcesContent":["import { AssistantStreamChunk } from \"../../AssistantStreamChunk\";\nimport { PipeableTransformStream } from \"../../utils/stream/PipeableTransformStream\";\nimport { LineDecoderStream } from \"../../utils/stream/LineDecoderStream\";\nimport { AssistantStreamEncoder } from \"../../AssistantStream\";\n\n/**\n * AssistantTransportEncoder encodes AssistantStreamChunks into SSE format\n * and emits [DONE] when the stream completes.\n */\nexport class AssistantTransportEncoder\n extends PipeableTransformStream<AssistantStreamChunk, Uint8Array<ArrayBuffer>>\n implements AssistantStreamEncoder\n{\n headers = new Headers({\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n\n constructor() {\n super((readable) => {\n return readable\n .pipeThrough(\n new TransformStream<AssistantStreamChunk, string>({\n transform(chunk, controller) {\n controller.enqueue(`data: ${JSON.stringify(chunk)}\\n\\n`);\n },\n flush(controller) {\n controller.enqueue(\"data: [DONE]\\n\\n\");\n },\n }),\n )\n .pipeThrough(new TextEncoderStream());\n });\n }\n}\n\ntype SSEEvent = {\n event: string;\n data: string;\n id?: string | undefined;\n retry?: number | undefined;\n};\n\nclass SSEEventStream extends TransformStream<string, SSEEvent> {\n constructor() {\n let eventBuffer: Partial<SSEEvent> = {};\n let dataLines: string[] = [];\n\n super({\n start() {\n eventBuffer = {};\n dataLines = [];\n },\n transform(line, controller) {\n if (line.startsWith(\":\")) return; // Ignore comments\n\n if (line === \"\") {\n if (dataLines.length > 0) {\n controller.enqueue({\n event: eventBuffer.event || \"message\",\n data: dataLines.join(\"\\n\"),\n id: eventBuffer.id,\n retry: eventBuffer.retry,\n });\n }\n eventBuffer = {};\n dataLines = [];\n return;\n }\n\n const [field, ...rest] = line.split(\":\");\n const value = rest.join(\":\").trimStart();\n\n switch (field) {\n case \"event\":\n eventBuffer.event = value;\n break;\n case \"data\":\n dataLines.push(value);\n break;\n case \"id\":\n eventBuffer.id = value;\n break;\n case \"retry\":\n eventBuffer.retry = Number(value);\n break;\n }\n },\n flush(controller) {\n if (dataLines.length > 0) {\n controller.enqueue({\n event: eventBuffer.event || \"message\",\n data: dataLines.join(\"\\n\"),\n id: eventBuffer.id,\n retry: eventBuffer.retry,\n });\n }\n },\n });\n }\n}\n\n/**\n * AssistantTransportDecoder decodes SSE format into AssistantStreamChunks.\n * It stops decoding when it encounters [DONE].\n */\nexport class AssistantTransportDecoder extends PipeableTransformStream<\n Uint8Array<ArrayBuffer>,\n AssistantStreamChunk\n> {\n constructor() {\n super((readable) => {\n let receivedDone = false;\n\n return readable\n .pipeThrough(new TextDecoderStream())\n .pipeThrough(new LineDecoderStream())\n .pipeThrough(new SSEEventStream())\n .pipeThrough(\n new TransformStream<SSEEvent, AssistantStreamChunk>({\n transform(event, controller) {\n switch (event.event) {\n case \"message\":\n if (event.data === \"[DONE]\") {\n // Mark that we received [DONE]\n receivedDone = true;\n // Stop processing when we encounter [DONE]\n controller.terminate();\n } else {\n controller.enqueue(JSON.parse(event.data));\n }\n break;\n default:\n throw new Error(`Unknown SSE event type: ${event.event}`);\n }\n },\n flush() {\n if (!receivedDone) {\n throw new Error(\n \"Stream ended abruptly without receiving [DONE] marker\",\n );\n }\n },\n }),\n );\n });\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssistantTransport.test.d.ts","sourceRoot":"","sources":["../../../../src/core/serialization/assistant-transport/AssistantTransport.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assistant-stream",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.39",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -25,11 +25,9 @@
|
|
|
25
25
|
"sideEffects": false,
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@standard-schema/spec": "^1.0.0",
|
|
28
|
-
"@types/node": "^24.
|
|
29
|
-
"eslint": "^9",
|
|
30
|
-
"eslint-config-next": "16.0.0",
|
|
28
|
+
"@types/node": "^24.10.0",
|
|
31
29
|
"tsx": "^4.20.6",
|
|
32
|
-
"vitest": "^4.0.
|
|
30
|
+
"vitest": "^4.0.6",
|
|
33
31
|
"@assistant-ui/x-buildutils": "0.0.1"
|
|
34
32
|
},
|
|
35
33
|
"publishConfig": {
|
package/src/core/index.ts
CHANGED
|
@@ -15,6 +15,10 @@ export {
|
|
|
15
15
|
DataStreamEncoder,
|
|
16
16
|
} from "./serialization/data-stream/DataStream";
|
|
17
17
|
export { PlainTextDecoder, PlainTextEncoder } from "./serialization/PlainText";
|
|
18
|
+
export {
|
|
19
|
+
AssistantTransportDecoder,
|
|
20
|
+
AssistantTransportEncoder,
|
|
21
|
+
} from "./serialization/assistant-transport/AssistantTransport";
|
|
18
22
|
export { AssistantMessageStream } from "./accumulators/AssistantMessageStream";
|
|
19
23
|
export type { AssistantMessage } from "./utils/types";
|
|
20
24
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
AssistantTransportEncoder,
|
|
4
|
+
AssistantTransportDecoder,
|
|
5
|
+
} from "./AssistantTransport";
|
|
6
|
+
import { AssistantStreamChunk } from "../../AssistantStreamChunk";
|
|
7
|
+
|
|
8
|
+
// Helper function to collect all chunks from a stream
|
|
9
|
+
async function collectChunks<T>(stream: ReadableStream<T>): Promise<T[]> {
|
|
10
|
+
const reader = stream.getReader();
|
|
11
|
+
const chunks: T[] = [];
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
while (true) {
|
|
15
|
+
const { done, value } = await reader.read();
|
|
16
|
+
if (done) break;
|
|
17
|
+
chunks.push(value);
|
|
18
|
+
}
|
|
19
|
+
} finally {
|
|
20
|
+
reader.releaseLock();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return chunks;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Helper function to encode and decode a stream
|
|
27
|
+
async function encodeAndDecode(
|
|
28
|
+
stream: ReadableStream<AssistantStreamChunk>,
|
|
29
|
+
): Promise<ReadableStream<AssistantStreamChunk>> {
|
|
30
|
+
// Encode the stream to Uint8Array (simulating network transmission)
|
|
31
|
+
const encodedStream = stream.pipeThrough(new AssistantTransportEncoder());
|
|
32
|
+
|
|
33
|
+
// Collect all encoded chunks
|
|
34
|
+
const encodedChunks = await collectChunks(encodedStream);
|
|
35
|
+
|
|
36
|
+
// Create a new stream from the encoded chunks
|
|
37
|
+
const reconstructedStream = new ReadableStream<Uint8Array>({
|
|
38
|
+
start(controller) {
|
|
39
|
+
for (const chunk of encodedChunks) {
|
|
40
|
+
controller.enqueue(chunk);
|
|
41
|
+
}
|
|
42
|
+
controller.close();
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Decode the reconstructed stream
|
|
47
|
+
return reconstructedStream.pipeThrough(new AssistantTransportDecoder());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("AssistantTransportEncoder", () => {
|
|
51
|
+
it("should encode text-delta chunks to SSE format", async () => {
|
|
52
|
+
const chunks: AssistantStreamChunk[] = [
|
|
53
|
+
{ type: "text-delta", textDelta: "Hello", path: [] },
|
|
54
|
+
{ type: "text-delta", textDelta: " world", path: [] },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const stream = new ReadableStream<AssistantStreamChunk>({
|
|
58
|
+
start(controller) {
|
|
59
|
+
for (const chunk of chunks) {
|
|
60
|
+
controller.enqueue(chunk);
|
|
61
|
+
}
|
|
62
|
+
controller.close();
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const encodedStream = stream.pipeThrough(new AssistantTransportEncoder());
|
|
67
|
+
const encodedChunks = await collectChunks(encodedStream);
|
|
68
|
+
|
|
69
|
+
// Decode the chunks to verify format
|
|
70
|
+
const decoder = new TextDecoder();
|
|
71
|
+
const text = encodedChunks.map((chunk) => decoder.decode(chunk)).join("");
|
|
72
|
+
|
|
73
|
+
// Should contain SSE formatted data
|
|
74
|
+
expect(text).toContain('data: {"type":"text-delta"');
|
|
75
|
+
expect(text).toContain('"textDelta":"Hello"');
|
|
76
|
+
expect(text).toContain('"textDelta":" world"');
|
|
77
|
+
// Should end with [DONE]
|
|
78
|
+
expect(text).toContain("data: [DONE]");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should set correct headers", () => {
|
|
82
|
+
const encoder = new AssistantTransportEncoder();
|
|
83
|
+
expect(encoder.headers.get("Content-Type")).toBe("text/event-stream");
|
|
84
|
+
expect(encoder.headers.get("Cache-Control")).toBe("no-cache");
|
|
85
|
+
expect(encoder.headers.get("Connection")).toBe("keep-alive");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("AssistantTransportDecoder", () => {
|
|
90
|
+
it("should decode SSE format back to chunks", async () => {
|
|
91
|
+
const originalChunks: AssistantStreamChunk[] = [
|
|
92
|
+
{ type: "text-delta", textDelta: "Hello", path: [] },
|
|
93
|
+
{ type: "text-delta", textDelta: " world", path: [] },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const stream = new ReadableStream<AssistantStreamChunk>({
|
|
97
|
+
start(controller) {
|
|
98
|
+
for (const chunk of originalChunks) {
|
|
99
|
+
controller.enqueue(chunk);
|
|
100
|
+
}
|
|
101
|
+
controller.close();
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
106
|
+
const decodedChunks = await collectChunks(decodedStream);
|
|
107
|
+
|
|
108
|
+
expect(decodedChunks).toEqual(originalChunks);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should stop decoding at [DONE]", async () => {
|
|
112
|
+
// Manually create an SSE stream with [DONE] in the middle
|
|
113
|
+
const sseText =
|
|
114
|
+
'data: {"type":"text-delta","textDelta":"Hello","path":[]}\n\n' +
|
|
115
|
+
"data: [DONE]\n\n" +
|
|
116
|
+
'data: {"type":"text-delta","textDelta":"Should not appear","path":[]}\n\n';
|
|
117
|
+
|
|
118
|
+
const encoder = new TextEncoder();
|
|
119
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
120
|
+
start(controller) {
|
|
121
|
+
controller.enqueue(encoder.encode(sseText));
|
|
122
|
+
controller.close();
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const decodedStream = stream.pipeThrough(new AssistantTransportDecoder());
|
|
127
|
+
const decodedChunks = await collectChunks(decodedStream);
|
|
128
|
+
|
|
129
|
+
// Should only have the chunk before [DONE]
|
|
130
|
+
expect(decodedChunks).toHaveLength(1);
|
|
131
|
+
expect(decodedChunks[0]).toEqual({
|
|
132
|
+
type: "text-delta",
|
|
133
|
+
textDelta: "Hello",
|
|
134
|
+
path: [],
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should handle part-start chunks", async () => {
|
|
139
|
+
const originalChunks: AssistantStreamChunk[] = [
|
|
140
|
+
{
|
|
141
|
+
type: "part-start",
|
|
142
|
+
part: { type: "text" },
|
|
143
|
+
path: [],
|
|
144
|
+
},
|
|
145
|
+
{ type: "text-delta", textDelta: "Hello", path: [] },
|
|
146
|
+
{ type: "part-finish", path: [] },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const stream = new ReadableStream<AssistantStreamChunk>({
|
|
150
|
+
start(controller) {
|
|
151
|
+
for (const chunk of originalChunks) {
|
|
152
|
+
controller.enqueue(chunk);
|
|
153
|
+
}
|
|
154
|
+
controller.close();
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
159
|
+
const decodedChunks = await collectChunks(decodedStream);
|
|
160
|
+
|
|
161
|
+
expect(decodedChunks).toEqual(originalChunks);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should throw error when stream ends without [DONE]", async () => {
|
|
165
|
+
// Manually create an SSE stream without [DONE]
|
|
166
|
+
const sseText =
|
|
167
|
+
'data: {"type":"text-delta","textDelta":"Hello","path":[]}\n\n' +
|
|
168
|
+
'data: {"type":"text-delta","textDelta":" world","path":[]}\n\n';
|
|
169
|
+
|
|
170
|
+
const encoder = new TextEncoder();
|
|
171
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
172
|
+
start(controller) {
|
|
173
|
+
controller.enqueue(encoder.encode(sseText));
|
|
174
|
+
controller.close();
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const decodedStream = stream.pipeThrough(new AssistantTransportDecoder());
|
|
179
|
+
|
|
180
|
+
// Should throw when trying to collect all chunks
|
|
181
|
+
await expect(collectChunks(decodedStream)).rejects.toThrow(
|
|
182
|
+
"Stream ended abruptly without receiving [DONE] marker",
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { AssistantStreamChunk } from "../../AssistantStreamChunk";
|
|
2
|
+
import { PipeableTransformStream } from "../../utils/stream/PipeableTransformStream";
|
|
3
|
+
import { LineDecoderStream } from "../../utils/stream/LineDecoderStream";
|
|
4
|
+
import { AssistantStreamEncoder } from "../../AssistantStream";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AssistantTransportEncoder encodes AssistantStreamChunks into SSE format
|
|
8
|
+
* and emits [DONE] when the stream completes.
|
|
9
|
+
*/
|
|
10
|
+
export class AssistantTransportEncoder
|
|
11
|
+
extends PipeableTransformStream<AssistantStreamChunk, Uint8Array<ArrayBuffer>>
|
|
12
|
+
implements AssistantStreamEncoder
|
|
13
|
+
{
|
|
14
|
+
headers = new Headers({
|
|
15
|
+
"Content-Type": "text/event-stream",
|
|
16
|
+
"Cache-Control": "no-cache",
|
|
17
|
+
Connection: "keep-alive",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
super((readable) => {
|
|
22
|
+
return readable
|
|
23
|
+
.pipeThrough(
|
|
24
|
+
new TransformStream<AssistantStreamChunk, string>({
|
|
25
|
+
transform(chunk, controller) {
|
|
26
|
+
controller.enqueue(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
27
|
+
},
|
|
28
|
+
flush(controller) {
|
|
29
|
+
controller.enqueue("data: [DONE]\n\n");
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
)
|
|
33
|
+
.pipeThrough(new TextEncoderStream());
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type SSEEvent = {
|
|
39
|
+
event: string;
|
|
40
|
+
data: string;
|
|
41
|
+
id?: string | undefined;
|
|
42
|
+
retry?: number | undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
class SSEEventStream extends TransformStream<string, SSEEvent> {
|
|
46
|
+
constructor() {
|
|
47
|
+
let eventBuffer: Partial<SSEEvent> = {};
|
|
48
|
+
let dataLines: string[] = [];
|
|
49
|
+
|
|
50
|
+
super({
|
|
51
|
+
start() {
|
|
52
|
+
eventBuffer = {};
|
|
53
|
+
dataLines = [];
|
|
54
|
+
},
|
|
55
|
+
transform(line, controller) {
|
|
56
|
+
if (line.startsWith(":")) return; // Ignore comments
|
|
57
|
+
|
|
58
|
+
if (line === "") {
|
|
59
|
+
if (dataLines.length > 0) {
|
|
60
|
+
controller.enqueue({
|
|
61
|
+
event: eventBuffer.event || "message",
|
|
62
|
+
data: dataLines.join("\n"),
|
|
63
|
+
id: eventBuffer.id,
|
|
64
|
+
retry: eventBuffer.retry,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
eventBuffer = {};
|
|
68
|
+
dataLines = [];
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const [field, ...rest] = line.split(":");
|
|
73
|
+
const value = rest.join(":").trimStart();
|
|
74
|
+
|
|
75
|
+
switch (field) {
|
|
76
|
+
case "event":
|
|
77
|
+
eventBuffer.event = value;
|
|
78
|
+
break;
|
|
79
|
+
case "data":
|
|
80
|
+
dataLines.push(value);
|
|
81
|
+
break;
|
|
82
|
+
case "id":
|
|
83
|
+
eventBuffer.id = value;
|
|
84
|
+
break;
|
|
85
|
+
case "retry":
|
|
86
|
+
eventBuffer.retry = Number(value);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
flush(controller) {
|
|
91
|
+
if (dataLines.length > 0) {
|
|
92
|
+
controller.enqueue({
|
|
93
|
+
event: eventBuffer.event || "message",
|
|
94
|
+
data: dataLines.join("\n"),
|
|
95
|
+
id: eventBuffer.id,
|
|
96
|
+
retry: eventBuffer.retry,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* AssistantTransportDecoder decodes SSE format into AssistantStreamChunks.
|
|
106
|
+
* It stops decoding when it encounters [DONE].
|
|
107
|
+
*/
|
|
108
|
+
export class AssistantTransportDecoder extends PipeableTransformStream<
|
|
109
|
+
Uint8Array<ArrayBuffer>,
|
|
110
|
+
AssistantStreamChunk
|
|
111
|
+
> {
|
|
112
|
+
constructor() {
|
|
113
|
+
super((readable) => {
|
|
114
|
+
let receivedDone = false;
|
|
115
|
+
|
|
116
|
+
return readable
|
|
117
|
+
.pipeThrough(new TextDecoderStream())
|
|
118
|
+
.pipeThrough(new LineDecoderStream())
|
|
119
|
+
.pipeThrough(new SSEEventStream())
|
|
120
|
+
.pipeThrough(
|
|
121
|
+
new TransformStream<SSEEvent, AssistantStreamChunk>({
|
|
122
|
+
transform(event, controller) {
|
|
123
|
+
switch (event.event) {
|
|
124
|
+
case "message":
|
|
125
|
+
if (event.data === "[DONE]") {
|
|
126
|
+
// Mark that we received [DONE]
|
|
127
|
+
receivedDone = true;
|
|
128
|
+
// Stop processing when we encounter [DONE]
|
|
129
|
+
controller.terminate();
|
|
130
|
+
} else {
|
|
131
|
+
controller.enqueue(JSON.parse(event.data));
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
throw new Error(`Unknown SSE event type: ${event.event}`);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
flush() {
|
|
139
|
+
if (!receivedDone) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"Stream ended abruptly without receiving [DONE] marker",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|