@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/workflow",
3
- "version": "1.0.5",
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.5"
32
+ "ai": "7.0.7"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "22.19.19",
@@ -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
@@ -45,3 +45,5 @@ export {
45
45
  type SendMessagesOptions,
46
46
  type ReconnectToStreamOptions,
47
47
  } from './workflow-chat-transport.js';
48
+
49
+ export { normalizeUIMessageStreamParts } from './normalize-ui-message-stream.js';
@@ -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);
@@ -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 = this.reconnectToStreamIterator(options);
296
+ const reconnectIterator = normalizeUIMessageStreamParts(
297
+ this.reconnectToStreamIterator(options),
298
+ );
296
299
  return convertAsyncIteratorToReadableStream(reconnectIterator);
297
300
  }
298
301