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
@@ -40,6 +40,22 @@ describe("LineDecoderStream", () => {
40
40
  expect(lines).toEqual(["line1", "line2", "line3"]);
41
41
  });
42
42
 
43
+ it("should split lines on CR (\\r)", async () => {
44
+ const stream = createTextStream(["line1\rline2\rline3\r"]);
45
+ const lines = await collectLines(
46
+ stream.pipeThrough(new LineDecoderStream()),
47
+ );
48
+ expect(lines).toEqual(["line1", "line2", "line3"]);
49
+ });
50
+
51
+ it("should handle CR line endings split across chunks", async () => {
52
+ const stream = createTextStream(["line1\r", "line2\r", "line3\r"]);
53
+ const lines = await collectLines(
54
+ stream.pipeThrough(new LineDecoderStream()),
55
+ );
56
+ expect(lines).toEqual(["line1", "line2", "line3"]);
57
+ });
58
+
43
59
  it("should handle CRLF split across chunks", async () => {
44
60
  // The \r is at the end of one chunk and \n at the start of the next
45
61
  const stream = createTextStream(["line1\r", "\nline2\r\n"]);
@@ -65,8 +81,16 @@ describe("LineDecoderStream", () => {
65
81
  expect(lines).toEqual(["line1", "", "line2"]);
66
82
  });
67
83
 
68
- it("should handle mixed LF and CRLF", async () => {
69
- const stream = createTextStream(["line1\nline2\r\nline3\n"]);
84
+ it("should handle empty lines with CR", async () => {
85
+ const stream = createTextStream(["line1\r\rline2\r"]);
86
+ const lines = await collectLines(
87
+ stream.pipeThrough(new LineDecoderStream()),
88
+ );
89
+ expect(lines).toEqual(["line1", "", "line2"]);
90
+ });
91
+
92
+ it("should handle mixed standard line endings", async () => {
93
+ const stream = createTextStream(["line1\nline2\r\nline3\r"]);
70
94
  const lines = await collectLines(
71
95
  stream.pipeThrough(new LineDecoderStream()),
72
96
  );
@@ -1,25 +1,35 @@
1
1
  export class LineDecoderStream extends TransformStream<string, string> {
2
2
  private buffer = "";
3
+ private skipNextLineFeed = false;
3
4
 
4
5
  constructor() {
5
6
  super({
6
7
  transform: (chunk, controller) => {
7
- this.buffer += chunk;
8
- const lines = this.buffer.split("\n");
8
+ let lineStart = 0;
9
9
 
10
- // Process all complete lines
11
- for (let i = 0; i < lines.length - 1; i++) {
12
- // Strip trailing \r to handle \r\n (CRLF) line endings per the SSE spec.
13
- const line = lines[i]!;
14
- controller.enqueue(line.endsWith("\r") ? line.slice(0, -1) : line);
10
+ for (let i = 0; i < chunk.length; i++) {
11
+ const character = chunk[i]!;
12
+
13
+ if (this.skipNextLineFeed) {
14
+ this.skipNextLineFeed = false;
15
+ if (character === "\n") {
16
+ lineStart = i + 1;
17
+ continue;
18
+ }
19
+ }
20
+
21
+ if (character === "\n" || character === "\r") {
22
+ this.buffer += chunk.slice(lineStart, i);
23
+ controller.enqueue(this.buffer);
24
+ this.buffer = "";
25
+ this.skipNextLineFeed = character === "\r";
26
+ lineStart = i + 1;
27
+ }
15
28
  }
16
29
 
17
- // Keep the last incomplete line in the buffer
18
- this.buffer = lines[lines.length - 1] || "";
30
+ this.buffer += chunk.slice(lineStart);
19
31
  },
20
32
  flush: () => {
21
- // If there's content in the buffer when the stream ends, it means
22
- // the stream ended with an incomplete line (no trailing newline)
23
33
  if (this.buffer) {
24
34
  throw new Error(
25
35
  `Stream ended with an incomplete line: "${this.buffer}"`,
@@ -1,5 +1,8 @@
1
1
  import { PipeableTransformStream } from "./PipeableTransformStream";
2
- import { LineDecoderStream } from "./LineDecoderStream";
2
+ import {
3
+ SSEEventDecoderStream,
4
+ type PipelineSSEEvent,
5
+ } from "./SSEEventDecoderStream";
3
6
 
4
7
  export class SSEEncoder<T> extends PipeableTransformStream<
5
8
  T,
@@ -28,72 +31,6 @@ export class SSEEncoder<T> extends PipeableTransformStream<
28
31
  }
29
32
  }
30
33
 
31
- type SSEEvent = {
32
- event: string;
33
- data: string;
34
- id?: string | undefined;
35
- retry?: number | undefined;
36
- };
37
-
38
- class SSEEventStream extends TransformStream<string, SSEEvent> {
39
- constructor() {
40
- let eventBuffer: Partial<SSEEvent> = {};
41
- let dataLines: string[] = [];
42
-
43
- super({
44
- start() {
45
- eventBuffer = {};
46
- dataLines = [];
47
- },
48
- transform(line, controller) {
49
- if (line.startsWith(":")) return; // Ignore comments
50
-
51
- if (line === "") {
52
- if (dataLines.length > 0) {
53
- controller.enqueue({
54
- event: eventBuffer.event || "message",
55
- data: dataLines.join("\n"),
56
- id: eventBuffer.id,
57
- retry: eventBuffer.retry,
58
- });
59
- }
60
- eventBuffer = {};
61
- dataLines = [];
62
- return;
63
- }
64
-
65
- const [field, ...rest] = line.split(":");
66
- const value = rest.join(":").trimStart();
67
-
68
- switch (field) {
69
- case "event":
70
- eventBuffer.event = value;
71
- break;
72
- case "data":
73
- dataLines.push(value);
74
- break;
75
- case "id":
76
- eventBuffer.id = value;
77
- break;
78
- case "retry":
79
- eventBuffer.retry = Number(value);
80
- break;
81
- }
82
- },
83
- flush(controller) {
84
- if (dataLines.length > 0) {
85
- controller.enqueue({
86
- event: eventBuffer.event || "message",
87
- data: dataLines.join("\n"),
88
- id: eventBuffer.id,
89
- retry: eventBuffer.retry,
90
- });
91
- }
92
- },
93
- });
94
- }
95
- }
96
-
97
34
  export class SSEDecoder<T> extends PipeableTransformStream<
98
35
  Uint8Array<ArrayBuffer>,
99
36
  T
@@ -102,10 +39,9 @@ export class SSEDecoder<T> extends PipeableTransformStream<
102
39
  super((readable) =>
103
40
  readable
104
41
  .pipeThrough(new TextDecoderStream())
105
- .pipeThrough(new LineDecoderStream())
106
- .pipeThrough(new SSEEventStream())
42
+ .pipeThrough(new SSEEventDecoderStream())
107
43
  .pipeThrough(
108
- new TransformStream<SSEEvent, T>({
44
+ new TransformStream<PipelineSSEEvent, T>({
109
45
  transform(event, controller) {
110
46
  switch (event.event) {
111
47
  case "message":
@@ -0,0 +1,146 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { SSEEventDecoder } from "./SSEEventDecoder";
3
+
4
+ describe("SSEEventDecoder", () => {
5
+ it("decodes LF terminated events", () => {
6
+ const decoder = new SSEEventDecoder();
7
+ expect(decoder.push("data: x\n\n")).toEqual([{ data: "x" }]);
8
+ });
9
+
10
+ it("decodes CRLF terminated events", () => {
11
+ const decoder = new SSEEventDecoder();
12
+ expect(decoder.push("data: x\r\n\r\n")).toEqual([{ data: "x" }]);
13
+ });
14
+
15
+ it("decodes CR terminated events", () => {
16
+ const decoder = new SSEEventDecoder();
17
+ expect(decoder.push("data: x\r\r")).toEqual([{ data: "x" }]);
18
+ });
19
+
20
+ it("handles CRLF split across pushes", () => {
21
+ const decoder = new SSEEventDecoder();
22
+ expect(decoder.push("data: x\r")).toEqual([]);
23
+ expect(decoder.push("\n\r\n")).toEqual([{ data: "x" }]);
24
+ });
25
+
26
+ it("handles CR terminated lines split across pushes", () => {
27
+ const decoder = new SSEEventDecoder();
28
+ expect(decoder.push("data: x\r")).toEqual([]);
29
+ expect(decoder.push("\r")).toEqual([{ data: "x" }]);
30
+ });
31
+
32
+ it("decodes multiple events in one push", () => {
33
+ const decoder = new SSEEventDecoder();
34
+ expect(decoder.push("data: first\n\ndata: second\n\n")).toEqual([
35
+ { data: "first" },
36
+ { data: "second" },
37
+ ]);
38
+ });
39
+
40
+ it("joins multiple data lines with a newline", () => {
41
+ const decoder = new SSEEventDecoder();
42
+ expect(decoder.push("data: first\ndata: second\n\n")).toEqual([
43
+ { data: "first\nsecond" },
44
+ ]);
45
+ });
46
+
47
+ it("preserves data values without a space after the colon", () => {
48
+ const decoder = new SSEEventDecoder();
49
+ expect(decoder.push("data:x\n\n")).toEqual([{ data: "x" }]);
50
+ });
51
+
52
+ it("skips comment lines", () => {
53
+ const decoder = new SSEEventDecoder();
54
+ expect(decoder.push(": heartbeat\ndata: x\n\n")).toEqual([{ data: "x" }]);
55
+ });
56
+
57
+ it("captures event fields and clears them after dispatch", () => {
58
+ const decoder = new SSEEventDecoder();
59
+ expect(
60
+ decoder.push("event: update\ndata: first\n\ndata: second\n\n"),
61
+ ).toEqual([{ event: "update", data: "first" }, { data: "second" }]);
62
+ });
63
+
64
+ it("persists IDs across frames", () => {
65
+ const decoder = new SSEEventDecoder();
66
+ expect(decoder.push("id: 1\ndata: first\n\ndata: second\n\n")).toEqual([
67
+ { data: "first", id: "1" },
68
+ { data: "second", id: "1" },
69
+ ]);
70
+ });
71
+
72
+ it("parses numeric retries and ignores non-numeric retries", () => {
73
+ const decoder = new SSEEventDecoder();
74
+ expect(
75
+ decoder.push(
76
+ "retry: 1000\ndata: first\n\nretry: invalid\ndata: second\n\n",
77
+ ),
78
+ ).toEqual([
79
+ { data: "first", retry: 1000 },
80
+ { data: "second", retry: 1000 },
81
+ ]);
82
+ });
83
+
84
+ it("ignores ids containing NUL and keeps ids containing spaces", () => {
85
+ const decoder = new SSEEventDecoder();
86
+ expect(
87
+ decoder.push(
88
+ "id: ok\ndata: a\n\nid: bad\u0000id\ndata: b\n\nid: spaced id\ndata: c\n\n",
89
+ ),
90
+ ).toEqual([
91
+ { data: "a", id: "ok" },
92
+ { data: "b", id: "ok" },
93
+ { data: "c", id: "spaced id" },
94
+ ]);
95
+ });
96
+
97
+ it("ignores non-digit and unsafe-integer retry values", () => {
98
+ const decoder = new SSEEventDecoder();
99
+ expect(
100
+ decoder.push(
101
+ "retry: 1000\ndata: a\n\nretry: -1\ndata: b\n\nretry: 1.5\ndata: c\n\nretry: 1e3\ndata: d\n\nretry:\ndata: e\n\n" +
102
+ `retry: ${"9".repeat(400)}\ndata: f\n\n`,
103
+ ),
104
+ ).toEqual([
105
+ { data: "a", retry: 1000 },
106
+ { data: "b", retry: 1000 },
107
+ { data: "c", retry: 1000 },
108
+ { data: "d", retry: 1000 },
109
+ { data: "e", retry: 1000 },
110
+ { data: "f", retry: 1000 },
111
+ ]);
112
+ });
113
+
114
+ it("does not dispatch blank frames", () => {
115
+ const decoder = new SSEEventDecoder();
116
+ expect(decoder.push("event: update\n\ndata: x\n\n")).toEqual([
117
+ { data: "x" },
118
+ ]);
119
+ });
120
+
121
+ it("preserves a pending LF across empty pushes", () => {
122
+ const decoder = new SSEEventDecoder();
123
+ expect(decoder.push("data: x\r")).toEqual([]);
124
+ expect(decoder.push("")).toEqual([]);
125
+ expect(decoder.push("\n\r\n")).toEqual([{ data: "x" }]);
126
+ });
127
+
128
+ it("drops unterminated trailing data by default", () => {
129
+ const decoder = new SSEEventDecoder();
130
+ expect(decoder.push("data: x")).toEqual([]);
131
+ expect(decoder.flush()).toBeNull();
132
+ });
133
+
134
+ it("dispatches an unterminated final data line when configured", () => {
135
+ const decoder = new SSEEventDecoder({ trailing: "dispatch" });
136
+ expect(decoder.push("data: x")).toEqual([]);
137
+ expect(decoder.flush()).toEqual({ data: "x" });
138
+ });
139
+
140
+ it("makes flush idempotent", () => {
141
+ const decoder = new SSEEventDecoder({ trailing: "dispatch" });
142
+ decoder.push("data: x");
143
+ expect(decoder.flush()).toEqual({ data: "x" });
144
+ expect(decoder.flush()).toBeNull();
145
+ });
146
+ });
@@ -0,0 +1,109 @@
1
+ export type SSEEvent = {
2
+ event?: string;
3
+ data: string;
4
+ id?: string;
5
+ retry?: number;
6
+ };
7
+
8
+ export class SSEEventDecoder {
9
+ private lineBuffer = "";
10
+ private dataLines: string[] = [];
11
+ private eventName: string | undefined;
12
+ private lastEventId: string | undefined;
13
+ private retry: number | undefined;
14
+ private pendingLF = false;
15
+ private readonly trailing: "drop" | "dispatch";
16
+
17
+ constructor(options?: { trailing?: "drop" | "dispatch" }) {
18
+ this.trailing = options?.trailing ?? "drop";
19
+ }
20
+
21
+ push(text: string): SSEEvent[] {
22
+ const events: SSEEvent[] = [];
23
+ if (text === "") return events;
24
+
25
+ // Lines end with LF, CRLF, or CR. A chunk-trailing "\r" terminates its
26
+ // line immediately; pendingLF then swallows the leading "\n" of the next
27
+ // chunk so a CRLF split across chunks is not counted twice.
28
+ if (this.pendingLF && text.startsWith("\n")) text = text.slice(1);
29
+ this.pendingLF = text.endsWith("\r");
30
+
31
+ this.lineBuffer += text;
32
+ const lines = this.lineBuffer.split(/\r\n|\r|\n/);
33
+ this.lineBuffer = lines.pop()!;
34
+
35
+ for (const line of lines) {
36
+ const event = this.processLine(line);
37
+ if (event) events.push(event);
38
+ }
39
+
40
+ return events;
41
+ }
42
+
43
+ flush(): SSEEvent | null {
44
+ if (this.trailing === "drop") {
45
+ this.resetFrame();
46
+ this.lineBuffer = "";
47
+ this.pendingLF = false;
48
+ return null;
49
+ }
50
+
51
+ if (this.lineBuffer.length > 0) {
52
+ this.processLine(this.lineBuffer);
53
+ }
54
+
55
+ this.lineBuffer = "";
56
+ this.pendingLF = false;
57
+ return this.dispatchEvent();
58
+ }
59
+
60
+ private processLine(line: string): SSEEvent | null {
61
+ if (line === "") return this.dispatchEvent();
62
+ if (line.startsWith(":")) return null;
63
+
64
+ const separator = line.indexOf(":");
65
+ const field = separator === -1 ? line : line.slice(0, separator);
66
+ let value = separator === -1 ? "" : line.slice(separator + 1);
67
+ if (value.startsWith(" ")) value = value.slice(1);
68
+
69
+ switch (field) {
70
+ case "data":
71
+ this.dataLines.push(value);
72
+ break;
73
+ case "event":
74
+ this.eventName = value;
75
+ break;
76
+ case "id":
77
+ if (!value.includes("\u0000")) this.lastEventId = value;
78
+ break;
79
+ case "retry": {
80
+ const retry = Number(value);
81
+ if (/^\d+$/.test(value) && Number.isSafeInteger(retry)) {
82
+ this.retry = retry;
83
+ }
84
+ break;
85
+ }
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ private dispatchEvent(): SSEEvent | null {
92
+ if (this.dataLines.length === 0) {
93
+ this.resetFrame();
94
+ return null;
95
+ }
96
+
97
+ const event: SSEEvent = { data: this.dataLines.join("\n") };
98
+ if (this.eventName !== undefined) event.event = this.eventName;
99
+ if (this.lastEventId !== undefined) event.id = this.lastEventId;
100
+ if (this.retry !== undefined) event.retry = this.retry;
101
+ this.resetFrame();
102
+ return event;
103
+ }
104
+
105
+ private resetFrame() {
106
+ this.dataLines = [];
107
+ this.eventName = undefined;
108
+ }
109
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ SSEEventDecoderStream,
4
+ type PipelineSSEEvent,
5
+ } from "./SSEEventDecoderStream";
6
+
7
+ const collectEvents = async (chunks: string[]): Promise<PipelineSSEEvent[]> => {
8
+ const stream = new ReadableStream<string>({
9
+ start(controller) {
10
+ for (const chunk of chunks) controller.enqueue(chunk);
11
+ controller.close();
12
+ },
13
+ }).pipeThrough(new SSEEventDecoderStream());
14
+
15
+ const events: PipelineSSEEvent[] = [];
16
+ for await (const event of stream as unknown as AsyncIterable<PipelineSSEEvent>) {
17
+ events.push(event);
18
+ }
19
+ return events;
20
+ };
21
+
22
+ describe("SSEEventDecoderStream", () => {
23
+ it("defaults unnamed events to message", async () => {
24
+ const events = await collectEvents(["data: hello\n\n"]);
25
+ expect(events).toEqual([{ event: "message", data: "hello" }]);
26
+ });
27
+
28
+ it("normalizes an explicit empty event field to message", async () => {
29
+ const events = await collectEvents(["event:\ndata: hello\n\n"]);
30
+ expect(events).toEqual([{ event: "message", data: "hello" }]);
31
+ });
32
+
33
+ it("passes named events through", async () => {
34
+ const events = await collectEvents(["event: custom\ndata: hello\n\n"]);
35
+ expect(events).toEqual([{ event: "custom", data: "hello" }]);
36
+ });
37
+
38
+ it("drops an unterminated trailing frame", async () => {
39
+ const events = await collectEvents(["data: partial\n"]);
40
+ expect(events).toEqual([]);
41
+ });
42
+ });
@@ -0,0 +1,34 @@
1
+ import { SSEEventDecoder } from "./SSEEventDecoder";
2
+ import type { SSEEvent } from "./SSEEventDecoder";
3
+
4
+ export type PipelineSSEEvent = {
5
+ event: string;
6
+ data: string;
7
+ id?: string;
8
+ retry?: number;
9
+ };
10
+
11
+ export class SSEEventDecoderStream extends TransformStream<
12
+ string,
13
+ PipelineSSEEvent
14
+ > {
15
+ constructor() {
16
+ const decoder = new SSEEventDecoder({ trailing: "drop" });
17
+ const normalize = (event: SSEEvent): PipelineSSEEvent => ({
18
+ ...event,
19
+ event: event.event || "message",
20
+ });
21
+
22
+ super({
23
+ transform(chunk, controller) {
24
+ for (const event of decoder.push(chunk)) {
25
+ controller.enqueue(normalize(event));
26
+ }
27
+ },
28
+ flush(controller) {
29
+ const event = decoder.flush();
30
+ if (event) controller.enqueue(normalize(event));
31
+ },
32
+ });
33
+ }
34
+ }
@@ -9,9 +9,18 @@ type MergeStreamItem = {
9
9
  export const createMergeStream = () => {
10
10
  const list: MergeStreamItem[] = [];
11
11
  let sealed = false;
12
+ let cancelled = false;
13
+ let errored = false;
12
14
  let controller: ReadableStreamDefaultController<AssistantStreamChunk>;
13
15
  let currentPull: ReturnType<typeof promiseWithResolvers<void>> | undefined;
14
16
 
17
+ const cancelAllReaders = () => {
18
+ list.forEach((item) => {
19
+ void item.reader.cancel().catch(() => undefined);
20
+ });
21
+ list.length = 0;
22
+ };
23
+
15
24
  const handlePull = (item: MergeStreamItem) => {
16
25
  if (!item.promise) {
17
26
  // TODO for most streams, we can directly pipeTo to avoid the microTask queue
@@ -23,6 +32,8 @@ export const createMergeStream = () => {
23
32
  .read()
24
33
  .then(({ done, value }) => {
25
34
  item.promise = undefined;
35
+ if (cancelled || errored) return;
36
+
26
37
  if (done) {
27
38
  list.splice(list.indexOf(item), 1);
28
39
  if (sealed && list.length === 0) {
@@ -36,12 +47,11 @@ export const createMergeStream = () => {
36
47
  currentPull = undefined;
37
48
  })
38
49
  .catch((e) => {
39
- console.error(e);
50
+ if (cancelled || errored) return;
40
51
 
41
- list.forEach((item) => {
42
- item.reader.cancel();
43
- });
44
- list.length = 0;
52
+ errored = true;
53
+ console.error(e);
54
+ cancelAllReaders();
45
55
 
46
56
  controller.error(e);
47
57
 
@@ -64,10 +74,10 @@ export const createMergeStream = () => {
64
74
  return currentPull.promise;
65
75
  },
66
76
  cancel() {
67
- list.forEach((item) => {
68
- item.reader.cancel();
69
- });
70
- list.length = 0;
77
+ cancelled = true;
78
+ cancelAllReaders();
79
+ currentPull?.resolve();
80
+ currentPull = undefined;
71
81
  },
72
82
  });
73
83
 
@@ -76,11 +86,23 @@ export const createMergeStream = () => {
76
86
  isSealed() {
77
87
  return sealed;
78
88
  },
89
+ isCancelled() {
90
+ return cancelled;
91
+ },
92
+ isErrored() {
93
+ return errored;
94
+ },
79
95
  seal() {
96
+ if (cancelled || errored) return;
80
97
  sealed = true;
81
98
  if (list.length === 0) controller.close();
82
99
  },
83
100
  addStream(stream: ReadableStream<AssistantStreamChunk>) {
101
+ if (cancelled || errored) {
102
+ void stream.cancel().catch(() => undefined);
103
+ return;
104
+ }
105
+
84
106
  if (sealed)
85
107
  throw new Error(
86
108
  "Cannot add streams after the run callback has settled.",
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as entry from "./index";
3
+
4
+ describe("assistant-transport state exports", () => {
5
+ it("exposes the assistant-transport surface and no gorp-shaped names", () => {
6
+ expect(typeof entry.AssistantTransportDeltaTracker).toBe("function");
7
+ expect(typeof entry.createObjectStream).toBe("function");
8
+ expect(typeof entry.ObjectStreamResponse).toBe("function");
9
+ expect(typeof entry.fromObjectStreamResponse).toBe("function");
10
+ for (const name of Object.keys(entry)) {
11
+ expect(name.toLowerCase()).not.toContain("gorp");
12
+ }
13
+ });
14
+
15
+ it("streams operations end to end through the SSE wire", async () => {
16
+ const stream = entry.createObjectStream({
17
+ execute: (controller) => {
18
+ controller.enqueue([
19
+ { type: "set", path: ["message"], value: "Hello" },
20
+ ]);
21
+ controller.enqueue([
22
+ { type: "append-text", path: ["message"], value: " World" },
23
+ ]);
24
+ },
25
+ });
26
+
27
+ const response = new entry.ObjectStreamResponse(stream);
28
+ const decoded = entry.fromObjectStreamResponse(response);
29
+
30
+ const tracker = new entry.AssistantTransportDeltaTracker();
31
+ const reader = decoded.getReader();
32
+ while (true) {
33
+ const { done, value } = await reader.read();
34
+ if (done) break;
35
+ tracker.append(value.operations);
36
+ expect(tracker.state).toEqual(value.snapshot);
37
+ }
38
+
39
+ expect(tracker.state).toEqual({ message: "Hello World" });
40
+ expect(tracker.isChangedAt(["message"])).toBe(true);
41
+ });
42
+ });
package/src/index.ts CHANGED
@@ -62,12 +62,27 @@ export {
62
62
  export type { TextStreamController } from "./core/modules/text";
63
63
  export type { ToolCallStreamController } from "./core/modules/tool-call";
64
64
 
65
- export { createObjectStream } from "./core/object/createObjectStream";
66
- export {
67
- ObjectStreamResponse,
68
- fromObjectStreamResponse,
69
- } from "./core/object/ObjectStreamResponse";
70
- export type { ObjectStreamChunk } from "./core/object/types";
65
+ import { createGorpStream } from "./core/gorp/createGorpStream";
66
+ import {
67
+ GorpStreamResponse,
68
+ fromGorpStreamResponse,
69
+ } from "./core/gorp/GorpStreamResponse";
70
+ import type { GorpStreamChunk } from "./core/gorp/types";
71
+ import { GorpStreamDeltaTracker } from "./core/gorp/GorpStreamDeltaTracker";
72
+
73
+ export { GorpStreamDeltaTracker as AssistantTransportDeltaTracker };
74
+ export type { AssistantTransportStateOperation } from "./core/gorp/types";
75
+
76
+ /** @deprecated Use the assistant-transport surface instead. */
77
+ export const createObjectStream = createGorpStream;
78
+ /** @deprecated Use the assistant-transport surface instead. */
79
+ export const ObjectStreamResponse = GorpStreamResponse;
80
+ /** @deprecated Use the assistant-transport surface instead. */
81
+ export type ObjectStreamResponse = GorpStreamResponse;
82
+ /** @deprecated Use the assistant-transport surface instead. */
83
+ export const fromObjectStreamResponse = fromGorpStreamResponse;
84
+ /** @deprecated Use the assistant-transport surface instead. */
85
+ export type ObjectStreamChunk = GorpStreamChunk;
71
86
 
72
87
  export {
73
88
  toGenericMessages,