@ai-sdk/workflow 1.0.16 → 1.0.17
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 +9 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +173 -123
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/do-stream-step.ts +25 -121
- package/src/stream-text-iterator.ts +114 -4
- package/src/workflow-agent.ts +30 -12
- package/src/workflow-chat-transport.ts +94 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/workflow",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
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.2",
|
|
31
31
|
"@ai-sdk/provider-utils": "5.0.5",
|
|
32
|
-
"ai": "7.0.
|
|
32
|
+
"ai": "7.0.17"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "22.19.19",
|
package/src/do-stream-step.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type {
|
|
|
2
2
|
LanguageModelV4CallOptions,
|
|
3
3
|
LanguageModelV4Prompt,
|
|
4
4
|
} from '@ai-sdk/provider';
|
|
5
|
-
import type { Context } from '@ai-sdk/provider-utils';
|
|
6
5
|
import {
|
|
7
6
|
experimental_streamLanguageModelCall as streamModelCall,
|
|
8
7
|
gateway,
|
|
@@ -11,7 +10,6 @@ import {
|
|
|
11
10
|
type LanguageModel,
|
|
12
11
|
type LanguageModelUsage,
|
|
13
12
|
type ModelMessage,
|
|
14
|
-
type StepResult,
|
|
15
13
|
type StopCondition,
|
|
16
14
|
type ToolCallRepairFunction,
|
|
17
15
|
type ToolChoice,
|
|
@@ -57,13 +55,6 @@ export interface DoStreamStepOptions {
|
|
|
57
55
|
includeRawChunks?: boolean;
|
|
58
56
|
repairToolCall?: ToolCallRepairFunction<ToolSet>;
|
|
59
57
|
responseFormat?: LanguageModelV4CallOptions['responseFormat'];
|
|
60
|
-
runtimeContext?: Context;
|
|
61
|
-
toolsContext?: Record<string, Context | undefined>;
|
|
62
|
-
/**
|
|
63
|
-
* The step number for the returned StepResult. Defaults to 0 for direct
|
|
64
|
-
* callers; stream iterators pass their current index. See #15151.
|
|
65
|
-
*/
|
|
66
|
-
stepNumber?: number;
|
|
67
58
|
}
|
|
68
59
|
|
|
69
60
|
/**
|
|
@@ -91,6 +82,23 @@ export interface StreamFinish {
|
|
|
91
82
|
providerMetadata?: Record<string, unknown>;
|
|
92
83
|
}
|
|
93
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
|
+
|
|
94
102
|
export async function doStreamStep(
|
|
95
103
|
conversationPrompt: LanguageModelV4Prompt,
|
|
96
104
|
modelInit: LanguageModel,
|
|
@@ -100,8 +108,7 @@ export async function doStreamStep(
|
|
|
100
108
|
): Promise<{
|
|
101
109
|
toolCalls: ParsedToolCall[];
|
|
102
110
|
finish: StreamFinish | undefined;
|
|
103
|
-
|
|
104
|
-
chunks?: unknown[];
|
|
111
|
+
raw: DoStreamStepRawResult;
|
|
105
112
|
providerExecutedToolResults: Map<string, ProviderExecutedToolResult>;
|
|
106
113
|
}> {
|
|
107
114
|
'use step';
|
|
@@ -155,10 +162,9 @@ export async function doStreamStep(
|
|
|
155
162
|
>();
|
|
156
163
|
let finish: StreamFinish | undefined;
|
|
157
164
|
|
|
158
|
-
//
|
|
165
|
+
// Minimal aggregation — only what buildStepResult needs outside the step.
|
|
159
166
|
let text = '';
|
|
160
167
|
const reasoningParts: Array<{ text: string }> = [];
|
|
161
|
-
const chunks: unknown[] = [];
|
|
162
168
|
let responseMetadata:
|
|
163
169
|
| { id?: string; timestamp?: Date; modelId?: string }
|
|
164
170
|
| undefined;
|
|
@@ -169,14 +175,6 @@ export async function doStreamStep(
|
|
|
169
175
|
|
|
170
176
|
try {
|
|
171
177
|
for await (const part of modelStream) {
|
|
172
|
-
if (
|
|
173
|
-
part.type !== 'model-call-start' &&
|
|
174
|
-
part.type !== 'model-call-end' &&
|
|
175
|
-
part.type !== 'model-call-response-metadata'
|
|
176
|
-
) {
|
|
177
|
-
chunks.push(part);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
178
|
switch (part.type) {
|
|
181
179
|
case 'text-delta':
|
|
182
180
|
text += part.text;
|
|
@@ -253,109 +251,15 @@ export async function doStreamStep(
|
|
|
253
251
|
writer?.releaseLock();
|
|
254
252
|
}
|
|
255
253
|
|
|
256
|
-
// Build StepResult
|
|
257
|
-
const reasoningText = reasoningParts.map(r => r.text).join('') || undefined;
|
|
258
|
-
|
|
259
|
-
const step: StepResult<ToolSet, any> = {
|
|
260
|
-
callId: 'workflow-agent',
|
|
261
|
-
stepNumber: options?.stepNumber ?? 0,
|
|
262
|
-
model: {
|
|
263
|
-
provider: responseMetadata?.modelId?.split(':')[0] ?? 'unknown',
|
|
264
|
-
modelId: responseMetadata?.modelId ?? 'unknown',
|
|
265
|
-
},
|
|
266
|
-
functionId: undefined,
|
|
267
|
-
metadata: undefined,
|
|
268
|
-
runtimeContext: options?.runtimeContext ?? {},
|
|
269
|
-
toolsContext: options?.toolsContext ?? {},
|
|
270
|
-
content: [
|
|
271
|
-
...(text ? [{ type: 'text' as const, text }] : []),
|
|
272
|
-
...toolCalls
|
|
273
|
-
.filter(tc => !tc.invalid)
|
|
274
|
-
.map(tc => ({
|
|
275
|
-
type: 'tool-call' as const,
|
|
276
|
-
toolCallId: tc.toolCallId,
|
|
277
|
-
toolName: tc.toolName,
|
|
278
|
-
input: tc.input,
|
|
279
|
-
...(tc.dynamic ? { dynamic: true as const } : {}),
|
|
280
|
-
})),
|
|
281
|
-
],
|
|
282
|
-
text,
|
|
283
|
-
reasoning: reasoningParts.map(r => ({
|
|
284
|
-
type: 'reasoning' as const,
|
|
285
|
-
text: r.text,
|
|
286
|
-
})),
|
|
287
|
-
reasoningText,
|
|
288
|
-
files: [],
|
|
289
|
-
sources: [],
|
|
290
|
-
toolCalls: toolCalls
|
|
291
|
-
.filter(tc => !tc.invalid)
|
|
292
|
-
.map(tc => ({
|
|
293
|
-
type: 'tool-call' as const,
|
|
294
|
-
toolCallId: tc.toolCallId,
|
|
295
|
-
toolName: tc.toolName,
|
|
296
|
-
input: tc.input,
|
|
297
|
-
...(tc.dynamic ? { dynamic: true as const } : {}),
|
|
298
|
-
})),
|
|
299
|
-
staticToolCalls: [],
|
|
300
|
-
dynamicToolCalls: toolCalls
|
|
301
|
-
.filter(tc => !tc.invalid && tc.dynamic)
|
|
302
|
-
.map(tc => ({
|
|
303
|
-
type: 'tool-call' as const,
|
|
304
|
-
toolCallId: tc.toolCallId,
|
|
305
|
-
toolName: tc.toolName,
|
|
306
|
-
input: tc.input,
|
|
307
|
-
dynamic: true as const,
|
|
308
|
-
})),
|
|
309
|
-
toolResults: [],
|
|
310
|
-
staticToolResults: [],
|
|
311
|
-
dynamicToolResults: [],
|
|
312
|
-
finishReason: finish?.finishReason ?? 'other',
|
|
313
|
-
rawFinishReason: finish?.rawFinishReason,
|
|
314
|
-
usage:
|
|
315
|
-
finish?.usage ??
|
|
316
|
-
({
|
|
317
|
-
inputTokens: 0,
|
|
318
|
-
inputTokenDetails: {
|
|
319
|
-
noCacheTokens: undefined,
|
|
320
|
-
cacheReadTokens: undefined,
|
|
321
|
-
cacheWriteTokens: undefined,
|
|
322
|
-
},
|
|
323
|
-
outputTokens: 0,
|
|
324
|
-
outputTokenDetails: {
|
|
325
|
-
textTokens: undefined,
|
|
326
|
-
reasoningTokens: undefined,
|
|
327
|
-
},
|
|
328
|
-
totalTokens: 0,
|
|
329
|
-
} as LanguageModelUsage),
|
|
330
|
-
performance: {
|
|
331
|
-
effectiveOutputTokensPerSecond: 0,
|
|
332
|
-
outputTokensPerSecond: undefined,
|
|
333
|
-
inputTokensPerSecond: undefined,
|
|
334
|
-
effectiveTotalTokensPerSecond: 0,
|
|
335
|
-
stepTimeMs: 0,
|
|
336
|
-
responseTimeMs: 0,
|
|
337
|
-
toolExecutionMs: {},
|
|
338
|
-
timeToFirstOutputMs: undefined,
|
|
339
|
-
},
|
|
340
|
-
warnings,
|
|
341
|
-
request: {
|
|
342
|
-
body: '',
|
|
343
|
-
messages: [], // TODO implement step request messages
|
|
344
|
-
},
|
|
345
|
-
response: {
|
|
346
|
-
id: responseMetadata?.id ?? 'unknown',
|
|
347
|
-
timestamp: responseMetadata?.timestamp ?? new Date(),
|
|
348
|
-
modelId: responseMetadata?.modelId ?? 'unknown',
|
|
349
|
-
messages: [],
|
|
350
|
-
},
|
|
351
|
-
providerMetadata: finish?.providerMetadata ?? {},
|
|
352
|
-
} as StepResult<ToolSet, any>;
|
|
353
|
-
|
|
354
254
|
return {
|
|
355
255
|
toolCalls,
|
|
356
256
|
finish,
|
|
357
|
-
|
|
358
|
-
|
|
257
|
+
raw: {
|
|
258
|
+
text,
|
|
259
|
+
reasoning: reasoningParts,
|
|
260
|
+
responseMetadata,
|
|
261
|
+
warnings,
|
|
262
|
+
},
|
|
359
263
|
providerExecutedToolResults,
|
|
360
264
|
};
|
|
361
265
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type Experimental_LanguageModelStreamPart as ModelCallStreamPart,
|
|
10
10
|
type Experimental_SandboxSession as SandboxSession,
|
|
11
11
|
type LanguageModel,
|
|
12
|
+
type LanguageModelUsage,
|
|
12
13
|
type ModelMessage,
|
|
13
14
|
type StepResult,
|
|
14
15
|
type ToolCallRepairFunction,
|
|
@@ -17,10 +18,12 @@ import {
|
|
|
17
18
|
} from 'ai';
|
|
18
19
|
import { createRestrictedTelemetryDispatcher } from 'ai/internal';
|
|
19
20
|
import {
|
|
21
|
+
type DoStreamStepRawResult,
|
|
20
22
|
doStreamStep,
|
|
21
23
|
type ModelStopCondition,
|
|
22
24
|
type ParsedToolCall,
|
|
23
25
|
type ProviderExecutedToolResult,
|
|
26
|
+
type StreamFinish,
|
|
24
27
|
} from './do-stream-step.js';
|
|
25
28
|
import { serializeToolSet } from './serializable-schema.js';
|
|
26
29
|
import type {
|
|
@@ -344,7 +347,7 @@ export async function* streamTextIterator({
|
|
|
344
347
|
headers: currentGenerationSettings.headers,
|
|
345
348
|
} as never);
|
|
346
349
|
|
|
347
|
-
const { toolCalls, finish,
|
|
350
|
+
const { toolCalls, finish, raw, providerExecutedToolResults } =
|
|
348
351
|
await doStreamStep(
|
|
349
352
|
conversationPrompt,
|
|
350
353
|
currentModel,
|
|
@@ -356,11 +359,16 @@ export async function* streamTextIterator({
|
|
|
356
359
|
includeRawChunks,
|
|
357
360
|
repairToolCall,
|
|
358
361
|
responseFormat,
|
|
359
|
-
runtimeContext: currentRuntimeContext,
|
|
360
|
-
toolsContext: currentToolsContext,
|
|
361
|
-
stepNumber,
|
|
362
362
|
},
|
|
363
363
|
);
|
|
364
|
+
// Reconstruct the full StepResult outside the step boundary so the
|
|
365
|
+
// durable event log doesn't carry StepResult's redundant copies (or the
|
|
366
|
+
// per-chunk snapshot the step used to return).
|
|
367
|
+
const step = buildStepResult(raw, toolCalls, finish, {
|
|
368
|
+
stepNumber,
|
|
369
|
+
runtimeContext: currentRuntimeContext,
|
|
370
|
+
toolsContext: currentToolsContext,
|
|
371
|
+
});
|
|
364
372
|
|
|
365
373
|
await telemetryDispatcher.onLanguageModelCallEnd?.({
|
|
366
374
|
callId: step.callId,
|
|
@@ -514,6 +522,108 @@ function normalizeStepForTelemetry(step: StepResult<any, any>) {
|
|
|
514
522
|
};
|
|
515
523
|
}
|
|
516
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Reconstruct a full `StepResult` from the minimal aggregates returned by
|
|
527
|
+
* `doStreamStep`. Runs outside the step boundary so StepResult's redundant
|
|
528
|
+
* fields (duplicate tool-call lists, `content`, `reasoningText`, the
|
|
529
|
+
* always-empty `*ToolResults` arrays) and the per-chunk snapshot don't cross
|
|
530
|
+
* it. The shape matches what the AI SDK's `streamText` exposes to callers.
|
|
531
|
+
*/
|
|
532
|
+
function buildStepResult(
|
|
533
|
+
raw: DoStreamStepRawResult,
|
|
534
|
+
toolCalls: ParsedToolCall[],
|
|
535
|
+
finish: StreamFinish | undefined,
|
|
536
|
+
opts: {
|
|
537
|
+
stepNumber: number;
|
|
538
|
+
runtimeContext: Context;
|
|
539
|
+
toolsContext: Record<string, Context | undefined>;
|
|
540
|
+
},
|
|
541
|
+
): StepResult<ToolSet, any> {
|
|
542
|
+
const { text, reasoning: reasoningParts, responseMetadata, warnings } = raw;
|
|
543
|
+
const reasoningText = reasoningParts.map(r => r.text).join('') || undefined;
|
|
544
|
+
|
|
545
|
+
const validToolCalls = toolCalls
|
|
546
|
+
.filter(tc => !tc.invalid)
|
|
547
|
+
.map(tc => ({
|
|
548
|
+
type: 'tool-call' as const,
|
|
549
|
+
toolCallId: tc.toolCallId,
|
|
550
|
+
toolName: tc.toolName,
|
|
551
|
+
input: tc.input,
|
|
552
|
+
...(tc.dynamic ? { dynamic: true as const } : {}),
|
|
553
|
+
}));
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
callId: 'workflow-agent',
|
|
557
|
+
stepNumber: opts.stepNumber,
|
|
558
|
+
model: {
|
|
559
|
+
provider: responseMetadata?.modelId?.split(':')[0] ?? 'unknown',
|
|
560
|
+
modelId: responseMetadata?.modelId ?? 'unknown',
|
|
561
|
+
},
|
|
562
|
+
functionId: undefined,
|
|
563
|
+
metadata: undefined,
|
|
564
|
+
runtimeContext: opts.runtimeContext ?? {},
|
|
565
|
+
toolsContext: opts.toolsContext ?? {},
|
|
566
|
+
content: [
|
|
567
|
+
...(text ? [{ type: 'text' as const, text }] : []),
|
|
568
|
+
...validToolCalls,
|
|
569
|
+
],
|
|
570
|
+
text,
|
|
571
|
+
reasoning: reasoningParts.map(r => ({
|
|
572
|
+
type: 'reasoning' as const,
|
|
573
|
+
text: r.text,
|
|
574
|
+
})),
|
|
575
|
+
reasoningText,
|
|
576
|
+
files: [],
|
|
577
|
+
sources: [],
|
|
578
|
+
toolCalls: validToolCalls,
|
|
579
|
+
staticToolCalls: [],
|
|
580
|
+
dynamicToolCalls: validToolCalls.filter(tc => tc.dynamic),
|
|
581
|
+
toolResults: [],
|
|
582
|
+
staticToolResults: [],
|
|
583
|
+
dynamicToolResults: [],
|
|
584
|
+
finishReason: finish?.finishReason ?? 'other',
|
|
585
|
+
rawFinishReason: finish?.rawFinishReason,
|
|
586
|
+
usage:
|
|
587
|
+
finish?.usage ??
|
|
588
|
+
({
|
|
589
|
+
inputTokens: 0,
|
|
590
|
+
inputTokenDetails: {
|
|
591
|
+
noCacheTokens: undefined,
|
|
592
|
+
cacheReadTokens: undefined,
|
|
593
|
+
cacheWriteTokens: undefined,
|
|
594
|
+
},
|
|
595
|
+
outputTokens: 0,
|
|
596
|
+
outputTokenDetails: {
|
|
597
|
+
textTokens: undefined,
|
|
598
|
+
reasoningTokens: undefined,
|
|
599
|
+
},
|
|
600
|
+
totalTokens: 0,
|
|
601
|
+
} as LanguageModelUsage),
|
|
602
|
+
performance: {
|
|
603
|
+
effectiveOutputTokensPerSecond: 0,
|
|
604
|
+
outputTokensPerSecond: undefined,
|
|
605
|
+
inputTokensPerSecond: undefined,
|
|
606
|
+
effectiveTotalTokensPerSecond: 0,
|
|
607
|
+
stepTimeMs: 0,
|
|
608
|
+
responseTimeMs: 0,
|
|
609
|
+
toolExecutionMs: {},
|
|
610
|
+
timeToFirstOutputMs: undefined,
|
|
611
|
+
},
|
|
612
|
+
warnings,
|
|
613
|
+
request: {
|
|
614
|
+
body: '',
|
|
615
|
+
messages: [], // TODO implement step request messages
|
|
616
|
+
},
|
|
617
|
+
response: {
|
|
618
|
+
id: responseMetadata?.id ?? 'unknown',
|
|
619
|
+
timestamp: responseMetadata?.timestamp ?? new Date(),
|
|
620
|
+
modelId: responseMetadata?.modelId ?? 'unknown',
|
|
621
|
+
messages: [],
|
|
622
|
+
},
|
|
623
|
+
providerMetadata: finish?.providerMetadata ?? {},
|
|
624
|
+
} as StepResult<ToolSet, any>;
|
|
625
|
+
}
|
|
626
|
+
|
|
517
627
|
/**
|
|
518
628
|
* Strip OpenAI's itemId from providerMetadata (requires reasoning items we don't preserve).
|
|
519
629
|
* Preserves all other provider metadata (e.g., Gemini's thoughtSignature).
|
package/src/workflow-agent.ts
CHANGED
|
@@ -1148,6 +1148,16 @@ export interface WorkflowAgentStreamResult<
|
|
|
1148
1148
|
*/
|
|
1149
1149
|
toolResults: ToolResult[];
|
|
1150
1150
|
|
|
1151
|
+
/**
|
|
1152
|
+
* The finish reason from the last step.
|
|
1153
|
+
*/
|
|
1154
|
+
finishReason: FinishReason;
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* The total token usage across all steps.
|
|
1158
|
+
*/
|
|
1159
|
+
totalUsage: LanguageModelUsage;
|
|
1160
|
+
|
|
1151
1161
|
/**
|
|
1152
1162
|
* The generated structured output. It uses the `output` specification.
|
|
1153
1163
|
* Only available when `output` is specified.
|
|
@@ -2062,6 +2072,8 @@ export class WorkflowAgent<
|
|
|
2062
2072
|
steps,
|
|
2063
2073
|
toolCalls: [],
|
|
2064
2074
|
toolResults: [],
|
|
2075
|
+
finishReason: 'other',
|
|
2076
|
+
totalUsage: aggregateUsage(steps),
|
|
2065
2077
|
output: undefined as OUTPUT,
|
|
2066
2078
|
};
|
|
2067
2079
|
}
|
|
@@ -2264,15 +2276,16 @@ export class WorkflowAgent<
|
|
|
2264
2276
|
}
|
|
2265
2277
|
|
|
2266
2278
|
const messages = iterMessages as unknown as ModelMessage[];
|
|
2279
|
+
const lastStep = steps[steps.length - 1];
|
|
2280
|
+
const totalUsage = aggregateUsage(steps);
|
|
2281
|
+
const finishReason = lastStep?.finishReason ?? 'other';
|
|
2267
2282
|
|
|
2268
2283
|
if (mergedOnEnd && !wasAborted) {
|
|
2269
|
-
const lastStep = steps[steps.length - 1];
|
|
2270
|
-
const totalUsage = aggregateUsage(steps);
|
|
2271
2284
|
await mergedOnEnd({
|
|
2272
2285
|
steps,
|
|
2273
2286
|
messages,
|
|
2274
2287
|
text: lastStep?.text ?? '',
|
|
2275
|
-
finishReason
|
|
2288
|
+
finishReason,
|
|
2276
2289
|
usage: totalUsage,
|
|
2277
2290
|
totalUsage,
|
|
2278
2291
|
runtimeContext,
|
|
@@ -2283,10 +2296,10 @@ export class WorkflowAgent<
|
|
|
2283
2296
|
}
|
|
2284
2297
|
if (!wasAborted && steps.length > 0) {
|
|
2285
2298
|
const telemetrySteps = steps.map(normalizeStepForTelemetry);
|
|
2286
|
-
const
|
|
2287
|
-
|
|
2299
|
+
const lastTelemetryStep =
|
|
2300
|
+
telemetrySteps[telemetrySteps.length - 1];
|
|
2288
2301
|
await telemetryDispatcher.onEnd?.({
|
|
2289
|
-
...
|
|
2302
|
+
...lastTelemetryStep,
|
|
2290
2303
|
steps: telemetrySteps,
|
|
2291
2304
|
usage: totalUsage,
|
|
2292
2305
|
totalUsage,
|
|
@@ -2327,6 +2340,8 @@ export class WorkflowAgent<
|
|
|
2327
2340
|
steps,
|
|
2328
2341
|
toolCalls: allToolCalls,
|
|
2329
2342
|
toolResults: allToolResults,
|
|
2343
|
+
finishReason,
|
|
2344
|
+
totalUsage,
|
|
2330
2345
|
output: undefined as OUTPUT,
|
|
2331
2346
|
};
|
|
2332
2347
|
}
|
|
@@ -2491,15 +2506,17 @@ export class WorkflowAgent<
|
|
|
2491
2506
|
}
|
|
2492
2507
|
}
|
|
2493
2508
|
|
|
2509
|
+
const lastStep = steps[steps.length - 1];
|
|
2510
|
+
const totalUsage = aggregateUsage(steps);
|
|
2511
|
+
const finishReason = lastStep?.finishReason ?? 'other';
|
|
2512
|
+
|
|
2494
2513
|
// Call onEnd callback if provided (always call, even on errors, but not on abort)
|
|
2495
2514
|
if (mergedOnEnd && !wasAborted) {
|
|
2496
|
-
const lastStep = steps[steps.length - 1];
|
|
2497
|
-
const totalUsage = aggregateUsage(steps);
|
|
2498
2515
|
await mergedOnEnd({
|
|
2499
2516
|
steps,
|
|
2500
2517
|
messages: messages as ModelMessage[],
|
|
2501
2518
|
text: lastStep?.text ?? '',
|
|
2502
|
-
finishReason
|
|
2519
|
+
finishReason,
|
|
2503
2520
|
usage: totalUsage,
|
|
2504
2521
|
totalUsage,
|
|
2505
2522
|
runtimeContext,
|
|
@@ -2509,10 +2526,9 @@ export class WorkflowAgent<
|
|
|
2509
2526
|
}
|
|
2510
2527
|
if (!wasAborted && steps.length > 0) {
|
|
2511
2528
|
const telemetrySteps = steps.map(normalizeStepForTelemetry);
|
|
2512
|
-
const
|
|
2513
|
-
const totalUsage = aggregateUsage(steps);
|
|
2529
|
+
const lastTelemetryStep = telemetrySteps[telemetrySteps.length - 1];
|
|
2514
2530
|
await telemetryDispatcher.onEnd?.({
|
|
2515
|
-
...
|
|
2531
|
+
...lastTelemetryStep,
|
|
2516
2532
|
steps: telemetrySteps,
|
|
2517
2533
|
usage: totalUsage,
|
|
2518
2534
|
totalUsage,
|
|
@@ -2546,6 +2562,8 @@ export class WorkflowAgent<
|
|
|
2546
2562
|
steps,
|
|
2547
2563
|
toolCalls: lastStepToolCalls,
|
|
2548
2564
|
toolResults: lastStepToolResults,
|
|
2565
|
+
finishReason,
|
|
2566
|
+
totalUsage,
|
|
2549
2567
|
output: experimentalOutput,
|
|
2550
2568
|
};
|
|
2551
2569
|
}
|
|
@@ -15,6 +15,87 @@ import {
|
|
|
15
15
|
import { createAsyncIterableStream } from 'ai/internal';
|
|
16
16
|
import { normalizeUIMessageStreamParts } from './normalize-ui-message-stream.js';
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Tracks `*-start` chunks the client has accepted so we can drop deltas/ends
|
|
20
|
+
* that refer to a part whose start was emitted before the resume cursor.
|
|
21
|
+
*
|
|
22
|
+
* AI SDK's UI stream processor throws on `text-delta`/`reasoning-delta`/
|
|
23
|
+
* `tool-input-delta` (and the matching `*-end`) when the start chunk for that
|
|
24
|
+
* id was never observed, and on tool output/approval chunks when no tool part
|
|
25
|
+
* exists for the call id. A negative `startIndex` on a flat chunk stream can
|
|
26
|
+
* easily land mid-part, so without this guard the client crashes on resume.
|
|
27
|
+
*
|
|
28
|
+
* A tool part is established by `tool-input-start` OR by a self-contained
|
|
29
|
+
* `tool-input-available`/`tool-input-error` chunk (the AI SDK creates the
|
|
30
|
+
* part from those directly), so all three mark the call id as seen.
|
|
31
|
+
*
|
|
32
|
+
* This is a best-effort safety net — it preserves only the parts that the
|
|
33
|
+
* resumed window includes a `*-start` for. Server-side rewinding to a step
|
|
34
|
+
* boundary is the proper fix when you want the full message preserved.
|
|
35
|
+
*/
|
|
36
|
+
type OrphanFilter = {
|
|
37
|
+
shouldDrop: (chunk: UIMessageChunk) => boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function createOrphanFilter(): OrphanFilter {
|
|
41
|
+
const seenStartedIds = new Set<string>();
|
|
42
|
+
const seenStartedToolCallIds = new Set<string>();
|
|
43
|
+
let warnedOnce = false;
|
|
44
|
+
|
|
45
|
+
function warnOnce(orphanKind: string, orphanRef: string) {
|
|
46
|
+
if (warnedOnce) return;
|
|
47
|
+
warnedOnce = true;
|
|
48
|
+
console.warn(
|
|
49
|
+
'[WorkflowChatTransport] Dropping orphan UI chunk ' +
|
|
50
|
+
`(${orphanKind} for id "${orphanRef}") on resume — ` +
|
|
51
|
+
'the resume position landed mid-part. The dropped chunk(s) ' +
|
|
52
|
+
"reference a part whose start chunk wasn't in the resumed " +
|
|
53
|
+
'window. To preserve the full message, configure your ' +
|
|
54
|
+
'stream endpoint to rewind to a step boundary before ' +
|
|
55
|
+
'returning the readable. See: ' +
|
|
56
|
+
'https://workflow.dev/docs/ai/resumable-streams#mid-part-resumes',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shouldDrop(chunk: UIMessageChunk): boolean {
|
|
61
|
+
switch (chunk.type) {
|
|
62
|
+
case 'text-start':
|
|
63
|
+
case 'reasoning-start':
|
|
64
|
+
seenStartedIds.add(chunk.id);
|
|
65
|
+
return false;
|
|
66
|
+
case 'tool-input-start':
|
|
67
|
+
// `tool-input-available` / `tool-input-error` are self-contained: the
|
|
68
|
+
// AI SDK creates the tool part from them directly (non-streamed tool
|
|
69
|
+
// calls are emitted as a bare `tool-input-available`), so they must
|
|
70
|
+
// never be dropped. They also carry the full input, so they recover a
|
|
71
|
+
// tool call whose `tool-input-start` fell outside the resumed window.
|
|
72
|
+
case 'tool-input-available':
|
|
73
|
+
case 'tool-input-error':
|
|
74
|
+
seenStartedToolCallIds.add(chunk.toolCallId);
|
|
75
|
+
return false;
|
|
76
|
+
case 'text-delta':
|
|
77
|
+
case 'text-end':
|
|
78
|
+
case 'reasoning-delta':
|
|
79
|
+
case 'reasoning-end':
|
|
80
|
+
if (seenStartedIds.has(chunk.id)) return false;
|
|
81
|
+
warnOnce(chunk.type, chunk.id);
|
|
82
|
+
return true;
|
|
83
|
+
case 'tool-input-delta':
|
|
84
|
+
case 'tool-approval-request':
|
|
85
|
+
case 'tool-output-available':
|
|
86
|
+
case 'tool-output-error':
|
|
87
|
+
case 'tool-output-denied':
|
|
88
|
+
if (seenStartedToolCallIds.has(chunk.toolCallId)) return false;
|
|
89
|
+
warnOnce(chunk.type, chunk.toolCallId);
|
|
90
|
+
return true;
|
|
91
|
+
default:
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { shouldDrop };
|
|
97
|
+
}
|
|
98
|
+
|
|
18
99
|
export interface SendMessagesOptions<UI_MESSAGE extends UIMessage> {
|
|
19
100
|
trigger: 'submit-message' | 'regenerate-message';
|
|
20
101
|
chatId: string;
|
|
@@ -339,6 +420,17 @@ export class WorkflowChatTransport<
|
|
|
339
420
|
// the incremental chunkIndex which would be wrong.
|
|
340
421
|
let replayFromStart = false;
|
|
341
422
|
|
|
423
|
+
// When resuming with a negative startIndex, the resolved chunk can land in
|
|
424
|
+
// the middle of a `*-start` / `*-delta` / `*-end` sequence, which crashes
|
|
425
|
+
// the AI SDK UI stream processor. The orphan filter drops chunks whose
|
|
426
|
+
// start chunk was emitted before the resume window. Only activated for
|
|
427
|
+
// negative resumes — non-negative startIndex is the caller's explicit
|
|
428
|
+
// choice and we trust them. See: https://github.com/vercel/workflow/issues/1835
|
|
429
|
+
const orphanFilter =
|
|
430
|
+
useExplicitStartIndex && explicitStartIndex < 0
|
|
431
|
+
? createOrphanFilter()
|
|
432
|
+
: null;
|
|
433
|
+
|
|
342
434
|
while (!gotFinish) {
|
|
343
435
|
const startIndex = useExplicitStartIndex
|
|
344
436
|
? explicitStartIndex
|
|
@@ -404,6 +496,8 @@ export class WorkflowChatTransport<
|
|
|
404
496
|
|
|
405
497
|
chunkIndex++;
|
|
406
498
|
|
|
499
|
+
if (orphanFilter?.shouldDrop(chunk.value)) continue;
|
|
500
|
+
|
|
407
501
|
yield chunk.value;
|
|
408
502
|
|
|
409
503
|
if (chunk.value.type === 'finish') {
|