assistant-stream 0.2.5 → 0.2.7
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/AssistantStreamChunk.d.ts +5 -1
- package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
- package/dist/core/accumulators/AssistantMessageStream.d.ts.map +1 -1
- package/dist/core/accumulators/AssistantMessageStream.js +1 -0
- package/dist/core/accumulators/AssistantMessageStream.js.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.js +19 -1
- package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +9 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/modules/tool-call.d.ts.map +1 -1
- package/dist/core/modules/tool-call.js +1 -1
- package/dist/core/modules/tool-call.js.map +1 -1
- package/dist/core/object/ObjectStream.test.d.ts +2 -0
- package/dist/core/object/ObjectStream.test.d.ts.map +1 -0
- package/dist/core/object/ObjectStreamAccumulator.d.ts +11 -0
- package/dist/core/object/ObjectStreamAccumulator.d.ts.map +1 -0
- package/dist/core/object/ObjectStreamAccumulator.js +64 -0
- package/dist/core/object/ObjectStreamAccumulator.js.map +1 -0
- package/dist/core/object/ObjectStreamResponse.d.ts +13 -0
- package/dist/core/object/ObjectStreamResponse.d.ts.map +1 -0
- package/dist/core/object/ObjectStreamResponse.js +66 -0
- package/dist/core/object/ObjectStreamResponse.js.map +1 -0
- package/dist/core/object/createObjectStream.d.ts +13 -0
- package/dist/core/object/createObjectStream.d.ts.map +1 -0
- package/dist/core/object/createObjectStream.js +63 -0
- package/dist/core/object/createObjectStream.js.map +1 -0
- package/dist/core/object/types.d.ts +15 -0
- package/dist/core/object/types.d.ts.map +1 -0
- package/dist/core/object/types.js +1 -0
- package/dist/core/object/types.js.map +1 -0
- package/dist/core/serialization/PlainText.d.ts.map +1 -1
- package/dist/core/serialization/PlainText.js.map +1 -1
- package/dist/core/serialization/data-stream/DataStream.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/DataStream.js +14 -0
- package/dist/core/serialization/data-stream/DataStream.js.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.d.ts +4 -1
- package/dist/core/serialization/data-stream/chunk-types.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.js +1 -0
- package/dist/core/serialization/data-stream/chunk-types.js.map +1 -1
- package/dist/core/tool/ToolCallReader.d.ts +8 -7
- package/dist/core/tool/ToolCallReader.d.ts.map +1 -1
- package/dist/core/tool/ToolCallReader.js +11 -4
- package/dist/core/tool/ToolCallReader.js.map +1 -1
- package/dist/core/tool/ToolExecutionStream.d.ts +3 -3
- package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
- package/dist/core/tool/ToolExecutionStream.js.map +1 -1
- package/dist/core/tool/ToolResponse.d.ts +2 -2
- package/dist/core/tool/ToolResponse.d.ts.map +1 -1
- package/dist/core/tool/ToolResponse.js +3 -1
- package/dist/core/tool/ToolResponse.js.map +1 -1
- package/dist/core/tool/tool-types.d.ts +4 -5
- package/dist/core/tool/tool-types.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.d.ts +2 -2
- package/dist/core/tool/toolResultStream.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.js +3 -2
- package/dist/core/tool/toolResultStream.js.map +1 -1
- package/dist/core/utils/stream/SSE.d.ts +10 -0
- package/dist/core/utils/stream/SSE.d.ts.map +1 -0
- package/dist/core/utils/stream/SSE.js +102 -0
- package/dist/core/utils/stream/SSE.js.map +1 -0
- package/dist/core/utils/types.d.ts +14 -3
- package/dist/core/utils/types.d.ts.map +1 -1
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/json/index.d.ts +2 -0
- package/dist/utils/json/index.d.ts.map +1 -0
- package/dist/utils/json/index.js +1 -0
- package/dist/utils/json/index.js.map +1 -0
- package/package.json +7 -7
- package/src/core/AssistantStreamChunk.ts +6 -1
- package/src/core/accumulators/AssistantMessageStream.ts +1 -0
- package/src/core/accumulators/assistant-message-accumulator.ts +25 -1
- package/src/core/index.ts +7 -0
- package/src/core/modules/tool-call.ts +3 -1
- package/src/core/object/ObjectStream.test.ts +376 -0
- package/src/core/object/ObjectStreamAccumulator.ts +80 -0
- package/src/core/object/ObjectStreamResponse.ts +81 -0
- package/src/core/object/createObjectStream.ts +87 -0
- package/src/core/object/types.ts +18 -0
- package/src/core/serialization/PlainText.ts +2 -1
- package/src/core/serialization/data-stream/DataStream.ts +16 -0
- package/src/core/serialization/data-stream/chunk-types.ts +6 -0
- package/src/core/tool/ToolCallReader.ts +57 -36
- package/src/core/tool/ToolExecutionStream.ts +14 -5
- package/src/core/tool/ToolResponse.ts +7 -3
- package/src/core/tool/tool-types.ts +10 -5
- package/src/core/tool/toolResultStream.ts +19 -13
- package/src/core/utils/stream/SSE.ts +116 -0
- package/src/core/utils/types.ts +18 -3
- package/src/utils/index.ts +5 -0
- package/src/utils/json/index.ts +1 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createObjectStream } from "./createObjectStream";
|
|
3
|
+
import {
|
|
4
|
+
ObjectStreamEncoder,
|
|
5
|
+
ObjectStreamDecoder,
|
|
6
|
+
} from "./ObjectStreamResponse";
|
|
7
|
+
import { ReadonlyJSONValue } from "../../utils";
|
|
8
|
+
import { ObjectStreamChunk } from "./types";
|
|
9
|
+
|
|
10
|
+
// Helper function to collect all chunks from a stream
|
|
11
|
+
async function collectChunks<T>(stream: ReadableStream<T>): Promise<T[]> {
|
|
12
|
+
const reader = stream.getReader();
|
|
13
|
+
const chunks: T[] = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
while (true) {
|
|
17
|
+
const { done, value } = await reader.read();
|
|
18
|
+
if (done) break;
|
|
19
|
+
chunks.push(value);
|
|
20
|
+
}
|
|
21
|
+
} finally {
|
|
22
|
+
reader.releaseLock();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return chunks;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Helper function to encode and decode a stream
|
|
29
|
+
async function encodeAndDecode(
|
|
30
|
+
stream: ReadableStream<ObjectStreamChunk>,
|
|
31
|
+
): Promise<ReadableStream<ObjectStreamChunk>> {
|
|
32
|
+
// Encode the stream to Uint8Array (simulating network transmission)
|
|
33
|
+
const encodedStream = stream.pipeThrough(new ObjectStreamEncoder());
|
|
34
|
+
|
|
35
|
+
// Collect all encoded chunks
|
|
36
|
+
const encodedChunks = await collectChunks(encodedStream);
|
|
37
|
+
|
|
38
|
+
// Create a new stream from the encoded chunks
|
|
39
|
+
const reconstructedStream = new ReadableStream<Uint8Array>({
|
|
40
|
+
start(controller) {
|
|
41
|
+
for (const chunk of encodedChunks) {
|
|
42
|
+
controller.enqueue(chunk);
|
|
43
|
+
}
|
|
44
|
+
controller.close();
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Decode the stream back to ObjectStreamChunk
|
|
49
|
+
return reconstructedStream.pipeThrough(new ObjectStreamDecoder());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("ObjectStream serialization and deserialization", () => {
|
|
53
|
+
it("should correctly serialize and deserialize simple objects", async () => {
|
|
54
|
+
// Create an object stream with simple operations
|
|
55
|
+
const stream = createObjectStream({
|
|
56
|
+
execute: (controller) => {
|
|
57
|
+
controller.enqueue([
|
|
58
|
+
{ type: "set", path: ["name"], value: "John" },
|
|
59
|
+
{ type: "set", path: ["age"], value: 30 },
|
|
60
|
+
]);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Encode and decode the stream
|
|
65
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
66
|
+
|
|
67
|
+
// Collect all chunks from the decoded stream
|
|
68
|
+
const chunks = await collectChunks(decodedStream);
|
|
69
|
+
|
|
70
|
+
// Verify the final state
|
|
71
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
72
|
+
expect(finalChunk.snapshot).toEqual({
|
|
73
|
+
name: "John",
|
|
74
|
+
age: 30,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should correctly handle nested objects", async () => {
|
|
79
|
+
const stream = createObjectStream({
|
|
80
|
+
execute: (controller) => {
|
|
81
|
+
controller.enqueue([
|
|
82
|
+
{ type: "set", path: ["user", "profile", "name"], value: "Jane" },
|
|
83
|
+
{
|
|
84
|
+
type: "set",
|
|
85
|
+
path: ["user", "profile", "email"],
|
|
86
|
+
value: "jane@example.com",
|
|
87
|
+
},
|
|
88
|
+
{ type: "set", path: ["user", "settings", "theme"], value: "dark" },
|
|
89
|
+
]);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
94
|
+
const chunks = await collectChunks(decodedStream);
|
|
95
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
96
|
+
|
|
97
|
+
expect(finalChunk.snapshot).toEqual({
|
|
98
|
+
user: {
|
|
99
|
+
profile: {
|
|
100
|
+
name: "Jane",
|
|
101
|
+
email: "jane@example.com",
|
|
102
|
+
},
|
|
103
|
+
settings: {
|
|
104
|
+
theme: "dark",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should correctly handle arrays", async () => {
|
|
111
|
+
const stream = createObjectStream({
|
|
112
|
+
execute: (controller) => {
|
|
113
|
+
controller.enqueue([
|
|
114
|
+
{ type: "set", path: ["items"], value: [] },
|
|
115
|
+
{ type: "set", path: ["items", "0"], value: "apple" },
|
|
116
|
+
{ type: "set", path: ["items", "1"], value: "banana" },
|
|
117
|
+
{ type: "set", path: ["items", "2"], value: "cherry" },
|
|
118
|
+
]);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
123
|
+
const chunks = await collectChunks(decodedStream);
|
|
124
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
125
|
+
|
|
126
|
+
expect(finalChunk.snapshot).toEqual({
|
|
127
|
+
items: ["apple", "banana", "cherry"],
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should correctly handle mixed arrays and objects", async () => {
|
|
132
|
+
const stream = createObjectStream({
|
|
133
|
+
execute: (controller) => {
|
|
134
|
+
controller.enqueue([
|
|
135
|
+
{ type: "set", path: ["users"], value: [] },
|
|
136
|
+
{ type: "set", path: ["users", "0"], value: {} },
|
|
137
|
+
{ type: "set", path: ["users", "0", "id"], value: 1 },
|
|
138
|
+
{ type: "set", path: ["users", "0", "name"], value: "Alice" },
|
|
139
|
+
{ type: "set", path: ["users", "1"], value: {} },
|
|
140
|
+
{ type: "set", path: ["users", "1", "id"], value: 2 },
|
|
141
|
+
{ type: "set", path: ["users", "1", "name"], value: "Bob" },
|
|
142
|
+
]);
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
147
|
+
const chunks = await collectChunks(decodedStream);
|
|
148
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
149
|
+
|
|
150
|
+
expect(finalChunk.snapshot).toEqual({
|
|
151
|
+
users: [
|
|
152
|
+
{ id: 1, name: "Alice" },
|
|
153
|
+
{ id: 2, name: "Bob" },
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should correctly handle append-text operations", async () => {
|
|
159
|
+
const stream = createObjectStream({
|
|
160
|
+
execute: (controller) => {
|
|
161
|
+
controller.enqueue([
|
|
162
|
+
{ type: "set", path: ["message"], value: "Hello" },
|
|
163
|
+
{ type: "append-text", path: ["message"], value: " " },
|
|
164
|
+
{ type: "append-text", path: ["message"], value: "World" },
|
|
165
|
+
{ type: "append-text", path: ["message"], value: "!" },
|
|
166
|
+
]);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
171
|
+
const chunks = await collectChunks(decodedStream);
|
|
172
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
173
|
+
|
|
174
|
+
expect(finalChunk.snapshot).toEqual({
|
|
175
|
+
message: "Hello World!",
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should correctly handle special characters and Unicode", async () => {
|
|
180
|
+
const stream = createObjectStream({
|
|
181
|
+
execute: (controller) => {
|
|
182
|
+
controller.enqueue([
|
|
183
|
+
{
|
|
184
|
+
type: "set",
|
|
185
|
+
path: ["special"],
|
|
186
|
+
value: "Special chars: !@#$%^&*()",
|
|
187
|
+
},
|
|
188
|
+
{ type: "set", path: ["unicode"], value: "Unicode: 😀🌍🚀" },
|
|
189
|
+
{ type: "set", path: ["quotes"], value: "Quotes: \"'`" },
|
|
190
|
+
{
|
|
191
|
+
type: "set",
|
|
192
|
+
path: ["newlines"],
|
|
193
|
+
value: "Line 1\nLine 2\r\nLine 3",
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
200
|
+
const chunks = await collectChunks(decodedStream);
|
|
201
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
202
|
+
|
|
203
|
+
expect(finalChunk.snapshot).toEqual({
|
|
204
|
+
special: "Special chars: !@#$%^&*()",
|
|
205
|
+
unicode: "Unicode: 😀🌍🚀",
|
|
206
|
+
quotes: "Quotes: \"'`",
|
|
207
|
+
newlines: "Line 1\nLine 2\r\nLine 3",
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should correctly handle null and undefined values", async () => {
|
|
212
|
+
const stream = createObjectStream({
|
|
213
|
+
execute: (controller) => {
|
|
214
|
+
controller.enqueue([
|
|
215
|
+
{ type: "set", path: ["nullValue"], value: null },
|
|
216
|
+
{ type: "set", path: ["emptyObject"], value: {} },
|
|
217
|
+
{ type: "set", path: ["emptyArray"], value: [] },
|
|
218
|
+
]);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
223
|
+
const chunks = await collectChunks(decodedStream);
|
|
224
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
225
|
+
|
|
226
|
+
expect(finalChunk.snapshot).toEqual({
|
|
227
|
+
nullValue: null,
|
|
228
|
+
emptyObject: {},
|
|
229
|
+
emptyArray: [],
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should correctly handle large nested structures", async () => {
|
|
234
|
+
// Create a deep nested structure
|
|
235
|
+
const stream = createObjectStream({
|
|
236
|
+
execute: (controller) => {
|
|
237
|
+
controller.enqueue([
|
|
238
|
+
{ type: "set", path: ["level1"], value: {} },
|
|
239
|
+
{ type: "set", path: ["level1", "level2"], value: {} },
|
|
240
|
+
{ type: "set", path: ["level1", "level2", "level3"], value: {} },
|
|
241
|
+
{
|
|
242
|
+
type: "set",
|
|
243
|
+
path: ["level1", "level2", "level3", "level4"],
|
|
244
|
+
value: {},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
type: "set",
|
|
248
|
+
path: ["level1", "level2", "level3", "level4", "level5"],
|
|
249
|
+
value: "deep value",
|
|
250
|
+
},
|
|
251
|
+
]);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
256
|
+
const chunks = await collectChunks(decodedStream);
|
|
257
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
258
|
+
|
|
259
|
+
expect(finalChunk.snapshot).toEqual({
|
|
260
|
+
level1: {
|
|
261
|
+
level2: {
|
|
262
|
+
level3: {
|
|
263
|
+
level4: {
|
|
264
|
+
level5: "deep value",
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should correctly handle operations in multiple enqueue calls", async () => {
|
|
273
|
+
const stream = createObjectStream({
|
|
274
|
+
execute: (controller) => {
|
|
275
|
+
// First batch of operations
|
|
276
|
+
controller.enqueue([
|
|
277
|
+
{ type: "set", path: ["user"], value: { name: "Initial" } },
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
// Second batch of operations
|
|
281
|
+
controller.enqueue([
|
|
282
|
+
{ type: "set", path: ["user", "name"], value: "Updated" },
|
|
283
|
+
{ type: "set", path: ["user", "email"], value: "user@example.com" },
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
// Third batch of operations
|
|
287
|
+
controller.enqueue([
|
|
288
|
+
{ type: "set", path: ["status"], value: "complete" },
|
|
289
|
+
]);
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
294
|
+
const chunks = await collectChunks(decodedStream);
|
|
295
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
296
|
+
|
|
297
|
+
expect(finalChunk.snapshot).toEqual({
|
|
298
|
+
user: {
|
|
299
|
+
name: "Updated",
|
|
300
|
+
email: "user@example.com",
|
|
301
|
+
},
|
|
302
|
+
status: "complete",
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Verify that we got the correct number of chunks
|
|
306
|
+
expect(chunks.length).toBe(3);
|
|
307
|
+
|
|
308
|
+
// Verify intermediate states
|
|
309
|
+
expect(chunks[0]!.snapshot).toEqual({
|
|
310
|
+
user: { name: "Initial" },
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
expect(chunks[1]!.snapshot).toEqual({
|
|
314
|
+
user: {
|
|
315
|
+
name: "Updated",
|
|
316
|
+
email: "user@example.com",
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("should correctly handle overwriting existing values", async () => {
|
|
322
|
+
const stream = createObjectStream({
|
|
323
|
+
execute: (controller) => {
|
|
324
|
+
controller.enqueue([
|
|
325
|
+
{ type: "set", path: ["value"], value: "initial" },
|
|
326
|
+
{ type: "set", path: ["nested"], value: { prop: "initial" } },
|
|
327
|
+
]);
|
|
328
|
+
|
|
329
|
+
controller.enqueue([
|
|
330
|
+
{ type: "set", path: ["value"], value: "updated" },
|
|
331
|
+
{ type: "set", path: ["nested"], value: "completely replaced" },
|
|
332
|
+
]);
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
337
|
+
const chunks = await collectChunks(decodedStream);
|
|
338
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
339
|
+
|
|
340
|
+
expect(finalChunk.snapshot).toEqual({
|
|
341
|
+
value: "updated",
|
|
342
|
+
nested: "completely replaced",
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("should correctly handle custom initial values", async () => {
|
|
347
|
+
const initialValue: ReadonlyJSONValue = {
|
|
348
|
+
existing: "value",
|
|
349
|
+
nested: {
|
|
350
|
+
prop: 123,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const stream = createObjectStream({
|
|
355
|
+
defaultValue: initialValue,
|
|
356
|
+
execute: (controller) => {
|
|
357
|
+
controller.enqueue([
|
|
358
|
+
{ type: "set", path: ["new"], value: "added" },
|
|
359
|
+
{ type: "set", path: ["nested", "prop"], value: 456 },
|
|
360
|
+
]);
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
365
|
+
const chunks = await collectChunks(decodedStream);
|
|
366
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
367
|
+
|
|
368
|
+
expect(finalChunk.snapshot).toEqual({
|
|
369
|
+
existing: "value",
|
|
370
|
+
nested: {
|
|
371
|
+
prop: 456,
|
|
372
|
+
},
|
|
373
|
+
new: "added",
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ReadonlyJSONValue, ReadonlyJSONObject } from "../../utils";
|
|
2
|
+
import { ObjectStreamOperation } from "./types";
|
|
3
|
+
|
|
4
|
+
export class ObjectStreamAccumulator {
|
|
5
|
+
private _state: ReadonlyJSONValue;
|
|
6
|
+
|
|
7
|
+
constructor(initialValue: ReadonlyJSONValue = null) {
|
|
8
|
+
this._state = initialValue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get state() {
|
|
12
|
+
return this._state;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
append(ops: readonly ObjectStreamOperation[]) {
|
|
16
|
+
this._state = ops.reduce(
|
|
17
|
+
(state, op) => ObjectStreamAccumulator.apply(state, op),
|
|
18
|
+
this._state,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private static apply(state: ReadonlyJSONValue, op: ObjectStreamOperation) {
|
|
23
|
+
const type = op.type;
|
|
24
|
+
switch (type) {
|
|
25
|
+
case "set":
|
|
26
|
+
return ObjectStreamAccumulator.updatePath(
|
|
27
|
+
state,
|
|
28
|
+
op.path,
|
|
29
|
+
() => op.value,
|
|
30
|
+
);
|
|
31
|
+
case "append-text":
|
|
32
|
+
return ObjectStreamAccumulator.updatePath(state, op.path, (current) => {
|
|
33
|
+
if (typeof current !== "string")
|
|
34
|
+
throw new Error(`Expected string at path [${op.path.join(", ")}]`);
|
|
35
|
+
return current + op.value;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
default: {
|
|
39
|
+
const _exhaustiveCheck: never = type;
|
|
40
|
+
throw new Error(`Invalid operation type: ${_exhaustiveCheck}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private static updatePath(
|
|
46
|
+
state: ReadonlyJSONValue | undefined,
|
|
47
|
+
path: readonly string[],
|
|
48
|
+
updater: (current: ReadonlyJSONValue | undefined) => ReadonlyJSONValue,
|
|
49
|
+
): ReadonlyJSONValue {
|
|
50
|
+
if (path.length === 0) return updater(state);
|
|
51
|
+
|
|
52
|
+
// Initialize state as empty object if it's null and we're trying to set a property
|
|
53
|
+
if (state === null) {
|
|
54
|
+
state = {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof state !== "object") {
|
|
58
|
+
throw new Error(`Invalid path: [${path.join(", ")}]`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [key, ...rest] = path as [string, ...(readonly string[])];
|
|
62
|
+
if (Array.isArray(state)) {
|
|
63
|
+
const idx = Number(key);
|
|
64
|
+
if (isNaN(idx))
|
|
65
|
+
throw new Error(`Expected array index at [${path.join(", ")}]`);
|
|
66
|
+
if (idx > state.length || idx < 0)
|
|
67
|
+
throw new Error(`Insert array index out of bounds`);
|
|
68
|
+
|
|
69
|
+
const nextState = [...state];
|
|
70
|
+
nextState[idx] = this.updatePath(nextState[idx], rest, updater);
|
|
71
|
+
|
|
72
|
+
return nextState;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const nextState = { ...(state as ReadonlyJSONObject) };
|
|
76
|
+
nextState[key] = this.updatePath(nextState[key], rest, updater);
|
|
77
|
+
|
|
78
|
+
return nextState;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { PipeableTransformStream } from "../utils/stream/PipeableTransformStream";
|
|
2
|
+
import { ObjectStreamAccumulator } from "./ObjectStreamAccumulator";
|
|
3
|
+
import { SSEDecoder, SSEEncoder } from "../utils/stream/SSE";
|
|
4
|
+
import { ObjectStreamChunk, ObjectStreamOperation } from "./types";
|
|
5
|
+
|
|
6
|
+
export class ObjectStreamEncoder extends PipeableTransformStream<
|
|
7
|
+
ObjectStreamChunk,
|
|
8
|
+
Uint8Array
|
|
9
|
+
> {
|
|
10
|
+
constructor() {
|
|
11
|
+
super((readable) =>
|
|
12
|
+
readable
|
|
13
|
+
.pipeThrough(
|
|
14
|
+
new TransformStream<
|
|
15
|
+
ObjectStreamChunk,
|
|
16
|
+
readonly ObjectStreamOperation[]
|
|
17
|
+
>({
|
|
18
|
+
transform(chunk, controller) {
|
|
19
|
+
controller.enqueue(chunk.operations);
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
)
|
|
23
|
+
.pipeThrough(new SSEEncoder()),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ObjectStreamDecoder extends PipeableTransformStream<
|
|
29
|
+
Uint8Array,
|
|
30
|
+
ObjectStreamChunk
|
|
31
|
+
> {
|
|
32
|
+
constructor() {
|
|
33
|
+
const accumulator = new ObjectStreamAccumulator();
|
|
34
|
+
super((readable) =>
|
|
35
|
+
readable
|
|
36
|
+
.pipeThrough(new SSEDecoder<readonly ObjectStreamOperation[]>())
|
|
37
|
+
.pipeThrough(
|
|
38
|
+
new TransformStream<
|
|
39
|
+
readonly ObjectStreamOperation[],
|
|
40
|
+
ObjectStreamChunk
|
|
41
|
+
>({
|
|
42
|
+
transform(operations, controller) {
|
|
43
|
+
accumulator.append(operations);
|
|
44
|
+
controller.enqueue({
|
|
45
|
+
snapshot: accumulator.state,
|
|
46
|
+
operations,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class ObjectStreamResponse extends Response {
|
|
56
|
+
constructor(body: ReadableStream<ObjectStreamChunk>) {
|
|
57
|
+
super(body.pipeThrough(new ObjectStreamEncoder()), {
|
|
58
|
+
headers: new Headers({
|
|
59
|
+
"Content-Type": "text/event-stream",
|
|
60
|
+
"Cache-Control": "no-cache",
|
|
61
|
+
Connection: "keep-alive",
|
|
62
|
+
"Assistant-Stream-Format": "object-stream/v0",
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const fromObjectStreamResponse = (
|
|
69
|
+
response: Response,
|
|
70
|
+
): ReadableStream<ObjectStreamChunk> => {
|
|
71
|
+
if (!response.ok)
|
|
72
|
+
throw new Error(`Response failed, status ${response.status}`);
|
|
73
|
+
if (!response.body) throw new Error("Response body is null");
|
|
74
|
+
if (response.headers.get("Content-Type") !== "text/event-stream") {
|
|
75
|
+
throw new Error("Response is not an event stream");
|
|
76
|
+
}
|
|
77
|
+
if (response.headers.get("Assistant-Stream-Format") !== "object-stream/v0") {
|
|
78
|
+
throw new Error("Unsupported Assistant-Stream-Format header");
|
|
79
|
+
}
|
|
80
|
+
return response.body.pipeThrough(new ObjectStreamDecoder());
|
|
81
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ReadonlyJSONValue } from "../../utils";
|
|
2
|
+
import { withPromiseOrValue } from "../utils/withPromiseOrValue";
|
|
3
|
+
import { ObjectStreamAccumulator } from "./ObjectStreamAccumulator";
|
|
4
|
+
import { ObjectStreamOperation, ObjectStreamChunk } from "./types";
|
|
5
|
+
|
|
6
|
+
type ObjectStreamController = {
|
|
7
|
+
readonly abortSignal: AbortSignal;
|
|
8
|
+
|
|
9
|
+
enqueue(operations: readonly ObjectStreamOperation[]): void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
class ObjectStreamControllerImpl implements ObjectStreamController {
|
|
13
|
+
private _controller: ReadableStreamDefaultController<ObjectStreamChunk>;
|
|
14
|
+
private _abortController = new AbortController();
|
|
15
|
+
private _accumulator: ObjectStreamAccumulator;
|
|
16
|
+
|
|
17
|
+
get abortSignal() {
|
|
18
|
+
return this._abortController.signal;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
controller: ReadableStreamDefaultController<ObjectStreamChunk>,
|
|
23
|
+
defaultValue: ReadonlyJSONValue,
|
|
24
|
+
) {
|
|
25
|
+
this._controller = controller;
|
|
26
|
+
this._accumulator = new ObjectStreamAccumulator(defaultValue);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
enqueue(operations: readonly ObjectStreamOperation[]) {
|
|
30
|
+
this._accumulator.append(operations);
|
|
31
|
+
|
|
32
|
+
this._controller.enqueue({
|
|
33
|
+
snapshot: this._accumulator.state,
|
|
34
|
+
operations,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
__internalError(error: unknown) {
|
|
39
|
+
this._controller.error(error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
__internalClose() {
|
|
43
|
+
this._controller.close();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
__internalCancel(reason?: unknown) {
|
|
47
|
+
this._abortController.abort(reason);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const getStreamControllerPair = (defaultValue: ReadonlyJSONValue) => {
|
|
52
|
+
let controller!: ObjectStreamControllerImpl;
|
|
53
|
+
const stream = new ReadableStream<ObjectStreamChunk>({
|
|
54
|
+
start(c) {
|
|
55
|
+
controller = new ObjectStreamControllerImpl(c, defaultValue);
|
|
56
|
+
},
|
|
57
|
+
cancel(reason: unknown) {
|
|
58
|
+
controller.__internalCancel(reason);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return [stream, controller] as const;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type CreateObjectStreamOptions = {
|
|
66
|
+
execute: (controller: ObjectStreamController) => void | PromiseLike<void>;
|
|
67
|
+
defaultValue?: ReadonlyJSONValue;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const createObjectStream = ({
|
|
71
|
+
execute,
|
|
72
|
+
defaultValue = {},
|
|
73
|
+
}: CreateObjectStreamOptions) => {
|
|
74
|
+
const [stream, controller] = getStreamControllerPair(defaultValue);
|
|
75
|
+
|
|
76
|
+
withPromiseOrValue(
|
|
77
|
+
() => execute(controller),
|
|
78
|
+
() => {
|
|
79
|
+
controller.__internalClose();
|
|
80
|
+
},
|
|
81
|
+
(e: unknown) => {
|
|
82
|
+
controller.__internalError(e);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return stream;
|
|
87
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReadonlyJSONValue } from "../../utils";
|
|
2
|
+
|
|
3
|
+
export type ObjectStreamOperation =
|
|
4
|
+
| {
|
|
5
|
+
readonly type: "set";
|
|
6
|
+
readonly path: readonly string[];
|
|
7
|
+
readonly value: ReadonlyJSONValue;
|
|
8
|
+
}
|
|
9
|
+
| {
|
|
10
|
+
readonly type: "append-text";
|
|
11
|
+
readonly path: readonly string[];
|
|
12
|
+
readonly value: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ObjectStreamChunk = {
|
|
16
|
+
readonly snapshot: ReadonlyJSONValue;
|
|
17
|
+
readonly operations: readonly ObjectStreamOperation[];
|
|
18
|
+
};
|
|
@@ -150,6 +150,14 @@ export class DataStreamEncoder
|
|
|
150
150
|
break;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
case "update-state": {
|
|
154
|
+
controller.enqueue({
|
|
155
|
+
type: DataStreamStreamChunkType.AuiUpdateStateOperations,
|
|
156
|
+
value: chunk.operations,
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
// TODO ignore for now
|
|
154
162
|
// in the future, we should create a handler that waits for text parts to finish before continuing
|
|
155
163
|
case "tool-call-args-text-finish":
|
|
@@ -326,6 +334,14 @@ export class DataStreamDecoder extends PipeableTransformStream<
|
|
|
326
334
|
});
|
|
327
335
|
break;
|
|
328
336
|
|
|
337
|
+
case DataStreamStreamChunkType.AuiUpdateStateOperations:
|
|
338
|
+
controller.enqueue({
|
|
339
|
+
type: "update-state",
|
|
340
|
+
path: [],
|
|
341
|
+
operations: value,
|
|
342
|
+
});
|
|
343
|
+
break;
|
|
344
|
+
|
|
329
345
|
case DataStreamStreamChunkType.ReasoningSignature:
|
|
330
346
|
case DataStreamStreamChunkType.RedactedReasoning:
|
|
331
347
|
// ignore these for now
|