ai-stream-utils 2.0.1 → 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 +59 -15
- package/dist/index.d.mts +85 -2
- package/dist/index.mjs +30 -1
- package/package.json +1 -1
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
|
|
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,
|
|
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
|
-
|
|
55
|
+
#### Type Guards
|
|
56
56
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
187
|
-
// part
|
|
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
|
/**
|
|
@@ -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 };
|