@ai-sdk/workflow 0.0.0-6b196531-20260710185421

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 ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@ai-sdk/workflow",
3
+ "version": "0.0.0-6b196531-20260710185421",
4
+ "type": "module",
5
+ "description": "WorkflowAgent for building AI agents with AI SDK",
6
+ "license": "Apache-2.0",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "source": "./src/index.ts",
10
+ "files": [
11
+ "dist/**/*",
12
+ "src",
13
+ "!src/**/*.test.ts",
14
+ "!src/**/*.test-d.ts",
15
+ "!src/**/__snapshots__",
16
+ "!src/**/__fixtures__",
17
+ "CHANGELOG.md",
18
+ "README.md"
19
+ ],
20
+ "exports": {
21
+ "./package.json": "./package.json",
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "default": "./dist/index.js"
26
+ }
27
+ },
28
+ "dependencies": {
29
+ "ajv": "^8.20.0",
30
+ "@ai-sdk/provider": "4.0.3",
31
+ "@ai-sdk/provider-utils": "0.0.0-6b196531-20260710185421",
32
+ "ai": "0.0.0-6b196531-20260710185421"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "22.19.19",
36
+ "@workflow/vitest": "4.0.1",
37
+ "tsup": "^8.5.1",
38
+ "typescript": "5.8.3",
39
+ "vitest": "4.1.6",
40
+ "workflow": "4.2.4",
41
+ "zod": "3.25.76",
42
+ "@vercel/ai-tsconfig": "0.0.0"
43
+ },
44
+ "peerDependencies": {
45
+ "zod": "^3.25.76 || ^4.1.8"
46
+ },
47
+ "engines": {
48
+ "node": ">=22"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "provenance": true
53
+ },
54
+ "homepage": "https://ai-sdk.dev/docs",
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "https://github.com/vercel/ai",
58
+ "directory": "packages/workflow"
59
+ },
60
+ "bugs": {
61
+ "url": "https://github.com/vercel/ai/issues"
62
+ },
63
+ "keywords": [
64
+ "ai",
65
+ "agent",
66
+ "workflow"
67
+ ],
68
+ "scripts": {
69
+ "build": "pnpm clean && tsup --tsconfig tsconfig.build.json",
70
+ "build:watch": "pnpm clean && tsup --watch --tsconfig tsconfig.build.json",
71
+ "clean": "del-cli dist *.tsbuildinfo",
72
+ "type-check": "tsc --build",
73
+ "test": "pnpm test:node && pnpm test:edge",
74
+ "test:update": "pnpm test:node -u",
75
+ "test:watch": "vitest --config vitest.node.config.js",
76
+ "test:edge": "vitest --config vitest.edge.config.js --run",
77
+ "test:integration": "vitest --config vitest.integration.config.mjs --run",
78
+ "test:node": "vitest --config vitest.node.config.js --run"
79
+ }
80
+ }
@@ -0,0 +1,85 @@
1
+ import type { LanguageModelV4ToolResultOutput } from '@ai-sdk/provider';
2
+ import type { Tool } from '@ai-sdk/provider-utils';
3
+ import type { ModelMessage } from 'ai';
4
+ import {
5
+ createDefaultDownloadFunction,
6
+ createToolModelOutput,
7
+ downloadAssets,
8
+ mapToolResultOutput,
9
+ type DownloadFunction,
10
+ } from 'ai/internal';
11
+
12
+ /**
13
+ * Converts a single tool result into a provider-level
14
+ * `LanguageModelV4ToolResultOutput`, honoring the tool's optional
15
+ * `toModelOutput` hook.
16
+ *
17
+ * Unlike `generateText`/`streamText`, `WorkflowAgent` assembles the
18
+ * `LanguageModelV4` prompt incrementally — appending one tool result at a time
19
+ * — instead of building AI-level `ModelMessage`s and converting the whole
20
+ * prompt once via `convertToLanguageModelPrompt`. This helper performs the
21
+ * equivalent per-result conversion using the shared `ai/internal` primitives:
22
+ *
23
+ * 1. `createToolModelOutput` — applies `tool.toModelOutput` (or the
24
+ * text/json/error fallback).
25
+ * 2. `downloadAssets` — for `content`-type outputs, downloads any file/image
26
+ * assets so URLs become bytes the provider can consume.
27
+ * 3. `mapToolResultOutput` — maps the AI-level `ToolResultOutput` to the
28
+ * provider-level output and converts legacy file types.
29
+ */
30
+ export async function createLanguageModelToolResultOutput({
31
+ toolCallId,
32
+ toolName,
33
+ input,
34
+ output,
35
+ tool,
36
+ errorMode,
37
+ supportedUrls,
38
+ download = createDefaultDownloadFunction(),
39
+ provider,
40
+ }: {
41
+ toolCallId: string;
42
+ toolName: string;
43
+ input: unknown;
44
+ output: unknown;
45
+ tool: Tool | undefined;
46
+ errorMode: 'none' | 'text' | 'json';
47
+ supportedUrls: Record<string, RegExp[]>;
48
+ download?: DownloadFunction;
49
+ provider?: string;
50
+ }): Promise<LanguageModelV4ToolResultOutput> {
51
+ const modelOutput = await createToolModelOutput({
52
+ toolCallId,
53
+ input,
54
+ output,
55
+ tool,
56
+ errorMode,
57
+ });
58
+
59
+ const downloadedAssets =
60
+ modelOutput.type === 'content'
61
+ ? await downloadAssets(
62
+ [
63
+ {
64
+ role: 'tool',
65
+ content: [
66
+ {
67
+ type: 'tool-result',
68
+ toolCallId,
69
+ toolName,
70
+ output: modelOutput,
71
+ },
72
+ ],
73
+ } satisfies ModelMessage,
74
+ ],
75
+ download,
76
+ supportedUrls,
77
+ )
78
+ : {};
79
+
80
+ return mapToolResultOutput({
81
+ output: modelOutput,
82
+ provider,
83
+ downloadedAssets,
84
+ });
85
+ }
@@ -0,0 +1,265 @@
1
+ import type {
2
+ LanguageModelV4CallOptions,
3
+ LanguageModelV4Prompt,
4
+ } from '@ai-sdk/provider';
5
+ import {
6
+ experimental_streamLanguageModelCall as streamModelCall,
7
+ gateway,
8
+ type Experimental_LanguageModelStreamPart as ModelCallStreamPart,
9
+ type FinishReason,
10
+ type LanguageModel,
11
+ type LanguageModelUsage,
12
+ type ModelMessage,
13
+ type StopCondition,
14
+ type ToolCallRepairFunction,
15
+ type ToolChoice,
16
+ type ToolSet,
17
+ } from 'ai';
18
+ import type { ProviderOptions } from './workflow-agent.js';
19
+ import {
20
+ resolveSerializableTools,
21
+ type SerializableToolDef,
22
+ } from './serializable-schema.js';
23
+ export type { Experimental_LanguageModelStreamPart as ModelCallStreamPart } from 'ai';
24
+
25
+ export type ModelStopCondition = StopCondition<NoInfer<ToolSet>, any>;
26
+
27
+ /**
28
+ * Provider-executed tool result captured from the stream.
29
+ */
30
+ export interface ProviderExecutedToolResult {
31
+ toolCallId: string;
32
+ toolName: string;
33
+ result: unknown;
34
+ isError?: boolean;
35
+ }
36
+
37
+ /**
38
+ * Options for the doStreamStep function.
39
+ */
40
+ export interface DoStreamStepOptions {
41
+ maxOutputTokens?: number;
42
+ temperature?: number;
43
+ topP?: number;
44
+ topK?: number;
45
+ presencePenalty?: number;
46
+ frequencyPenalty?: number;
47
+ stopSequences?: string[];
48
+ seed?: number;
49
+ maxRetries?: number;
50
+ abortSignal?: AbortSignal;
51
+ headers?: Record<string, string | undefined>;
52
+ reasoning?: LanguageModelV4CallOptions['reasoning'];
53
+ providerOptions?: ProviderOptions;
54
+ toolChoice?: ToolChoice<ToolSet>;
55
+ includeRawChunks?: boolean;
56
+ repairToolCall?: ToolCallRepairFunction<ToolSet>;
57
+ responseFormat?: LanguageModelV4CallOptions['responseFormat'];
58
+ }
59
+
60
+ /**
61
+ * Parsed tool call from the stream (parsed by streamModelCall's transform).
62
+ */
63
+ export interface ParsedToolCall {
64
+ type: 'tool-call';
65
+ toolCallId: string;
66
+ toolName: string;
67
+ input: unknown;
68
+ providerExecuted?: boolean;
69
+ providerMetadata?: Record<string, unknown>;
70
+ dynamic?: boolean;
71
+ invalid?: boolean;
72
+ error?: unknown;
73
+ }
74
+
75
+ /**
76
+ * Finish metadata from the stream.
77
+ */
78
+ export interface StreamFinish {
79
+ finishReason: FinishReason;
80
+ rawFinishReason: string | undefined;
81
+ usage: LanguageModelUsage;
82
+ providerMetadata?: Record<string, unknown>;
83
+ }
84
+
85
+ /**
86
+ * Minimal aggregates needed to reconstruct a `StepResult` outside the step
87
+ * boundary. By returning only these fields (instead of a fully-populated
88
+ * StepResult plus the raw `chunks[]` array), the durable event log doesn't
89
+ * carry StepResult's redundant copies — `content`, the duplicate
90
+ * `toolCalls`/`dynamicToolCalls` lists, `reasoningText`, the always-empty
91
+ * `*ToolResults` arrays, and the per-chunk `chunks[]` snapshot the iterator
92
+ * never reads. The caller reconstructs the full StepResult via
93
+ * `buildStepResult`.
94
+ */
95
+ export interface DoStreamStepRawResult {
96
+ text: string;
97
+ reasoning: Array<{ text: string }>;
98
+ responseMetadata?: { id?: string; timestamp?: Date; modelId?: string };
99
+ warnings?: unknown[];
100
+ }
101
+
102
+ export async function doStreamStep(
103
+ conversationPrompt: LanguageModelV4Prompt,
104
+ modelInit: LanguageModel,
105
+ writable?: WritableStream<ModelCallStreamPart<ToolSet>>,
106
+ serializedTools?: Record<string, SerializableToolDef>,
107
+ options?: DoStreamStepOptions,
108
+ ): Promise<{
109
+ toolCalls: ParsedToolCall[];
110
+ finish: StreamFinish | undefined;
111
+ raw: DoStreamStepRawResult;
112
+ providerExecutedToolResults: Map<string, ProviderExecutedToolResult>;
113
+ }> {
114
+ 'use step';
115
+
116
+ // Resolve model inside step (must happen here for serialization boundary)
117
+ const model: LanguageModel =
118
+ typeof modelInit === 'string'
119
+ ? gateway.languageModel(modelInit)
120
+ : modelInit;
121
+
122
+ // Reconstruct tools from serializable definitions with Ajv validation.
123
+ // Tools are serialized before crossing the step boundary because zod schemas
124
+ // contain functions that can't be serialized by the workflow runtime.
125
+ const tools = serializedTools
126
+ ? resolveSerializableTools(serializedTools)
127
+ : undefined;
128
+
129
+ // streamModelCall handles: prompt standardization, tool preparation,
130
+ // model.doStream(), retry logic, and stream part transformation
131
+ // (tool call parsing, finish reason mapping, file wrapping).
132
+ const { stream: modelStream } = await streamModelCall({
133
+ model,
134
+ // streamModelCall expects Prompt (ModelMessage[]) but we pass the
135
+ // pre-converted LanguageModelV4Prompt. standardizePrompt inside
136
+ // streamModelCall handles both formats.
137
+ messages: conversationPrompt as unknown as ModelMessage[],
138
+ allowSystemInMessages: true,
139
+ tools,
140
+ toolChoice: options?.toolChoice,
141
+ includeRawChunks: options?.includeRawChunks,
142
+ providerOptions: options?.providerOptions,
143
+ abortSignal: options?.abortSignal,
144
+ headers: options?.headers,
145
+ reasoning: options?.reasoning,
146
+ maxOutputTokens: options?.maxOutputTokens,
147
+ temperature: options?.temperature,
148
+ topP: options?.topP,
149
+ topK: options?.topK,
150
+ presencePenalty: options?.presencePenalty,
151
+ frequencyPenalty: options?.frequencyPenalty,
152
+ stopSequences: options?.stopSequences,
153
+ seed: options?.seed,
154
+ repairToolCall: options?.repairToolCall,
155
+ });
156
+
157
+ // Consume the stream: capture data and write to writable in real-time
158
+ const toolCalls: ParsedToolCall[] = [];
159
+ const providerExecutedToolResults = new Map<
160
+ string,
161
+ ProviderExecutedToolResult
162
+ >();
163
+ let finish: StreamFinish | undefined;
164
+
165
+ // Minimal aggregation — only what buildStepResult needs outside the step.
166
+ let text = '';
167
+ const reasoningParts: Array<{ text: string }> = [];
168
+ let responseMetadata:
169
+ | { id?: string; timestamp?: Date; modelId?: string }
170
+ | undefined;
171
+ let warnings: unknown[] | undefined;
172
+
173
+ // Acquire writer once before the loop to avoid per-chunk lock overhead
174
+ const writer = writable?.getWriter();
175
+
176
+ try {
177
+ for await (const part of modelStream) {
178
+ switch (part.type) {
179
+ case 'text-delta':
180
+ text += part.text;
181
+ break;
182
+ case 'reasoning-delta':
183
+ reasoningParts.push({ text: part.text });
184
+ break;
185
+ case 'tool-call': {
186
+ // parseToolCall adds dynamic/invalid/error at runtime
187
+ const toolCallPart = part as typeof part & Partial<ParsedToolCall>;
188
+ toolCalls.push({
189
+ type: 'tool-call',
190
+ toolCallId: toolCallPart.toolCallId,
191
+ toolName: toolCallPart.toolName,
192
+ input: toolCallPart.input,
193
+ providerExecuted: toolCallPart.providerExecuted,
194
+ providerMetadata: toolCallPart.providerMetadata as
195
+ | Record<string, unknown>
196
+ | undefined,
197
+ dynamic: toolCallPart.dynamic,
198
+ invalid: toolCallPart.invalid,
199
+ error: toolCallPart.error,
200
+ });
201
+ break;
202
+ }
203
+ case 'tool-result':
204
+ if (part.providerExecuted) {
205
+ providerExecutedToolResults.set(part.toolCallId, {
206
+ toolCallId: part.toolCallId,
207
+ toolName: part.toolName,
208
+ result: part.output,
209
+ isError: false,
210
+ });
211
+ }
212
+ break;
213
+ case 'tool-error': {
214
+ const errorPart = part as typeof part & {
215
+ providerExecuted?: boolean;
216
+ };
217
+ if (errorPart.providerExecuted) {
218
+ providerExecutedToolResults.set(errorPart.toolCallId, {
219
+ toolCallId: errorPart.toolCallId,
220
+ toolName: errorPart.toolName,
221
+ result: errorPart.error,
222
+ isError: true,
223
+ });
224
+ }
225
+ break;
226
+ }
227
+ case 'model-call-end':
228
+ finish = {
229
+ finishReason: part.finishReason,
230
+ rawFinishReason: part.rawFinishReason,
231
+ usage: part.usage,
232
+ providerMetadata: part.providerMetadata as
233
+ | Record<string, unknown>
234
+ | undefined,
235
+ };
236
+ break;
237
+ case 'model-call-start':
238
+ warnings = part.warnings;
239
+ break;
240
+ case 'model-call-response-metadata':
241
+ responseMetadata = part;
242
+ break;
243
+ }
244
+
245
+ // Write to writable in real-time
246
+ if (writer) {
247
+ await writer.write(part);
248
+ }
249
+ }
250
+ } finally {
251
+ writer?.releaseLock();
252
+ }
253
+
254
+ return {
255
+ toolCalls,
256
+ finish,
257
+ raw: {
258
+ text,
259
+ reasoning: reasoningParts,
260
+ responseMetadata,
261
+ warnings,
262
+ },
263
+ providerExecutedToolResults,
264
+ };
265
+ }
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ export {
2
+ WorkflowAgent,
3
+ Output,
4
+ type CompatibleLanguageModel,
5
+ type DownloadFunction,
6
+ type WorkflowAgentOptions,
7
+ type WorkflowAgentStreamOptions,
8
+ type WorkflowAgentStreamResult,
9
+ type GenerationSettings,
10
+ type InferWorkflowAgentTools,
11
+ type InferWorkflowAgentUIMessage,
12
+ type OutputSpecification,
13
+ type PrepareCallCallback,
14
+ type PrepareCallOptions,
15
+ type PrepareCallResult,
16
+ type PrepareStepCallback,
17
+ type PrepareStepInfo,
18
+ type PrepareStepResult,
19
+ type ProviderOptions,
20
+ type WorkflowAgentOnAbortCallback,
21
+ type WorkflowAgentOnEndCallback,
22
+ type WorkflowAgentOnErrorCallback,
23
+ type WorkflowAgentOnFinishCallback,
24
+ type WorkflowAgentOnStepEndCallback,
25
+ type WorkflowAgentOnStepFinishCallback,
26
+ type StreamTextTransform,
27
+ type TelemetryOptions,
28
+ type ToolCallRepairFunction,
29
+ type WorkflowAgentOnStartCallback,
30
+ type WorkflowAgentOnStepStartCallback,
31
+ type WorkflowAgentOnToolExecutionStartCallback,
32
+ type WorkflowAgentOnToolExecutionEndCallback,
33
+ } from './workflow-agent.js';
34
+
35
+ export {
36
+ createModelCallToUIChunkTransform,
37
+ toUIMessageChunk,
38
+ } from './to-ui-message-chunk.js';
39
+
40
+ export type { ModelCallStreamPart } from './do-stream-step.js';
41
+
42
+ export {
43
+ WorkflowChatTransport,
44
+ type WorkflowChatTransportOptions,
45
+ type SendMessagesOptions,
46
+ type ReconnectToStreamOptions,
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
+ }
@@ -0,0 +1,11 @@
1
+ import { MockLanguageModelV4 } from 'ai/test';
2
+
3
+ // Workaround for SWC plugin bug (https://github.com/vercel/workflow/issues/1365):
4
+ // `new ClassName(...)` in a step closure doesn't get closure vars hoisted
5
+ // correctly. Wrapping the constructor call in a plain function (imported
6
+ // from a separate file) fixes it.
7
+ export function mockProvider(
8
+ ...args: ConstructorParameters<typeof MockLanguageModelV4>
9
+ ) {
10
+ return new MockLanguageModelV4(...args);
11
+ }