@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/CHANGELOG.md +1145 -0
- package/LICENSE +13 -0
- package/README.md +62 -0
- package/dist/index.d.ts +1188 -0
- package/dist/index.js +2522 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
- package/src/create-language-model-tool-result-output.ts +85 -0
- package/src/do-stream-step.ts +265 -0
- package/src/index.ts +49 -0
- package/src/normalize-ui-message-stream.ts +164 -0
- package/src/providers/mock-function-wrapper.ts +11 -0
- package/src/providers/mock.ts +110 -0
- package/src/serializable-schema.ts +114 -0
- package/src/stream-text-iterator.ts +661 -0
- package/src/test/agent-e2e-workflows.ts +673 -0
- package/src/test/calculate-workflow.ts +19 -0
- package/src/test/test-sandbox.ts +26 -0
- package/src/to-ui-message-chunk.ts +238 -0
- package/src/types.ts +11 -0
- package/src/workflow-agent.ts +2930 -0
- package/src/workflow-chat-transport.ts +534 -0
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
|
+
}
|