assistant-stream 0.2.0 → 0.2.1

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 (129) hide show
  1. package/dist/ai-sdk/index.d.ts +5 -0
  2. package/dist/ai-sdk/index.d.ts.map +1 -0
  3. package/dist/ai-sdk/language-model.d.ts +6 -0
  4. package/dist/ai-sdk/language-model.d.ts.map +1 -0
  5. package/dist/ai-sdk.d.ts +3 -0
  6. package/dist/ai-sdk.d.ts.map +1 -0
  7. package/dist/core/AssistantStream.d.ts +12 -0
  8. package/dist/core/AssistantStream.d.ts.map +1 -0
  9. package/dist/core/AssistantStreamChunk.d.ts +64 -0
  10. package/dist/core/AssistantStreamChunk.d.ts.map +1 -0
  11. package/dist/core/accumulators/AssistantMessageStream.d.ts +13 -0
  12. package/dist/core/accumulators/AssistantMessageStream.d.ts.map +1 -0
  13. package/dist/core/accumulators/assistant-message-accumulator.d.ts +8 -0
  14. package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -0
  15. package/dist/core/index.d.ts +11 -0
  16. package/dist/core/index.d.ts.map +1 -0
  17. package/dist/core/modules/assistant-stream.d.ts +31 -0
  18. package/dist/core/modules/assistant-stream.d.ts.map +1 -0
  19. package/dist/core/modules/text.d.ts +9 -0
  20. package/dist/core/modules/text.d.ts.map +1 -0
  21. package/dist/core/modules/tool-call.d.ts +13 -0
  22. package/dist/core/modules/tool-call.d.ts.map +1 -0
  23. package/dist/core/serialization/PlainText.d.ts +11 -0
  24. package/dist/core/serialization/PlainText.d.ts.map +1 -0
  25. package/dist/core/serialization/data-stream/DataStream.d.ts +11 -0
  26. package/dist/core/serialization/data-stream/DataStream.d.ts.map +1 -0
  27. package/dist/core/serialization/data-stream/chunk-types.d.ts +86 -0
  28. package/dist/core/serialization/data-stream/chunk-types.d.ts.map +1 -0
  29. package/dist/core/serialization/data-stream/serialization.d.ts +8 -0
  30. package/dist/core/serialization/data-stream/serialization.d.ts.map +1 -0
  31. package/dist/core/tool/ToolCallReader.d.ts +33 -0
  32. package/dist/core/tool/ToolCallReader.d.ts.map +1 -0
  33. package/dist/core/tool/ToolExecutionStream.d.ts +24 -0
  34. package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -0
  35. package/dist/core/tool/ToolResponse.d.ts +17 -0
  36. package/dist/core/tool/ToolResponse.d.ts.map +1 -0
  37. package/dist/core/tool/index.d.ts +6 -0
  38. package/dist/core/tool/index.d.ts.map +1 -0
  39. package/dist/core/tool/tool-types.d.ts +76 -0
  40. package/dist/core/tool/tool-types.d.ts.map +1 -0
  41. package/dist/core/tool/toolResultStream.d.ts +6 -0
  42. package/dist/core/tool/toolResultStream.d.ts.map +1 -0
  43. package/dist/core/tool/type-path-utils.d.ts +23 -0
  44. package/dist/core/tool/type-path-utils.d.ts.map +1 -0
  45. package/dist/core/utils/Counter.d.ts +5 -0
  46. package/dist/core/utils/Counter.d.ts.map +1 -0
  47. package/dist/core/utils/generateId.d.ts +2 -0
  48. package/dist/core/utils/generateId.d.ts.map +1 -0
  49. package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts +20 -0
  50. package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts.map +1 -0
  51. package/dist/core/utils/stream/AssistantTransformStream.d.ts +15 -0
  52. package/dist/core/utils/stream/AssistantTransformStream.d.ts.map +1 -0
  53. package/dist/core/utils/stream/LineDecoderStream.d.ts +5 -0
  54. package/dist/core/utils/stream/LineDecoderStream.d.ts.map +1 -0
  55. package/dist/core/utils/stream/PipeableTransformStream.d.ts +4 -0
  56. package/dist/core/utils/stream/PipeableTransformStream.d.ts.map +1 -0
  57. package/dist/core/utils/stream/UnderlyingReadable.d.ts +6 -0
  58. package/dist/core/utils/stream/UnderlyingReadable.d.ts.map +1 -0
  59. package/dist/core/utils/stream/merge.d.ts +9 -0
  60. package/dist/core/utils/stream/merge.d.ts.map +1 -0
  61. package/dist/core/utils/stream/path-utils.d.ts +12 -0
  62. package/dist/core/utils/stream/path-utils.d.ts.map +1 -0
  63. package/dist/core/utils/types.d.ts +102 -0
  64. package/dist/core/utils/types.d.ts.map +1 -0
  65. package/dist/core/utils/withPromiseOrValue.d.ts +2 -0
  66. package/dist/core/utils/withPromiseOrValue.d.ts.map +1 -0
  67. package/dist/index.d.ts +2 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/utils/AsyncIterableStream.d.ts +3 -0
  70. package/dist/utils/AsyncIterableStream.d.ts.map +1 -0
  71. package/dist/utils/json/fix-json.d.ts +2 -0
  72. package/dist/utils/json/fix-json.d.ts.map +1 -0
  73. package/dist/utils/json/is-json.d.ts +5 -0
  74. package/dist/utils/json/is-json.d.ts.map +1 -0
  75. package/dist/utils/json/json-value.d.ts +6 -0
  76. package/dist/utils/json/json-value.d.ts.map +1 -0
  77. package/dist/utils/json/parse-partial-json-object.d.ts +14 -0
  78. package/dist/utils/json/parse-partial-json-object.d.ts.map +1 -0
  79. package/dist/utils/json/parse-partial-json-object.test.d.ts +2 -0
  80. package/dist/utils/json/parse-partial-json-object.test.d.ts.map +1 -0
  81. package/dist/utils/promiseWithResolvers.d.ts +6 -0
  82. package/dist/utils/promiseWithResolvers.d.ts.map +1 -0
  83. package/dist/utils.d.ts +4 -0
  84. package/dist/utils.d.ts.map +1 -0
  85. package/package.json +3 -2
  86. package/src/ai-sdk/index.ts +204 -0
  87. package/src/ai-sdk/language-model.ts +122 -0
  88. package/src/ai-sdk.ts +2 -0
  89. package/src/core/AssistantStream.ts +39 -0
  90. package/src/core/AssistantStreamChunk.ts +93 -0
  91. package/src/core/accumulators/AssistantMessageStream.ts +56 -0
  92. package/src/core/accumulators/assistant-message-accumulator.ts +394 -0
  93. package/src/core/index.ts +17 -0
  94. package/src/core/modules/assistant-stream.ts +264 -0
  95. package/src/core/modules/text.ts +64 -0
  96. package/src/core/modules/tool-call.ts +105 -0
  97. package/src/core/serialization/PlainText.ts +68 -0
  98. package/src/core/serialization/data-stream/DataStream.ts +355 -0
  99. package/src/core/serialization/data-stream/chunk-types.ts +93 -0
  100. package/src/core/serialization/data-stream/serialization.ts +32 -0
  101. package/src/core/tool/ToolCallReader.ts +432 -0
  102. package/src/core/tool/ToolExecutionStream.ts +180 -0
  103. package/src/core/tool/ToolResponse.ts +31 -0
  104. package/src/core/tool/index.ts +8 -0
  105. package/src/core/tool/tool-types.ts +107 -0
  106. package/src/core/tool/toolResultStream.ts +119 -0
  107. package/src/core/tool/type-path-utils.ts +36 -0
  108. package/src/core/utils/Counter.ts +7 -0
  109. package/src/core/utils/generateId.tsx +6 -0
  110. package/src/core/utils/stream/AssistantMetaTransformStream.ts +74 -0
  111. package/src/core/utils/stream/AssistantTransformStream.ts +74 -0
  112. package/src/core/utils/stream/LineDecoderStream.ts +29 -0
  113. package/src/core/utils/stream/PipeableTransformStream.ts +10 -0
  114. package/src/core/utils/stream/UnderlyingReadable.ts +5 -0
  115. package/src/core/utils/stream/merge.ts +212 -0
  116. package/src/core/utils/stream/path-utils.ts +71 -0
  117. package/src/core/utils/types.ts +150 -0
  118. package/src/core/utils/withPromiseOrValue.ts +20 -0
  119. package/src/index.ts +1 -0
  120. package/src/utils/AsyncIterableStream.ts +24 -0
  121. package/src/utils/json/fix-json.ts +488 -0
  122. package/src/utils/json/is-json.ts +43 -0
  123. package/src/utils/json/json-value.ts +13 -0
  124. package/src/utils/json/parse-partial-json-object.test.ts +225 -0
  125. package/src/utils/json/parse-partial-json-object.ts +103 -0
  126. package/src/utils/promiseWithResolvers.ts +10 -0
  127. package/src/utils.ts +13 -0
  128. package/utils/README.md +0 -1
  129. package/utils/package.json +0 -5
@@ -0,0 +1,107 @@
1
+ import { JSONSchema7 } from "json-schema";
2
+ import { TypeAtPath, TypePath } from "./type-path-utils";
3
+ import { DeepPartial } from "ai";
4
+ import { AsyncIterableStream } from "../../utils";
5
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
6
+ import { ToolResponse } from "./ToolResponse";
7
+
8
+ /**
9
+ * Interface for reading tool call arguments from a stream, which are
10
+ * generated by a language learning model (LLM). Provides methods to
11
+ * retrieve specific values, partial streams, or complete items from
12
+ * an array, based on a specified path.
13
+ *
14
+ * @template TArgs The type of arguments being read.
15
+ */
16
+ export interface ToolCallArgsReader<TArgs> {
17
+ /**
18
+ * Returns a promise that will resolve to the value at the given path,
19
+ * as soon as that path is generated by the LLM.
20
+ *
21
+ * @param fieldPath An array of object keys or array indices.
22
+ */
23
+ get<PathT extends TypePath<TArgs>>(
24
+ ...fieldPath: PathT
25
+ ): Promise<TypeAtPath<TArgs, PathT>>;
26
+
27
+ /**
28
+ * Returns a stream that will emit partial values at the given path,
29
+ * as they are generated by the LLM.
30
+ *
31
+ * @param fieldPath An array of object keys or array indices.
32
+ */
33
+ streamValues<PathT extends TypePath<TArgs>>(
34
+ ...fieldPath: PathT
35
+ ): AsyncIterableStream<DeepPartial<TypeAtPath<TArgs, PathT>>>;
36
+
37
+ /**
38
+ * Returns a stream that will emit partial text at the given path,
39
+ * as they are generated by the LLM.
40
+ *
41
+ * @param fieldPath An array of object keys or array indices.
42
+ */
43
+ streamText<PathT extends TypePath<TArgs>>(
44
+ ...fieldPath: PathT
45
+ ): TypeAtPath<TArgs, PathT> extends string & infer U
46
+ ? AsyncIterableStream<U>
47
+ : never;
48
+
49
+ /**
50
+ * Returns a stream that will emit complete items in the array
51
+ * at the given path, as they are generated by the LLM.
52
+ *
53
+ * @param fieldPath An array of object keys or array indices.
54
+ */
55
+ forEach<PathT extends TypePath<TArgs>>(
56
+ ...fieldPath: PathT
57
+ ): TypeAtPath<TArgs, PathT> extends Array<infer U>
58
+ ? AsyncIterableStream<U>
59
+ : never;
60
+ }
61
+
62
+ export interface ToolCallResponseReader<TResult> {
63
+ get: () => Promise<ToolResponse<TResult>>;
64
+ }
65
+
66
+ export interface ToolCallReader<TArgs, TResult> {
67
+ args: ToolCallArgsReader<TArgs>;
68
+ response: ToolCallResponseReader<TResult>;
69
+
70
+ /**
71
+ * @deprecated Deprecated. Use `response.get().result` instead.
72
+ */
73
+ result: {
74
+ get: () => Promise<TResult>;
75
+ };
76
+ }
77
+
78
+ type ToolExecutionContext = {
79
+ toolCallId: string;
80
+ abortSignal: AbortSignal;
81
+ };
82
+
83
+ export type ToolExecuteFunction<TArgs, TResult> = (
84
+ args: TArgs,
85
+ context: ToolExecutionContext,
86
+ ) => TResult | Promise<TResult>;
87
+
88
+ export type ToolStreamCallFunction<TArgs, TResult> = (
89
+ reader: ToolCallReader<TArgs, TResult>,
90
+ context: ToolExecutionContext,
91
+ ) => void;
92
+
93
+ type OnSchemaValidationErrorFunction<TResult> = ToolExecuteFunction<
94
+ unknown,
95
+ TResult
96
+ >;
97
+
98
+ export type Tool<TArgs = unknown, TResult = unknown> = {
99
+ description?: string | undefined;
100
+ parameters: StandardSchemaV1<TArgs> | JSONSchema7;
101
+ execute?: ToolExecuteFunction<TArgs, TResult>;
102
+ /**
103
+ * @deprecated Experimental, API may change.
104
+ */
105
+ streamCall?: ToolStreamCallFunction<TArgs, TResult>;
106
+ experimental_onSchemaValidationError?: OnSchemaValidationErrorFunction<TResult>;
107
+ };
@@ -0,0 +1,119 @@
1
+ import { Tool, ToolCallReader, ToolExecuteFunction } from "./tool-types";
2
+ import { StandardSchemaV1 } from "@standard-schema/spec";
3
+ import { ToolResponse } from "./ToolResponse";
4
+ import { ToolExecutionStream } from "./ToolExecutionStream";
5
+ import { AssistantMessage } from "../utils/types";
6
+
7
+ const isStandardSchemaV1 = (
8
+ schema: unknown,
9
+ ): schema is StandardSchemaV1<unknown> => {
10
+ return (
11
+ typeof schema === "object" &&
12
+ schema !== null &&
13
+ "~standard" in schema &&
14
+ (schema as StandardSchemaV1<unknown>)["~standard"].version === 1
15
+ );
16
+ };
17
+
18
+ function getToolResponse(
19
+ tools: Record<string, Tool<any, any>> | undefined,
20
+ abortSignal: AbortSignal,
21
+ toolCall: {
22
+ toolCallId: string;
23
+ toolName: string;
24
+ args: unknown;
25
+ },
26
+ ) {
27
+ const tool = tools?.[toolCall.toolName];
28
+ if (!tool || !tool.execute) return undefined;
29
+
30
+ const getResult = async (toolExecute: ToolExecuteFunction<any, any>) => {
31
+ let executeFn = toolExecute;
32
+
33
+ if (isStandardSchemaV1(tool.parameters)) {
34
+ let result = tool.parameters["~standard"].validate(toolCall.args);
35
+ if (result instanceof Promise) result = await result;
36
+
37
+ if (result.issues) {
38
+ executeFn =
39
+ tool.experimental_onSchemaValidationError ??
40
+ (() => {
41
+ throw new Error(
42
+ `Function parameter validation failed. ${JSON.stringify(result.issues)}`,
43
+ );
44
+ });
45
+ }
46
+ }
47
+
48
+ const result = await executeFn(toolCall.args, {
49
+ toolCallId: toolCall.toolCallId,
50
+ abortSignal,
51
+ });
52
+ if (result instanceof ToolResponse) return result;
53
+ return new ToolResponse({
54
+ result: result === undefined ? "<no result>" : result,
55
+ });
56
+ };
57
+
58
+ return getResult(tool.execute);
59
+ }
60
+
61
+ function getToolStreamResponse<TArgs, TResult>(
62
+ tools: Record<string, Tool<any, any>> | undefined,
63
+ abortSignal: AbortSignal,
64
+ reader: ToolCallReader<TArgs, TResult>,
65
+ context: {
66
+ toolCallId: string;
67
+ toolName: string;
68
+ },
69
+ ) {
70
+ tools?.[context.toolName]?.streamCall?.(reader, {
71
+ toolCallId: context.toolCallId,
72
+ abortSignal,
73
+ });
74
+ }
75
+
76
+ export async function unstable_runPendingTools(
77
+ message: AssistantMessage,
78
+ tools: Record<string, Tool<any, any>> | undefined,
79
+ abortSignal: AbortSignal,
80
+ ) {
81
+ // TODO parallel tool calling
82
+ for (const part of message.parts) {
83
+ if (part.type === "tool-call") {
84
+ const promiseOrUndefined = getToolResponse(tools, abortSignal, part);
85
+ if (promiseOrUndefined) {
86
+ const result = await promiseOrUndefined;
87
+ const updatedParts = message.parts.map((p) => {
88
+ if (p.type === "tool-call" && p.toolCallId === part.toolCallId) {
89
+ return {
90
+ ...p,
91
+ state: "result" as const,
92
+ artifact: result.artifact,
93
+ result: result.result,
94
+ isError: result.isError,
95
+ };
96
+ }
97
+ return p;
98
+ });
99
+ message = {
100
+ ...message,
101
+ parts: updatedParts,
102
+ content: updatedParts,
103
+ };
104
+ }
105
+ }
106
+ }
107
+ return message;
108
+ }
109
+
110
+ export function toolResultStream(
111
+ tools: Record<string, Tool<any, any>> | undefined,
112
+ abortSignal: AbortSignal,
113
+ ) {
114
+ return new ToolExecutionStream({
115
+ execute: (toolCall) => getToolResponse(tools, abortSignal, toolCall),
116
+ streamCall: ({ reader, ...context }) =>
117
+ getToolStreamResponse(tools, abortSignal, reader, context),
118
+ });
119
+ }
@@ -0,0 +1,36 @@
1
+ type AsNumber<K> = K extends `${infer N extends number}` ? N | K : never;
2
+ type TupleIndex<T extends readonly any[]> = Exclude<keyof T, keyof any[]>;
3
+ type ObjectKey<T> = keyof T & (string | number);
4
+
5
+ export type TypePath<T> =
6
+ | []
7
+ | (0 extends 1 & T // IsAny
8
+ ? any[]
9
+ : T extends object // IsObjectOrArrayOrTuple
10
+ ? T extends readonly any[] // IsArrayOrTuple
11
+ ? number extends T["length"] // IsTuple
12
+ ? // Tuple: make union of [index, ...TypePath<element>]
13
+ {
14
+ [K in TupleIndex<T>]: [AsNumber<K>, ...TypePath<T[K]>];
15
+ }[TupleIndex<T>]
16
+ : // Array: use number index
17
+ [number, ...TypePath<T[number]>]
18
+ : // Object: make union of [key, ...TypePath<value>]
19
+ { [K in ObjectKey<T>]: [K, ...TypePath<T[K]>] }[ObjectKey<T>]
20
+ : // Base case: primitive values have no path
21
+ []);
22
+
23
+ export type TypeAtPath<T, P extends readonly any[]> = P extends [
24
+ infer Head,
25
+ ...infer Rest,
26
+ ]
27
+ ? Head extends keyof T
28
+ ? TypeAtPath<T[Head], Rest>
29
+ : never
30
+ : T;
31
+
32
+ export type DeepPartial<T> = T extends readonly any[]
33
+ ? readonly DeepPartial<T[number]>[]
34
+ : T extends { [key: string]: any }
35
+ ? { readonly [K in keyof T]?: DeepPartial<T[K]> }
36
+ : T;
@@ -0,0 +1,7 @@
1
+ export class Counter {
2
+ public value = -1;
3
+
4
+ up() {
5
+ return ++this.value;
6
+ }
7
+ }
@@ -0,0 +1,6 @@
1
+ import { customAlphabet } from "nanoid/non-secure";
2
+
3
+ export const generateId = customAlphabet(
4
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
5
+ 7,
6
+ );
@@ -0,0 +1,74 @@
1
+ import { AssistantStreamChunk, PartInit } from "../../AssistantStreamChunk";
2
+
3
+ /**
4
+ * For chunk types that are associated with a part,
5
+ * we require a non‑nullable meta field.
6
+ */
7
+ export type AssistantMetaStreamChunk =
8
+ | (AssistantStreamChunk & {
9
+ type: "text-delta" | "part-finish";
10
+ meta: PartInit;
11
+ })
12
+ | (AssistantStreamChunk & {
13
+ type: "result" | "tool-call-args-text-finish";
14
+ meta: PartInit & { type: "tool-call" };
15
+ })
16
+ | (AssistantStreamChunk & {
17
+ type: Exclude<
18
+ AssistantStreamChunk["type"],
19
+ "text-delta" | "result" | "tool-call-args-text-finish" | "part-finish"
20
+ >;
21
+ });
22
+ export class AssistantMetaTransformStream extends TransformStream<
23
+ AssistantStreamChunk,
24
+ AssistantMetaStreamChunk
25
+ > {
26
+ constructor() {
27
+ // We use an array to record parts as they are introduced.
28
+ const parts: PartInit[] = [];
29
+
30
+ super({
31
+ transform(chunk, controller) {
32
+ // For chunks that introduce a new part.
33
+ if (chunk.type === "part-start") {
34
+ if (chunk.path.length !== 0) {
35
+ controller.error(new Error("Nested parts are not supported"));
36
+ return;
37
+ }
38
+ parts.push(chunk.part);
39
+ controller.enqueue(chunk);
40
+ return;
41
+ }
42
+
43
+ // For chunks that expect an associated part.
44
+ if (
45
+ chunk.type === "text-delta" ||
46
+ chunk.type === "result" ||
47
+ chunk.type === "part-finish" ||
48
+ chunk.type === "tool-call-args-text-finish"
49
+ ) {
50
+ if (chunk.path.length !== 1) {
51
+ controller.error(
52
+ new Error(`${chunk.type} chunks must have a path of length 1`),
53
+ );
54
+ return;
55
+ }
56
+ const idx = chunk.path[0]!;
57
+ if (idx < 0 || idx >= parts.length) {
58
+ controller.error(new Error(`Invalid path index: ${idx}`));
59
+ return;
60
+ }
61
+ const part = parts[idx]!;
62
+
63
+ controller.enqueue({
64
+ ...chunk,
65
+ meta: part as any, // TODO
66
+ });
67
+ return;
68
+ }
69
+
70
+ controller.enqueue(chunk);
71
+ },
72
+ });
73
+ }
74
+ }
@@ -0,0 +1,74 @@
1
+ import { AssistantStreamChunk } from "../../AssistantStreamChunk";
2
+ import {
3
+ AssistantStreamController,
4
+ createAssistantStreamController,
5
+ } from "../../modules/assistant-stream";
6
+
7
+ type AssistantTransformerFlushCallback = (
8
+ controller: AssistantStreamController,
9
+ ) => void | PromiseLike<void>;
10
+
11
+ type AssistantTransformerStartCallback = (
12
+ controller: AssistantStreamController,
13
+ ) => void | PromiseLike<void>;
14
+
15
+ type AssistantTransformerTransformCallback<I> = (
16
+ chunk: I,
17
+ controller: AssistantStreamController,
18
+ ) => void | PromiseLike<void>;
19
+
20
+ type AssistantTransformer<I> = {
21
+ flush?: AssistantTransformerFlushCallback;
22
+ start?: AssistantTransformerStartCallback;
23
+ transform?: AssistantTransformerTransformCallback<I>;
24
+ };
25
+
26
+ export class AssistantTransformStream<I> extends TransformStream<
27
+ I,
28
+ AssistantStreamChunk
29
+ > {
30
+ constructor(
31
+ transformer: AssistantTransformer<I>,
32
+ writableStrategy?: QueuingStrategy<I>,
33
+ readableStrategy?: QueuingStrategy<AssistantStreamChunk>,
34
+ ) {
35
+ const [stream, runController] = createAssistantStreamController();
36
+
37
+ let runPipeTask: Promise<void>;
38
+ super(
39
+ {
40
+ start(controller) {
41
+ runPipeTask = stream
42
+ .pipeTo(
43
+ new WritableStream({
44
+ write(chunk) {
45
+ controller.enqueue(chunk);
46
+ },
47
+ abort(reason?: any) {
48
+ controller.error(reason);
49
+ },
50
+ close() {
51
+ controller.terminate();
52
+ },
53
+ }),
54
+ )
55
+ .catch((error) => {
56
+ controller.error(error);
57
+ });
58
+
59
+ return transformer.start?.(runController);
60
+ },
61
+ transform(chunk) {
62
+ return transformer.transform?.(chunk, runController);
63
+ },
64
+ async flush() {
65
+ await transformer.flush?.(runController);
66
+ runController.close();
67
+ await runPipeTask;
68
+ },
69
+ },
70
+ writableStrategy,
71
+ readableStrategy,
72
+ );
73
+ }
74
+ }
@@ -0,0 +1,29 @@
1
+ export class LineDecoderStream extends TransformStream<string, string> {
2
+ private buffer = "";
3
+
4
+ constructor() {
5
+ super({
6
+ transform: (chunk, controller) => {
7
+ this.buffer += chunk;
8
+ const lines = this.buffer.split("\n");
9
+
10
+ // Process all complete lines
11
+ for (let i = 0; i < lines.length - 1; i++) {
12
+ controller.enqueue(lines[i]);
13
+ }
14
+
15
+ // Keep the last incomplete line in the buffer
16
+ this.buffer = lines[lines.length - 1] || "";
17
+ },
18
+ flush: () => {
19
+ // If there's content in the buffer when the stream ends, it means
20
+ // the stream ended with an incomplete line (no trailing newline)
21
+ if (this.buffer) {
22
+ throw new Error(
23
+ `Stream ended with an incomplete line: "${this.buffer}"`,
24
+ );
25
+ }
26
+ },
27
+ });
28
+ }
29
+ }
@@ -0,0 +1,10 @@
1
+ export class PipeableTransformStream<I, O> extends TransformStream<I, O> {
2
+ constructor(transform: (readable: ReadableStream<I>) => ReadableStream<O>) {
3
+ super();
4
+ const readable = transform(super.readable as unknown as ReadableStream<I>);
5
+ Object.defineProperty(this, "readable", {
6
+ value: readable,
7
+ writable: false,
8
+ });
9
+ }
10
+ }
@@ -0,0 +1,5 @@
1
+ export type UnderlyingReadable<TController> = {
2
+ start?: (controller: TController) => void;
3
+ pull?: (controller: TController) => void | PromiseLike<void>;
4
+ cancel?: UnderlyingSourceCancelCallback;
5
+ };
@@ -0,0 +1,212 @@
1
+ import { AssistantStreamChunk } from "../../AssistantStreamChunk";
2
+ import { promiseWithResolvers } from "../../../utils/promiseWithResolvers";
3
+
4
+ type MergeStreamItem = {
5
+ reader: ReadableStreamDefaultReader<AssistantStreamChunk>;
6
+ promise?: Promise<unknown> | undefined;
7
+ };
8
+
9
+ export const createMergeStream = () => {
10
+ const list: MergeStreamItem[] = [];
11
+ let sealed = false;
12
+ let controller: ReadableStreamDefaultController<AssistantStreamChunk>;
13
+ let currentPull: ReturnType<typeof promiseWithResolvers<void>> | undefined;
14
+
15
+ const handlePull = (item: MergeStreamItem) => {
16
+ if (!item.promise) {
17
+ // TODO for most streams, we can directly pipeTo to avoid the microTask queue
18
+ // add an option to eagerly pipe the stream to the merge stream
19
+ // ideally, using assitant-stream w sync run method + piping to a sync WritableStream runs in the same microtask
20
+ // this is useful because we often use AssistantStreams internally as a serialization utility, e. g. AssistantTransformStream
21
+ // idea: avoid reader.read() by instead using a WritableStream & if (!hasPendingPull) await waitForPull()?
22
+ item.promise = item.reader
23
+ .read()
24
+ .then(({ done, value }) => {
25
+ item.promise = undefined;
26
+ if (done) {
27
+ list.splice(list.indexOf(item), 1);
28
+ if (sealed && list.length === 0) {
29
+ controller.close();
30
+ }
31
+ } else {
32
+ controller.enqueue(value);
33
+ }
34
+
35
+ currentPull?.resolve();
36
+ currentPull = undefined;
37
+ })
38
+ .catch((e) => {
39
+ console.error(e);
40
+
41
+ list.forEach((item) => {
42
+ item.reader.cancel();
43
+ });
44
+ list.length = 0;
45
+
46
+ controller.error(e);
47
+
48
+ currentPull?.reject(e);
49
+ currentPull = undefined;
50
+ });
51
+ }
52
+ };
53
+
54
+ const readable = new ReadableStream<AssistantStreamChunk>({
55
+ start(c) {
56
+ controller = c;
57
+ },
58
+ pull() {
59
+ currentPull = promiseWithResolvers();
60
+ list.forEach((item) => {
61
+ handlePull(item);
62
+ });
63
+
64
+ return currentPull.promise;
65
+ },
66
+ cancel() {
67
+ list.forEach((item) => {
68
+ item.reader.cancel();
69
+ });
70
+ list.length = 0;
71
+ },
72
+ });
73
+
74
+ return {
75
+ readable,
76
+ isSealed() {
77
+ return sealed;
78
+ },
79
+ seal() {
80
+ sealed = true;
81
+ if (list.length === 0) controller.close();
82
+ },
83
+ addStream(stream: ReadableStream<AssistantStreamChunk>) {
84
+ if (sealed)
85
+ throw new Error(
86
+ "Cannot add streams after the run callback has settled.",
87
+ );
88
+
89
+ const item = { reader: stream.getReader() };
90
+ list.push(item);
91
+ handlePull(item);
92
+ },
93
+ enqueue(chunk: AssistantStreamChunk) {
94
+ this.addStream(
95
+ new ReadableStream({
96
+ start(c) {
97
+ c.enqueue(chunk);
98
+ c.close();
99
+ },
100
+ }),
101
+ );
102
+ },
103
+ };
104
+ };
105
+
106
+ // TODO
107
+ // export class SpanContainerMerger {
108
+ // public get isSealed() {
109
+ // return this.mergeStream.isSealed();
110
+ // }
111
+
112
+ // public get readable() {
113
+ // return this.mergeStream.readable;
114
+ // }
115
+
116
+ // private subAllocator = new Counter();
117
+ // private mergeStream = createMergeStream();
118
+
119
+ // constructor() {
120
+ // // id 0 is auto allocated
121
+ // this.subAllocator.up();
122
+ // }
123
+
124
+ // add(stream: ReadableStream<AssistantStreamChunk>) {
125
+ // this.mergeStream.addStream(
126
+ // stream.pipeThrough(new SpanParentEncoder(this.subAllocator)),
127
+ // );
128
+ // }
129
+
130
+ // enqueue(chunk: AssistantStreamChunk & { parentId: 0 }) {
131
+ // this.mergeStream.addStream(
132
+ // new ReadableStream({
133
+ // start(c) {
134
+ // c.enqueue(chunk);
135
+ // c.close();
136
+ // },
137
+ // }),
138
+ // );
139
+ // }
140
+
141
+ // seal() {
142
+ // this.mergeStream.seal();
143
+ // }
144
+ // }
145
+
146
+ // export class SpanContainerSplitter {
147
+ // public writable;
148
+
149
+ // private isSealed = false;
150
+ // private writers = new Map<
151
+ // number,
152
+ // WritableStreamDefaultWriter<AssistantStreamChunk>
153
+ // >();
154
+
155
+ // private closeTasks: Promise<void>[] = [];
156
+
157
+ // private allocator = new Counter();
158
+ // private subAllocator = new Counter();
159
+
160
+ // constructor() {
161
+ // // id 0 is auto-allocated
162
+ // this.allocator.up();
163
+
164
+ // this.writable = new WritableStream({
165
+ // write: (chunk) => {
166
+ // const { type, parentId } = chunk;
167
+
168
+ // const writer = this.writers.get(parentId);
169
+ // if (writer === undefined) throw new Error("Parent id not found");
170
+
171
+ // writer.write(chunk);
172
+
173
+ // if (type === "span") {
174
+ // // allocate a new span id
175
+ // this.writers.set(this.allocator.up(), writer);
176
+ // }
177
+ // if (type === "finish") {
178
+ // this.writers.delete(parentId);
179
+ // writer.close();
180
+
181
+ // if (this.writers.size === 0) {
182
+ // const closeTask = this.writable.close();
183
+ // this.closeTasks.push(closeTask);
184
+ // closeTask.then(() => {
185
+ // this.closeTasks.splice(this.closeTasks.indexOf(closeTask), 1);
186
+ // });
187
+ // }
188
+ // }
189
+ // },
190
+ // close: async () => {
191
+ // if (this.writers.size > 0) throw new Error("Not all writers closed");
192
+
193
+ // // await and throw on any errors
194
+ // await Promise.all(this.closeTasks);
195
+ // },
196
+ // });
197
+ // }
198
+
199
+ // add(stream: WritableStream<AssistantStreamChunk>) {
200
+ // if (this.isSealed) throw new Error("Cannot add streams after sealing");
201
+
202
+ // const decoder = new SpanParentDecoder(this.subAllocator);
203
+ // decoder.readable.pipeTo(stream);
204
+
205
+ // this.writers.set(this.allocator.up(), decoder.writable.getWriter());
206
+ // }
207
+
208
+ // seal() {
209
+ // this.isSealed = true;
210
+ // if (this.writers.size === 0) this.writable.close();
211
+ // }
212
+ // }