@ai-sdk/workflow 1.0.5 → 1.0.7
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/CHANGELOG.md +16 -0
- package/dist/index.d.mts +59 -2
- package/dist/index.mjs +95 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/do-stream-step.ts +2 -0
- package/src/index.ts +2 -0
- package/src/normalize-ui-message-stream.ts +164 -0
- package/src/stream-text-iterator.ts +7 -0
- package/src/workflow-agent.ts +11 -0
- package/src/workflow-chat-transport.ts +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/workflow",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "WorkflowAgent for building AI agents with AI SDK",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"ajv": "^8.20.0",
|
|
30
30
|
"@ai-sdk/provider": "4.0.0",
|
|
31
31
|
"@ai-sdk/provider-utils": "5.0.1",
|
|
32
|
-
"ai": "7.0.
|
|
32
|
+
"ai": "7.0.7"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "22.19.19",
|
package/src/do-stream-step.ts
CHANGED
|
@@ -51,6 +51,7 @@ export interface DoStreamStepOptions {
|
|
|
51
51
|
maxRetries?: number;
|
|
52
52
|
abortSignal?: AbortSignal;
|
|
53
53
|
headers?: Record<string, string | undefined>;
|
|
54
|
+
reasoning?: LanguageModelV4CallOptions['reasoning'];
|
|
54
55
|
providerOptions?: ProviderOptions;
|
|
55
56
|
toolChoice?: ToolChoice<ToolSet>;
|
|
56
57
|
includeRawChunks?: boolean;
|
|
@@ -134,6 +135,7 @@ export async function doStreamStep(
|
|
|
134
135
|
providerOptions: options?.providerOptions,
|
|
135
136
|
abortSignal: options?.abortSignal,
|
|
136
137
|
headers: options?.headers,
|
|
138
|
+
reasoning: options?.reasoning,
|
|
137
139
|
maxOutputTokens: options?.maxOutputTokens,
|
|
138
140
|
temperature: options?.temperature,
|
|
139
141
|
topP: options?.topP,
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { UIMessageChunk } from 'ai';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tracks, for one part family (text or reasoning), which part ids are open or
|
|
5
|
+
* ended within the current step.
|
|
6
|
+
*/
|
|
7
|
+
interface PartFrameState {
|
|
8
|
+
/** A `*-start` was seen and not yet ended in the current step. */
|
|
9
|
+
open: Set<string>;
|
|
10
|
+
/** A part that was opened and ended in the current step. */
|
|
11
|
+
ended: Set<string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const newPartFrameState = (): PartFrameState => ({
|
|
15
|
+
open: new Set(),
|
|
16
|
+
ended: new Set(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Repairs the framing for a single `*-start` / `*-delta` / `*-end` chunk
|
|
21
|
+
* against the running per-step state, yielding the chunks the consumer should
|
|
22
|
+
* see. Text and reasoning parts share this logic (`startType` differentiates
|
|
23
|
+
* the synthesized start chunk).
|
|
24
|
+
*
|
|
25
|
+
* @yields the chunks (possibly synthesized, possibly none) the consumer should see.
|
|
26
|
+
*/
|
|
27
|
+
function* repairPart(
|
|
28
|
+
kind: 'start' | 'delta' | 'end',
|
|
29
|
+
id: string,
|
|
30
|
+
chunk: UIMessageChunk,
|
|
31
|
+
state: PartFrameState,
|
|
32
|
+
startType: 'text-start' | 'reasoning-start',
|
|
33
|
+
): Generator<UIMessageChunk> {
|
|
34
|
+
if (kind === 'start') {
|
|
35
|
+
// Drop a duplicate/replayed start for a part already framed this step.
|
|
36
|
+
if (state.open.has(id) || state.ended.has(id)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
state.open.add(id);
|
|
40
|
+
yield chunk;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// delta / end: drop a re-delivered chunk for an already-ended part.
|
|
45
|
+
if (state.ended.has(id)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Synthesize the missing start for an orphaned delta/end.
|
|
49
|
+
if (!state.open.has(id)) {
|
|
50
|
+
state.open.add(id);
|
|
51
|
+
yield { type: startType, id } as UIMessageChunk;
|
|
52
|
+
}
|
|
53
|
+
if (kind === 'end') {
|
|
54
|
+
state.open.delete(id);
|
|
55
|
+
state.ended.add(id);
|
|
56
|
+
}
|
|
57
|
+
yield chunk;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Normalizes the part framing of a UI message stream so it is always
|
|
62
|
+
* well-formed for the AI SDK's stream consumer (`processUIMessageStream`,
|
|
63
|
+
* which backs `useChat`/`readUIMessageStream`).
|
|
64
|
+
*
|
|
65
|
+
* ## Why this exists
|
|
66
|
+
*
|
|
67
|
+
* The consumer maintains a map of "active" text/reasoning parts keyed by id.
|
|
68
|
+
* A `text-delta`/`text-end` for an id that has no open part is a fatal error
|
|
69
|
+
* (`Received text-delta for missing text part with ID "0" ...`) that kills the
|
|
70
|
+
* whole turn. Two properties of the durable streaming model make that error
|
|
71
|
+
* reachable:
|
|
72
|
+
*
|
|
73
|
+
* - A workflow run owns a single shared stream, and the consumer resets its
|
|
74
|
+
* active-part maps on every `finish-step`. Multi-step turns reuse the same
|
|
75
|
+
* part id (commonly `"0"`) in each step, so a single dropped or duplicated
|
|
76
|
+
* `*-start` across a step boundary orphans the rest of that step's content.
|
|
77
|
+
* - The same stream is read across reconnects, and a stream-producing step can
|
|
78
|
+
* run more than once (retry/redelivery, or the concurrent-worker duplication
|
|
79
|
+
* tracked in vercel/workflow#2331 and #2039). Either can interleave or
|
|
80
|
+
* duplicate chunks on the shared stream — e.g. a `finish-step` landing in the
|
|
81
|
+
* middle of another execution's text part.
|
|
82
|
+
*
|
|
83
|
+
* Since the content is still flowing and only the framing is damaged, repairing
|
|
84
|
+
* the framing here degrades the worst case to "text begins slightly into the
|
|
85
|
+
* step" or "a duplicated tail is dropped" instead of a dead turn.
|
|
86
|
+
*
|
|
87
|
+
* ## What it does
|
|
88
|
+
*
|
|
89
|
+
* Mirrors the consumer's part-lifetime state machine, per part type, per step:
|
|
90
|
+
* - resets tracking on `finish-step` (exactly where the consumer resets);
|
|
91
|
+
* - synthesizes a missing `*-start` when an orphaned `*-delta`/`*-end` arrives;
|
|
92
|
+
* - drops a re-delivered `*-start`/`*-delta`/`*-end` for a part already
|
|
93
|
+
* open or ended in the current step (reconnect/replay overlap).
|
|
94
|
+
*
|
|
95
|
+
* A well-formed stream passes through unchanged.
|
|
96
|
+
*
|
|
97
|
+
* ## Scope: text and reasoning only
|
|
98
|
+
*
|
|
99
|
+
* `tool-input-delta` raises the same class of fatal error (`Received
|
|
100
|
+
* tool-input-delta for missing tool call ...`), but tool parts are deliberately
|
|
101
|
+
* left untouched: the consumer does not reset its tool-call map on `finish-step`
|
|
102
|
+
* and tool-call ids are unique, so the step-boundary id-reuse orphaning that
|
|
103
|
+
* makes text/reasoning fragile does not apply to them. If a future duplication
|
|
104
|
+
* mode is found to orphan tool-input parts, extend the same machine to that
|
|
105
|
+
* family rather than special-casing it.
|
|
106
|
+
*
|
|
107
|
+
* @param source the raw UI message chunk stream to normalize.
|
|
108
|
+
* @yields the framing-corrected UI message chunks.
|
|
109
|
+
*/
|
|
110
|
+
export async function* normalizeUIMessageStreamParts(
|
|
111
|
+
source: AsyncIterable<UIMessageChunk>,
|
|
112
|
+
): AsyncGenerator<UIMessageChunk> {
|
|
113
|
+
const text = newPartFrameState();
|
|
114
|
+
const reasoning = newPartFrameState();
|
|
115
|
+
|
|
116
|
+
for await (const chunk of source) {
|
|
117
|
+
switch (chunk.type) {
|
|
118
|
+
case 'finish-step':
|
|
119
|
+
// The consumer clears its active-part maps here, so part ids may be
|
|
120
|
+
// legitimately reused in the next step. Reset to match.
|
|
121
|
+
text.open.clear();
|
|
122
|
+
text.ended.clear();
|
|
123
|
+
reasoning.open.clear();
|
|
124
|
+
reasoning.ended.clear();
|
|
125
|
+
yield chunk;
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case 'text-start':
|
|
129
|
+
yield* repairPart('start', chunk.id, chunk, text, 'text-start');
|
|
130
|
+
break;
|
|
131
|
+
case 'text-delta':
|
|
132
|
+
yield* repairPart('delta', chunk.id, chunk, text, 'text-start');
|
|
133
|
+
break;
|
|
134
|
+
case 'text-end':
|
|
135
|
+
yield* repairPart('end', chunk.id, chunk, text, 'text-start');
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case 'reasoning-start':
|
|
139
|
+
yield* repairPart(
|
|
140
|
+
'start',
|
|
141
|
+
chunk.id,
|
|
142
|
+
chunk,
|
|
143
|
+
reasoning,
|
|
144
|
+
'reasoning-start',
|
|
145
|
+
);
|
|
146
|
+
break;
|
|
147
|
+
case 'reasoning-delta':
|
|
148
|
+
yield* repairPart(
|
|
149
|
+
'delta',
|
|
150
|
+
chunk.id,
|
|
151
|
+
chunk,
|
|
152
|
+
reasoning,
|
|
153
|
+
'reasoning-start',
|
|
154
|
+
);
|
|
155
|
+
break;
|
|
156
|
+
case 'reasoning-end':
|
|
157
|
+
yield* repairPart('end', chunk.id, chunk, reasoning, 'reasoning-start');
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
default:
|
|
161
|
+
yield chunk;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -259,6 +259,12 @@ export async function* streamTextIterator({
|
|
|
259
259
|
headers: prepareResult.headers,
|
|
260
260
|
};
|
|
261
261
|
}
|
|
262
|
+
if (prepareResult?.reasoning !== undefined) {
|
|
263
|
+
currentGenerationSettings = {
|
|
264
|
+
...currentGenerationSettings,
|
|
265
|
+
reasoning: prepareResult.reasoning,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
262
268
|
if (prepareResult?.providerOptions !== undefined) {
|
|
263
269
|
currentGenerationSettings = {
|
|
264
270
|
...currentGenerationSettings,
|
|
@@ -333,6 +339,7 @@ export async function* streamTextIterator({
|
|
|
333
339
|
frequencyPenalty: currentGenerationSettings.frequencyPenalty,
|
|
334
340
|
stopSequences: currentGenerationSettings.stopSequences,
|
|
335
341
|
seed: currentGenerationSettings.seed,
|
|
342
|
+
reasoning: currentGenerationSettings.reasoning,
|
|
336
343
|
providerOptions: currentGenerationSettings.providerOptions,
|
|
337
344
|
headers: currentGenerationSettings.headers,
|
|
338
345
|
} as never);
|
package/src/workflow-agent.ts
CHANGED
|
@@ -229,6 +229,12 @@ export interface GenerationSettings {
|
|
|
229
229
|
*/
|
|
230
230
|
headers?: Record<string, string | undefined>;
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Reasoning effort level for the model. Controls how much reasoning
|
|
234
|
+
* the model performs before generating a response.
|
|
235
|
+
*/
|
|
236
|
+
reasoning?: LanguageModelV4CallOptions['reasoning'];
|
|
237
|
+
|
|
232
238
|
/**
|
|
233
239
|
* Additional provider-specific options. They are passed through
|
|
234
240
|
* to the provider from the AI SDK and enable provider-specific
|
|
@@ -1266,6 +1272,7 @@ export class WorkflowAgent<
|
|
|
1266
1272
|
maxRetries: options.maxRetries,
|
|
1267
1273
|
abortSignal: options.abortSignal,
|
|
1268
1274
|
headers: options.headers,
|
|
1275
|
+
reasoning: options.reasoning,
|
|
1269
1276
|
providerOptions: options.providerOptions,
|
|
1270
1277
|
};
|
|
1271
1278
|
}
|
|
@@ -1365,6 +1372,8 @@ export class WorkflowAgent<
|
|
|
1365
1372
|
effectiveGenerationSettings.seed = prepared.seed;
|
|
1366
1373
|
if (prepared.headers !== undefined)
|
|
1367
1374
|
effectiveGenerationSettings.headers = prepared.headers;
|
|
1375
|
+
if (prepared.reasoning !== undefined)
|
|
1376
|
+
effectiveGenerationSettings.reasoning = prepared.reasoning;
|
|
1368
1377
|
if (prepared.providerOptions !== undefined)
|
|
1369
1378
|
effectiveGenerationSettings.providerOptions = prepared.providerOptions;
|
|
1370
1379
|
}
|
|
@@ -1744,6 +1753,7 @@ export class WorkflowAgent<
|
|
|
1744
1753
|
abortSignal: effectiveAbortSignal,
|
|
1745
1754
|
}),
|
|
1746
1755
|
...(options.headers !== undefined && { headers: options.headers }),
|
|
1756
|
+
...(options.reasoning !== undefined && { reasoning: options.reasoning }),
|
|
1747
1757
|
...(options.providerOptions !== undefined && {
|
|
1748
1758
|
providerOptions: options.providerOptions,
|
|
1749
1759
|
}),
|
|
@@ -1846,6 +1856,7 @@ export class WorkflowAgent<
|
|
|
1846
1856
|
maxRetries: mergedGenerationSettings.maxRetries ?? 2,
|
|
1847
1857
|
timeout: undefined,
|
|
1848
1858
|
headers: mergedGenerationSettings.headers,
|
|
1859
|
+
reasoning: mergedGenerationSettings.reasoning,
|
|
1849
1860
|
providerOptions: mergedGenerationSettings.providerOptions,
|
|
1850
1861
|
output: (options.output ?? this.output) as never,
|
|
1851
1862
|
runtimeContext,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getErrorMessage,
|
|
14
14
|
} from '@ai-sdk/provider-utils';
|
|
15
15
|
import { createAsyncIterableStream } from 'ai/internal';
|
|
16
|
+
import { normalizeUIMessageStreamParts } from './normalize-ui-message-stream.js';
|
|
16
17
|
|
|
17
18
|
export interface SendMessagesOptions<UI_MESSAGE extends UIMessage> {
|
|
18
19
|
trigger: 'submit-message' | 'regenerate-message';
|
|
@@ -186,7 +187,7 @@ export class WorkflowChatTransport<
|
|
|
186
187
|
options: SendMessagesOptions<UI_MESSAGE> & ChatRequestOptions,
|
|
187
188
|
): Promise<ReadableStream<UIMessageChunk>> {
|
|
188
189
|
return convertAsyncIteratorToReadableStream(
|
|
189
|
-
this.sendMessagesIterator(options),
|
|
190
|
+
normalizeUIMessageStreamParts(this.sendMessagesIterator(options)),
|
|
190
191
|
);
|
|
191
192
|
}
|
|
192
193
|
|
|
@@ -292,7 +293,9 @@ export class WorkflowChatTransport<
|
|
|
292
293
|
async reconnectToStream(
|
|
293
294
|
options: ReconnectToStreamOptions & ChatRequestOptions,
|
|
294
295
|
): Promise<ReadableStream<UIMessageChunk> | null> {
|
|
295
|
-
const reconnectIterator =
|
|
296
|
+
const reconnectIterator = normalizeUIMessageStreamParts(
|
|
297
|
+
this.reconnectToStreamIterator(options),
|
|
298
|
+
);
|
|
296
299
|
return convertAsyncIteratorToReadableStream(reconnectIterator);
|
|
297
300
|
}
|
|
298
301
|
|