@ai-sdk/workflow 0.0.0-bf6e4b15-20260402200305

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.
@@ -0,0 +1,1647 @@
1
+ import type {
2
+ JSONValue,
3
+ LanguageModelV4CallOptions,
4
+ LanguageModelV4Prompt,
5
+ LanguageModelV4StreamPart,
6
+ LanguageModelV4ToolResultPart,
7
+ SharedV4ProviderOptions,
8
+ } from '@ai-sdk/provider';
9
+ import {
10
+ asSchema,
11
+ type Experimental_ModelCallStreamPart as ModelCallStreamPart,
12
+ type FinishReason,
13
+ type LanguageModelResponseMetadata,
14
+ type LanguageModelUsage,
15
+ type ModelMessage,
16
+ Output,
17
+ type StepResult,
18
+ type StopCondition,
19
+ type StreamTextOnStepFinishCallback,
20
+ type SystemModelMessage,
21
+ type ToolCallRepairFunction,
22
+ type ToolChoice,
23
+ type ToolSet,
24
+ type UIMessage,
25
+ } from 'ai';
26
+ import {
27
+ convertToLanguageModelPrompt,
28
+ mergeAbortSignals,
29
+ standardizePrompt,
30
+ } from 'ai/internal';
31
+ import { mergeCallbacks } from 'ai/internal';
32
+ import { recordSpan } from './telemetry.js';
33
+ import { streamTextIterator } from './stream-text-iterator.js';
34
+ import type { CompatibleLanguageModel } from './types.js';
35
+
36
+ // Re-export for consumers
37
+ export type { CompatibleLanguageModel } from './types.js';
38
+
39
+ /**
40
+ * Infer the type of the tools of a workflow agent.
41
+ */
42
+ export type InferWorkflowAgentTools<WORKFLOW_AGENT> =
43
+ WORKFLOW_AGENT extends WorkflowAgent<infer TOOLS> ? TOOLS : never;
44
+
45
+ /**
46
+ * Infer the UI message type of a workflow agent.
47
+ */
48
+ export type InferWorkflowAgentUIMessage<
49
+ WORKFLOW_AGENT,
50
+ MESSAGE_METADATA = unknown,
51
+ > = UIMessage<MESSAGE_METADATA>;
52
+
53
+ /**
54
+ * Re-export the Output helper for structured output specifications.
55
+ * Use `Output.object({ schema })` for structured output or `Output.text()` for text output.
56
+ */
57
+ export { Output };
58
+
59
+ /**
60
+ * Output specification interface for structured outputs.
61
+ * Use `Output.object({ schema })` or `Output.text()` to create an output specification.
62
+ */
63
+ export interface OutputSpecification<OUTPUT, PARTIAL> {
64
+ readonly name: string;
65
+ responseFormat: PromiseLike<LanguageModelV4CallOptions['responseFormat']>;
66
+ parsePartialOutput(options: {
67
+ text: string;
68
+ }): Promise<{ partial: PARTIAL } | undefined>;
69
+ parseCompleteOutput(
70
+ options: { text: string },
71
+ context: {
72
+ response: LanguageModelResponseMetadata;
73
+ usage: LanguageModelUsage;
74
+ finishReason: FinishReason;
75
+ },
76
+ ): Promise<OUTPUT>;
77
+ }
78
+
79
+ /**
80
+ * Provider-specific options type. This is equivalent to SharedV4ProviderOptions from @ai-sdk/provider.
81
+ */
82
+ export type ProviderOptions = SharedV4ProviderOptions;
83
+
84
+ /**
85
+ * Telemetry settings for observability.
86
+ */
87
+ export interface TelemetrySettings {
88
+ /**
89
+ * Enable or disable telemetry. Defaults to true.
90
+ */
91
+ isEnabled?: boolean;
92
+
93
+ /**
94
+ * Identifier for this function. Used to group telemetry data by function.
95
+ */
96
+ functionId?: string;
97
+
98
+ /**
99
+ * Additional information to include in the telemetry data.
100
+ */
101
+ metadata?: Record<
102
+ string,
103
+ | string
104
+ | number
105
+ | boolean
106
+ | Array<string | number | boolean>
107
+ | null
108
+ | undefined
109
+ >;
110
+
111
+ /**
112
+ * Enable or disable input recording. Enabled by default.
113
+ *
114
+ * You might want to disable input recording to avoid recording sensitive
115
+ * information, to reduce data transfers, or to increase performance.
116
+ */
117
+ recordInputs?: boolean;
118
+
119
+ /**
120
+ * Enable or disable output recording. Enabled by default.
121
+ *
122
+ * You might want to disable output recording to avoid recording sensitive
123
+ * information, to reduce data transfers, or to increase performance.
124
+ */
125
+ recordOutputs?: boolean;
126
+
127
+ /**
128
+ * Custom tracer for the telemetry.
129
+ */
130
+ tracer?: unknown;
131
+ }
132
+
133
+ /**
134
+ * A transformation that is applied to the stream.
135
+ */
136
+ export type StreamTextTransform<TTools extends ToolSet> = (options: {
137
+ tools: TTools;
138
+ stopStream: () => void;
139
+ }) => TransformStream<LanguageModelV4StreamPart, LanguageModelV4StreamPart>;
140
+
141
+ /**
142
+ * Function to repair a tool call that failed to parse.
143
+ * Re-exported from the AI SDK core.
144
+ */
145
+ export type { ToolCallRepairFunction } from 'ai';
146
+
147
+ /**
148
+ * Custom download function for URLs.
149
+ * The function receives an array of URLs with information about whether
150
+ * the model supports them directly.
151
+ */
152
+ export type DownloadFunction = (
153
+ options: {
154
+ url: URL;
155
+ isUrlSupportedByModel: boolean;
156
+ }[],
157
+ ) => PromiseLike<
158
+ ({ data: Uint8Array; mediaType: string | undefined } | null)[]
159
+ >;
160
+
161
+ /**
162
+ * Generation settings that can be passed to the model.
163
+ * These map directly to LanguageModelV4CallOptions.
164
+ */
165
+ export interface GenerationSettings {
166
+ /**
167
+ * Maximum number of tokens to generate.
168
+ */
169
+ maxOutputTokens?: number;
170
+
171
+ /**
172
+ * Temperature setting. The range depends on the provider and model.
173
+ * It is recommended to set either `temperature` or `topP`, but not both.
174
+ */
175
+ temperature?: number;
176
+
177
+ /**
178
+ * Nucleus sampling. This is a number between 0 and 1.
179
+ * E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered.
180
+ * It is recommended to set either `temperature` or `topP`, but not both.
181
+ */
182
+ topP?: number;
183
+
184
+ /**
185
+ * Only sample from the top K options for each subsequent token.
186
+ * Used to remove "long tail" low probability responses.
187
+ * Recommended for advanced use cases only. You usually only need to use temperature.
188
+ */
189
+ topK?: number;
190
+
191
+ /**
192
+ * Presence penalty setting. It affects the likelihood of the model to
193
+ * repeat information that is already in the prompt.
194
+ * The presence penalty is a number between -1 (increase repetition)
195
+ * and 1 (maximum penalty, decrease repetition). 0 means no penalty.
196
+ */
197
+ presencePenalty?: number;
198
+
199
+ /**
200
+ * Frequency penalty setting. It affects the likelihood of the model
201
+ * to repeatedly use the same words or phrases.
202
+ * The frequency penalty is a number between -1 (increase repetition)
203
+ * and 1 (maximum penalty, decrease repetition). 0 means no penalty.
204
+ */
205
+ frequencyPenalty?: number;
206
+
207
+ /**
208
+ * Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated.
209
+ * Providers may have limits on the number of stop sequences.
210
+ */
211
+ stopSequences?: string[];
212
+
213
+ /**
214
+ * The seed (integer) to use for random sampling. If set and supported
215
+ * by the model, calls will generate deterministic results.
216
+ */
217
+ seed?: number;
218
+
219
+ /**
220
+ * Maximum number of retries. Set to 0 to disable retries.
221
+ * Note: In workflow context, retries are typically handled by the workflow step mechanism.
222
+ * @default 2
223
+ */
224
+ maxRetries?: number;
225
+
226
+ /**
227
+ * Abort signal for cancelling the operation.
228
+ */
229
+ abortSignal?: AbortSignal;
230
+
231
+ /**
232
+ * Additional HTTP headers to be sent with the request.
233
+ * Only applicable for HTTP-based providers.
234
+ */
235
+ headers?: Record<string, string | undefined>;
236
+
237
+ /**
238
+ * Additional provider-specific options. They are passed through
239
+ * to the provider from the AI SDK and enable provider-specific
240
+ * functionality that can be fully encapsulated in the provider.
241
+ */
242
+ providerOptions?: ProviderOptions;
243
+ }
244
+
245
+ /**
246
+ * Information passed to the prepareStep callback.
247
+ */
248
+ export interface PrepareStepInfo<TTools extends ToolSet = ToolSet> {
249
+ /**
250
+ * The current model configuration (string or function).
251
+ * The function should return a LanguageModelV4 instance.
252
+ */
253
+ model: string | (() => Promise<CompatibleLanguageModel>);
254
+
255
+ /**
256
+ * The current step number (0-indexed).
257
+ */
258
+ stepNumber: number;
259
+
260
+ /**
261
+ * All previous steps with their results.
262
+ */
263
+ steps: StepResult<TTools, any>[];
264
+
265
+ /**
266
+ * The messages that will be sent to the model.
267
+ * This is the LanguageModelV4Prompt format used internally.
268
+ */
269
+ messages: LanguageModelV4Prompt;
270
+
271
+ /**
272
+ * The context passed via the experimental_context setting (experimental).
273
+ */
274
+ experimental_context: unknown;
275
+ }
276
+
277
+ /**
278
+ * Return type from the prepareStep callback.
279
+ * All properties are optional - only return the ones you want to override.
280
+ */
281
+ export interface PrepareStepResult extends Partial<GenerationSettings> {
282
+ /**
283
+ * Override the model for this step.
284
+ * The function should return a LanguageModelV4 instance.
285
+ */
286
+ model?: string | (() => Promise<CompatibleLanguageModel>);
287
+
288
+ /**
289
+ * Override the system message for this step.
290
+ */
291
+ system?: string;
292
+
293
+ /**
294
+ * Override the messages for this step.
295
+ * Use this for context management or message injection.
296
+ */
297
+ messages?: LanguageModelV4Prompt;
298
+
299
+ /**
300
+ * Override the tool choice for this step.
301
+ */
302
+ toolChoice?: ToolChoice<ToolSet>;
303
+
304
+ /**
305
+ * Override the active tools for this step.
306
+ * Limits the tools that are available for the model to call.
307
+ */
308
+ activeTools?: string[];
309
+
310
+ /**
311
+ * Context that is passed into tool execution. Experimental.
312
+ * Changing the context will affect the context in this step and all subsequent steps.
313
+ */
314
+ experimental_context?: unknown;
315
+ }
316
+
317
+ /**
318
+ * Callback function called before each step in the agent loop.
319
+ * Use this to modify settings, manage context, or implement dynamic behavior.
320
+ */
321
+ export type PrepareStepCallback<TTools extends ToolSet = ToolSet> = (
322
+ info: PrepareStepInfo<TTools>,
323
+ ) => PrepareStepResult | Promise<PrepareStepResult>;
324
+
325
+ /**
326
+ * Options passed to the prepareCall callback.
327
+ */
328
+ export interface PrepareCallOptions<
329
+ TTools extends ToolSet = ToolSet,
330
+ > extends Partial<GenerationSettings> {
331
+ model: string | (() => Promise<CompatibleLanguageModel>);
332
+ tools: TTools;
333
+ instructions?: string | SystemModelMessage | Array<SystemModelMessage>;
334
+ toolChoice?: ToolChoice<TTools>;
335
+ experimental_telemetry?: TelemetrySettings;
336
+ experimental_context?: unknown;
337
+ messages: ModelMessage[];
338
+ }
339
+
340
+ /**
341
+ * Result of the prepareCall callback. All fields are optional —
342
+ * only returned fields override the defaults.
343
+ * Note: `tools` cannot be overridden via prepareCall because they are
344
+ * bound at construction time for type safety.
345
+ */
346
+ export type PrepareCallResult<TTools extends ToolSet = ToolSet> = Partial<
347
+ Omit<PrepareCallOptions<TTools>, 'tools'>
348
+ >;
349
+
350
+ /**
351
+ * Callback called once before the agent loop starts to transform call parameters.
352
+ */
353
+ export type PrepareCallCallback<TTools extends ToolSet = ToolSet> = (
354
+ options: PrepareCallOptions<TTools>,
355
+ ) => PrepareCallResult<TTools> | Promise<PrepareCallResult<TTools>>;
356
+
357
+ /**
358
+ * Configuration options for creating a {@link WorkflowAgent} instance.
359
+ */
360
+ export interface WorkflowAgentOptions<
361
+ TTools extends ToolSet = ToolSet,
362
+ > extends GenerationSettings {
363
+ /**
364
+ * The model provider to use for the agent.
365
+ *
366
+ * This should be a string compatible with the Vercel AI Gateway (e.g., 'anthropic/claude-opus'),
367
+ * or a step function that returns a LanguageModelV4 instance.
368
+ */
369
+ model: string | (() => Promise<CompatibleLanguageModel>);
370
+
371
+ /**
372
+ * A set of tools available to the agent.
373
+ * Tools can be implemented as workflow steps for automatic retries and persistence,
374
+ * or as regular workflow-level logic using core library features like sleep() and Hooks.
375
+ */
376
+ tools?: TTools;
377
+
378
+ /**
379
+ * Agent instructions. Can be a string, a SystemModelMessage, or an array of SystemModelMessages.
380
+ * Supports provider-specific options (e.g., caching) when using the SystemModelMessage form.
381
+ */
382
+ instructions?: string | SystemModelMessage | Array<SystemModelMessage>;
383
+
384
+ /**
385
+ * Optional system prompt to guide the agent's behavior.
386
+ * @deprecated Use `instructions` instead.
387
+ */
388
+ system?: string;
389
+
390
+ /**
391
+ * The tool choice strategy. Default: 'auto'.
392
+ */
393
+ toolChoice?: ToolChoice<TTools>;
394
+
395
+ /**
396
+ * Optional telemetry configuration (experimental).
397
+ */
398
+ experimental_telemetry?: TelemetrySettings;
399
+
400
+ /**
401
+ * Default context that is passed into tool execution for every stream call on this agent.
402
+ *
403
+ * Per-stream `experimental_context` values passed to `stream()` override this default.
404
+ * Experimental (can break in patch releases).
405
+ * @default undefined
406
+ */
407
+ experimental_context?: unknown;
408
+
409
+ /**
410
+ * Default callback function called before each step in the agent loop.
411
+ * Use this to modify settings, manage context, or inject messages dynamically
412
+ * for every stream call on this agent instance.
413
+ *
414
+ * Per-stream `prepareStep` values passed to `stream()` override this default.
415
+ */
416
+ prepareStep?: PrepareStepCallback<TTools>;
417
+
418
+ /**
419
+ * Callback function to be called after each step completes.
420
+ */
421
+ onStepFinish?: StreamTextOnStepFinishCallback<ToolSet, any>;
422
+
423
+ /**
424
+ * Callback that is called when the LLM response and all request tool executions are finished.
425
+ */
426
+ onFinish?: StreamTextOnFinishCallback<ToolSet>;
427
+
428
+ /**
429
+ * Callback called when the agent starts streaming, before any LLM calls.
430
+ */
431
+ experimental_onStart?: WorkflowAgentOnStartCallback;
432
+
433
+ /**
434
+ * Callback called before each step (LLM call) begins.
435
+ */
436
+ experimental_onStepStart?: WorkflowAgentOnStepStartCallback;
437
+
438
+ /**
439
+ * Callback called before a tool's execute function runs.
440
+ */
441
+ experimental_onToolCallStart?: WorkflowAgentOnToolCallStartCallback;
442
+
443
+ /**
444
+ * Callback called after a tool execution completes.
445
+ */
446
+ experimental_onToolCallFinish?: WorkflowAgentOnToolCallFinishCallback;
447
+
448
+ /**
449
+ * Prepare the parameters for the stream call.
450
+ * Called once before the agent loop starts. Use this to transform
451
+ * model, tools, instructions, or other settings based on runtime context.
452
+ */
453
+ prepareCall?: PrepareCallCallback<TTools>;
454
+ }
455
+
456
+ /**
457
+ * Callback that is called when the LLM response and all request tool executions are finished.
458
+ */
459
+ export type StreamTextOnFinishCallback<
460
+ TTools extends ToolSet = ToolSet,
461
+ OUTPUT = never,
462
+ > = (event: {
463
+ /**
464
+ * Details for all steps.
465
+ */
466
+ readonly steps: StepResult<TTools, any>[];
467
+
468
+ /**
469
+ * The final messages including all tool calls and results.
470
+ */
471
+ readonly messages: ModelMessage[];
472
+
473
+ /**
474
+ * The text output from the last step.
475
+ */
476
+ readonly text: string;
477
+
478
+ /**
479
+ * The finish reason from the last step.
480
+ */
481
+ readonly finishReason: FinishReason;
482
+
483
+ /**
484
+ * The total token usage across all steps.
485
+ */
486
+ readonly totalUsage: LanguageModelUsage;
487
+
488
+ /**
489
+ * Context that is passed into tool execution.
490
+ */
491
+ readonly experimental_context: unknown;
492
+
493
+ /**
494
+ * The generated structured output. It uses the `experimental_output` specification.
495
+ * Only available when `experimental_output` is specified.
496
+ */
497
+ readonly experimental_output: OUTPUT;
498
+ }) => PromiseLike<void> | void;
499
+
500
+ /**
501
+ * Callback that is invoked when an error occurs during streaming.
502
+ */
503
+ export type StreamTextOnErrorCallback = (event: {
504
+ error: unknown;
505
+ }) => PromiseLike<void> | void;
506
+
507
+ /**
508
+ * Callback that is set using the `onAbort` option.
509
+ */
510
+ export type StreamTextOnAbortCallback<TTools extends ToolSet = ToolSet> =
511
+ (event: {
512
+ /**
513
+ * Details for all previously finished steps.
514
+ */
515
+ readonly steps: StepResult<TTools, any>[];
516
+ }) => PromiseLike<void> | void;
517
+
518
+ /**
519
+ * Callback that is called when the agent starts streaming, before any LLM calls.
520
+ */
521
+ export type WorkflowAgentOnStartCallback = (event: {
522
+ /** The model being used */
523
+ readonly model: string | (() => Promise<CompatibleLanguageModel>);
524
+ /** The messages being sent */
525
+ readonly messages: ModelMessage[];
526
+ }) => PromiseLike<void> | void;
527
+
528
+ /**
529
+ * Callback that is called before each step (LLM call) begins.
530
+ */
531
+ export type WorkflowAgentOnStepStartCallback = (event: {
532
+ /** The current step number (0-based) */
533
+ readonly stepNumber: number;
534
+ /** The model being used for this step */
535
+ readonly model: string | (() => Promise<CompatibleLanguageModel>);
536
+ /** The messages being sent for this step */
537
+ readonly messages: ModelMessage[];
538
+ }) => PromiseLike<void> | void;
539
+
540
+ /**
541
+ * Callback that is called before a tool's execute function runs.
542
+ */
543
+ export type WorkflowAgentOnToolCallStartCallback = (event: {
544
+ /** The tool call being executed */
545
+ readonly toolCall: ToolCall;
546
+ }) => PromiseLike<void> | void;
547
+
548
+ /**
549
+ * Callback that is called after a tool execution completes.
550
+ */
551
+ export type WorkflowAgentOnToolCallFinishCallback = (event: {
552
+ /** The tool call that was executed */
553
+ readonly toolCall: ToolCall;
554
+ /** The tool result (undefined if execution failed) */
555
+ readonly result?: unknown;
556
+ /** The error if execution failed */
557
+ readonly error?: unknown;
558
+ }) => PromiseLike<void> | void;
559
+
560
+ /**
561
+ * Options for the {@link WorkflowAgent.stream} method.
562
+ */
563
+ export interface WorkflowAgentStreamOptions<
564
+ TTools extends ToolSet = ToolSet,
565
+ OUTPUT = never,
566
+ PARTIAL_OUTPUT = never,
567
+ > extends Partial<GenerationSettings> {
568
+ /**
569
+ * The conversation messages to process. Should follow the AI SDK's ModelMessage format.
570
+ */
571
+ messages: ModelMessage[];
572
+
573
+ /**
574
+ * Optional system prompt override. If provided, overrides the system prompt from the constructor.
575
+ */
576
+ system?: string;
577
+
578
+ /**
579
+ * A WritableStream that receives raw LanguageModelV4StreamPart chunks in real-time
580
+ * as the model generates them. This enables streaming to the client without
581
+ * coupling WorkflowAgent to UIMessageChunk format.
582
+ *
583
+ * Convert to UIMessageChunks at the response boundary using
584
+ * `createUIMessageChunkTransform()` from `@ai-sdk/workflow`.
585
+ *
586
+ * @example
587
+ * ```typescript
588
+ * // In the workflow:
589
+ * await agent.stream({
590
+ * messages,
591
+ * writable: getWritable<ModelCallStreamPart>(),
592
+ * });
593
+ *
594
+ * // In the route handler:
595
+ * return createUIMessageStreamResponse({
596
+ * stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
597
+ * });
598
+ * ```
599
+ */
600
+ writable?: WritableStream<ModelCallStreamPart<ToolSet>>;
601
+
602
+ /**
603
+ * Condition for stopping the generation when there are tool results in the last step.
604
+ * When the condition is an array, any of the conditions can be met to stop the generation.
605
+ */
606
+ stopWhen?:
607
+ | StopCondition<NoInfer<ToolSet>, any>
608
+ | Array<StopCondition<NoInfer<ToolSet>, any>>;
609
+
610
+ /**
611
+ * Maximum number of sequential LLM calls (steps), e.g. when you use tool calls.
612
+ * A maximum number can be set to prevent infinite loops in the case of misconfigured tools.
613
+ * By default, it's unlimited (the agent loops until completion).
614
+ */
615
+ maxSteps?: number;
616
+
617
+ /**
618
+ * The tool choice strategy. Default: 'auto'.
619
+ * Overrides the toolChoice from the constructor if provided.
620
+ */
621
+ toolChoice?: ToolChoice<TTools>;
622
+
623
+ /**
624
+ * Limits the tools that are available for the model to call without
625
+ * changing the tool call and result types in the result.
626
+ */
627
+ activeTools?: Array<keyof NoInfer<TTools>>;
628
+
629
+ /**
630
+ * Optional telemetry configuration (experimental).
631
+ */
632
+ experimental_telemetry?: TelemetrySettings;
633
+
634
+ /**
635
+ * Context that is passed into tool execution.
636
+ * Experimental (can break in patch releases).
637
+ * @default undefined
638
+ */
639
+ experimental_context?: unknown;
640
+
641
+ /**
642
+ * Optional specification for parsing structured outputs from the LLM response.
643
+ * Use `Output.object({ schema })` for structured output or `Output.text()` for text output.
644
+ *
645
+ * @example
646
+ * ```typescript
647
+ * import { Output } from '@workflow/ai';
648
+ * import { z } from 'zod';
649
+ *
650
+ * const result = await agent.stream({
651
+ * messages: [...],
652
+ * writable: getWritable(),
653
+ * experimental_output: Output.object({
654
+ * schema: z.object({
655
+ * sentiment: z.enum(['positive', 'negative', 'neutral']),
656
+ * confidence: z.number(),
657
+ * }),
658
+ * }),
659
+ * });
660
+ *
661
+ * console.log(result.experimental_output); // { sentiment: 'positive', confidence: 0.95 }
662
+ * ```
663
+ */
664
+ experimental_output?: OutputSpecification<OUTPUT, PARTIAL_OUTPUT>;
665
+
666
+ /**
667
+ * Whether to include raw chunks from the provider in the stream.
668
+ * When enabled, you will receive raw chunks with type 'raw' that contain the unprocessed data from the provider.
669
+ * This allows access to cutting-edge provider features not yet wrapped by the AI SDK.
670
+ * Defaults to false.
671
+ */
672
+ includeRawChunks?: boolean;
673
+
674
+ /**
675
+ * A function that attempts to repair a tool call that failed to parse.
676
+ */
677
+ experimental_repairToolCall?: ToolCallRepairFunction<TTools>;
678
+
679
+ /**
680
+ * Optional stream transformations.
681
+ * They are applied in the order they are provided.
682
+ * The stream transformations must maintain the stream structure for streamText to work correctly.
683
+ */
684
+ experimental_transform?:
685
+ | StreamTextTransform<TTools>
686
+ | Array<StreamTextTransform<TTools>>;
687
+
688
+ /**
689
+ * Custom download function to use for URLs.
690
+ * By default, files are downloaded if the model does not support the URL for the given media type.
691
+ */
692
+ experimental_download?: DownloadFunction;
693
+
694
+ /**
695
+ * Callback function to be called after each step completes.
696
+ */
697
+ onStepFinish?: StreamTextOnStepFinishCallback<TTools, any>;
698
+
699
+ /**
700
+ * Callback that is invoked when an error occurs during streaming.
701
+ * You can use it to log errors.
702
+ */
703
+ onError?: StreamTextOnErrorCallback;
704
+
705
+ /**
706
+ * Callback that is called when the LLM response and all request tool executions
707
+ * (for tools that have an `execute` function) are finished.
708
+ */
709
+ onFinish?: StreamTextOnFinishCallback<TTools, OUTPUT>;
710
+
711
+ /**
712
+ * Callback that is called when the operation is aborted.
713
+ */
714
+ onAbort?: StreamTextOnAbortCallback<TTools>;
715
+
716
+ /**
717
+ * Callback called when the agent starts streaming, before any LLM calls.
718
+ */
719
+ experimental_onStart?: WorkflowAgentOnStartCallback;
720
+
721
+ /**
722
+ * Callback called before each step (LLM call) begins.
723
+ */
724
+ experimental_onStepStart?: WorkflowAgentOnStepStartCallback;
725
+
726
+ /**
727
+ * Callback called before a tool's execute function runs.
728
+ */
729
+ experimental_onToolCallStart?: WorkflowAgentOnToolCallStartCallback;
730
+
731
+ /**
732
+ * Callback called after a tool execution completes.
733
+ */
734
+ experimental_onToolCallFinish?: WorkflowAgentOnToolCallFinishCallback;
735
+
736
+ /**
737
+ * Callback function called before each step in the agent loop.
738
+ * Use this to modify settings, manage context, or inject messages dynamically.
739
+ *
740
+ * @example
741
+ * ```typescript
742
+ * prepareStep: async ({ messages, stepNumber }) => {
743
+ * // Inject messages from a queue
744
+ * const queuedMessages = await getQueuedMessages();
745
+ * if (queuedMessages.length > 0) {
746
+ * return {
747
+ * messages: [...messages, ...queuedMessages],
748
+ * };
749
+ * }
750
+ * return {};
751
+ * }
752
+ * ```
753
+ */
754
+ prepareStep?: PrepareStepCallback<TTools>;
755
+
756
+ /**
757
+ * Timeout in milliseconds for the stream operation.
758
+ * When specified, creates an AbortSignal that will abort the operation after the given time.
759
+ * If both `timeout` and `abortSignal` are provided, whichever triggers first will abort.
760
+ */
761
+ timeout?: number;
762
+ }
763
+
764
+ /**
765
+ * A tool call made by the model. Matches the AI SDK's tool call shape.
766
+ */
767
+ export interface ToolCall {
768
+ /** Discriminator for content part arrays */
769
+ type: 'tool-call';
770
+ /** The unique identifier of the tool call */
771
+ toolCallId: string;
772
+ /** The name of the tool that was called */
773
+ toolName: string;
774
+ /** The parsed input arguments for the tool call */
775
+ input: unknown;
776
+ }
777
+
778
+ /**
779
+ * A tool result from executing a tool. Matches the AI SDK's tool result shape.
780
+ */
781
+ export interface ToolResult {
782
+ /** Discriminator for content part arrays */
783
+ type: 'tool-result';
784
+ /** The tool call ID this result corresponds to */
785
+ toolCallId: string;
786
+ /** The name of the tool that was executed */
787
+ toolName: string;
788
+ /** The parsed input arguments that were passed to the tool */
789
+ input: unknown;
790
+ /** The output produced by the tool */
791
+ output: unknown;
792
+ }
793
+
794
+ /**
795
+ * Result of the WorkflowAgent.stream method.
796
+ */
797
+ export interface WorkflowAgentStreamResult<
798
+ TTools extends ToolSet = ToolSet,
799
+ OUTPUT = never,
800
+ > {
801
+ /**
802
+ * The final messages including all tool calls and results.
803
+ */
804
+ messages: ModelMessage[];
805
+
806
+ /**
807
+ * Details for all steps.
808
+ */
809
+ steps: StepResult<TTools, any>[];
810
+
811
+ /**
812
+ * The tool calls from the last step.
813
+ * Includes all tool calls regardless of whether they were executed.
814
+ *
815
+ * When the agent stops because a tool without an `execute` function was called,
816
+ * this array will contain those calls. Compare with `toolResults` to find
817
+ * unresolved tool calls that need client-side handling:
818
+ *
819
+ * ```ts
820
+ * const unresolved = result.toolCalls.filter(
821
+ * tc => !result.toolResults.some(tr => tr.toolCallId === tc.toolCallId)
822
+ * );
823
+ * ```
824
+ *
825
+ * This matches the AI SDK's `GenerateTextResult.toolCalls` behavior.
826
+ */
827
+ toolCalls: ToolCall[];
828
+
829
+ /**
830
+ * The tool results from the last step.
831
+ * Only includes results for tools that were actually executed (server-side or provider-executed).
832
+ * Tools without an `execute` function will NOT have entries here.
833
+ *
834
+ * This matches the AI SDK's `GenerateTextResult.toolResults` behavior.
835
+ */
836
+ toolResults: ToolResult[];
837
+
838
+ /**
839
+ * The generated structured output. It uses the `experimental_output` specification.
840
+ * Only available when `experimental_output` is specified.
841
+ */
842
+ experimental_output: OUTPUT;
843
+ }
844
+
845
+ /**
846
+ * A class for building durable AI agents within workflows.
847
+ *
848
+ * WorkflowAgent enables you to create AI-powered agents that can maintain state
849
+ * across workflow steps, call tools, and gracefully handle interruptions and resumptions.
850
+ * It integrates seamlessly with the AI SDK and the Workflow DevKit for
851
+ * production-grade reliability.
852
+ *
853
+ * @example
854
+ * ```typescript
855
+ * const agent = new WorkflowAgent({
856
+ * model: 'anthropic/claude-opus',
857
+ * tools: {
858
+ * getWeather: {
859
+ * description: 'Get weather for a location',
860
+ * inputSchema: z.object({ location: z.string() }),
861
+ * execute: getWeatherStep,
862
+ * },
863
+ * },
864
+ * instructions: 'You are a helpful weather assistant.',
865
+ * });
866
+ *
867
+ * const result = await agent.stream({
868
+ * messages: [{ role: 'user', content: 'What is the weather?' }],
869
+ * });
870
+ * ```
871
+ */
872
+ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
873
+ private model: string | (() => Promise<CompatibleLanguageModel>);
874
+ /**
875
+ * The tool set configured for this agent.
876
+ */
877
+ public readonly tools: TBaseTools;
878
+ private instructions?:
879
+ | string
880
+ | SystemModelMessage
881
+ | Array<SystemModelMessage>;
882
+ private generationSettings: GenerationSettings;
883
+ private toolChoice?: ToolChoice<TBaseTools>;
884
+ private telemetry?: TelemetrySettings;
885
+ private experimentalContext: unknown;
886
+ private prepareStep?: PrepareStepCallback<TBaseTools>;
887
+ private constructorOnStepFinish?: StreamTextOnStepFinishCallback<
888
+ ToolSet,
889
+ any
890
+ >;
891
+ private constructorOnFinish?: StreamTextOnFinishCallback<ToolSet>;
892
+ private constructorOnStart?: WorkflowAgentOnStartCallback;
893
+ private constructorOnStepStart?: WorkflowAgentOnStepStartCallback;
894
+ private constructorOnToolCallStart?: WorkflowAgentOnToolCallStartCallback;
895
+ private constructorOnToolCallFinish?: WorkflowAgentOnToolCallFinishCallback;
896
+ private prepareCall?: PrepareCallCallback<TBaseTools>;
897
+
898
+ constructor(options: WorkflowAgentOptions<TBaseTools>) {
899
+ this.model = options.model;
900
+ this.tools = (options.tools ?? {}) as TBaseTools;
901
+ // `instructions` takes precedence over deprecated `system`
902
+ this.instructions = options.instructions ?? options.system;
903
+ this.toolChoice = options.toolChoice;
904
+ this.telemetry = options.experimental_telemetry;
905
+ this.experimentalContext = options.experimental_context;
906
+ this.prepareStep = options.prepareStep;
907
+ this.constructorOnStepFinish = options.onStepFinish;
908
+ this.constructorOnFinish = options.onFinish;
909
+ this.constructorOnStart = options.experimental_onStart;
910
+ this.constructorOnStepStart = options.experimental_onStepStart;
911
+ this.constructorOnToolCallStart = options.experimental_onToolCallStart;
912
+ this.constructorOnToolCallFinish = options.experimental_onToolCallFinish;
913
+ this.prepareCall = options.prepareCall;
914
+
915
+ // Extract generation settings
916
+ this.generationSettings = {
917
+ maxOutputTokens: options.maxOutputTokens,
918
+ temperature: options.temperature,
919
+ topP: options.topP,
920
+ topK: options.topK,
921
+ presencePenalty: options.presencePenalty,
922
+ frequencyPenalty: options.frequencyPenalty,
923
+ stopSequences: options.stopSequences,
924
+ seed: options.seed,
925
+ maxRetries: options.maxRetries,
926
+ abortSignal: options.abortSignal,
927
+ headers: options.headers,
928
+ providerOptions: options.providerOptions,
929
+ };
930
+ }
931
+
932
+ generate() {
933
+ throw new Error('Not implemented');
934
+ }
935
+
936
+ async stream<
937
+ TTools extends TBaseTools = TBaseTools,
938
+ OUTPUT = never,
939
+ PARTIAL_OUTPUT = never,
940
+ >(
941
+ options: WorkflowAgentStreamOptions<TTools, OUTPUT, PARTIAL_OUTPUT>,
942
+ ): Promise<WorkflowAgentStreamResult<TTools, OUTPUT>> {
943
+ // Call prepareCall to transform parameters before the agent loop
944
+ let effectiveModel: string | (() => Promise<CompatibleLanguageModel>) =
945
+ this.model;
946
+ let effectiveInstructions = options.system ?? this.instructions;
947
+ let effectiveMessages = options.messages;
948
+ let effectiveGenerationSettings = { ...this.generationSettings };
949
+ let effectiveExperimentalContext =
950
+ options.experimental_context ?? this.experimentalContext;
951
+ let effectiveToolChoiceFromPrepare = options.toolChoice ?? this.toolChoice;
952
+ let effectiveTelemetryFromPrepare =
953
+ options.experimental_telemetry ?? this.telemetry;
954
+
955
+ if (this.prepareCall) {
956
+ const prepared = await this.prepareCall({
957
+ model: effectiveModel,
958
+ tools: this.tools,
959
+ instructions: effectiveInstructions,
960
+ toolChoice: effectiveToolChoiceFromPrepare as ToolChoice<TBaseTools>,
961
+ experimental_telemetry: effectiveTelemetryFromPrepare,
962
+ experimental_context: effectiveExperimentalContext,
963
+ messages: effectiveMessages as ModelMessage[],
964
+ ...effectiveGenerationSettings,
965
+ } as PrepareCallOptions<TBaseTools>);
966
+
967
+ if (prepared.model !== undefined) effectiveModel = prepared.model;
968
+ if (prepared.instructions !== undefined)
969
+ effectiveInstructions = prepared.instructions;
970
+ if (prepared.messages !== undefined)
971
+ effectiveMessages =
972
+ prepared.messages as WorkflowAgentStreamOptions<TTools>['messages'];
973
+ if (prepared.experimental_context !== undefined)
974
+ effectiveExperimentalContext = prepared.experimental_context;
975
+ if (prepared.toolChoice !== undefined)
976
+ effectiveToolChoiceFromPrepare =
977
+ prepared.toolChoice as ToolChoice<TBaseTools>;
978
+ if (prepared.experimental_telemetry !== undefined)
979
+ effectiveTelemetryFromPrepare = prepared.experimental_telemetry;
980
+ if (prepared.maxOutputTokens !== undefined)
981
+ effectiveGenerationSettings.maxOutputTokens = prepared.maxOutputTokens;
982
+ if (prepared.temperature !== undefined)
983
+ effectiveGenerationSettings.temperature = prepared.temperature;
984
+ if (prepared.topP !== undefined)
985
+ effectiveGenerationSettings.topP = prepared.topP;
986
+ if (prepared.topK !== undefined)
987
+ effectiveGenerationSettings.topK = prepared.topK;
988
+ if (prepared.presencePenalty !== undefined)
989
+ effectiveGenerationSettings.presencePenalty = prepared.presencePenalty;
990
+ if (prepared.frequencyPenalty !== undefined)
991
+ effectiveGenerationSettings.frequencyPenalty =
992
+ prepared.frequencyPenalty;
993
+ if (prepared.stopSequences !== undefined)
994
+ effectiveGenerationSettings.stopSequences = prepared.stopSequences;
995
+ if (prepared.seed !== undefined)
996
+ effectiveGenerationSettings.seed = prepared.seed;
997
+ if (prepared.headers !== undefined)
998
+ effectiveGenerationSettings.headers = prepared.headers;
999
+ if (prepared.providerOptions !== undefined)
1000
+ effectiveGenerationSettings.providerOptions = prepared.providerOptions;
1001
+ }
1002
+
1003
+ const prompt = await standardizePrompt({
1004
+ system: effectiveInstructions,
1005
+ messages: effectiveMessages,
1006
+ });
1007
+
1008
+ const modelPrompt = await convertToLanguageModelPrompt({
1009
+ prompt,
1010
+ supportedUrls: {},
1011
+ download: options.experimental_download,
1012
+ });
1013
+
1014
+ const effectiveAbortSignal = mergeAbortSignals(
1015
+ options.abortSignal ?? effectiveGenerationSettings.abortSignal,
1016
+ options.timeout != null
1017
+ ? AbortSignal.timeout(options.timeout)
1018
+ : undefined,
1019
+ );
1020
+
1021
+ // Merge generation settings: constructor defaults < prepareCall < stream options
1022
+ const mergedGenerationSettings: GenerationSettings = {
1023
+ ...effectiveGenerationSettings,
1024
+ ...(options.maxOutputTokens !== undefined && {
1025
+ maxOutputTokens: options.maxOutputTokens,
1026
+ }),
1027
+ ...(options.temperature !== undefined && {
1028
+ temperature: options.temperature,
1029
+ }),
1030
+ ...(options.topP !== undefined && { topP: options.topP }),
1031
+ ...(options.topK !== undefined && { topK: options.topK }),
1032
+ ...(options.presencePenalty !== undefined && {
1033
+ presencePenalty: options.presencePenalty,
1034
+ }),
1035
+ ...(options.frequencyPenalty !== undefined && {
1036
+ frequencyPenalty: options.frequencyPenalty,
1037
+ }),
1038
+ ...(options.stopSequences !== undefined && {
1039
+ stopSequences: options.stopSequences,
1040
+ }),
1041
+ ...(options.seed !== undefined && { seed: options.seed }),
1042
+ ...(options.maxRetries !== undefined && {
1043
+ maxRetries: options.maxRetries,
1044
+ }),
1045
+ ...(effectiveAbortSignal !== undefined && {
1046
+ abortSignal: effectiveAbortSignal,
1047
+ }),
1048
+ ...(options.headers !== undefined && { headers: options.headers }),
1049
+ ...(options.providerOptions !== undefined && {
1050
+ providerOptions: options.providerOptions,
1051
+ }),
1052
+ };
1053
+
1054
+ // Merge constructor + stream callbacks (constructor first, then stream)
1055
+ const mergedOnStepFinish = mergeCallbacks(
1056
+ this.constructorOnStepFinish as
1057
+ | StreamTextOnStepFinishCallback<TTools, any>
1058
+ | undefined,
1059
+ options.onStepFinish,
1060
+ );
1061
+ const mergedOnFinish = mergeCallbacks(
1062
+ this.constructorOnFinish as
1063
+ | StreamTextOnFinishCallback<TTools, OUTPUT>
1064
+ | undefined,
1065
+ options.onFinish,
1066
+ );
1067
+ const mergedOnStart = mergeCallbacks(
1068
+ this.constructorOnStart,
1069
+ options.experimental_onStart,
1070
+ );
1071
+ const mergedOnStepStart = mergeCallbacks(
1072
+ this.constructorOnStepStart,
1073
+ options.experimental_onStepStart,
1074
+ );
1075
+ const mergedOnToolCallStart = mergeCallbacks(
1076
+ this.constructorOnToolCallStart,
1077
+ options.experimental_onToolCallStart,
1078
+ );
1079
+ const mergedOnToolCallFinish = mergeCallbacks(
1080
+ this.constructorOnToolCallFinish,
1081
+ options.experimental_onToolCallFinish,
1082
+ );
1083
+
1084
+ // Determine effective tool choice
1085
+ const effectiveToolChoice = effectiveToolChoiceFromPrepare;
1086
+
1087
+ // Merge telemetry settings
1088
+ const effectiveTelemetry = effectiveTelemetryFromPrepare;
1089
+
1090
+ // Filter tools if activeTools is specified
1091
+ const effectiveTools =
1092
+ options.activeTools && options.activeTools.length > 0
1093
+ ? filterTools(this.tools, options.activeTools as string[])
1094
+ : this.tools;
1095
+
1096
+ // Initialize context
1097
+ let experimentalContext = effectiveExperimentalContext;
1098
+
1099
+ const steps: StepResult<TTools, any>[] = [];
1100
+
1101
+ // Track tool calls and results from the last step for the result
1102
+ let lastStepToolCalls: ToolCall[] = [];
1103
+ let lastStepToolResults: ToolResult[] = [];
1104
+
1105
+ // Call onStart before the agent loop
1106
+ if (mergedOnStart) {
1107
+ await mergedOnStart({
1108
+ model: effectiveModel,
1109
+ messages: effectiveMessages as ModelMessage[],
1110
+ });
1111
+ }
1112
+
1113
+ // Helper to wrap executeTool with onToolCallStart/onToolCallFinish callbacks
1114
+ const executeToolWithCallbacks = async (
1115
+ toolCall: { toolCallId: string; toolName: string; input: unknown },
1116
+ tools: ToolSet,
1117
+ messages: LanguageModelV4Prompt,
1118
+ context?: unknown,
1119
+ ): Promise<LanguageModelV4ToolResultPart> => {
1120
+ if (mergedOnToolCallStart) {
1121
+ await mergedOnToolCallStart({
1122
+ toolCall: {
1123
+ type: 'tool-call',
1124
+ toolCallId: toolCall.toolCallId,
1125
+ toolName: toolCall.toolName,
1126
+ input: toolCall.input,
1127
+ },
1128
+ });
1129
+ }
1130
+ let result: LanguageModelV4ToolResultPart;
1131
+ try {
1132
+ result = await executeTool(toolCall, tools, messages, context);
1133
+ } catch (err) {
1134
+ if (mergedOnToolCallFinish) {
1135
+ await mergedOnToolCallFinish({
1136
+ toolCall: {
1137
+ type: 'tool-call',
1138
+ toolCallId: toolCall.toolCallId,
1139
+ toolName: toolCall.toolName,
1140
+ input: toolCall.input,
1141
+ },
1142
+ error: err,
1143
+ });
1144
+ }
1145
+ throw err;
1146
+ }
1147
+ if (mergedOnToolCallFinish) {
1148
+ const isError =
1149
+ result.output &&
1150
+ 'type' in result.output &&
1151
+ (result.output.type === 'error-text' ||
1152
+ result.output.type === 'error-json');
1153
+ await mergedOnToolCallFinish({
1154
+ toolCall: {
1155
+ type: 'tool-call',
1156
+ toolCallId: toolCall.toolCallId,
1157
+ toolName: toolCall.toolName,
1158
+ input: toolCall.input,
1159
+ },
1160
+ ...(isError
1161
+ ? {
1162
+ error:
1163
+ 'value' in result.output ? result.output.value : undefined,
1164
+ }
1165
+ : {
1166
+ result:
1167
+ result.output && 'value' in result.output
1168
+ ? result.output.value
1169
+ : undefined,
1170
+ }),
1171
+ });
1172
+ }
1173
+ return result;
1174
+ };
1175
+
1176
+ // Check for abort before starting
1177
+ if (mergedGenerationSettings.abortSignal?.aborted) {
1178
+ if (options.onAbort) {
1179
+ await options.onAbort({ steps });
1180
+ }
1181
+ return {
1182
+ messages: options.messages as unknown as ModelMessage[],
1183
+ steps,
1184
+ toolCalls: [],
1185
+ toolResults: [],
1186
+ experimental_output: undefined as OUTPUT,
1187
+ };
1188
+ }
1189
+
1190
+ const iterator = streamTextIterator({
1191
+ model: effectiveModel,
1192
+ tools: effectiveTools as ToolSet,
1193
+ writable: options.writable,
1194
+ prompt: modelPrompt,
1195
+ stopConditions: options.stopWhen,
1196
+ maxSteps: options.maxSteps,
1197
+ onStepFinish: mergedOnStepFinish,
1198
+ onStepStart: mergedOnStepStart,
1199
+ onError: options.onError,
1200
+ prepareStep:
1201
+ options.prepareStep ??
1202
+ (this.prepareStep as PrepareStepCallback<ToolSet> | undefined),
1203
+ generationSettings: mergedGenerationSettings,
1204
+ toolChoice: effectiveToolChoice as ToolChoice<ToolSet>,
1205
+ experimental_context: experimentalContext,
1206
+ experimental_telemetry: effectiveTelemetry,
1207
+ includeRawChunks: options.includeRawChunks ?? false,
1208
+ repairToolCall:
1209
+ options.experimental_repairToolCall as ToolCallRepairFunction<ToolSet>,
1210
+ responseFormat: await options.experimental_output?.responseFormat,
1211
+ });
1212
+
1213
+ // Track the final conversation messages from the iterator
1214
+ let finalMessages: LanguageModelV4Prompt | undefined;
1215
+ let encounteredError: unknown;
1216
+ let wasAborted = false;
1217
+
1218
+ try {
1219
+ let result = await iterator.next();
1220
+ while (!result.done) {
1221
+ // Check for abort during iteration
1222
+ if (mergedGenerationSettings.abortSignal?.aborted) {
1223
+ wasAborted = true;
1224
+ if (options.onAbort) {
1225
+ await options.onAbort({ steps });
1226
+ }
1227
+ break;
1228
+ }
1229
+
1230
+ const {
1231
+ toolCalls,
1232
+ messages: iterMessages,
1233
+ step,
1234
+ context,
1235
+ providerExecutedToolResults,
1236
+ } = result.value;
1237
+ if (step) {
1238
+ steps.push(step as unknown as StepResult<TTools, any>);
1239
+ }
1240
+ if (context !== undefined) {
1241
+ experimentalContext = context;
1242
+ }
1243
+
1244
+ // Only execute tools if there are tool calls
1245
+ if (toolCalls.length > 0) {
1246
+ // Separate provider-executed tool calls from client-executed ones
1247
+ const nonProviderToolCalls = toolCalls.filter(
1248
+ tc => !tc.providerExecuted,
1249
+ );
1250
+ const providerToolCalls = toolCalls.filter(tc => tc.providerExecuted);
1251
+
1252
+ // Further split non-provider tool calls into executable (has execute function)
1253
+ // and client-side (no execute function, needs external resolution)
1254
+ // Note: missing tools (!tool) are left to executeTool which will throw —
1255
+ // only tools that exist but lack execute are treated as client-side.
1256
+ const executableToolCalls = nonProviderToolCalls.filter(tc => {
1257
+ const tool = (effectiveTools as ToolSet)[tc.toolName];
1258
+ return !tool || typeof tool.execute === 'function';
1259
+ });
1260
+ const clientSideToolCalls = nonProviderToolCalls.filter(tc => {
1261
+ const tool = (effectiveTools as ToolSet)[tc.toolName];
1262
+ return tool && typeof tool.execute !== 'function';
1263
+ });
1264
+
1265
+ // If there are client-side tool calls, stop the loop and return them
1266
+ // This matches AI SDK behavior: tools without execute pause the agent loop
1267
+ if (clientSideToolCalls.length > 0) {
1268
+ // Execute any executable tools that were also called in this step
1269
+ const executableResults = await Promise.all(
1270
+ executableToolCalls.map(
1271
+ (toolCall): Promise<LanguageModelV4ToolResultPart> =>
1272
+ executeToolWithCallbacks(
1273
+ toolCall,
1274
+ effectiveTools as ToolSet,
1275
+ iterMessages,
1276
+ experimentalContext,
1277
+ ),
1278
+ ),
1279
+ );
1280
+
1281
+ // Collect provider tool results
1282
+ const providerResults: LanguageModelV4ToolResultPart[] =
1283
+ providerToolCalls.map(toolCall =>
1284
+ resolveProviderToolResult(
1285
+ toolCall,
1286
+ providerExecutedToolResults,
1287
+ ),
1288
+ );
1289
+
1290
+ const resolvedResults = [...executableResults, ...providerResults];
1291
+
1292
+ const allToolCalls: ToolCall[] = toolCalls.map(tc => ({
1293
+ type: 'tool-call' as const,
1294
+ toolCallId: tc.toolCallId,
1295
+ toolName: tc.toolName,
1296
+ input: tc.input,
1297
+ }));
1298
+
1299
+ const allToolResults: ToolResult[] = resolvedResults.map(r => ({
1300
+ type: 'tool-result' as const,
1301
+ toolCallId: r.toolCallId,
1302
+ toolName: r.toolName,
1303
+ input: toolCalls.find(tc => tc.toolCallId === r.toolCallId)
1304
+ ?.input,
1305
+ output: 'value' in r.output ? r.output.value : undefined,
1306
+ }));
1307
+
1308
+ if (resolvedResults.length > 0) {
1309
+ iterMessages.push({
1310
+ role: 'tool',
1311
+ content: resolvedResults,
1312
+ });
1313
+ }
1314
+
1315
+ const messages = iterMessages as unknown as ModelMessage[];
1316
+
1317
+ if (mergedOnFinish && !wasAborted) {
1318
+ const lastStep = steps[steps.length - 1];
1319
+ await mergedOnFinish({
1320
+ steps,
1321
+ messages,
1322
+ text: lastStep?.text ?? '',
1323
+ finishReason: lastStep?.finishReason ?? 'other',
1324
+ totalUsage: aggregateUsage(steps),
1325
+ experimental_context: experimentalContext,
1326
+ experimental_output: undefined as OUTPUT,
1327
+ });
1328
+ }
1329
+
1330
+ return {
1331
+ messages,
1332
+ steps,
1333
+ toolCalls: allToolCalls,
1334
+ toolResults: allToolResults,
1335
+ experimental_output: undefined as OUTPUT,
1336
+ };
1337
+ }
1338
+
1339
+ // Execute client tools (all have execute functions at this point)
1340
+ const clientToolResults = await Promise.all(
1341
+ nonProviderToolCalls.map(
1342
+ (toolCall): Promise<LanguageModelV4ToolResultPart> =>
1343
+ executeToolWithCallbacks(
1344
+ toolCall,
1345
+ effectiveTools as ToolSet,
1346
+ iterMessages,
1347
+ experimentalContext,
1348
+ ),
1349
+ ),
1350
+ );
1351
+
1352
+ // For provider-executed tools, use the results from the stream
1353
+ const providerToolResults: LanguageModelV4ToolResultPart[] =
1354
+ providerToolCalls.map(toolCall =>
1355
+ resolveProviderToolResult(toolCall, providerExecutedToolResults),
1356
+ );
1357
+
1358
+ // Combine results in the original order
1359
+ const toolResults = toolCalls.map(tc => {
1360
+ const clientResult = clientToolResults.find(
1361
+ r => r.toolCallId === tc.toolCallId,
1362
+ );
1363
+ if (clientResult) return clientResult;
1364
+ const providerResult = providerToolResults.find(
1365
+ r => r.toolCallId === tc.toolCallId,
1366
+ );
1367
+ if (providerResult) return providerResult;
1368
+ // This should never happen, but return empty result as fallback
1369
+ return {
1370
+ type: 'tool-result' as const,
1371
+ toolCallId: tc.toolCallId,
1372
+ toolName: tc.toolName,
1373
+ output: { type: 'text' as const, value: '' },
1374
+ };
1375
+ });
1376
+
1377
+ // Track the tool calls and results for this step
1378
+ lastStepToolCalls = toolCalls.map(tc => ({
1379
+ type: 'tool-call' as const,
1380
+ toolCallId: tc.toolCallId,
1381
+ toolName: tc.toolName,
1382
+ input: tc.input,
1383
+ }));
1384
+ lastStepToolResults = toolResults.map(r => ({
1385
+ type: 'tool-result' as const,
1386
+ toolCallId: r.toolCallId,
1387
+ toolName: r.toolName,
1388
+ input: toolCalls.find(tc => tc.toolCallId === r.toolCallId)?.input,
1389
+ output: 'value' in r.output ? r.output.value : undefined,
1390
+ }));
1391
+
1392
+ result = await iterator.next(toolResults);
1393
+ } else {
1394
+ // Final step with no tool calls - reset tracking
1395
+ lastStepToolCalls = [];
1396
+ lastStepToolResults = [];
1397
+ result = await iterator.next([]);
1398
+ }
1399
+ }
1400
+
1401
+ // When the iterator completes normally, result.value contains the final conversation prompt
1402
+ if (result.done) {
1403
+ finalMessages = result.value;
1404
+ }
1405
+ } catch (error) {
1406
+ encounteredError = error;
1407
+ // Check if this is an abort error
1408
+ if (error instanceof Error && error.name === 'AbortError') {
1409
+ wasAborted = true;
1410
+ if (options.onAbort) {
1411
+ await options.onAbort({ steps });
1412
+ }
1413
+ } else if (options.onError) {
1414
+ // Call onError for non-abort errors (including tool execution errors)
1415
+ await options.onError({ error });
1416
+ }
1417
+ // Don't throw yet - we want to call onFinish first
1418
+ }
1419
+
1420
+ // Use the final messages from the iterator, or fall back to original messages
1421
+ const messages = (finalMessages ??
1422
+ options.messages) as unknown as ModelMessage[];
1423
+
1424
+ // Parse structured output if experimental_output is specified
1425
+ let experimentalOutput: OUTPUT = undefined as OUTPUT;
1426
+ if (options.experimental_output && steps.length > 0) {
1427
+ const lastStep = steps[steps.length - 1];
1428
+ const text = lastStep.text;
1429
+ if (text) {
1430
+ try {
1431
+ experimentalOutput =
1432
+ await options.experimental_output.parseCompleteOutput(
1433
+ { text },
1434
+ {
1435
+ response: lastStep.response,
1436
+ usage: lastStep.usage,
1437
+ finishReason: lastStep.finishReason,
1438
+ },
1439
+ );
1440
+ } catch (parseError) {
1441
+ // If there's already an error, don't override it
1442
+ // If not, set this as the error
1443
+ if (!encounteredError) {
1444
+ encounteredError = parseError;
1445
+ }
1446
+ }
1447
+ }
1448
+ }
1449
+
1450
+ // Call onFinish callback if provided (always call, even on errors, but not on abort)
1451
+ if (mergedOnFinish && !wasAborted) {
1452
+ const lastStep = steps[steps.length - 1];
1453
+ await mergedOnFinish({
1454
+ steps,
1455
+ messages: messages as ModelMessage[],
1456
+ text: lastStep?.text ?? '',
1457
+ finishReason: lastStep?.finishReason ?? 'other',
1458
+ totalUsage: aggregateUsage(steps),
1459
+ experimental_context: experimentalContext,
1460
+ experimental_output: experimentalOutput,
1461
+ });
1462
+ }
1463
+
1464
+ // Re-throw any error that occurred
1465
+ if (encounteredError) {
1466
+ throw encounteredError;
1467
+ }
1468
+
1469
+ return {
1470
+ messages: messages as ModelMessage[],
1471
+ steps,
1472
+ toolCalls: lastStepToolCalls,
1473
+ toolResults: lastStepToolResults,
1474
+ experimental_output: experimentalOutput,
1475
+ };
1476
+ }
1477
+ }
1478
+
1479
+ /**
1480
+ * Filter tools to only include the specified active tools.
1481
+ */
1482
+ /**
1483
+ * Aggregate token usage across all steps.
1484
+ */
1485
+ function aggregateUsage(steps: StepResult<any, any>[]): LanguageModelUsage {
1486
+ let inputTokens = 0;
1487
+ let outputTokens = 0;
1488
+ for (const step of steps) {
1489
+ inputTokens += step.usage?.inputTokens ?? 0;
1490
+ outputTokens += step.usage?.outputTokens ?? 0;
1491
+ }
1492
+ return {
1493
+ inputTokens,
1494
+ outputTokens,
1495
+ totalTokens: inputTokens + outputTokens,
1496
+ } as LanguageModelUsage;
1497
+ }
1498
+
1499
+ function filterTools<TTools extends ToolSet>(
1500
+ tools: TTools,
1501
+ activeTools: string[],
1502
+ ): ToolSet {
1503
+ const filtered: ToolSet = {};
1504
+ for (const toolName of activeTools) {
1505
+ if (toolName in tools) {
1506
+ filtered[toolName] = tools[toolName];
1507
+ }
1508
+ }
1509
+ return filtered;
1510
+ }
1511
+
1512
+ /**
1513
+ * Safely parse tool call input JSON. Returns the parsed value or the raw string
1514
+ * if parsing fails (e.g., for tool calls that were repaired).
1515
+ */
1516
+ function safeParseInput(input: string | undefined): unknown {
1517
+ try {
1518
+ return JSON.parse(input || '{}');
1519
+ } catch {
1520
+ return input;
1521
+ }
1522
+ }
1523
+
1524
+ // Matches AI SDK's getErrorMessage from @ai-sdk/provider-utils
1525
+ function getErrorMessage(error: unknown): string {
1526
+ if (error == null) {
1527
+ return 'unknown error';
1528
+ }
1529
+
1530
+ if (typeof error === 'string') {
1531
+ return error;
1532
+ }
1533
+
1534
+ if (error instanceof Error) {
1535
+ return error.message;
1536
+ }
1537
+
1538
+ return JSON.stringify(error);
1539
+ }
1540
+
1541
+ function resolveProviderToolResult(
1542
+ toolCall: { toolCallId: string; toolName: string },
1543
+ providerExecutedToolResults?: Map<
1544
+ string,
1545
+ { toolCallId: string; toolName: string; result: unknown; isError?: boolean }
1546
+ >,
1547
+ ): LanguageModelV4ToolResultPart {
1548
+ const streamResult = providerExecutedToolResults?.get(toolCall.toolCallId);
1549
+ if (!streamResult) {
1550
+ console.warn(
1551
+ `[WorkflowAgent] Provider-executed tool "${toolCall.toolName}" (${toolCall.toolCallId}) ` +
1552
+ `did not receive a result from the stream. This may indicate a provider issue.`,
1553
+ );
1554
+ return {
1555
+ type: 'tool-result' as const,
1556
+ toolCallId: toolCall.toolCallId,
1557
+ toolName: toolCall.toolName,
1558
+ output: {
1559
+ type: 'text' as const,
1560
+ value: '',
1561
+ },
1562
+ };
1563
+ }
1564
+
1565
+ const result = streamResult.result;
1566
+ const isString = typeof result === 'string';
1567
+
1568
+ return {
1569
+ type: 'tool-result' as const,
1570
+ toolCallId: toolCall.toolCallId,
1571
+ toolName: toolCall.toolName,
1572
+ output: isString
1573
+ ? streamResult.isError
1574
+ ? { type: 'error-text' as const, value: result }
1575
+ : { type: 'text' as const, value: result }
1576
+ : streamResult.isError
1577
+ ? {
1578
+ type: 'error-json' as const,
1579
+ value: result as JSONValue,
1580
+ }
1581
+ : {
1582
+ type: 'json' as const,
1583
+ value: result as JSONValue,
1584
+ },
1585
+ };
1586
+ }
1587
+
1588
+ async function executeTool(
1589
+ toolCall: { toolCallId: string; toolName: string; input: unknown },
1590
+ tools: ToolSet,
1591
+ messages: LanguageModelV4Prompt,
1592
+ experimentalContext?: unknown,
1593
+ ): Promise<LanguageModelV4ToolResultPart> {
1594
+ const tool = tools[toolCall.toolName];
1595
+ if (!tool) throw new Error(`Tool "${toolCall.toolName}" not found`);
1596
+ if (typeof tool.execute !== 'function') {
1597
+ throw new Error(
1598
+ `Tool "${toolCall.toolName}" does not have an execute function. ` +
1599
+ `Client-side tools should be filtered before calling executeTool.`,
1600
+ );
1601
+ }
1602
+ // Input is already parsed and validated by streamModelCall's parseToolCall
1603
+ const parsedInput = toolCall.input;
1604
+
1605
+ try {
1606
+ // Extract execute function to avoid binding `this` to the tool object.
1607
+ // If we called `tool.execute(...)` directly, JavaScript would bind `this`
1608
+ // to `tool`, which contains non-serializable properties like `inputSchema`.
1609
+ // When the execute function is a workflow step (marked with 'use step'),
1610
+ // the step system captures `this` for serialization, causing failures.
1611
+ const { execute } = tool;
1612
+ const toolResult = await execute(parsedInput, {
1613
+ toolCallId: toolCall.toolCallId,
1614
+ // Pass the conversation messages to the tool so it has context about the conversation
1615
+ messages,
1616
+ // Pass experimental context to the tool
1617
+ experimental_context: experimentalContext,
1618
+ });
1619
+
1620
+ // Use the appropriate output type based on the result
1621
+ // AI SDK supports 'text' for strings and 'json' for objects
1622
+ const output =
1623
+ typeof toolResult === 'string'
1624
+ ? { type: 'text' as const, value: toolResult }
1625
+ : { type: 'json' as const, value: toolResult };
1626
+
1627
+ return {
1628
+ type: 'tool-result' as const,
1629
+ toolCallId: toolCall.toolCallId,
1630
+ toolName: toolCall.toolName,
1631
+ output,
1632
+ };
1633
+ } catch (error) {
1634
+ // Convert tool errors to error-text results sent back to the model,
1635
+ // allowing the agent to recover rather than killing the entire stream.
1636
+ // This aligns with AI SDK's streamText behavior for individual tool failures.
1637
+ return {
1638
+ type: 'tool-result',
1639
+ toolCallId: toolCall.toolCallId,
1640
+ toolName: toolCall.toolName,
1641
+ output: {
1642
+ type: 'error-text',
1643
+ value: getErrorMessage(error),
1644
+ },
1645
+ };
1646
+ }
1647
+ }