assistant-stream 0.3.25 → 0.3.27

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.
Files changed (216) hide show
  1. package/README.md +4 -1
  2. package/dist/core/AssistantStream.d.ts +0 -1
  3. package/dist/core/AssistantStream.d.ts.map +1 -1
  4. package/dist/core/AssistantStreamChunk.d.ts +26 -14
  5. package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
  6. package/dist/core/accumulators/AssistantMessageStream.d.ts +0 -1
  7. package/dist/core/accumulators/AssistantMessageStream.d.ts.map +1 -1
  8. package/dist/core/accumulators/TimingTracker.d.ts +0 -1
  9. package/dist/core/accumulators/TimingTracker.d.ts.map +1 -1
  10. package/dist/core/accumulators/assistant-message-accumulator.d.ts +3 -9
  11. package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
  12. package/dist/core/accumulators/assistant-message-accumulator.js +70 -28
  13. package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
  14. package/dist/core/converters/toGenericMessages.d.ts.map +1 -1
  15. package/dist/core/converters/toGenericMessages.js +2 -1
  16. package/dist/core/converters/toGenericMessages.js.map +1 -1
  17. package/dist/core/gorp/GorpStreamAccumulator.d.ts +15 -0
  18. package/dist/core/gorp/GorpStreamAccumulator.d.ts.map +1 -0
  19. package/dist/core/{object/ObjectStreamAccumulator.js → gorp/GorpStreamAccumulator.js} +11 -9
  20. package/dist/core/gorp/GorpStreamAccumulator.js.map +1 -0
  21. package/dist/core/gorp/GorpStreamDeltaTracker.d.ts +17 -0
  22. package/dist/core/gorp/GorpStreamDeltaTracker.d.ts.map +1 -0
  23. package/dist/core/gorp/GorpStreamDeltaTracker.js +38 -0
  24. package/dist/core/gorp/GorpStreamDeltaTracker.js.map +1 -0
  25. package/dist/core/gorp/GorpStreamResponse.d.ts +16 -0
  26. package/dist/core/gorp/GorpStreamResponse.d.ts.map +1 -0
  27. package/dist/core/gorp/GorpStreamResponse.js +51 -0
  28. package/dist/core/gorp/GorpStreamResponse.js.map +1 -0
  29. package/dist/core/gorp/changeTree.d.ts +16 -0
  30. package/dist/core/gorp/changeTree.d.ts.map +1 -0
  31. package/dist/core/gorp/changeTree.js +77 -0
  32. package/dist/core/gorp/changeTree.js.map +1 -0
  33. package/dist/core/gorp/createGorpStream.d.ts +16 -0
  34. package/dist/core/gorp/createGorpStream.d.ts.map +1 -0
  35. package/dist/core/{object/createObjectStream.js → gorp/createGorpStream.js} +13 -8
  36. package/dist/core/gorp/createGorpStream.js.map +1 -0
  37. package/dist/core/gorp/types.d.ts +20 -0
  38. package/dist/core/gorp/types.d.ts.map +1 -0
  39. package/dist/core/modules/assistant-stream.d.ts +18 -11
  40. package/dist/core/modules/assistant-stream.d.ts.map +1 -1
  41. package/dist/core/modules/assistant-stream.js +10 -6
  42. package/dist/core/modules/assistant-stream.js.map +1 -1
  43. package/dist/core/modules/text.d.ts +0 -1
  44. package/dist/core/modules/text.d.ts.map +1 -1
  45. package/dist/core/modules/tool-call.d.ts +0 -1
  46. package/dist/core/modules/tool-call.d.ts.map +1 -1
  47. package/dist/core/modules/tool-call.js +2 -2
  48. package/dist/core/modules/tool-call.js.map +1 -1
  49. package/dist/core/serialization/PlainText.d.ts +0 -1
  50. package/dist/core/serialization/PlainText.d.ts.map +1 -1
  51. package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts +0 -1
  52. package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts.map +1 -1
  53. package/dist/core/serialization/assistant-transport/AssistantTransport.js +93 -53
  54. package/dist/core/serialization/assistant-transport/AssistantTransport.js.map +1 -1
  55. package/dist/core/serialization/data-stream/DataStream.d.ts +0 -1
  56. package/dist/core/serialization/data-stream/DataStream.d.ts.map +1 -1
  57. package/dist/core/serialization/data-stream/chunk-types.d.ts +3 -4
  58. package/dist/core/serialization/data-stream/chunk-types.d.ts.map +1 -1
  59. package/dist/core/serialization/data-stream/chunk-types.js.map +1 -1
  60. package/dist/core/serialization/data-stream/serialization.d.ts +0 -1
  61. package/dist/core/serialization/data-stream/serialization.d.ts.map +1 -1
  62. package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts +0 -1
  63. package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts.map +1 -1
  64. package/dist/core/serialization/ui-message-stream/UIMessageStream.js +76 -53
  65. package/dist/core/serialization/ui-message-stream/UIMessageStream.js.map +1 -1
  66. package/dist/core/serialization/ui-message-stream/chunk-types.d.ts +0 -1
  67. package/dist/core/serialization/ui-message-stream/chunk-types.d.ts.map +1 -1
  68. package/dist/core/tool/ToolCallReader.d.ts +1 -0
  69. package/dist/core/tool/ToolCallReader.d.ts.map +1 -1
  70. package/dist/core/tool/ToolCallReader.js.map +1 -1
  71. package/dist/core/tool/ToolExecutionStream.d.ts +0 -1
  72. package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
  73. package/dist/core/tool/ToolExecutionStream.js +9 -1
  74. package/dist/core/tool/ToolExecutionStream.js.map +1 -1
  75. package/dist/core/tool/ToolResponse.d.ts +6 -4
  76. package/dist/core/tool/ToolResponse.d.ts.map +1 -1
  77. package/dist/core/tool/schema-utils.d.ts +0 -1
  78. package/dist/core/tool/schema-utils.d.ts.map +1 -1
  79. package/dist/core/tool/tool-types.d.ts +71 -33
  80. package/dist/core/tool/tool-types.d.ts.map +1 -1
  81. package/dist/core/tool/toolResultStream.d.ts +3 -2
  82. package/dist/core/tool/toolResultStream.d.ts.map +1 -1
  83. package/dist/core/tool/toolResultStream.js +7 -2
  84. package/dist/core/tool/toolResultStream.js.map +1 -1
  85. package/dist/core/tool/type-path-utils.d.ts +2 -2
  86. package/dist/core/tool/type-path-utils.d.ts.map +1 -1
  87. package/dist/core/utils/Counter.d.ts.map +1 -1
  88. package/dist/core/utils/generateId.d.ts.map +1 -1
  89. package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts +0 -1
  90. package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts.map +1 -1
  91. package/dist/core/utils/stream/AssistantTransformStream.d.ts +0 -1
  92. package/dist/core/utils/stream/AssistantTransformStream.d.ts.map +1 -1
  93. package/dist/core/utils/stream/LineDecoderStream.d.ts +1 -0
  94. package/dist/core/utils/stream/LineDecoderStream.d.ts.map +1 -1
  95. package/dist/core/utils/stream/LineDecoderStream.js +19 -6
  96. package/dist/core/utils/stream/LineDecoderStream.js.map +1 -1
  97. package/dist/core/utils/stream/PipeableTransformStream.d.ts.map +1 -1
  98. package/dist/core/utils/stream/SSE.d.ts +0 -1
  99. package/dist/core/utils/stream/SSE.d.ts.map +1 -1
  100. package/dist/core/utils/stream/SSE.js +2 -52
  101. package/dist/core/utils/stream/SSE.js.map +1 -1
  102. package/dist/core/utils/stream/SSEEventDecoder.d.ts +27 -0
  103. package/dist/core/utils/stream/SSEEventDecoder.d.ts.map +1 -0
  104. package/dist/core/utils/stream/SSEEventDecoder.js +84 -0
  105. package/dist/core/utils/stream/SSEEventDecoder.js.map +1 -0
  106. package/dist/core/utils/stream/SSEEventDecoderStream.d.ts +13 -0
  107. package/dist/core/utils/stream/SSEEventDecoderStream.d.ts.map +1 -0
  108. package/dist/core/utils/stream/SSEEventDecoderStream.js +24 -0
  109. package/dist/core/utils/stream/SSEEventDecoderStream.js.map +1 -0
  110. package/dist/core/utils/stream/UnderlyingReadable.d.ts.map +1 -1
  111. package/dist/core/utils/stream/merge.d.ts +2 -1
  112. package/dist/core/utils/stream/merge.d.ts.map +1 -1
  113. package/dist/core/utils/stream/merge.js +27 -8
  114. package/dist/core/utils/stream/merge.js.map +1 -1
  115. package/dist/core/utils/stream/path-utils.d.ts +0 -1
  116. package/dist/core/utils/stream/path-utils.d.ts.map +1 -1
  117. package/dist/core/utils/types.d.ts +16 -8
  118. package/dist/core/utils/types.d.ts.map +1 -1
  119. package/dist/core/utils/withPromiseOrValue.d.ts.map +1 -1
  120. package/dist/index.d.ts +25 -4
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +14 -3
  123. package/dist/index.js.map +1 -0
  124. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
  125. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
  126. package/dist/resumable/ResumableStreamContext.d.ts +0 -1
  127. package/dist/resumable/ResumableStreamContext.d.ts.map +1 -1
  128. package/dist/resumable/ResumableStreamContext.js +12 -5
  129. package/dist/resumable/ResumableStreamContext.js.map +1 -1
  130. package/dist/resumable/constants.d.ts.map +1 -1
  131. package/dist/resumable/createResumableAssistantStreamResponse.d.ts +6 -4
  132. package/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +1 -1
  133. package/dist/resumable/errors.d.ts.map +1 -1
  134. package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts +0 -1
  135. package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts.map +1 -1
  136. package/dist/resumable/stores/InMemoryResumableStreamStore.js +2 -1
  137. package/dist/resumable/stores/InMemoryResumableStreamStore.js.map +1 -1
  138. package/dist/resumable/stores/ioredis.d.ts +0 -1
  139. package/dist/resumable/stores/ioredis.d.ts.map +1 -1
  140. package/dist/resumable/stores/redis-impl.d.ts +2 -2
  141. package/dist/resumable/stores/redis-impl.d.ts.map +1 -1
  142. package/dist/resumable/stores/redis.d.ts +0 -1
  143. package/dist/resumable/stores/redis.d.ts.map +1 -1
  144. package/dist/resumable/types.d.ts.map +1 -1
  145. package/dist/utils/AsyncIterableStream.d.ts.map +1 -1
  146. package/dist/utils/AsyncIterableStream.js +18 -3
  147. package/dist/utils/AsyncIterableStream.js.map +1 -1
  148. package/dist/utils/json/fix-json.d.ts.map +1 -1
  149. package/dist/utils/json/json-value.d.ts.map +1 -1
  150. package/dist/utils/json/parse-partial-json-object.d.ts +0 -1
  151. package/dist/utils/json/parse-partial-json-object.d.ts.map +1 -1
  152. package/dist/utils/promiseWithResolvers.d.ts.map +1 -1
  153. package/dist/utils.d.ts +2 -1
  154. package/dist/utils.js +2 -1
  155. package/package.json +5 -5
  156. package/src/core/AssistantStreamChunk.ts +5 -3
  157. package/src/core/accumulators/assistant-message-accumulator.test.ts +437 -1
  158. package/src/core/accumulators/assistant-message-accumulator.ts +89 -26
  159. package/src/core/{object/ObjectStream.test.ts → gorp/GorpStream.test.ts} +104 -23
  160. package/src/core/gorp/GorpStreamAccumulator.test.ts +28 -0
  161. package/src/core/{object/ObjectStreamAccumulator.ts → gorp/GorpStreamAccumulator.ts} +13 -15
  162. package/src/core/gorp/GorpStreamDeltaTracker.test.ts +182 -0
  163. package/src/core/gorp/GorpStreamDeltaTracker.ts +55 -0
  164. package/src/core/gorp/GorpStreamResponse.test.ts +32 -0
  165. package/src/core/gorp/GorpStreamResponse.ts +88 -0
  166. package/src/core/gorp/changeTree.test.ts +168 -0
  167. package/src/core/gorp/changeTree.ts +96 -0
  168. package/src/core/{object/createObjectStream.ts → gorp/createGorpStream.ts} +23 -17
  169. package/src/core/{object → gorp}/types.ts +5 -3
  170. package/src/core/modules/assistant-stream.test.ts +154 -2
  171. package/src/core/modules/assistant-stream.ts +16 -6
  172. package/src/core/modules/tool-call.test.ts +67 -0
  173. package/src/core/modules/tool-call.ts +3 -3
  174. package/src/core/serialization/assistant-transport/AssistantTransport.test.ts +215 -1
  175. package/src/core/serialization/assistant-transport/AssistantTransport.ts +83 -71
  176. package/src/core/serialization/data-stream/chunk-types.ts +2 -2
  177. package/src/core/serialization/ui-message-stream/UIMessageStream.test.ts +323 -4
  178. package/src/core/serialization/ui-message-stream/UIMessageStream.ts +81 -71
  179. package/src/core/tool/ToolExecutionStream.ts +10 -0
  180. package/src/core/tool/toolResultStream.test.ts +50 -0
  181. package/src/core/tool/toolResultStream.ts +7 -2
  182. package/src/core/utils/stream/LineDecoderStream.test.ts +26 -2
  183. package/src/core/utils/stream/LineDecoderStream.ts +21 -11
  184. package/src/core/utils/stream/SSE.ts +6 -70
  185. package/src/core/utils/stream/SSEEventDecoder.test.ts +146 -0
  186. package/src/core/utils/stream/SSEEventDecoder.ts +109 -0
  187. package/src/core/utils/stream/SSEEventDecoderStream.test.ts +42 -0
  188. package/src/core/utils/stream/SSEEventDecoderStream.ts +34 -0
  189. package/src/core/utils/stream/merge.ts +31 -9
  190. package/src/gorp.test.ts +42 -0
  191. package/src/index.ts +21 -6
  192. package/src/resumable/ResumableStreamContext.test.ts +116 -1
  193. package/src/resumable/ResumableStreamContext.ts +32 -5
  194. package/src/resumable/createResumableAssistantStreamResponse.test.ts +1 -25
  195. package/src/utils/AsyncIterableStream.test.ts +61 -0
  196. package/src/utils/AsyncIterableStream.ts +18 -3
  197. package/src/utils.ts +4 -0
  198. package/dist/core/object/ObjectStreamAccumulator.d.ts +0 -15
  199. package/dist/core/object/ObjectStreamAccumulator.d.ts.map +0 -1
  200. package/dist/core/object/ObjectStreamAccumulator.js.map +0 -1
  201. package/dist/core/object/ObjectStreamResponse.d.ts +0 -17
  202. package/dist/core/object/ObjectStreamResponse.d.ts.map +0 -1
  203. package/dist/core/object/ObjectStreamResponse.js +0 -57
  204. package/dist/core/object/ObjectStreamResponse.js.map +0 -1
  205. package/dist/core/object/createObjectStream.d.ts +0 -19
  206. package/dist/core/object/createObjectStream.d.ts.map +0 -1
  207. package/dist/core/object/createObjectStream.js.map +0 -1
  208. package/dist/core/object/types.d.ts +0 -18
  209. package/dist/core/object/types.d.ts.map +0 -1
  210. package/dist/utils/json/is-json.d.ts +0 -9
  211. package/dist/utils/json/is-json.d.ts.map +0 -1
  212. package/dist/utils/json/is-json.js +0 -17
  213. package/dist/utils/json/is-json.js.map +0 -1
  214. package/src/core/object/ObjectStreamResponse.ts +0 -109
  215. package/src/utils/json/is-json.ts +0 -43
  216. /package/dist/core/{object → gorp}/types.js +0 -0
@@ -1,11 +1,8 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { createObjectStream } from "./createObjectStream";
3
- import {
4
- ObjectStreamEncoder,
5
- ObjectStreamDecoder,
6
- } from "./ObjectStreamResponse";
2
+ import { createGorpStream } from "./createGorpStream";
3
+ import { GorpStreamEncoder, GorpStreamDecoder } from "./GorpStreamResponse";
7
4
  import type { ReadonlyJSONValue } from "../../utils";
8
- import type { ObjectStreamChunk } from "./types";
5
+ import type { GorpStreamChunk } from "./types";
9
6
 
10
7
  // Helper function to collect all chunks from a stream
11
8
  async function collectChunks<T>(stream: ReadableStream<T>): Promise<T[]> {
@@ -27,10 +24,10 @@ async function collectChunks<T>(stream: ReadableStream<T>): Promise<T[]> {
27
24
 
28
25
  // Helper function to encode and decode a stream
29
26
  async function encodeAndDecode(
30
- stream: ReadableStream<ObjectStreamChunk>,
31
- ): Promise<ReadableStream<ObjectStreamChunk>> {
27
+ stream: ReadableStream<GorpStreamChunk>,
28
+ ): Promise<ReadableStream<GorpStreamChunk>> {
32
29
  // Encode the stream to Uint8Array (simulating network transmission)
33
- const encodedStream = stream.pipeThrough(new ObjectStreamEncoder());
30
+ const encodedStream = stream.pipeThrough(new GorpStreamEncoder());
34
31
 
35
32
  // Collect all encoded chunks
36
33
  const encodedChunks = await collectChunks(encodedStream);
@@ -45,14 +42,58 @@ async function encodeAndDecode(
45
42
  },
46
43
  });
47
44
 
48
- // Decode the stream back to ObjectStreamChunk
49
- return reconstructedStream.pipeThrough(new ObjectStreamDecoder());
45
+ // Decode the stream back to GorpStreamChunk
46
+ return reconstructedStream.pipeThrough(new GorpStreamDecoder());
50
47
  }
51
48
 
52
- describe("ObjectStream serialization and deserialization", () => {
49
+ describe("GorpStream serialization and deserialization", () => {
50
+ it("does not settle the stream after cancellation", async () => {
51
+ let finishExecution!: () => void;
52
+ let abortSignal!: AbortSignal;
53
+ const execution = new Promise<void>((resolve) => {
54
+ finishExecution = resolve;
55
+ });
56
+ const stream = createGorpStream({
57
+ execute: (controller) => {
58
+ abortSignal = controller.abortSignal;
59
+ return execution;
60
+ },
61
+ });
62
+ const reader = stream.getReader();
63
+
64
+ await reader.cancel("consumer stopped");
65
+
66
+ expect(abortSignal.aborted).toBe(true);
67
+ expect(abortSignal.reason).toBe("consumer stopped");
68
+
69
+ finishExecution();
70
+ await execution;
71
+ await new Promise((resolve) => setTimeout(resolve, 0));
72
+ });
73
+
74
+ it("should discard an unterminated SSE event", async () => {
75
+ const operations: GorpStreamChunk["operations"] = [
76
+ { type: "set", path: ["status"], value: "complete" },
77
+ ];
78
+ const stream = new ReadableStream<Uint8Array>({
79
+ start(controller) {
80
+ controller.enqueue(
81
+ new TextEncoder().encode(`data: ${JSON.stringify(operations)}\n`),
82
+ );
83
+ controller.close();
84
+ },
85
+ });
86
+
87
+ const chunks = await collectChunks(
88
+ stream.pipeThrough(new GorpStreamDecoder()),
89
+ );
90
+
91
+ expect(chunks).toEqual([]);
92
+ });
93
+
53
94
  it("should correctly serialize and deserialize simple objects", async () => {
54
95
  // Create an object stream with simple operations
55
- const stream = createObjectStream({
96
+ const stream = createGorpStream({
56
97
  execute: (controller) => {
57
98
  controller.enqueue([
58
99
  { type: "set", path: ["name"], value: "John" },
@@ -76,7 +117,7 @@ describe("ObjectStream serialization and deserialization", () => {
76
117
  });
77
118
 
78
119
  it("should correctly handle nested objects", async () => {
79
- const stream = createObjectStream({
120
+ const stream = createGorpStream({
80
121
  execute: (controller) => {
81
122
  controller.enqueue([
82
123
  { type: "set", path: ["user", "profile", "name"], value: "Jane" },
@@ -108,7 +149,7 @@ describe("ObjectStream serialization and deserialization", () => {
108
149
  });
109
150
 
110
151
  it("should correctly handle arrays", async () => {
111
- const stream = createObjectStream({
152
+ const stream = createGorpStream({
112
153
  execute: (controller) => {
113
154
  controller.enqueue([
114
155
  { type: "set", path: ["items"], value: [] },
@@ -129,7 +170,7 @@ describe("ObjectStream serialization and deserialization", () => {
129
170
  });
130
171
 
131
172
  it("should correctly handle mixed arrays and objects", async () => {
132
- const stream = createObjectStream({
173
+ const stream = createGorpStream({
133
174
  execute: (controller) => {
134
175
  controller.enqueue([
135
176
  { type: "set", path: ["users"], value: [] },
@@ -156,7 +197,7 @@ describe("ObjectStream serialization and deserialization", () => {
156
197
  });
157
198
 
158
199
  it("should correctly handle append-text operations", async () => {
159
- const stream = createObjectStream({
200
+ const stream = createGorpStream({
160
201
  execute: (controller) => {
161
202
  controller.enqueue([
162
203
  { type: "set", path: ["message"], value: "Hello" },
@@ -176,8 +217,48 @@ describe("ObjectStream serialization and deserialization", () => {
176
217
  });
177
218
  });
178
219
 
220
+ it("does not apply first-chunk operations twice", async () => {
221
+ const stream = createGorpStream({
222
+ defaultValue: { message: "Hello" },
223
+ execute: (controller) => {
224
+ controller.enqueue([
225
+ { type: "append-text", path: ["message"], value: " world" },
226
+ ]);
227
+ controller.enqueue([
228
+ { type: "append-text", path: ["message"], value: "!" },
229
+ ]);
230
+ },
231
+ });
232
+
233
+ const decodedStream = await encodeAndDecode(stream);
234
+ const chunks = await collectChunks(decodedStream);
235
+
236
+ expect(chunks).toEqual([
237
+ {
238
+ snapshot: { message: "Hello world" },
239
+ operations: [
240
+ {
241
+ type: "set",
242
+ path: [],
243
+ value: { message: "Hello world" },
244
+ },
245
+ ],
246
+ },
247
+ {
248
+ snapshot: { message: "Hello world!" },
249
+ operations: [
250
+ {
251
+ type: "append-text",
252
+ path: ["message"],
253
+ value: "!",
254
+ },
255
+ ],
256
+ },
257
+ ]);
258
+ });
259
+
179
260
  it("should correctly handle special characters and Unicode", async () => {
180
- const stream = createObjectStream({
261
+ const stream = createGorpStream({
181
262
  execute: (controller) => {
182
263
  controller.enqueue([
183
264
  {
@@ -209,7 +290,7 @@ describe("ObjectStream serialization and deserialization", () => {
209
290
  });
210
291
 
211
292
  it("should correctly handle null and undefined values", async () => {
212
- const stream = createObjectStream({
293
+ const stream = createGorpStream({
213
294
  execute: (controller) => {
214
295
  controller.enqueue([
215
296
  { type: "set", path: ["nullValue"], value: null },
@@ -232,7 +313,7 @@ describe("ObjectStream serialization and deserialization", () => {
232
313
 
233
314
  it("should correctly handle large nested structures", async () => {
234
315
  // Create a deep nested structure
235
- const stream = createObjectStream({
316
+ const stream = createGorpStream({
236
317
  execute: (controller) => {
237
318
  controller.enqueue([
238
319
  { type: "set", path: ["level1"], value: {} },
@@ -270,7 +351,7 @@ describe("ObjectStream serialization and deserialization", () => {
270
351
  });
271
352
 
272
353
  it("should correctly handle operations in multiple enqueue calls", async () => {
273
- const stream = createObjectStream({
354
+ const stream = createGorpStream({
274
355
  execute: (controller) => {
275
356
  // First batch of operations
276
357
  controller.enqueue([
@@ -319,7 +400,7 @@ describe("ObjectStream serialization and deserialization", () => {
319
400
  });
320
401
 
321
402
  it("should correctly handle overwriting existing values", async () => {
322
- const stream = createObjectStream({
403
+ const stream = createGorpStream({
323
404
  execute: (controller) => {
324
405
  controller.enqueue([
325
406
  { type: "set", path: ["value"], value: "initial" },
@@ -351,7 +432,7 @@ describe("ObjectStream serialization and deserialization", () => {
351
432
  },
352
433
  };
353
434
 
354
- const stream = createObjectStream({
435
+ const stream = createGorpStream({
355
436
  defaultValue: initialValue,
356
437
  execute: (controller) => {
357
438
  controller.enqueue([
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { GorpStreamAccumulator } from "./GorpStreamAccumulator";
3
+
4
+ describe("GorpStreamAccumulator", () => {
5
+ it("rejects unsafe path segments", () => {
6
+ for (const path of [
7
+ ["__proto__", "polluted"],
8
+ ["constructor", "prototype", "polluted"],
9
+ ["a", "__proto__"],
10
+ ]) {
11
+ const acc = new GorpStreamAccumulator({});
12
+ expect(() => acc.append([{ type: "set", path, value: true }])).toThrow(
13
+ /Unsafe gorp path segment/,
14
+ );
15
+ }
16
+ expect(({} as { polluted?: unknown }).polluted).toBeUndefined();
17
+ });
18
+
19
+ it("treats inherited keys as absent when navigating", () => {
20
+ const acc = new GorpStreamAccumulator({});
21
+ acc.append([{ type: "set", path: ["toString", "x"], value: 1 }]);
22
+ expect(acc.state).toEqual({ toString: { x: 1 } });
23
+
24
+ const append = new GorpStreamAccumulator({ toString: "hi" });
25
+ append.append([{ type: "append-text", path: ["toString"], value: "!" }]);
26
+ expect(append.state).toEqual({ toString: "hi!" });
27
+ });
28
+ });
@@ -1,7 +1,8 @@
1
1
  import type { ReadonlyJSONValue, ReadonlyJSONObject } from "../../utils";
2
- import type { ObjectStreamOperation } from "./types";
2
+ import { assertSafePathSegment } from "./changeTree";
3
+ import type { GorpStreamOperation } from "./types";
3
4
 
4
- export class ObjectStreamAccumulator {
5
+ export class GorpStreamAccumulator {
5
6
  private _state: ReadonlyJSONValue;
6
7
 
7
8
  constructor(initialValue: ReadonlyJSONValue = null) {
@@ -12,24 +13,20 @@ export class ObjectStreamAccumulator {
12
13
  return this._state;
13
14
  }
14
15
 
15
- append(ops: readonly ObjectStreamOperation[]) {
16
+ append(ops: readonly GorpStreamOperation[]) {
16
17
  this._state = ops.reduce(
17
- (state, op) => ObjectStreamAccumulator.apply(state, op),
18
+ (state, op) => GorpStreamAccumulator.apply(state, op),
18
19
  this._state,
19
20
  );
20
21
  }
21
22
 
22
- private static apply(state: ReadonlyJSONValue, op: ObjectStreamOperation) {
23
+ private static apply(state: ReadonlyJSONValue, op: GorpStreamOperation) {
23
24
  const type = op.type;
24
25
  switch (type) {
25
26
  case "set":
26
- return ObjectStreamAccumulator.updatePath(
27
- state,
28
- op.path,
29
- () => op.value,
30
- );
27
+ return GorpStreamAccumulator.updatePath(state, op.path, () => op.value);
31
28
  case "append-text":
32
- return ObjectStreamAccumulator.updatePath(state, op.path, (current) => {
29
+ return GorpStreamAccumulator.updatePath(state, op.path, (current) => {
33
30
  if (typeof current !== "string")
34
31
  throw new Error(`Expected string at path [${op.path.join(", ")}]`);
35
32
  return current + op.value;
@@ -57,6 +54,7 @@ export class ObjectStreamAccumulator {
57
54
  }
58
55
 
59
56
  const [key, ...rest] = path as [string, ...(readonly string[])];
57
+ assertSafePathSegment(key);
60
58
  if (Array.isArray(state)) {
61
59
  const idx = Number(key);
62
60
  if (Number.isNaN(idx))
@@ -65,8 +63,8 @@ export class ObjectStreamAccumulator {
65
63
  throw new Error(`Insert array index out of bounds`);
66
64
 
67
65
  const nextState = [...state];
68
- nextState[idx] = ObjectStreamAccumulator.updatePath(
69
- nextState[idx],
66
+ nextState[idx] = GorpStreamAccumulator.updatePath(
67
+ Object.hasOwn(nextState, idx) ? nextState[idx] : undefined,
70
68
  rest,
71
69
  updater,
72
70
  );
@@ -75,8 +73,8 @@ export class ObjectStreamAccumulator {
75
73
  }
76
74
 
77
75
  const nextState = { ...(state as ReadonlyJSONObject) };
78
- nextState[key] = ObjectStreamAccumulator.updatePath(
79
- nextState[key],
76
+ nextState[key] = GorpStreamAccumulator.updatePath(
77
+ Object.hasOwn(nextState, key) ? nextState[key] : undefined,
80
78
  rest,
81
79
  updater,
82
80
  );
@@ -0,0 +1,182 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { GorpStreamDeltaTracker } from "./GorpStreamDeltaTracker";
3
+ import { createGorpStream } from "./createGorpStream";
4
+ import type { ReadonlyJSONObject } from "../../utils";
5
+
6
+ describe("GorpStreamDeltaTracker frames", () => {
7
+ it("reports ops of the current frame in getChangedKeys", () => {
8
+ const tracker = new GorpStreamDeltaTracker({ items: {} });
9
+ tracker.append([
10
+ { type: "set", path: ["items", "a"], value: { name: "alpha" } },
11
+ ]);
12
+ expect(tracker.getChangedKeys(["items"])).toEqual(["a"]);
13
+ expect((tracker.state as ReadonlyJSONObject).items).toEqual({
14
+ a: { name: "alpha" },
15
+ });
16
+ });
17
+
18
+ it("opens a separate frame per append", () => {
19
+ const tracker = new GorpStreamDeltaTracker({ items: {} });
20
+ tracker.append([
21
+ { type: "set", path: ["items", "a"], value: { name: "alpha" } },
22
+ ]);
23
+ tracker.append([
24
+ { type: "set", path: ["items", "b"], value: { name: "beta" } },
25
+ ]);
26
+ expect(tracker.getChangedKeys(["items"])).toEqual(["b"]);
27
+ expect(tracker.isChangedAt(["items", "a"])).toBe(false);
28
+ });
29
+
30
+ it("reports an empty change set before the first append and after an empty append", () => {
31
+ const tracker = new GorpStreamDeltaTracker({ count: 0 });
32
+ expect(tracker.isChangedAt([])).toBe(false);
33
+ expect(tracker.isChangedAt(["count"])).toBe(false);
34
+ expect(tracker.getChangedKeys([])).toEqual([]);
35
+ tracker.append([{ type: "set", path: ["count"], value: 1 }]);
36
+ expect(tracker.isChangedAt([])).toBe(true);
37
+ tracker.append([]);
38
+ expect(tracker.isChangedAt([])).toBe(false);
39
+ expect(tracker.isChangedAt(["count"])).toBe(false);
40
+ expect(tracker.getChangedKeys([])).toEqual([]);
41
+ });
42
+ });
43
+
44
+ describe("GorpStreamDeltaTracker isChangedAt", () => {
45
+ it("returns truthy for any path inside a marked subtree", () => {
46
+ const tracker = new GorpStreamDeltaTracker();
47
+ tracker.append([{ type: "set", path: [], value: { items: {} } }]);
48
+ expect(tracker.isChangedAt([])).toBe(true);
49
+ expect(tracker.isChangedAt(["items"])).toBe(true);
50
+ expect(tracker.isChangedAt(["items", "anything", "deep"])).toBe(true);
51
+ });
52
+
53
+ it("returns true for ancestors of a changed path and false for siblings", () => {
54
+ const tracker = new GorpStreamDeltaTracker({ a: { b: "x" }, c: "y" });
55
+ tracker.append([{ type: "set", path: ["a", "b"], value: "z" }]);
56
+ expect(tracker.isChangedAt([])).toBe(true);
57
+ expect(tracker.isChangedAt(["a"])).toBe(true);
58
+ expect(tracker.isChangedAt(["a", "b"])).toBe(true);
59
+ expect(tracker.isChangedAt(["c"])).toBe(false);
60
+ });
61
+
62
+ it("marks append-text targets as changed", () => {
63
+ const tracker = new GorpStreamDeltaTracker({ message: "Hello" });
64
+ tracker.append([{ type: "append-text", path: ["message"], value: "!" }]);
65
+ expect(tracker.isChangedAt(["message"])).toBe(true);
66
+ expect(tracker.getChangedKeys([])).toEqual(["message"]);
67
+ expect((tracker.state as ReadonlyJSONObject).message).toBe("Hello!");
68
+ });
69
+ });
70
+
71
+ describe("GorpStreamDeltaTracker getChangedKeys", () => {
72
+ it("diffs keys against the previous frame when a subtree is fully replaced", () => {
73
+ const tracker = new GorpStreamDeltaTracker({ count: 0, items: {} });
74
+ tracker.append([
75
+ { type: "set", path: ["items", "a"], value: { name: "alpha" } },
76
+ ]);
77
+ tracker.append([
78
+ { type: "set", path: ["items", "b"], value: { name: "beta" } },
79
+ ]);
80
+ tracker.append([
81
+ {
82
+ type: "set",
83
+ path: [],
84
+ value: {
85
+ count: 99,
86
+ items: { b: { name: "beta-new" }, c: { name: "gamma" } },
87
+ },
88
+ },
89
+ ]);
90
+ expect(tracker.getChangedKeys(["items"])).toEqual(["b", "c", "a"]);
91
+ });
92
+
93
+ it("returns interior node keys for a partially changed subtree", () => {
94
+ const tracker = new GorpStreamDeltaTracker({ items: { a: 1, b: 2 } });
95
+ tracker.append([
96
+ { type: "set", path: ["items", "a"], value: 10 },
97
+ { type: "set", path: ["items", "c"], value: 3 },
98
+ ]);
99
+ expect(tracker.getChangedKeys(["items"])).toEqual(["a", "c"]);
100
+ expect(tracker.getChangedKeys([])).toEqual(["items"]);
101
+ expect(tracker.getChangedKeys(["other"])).toEqual([]);
102
+ });
103
+
104
+ it("diffs keys when the previous value at a replaced path was not an object", () => {
105
+ const tracker = new GorpStreamDeltaTracker({ value: "initial" });
106
+ tracker.append([{ type: "set", path: ["value"], value: { nested: true } }]);
107
+ expect(tracker.getChangedKeys(["value"])).toEqual(["nested"]);
108
+ });
109
+ });
110
+
111
+ describe("GorpStreamDeltaTracker inherited object members", () => {
112
+ it("does not treat inherited members as changes on a fresh tracker", () => {
113
+ const tracker = new GorpStreamDeltaTracker({ items: {} });
114
+ expect(tracker.isChangedAt(["toString"])).toBe(false);
115
+ expect(tracker.isChangedAt(["hasOwnProperty"])).toBe(false);
116
+ expect(tracker.getChangedKeys(["toString"])).toEqual([]);
117
+ });
118
+
119
+ it("does not treat inherited members as changes on a populated tree", () => {
120
+ const tracker = new GorpStreamDeltaTracker({ items: {} });
121
+ tracker.append([{ type: "set", path: ["items", "a"], value: 1 }]);
122
+ expect(tracker.isChangedAt(["toString"])).toBe(false);
123
+ expect(tracker.isChangedAt(["items", "toString"])).toBe(false);
124
+ expect(tracker.isChangedAt(["items", "valueOf", "deep"])).toBe(false);
125
+ expect(tracker.getChangedKeys([])).toEqual(["items"]);
126
+ expect(tracker.getChangedKeys(["items"])).toEqual(["a"]);
127
+ });
128
+ });
129
+
130
+ describe("GorpStreamDeltaTracker failed append", () => {
131
+ it("leaves state and the change frame untouched when an operation throws", () => {
132
+ const tracker = new GorpStreamDeltaTracker({ count: 5, message: "hi" });
133
+ tracker.append([{ type: "set", path: ["message"], value: "hello" }]);
134
+ const stateBefore = tracker.state;
135
+
136
+ expect(() =>
137
+ tracker.append([
138
+ { type: "set", path: ["other"], value: 1 },
139
+ { type: "append-text", path: ["count"], value: "!" },
140
+ ]),
141
+ ).toThrow(/Expected string/);
142
+
143
+ expect(tracker.state).toBe(stateBefore);
144
+ expect(tracker.isChangedAt(["message"])).toBe(true);
145
+ expect(tracker.isChangedAt(["other"])).toBe(false);
146
+ expect(tracker.isChangedAt(["count"])).toBe(false);
147
+ expect(tracker.getChangedKeys([])).toEqual(["message"]);
148
+ });
149
+ });
150
+
151
+ describe("GorpStreamDeltaTracker streamed accumulation", () => {
152
+ it("tracks one frame per streamed chunk", async () => {
153
+ const stream = createGorpStream({
154
+ execute: (controller) => {
155
+ controller.enqueue([
156
+ { type: "set", path: ["user"], value: { name: "Initial" } },
157
+ ]);
158
+ controller.enqueue([
159
+ { type: "set", path: ["user", "name"], value: "Updated" },
160
+ { type: "set", path: ["user", "email"], value: "user@example.com" },
161
+ ]);
162
+ controller.enqueue([
163
+ { type: "set", path: ["status"], value: "complete" },
164
+ ]);
165
+ },
166
+ });
167
+
168
+ const tracker = new GorpStreamDeltaTracker();
169
+ const frames: string[][] = [];
170
+ for await (const chunk of stream) {
171
+ tracker.append(chunk.operations);
172
+ frames.push(tracker.getChangedKeys(["user"]));
173
+ expect(tracker.state).toEqual(chunk.snapshot);
174
+ }
175
+
176
+ expect(frames).toEqual([["name"], ["name", "email"], []]);
177
+ expect(tracker.state).toEqual({
178
+ user: { name: "Updated", email: "user@example.com" },
179
+ status: "complete",
180
+ });
181
+ });
182
+ });
@@ -0,0 +1,55 @@
1
+ import type { ReadonlyJSONValue } from "../../utils";
2
+ import { GorpStreamAccumulator } from "./GorpStreamAccumulator";
3
+ import type { GorpStreamOperation } from "./types";
4
+ import {
5
+ type ChangeNode,
6
+ createChangeNode,
7
+ diffKeys,
8
+ lookupChange,
9
+ lookupValue,
10
+ markChanged,
11
+ } from "./changeTree";
12
+
13
+ export class GorpStreamDeltaTracker {
14
+ private readonly accumulator: GorpStreamAccumulator;
15
+ private previousState: ReadonlyJSONValue;
16
+ private changes: ChangeNode = createChangeNode();
17
+
18
+ constructor(initialValue: ReadonlyJSONValue = null) {
19
+ this.accumulator = new GorpStreamAccumulator(initialValue);
20
+ this.previousState = this.accumulator.state;
21
+ }
22
+
23
+ get state(): ReadonlyJSONValue {
24
+ return this.accumulator.state;
25
+ }
26
+
27
+ append(operations: readonly GorpStreamOperation[]): void {
28
+ const previousState = this.accumulator.state;
29
+ let changes: ChangeNode = createChangeNode();
30
+ for (const op of operations) {
31
+ changes = markChanged(changes, op.path);
32
+ }
33
+ this.accumulator.append(operations);
34
+ this.previousState = previousState;
35
+ this.changes = changes;
36
+ }
37
+
38
+ isChangedAt(path: readonly string[]): boolean {
39
+ const node = lookupChange(this.changes, path);
40
+ if (node === true) return true;
41
+ return node !== false && Object.keys(node).length > 0;
42
+ }
43
+
44
+ getChangedKeys(path: readonly string[]): string[] {
45
+ const node = lookupChange(this.changes, path);
46
+ if (node === false) return [];
47
+ if (node === true) {
48
+ return diffKeys(
49
+ lookupValue(this.accumulator.state, path),
50
+ lookupValue(this.previousState, path),
51
+ );
52
+ }
53
+ return Object.keys(node);
54
+ }
55
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { fromGorpStreamResponse } from "./GorpStreamResponse";
3
+
4
+ const createResponse = (contentType?: string) => {
5
+ const headers = new Headers({
6
+ "Assistant-Stream-Format": "object-stream/v0",
7
+ });
8
+ if (contentType) headers.set("Content-Type", contentType);
9
+
10
+ return new Response(new Uint8Array(), { headers });
11
+ };
12
+
13
+ describe("fromGorpStreamResponse", () => {
14
+ it.each([
15
+ "text/event-stream",
16
+ "text/event-stream; charset=utf-8",
17
+ "Text/Event-Stream; Charset=UTF-8",
18
+ ])("accepts the event-stream content type %s", (contentType) => {
19
+ expect(() =>
20
+ fromGorpStreamResponse(createResponse(contentType)),
21
+ ).not.toThrow();
22
+ });
23
+
24
+ it.each([undefined, "application/json", "text/event-streaming"])(
25
+ "rejects the content type %s",
26
+ (contentType) => {
27
+ expect(() => fromGorpStreamResponse(createResponse(contentType))).toThrow(
28
+ "Response is not an event stream",
29
+ );
30
+ },
31
+ );
32
+ });
@@ -0,0 +1,88 @@
1
+ import { PipeableTransformStream } from "../utils/stream/PipeableTransformStream";
2
+ import { GorpStreamAccumulator } from "./GorpStreamAccumulator";
3
+ import { SSEDecoder, SSEEncoder } from "../utils/stream/SSE";
4
+ import type { GorpStreamChunk, GorpStreamOperation } from "./types";
5
+
6
+ export class GorpStreamEncoder extends PipeableTransformStream<
7
+ GorpStreamChunk,
8
+ Uint8Array
9
+ > {
10
+ constructor() {
11
+ let isFirstChunk = true;
12
+ super((readable) =>
13
+ readable
14
+ .pipeThrough(
15
+ new TransformStream<GorpStreamChunk, readonly GorpStreamOperation[]>({
16
+ transform(chunk, controller) {
17
+ if (isFirstChunk) {
18
+ controller.enqueue([
19
+ { type: "set", path: [], value: chunk.snapshot },
20
+ ]);
21
+ } else {
22
+ controller.enqueue(chunk.operations);
23
+ }
24
+ isFirstChunk = false;
25
+ },
26
+ }),
27
+ )
28
+ .pipeThrough(new SSEEncoder()),
29
+ );
30
+ }
31
+ }
32
+
33
+ export class GorpStreamDecoder extends PipeableTransformStream<
34
+ Uint8Array<ArrayBuffer>,
35
+ GorpStreamChunk
36
+ > {
37
+ constructor() {
38
+ const accumulator = new GorpStreamAccumulator();
39
+ super((readable) =>
40
+ readable
41
+ .pipeThrough(new SSEDecoder<readonly GorpStreamOperation[]>())
42
+ .pipeThrough(
43
+ new TransformStream<readonly GorpStreamOperation[], GorpStreamChunk>({
44
+ transform(operations, controller) {
45
+ accumulator.append(operations);
46
+ controller.enqueue({
47
+ snapshot: accumulator.state,
48
+ operations,
49
+ });
50
+ },
51
+ }),
52
+ ),
53
+ );
54
+ }
55
+ }
56
+
57
+ export class GorpStreamResponse extends Response {
58
+ constructor(body: ReadableStream<GorpStreamChunk>) {
59
+ super(body.pipeThrough(new GorpStreamEncoder()), {
60
+ headers: new Headers({
61
+ "Content-Type": "text/event-stream",
62
+ "Cache-Control": "no-cache",
63
+ Connection: "keep-alive",
64
+ "Assistant-Stream-Format": "object-stream/v0",
65
+ }),
66
+ });
67
+ }
68
+ }
69
+
70
+ export const fromGorpStreamResponse = (
71
+ response: Response,
72
+ ): ReadableStream<GorpStreamChunk> => {
73
+ if (!response.ok)
74
+ throw new Error(`Response failed, status ${response.status}`);
75
+ if (!response.body) throw new Error("Response body is null");
76
+ const mediaType = response.headers
77
+ .get("Content-Type")
78
+ ?.split(";", 1)[0]
79
+ ?.trim()
80
+ .toLowerCase();
81
+ if (mediaType !== "text/event-stream") {
82
+ throw new Error("Response is not an event stream");
83
+ }
84
+ if (response.headers.get("Assistant-Stream-Format") !== "object-stream/v0") {
85
+ throw new Error("Unsupported Assistant-Stream-Format header");
86
+ }
87
+ return response.body.pipeThrough(new GorpStreamDecoder());
88
+ };