@ai-sdk/workflow 1.0.66 → 1.0.68
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 +14 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +134 -47
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/do-stream-step.ts +111 -33
- package/src/providers/mock-function-wrapper.ts +101 -4
- package/src/providers/mock.ts +4 -93
- package/src/stream-text-iterator.ts +58 -15
- package/src/test/agent-e2e-workflows.ts +41 -0
- package/src/test/retrying-model.ts +79 -0
- package/src/workflow-agent.ts +48 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/workflow",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.68",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WorkflowAgent for building AI agents with AI SDK",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"ajv": "^8.20.0",
|
|
30
|
-
"@ai-sdk/provider": "4.0.7",
|
|
31
30
|
"@ai-sdk/provider-utils": "5.0.27",
|
|
32
|
-
"ai": "
|
|
31
|
+
"@ai-sdk/provider": "4.0.7",
|
|
32
|
+
"ai": "7.0.67"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "22.19.19",
|
package/src/do-stream-step.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
LanguageModelV4CallOptions,
|
|
3
3
|
LanguageModelV4Prompt,
|
|
4
4
|
} from '@ai-sdk/provider';
|
|
5
|
+
import { isAbortError } from '@ai-sdk/provider-utils';
|
|
5
6
|
import {
|
|
6
7
|
experimental_streamLanguageModelCall as streamModelCall,
|
|
7
8
|
gateway,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
type ToolChoice,
|
|
16
17
|
type ToolSet,
|
|
17
18
|
} from 'ai';
|
|
19
|
+
import { prepareRetries } from 'ai/internal';
|
|
18
20
|
import type { ProviderOptions } from './workflow-agent.js';
|
|
19
21
|
import {
|
|
20
22
|
resolveSerializableTools,
|
|
@@ -48,6 +50,7 @@ export interface DoStreamStepOptions {
|
|
|
48
50
|
seed?: number;
|
|
49
51
|
maxRetries?: number;
|
|
50
52
|
abortSignal?: AbortSignal;
|
|
53
|
+
timeoutAt?: number;
|
|
51
54
|
headers?: Record<string, string | undefined>;
|
|
52
55
|
reasoning?: LanguageModelV4CallOptions['reasoning'];
|
|
53
56
|
providerOptions?: ProviderOptions;
|
|
@@ -99,20 +102,44 @@ export interface DoStreamStepRawResult {
|
|
|
99
102
|
warnings?: unknown[];
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
export type DoStreamStepResult =
|
|
106
|
+
| { aborted: true }
|
|
107
|
+
| {
|
|
108
|
+
aborted?: false;
|
|
109
|
+
toolCalls: ParsedToolCall[];
|
|
110
|
+
finish: StreamFinish | undefined;
|
|
111
|
+
raw: DoStreamStepRawResult;
|
|
112
|
+
providerExecutedToolResults: Map<string, ProviderExecutedToolResult>;
|
|
113
|
+
/** Present when the model stream emitted an error part. */
|
|
114
|
+
terminalError?: unknown;
|
|
115
|
+
};
|
|
116
|
+
|
|
102
117
|
export async function doStreamStep(
|
|
103
118
|
conversationPrompt: LanguageModelV4Prompt,
|
|
104
119
|
modelInit: LanguageModel,
|
|
105
120
|
writable?: WritableStream<ModelCallStreamPart<ToolSet>>,
|
|
106
121
|
serializedTools?: Record<string, SerializableToolDef>,
|
|
107
122
|
options?: DoStreamStepOptions,
|
|
108
|
-
): Promise<{
|
|
109
|
-
toolCalls: ParsedToolCall[];
|
|
110
|
-
finish: StreamFinish | undefined;
|
|
111
|
-
raw: DoStreamStepRawResult;
|
|
112
|
-
providerExecutedToolResults: Map<string, ProviderExecutedToolResult>;
|
|
113
|
-
}> {
|
|
123
|
+
): Promise<DoStreamStepResult> {
|
|
114
124
|
'use step';
|
|
115
125
|
|
|
126
|
+
const timeout =
|
|
127
|
+
options?.timeoutAt == null ? undefined : options.timeoutAt - Date.now();
|
|
128
|
+
|
|
129
|
+
// AbortSignal.timeout(0) does not abort synchronously. Check the deadline
|
|
130
|
+
// explicitly so an expired call never reaches the model, including when a
|
|
131
|
+
// durable step is retried after the original timeout has elapsed.
|
|
132
|
+
if (options?.abortSignal?.aborted || (timeout != null && timeout <= 0)) {
|
|
133
|
+
return { aborted: true };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const abortSignal =
|
|
137
|
+
timeout == null
|
|
138
|
+
? options?.abortSignal
|
|
139
|
+
: options?.abortSignal == null
|
|
140
|
+
? AbortSignal.timeout(timeout)
|
|
141
|
+
: AbortSignal.any([options.abortSignal, AbortSignal.timeout(timeout)]);
|
|
142
|
+
|
|
116
143
|
// Resolve model inside step (must happen here for serialization boundary)
|
|
117
144
|
const model: LanguageModel =
|
|
118
145
|
typeof modelInit === 'string'
|
|
@@ -148,34 +175,56 @@ export async function doStreamStep(
|
|
|
148
175
|
},
|
|
149
176
|
};
|
|
150
177
|
|
|
151
|
-
// streamModelCall handles
|
|
152
|
-
// model.doStream(),
|
|
153
|
-
//
|
|
154
|
-
const {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// pre-converted LanguageModelV4Prompt. standardizePrompt inside
|
|
158
|
-
// streamModelCall handles both formats.
|
|
159
|
-
messages: conversationPrompt as unknown as ModelMessage[],
|
|
160
|
-
allowSystemInMessages: true,
|
|
161
|
-
tools,
|
|
162
|
-
toolChoice: options?.toolChoice,
|
|
163
|
-
includeRawChunks: options?.includeRawChunks,
|
|
164
|
-
providerOptions: options?.providerOptions,
|
|
165
|
-
abortSignal: options?.abortSignal,
|
|
166
|
-
headers: options?.headers,
|
|
167
|
-
reasoning: options?.reasoning,
|
|
168
|
-
output,
|
|
169
|
-
maxOutputTokens: options?.maxOutputTokens,
|
|
170
|
-
temperature: options?.temperature,
|
|
171
|
-
topP: options?.topP,
|
|
172
|
-
topK: options?.topK,
|
|
173
|
-
presencePenalty: options?.presencePenalty,
|
|
174
|
-
frequencyPenalty: options?.frequencyPenalty,
|
|
175
|
-
stopSequences: options?.stopSequences,
|
|
176
|
-
seed: options?.seed,
|
|
177
|
-
repairToolCall: options?.repairToolCall,
|
|
178
|
+
// streamModelCall handles prompt standardization, tool preparation,
|
|
179
|
+
// model.doStream(), and stream part transformation. Retries are applied
|
|
180
|
+
// around the model dispatch because streamModelCall itself does not retry.
|
|
181
|
+
const { retry } = prepareRetries({
|
|
182
|
+
maxRetries: options?.maxRetries,
|
|
183
|
+
abortSignal,
|
|
178
184
|
});
|
|
185
|
+
const modelStream = await (async () => {
|
|
186
|
+
try {
|
|
187
|
+
const { stream } = await retry(() =>
|
|
188
|
+
streamModelCall({
|
|
189
|
+
model,
|
|
190
|
+
// streamModelCall expects Prompt (ModelMessage[]) but we pass the
|
|
191
|
+
// pre-converted LanguageModelV4Prompt. standardizePrompt inside
|
|
192
|
+
// streamModelCall handles both formats.
|
|
193
|
+
messages: conversationPrompt as unknown as ModelMessage[],
|
|
194
|
+
allowSystemInMessages: true,
|
|
195
|
+
tools,
|
|
196
|
+
toolChoice: options?.toolChoice,
|
|
197
|
+
includeRawChunks: options?.includeRawChunks,
|
|
198
|
+
providerOptions: options?.providerOptions,
|
|
199
|
+
abortSignal,
|
|
200
|
+
headers: options?.headers,
|
|
201
|
+
reasoning: options?.reasoning,
|
|
202
|
+
output,
|
|
203
|
+
maxOutputTokens: options?.maxOutputTokens,
|
|
204
|
+
temperature: options?.temperature,
|
|
205
|
+
topP: options?.topP,
|
|
206
|
+
topK: options?.topK,
|
|
207
|
+
presencePenalty: options?.presencePenalty,
|
|
208
|
+
frequencyPenalty: options?.frequencyPenalty,
|
|
209
|
+
stopSequences: options?.stopSequences,
|
|
210
|
+
seed: options?.seed,
|
|
211
|
+
repairToolCall: options?.repairToolCall,
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return stream;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (abortSignal?.aborted && isAbortError(error)) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
})();
|
|
224
|
+
|
|
225
|
+
if (modelStream == null) {
|
|
226
|
+
return { aborted: true };
|
|
227
|
+
}
|
|
179
228
|
|
|
180
229
|
// Consume the stream: capture data and write to writable in real-time
|
|
181
230
|
const toolCalls: ParsedToolCall[] = [];
|
|
@@ -192,6 +241,8 @@ export async function doStreamStep(
|
|
|
192
241
|
| { id?: string; timestamp?: Date; modelId?: string }
|
|
193
242
|
| undefined;
|
|
194
243
|
let warnings: unknown[] | undefined;
|
|
244
|
+
let terminalError: unknown;
|
|
245
|
+
let hasTerminalError = false;
|
|
195
246
|
|
|
196
247
|
// Acquire writer once before the loop to avoid per-chunk lock overhead
|
|
197
248
|
const writer = writable?.getWriter();
|
|
@@ -269,11 +320,33 @@ export async function doStreamStep(
|
|
|
269
320
|
if (writer) {
|
|
270
321
|
await writer.write(part);
|
|
271
322
|
}
|
|
323
|
+
|
|
324
|
+
if (part.type === 'error' && !hasTerminalError) {
|
|
325
|
+
// Retain the first model error as step data. Throwing here would make
|
|
326
|
+
// the durable workflow runtime retry the model step and normalize the
|
|
327
|
+
// original value before WorkflowAgent can surface it. Continue
|
|
328
|
+
// consuming so the existing finish reason and usage are preserved.
|
|
329
|
+
terminalError = part.error;
|
|
330
|
+
hasTerminalError = true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (abortSignal?.aborted && isAbortError(error)) {
|
|
335
|
+
return { aborted: true };
|
|
272
336
|
}
|
|
337
|
+
|
|
338
|
+
throw error;
|
|
273
339
|
} finally {
|
|
274
340
|
writer?.releaseLock();
|
|
275
341
|
}
|
|
276
342
|
|
|
343
|
+
if (
|
|
344
|
+
abortSignal?.aborted ||
|
|
345
|
+
(options?.timeoutAt != null && options.timeoutAt <= Date.now())
|
|
346
|
+
) {
|
|
347
|
+
return { aborted: true };
|
|
348
|
+
}
|
|
349
|
+
|
|
277
350
|
return {
|
|
278
351
|
toolCalls,
|
|
279
352
|
finish,
|
|
@@ -284,5 +357,10 @@ export async function doStreamStep(
|
|
|
284
357
|
warnings,
|
|
285
358
|
},
|
|
286
359
|
providerExecutedToolResults,
|
|
360
|
+
...(hasTerminalError ? { terminalError } : {}),
|
|
287
361
|
};
|
|
288
362
|
}
|
|
363
|
+
|
|
364
|
+
// Model-call retries are handled above so the workflow runtime must not add
|
|
365
|
+
// another retry layer around the durable step.
|
|
366
|
+
doStreamStep.maxRetries = 0;
|
|
@@ -1,11 +1,108 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
WORKFLOW_DESERIALIZE,
|
|
3
|
+
WORKFLOW_SERIALIZE,
|
|
4
|
+
} from '@ai-sdk/provider-utils';
|
|
5
|
+
import { convertArrayToReadableStream, MockLanguageModelV4 } from 'ai/test';
|
|
6
|
+
import type { MockResponseDescriptor } from './mock.js';
|
|
7
|
+
|
|
8
|
+
type MockStreamOptions = Parameters<MockLanguageModelV4['doStream']>[0];
|
|
9
|
+
type MockStreamResult = Awaited<ReturnType<MockLanguageModelV4['doStream']>>;
|
|
10
|
+
type MockStreamPart = MockStreamResult extends {
|
|
11
|
+
stream: ReadableStream<infer PART>;
|
|
12
|
+
}
|
|
13
|
+
? PART
|
|
14
|
+
: never;
|
|
15
|
+
|
|
16
|
+
const usage = {
|
|
17
|
+
inputTokens: {
|
|
18
|
+
total: 5,
|
|
19
|
+
noCache: 5,
|
|
20
|
+
cacheRead: undefined,
|
|
21
|
+
cacheWrite: undefined,
|
|
22
|
+
},
|
|
23
|
+
outputTokens: { total: 10, text: 10, reasoning: undefined },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
class SerializableMockLanguageModel extends MockLanguageModelV4 {
|
|
27
|
+
static [WORKFLOW_SERIALIZE](model: SerializableMockLanguageModel) {
|
|
28
|
+
return { responses: model.responses };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static [WORKFLOW_DESERIALIZE](options: {
|
|
32
|
+
responses: MockResponseDescriptor[];
|
|
33
|
+
}) {
|
|
34
|
+
return new SerializableMockLanguageModel(options.responses);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(private readonly responses: MockResponseDescriptor[]) {
|
|
38
|
+
super({
|
|
39
|
+
provider: 'workflow-test',
|
|
40
|
+
modelId: 'workflow-test-model',
|
|
41
|
+
doStream: async (options: MockStreamOptions) => {
|
|
42
|
+
const responseIndex = Math.min(
|
|
43
|
+
options.prompt.filter(message => message.role === 'assistant').length,
|
|
44
|
+
responses.length - 1,
|
|
45
|
+
);
|
|
46
|
+
const response = responses[responseIndex];
|
|
47
|
+
const prefix: MockStreamPart[] = [
|
|
48
|
+
{ type: 'stream-start', warnings: [] },
|
|
49
|
+
{
|
|
50
|
+
type: 'response-metadata',
|
|
51
|
+
id: 'r',
|
|
52
|
+
modelId: 'mock',
|
|
53
|
+
timestamp: new Date(),
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const streamParts: MockStreamPart[] =
|
|
57
|
+
response.type === 'text'
|
|
58
|
+
? [
|
|
59
|
+
...prefix,
|
|
60
|
+
{ type: 'text-start', id: '1' },
|
|
61
|
+
{ type: 'text-delta', id: '1', delta: response.text },
|
|
62
|
+
{ type: 'text-end', id: '1' },
|
|
63
|
+
{
|
|
64
|
+
type: 'finish',
|
|
65
|
+
finishReason: { unified: 'stop', raw: 'stop' },
|
|
66
|
+
usage,
|
|
67
|
+
},
|
|
68
|
+
]
|
|
69
|
+
: response.type === 'tool-call'
|
|
70
|
+
? [
|
|
71
|
+
...prefix,
|
|
72
|
+
{
|
|
73
|
+
type: 'tool-call',
|
|
74
|
+
toolCallId: `call-${responseIndex + 1}`,
|
|
75
|
+
toolName: response.toolName,
|
|
76
|
+
input: response.input,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'finish',
|
|
80
|
+
finishReason: { unified: 'tool-calls', raw: undefined },
|
|
81
|
+
usage,
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
: [
|
|
85
|
+
...prefix,
|
|
86
|
+
{ type: 'error', error: response.error },
|
|
87
|
+
{
|
|
88
|
+
type: 'finish',
|
|
89
|
+
finishReason: { unified: 'error', raw: 'error' },
|
|
90
|
+
usage,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
return { stream: convertArrayToReadableStream(streamParts) };
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
2
99
|
|
|
3
100
|
// Workaround for SWC plugin bug (https://github.com/vercel/workflow/issues/1365):
|
|
4
101
|
// `new ClassName(...)` in a step closure doesn't get closure vars hoisted
|
|
5
102
|
// correctly. Wrapping the constructor call in a plain function (imported
|
|
6
103
|
// from a separate file) fixes it.
|
|
7
104
|
export function mockProvider(
|
|
8
|
-
|
|
9
|
-
) {
|
|
10
|
-
return new
|
|
105
|
+
responses: MockResponseDescriptor[],
|
|
106
|
+
): MockLanguageModelV4 {
|
|
107
|
+
return new SerializableMockLanguageModel(responses);
|
|
11
108
|
}
|
package/src/providers/mock.ts
CHANGED
|
@@ -2,42 +2,14 @@ import { mockProvider } from './mock-function-wrapper.js';
|
|
|
2
2
|
|
|
3
3
|
export type MockResponseDescriptor =
|
|
4
4
|
| { type: 'text'; text: string }
|
|
5
|
-
| { type: 'tool-call'; toolName: string; input: string }
|
|
5
|
+
| { type: 'tool-call'; toolName: string; input: string }
|
|
6
|
+
| { type: 'error'; error: unknown };
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Mock model that returns a fixed text response.
|
|
9
10
|
*/
|
|
10
11
|
export function mockTextModel(text: string) {
|
|
11
|
-
return mockProvider({
|
|
12
|
-
doStream: async () => ({
|
|
13
|
-
stream: new ReadableStream({
|
|
14
|
-
start(c) {
|
|
15
|
-
for (const v of [
|
|
16
|
-
{ type: 'stream-start', warnings: [] },
|
|
17
|
-
{
|
|
18
|
-
type: 'response-metadata',
|
|
19
|
-
id: 'r',
|
|
20
|
-
modelId: 'mock',
|
|
21
|
-
timestamp: new Date(),
|
|
22
|
-
},
|
|
23
|
-
{ type: 'text-start', id: '1' },
|
|
24
|
-
{ type: 'text-delta', id: '1', delta: text },
|
|
25
|
-
{ type: 'text-end', id: '1' },
|
|
26
|
-
{
|
|
27
|
-
type: 'finish',
|
|
28
|
-
finishReason: { unified: 'stop', raw: 'stop' },
|
|
29
|
-
usage: {
|
|
30
|
-
inputTokens: { total: 5, noCache: 5 },
|
|
31
|
-
outputTokens: { total: 10, text: 10 },
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
] as any[])
|
|
35
|
-
c.enqueue(v);
|
|
36
|
-
c.close();
|
|
37
|
-
},
|
|
38
|
-
}),
|
|
39
|
-
}),
|
|
40
|
-
});
|
|
12
|
+
return mockProvider([{ type: 'text', text }]);
|
|
41
13
|
}
|
|
42
14
|
|
|
43
15
|
/**
|
|
@@ -45,66 +17,5 @@ export function mockTextModel(text: string) {
|
|
|
45
17
|
* Determines which response to return by counting assistant messages in the prompt.
|
|
46
18
|
*/
|
|
47
19
|
export function mockSequenceModel(responses: MockResponseDescriptor[]) {
|
|
48
|
-
return mockProvider(
|
|
49
|
-
doStream: async (options: any) => {
|
|
50
|
-
const responseIndex = Math.min(
|
|
51
|
-
options.prompt.filter((m: any) => m.role === 'assistant').length,
|
|
52
|
-
responses.length - 1,
|
|
53
|
-
);
|
|
54
|
-
const selectedResponse = responses[responseIndex];
|
|
55
|
-
const parts =
|
|
56
|
-
selectedResponse.type === 'text'
|
|
57
|
-
? [
|
|
58
|
-
{ type: 'stream-start', warnings: [] },
|
|
59
|
-
{
|
|
60
|
-
type: 'response-metadata',
|
|
61
|
-
id: 'r',
|
|
62
|
-
modelId: 'mock',
|
|
63
|
-
timestamp: new Date(),
|
|
64
|
-
},
|
|
65
|
-
{ type: 'text-start', id: '1' },
|
|
66
|
-
{ type: 'text-delta', id: '1', delta: selectedResponse.text },
|
|
67
|
-
{ type: 'text-end', id: '1' },
|
|
68
|
-
{
|
|
69
|
-
type: 'finish',
|
|
70
|
-
finishReason: { unified: 'stop', raw: 'stop' },
|
|
71
|
-
usage: {
|
|
72
|
-
inputTokens: { total: 5, noCache: 5 },
|
|
73
|
-
outputTokens: { total: 10, text: 10 },
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
]
|
|
77
|
-
: [
|
|
78
|
-
{ type: 'stream-start', warnings: [] },
|
|
79
|
-
{
|
|
80
|
-
type: 'response-metadata',
|
|
81
|
-
id: 'r',
|
|
82
|
-
modelId: 'mock',
|
|
83
|
-
timestamp: new Date(),
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
type: 'tool-call',
|
|
87
|
-
toolCallId: `call-${responseIndex + 1}`,
|
|
88
|
-
toolName: selectedResponse.toolName,
|
|
89
|
-
input: selectedResponse.input,
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
type: 'finish',
|
|
93
|
-
finishReason: { unified: 'tool-calls', raw: undefined },
|
|
94
|
-
usage: {
|
|
95
|
-
inputTokens: { total: 5, noCache: 5 },
|
|
96
|
-
outputTokens: { total: 10, text: 10 },
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
];
|
|
100
|
-
return {
|
|
101
|
-
stream: new ReadableStream({
|
|
102
|
-
start(c) {
|
|
103
|
-
for (const streamPart of parts as any[]) c.enqueue(streamPart);
|
|
104
|
-
c.close();
|
|
105
|
-
},
|
|
106
|
-
}),
|
|
107
|
-
};
|
|
108
|
-
},
|
|
109
|
-
});
|
|
20
|
+
return mockProvider(responses);
|
|
110
21
|
}
|
|
@@ -92,6 +92,16 @@ export interface StreamTextIteratorYieldValue {
|
|
|
92
92
|
experimental_sandbox?: SandboxSession;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
export interface StreamTextIteratorAbortedValue {
|
|
96
|
+
aborted: true;
|
|
97
|
+
messages: LanguageModelV4Prompt;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface StreamTextIteratorErrorValue {
|
|
101
|
+
error: unknown;
|
|
102
|
+
messages: LanguageModelV4Prompt;
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
// This runs in the workflow context
|
|
96
106
|
export async function* streamTextIterator({
|
|
97
107
|
prompt,
|
|
@@ -112,6 +122,7 @@ export async function* streamTextIterator({
|
|
|
112
122
|
toolsContext,
|
|
113
123
|
telemetry,
|
|
114
124
|
includeRawChunks = false,
|
|
125
|
+
timeoutAt,
|
|
115
126
|
repairToolCall,
|
|
116
127
|
responseFormat,
|
|
117
128
|
experimental_sandbox: sandbox,
|
|
@@ -135,12 +146,15 @@ export async function* streamTextIterator({
|
|
|
135
146
|
toolsContext?: Record<string, Context | undefined>;
|
|
136
147
|
telemetry?: TelemetryOptions<Context, ToolSet>;
|
|
137
148
|
includeRawChunks?: boolean;
|
|
149
|
+
timeoutAt?: number;
|
|
138
150
|
repairToolCall?: ToolCallRepairFunction<ToolSet>;
|
|
139
151
|
responseFormat?: LanguageModelV4CallOptions['responseFormat'];
|
|
140
152
|
experimental_sandbox?: SandboxSession;
|
|
141
153
|
}): AsyncGenerator<
|
|
142
154
|
StreamTextIteratorYieldValue,
|
|
143
|
-
LanguageModelV4Prompt
|
|
155
|
+
| LanguageModelV4Prompt
|
|
156
|
+
| StreamTextIteratorAbortedValue
|
|
157
|
+
| StreamTextIteratorErrorValue,
|
|
144
158
|
LanguageModelV4ToolResultPart[]
|
|
145
159
|
> {
|
|
146
160
|
let conversationPrompt = [...prompt]; // Create a mutable copy
|
|
@@ -158,6 +172,9 @@ export async function* streamTextIterator({
|
|
|
158
172
|
let stepNumber = 0;
|
|
159
173
|
let lastStep: StepResult<any, any> | undefined;
|
|
160
174
|
let lastStepWasToolCalls = false;
|
|
175
|
+
let wasAborted = false;
|
|
176
|
+
let terminalError: unknown;
|
|
177
|
+
let hasTerminalError = false;
|
|
161
178
|
|
|
162
179
|
// TODO(#12164): replace this AI-core telemetry bridge with a
|
|
163
180
|
// WorkflowAgent-specific typed dispatcher. `streamTextIterator` widens
|
|
@@ -319,20 +336,33 @@ export async function* streamTextIterator({
|
|
|
319
336
|
headers: currentGenerationSettings.headers,
|
|
320
337
|
} as never);
|
|
321
338
|
|
|
339
|
+
const streamStepResult = await doStreamStep(
|
|
340
|
+
conversationPrompt,
|
|
341
|
+
currentModel,
|
|
342
|
+
writable,
|
|
343
|
+
serializedTools,
|
|
344
|
+
{
|
|
345
|
+
...currentGenerationSettings,
|
|
346
|
+
toolChoice: currentToolChoice,
|
|
347
|
+
includeRawChunks,
|
|
348
|
+
timeoutAt,
|
|
349
|
+
repairToolCall,
|
|
350
|
+
responseFormat,
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
if (streamStepResult.aborted) {
|
|
355
|
+
wasAborted = true;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if ('terminalError' in streamStepResult) {
|
|
360
|
+
terminalError = streamStepResult.terminalError;
|
|
361
|
+
hasTerminalError = true;
|
|
362
|
+
}
|
|
363
|
+
|
|
322
364
|
const { toolCalls, finish, raw, providerExecutedToolResults } =
|
|
323
|
-
|
|
324
|
-
conversationPrompt,
|
|
325
|
-
currentModel,
|
|
326
|
-
writable,
|
|
327
|
-
serializedTools,
|
|
328
|
-
{
|
|
329
|
-
...currentGenerationSettings,
|
|
330
|
-
toolChoice: currentToolChoice,
|
|
331
|
-
includeRawChunks,
|
|
332
|
-
repairToolCall,
|
|
333
|
-
responseFormat,
|
|
334
|
-
},
|
|
335
|
-
);
|
|
365
|
+
streamStepResult;
|
|
336
366
|
// Reconstruct the full StepResult outside the step boundary so the
|
|
337
367
|
// durable event log doesn't carry StepResult's redundant copies (or the
|
|
338
368
|
// per-chunk snapshot the step used to return).
|
|
@@ -363,7 +393,12 @@ export async function* streamTextIterator({
|
|
|
363
393
|
|
|
364
394
|
const finishReason = finish?.finishReason;
|
|
365
395
|
|
|
366
|
-
if (
|
|
396
|
+
if (hasTerminalError) {
|
|
397
|
+
// The error crossed the durable step boundary as data. End the loop
|
|
398
|
+
// without throwing so WorkflowAgent can preserve the existing
|
|
399
|
+
// resolved-result contract and expose the original value.
|
|
400
|
+
done = true;
|
|
401
|
+
} else if (finishReason === 'tool-calls') {
|
|
367
402
|
lastStepWasToolCalls = true;
|
|
368
403
|
|
|
369
404
|
const textContent = step.content.filter(
|
|
@@ -488,6 +523,14 @@ export async function* streamTextIterator({
|
|
|
488
523
|
};
|
|
489
524
|
}
|
|
490
525
|
|
|
526
|
+
if (wasAborted) {
|
|
527
|
+
return { aborted: true, messages: conversationPrompt };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (hasTerminalError) {
|
|
531
|
+
return { error: terminalError, messages: conversationPrompt };
|
|
532
|
+
}
|
|
533
|
+
|
|
491
534
|
return conversationPrompt;
|
|
492
535
|
}
|
|
493
536
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { tool } from 'ai';
|
|
5
5
|
import { WorkflowAgent } from '../workflow-agent.js';
|
|
6
6
|
import { mockTextModel, mockSequenceModel } from '../providers/mock.js';
|
|
7
|
+
import { retryingModel } from './retrying-model.js';
|
|
7
8
|
import { createTestSandbox } from './test-sandbox.js';
|
|
8
9
|
import { FatalError, getWritable } from 'workflow';
|
|
9
10
|
import { z } from 'zod/v4';
|
|
@@ -47,6 +48,46 @@ export async function agentBasicE2e(prompt: string) {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export async function agentModelRetriesE2e() {
|
|
52
|
+
'use workflow';
|
|
53
|
+
const agent = new WorkflowAgent({
|
|
54
|
+
model: retryingModel(),
|
|
55
|
+
maxRetries: 2,
|
|
56
|
+
});
|
|
57
|
+
const result = await agent.stream({
|
|
58
|
+
messages: [{ role: 'user', content: 'retry the model call' }],
|
|
59
|
+
writable: getWritable(),
|
|
60
|
+
});
|
|
61
|
+
return result.steps.at(-1)?.text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function agentStreamErrorE2e() {
|
|
65
|
+
'use workflow';
|
|
66
|
+
const terminal = {
|
|
67
|
+
type: 'credential',
|
|
68
|
+
code: 'safe-terminal-classification',
|
|
69
|
+
};
|
|
70
|
+
const callbackErrors: unknown[] = [];
|
|
71
|
+
const agent = new WorkflowAgent({
|
|
72
|
+
model: mockSequenceModel([{ type: 'error', error: terminal }]),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await agent.stream({
|
|
76
|
+
messages: [{ role: 'user', content: 'trigger the terminal error' }],
|
|
77
|
+
writable: getWritable(),
|
|
78
|
+
onError: async ({ error }) => {
|
|
79
|
+
callbackErrors.push(error);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
error: result.error,
|
|
85
|
+
finishReason: result.finishReason,
|
|
86
|
+
stepCount: result.steps.length,
|
|
87
|
+
callbackErrors,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
50
91
|
export async function agentToolCallE2e(a: number, b: number) {
|
|
51
92
|
'use workflow';
|
|
52
93
|
const agent = new WorkflowAgent({
|