ai-stream-utils 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,9 +15,9 @@ This library provides composable filter and transformation utilities for UI mess
15
15
 
16
16
  The AI SDK UI message stream created by [`toUIMessageStream()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#to-ui-message-stream) streams all parts (text, tools, reasoning, etc.) to the client by default. However, you may want to:
17
17
 
18
- - **Filter**: Tool calls like database queries often contain large amounts of data or sensitive information that should not be streamed to the client
18
+ - **Filter**: Tool calls like database searches often contain large amounts of data or sensitive information that should not be streamed to the client
19
19
  - **Transform**: Modify text or tool outputs while they are streamed to the client
20
- - **Observe**: Log stream lifecycle events, tool calls, or other chunks without consuming or modifying the stream
20
+ - **Observe**: Log stream lifecycle events, update states, or run side-effects without modifying the stream
21
21
 
22
22
  This library provides type-safe, composable utilities for all these use cases.
23
23
 
@@ -52,21 +52,56 @@ const stream = pipe(result.toUIMessageStream())
52
52
  .toStream();
53
53
  ```
54
54
 
55
- **Type guards** provide a simpler API for common filtering patterns:
55
+ #### Type Guards
56
56
 
57
- - `includeChunks("text-delta")` or `includeChunks(["text-delta", "text-end"])`: Include specific chunk types
58
- - `excludeChunks("text-delta")` or `excludeChunks(["text-delta", "text-end"])`: Exclude specific chunk types
59
- - `includeParts("text")` or `includeParts(["text", "reasoning"])`: Include specific part types
60
- - `excludeParts("reasoning")` or `excludeParts(["reasoning", "tool-database"])`: Exclude specific part types
57
+ Generic type guards provide a simpler API for common filtering patterns:
61
58
 
62
- **Example:** Exclude tool calls from the client.
59
+ - `includeChunks("text-delta")` or `includeChunks(["text-delta", "text-end"])`: Include **only** specific chunk types
60
+ - `excludeChunks("text-delta")` or `excludeChunks(["text-delta", "text-end"])`: Exclude **only** specific chunk types
61
+ - `includeParts("text")` or `includeParts(["text", "reasoning"])`: Include **only** specific part types
62
+ - `excludeParts("reasoning")` or `excludeParts(["reasoning", "tool-database"])`: Exclude **only** specific part types
63
+
64
+ Filtering tools is the most common use case and the tool-filter type guards provide a convenient API for filtering tool chunks by tool name:
65
+
66
+ - `excludeTools()` or `excludeTools("weather")` or `excludeTools(["weather", "database"])`: Exclude all tools or specific tools by name
67
+ - `includeTools()` or `includeTools("weather")` or `includeTools(["weather", "database"])`: Include all tools or specific tools by name
68
+
69
+ > [!NOTE]
70
+ > The tool-filter type guards only affect tool chunks. Non-tool chunks will pass through.
71
+
72
+ #### Examples
73
+
74
+ Exclude tool calls from the client.
63
75
 
64
76
  ```typescript
77
+ // Exclude by part type (requires "tool-" prefix)
65
78
  const stream = pipe(result.toUIMessageStream())
66
79
  .filter(excludeParts(["tool-weather", "tool-database"]))
67
80
  .toStream();
81
+
82
+ // Exclude by tool name (without "tool-" prefix)
83
+ const stream = pipe(result.toUIMessageStream())
84
+ .filter(excludeTools(["weather", "database"]))
85
+ .toStream();
86
+
87
+ // Exclude all tools
88
+ const stream = pipe(result.toUIMessageStream()).filter(excludeTools()).toStream();
89
+
90
+ // Include only specific tools (without "tool-" prefix)
91
+ const stream = pipe(result.toUIMessageStream())
92
+ .filter(includeTools(["weather"]))
93
+ .toStream();
68
94
  ```
69
95
 
96
+ > [!NOTE]
97
+ > `excludeTools()` and `includeTools()` filters tool chunks on the server before streaming to the client. This affects all tool types including:
98
+ >
99
+ > - Server-side tools with `execute` functions
100
+ > - Client-side tools without `execute` functions
101
+ > - Tools that require human approval via `needsApproval`
102
+ >
103
+ > Excluded tools will not appear in the client's message parts, so users won't see tool call UI or be able to approve/reject filtered tools.
104
+
70
105
  ### `.map()`
71
106
 
72
107
  Transform chunks by returning a chunk, an array of chunks, or `null` to exclude.
@@ -94,7 +129,9 @@ const stream = pipe(result.toUIMessageStream())
94
129
  .toStream();
95
130
  ```
96
131
 
97
- **Example:** Convert text to uppercase.
132
+ #### Examples
133
+
134
+ Convert text to uppercase.
98
135
 
99
136
  ```typescript
100
137
  const stream = pipe(result.toUIMessageStream())
@@ -127,6 +164,8 @@ const stream = pipe(result.toUIMessageStream())
127
164
  .toStream();
128
165
  ```
129
166
 
167
+ #### Type Guards
168
+
130
169
  **Type guard** provides a type-safe way to observe specific chunk types:
131
170
 
132
171
  - `chunkType("text-delta")` or `chunkType(["start", "finish"])`: Observe specific chunk types
@@ -135,18 +174,23 @@ const stream = pipe(result.toUIMessageStream())
135
174
  > [!NOTE]
136
175
  > The `partType` type guard still operates on chunks. That means `partType("text")` will match any text chunks such as `text-start`, `text-delta`, and `text-end`.
137
176
 
138
- **Example:** Log stream lifecycle events.
177
+ #### Examples
178
+
179
+ Log stream lifecycle events.
139
180
 
140
181
  ```typescript
141
182
  const stream = pipe(result.toUIMessageStream())
142
- .on(chunkType("start"), () => {
143
- console.log("Stream started");
183
+ .on(chunkType("start"), ({ chunk }) => {
184
+ console.log("Stream started:", chunk.messageId);
144
185
  })
145
186
  .on(chunkType("finish"), ({ chunk }) => {
146
187
  console.log("Stream finished:", chunk.finishReason);
147
188
  })
148
189
  .on(chunkType("tool-input-available"), ({ chunk }) => {
149
- console.log("Tool called:", chunk.toolName, chunk.input);
190
+ console.log("Tool input:", chunk.toolName, chunk.input);
191
+ })
192
+ .on(chunkType("tool-output-available"), ({ chunk }) => {
193
+ console.log("Tool output:", chunk.toolName, chunk.output);
150
194
  })
151
195
  .toStream();
152
196
  ```
@@ -183,8 +227,8 @@ Multiple operators can be chained together. After filtering with type guards, ch
183
227
  const stream = pipe<MyUIMessage>(result.toUIMessageStream())
184
228
  .filter(includeParts("text"))
185
229
  .map(({ chunk, part }) => {
186
- // chunk is narrowed to text chunks only
187
- // part.type is narrowed to "text"
230
+ // chunk is narrowed to text chunks: "text-start" | "text-delta" | "text-end"
231
+ // part is narrowed to "text"
188
232
  return chunk;
189
233
  })
190
234
  .toStream();
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as convertSSEToUIMessageStream, c as convertArrayToStream, i as convertStreamToArray, l as convertArrayToAsyncIterable, n as createAsyncIterableStream, o as convertAsyncIterableToStream, r as convertUIMessageToSSEStream, s as convertAsyncIterableToArray, t as AsyncIterableStream } from "./types-B4nePmEd.mjs";
2
2
  import "./utils/index.mjs";
3
- import { AsyncIterableStream as AsyncIterableStream$1, InferUIMessageChunk, UIMessage } from "ai";
3
+ import { AsyncIterableStream as AsyncIterableStream$1, InferUIMessageChunk, UIDataTypes, UIMessage, UITools } from "ai";
4
4
 
5
5
  //#region src/consume/consume-ui-message-stream.d.ts
6
6
  /**
@@ -181,6 +181,7 @@ declare function filterUIMessageStream<UI_MESSAGE extends UIMessage>(stream: Rea
181
181
  type InferUIMessagePart<UI_MESSAGE extends UIMessage> = UI_MESSAGE["parts"][number];
182
182
  type InferUIMessagePartType<UI_MESSAGE extends UIMessage> = InferUIMessagePart<UI_MESSAGE>["type"];
183
183
  type InferUIMessageChunkType<UI_MESSAGE extends UIMessage> = InferUIMessageChunk<UI_MESSAGE>["type"];
184
+ type InferUIMessageTools<UI_MESSAGE extends UIMessage> = UI_MESSAGE extends UIMessage<unknown, UIDataTypes, infer TOOLS> ? TOOLS : UITools;
184
185
  /**
185
186
  * Extracts chunk type strings that match the prefix exactly or as `${PREFIX}-*`.
186
187
  * Dynamically derives chunk types from the actual UIMessageChunk union.
@@ -318,6 +319,32 @@ type InferPartForChunk<UI_MESSAGE extends UIMessage, CHUNK_TYPE extends string>
318
319
  * ```
319
320
  */
320
321
  type ExcludePartForChunks<UI_MESSAGE extends UIMessage, EXCLUDED_CHUNK_TYPE extends string> = { [PT in InferUIMessagePartType<UI_MESSAGE>]: [Exclude<PartTypeToChunkTypes<UI_MESSAGE, PT>, EXCLUDED_CHUNK_TYPE>] extends [never] ? never : PT }[InferUIMessagePartType<UI_MESSAGE>];
322
+ /**
323
+ * Extract tool names from UIMessage tools.
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * type Tools = InferToolName<MyUIMessage>;
328
+ * // => 'weather' | 'calculator'
329
+ * ```
330
+ */
331
+ type InferToolName<UI_MESSAGE extends UIMessage> = keyof InferUIMessageTools<UI_MESSAGE> & string;
332
+ /**
333
+ * All tool-related part types (tool-* and dynamic-tool).
334
+ */
335
+ type ToolPartTypes<UI_MESSAGE extends UIMessage> = Extract<InferUIMessagePartType<UI_MESSAGE>, `tool-${string}` | `dynamic-tool`>;
336
+ /**
337
+ * Part types remaining after excluding all tools.
338
+ */
339
+ type ExcludeToolPartTypes<UI_MESSAGE extends UIMessage> = Exclude<InferUIMessagePartType<UI_MESSAGE>, ToolPartTypes<UI_MESSAGE>>;
340
+ /**
341
+ * Chunk types for tools (tool-input-*, tool-output-*, etc.).
342
+ */
343
+ type ToolChunkTypes<UI_MESSAGE extends UIMessage> = Extract<InferUIMessageChunkType<UI_MESSAGE>, `tool-${string}`>;
344
+ /**
345
+ * Content chunk types remaining after excluding all tool chunks.
346
+ */
347
+ type ExcludeToolChunkTypes<UI_MESSAGE extends UIMessage> = Exclude<ContentChunkType<UI_MESSAGE>, ToolChunkTypes<UI_MESSAGE>>;
321
348
  //#endregion
322
349
  //#region src/flat-map/flat-map-ui-message-stream.d.ts
323
350
  /**
@@ -529,9 +556,9 @@ declare class ChunkPipeline<UI_MESSAGE extends UIMessage, CHUNK extends InferUIM
529
556
  * Use with includeChunks(), includeParts(), excludeChunks(), or excludeParts().
530
557
  * The callback only receives content chunks because meta chunks pass through unchanged.
531
558
  */
532
- filter<NARROWED_CHUNK extends InferUIMessageChunk<UI_MESSAGE>, NARROWED_PART extends {
533
- type: string;
534
- }>(guard: FilterGuard<UI_MESSAGE, NARROWED_CHUNK, NARROWED_PART>): ChunkPipeline<UI_MESSAGE, NARROWED_CHUNK, NARROWED_PART>;
559
+ filter<NARROWED_CHUNK extends CHUNK, NARROWED_PART extends {
560
+ type: PART[`type`];
561
+ }>(guard: FilterGuard<UI_MESSAGE, NARROWED_CHUNK, NARROWED_PART>): ChunkPipeline<UI_MESSAGE, CHUNK & NARROWED_CHUNK, PART & NARROWED_PART>;
535
562
  /**
536
563
  * Filters chunks using a generic predicate function.
537
564
  * The callback only receives content chunks because meta chunks pass through unchanged.
@@ -549,8 +576,8 @@ declare class ChunkPipeline<UI_MESSAGE extends UIMessage, CHUNK extends InferUIM
549
576
  * Content chunks include a part object with the type, while meta chunks have undefined part.
550
577
  * All chunks pass through regardless of whether the callback is invoked.
551
578
  */
552
- on<NARROWED_CHUNK extends InferUIMessageChunk<UI_MESSAGE>, NARROWED_PART extends {
553
- type: string;
579
+ on<NARROWED_CHUNK extends CHUNK, NARROWED_PART extends {
580
+ type: PART[`type`];
554
581
  } | undefined>(guard: ObserveGuard<UI_MESSAGE, NARROWED_CHUNK, NARROWED_PART>, callback: ChunkObserveFn<NARROWED_CHUNK, NARROWED_PART>): ChunkPipeline<UI_MESSAGE, CHUNK, PART>;
555
582
  /**
556
583
  * Observes chunks matching a predicate without filtering them.
@@ -638,7 +665,7 @@ declare function includeParts<UI_MESSAGE extends UIMessage, PART_TYPE extends In
638
665
  * .map(({ chunk }) => chunk); // excludes text-start and text-end
639
666
  * ```
640
667
  */
641
- declare function excludeChunks<UI_MESSAGE extends UIMessage, CHUNK_TYPE extends ContentChunkType<UI_MESSAGE>>(types: CHUNK_TYPE | Array<CHUNK_TYPE>): FilterGuard<UI_MESSAGE, Exclude<ExtractChunk<UI_MESSAGE, ContentChunkType<UI_MESSAGE>>, ExtractChunk<UI_MESSAGE, CHUNK_TYPE>>, {
668
+ declare function excludeChunks<UI_MESSAGE extends UIMessage, CHUNK_TYPE extends ContentChunkType<UI_MESSAGE>>(types: CHUNK_TYPE | Array<CHUNK_TYPE>): FilterGuard<UI_MESSAGE, ExtractChunk<UI_MESSAGE, Exclude<ContentChunkType<UI_MESSAGE>, CHUNK_TYPE>>, {
642
669
  type: ExcludePartForChunks<UI_MESSAGE, CHUNK_TYPE>;
643
670
  }>;
644
671
  /**
@@ -656,7 +683,7 @@ declare function excludeChunks<UI_MESSAGE extends UIMessage, CHUNK_TYPE extends
656
683
  * .map(({ chunk }) => chunk); // excludes text and reasoning chunks
657
684
  * ```
658
685
  */
659
- declare function excludeParts<UI_MESSAGE extends UIMessage, PART_TYPE extends InferUIMessagePartType<UI_MESSAGE>>(types: PART_TYPE | Array<PART_TYPE>): FilterGuard<UI_MESSAGE, Exclude<ExtractChunk<UI_MESSAGE, ContentChunkType<UI_MESSAGE>>, ExtractChunkForPart<UI_MESSAGE, ExtractPart<UI_MESSAGE, PART_TYPE>>>, {
686
+ declare function excludeParts<UI_MESSAGE extends UIMessage, PART_TYPE extends InferUIMessagePartType<UI_MESSAGE>>(types: PART_TYPE | Array<PART_TYPE>): FilterGuard<UI_MESSAGE, ExtractChunk<UI_MESSAGE, Exclude<ContentChunkType<UI_MESSAGE>, PartTypeToChunkTypes<UI_MESSAGE, PART_TYPE>>>, {
660
687
  type: Exclude<InferUIMessagePartType<UI_MESSAGE>, PART_TYPE>;
661
688
  }>;
662
689
  /**
@@ -707,5 +734,61 @@ declare function chunkType<UI_MESSAGE extends UIMessage, CHUNK_TYPE extends Infe
707
734
  declare function partType<UI_MESSAGE extends UIMessage, PART_TYPE extends InferUIMessagePartType<UI_MESSAGE>>(types: PART_TYPE | Array<PART_TYPE>): ObserveGuard<UI_MESSAGE, ExtractChunkForPart<UI_MESSAGE, ExtractPart<UI_MESSAGE, PART_TYPE>>, {
708
735
  type: PART_TYPE;
709
736
  }>;
737
+ /**
738
+ * Creates a filter guard that includes only specific tools while keeping non-tool chunks.
739
+ * Use with `.filter()` to include only specific tools.
740
+ *
741
+ * @example
742
+ * ```typescript
743
+ * // No-op: all chunks pass through
744
+ * pipe<MyUIMessage>(stream)
745
+ * .filter(includeTools())
746
+ * .map(({ chunk }) => chunk); // all chunks pass
747
+ *
748
+ * // Include specific tool (non-tool chunks still pass)
749
+ * pipe<MyUIMessage>(stream)
750
+ * .filter(includeTools('weather'))
751
+ * .map(({ chunk }) => chunk); // text + tool-weather chunks
752
+ *
753
+ * // Include multiple tools
754
+ * pipe<MyUIMessage>(stream)
755
+ * .filter(includeTools(['weather', 'calculator']))
756
+ * .map(({ chunk }) => chunk); // text + specified tool chunks
757
+ * ```
758
+ */
759
+ declare function includeTools<UI_MESSAGE extends UIMessage>(): FilterGuard<UI_MESSAGE, InferUIMessageChunk<UI_MESSAGE>, {
760
+ type: InferUIMessagePartType<UI_MESSAGE>;
761
+ }>;
762
+ declare function includeTools<UI_MESSAGE extends UIMessage, TOOL_NAME extends InferToolName<UI_MESSAGE>>(toolNames: TOOL_NAME | Array<TOOL_NAME>): FilterGuard<UI_MESSAGE, InferUIMessageChunk<UI_MESSAGE>, {
763
+ type: ExcludeToolPartTypes<UI_MESSAGE> | `tool-${TOOL_NAME}`;
764
+ }>;
765
+ /**
766
+ * Creates a filter guard that excludes all or only specific tools while keeping non-tool chunks.
767
+ * Use with `.filter()` to exclude specific tools or all tools.
768
+ *
769
+ * @example
770
+ * ```typescript
771
+ * // Exclude all tools
772
+ * pipe<MyUIMessage>(stream)
773
+ * .filter(excludeTools())
774
+ * .map(({ chunk }) => chunk); // no tool chunks
775
+ *
776
+ * // Exclude specific tool
777
+ * pipe<MyUIMessage>(stream)
778
+ * .filter(excludeTools('weather'))
779
+ * .map(({ chunk }) => chunk); // excludes tool-weather chunks
780
+ *
781
+ * // Exclude multiple tools
782
+ * pipe<MyUIMessage>(stream)
783
+ * .filter(excludeTools(['weather', 'calculator']))
784
+ * .map(({ chunk }) => chunk); // excludes weather and calculator
785
+ * ```
786
+ */
787
+ declare function excludeTools<UI_MESSAGE extends UIMessage>(): FilterGuard<UI_MESSAGE, ExtractChunk<UI_MESSAGE, ExcludeToolChunkTypes<UI_MESSAGE>>, {
788
+ type: ExcludeToolPartTypes<UI_MESSAGE>;
789
+ }>;
790
+ declare function excludeTools<UI_MESSAGE extends UIMessage, TOOL_NAME extends InferToolName<UI_MESSAGE>>(toolNames: TOOL_NAME | Array<TOOL_NAME>): FilterGuard<UI_MESSAGE, InferUIMessageChunk<UI_MESSAGE>, {
791
+ type: Exclude<InferUIMessagePartType<UI_MESSAGE>, `tool-${TOOL_NAME}`>;
792
+ }>;
710
793
  //#endregion
711
- export { AsyncIterableStream, type FlatMapContext, type FlatMapInput, type FlatMapUIMessageStreamFn, type FlatMapUIMessageStreamPredicate, type MapInput, type MapUIMessageStreamFn, chunkType, consumeUIMessageStream, convertArrayToAsyncIterable, convertArrayToStream, convertAsyncIterableToArray, convertAsyncIterableToStream, convertSSEToUIMessageStream, convertStreamToArray, convertUIMessageToSSEStream, createAsyncIterableStream, excludeChunks, excludeParts, filterUIMessageStream, flatMapUIMessageStream, includeChunks, includeParts, mapUIMessageStream, partType, partTypeIs, pipe };
794
+ export { AsyncIterableStream, type FlatMapContext, type FlatMapInput, type FlatMapUIMessageStreamFn, type FlatMapUIMessageStreamPredicate, type MapInput, type MapUIMessageStreamFn, chunkType, consumeUIMessageStream, convertArrayToAsyncIterable, convertArrayToStream, convertAsyncIterableToArray, convertAsyncIterableToStream, convertSSEToUIMessageStream, convertStreamToArray, convertUIMessageToSSEStream, createAsyncIterableStream, excludeChunks, excludeParts, excludeTools, filterUIMessageStream, flatMapUIMessageStream, includeChunks, includeParts, includeTools, mapUIMessageStream, partType, partTypeIs, pipe };
package/dist/index.mjs CHANGED
@@ -908,6 +908,35 @@ function partType(types) {
908
908
  const guard = (input) => typeArray.includes(input.part?.type);
909
909
  return guard;
910
910
  }
911
+ function includeTools(toolNames) {
912
+ const toolNameArray = toolNames === void 0 ? void 0 : Array.isArray(toolNames) ? toolNames : [toolNames];
913
+ const guard = (input) => {
914
+ /** No args = no-op, all chunks pass */
915
+ if (toolNameArray === void 0) return true;
916
+ const partType = input.part?.type;
917
+ if (!partType) return true;
918
+ /** Non-tool chunks pass */
919
+ if (!partType.startsWith(`tool-`) && partType !== `dynamic-tool`) return true;
920
+ /** Only matching tool chunks pass */
921
+ for (const name of toolNameArray) if (partType === `tool-${name}`) return true;
922
+ return false;
923
+ };
924
+ return guard;
925
+ }
926
+ function excludeTools(toolNames) {
927
+ const toolNameArray = toolNames === void 0 ? void 0 : Array.isArray(toolNames) ? toolNames : [toolNames];
928
+ const guard = (input) => {
929
+ const partType = input.part?.type;
930
+ /** Meta chunks pass (no part type) */
931
+ if (!partType) return true;
932
+ /** No args = exclude all tool chunks */
933
+ if (toolNameArray === void 0) return !partType.startsWith(`tool-`) && partType !== `dynamic-tool`;
934
+ /** Exclude only matching tool chunks */
935
+ for (const name of toolNameArray) if (partType === `tool-${name}`) return false;
936
+ return true;
937
+ };
938
+ return guard;
939
+ }
911
940
 
912
941
  //#endregion
913
- export { chunkType, consumeUIMessageStream, convertArrayToAsyncIterable, convertArrayToStream, convertAsyncIterableToArray, convertAsyncIterableToStream, convertSSEToUIMessageStream, convertStreamToArray, convertUIMessageToSSEStream, createAsyncIterableStream, excludeChunks, excludeParts, filterUIMessageStream, flatMapUIMessageStream, includeChunks, includeParts, mapUIMessageStream, partType, partTypeIs, pipe };
942
+ export { chunkType, consumeUIMessageStream, convertArrayToAsyncIterable, convertArrayToStream, convertAsyncIterableToArray, convertAsyncIterableToStream, convertSSEToUIMessageStream, convertStreamToArray, convertUIMessageToSSEStream, createAsyncIterableStream, excludeChunks, excludeParts, excludeTools, filterUIMessageStream, flatMapUIMessageStream, includeChunks, includeParts, includeTools, mapUIMessageStream, partType, partTypeIs, pipe };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-stream-utils",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "AI SDK: Filter and transform UI messages while streaming to the client",
5
5
  "keywords": [
6
6
  "ai",