@ai-sdk/workflow 1.0.0-beta.10 → 1.0.0-beta.100

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.
@@ -1,5 +1,4 @@
1
1
  import type {
2
- JSONValue,
3
2
  LanguageModelV4CallOptions,
4
3
  LanguageModelV4Prompt,
5
4
  LanguageModelV4StreamPart,
@@ -7,48 +6,74 @@ import type {
7
6
  SharedV4ProviderOptions,
8
7
  } from '@ai-sdk/provider';
9
8
  import {
10
- type Experimental_LanguageModelStreamPart as ModelCallStreamPart,
9
+ getErrorMessage,
10
+ validateTypes,
11
+ type Context,
12
+ type HasRequiredKey,
13
+ type InferToolSetContext,
14
+ } from '@ai-sdk/provider-utils';
15
+ import {
16
+ Output,
17
+ experimental_filterActiveTools as filterActiveTools,
11
18
  type FinishReason,
12
19
  type LanguageModelResponseMetadata,
13
20
  type LanguageModelUsage,
21
+ type Experimental_LanguageModelStreamPart as ModelCallStreamPart,
14
22
  type ModelMessage,
15
- Output,
16
23
  type StepResult,
17
24
  type StopCondition,
18
- type StreamTextOnStepFinishCallback,
19
- type SystemModelMessage,
25
+ type GenerateTextOnStepEndCallback,
26
+ type ActiveTools,
20
27
  type ToolCallRepairFunction,
21
28
  type ToolChoice,
22
29
  type ToolSet,
23
30
  type UIMessage,
24
- LanguageModel,
31
+ type LanguageModel,
32
+ type Prompt,
33
+ type TelemetryOptions as CoreTelemetryOptions,
34
+ type Instructions,
25
35
  } from 'ai';
26
36
  import {
37
+ createRestrictedTelemetryDispatcher,
38
+ collectToolApprovals,
27
39
  convertToLanguageModelPrompt,
28
40
  mergeAbortSignals,
29
41
  mergeCallbacks,
30
42
  standardizePrompt,
43
+ validateApprovedToolApprovals,
31
44
  } from 'ai/internal';
45
+ import { createLanguageModelToolResultOutput } from './create-language-model-tool-result-output.js';
32
46
  import { streamTextIterator } from './stream-text-iterator.js';
33
- import type { CompatibleLanguageModel } from './types.js';
34
47
 
35
48
  // Re-export for consumers
36
49
  export type { CompatibleLanguageModel } from './types.js';
37
50
 
38
51
  /**
39
52
  * Callback function to be called after each step completes.
40
- * Alias for the AI SDK's StreamTextOnStepFinishCallback, using
53
+ * Alias for the AI SDK's GenerateTextOnStepEndCallback, using
41
54
  * WorkflowAgent-consistent naming.
42
55
  */
56
+ export type WorkflowAgentOnStepEndCallback<
57
+ TTools extends ToolSet = ToolSet,
58
+ TRuntimeContext extends Context = Context,
59
+ > = GenerateTextOnStepEndCallback<TTools, TRuntimeContext>;
60
+
61
+ /**
62
+ * Callback function to be called after each step completes.
63
+ * Deprecated alias for `WorkflowAgentOnStepEndCallback`.
64
+ *
65
+ * @deprecated Use `WorkflowAgentOnStepEndCallback` instead.
66
+ */
43
67
  export type WorkflowAgentOnStepFinishCallback<
44
68
  TTools extends ToolSet = ToolSet,
45
- > = StreamTextOnStepFinishCallback<TTools, any>;
69
+ TRuntimeContext extends Context = Context,
70
+ > = WorkflowAgentOnStepEndCallback<TTools, TRuntimeContext>;
46
71
 
47
72
  /**
48
73
  * Infer the type of the tools of a workflow agent.
49
74
  */
50
75
  export type InferWorkflowAgentTools<WORKFLOW_AGENT> =
51
- WORKFLOW_AGENT extends WorkflowAgent<infer TOOLS> ? TOOLS : never;
76
+ WORKFLOW_AGENT extends WorkflowAgent<infer TOOLS, any> ? TOOLS : never;
52
77
 
53
78
  /**
54
79
  * Infer the UI message type of a workflow agent.
@@ -89,54 +114,14 @@ export interface OutputSpecification<OUTPUT, PARTIAL> {
89
114
  */
90
115
  export type ProviderOptions = SharedV4ProviderOptions;
91
116
 
92
- /**
93
- * Telemetry settings for observability.
94
- */
95
- export interface TelemetrySettings {
96
- /**
97
- * Enable or disable telemetry. Defaults to true.
98
- */
99
- isEnabled?: boolean;
100
-
101
- /**
102
- * Identifier for this function. Used to group telemetry data by function.
103
- */
104
- functionId?: string;
105
-
106
- /**
107
- * Additional information to include in the telemetry data.
108
- */
109
- metadata?: Record<
110
- string,
111
- | string
112
- | number
113
- | boolean
114
- | Array<string | number | boolean>
115
- | null
116
- | undefined
117
- >;
118
-
119
- /**
120
- * Enable or disable input recording. Enabled by default.
121
- *
122
- * You might want to disable input recording to avoid recording sensitive
123
- * information, to reduce data transfers, or to increase performance.
124
- */
125
- recordInputs?: boolean;
126
-
127
- /**
128
- * Enable or disable output recording. Enabled by default.
129
- *
130
- * You might want to disable output recording to avoid recording sensitive
131
- * information, to reduce data transfers, or to increase performance.
132
- */
133
- recordOutputs?: boolean;
134
-
135
- /**
136
- * Custom tracer for the telemetry.
137
- */
138
- tracer?: unknown;
139
- }
117
+ type WorkflowAgentToolsContextParameter<TTools extends ToolSet> =
118
+ HasRequiredKey<InferToolSetContext<TTools>> extends true
119
+ ? { toolsContext: InferToolSetContext<TTools> }
120
+ : { toolsContext?: never };
121
+ export type TelemetryOptions<
122
+ TRuntimeContext extends Context = Context,
123
+ TTools extends ToolSet = ToolSet,
124
+ > = CoreTelemetryOptions<TRuntimeContext, TTools>;
140
125
 
141
126
  /**
142
127
  * A transformation that is applied to the stream.
@@ -253,7 +238,10 @@ export interface GenerationSettings {
253
238
  /**
254
239
  * Information passed to the prepareStep callback.
255
240
  */
256
- export interface PrepareStepInfo<TTools extends ToolSet = ToolSet> {
241
+ export interface PrepareStepInfo<
242
+ TTools extends ToolSet = ToolSet,
243
+ TRuntimeContext extends Context = Context,
244
+ > {
257
245
  /**
258
246
  * The current model configuration (string or function).
259
247
  * The function should return a LanguageModelV4 instance.
@@ -268,7 +256,7 @@ export interface PrepareStepInfo<TTools extends ToolSet = ToolSet> {
268
256
  /**
269
257
  * All previous steps with their results.
270
258
  */
271
- steps: StepResult<TTools, any>[];
259
+ steps: StepResult<TTools, TRuntimeContext>[];
272
260
 
273
261
  /**
274
262
  * The messages that will be sent to the model.
@@ -277,16 +265,29 @@ export interface PrepareStepInfo<TTools extends ToolSet = ToolSet> {
277
265
  messages: LanguageModelV4Prompt;
278
266
 
279
267
  /**
280
- * The context passed via the experimental_context setting (experimental).
268
+ * The runtime context that flows through the agent loop.
269
+ * Treat the value as immutable; return a new `runtimeContext` from
270
+ * `prepareStep` to update it for the current and subsequent steps.
281
271
  */
282
- experimental_context: unknown;
272
+ runtimeContext: TRuntimeContext;
273
+
274
+ /**
275
+ * Per-tool context, keyed by tool name. Each tool receives only its own
276
+ * validated entry as `context` during execution.
277
+ * Treat the value as immutable; return a new `toolsContext` from
278
+ * `prepareStep` to update it for the current and subsequent steps.
279
+ */
280
+ toolsContext: InferToolSetContext<TTools>;
283
281
  }
284
282
 
285
283
  /**
286
284
  * Return type from the prepareStep callback.
287
285
  * All properties are optional - only return the ones you want to override.
288
286
  */
289
- export interface PrepareStepResult extends Partial<GenerationSettings> {
287
+ export interface PrepareStepResult<
288
+ TTools extends ToolSet = ToolSet,
289
+ TRuntimeContext extends Context = Context,
290
+ > extends Partial<GenerationSettings> {
290
291
  /**
291
292
  * Override the model for this step.
292
293
  */
@@ -315,32 +316,53 @@ export interface PrepareStepResult extends Partial<GenerationSettings> {
315
316
  activeTools?: string[];
316
317
 
317
318
  /**
318
- * Context that is passed into tool execution. Experimental.
319
- * Changing the context will affect the context in this step and all subsequent steps.
319
+ * Updated runtime context for the current and subsequent steps.
320
+ * Returning a value replaces the agent's runtime context.
321
+ */
322
+ runtimeContext?: TRuntimeContext;
323
+
324
+ /**
325
+ * Updated per-tool context for the current and subsequent steps.
326
+ * Returning a value replaces the agent's tools context.
320
327
  */
321
- experimental_context?: unknown;
328
+ toolsContext?: InferToolSetContext<TTools>;
322
329
  }
323
330
 
324
331
  /**
325
332
  * Callback function called before each step in the agent loop.
326
333
  * Use this to modify settings, manage context, or implement dynamic behavior.
327
334
  */
328
- export type PrepareStepCallback<TTools extends ToolSet = ToolSet> = (
329
- info: PrepareStepInfo<TTools>,
330
- ) => PrepareStepResult | Promise<PrepareStepResult>;
335
+ export type PrepareStepCallback<
336
+ TTools extends ToolSet = ToolSet,
337
+ TRuntimeContext extends Context = Context,
338
+ > = (
339
+ info: PrepareStepInfo<TTools, TRuntimeContext>,
340
+ ) =>
341
+ | PrepareStepResult<TTools, TRuntimeContext>
342
+ | undefined
343
+ | Promise<PrepareStepResult<TTools, TRuntimeContext> | undefined>;
331
344
 
332
345
  /**
333
346
  * Options passed to the prepareCall callback.
334
347
  */
335
348
  export interface PrepareCallOptions<
336
349
  TTools extends ToolSet = ToolSet,
350
+ TRuntimeContext extends Context = Context,
337
351
  > extends Partial<GenerationSettings> {
338
352
  model: LanguageModel;
339
353
  tools: TTools;
340
- instructions?: string | SystemModelMessage | Array<SystemModelMessage>;
354
+ instructions?: Instructions;
341
355
  toolChoice?: ToolChoice<TTools>;
342
- experimental_telemetry?: TelemetrySettings;
343
- experimental_context?: unknown;
356
+ telemetry?: TelemetryOptions<TRuntimeContext, TTools>;
357
+ /**
358
+ * Runtime context that flows through the agent loop.
359
+ * Treat as immutable; return a new `runtimeContext` to update it for the call.
360
+ */
361
+ runtimeContext?: TRuntimeContext;
362
+ /**
363
+ * Per-tool context, keyed by tool name.
364
+ */
365
+ toolsContext?: InferToolSetContext<TTools>;
344
366
  messages: ModelMessage[];
345
367
  }
346
368
 
@@ -350,172 +372,207 @@ export interface PrepareCallOptions<
350
372
  * Note: `tools` cannot be overridden via prepareCall because they are
351
373
  * bound at construction time for type safety.
352
374
  */
353
- export type PrepareCallResult<TTools extends ToolSet = ToolSet> = Partial<
354
- Omit<PrepareCallOptions<TTools>, 'tools'>
355
- >;
375
+ export type PrepareCallResult<
376
+ TTools extends ToolSet = ToolSet,
377
+ TRuntimeContext extends Context = Context,
378
+ > = Partial<Omit<PrepareCallOptions<TTools, TRuntimeContext>, 'tools'>>;
356
379
 
357
380
  /**
358
381
  * Callback called once before the agent loop starts to transform call parameters.
359
382
  */
360
- export type PrepareCallCallback<TTools extends ToolSet = ToolSet> = (
361
- options: PrepareCallOptions<TTools>,
362
- ) => PrepareCallResult<TTools> | Promise<PrepareCallResult<TTools>>;
383
+ export type PrepareCallCallback<
384
+ TTools extends ToolSet = ToolSet,
385
+ TRuntimeContext extends Context = Context,
386
+ > = (
387
+ options: PrepareCallOptions<TTools, TRuntimeContext>,
388
+ ) =>
389
+ | PrepareCallResult<TTools, TRuntimeContext>
390
+ | Promise<PrepareCallResult<TTools, TRuntimeContext>>;
363
391
 
364
392
  /**
365
393
  * Configuration options for creating a {@link WorkflowAgent} instance.
366
394
  */
367
- export interface WorkflowAgentOptions<
395
+ export type WorkflowAgentOptions<
368
396
  TTools extends ToolSet = ToolSet,
369
- > extends GenerationSettings {
370
- /**
371
- * The id of the agent.
372
- */
373
- id?: string;
397
+ TRuntimeContext extends Context = Context,
398
+ > = GenerationSettings &
399
+ WorkflowAgentToolsContextParameter<TTools> & {
400
+ /**
401
+ * The id of the agent.
402
+ */
403
+ id?: string;
374
404
 
375
- /**
376
- * The model provider to use for the agent.
377
- *
378
- * This should be a string compatible with the Vercel AI Gateway (e.g., 'anthropic/claude-opus'),
379
- * or a LanguageModelV4 instance from a provider.
380
- */
381
- model: LanguageModel;
405
+ /**
406
+ * The model provider to use for the agent.
407
+ *
408
+ * This should be a string compatible with the Vercel AI Gateway (e.g., 'anthropic/claude-opus'),
409
+ * or a LanguageModelV4 instance from a provider.
410
+ */
411
+ model: LanguageModel;
382
412
 
383
- /**
384
- * A set of tools available to the agent.
385
- * Tools can be implemented as workflow steps for automatic retries and persistence,
386
- * or as regular workflow-level logic using core library features like sleep() and Hooks.
387
- */
388
- tools?: TTools;
413
+ /**
414
+ * A set of tools available to the agent.
415
+ * Tools can be implemented as workflow steps for automatic retries and persistence,
416
+ * or as regular workflow-level logic using core library features like sleep() and Hooks.
417
+ */
418
+ tools?: TTools;
389
419
 
390
- /**
391
- * Agent instructions. Can be a string, a SystemModelMessage, or an array of SystemModelMessages.
392
- * Supports provider-specific options (e.g., caching) when using the SystemModelMessage form.
393
- */
394
- instructions?: string | SystemModelMessage | Array<SystemModelMessage>;
420
+ /**
421
+ * Agent instructions. Can be a string, a SystemModelMessage, or an array of SystemModelMessages.
422
+ * Supports provider-specific options (e.g., caching) when using the SystemModelMessage form.
423
+ */
424
+ instructions?: Instructions;
395
425
 
396
- /**
397
- * Optional system prompt to guide the agent's behavior.
398
- * @deprecated Use `instructions` instead.
399
- */
400
- system?: string;
426
+ /**
427
+ * Optional system prompt to guide the agent's behavior.
428
+ * @deprecated Use `instructions` instead.
429
+ */
430
+ system?: string;
401
431
 
402
- /**
403
- * The tool choice strategy. Default: 'auto'.
404
- */
405
- toolChoice?: ToolChoice<TTools>;
432
+ /**
433
+ * The tool choice strategy. Default: 'auto'.
434
+ */
435
+ toolChoice?: ToolChoice<TTools>;
406
436
 
407
- /**
408
- * Optional telemetry configuration (experimental).
409
- */
410
- experimental_telemetry?: TelemetrySettings;
437
+ /**
438
+ * Optional telemetry configuration.
439
+ */
440
+ telemetry?: TelemetryOptions<TRuntimeContext, TTools>;
411
441
 
412
- /**
413
- * Default context that is passed into tool execution for every stream call on this agent.
414
- *
415
- * Per-stream `experimental_context` values passed to `stream()` override this default.
416
- * Experimental (can break in patch releases).
417
- * @default undefined
418
- */
419
- experimental_context?: unknown;
442
+ /**
443
+ * Default runtime context for every stream call on this agent.
444
+ *
445
+ * The runtime context flows through `prepareStep`, lifecycle callbacks,
446
+ * and step results.
447
+ * Treat as immutable; return a new `runtimeContext` from `prepareStep`
448
+ * to update it between steps.
449
+ *
450
+ * In workflow context, keep values serializable so they can cross workflow
451
+ * and step boundaries.
452
+ *
453
+ * Per-stream `runtimeContext` values passed to `stream()` override this default.
454
+ */
455
+ runtimeContext?: TRuntimeContext;
420
456
 
421
- /**
422
- * Default stop condition for the agent loop. When the condition is an array,
423
- * any of the conditions can be met to stop the generation.
424
- *
425
- * Per-stream `stopWhen` values passed to `stream()` override this default.
426
- */
427
- stopWhen?:
428
- | StopCondition<NoInfer<ToolSet>, any>
429
- | Array<StopCondition<NoInfer<ToolSet>, any>>;
457
+ /**
458
+ * Default stop condition for the agent loop. When the condition is an array,
459
+ * any of the conditions can be met to stop the generation.
460
+ *
461
+ * Per-stream `stopWhen` values passed to `stream()` override this default.
462
+ */
463
+ stopWhen?:
464
+ | StopCondition<NoInfer<ToolSet>, any>
465
+ | Array<StopCondition<NoInfer<ToolSet>, any>>;
430
466
 
431
- /**
432
- * Default set of active tools that limits which tools the model can call,
433
- * without changing the tool call and result types in the result.
434
- *
435
- * Per-stream `activeTools` values passed to `stream()` override this default.
436
- */
437
- activeTools?: Array<keyof NoInfer<TTools>>;
467
+ /**
468
+ * Default set of active tools that limits which tools the model can call,
469
+ * without changing the tool call and result types in the result.
470
+ *
471
+ * Per-stream `activeTools` values passed to `stream()` override this default.
472
+ */
473
+ activeTools?: ActiveTools<NoInfer<TTools>>;
438
474
 
439
- /**
440
- * Default output specification for structured outputs.
441
- * Use `Output.object({ schema })` for structured output or `Output.text()` for text output.
442
- *
443
- * Per-stream `output` values passed to `stream()` override this default.
444
- */
445
- output?: OutputSpecification<any, any>;
475
+ /**
476
+ * Default output specification for structured outputs.
477
+ * Use `Output.object({ schema })` for structured output or `Output.text()` for text output.
478
+ *
479
+ * Per-stream `output` values passed to `stream()` override this default.
480
+ */
481
+ output?: OutputSpecification<any, any>;
446
482
 
447
- /**
448
- * Default function that attempts to repair a tool call that failed to parse.
449
- *
450
- * Per-stream `experimental_repairToolCall` values passed to `stream()` override this default.
451
- */
452
- experimental_repairToolCall?: ToolCallRepairFunction<TTools>;
483
+ /**
484
+ * Default function that attempts to repair a tool call that failed to parse.
485
+ *
486
+ * Per-stream `experimental_repairToolCall` values passed to `stream()` override this default.
487
+ */
488
+ experimental_repairToolCall?: ToolCallRepairFunction<TTools>;
453
489
 
454
- /**
455
- * Default custom download function to use for URLs.
456
- *
457
- * Per-stream `experimental_download` values passed to `stream()` override this default.
458
- */
459
- experimental_download?: DownloadFunction;
490
+ /**
491
+ * Default custom download function to use for URLs.
492
+ *
493
+ * Per-stream `experimental_download` values passed to `stream()` override this default.
494
+ */
495
+ experimental_download?: DownloadFunction;
460
496
 
461
- /**
462
- * Default callback function called before each step in the agent loop.
463
- * Use this to modify settings, manage context, or inject messages dynamically
464
- * for every stream call on this agent instance.
465
- *
466
- * Per-stream `prepareStep` values passed to `stream()` override this default.
467
- */
468
- prepareStep?: PrepareStepCallback<TTools>;
497
+ /**
498
+ * Default callback function called before each step in the agent loop.
499
+ * Use this to modify settings, manage context, or inject messages dynamically
500
+ * for every stream call on this agent instance.
501
+ *
502
+ * Per-stream `prepareStep` values passed to `stream()` override this default.
503
+ */
504
+ prepareStep?: PrepareStepCallback<TTools, TRuntimeContext>;
469
505
 
470
- /**
471
- * Callback function to be called after each step completes.
472
- */
473
- onStepFinish?: WorkflowAgentOnStepFinishCallback<ToolSet>;
506
+ /**
507
+ * Callback function to be called after each step completes.
508
+ */
509
+ onStepEnd?: WorkflowAgentOnStepEndCallback<TTools, TRuntimeContext>;
474
510
 
475
- /**
476
- * Callback that is called when the LLM response and all request tool executions are finished.
477
- */
478
- onFinish?: WorkflowAgentOnFinishCallback<ToolSet>;
511
+ /**
512
+ * Callback function to be called after each step completes.
513
+ *
514
+ * @deprecated Use `onStepEnd` instead.
515
+ */
516
+ onStepFinish?: WorkflowAgentOnStepFinishCallback<TTools, TRuntimeContext>;
479
517
 
480
- /**
481
- * Callback called when the agent starts streaming, before any LLM calls.
482
- */
483
- experimental_onStart?: WorkflowAgentOnStartCallback;
518
+ /**
519
+ * Callback that is called when the LLM response and all request tool executions are finished.
520
+ */
521
+ onEnd?: WorkflowAgentOnEndCallback<TTools, TRuntimeContext>;
484
522
 
485
- /**
486
- * Callback called before each step (LLM call) begins.
487
- */
488
- experimental_onStepStart?: WorkflowAgentOnStepStartCallback;
523
+ /**
524
+ * Callback that is called when the LLM response and all request tool executions are finished.
525
+ *
526
+ * @deprecated Use `onEnd` instead.
527
+ */
528
+ onFinish?: WorkflowAgentOnFinishCallback<TTools, TRuntimeContext>;
489
529
 
490
- /**
491
- * Callback called before a tool's execute function runs.
492
- */
493
- experimental_onToolCallStart?: WorkflowAgentOnToolCallStartCallback;
530
+ /**
531
+ * Callback called when the agent starts streaming, before any LLM calls.
532
+ */
533
+ experimental_onStart?: WorkflowAgentOnStartCallback<
534
+ TTools,
535
+ TRuntimeContext
536
+ >;
494
537
 
495
- /**
496
- * Callback called after a tool execution completes.
497
- */
498
- experimental_onToolCallFinish?: WorkflowAgentOnToolCallFinishCallback;
538
+ /**
539
+ * Callback called before each step (LLM call) begins.
540
+ */
541
+ experimental_onStepStart?: WorkflowAgentOnStepStartCallback<
542
+ TTools,
543
+ TRuntimeContext
544
+ >;
499
545
 
500
- /**
501
- * Prepare the parameters for the stream call.
502
- * Called once before the agent loop starts. Use this to transform
503
- * model, tools, instructions, or other settings based on runtime context.
504
- */
505
- prepareCall?: PrepareCallCallback<TTools>;
506
- }
546
+ /**
547
+ * Callback called before a tool's execute function runs.
548
+ */
549
+ onToolExecutionStart?: WorkflowAgentOnToolExecutionStartCallback<TTools>;
550
+
551
+ /**
552
+ * Callback called after a tool execution completes.
553
+ */
554
+ onToolExecutionEnd?: WorkflowAgentOnToolExecutionEndCallback<TTools>;
555
+
556
+ /**
557
+ * Prepare the parameters for the stream call.
558
+ * Called once before the agent loop starts. Use this to transform
559
+ * model, tools, instructions, or other settings based on runtime context.
560
+ */
561
+ prepareCall?: PrepareCallCallback<TTools, TRuntimeContext>;
562
+ };
507
563
 
508
564
  /**
509
565
  * Callback that is called when the LLM response and all request tool executions are finished.
510
566
  */
511
- export type WorkflowAgentOnFinishCallback<
567
+ export type WorkflowAgentOnEndCallback<
512
568
  TTools extends ToolSet = ToolSet,
569
+ TRuntimeContext extends Context = Context,
513
570
  OUTPUT = never,
514
571
  > = (event: {
515
572
  /**
516
573
  * Details for all steps.
517
574
  */
518
- readonly steps: StepResult<TTools, any>[];
575
+ readonly steps: StepResult<TTools, TRuntimeContext>[];
519
576
 
520
577
  /**
521
578
  * The final messages including all tool calls and results.
@@ -532,15 +589,25 @@ export type WorkflowAgentOnFinishCallback<
532
589
  */
533
590
  readonly finishReason: FinishReason;
534
591
 
592
+ /**
593
+ * The total token usage across all steps.
594
+ */
595
+ readonly usage: LanguageModelUsage;
596
+
535
597
  /**
536
598
  * The total token usage across all steps.
537
599
  */
538
600
  readonly totalUsage: LanguageModelUsage;
539
601
 
540
602
  /**
541
- * Context that is passed into tool execution.
603
+ * The runtime context at the end of the agent loop.
604
+ */
605
+ readonly runtimeContext: TRuntimeContext;
606
+
607
+ /**
608
+ * The per-tool context at the end of the agent loop.
542
609
  */
543
- readonly experimental_context: unknown;
610
+ readonly toolsContext: InferToolSetContext<TTools>;
544
611
 
545
612
  /**
546
613
  * The generated structured output. It uses the `output` specification.
@@ -549,6 +616,17 @@ export type WorkflowAgentOnFinishCallback<
549
616
  readonly output: OUTPUT;
550
617
  }) => PromiseLike<void> | void;
551
618
 
619
+ /**
620
+ * Callback that is called when the LLM response and all request tool executions are finished.
621
+ *
622
+ * @deprecated Use `WorkflowAgentOnEndCallback` instead.
623
+ */
624
+ export type WorkflowAgentOnFinishCallback<
625
+ TTools extends ToolSet = ToolSet,
626
+ TRuntimeContext extends Context = Context,
627
+ OUTPUT = never,
628
+ > = WorkflowAgentOnEndCallback<TTools, TRuntimeContext, OUTPUT>;
629
+
552
630
  /**
553
631
  * Callback that is invoked when an error occurs during streaming.
554
632
  */
@@ -570,36 +648,57 @@ export type WorkflowAgentOnAbortCallback<TTools extends ToolSet = ToolSet> =
570
648
  /**
571
649
  * Callback that is called when the agent starts streaming, before any LLM calls.
572
650
  */
573
- export type WorkflowAgentOnStartCallback = (event: {
651
+ export type WorkflowAgentOnStartCallback<
652
+ TTools extends ToolSet = ToolSet,
653
+ TRuntimeContext extends Context = Context,
654
+ > = (event: {
574
655
  /** The model being used */
575
656
  readonly model: LanguageModel;
576
657
  /** The messages being sent */
577
658
  readonly messages: ModelMessage[];
659
+ /** Shared runtime context for this agent loop */
660
+ readonly runtimeContext: TRuntimeContext;
661
+ /** Per-tool context map for this agent loop */
662
+ readonly toolsContext: InferToolSetContext<TTools>;
578
663
  }) => PromiseLike<void> | void;
579
664
 
580
665
  /**
581
666
  * Callback that is called before each step (LLM call) begins.
582
667
  */
583
- export type WorkflowAgentOnStepStartCallback<TTools extends ToolSet = ToolSet> =
584
- (event: {
585
- /** The current step number (0-based) */
586
- readonly stepNumber: number;
587
- /** The model being used for this step */
588
- readonly model: LanguageModel;
589
- /** The messages being sent for this step */
590
- readonly messages: ModelMessage[];
591
- /** Results from all previously finished steps */
592
- readonly steps: ReadonlyArray<StepResult<TTools, any>>;
593
- }) => PromiseLike<void> | void;
668
+ export type WorkflowAgentOnStepStartCallback<
669
+ TTools extends ToolSet = ToolSet,
670
+ TRuntimeContext extends Context = Context,
671
+ > = (event: {
672
+ /** The current step number (0-based) */
673
+ readonly stepNumber: number;
674
+ /** The model being used for this step */
675
+ readonly model: LanguageModel;
676
+ /** The messages being sent for this step */
677
+ readonly messages: ModelMessage[];
678
+ /** Results from all previously finished steps */
679
+ readonly steps: ReadonlyArray<StepResult<TTools, TRuntimeContext>>;
680
+ /** Shared runtime context for this step */
681
+ readonly runtimeContext: TRuntimeContext;
682
+ /** Per-tool context map for this step */
683
+ readonly toolsContext: InferToolSetContext<TTools>;
684
+ }) => PromiseLike<void> | void;
594
685
 
595
686
  /**
596
687
  * Callback that is called before a tool's execute function runs.
597
688
  */
598
- export type WorkflowAgentOnToolCallStartCallback = (event: {
689
+ export type WorkflowAgentOnToolExecutionStartCallback<
690
+ TTools extends ToolSet = ToolSet,
691
+ > = (event: {
599
692
  /** The tool call being executed */
600
693
  readonly toolCall: ToolCall;
601
694
  /** The current step number (0-based) */
602
695
  readonly stepNumber: number;
696
+ /** Messages sent to the language model for the step that produced the call */
697
+ readonly messages: ModelMessage[];
698
+ /** Tool-specific context passed to the tool */
699
+ readonly toolContext:
700
+ | InferToolSetContext<TTools>[keyof InferToolSetContext<TTools>]
701
+ | undefined;
603
702
  }) => PromiseLike<void> | void;
604
703
 
605
704
  /**
@@ -607,7 +706,9 @@ export type WorkflowAgentOnToolCallStartCallback = (event: {
607
706
  * Uses a discriminated union pattern: check `success` to determine
608
707
  * whether `output` or `error` is available.
609
708
  */
610
- export type WorkflowAgentOnToolCallFinishCallback = (
709
+ export type WorkflowAgentOnToolExecutionEndCallback<
710
+ TTools extends ToolSet = ToolSet,
711
+ > = (
611
712
  event:
612
713
  | {
613
714
  /** The tool call that was executed */
@@ -616,6 +717,12 @@ export type WorkflowAgentOnToolCallFinishCallback = (
616
717
  readonly stepNumber: number;
617
718
  /** Execution time in milliseconds */
618
719
  readonly durationMs: number;
720
+ /** Messages sent to the language model for the step that produced the call */
721
+ readonly messages: ModelMessage[];
722
+ /** Tool-specific context passed to the tool */
723
+ readonly toolContext:
724
+ | InferToolSetContext<TTools>[keyof InferToolSetContext<TTools>]
725
+ | undefined;
619
726
  /** Whether the tool call succeeded */
620
727
  readonly success: true;
621
728
  /** The tool result */
@@ -629,6 +736,12 @@ export type WorkflowAgentOnToolCallFinishCallback = (
629
736
  readonly stepNumber: number;
630
737
  /** Execution time in milliseconds */
631
738
  readonly durationMs: number;
739
+ /** Messages sent to the language model for the step that produced the call */
740
+ readonly messages: ModelMessage[];
741
+ /** Tool-specific context passed to the tool */
742
+ readonly toolContext:
743
+ | InferToolSetContext<TTools>[keyof InferToolSetContext<TTools>]
744
+ | undefined;
632
745
  /** Whether the tool call succeeded */
633
746
  readonly success: false;
634
747
  /** The error that occurred */
@@ -642,6 +755,7 @@ export type WorkflowAgentOnToolCallFinishCallback = (
642
755
  */
643
756
  export type WorkflowAgentStreamOptions<
644
757
  TTools extends ToolSet = ToolSet,
758
+ TRuntimeContext extends Context = Context,
645
759
  OUTPUT = never,
646
760
  PARTIAL_OUTPUT = never,
647
761
  > = Partial<GenerationSettings> &
@@ -714,13 +828,6 @@ export type WorkflowAgentStreamOptions<
714
828
  | StopCondition<NoInfer<ToolSet>, any>
715
829
  | Array<StopCondition<NoInfer<ToolSet>, any>>;
716
830
 
717
- /**
718
- * Maximum number of sequential LLM calls (steps), e.g. when you use tool calls.
719
- * A maximum number can be set to prevent infinite loops in the case of misconfigured tools.
720
- * By default, it's unlimited (the agent loops until completion).
721
- */
722
- maxSteps?: number;
723
-
724
831
  /**
725
832
  * The tool choice strategy. Default: 'auto'.
726
833
  * Overrides the toolChoice from the constructor if provided.
@@ -731,19 +838,37 @@ export type WorkflowAgentStreamOptions<
731
838
  * Limits the tools that are available for the model to call without
732
839
  * changing the tool call and result types in the result.
733
840
  */
734
- activeTools?: Array<keyof NoInfer<TTools>>;
841
+ activeTools?: ActiveTools<NoInfer<TTools>>;
735
842
 
736
843
  /**
737
- * Optional telemetry configuration (experimental).
844
+ * Optional telemetry configuration.
738
845
  */
739
- experimental_telemetry?: TelemetrySettings;
846
+ telemetry?: TelemetryOptions<TRuntimeContext, TTools>;
740
847
 
741
848
  /**
742
- * Context that is passed into tool execution.
743
- * Experimental (can break in patch releases).
744
- * @default undefined
849
+ * Runtime context that flows through the agent loop.
850
+ *
851
+ * Treat as immutable; return a new `runtimeContext` from `prepareStep`
852
+ * to update it between steps.
853
+ *
854
+ * In workflow context, keep values serializable so they can cross workflow
855
+ * and step boundaries.
856
+ *
857
+ * Overrides the constructor-level `runtimeContext` if provided.
745
858
  */
746
- experimental_context?: unknown;
859
+ runtimeContext?: TRuntimeContext;
860
+
861
+ /**
862
+ * Per-tool context, keyed by tool name. Each tool receives only its own
863
+ * validated entry as `context` during execution. Tools that declare a
864
+ * `contextSchema` validate their entry against the schema.
865
+ *
866
+ * In workflow context, keep values serializable so they can cross workflow
867
+ * and step boundaries.
868
+ *
869
+ * Overrides the constructor-level `toolsContext` if provided.
870
+ */
871
+ toolsContext?: InferToolSetContext<TTools>;
747
872
 
748
873
  /**
749
874
  * Optional specification for parsing structured outputs from the LLM response.
@@ -801,7 +926,14 @@ export type WorkflowAgentStreamOptions<
801
926
  /**
802
927
  * Callback function to be called after each step completes.
803
928
  */
804
- onStepFinish?: WorkflowAgentOnStepFinishCallback<TTools>;
929
+ onStepEnd?: WorkflowAgentOnStepEndCallback<TTools, TRuntimeContext>;
930
+
931
+ /**
932
+ * Callback function to be called after each step completes.
933
+ *
934
+ * @deprecated Use `onStepEnd` instead.
935
+ */
936
+ onStepFinish?: WorkflowAgentOnStepFinishCallback<TTools, TRuntimeContext>;
805
937
 
806
938
  /**
807
939
  * Callback that is invoked when an error occurs during streaming.
@@ -813,7 +945,15 @@ export type WorkflowAgentStreamOptions<
813
945
  * Callback that is called when the LLM response and all request tool executions
814
946
  * (for tools that have an `execute` function) are finished.
815
947
  */
816
- onFinish?: WorkflowAgentOnFinishCallback<TTools, OUTPUT>;
948
+ onEnd?: WorkflowAgentOnEndCallback<TTools, TRuntimeContext, OUTPUT>;
949
+
950
+ /**
951
+ * Callback that is called when the LLM response and all request tool executions
952
+ * (for tools that have an `execute` function) are finished.
953
+ *
954
+ * @deprecated Use `onEnd` instead.
955
+ */
956
+ onFinish?: WorkflowAgentOnFinishCallback<TTools, TRuntimeContext, OUTPUT>;
817
957
 
818
958
  /**
819
959
  * Callback that is called when the operation is aborted.
@@ -823,22 +963,28 @@ export type WorkflowAgentStreamOptions<
823
963
  /**
824
964
  * Callback called when the agent starts streaming, before any LLM calls.
825
965
  */
826
- experimental_onStart?: WorkflowAgentOnStartCallback;
966
+ experimental_onStart?: WorkflowAgentOnStartCallback<
967
+ TTools,
968
+ TRuntimeContext
969
+ >;
827
970
 
828
971
  /**
829
972
  * Callback called before each step (LLM call) begins.
830
973
  */
831
- experimental_onStepStart?: WorkflowAgentOnStepStartCallback;
974
+ experimental_onStepStart?: WorkflowAgentOnStepStartCallback<
975
+ TTools,
976
+ TRuntimeContext
977
+ >;
832
978
 
833
979
  /**
834
980
  * Callback called before a tool's execute function runs.
835
981
  */
836
- experimental_onToolCallStart?: WorkflowAgentOnToolCallStartCallback;
982
+ onToolExecutionStart?: WorkflowAgentOnToolExecutionStartCallback<TTools>;
837
983
 
838
984
  /**
839
985
  * Callback called after a tool execution completes.
840
986
  */
841
- experimental_onToolCallFinish?: WorkflowAgentOnToolCallFinishCallback;
987
+ onToolExecutionEnd?: WorkflowAgentOnToolExecutionEndCallback<TTools>;
842
988
 
843
989
  /**
844
990
  * Callback function called before each step in the agent loop.
@@ -858,7 +1004,7 @@ export type WorkflowAgentStreamOptions<
858
1004
  * }
859
1005
  * ```
860
1006
  */
861
- prepareStep?: PrepareStepCallback<TTools>;
1007
+ prepareStep?: PrepareStepCallback<TTools, TRuntimeContext>;
862
1008
 
863
1009
  /**
864
1010
  * Timeout in milliseconds for the stream operation.
@@ -910,6 +1056,12 @@ export interface ToolResult {
910
1056
  output: unknown;
911
1057
  }
912
1058
 
1059
+ type WorkflowToolExecutionResult = {
1060
+ modelResult: LanguageModelV4ToolResultPart;
1061
+ rawOutput: unknown;
1062
+ isError: boolean;
1063
+ };
1064
+
913
1065
  /**
914
1066
  * Result of the WorkflowAgent.stream method.
915
1067
  */
@@ -988,7 +1140,10 @@ export interface WorkflowAgentStreamResult<
988
1140
  * });
989
1141
  * ```
990
1142
  */
991
- export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1143
+ export class WorkflowAgent<
1144
+ TBaseTools extends ToolSet = ToolSet,
1145
+ TRuntimeContext extends Context = Context,
1146
+ > {
992
1147
  /**
993
1148
  * The id of the agent.
994
1149
  */
@@ -999,51 +1154,63 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
999
1154
  * The tool set configured for this agent.
1000
1155
  */
1001
1156
  public readonly tools: TBaseTools;
1002
- private instructions?:
1003
- | string
1004
- | SystemModelMessage
1005
- | Array<SystemModelMessage>;
1157
+ private instructions?: Instructions;
1006
1158
  private generationSettings: GenerationSettings;
1007
1159
  private toolChoice?: ToolChoice<TBaseTools>;
1008
- private telemetry?: TelemetrySettings;
1009
- private experimentalContext: unknown;
1160
+ private telemetry?: TelemetryOptions<TRuntimeContext, TBaseTools>;
1161
+ private runtimeContext?: TRuntimeContext;
1162
+ private toolsContext?: InferToolSetContext<TBaseTools>;
1010
1163
  private stopWhen?:
1011
1164
  | StopCondition<ToolSet, any>
1012
1165
  | Array<StopCondition<ToolSet, any>>;
1013
- private activeTools?: Array<keyof TBaseTools>;
1166
+ private activeTools?: ActiveTools<TBaseTools>;
1014
1167
  private output?: OutputSpecification<any, any>;
1015
1168
  private experimentalRepairToolCall?: ToolCallRepairFunction<TBaseTools>;
1016
1169
  private experimentalDownload?: DownloadFunction;
1017
- private prepareStep?: PrepareStepCallback<TBaseTools>;
1018
- private constructorOnStepFinish?: WorkflowAgentOnStepFinishCallback<ToolSet>;
1019
- private constructorOnFinish?: WorkflowAgentOnFinishCallback<ToolSet>;
1020
- private constructorOnStart?: WorkflowAgentOnStartCallback;
1021
- private constructorOnStepStart?: WorkflowAgentOnStepStartCallback;
1022
- private constructorOnToolCallStart?: WorkflowAgentOnToolCallStartCallback;
1023
- private constructorOnToolCallFinish?: WorkflowAgentOnToolCallFinishCallback;
1024
- private prepareCall?: PrepareCallCallback<TBaseTools>;
1025
-
1026
- constructor(options: WorkflowAgentOptions<TBaseTools>) {
1170
+ private prepareStep?: PrepareStepCallback<TBaseTools, TRuntimeContext>;
1171
+ private constructorOnStepEnd?: WorkflowAgentOnStepEndCallback<
1172
+ TBaseTools,
1173
+ TRuntimeContext
1174
+ >;
1175
+ private constructorOnEnd?: WorkflowAgentOnEndCallback<
1176
+ TBaseTools,
1177
+ TRuntimeContext
1178
+ >;
1179
+ private constructorOnStart?: WorkflowAgentOnStartCallback<
1180
+ TBaseTools,
1181
+ TRuntimeContext
1182
+ >;
1183
+ private constructorOnStepStart?: WorkflowAgentOnStepStartCallback<
1184
+ TBaseTools,
1185
+ TRuntimeContext
1186
+ >;
1187
+ private constructorOnToolExecutionStart?: WorkflowAgentOnToolExecutionStartCallback<TBaseTools>;
1188
+ private constructorOnToolExecutionEnd?: WorkflowAgentOnToolExecutionEndCallback<TBaseTools>;
1189
+ private prepareCall?: PrepareCallCallback<TBaseTools, TRuntimeContext>;
1190
+
1191
+ constructor(options: WorkflowAgentOptions<TBaseTools, TRuntimeContext>) {
1027
1192
  this.id = options.id;
1028
1193
  this.model = options.model;
1029
1194
  this.tools = (options.tools ?? {}) as TBaseTools;
1030
1195
  // `instructions` takes precedence over deprecated `system`
1031
1196
  this.instructions = options.instructions ?? options.system;
1032
1197
  this.toolChoice = options.toolChoice;
1033
- this.telemetry = options.experimental_telemetry;
1034
- this.experimentalContext = options.experimental_context;
1198
+ this.telemetry = options.telemetry;
1199
+ this.runtimeContext = options.runtimeContext;
1200
+ this.toolsContext = options.toolsContext;
1035
1201
  this.stopWhen = options.stopWhen;
1036
1202
  this.activeTools = options.activeTools;
1037
1203
  this.output = options.output;
1038
1204
  this.experimentalRepairToolCall = options.experimental_repairToolCall;
1039
1205
  this.experimentalDownload = options.experimental_download;
1040
1206
  this.prepareStep = options.prepareStep;
1041
- this.constructorOnStepFinish = options.onStepFinish;
1042
- this.constructorOnFinish = options.onFinish;
1207
+ this.constructorOnStepEnd = options.onStepEnd ?? options.onStepFinish;
1208
+ const { onFinish, onEnd = onFinish } = options;
1209
+ this.constructorOnEnd = onEnd;
1043
1210
  this.constructorOnStart = options.experimental_onStart;
1044
1211
  this.constructorOnStepStart = options.experimental_onStepStart;
1045
- this.constructorOnToolCallStart = options.experimental_onToolCallStart;
1046
- this.constructorOnToolCallFinish = options.experimental_onToolCallFinish;
1212
+ this.constructorOnToolExecutionStart = options.onToolExecutionStart;
1213
+ this.constructorOnToolExecutionEnd = options.onToolExecutionEnd;
1047
1214
  this.prepareCall = options.prepareCall;
1048
1215
 
1049
1216
  // Extract generation settings
@@ -1072,8 +1239,15 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1072
1239
  OUTPUT = never,
1073
1240
  PARTIAL_OUTPUT = never,
1074
1241
  >(
1075
- options: WorkflowAgentStreamOptions<TTools, OUTPUT, PARTIAL_OUTPUT>,
1242
+ options: WorkflowAgentStreamOptions<
1243
+ TTools,
1244
+ TRuntimeContext,
1245
+ OUTPUT,
1246
+ PARTIAL_OUTPUT
1247
+ >,
1076
1248
  ): Promise<WorkflowAgentStreamResult<TTools, OUTPUT>> {
1249
+ const { onFinish, onEnd = onFinish } = options;
1250
+
1077
1251
  // Call prepareCall to transform parameters before the agent loop
1078
1252
  let effectiveModel: LanguageModel = this.model;
1079
1253
  let effectiveInstructions = options.system ?? this.instructions;
@@ -1081,11 +1255,16 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1081
1255
  options.prompt;
1082
1256
  let effectiveMessages: Array<ModelMessage> | undefined = options.messages;
1083
1257
  let effectiveGenerationSettings = { ...this.generationSettings };
1084
- let effectiveExperimentalContext =
1085
- options.experimental_context ?? this.experimentalContext;
1258
+ let effectiveRuntimeContext: TRuntimeContext = (options.runtimeContext ??
1259
+ this.runtimeContext ??
1260
+ {}) as TRuntimeContext;
1261
+ let effectiveToolsContext: Record<string, Context | undefined> =
1262
+ (options.toolsContext ?? this.toolsContext ?? {}) as unknown as Record<
1263
+ string,
1264
+ Context | undefined
1265
+ >;
1086
1266
  let effectiveToolChoiceFromPrepare = options.toolChoice ?? this.toolChoice;
1087
- let effectiveTelemetryFromPrepare =
1088
- options.experimental_telemetry ?? this.telemetry;
1267
+ let effectiveTelemetryFromPrepare = options.telemetry ?? this.telemetry;
1089
1268
 
1090
1269
  // Resolve messages for prepareCall: use messages directly, or convert prompt
1091
1270
  const resolvedMessagesForPrepareCall: ModelMessage[] =
@@ -1101,11 +1280,12 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1101
1280
  tools: this.tools,
1102
1281
  instructions: effectiveInstructions,
1103
1282
  toolChoice: effectiveToolChoiceFromPrepare as ToolChoice<TBaseTools>,
1104
- experimental_telemetry: effectiveTelemetryFromPrepare,
1105
- experimental_context: effectiveExperimentalContext,
1283
+ telemetry: effectiveTelemetryFromPrepare,
1284
+ runtimeContext: effectiveRuntimeContext,
1285
+ toolsContext: effectiveToolsContext as InferToolSetContext<TBaseTools>,
1106
1286
  messages: resolvedMessagesForPrepareCall,
1107
1287
  ...effectiveGenerationSettings,
1108
- } as PrepareCallOptions<TBaseTools>);
1288
+ } as PrepareCallOptions<TBaseTools, TRuntimeContext>);
1109
1289
 
1110
1290
  if (prepared.model !== undefined) effectiveModel = prepared.model;
1111
1291
  if (prepared.instructions !== undefined)
@@ -1114,13 +1294,18 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1114
1294
  effectiveMessages = prepared.messages as Array<ModelMessage>;
1115
1295
  effectivePrompt = undefined; // messages from prepareCall take precedence
1116
1296
  }
1117
- if (prepared.experimental_context !== undefined)
1118
- effectiveExperimentalContext = prepared.experimental_context;
1297
+ if (prepared.runtimeContext !== undefined)
1298
+ effectiveRuntimeContext = prepared.runtimeContext;
1299
+ if (prepared.toolsContext !== undefined)
1300
+ effectiveToolsContext = prepared.toolsContext as Record<
1301
+ string,
1302
+ Context | undefined
1303
+ >;
1119
1304
  if (prepared.toolChoice !== undefined)
1120
1305
  effectiveToolChoiceFromPrepare =
1121
1306
  prepared.toolChoice as ToolChoice<TBaseTools>;
1122
- if (prepared.experimental_telemetry !== undefined)
1123
- effectiveTelemetryFromPrepare = prepared.experimental_telemetry;
1307
+ if (prepared.telemetry !== undefined)
1308
+ effectiveTelemetryFromPrepare = prepared.telemetry;
1124
1309
  if (prepared.maxOutputTokens !== undefined)
1125
1310
  effectiveGenerationSettings.maxOutputTokens = prepared.maxOutputTokens;
1126
1311
  if (prepared.temperature !== undefined)
@@ -1144,61 +1329,257 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1144
1329
  effectiveGenerationSettings.providerOptions = prepared.providerOptions;
1145
1330
  }
1146
1331
 
1332
+ const effectiveTelemetry = effectiveTelemetryFromPrepare;
1333
+ const telemetryDispatcher = createRestrictedTelemetryDispatcher<
1334
+ any,
1335
+ any,
1336
+ any
1337
+ >({
1338
+ telemetry: effectiveTelemetry as any,
1339
+ includeRuntimeContext: effectiveTelemetry?.includeRuntimeContext,
1340
+ includeToolsContext: effectiveTelemetry?.includeToolsContext,
1341
+ }) as any;
1342
+
1147
1343
  const prompt = await standardizePrompt({
1148
1344
  system: effectiveInstructions,
1345
+ allowSystemInMessages: true, // TODO: consider exposing this as a parameter
1149
1346
  ...(effectivePrompt != null
1150
1347
  ? { prompt: effectivePrompt }
1151
1348
  : { messages: effectiveMessages! }),
1152
- });
1349
+ } as Prompt);
1350
+ const download = options.experimental_download ?? this.experimentalDownload;
1153
1351
 
1154
1352
  // Process tool approval responses before starting the agent loop.
1155
1353
  // This mirrors how stream-text.ts handles tool-approval-response parts:
1156
1354
  // approved tools are executed, denied tools get denial results, and
1157
1355
  // approval parts are stripped from the messages.
1158
- const { approvedToolApprovals, deniedToolApprovals } =
1159
- collectToolApprovalsFromMessages(prompt.messages);
1356
+ // Use the AI SDK core collector so this path cannot drift from the
1357
+ // hardened generateText/streamText implementation. The collected approvals
1358
+ // are mapped to the flat shape used below; the original (nested) approval
1359
+ // is carried on `collected` for re-validation.
1360
+ const collectedApprovals = collectToolApprovals<ToolSet>({
1361
+ messages: prompt.messages,
1362
+ });
1363
+ const approvedToolApprovals = collectedApprovals.approvedToolApprovals.map(
1364
+ collected => ({
1365
+ toolCallId: collected.toolCall.toolCallId,
1366
+ toolName: collected.toolCall.toolName,
1367
+ input: collected.toolCall.input,
1368
+ reason: collected.approvalResponse.reason,
1369
+ providerExecuted: collected.toolCall.providerExecuted === true,
1370
+ collected,
1371
+ }),
1372
+ );
1373
+ const deniedToolApprovals = collectedApprovals.deniedToolApprovals.map(
1374
+ collected => ({
1375
+ toolCallId: collected.toolCall.toolCallId,
1376
+ toolName: collected.toolCall.toolName,
1377
+ input: collected.toolCall.input,
1378
+ reason: collected.approvalResponse.reason,
1379
+ providerExecuted: collected.toolCall.providerExecuted === true,
1380
+ }),
1381
+ );
1382
+
1383
+ // Approval ids of provider-executed tool calls. Provider-executed tools
1384
+ // (e.g. MCP via the Responses API) cannot be resolved locally — the
1385
+ // provider owns execution. We therefore skip them from local execution
1386
+ // and preserve their approval responses in the messages so the provider
1387
+ // receives the approval on the next call. The discriminator is sourced
1388
+ // from the original `tool-call` part (matching how core's stream-text.ts
1389
+ // decides), not from the response part which may be missing the flag.
1390
+ const providerExecutedApprovalIds = new Set<string>(
1391
+ [
1392
+ ...collectedApprovals.approvedToolApprovals,
1393
+ ...collectedApprovals.deniedToolApprovals,
1394
+ ]
1395
+ .filter(collected => collected.toolCall.providerExecuted === true)
1396
+ .map(collected => collected.approvalResponse.approvalId),
1397
+ );
1160
1398
 
1161
1399
  if (approvedToolApprovals.length > 0 || deniedToolApprovals.length > 0) {
1162
1400
  const _toolResultMessages: ModelMessage[] = [];
1163
- const toolResultContent: Array<{
1164
- type: 'tool-result';
1401
+ const toolResultContent: LanguageModelV4ToolResultPart[] = [];
1402
+ const approvedRawResults: Array<{
1165
1403
  toolCallId: string;
1166
1404
  toolName: string;
1167
- output:
1168
- | { type: 'text'; value: string }
1169
- | { type: 'json'; value: JSONValue }
1170
- | { type: 'execution-denied'; reason: string | undefined };
1405
+ input: unknown;
1406
+ output: unknown;
1171
1407
  }> = [];
1172
1408
 
1173
1409
  // Execute approved tools
1174
1410
  for (const approval of approvedToolApprovals) {
1411
+ // Provider-executed approvals are forwarded to the provider via the
1412
+ // preserved approval response below, not executed locally.
1413
+ if (approval.providerExecuted) {
1414
+ continue;
1415
+ }
1175
1416
  const tool = (this.tools as ToolSet)[approval.toolName];
1176
1417
  if (tool && typeof tool.execute === 'function') {
1418
+ if (!tool.needsApproval) {
1419
+ const reason = `Tool "${approval.toolName}" does not require approval`;
1420
+ toolResultContent.push({
1421
+ type: 'tool-result' as const,
1422
+ toolCallId: approval.toolCallId,
1423
+ toolName: approval.toolName,
1424
+ output: await createLanguageModelToolResultOutput({
1425
+ toolCallId: approval.toolCallId,
1426
+ toolName: approval.toolName,
1427
+ input: approval.input,
1428
+ output: reason,
1429
+ tool,
1430
+ errorMode: 'text',
1431
+ supportedUrls: {},
1432
+ download,
1433
+ }),
1434
+ });
1435
+ continue;
1436
+ }
1437
+
1438
+ // Re-validate through the shared core implementation: input schema,
1439
+ // HMAC signature (when configured), and approval policy. It throws on
1440
+ // invalid input/signature; convert that to a denial result so the
1441
+ // agent loop can continue gracefully.
1442
+ let revalidationReason: string | undefined;
1443
+ try {
1444
+ const { deniedToolApprovals: policyDenied } =
1445
+ await validateApprovedToolApprovals({
1446
+ approvedToolApprovals: [approval.collected],
1447
+ tools: this.tools as ToolSet,
1448
+ toolApproval: undefined,
1449
+ messages: prompt.messages,
1450
+ toolsContext:
1451
+ effectiveToolsContext as InferToolSetContext<ToolSet>,
1452
+ runtimeContext: effectiveRuntimeContext,
1453
+ });
1454
+ if (policyDenied.length > 0) {
1455
+ revalidationReason =
1456
+ policyDenied[0].approvalResponse.reason ??
1457
+ 'Tool approval denied';
1458
+ }
1459
+ } catch (error) {
1460
+ revalidationReason = getErrorMessage(error);
1461
+ }
1462
+
1463
+ if (revalidationReason != null) {
1464
+ toolResultContent.push({
1465
+ type: 'tool-result' as const,
1466
+ toolCallId: approval.toolCallId,
1467
+ toolName: approval.toolName,
1468
+ output: await createLanguageModelToolResultOutput({
1469
+ toolCallId: approval.toolCallId,
1470
+ toolName: approval.toolName,
1471
+ input: approval.input,
1472
+ output: revalidationReason,
1473
+ tool,
1474
+ errorMode: 'text',
1475
+ supportedUrls: {},
1476
+ download,
1477
+ }),
1478
+ });
1479
+ continue;
1480
+ }
1481
+
1177
1482
  try {
1178
1483
  const { execute } = tool;
1179
- const toolResult = await execute(approval.input, {
1484
+ const resolvedContext = await resolveToolContext({
1485
+ toolName: approval.toolName,
1486
+ tool,
1487
+ toolsContext: effectiveToolsContext,
1488
+ });
1489
+ const toolCallEvent: ToolCall = {
1490
+ type: 'tool-call',
1180
1491
  toolCallId: approval.toolCallId,
1181
- messages: [],
1182
- context: effectiveExperimentalContext,
1492
+ toolName: approval.toolName,
1493
+ input: approval.input,
1494
+ };
1495
+ const messages = prompt.messages as unknown as ModelMessage[];
1496
+ await telemetryDispatcher.onToolExecutionStart?.({
1497
+ toolCall: toolCallEvent,
1498
+ stepNumber: 0,
1499
+ messages,
1500
+ toolContext: resolvedContext,
1501
+ });
1502
+ const startTime = Date.now();
1503
+ const executeApprovedTool = () =>
1504
+ execute(approval.input, {
1505
+ toolCallId: approval.toolCallId,
1506
+ messages: [],
1507
+ context: resolvedContext,
1508
+ });
1509
+ const toolResult =
1510
+ telemetryDispatcher.executeTool != null
1511
+ ? await telemetryDispatcher.executeTool({
1512
+ callId: 'workflow-agent',
1513
+ toolCallId: approval.toolCallId,
1514
+ execute: executeApprovedTool,
1515
+ })
1516
+ : await executeApprovedTool();
1517
+ await telemetryDispatcher.onToolExecutionEnd?.({
1518
+ toolCall: toolCallEvent,
1519
+ stepNumber: 0,
1520
+ durationMs: Date.now() - startTime,
1521
+ messages,
1522
+ toolContext: resolvedContext,
1523
+ success: true,
1524
+ output: toolResult,
1183
1525
  });
1184
1526
  toolResultContent.push({
1185
1527
  type: 'tool-result' as const,
1186
1528
  toolCallId: approval.toolCallId,
1187
1529
  toolName: approval.toolName,
1188
- output:
1189
- typeof toolResult === 'string'
1190
- ? { type: 'text' as const, value: toolResult }
1191
- : { type: 'json' as const, value: toolResult },
1530
+ output: await createLanguageModelToolResultOutput({
1531
+ toolCallId: approval.toolCallId,
1532
+ toolName: approval.toolName,
1533
+ input: approval.input,
1534
+ output: toolResult,
1535
+ tool,
1536
+ errorMode: 'none',
1537
+ supportedUrls: {},
1538
+ download,
1539
+ }),
1540
+ });
1541
+ approvedRawResults.push({
1542
+ toolCallId: approval.toolCallId,
1543
+ toolName: approval.toolName,
1544
+ input: approval.input,
1545
+ output: toolResult,
1192
1546
  });
1193
1547
  } catch (error) {
1548
+ const errorMessage = getErrorMessage(error);
1549
+ await telemetryDispatcher.onToolExecutionEnd?.({
1550
+ toolCall: {
1551
+ type: 'tool-call',
1552
+ toolCallId: approval.toolCallId,
1553
+ toolName: approval.toolName,
1554
+ input: approval.input,
1555
+ },
1556
+ stepNumber: 0,
1557
+ durationMs: 0,
1558
+ messages: prompt.messages as unknown as ModelMessage[],
1559
+ toolContext: undefined,
1560
+ success: false,
1561
+ error,
1562
+ });
1194
1563
  toolResultContent.push({
1195
1564
  type: 'tool-result' as const,
1196
1565
  toolCallId: approval.toolCallId,
1197
1566
  toolName: approval.toolName,
1198
- output: {
1199
- type: 'text' as const,
1200
- value: getErrorMessage(error),
1201
- },
1567
+ output: await createLanguageModelToolResultOutput({
1568
+ toolCallId: approval.toolCallId,
1569
+ toolName: approval.toolName,
1570
+ input: approval.input,
1571
+ output: errorMessage,
1572
+ tool,
1573
+ errorMode: 'text',
1574
+ supportedUrls: {},
1575
+ download,
1576
+ }),
1577
+ });
1578
+ approvedRawResults.push({
1579
+ toolCallId: approval.toolCallId,
1580
+ toolName: approval.toolName,
1581
+ input: approval.input,
1582
+ output: errorMessage,
1202
1583
  });
1203
1584
  }
1204
1585
  }
@@ -1206,6 +1587,11 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1206
1587
 
1207
1588
  // Create denial results for denied tools
1208
1589
  for (const denial of deniedToolApprovals) {
1590
+ // Provider-executed denials are forwarded to the provider via the
1591
+ // preserved approval response below, not turned into a local result.
1592
+ if (denial.providerExecuted) {
1593
+ continue;
1594
+ }
1209
1595
  toolResultContent.push({
1210
1596
  type: 'tool-result' as const,
1211
1597
  toolCallId: denial.toolCallId,
@@ -1217,20 +1603,33 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1217
1603
  });
1218
1604
  }
1219
1605
 
1220
- // Strip approval parts from messages and inject tool results
1606
+ // Strip approval parts that we resolved locally and inject tool results.
1607
+ // Provider-executed approval parts are preserved so the next call to
1608
+ // `convertToLanguageModelPrompt` forwards the approval response to the
1609
+ // provider (it only forwards responses flagged `providerExecuted`).
1221
1610
  const cleanedMessages: ModelMessage[] = [];
1222
1611
  for (const msg of prompt.messages) {
1223
1612
  if (msg.role === 'assistant' && Array.isArray(msg.content)) {
1224
1613
  const filtered = (msg.content as any[]).filter(
1225
- (p: any) => p.type !== 'tool-approval-request',
1614
+ (p: any) =>
1615
+ p.type !== 'tool-approval-request' ||
1616
+ providerExecutedApprovalIds.has(p.approvalId),
1226
1617
  );
1227
1618
  if (filtered.length > 0) {
1228
1619
  cleanedMessages.push({ ...msg, content: filtered });
1229
1620
  }
1230
1621
  } else if (msg.role === 'tool') {
1231
- const filtered = (msg.content as any[]).filter(
1232
- (p: any) => p.type !== 'tool-approval-response',
1233
- );
1622
+ const filtered = (msg.content as any[]).flatMap((p: any) => {
1623
+ if (p.type !== 'tool-approval-response') {
1624
+ return [p];
1625
+ }
1626
+ if (!providerExecutedApprovalIds.has(p.approvalId)) {
1627
+ return [];
1628
+ }
1629
+ // Re-stamp `providerExecuted` so the conversion layer forwards the
1630
+ // response even if the client omitted the flag on the response part.
1631
+ return [{ ...p, providerExecuted: true }];
1632
+ });
1234
1633
  if (filtered.length > 0) {
1235
1634
  cleanedMessages.push({ ...msg, content: filtered });
1236
1635
  }
@@ -1253,22 +1652,12 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1253
1652
  // can transition approved/denied tool parts to the correct state
1254
1653
  // and properly separate them from the subsequent model step.
1255
1654
  if (options.writable && toolResultContent.length > 0) {
1256
- const approvedResults = toolResultContent
1257
- .filter(r => r.output.type !== 'execution-denied')
1258
- .map(r => ({
1259
- toolCallId: r.toolCallId,
1260
- toolName: r.toolName,
1261
- input: approvedToolApprovals.find(
1262
- a => a.toolCallId === r.toolCallId,
1263
- )?.input,
1264
- output: 'value' in r.output ? r.output.value : undefined,
1265
- }));
1266
1655
  const deniedResults = toolResultContent
1267
1656
  .filter(r => r.output.type === 'execution-denied')
1268
1657
  .map(r => ({ toolCallId: r.toolCallId }));
1269
1658
  await writeApprovalToolResults(
1270
1659
  options.writable,
1271
- approvedResults,
1660
+ approvedRawResults,
1272
1661
  deniedResults,
1273
1662
  );
1274
1663
  }
@@ -1277,14 +1666,12 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1277
1666
  const modelPrompt = await convertToLanguageModelPrompt({
1278
1667
  prompt,
1279
1668
  supportedUrls: {},
1280
- download: options.experimental_download ?? this.experimentalDownload,
1669
+ download,
1281
1670
  });
1282
1671
 
1283
1672
  const effectiveAbortSignal = mergeAbortSignals(
1284
1673
  options.abortSignal ?? effectiveGenerationSettings.abortSignal,
1285
- options.timeout != null
1286
- ? AbortSignal.timeout(options.timeout)
1287
- : undefined,
1674
+ options.timeout,
1288
1675
  );
1289
1676
 
1290
1677
  // Merge generation settings: constructor defaults < prepareCall < stream options
@@ -1321,52 +1708,59 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1321
1708
  };
1322
1709
 
1323
1710
  // Merge constructor + stream callbacks (constructor first, then stream)
1324
- const mergedOnStepFinish = mergeCallbacks(
1325
- this.constructorOnStepFinish as
1326
- | WorkflowAgentOnStepFinishCallback<TTools>
1711
+ const mergedOnStepEnd = mergeCallbacks(
1712
+ this.constructorOnStepEnd as
1713
+ | WorkflowAgentOnStepEndCallback<TTools, TRuntimeContext>
1327
1714
  | undefined,
1328
- options.onStepFinish,
1715
+ options.onStepEnd ?? options.onStepFinish,
1329
1716
  );
1330
- const mergedOnFinish = mergeCallbacks(
1331
- this.constructorOnFinish as
1332
- | WorkflowAgentOnFinishCallback<TTools, OUTPUT>
1717
+ const mergedOnEnd = mergeCallbacks(
1718
+ this.constructorOnEnd as
1719
+ | WorkflowAgentOnEndCallback<TTools, TRuntimeContext, OUTPUT>
1333
1720
  | undefined,
1334
- options.onFinish,
1721
+ onEnd,
1335
1722
  );
1336
1723
  const mergedOnStart = mergeCallbacks(
1337
- this.constructorOnStart,
1724
+ this.constructorOnStart as
1725
+ | WorkflowAgentOnStartCallback<TTools, TRuntimeContext>
1726
+ | undefined,
1338
1727
  options.experimental_onStart,
1339
1728
  );
1340
1729
  const mergedOnStepStart = mergeCallbacks(
1341
- this.constructorOnStepStart,
1730
+ this.constructorOnStepStart as
1731
+ | WorkflowAgentOnStepStartCallback<TTools, TRuntimeContext>
1732
+ | undefined,
1342
1733
  options.experimental_onStepStart,
1343
1734
  );
1344
- const mergedOnToolCallStart = mergeCallbacks(
1345
- this.constructorOnToolCallStart,
1346
- options.experimental_onToolCallStart,
1735
+ const mergedOnToolExecutionStart = mergeCallbacks(
1736
+ this.constructorOnToolExecutionStart,
1737
+ options.onToolExecutionStart,
1347
1738
  );
1348
- const mergedOnToolCallFinish = mergeCallbacks(
1349
- this.constructorOnToolCallFinish,
1350
- options.experimental_onToolCallFinish,
1739
+ const mergedOnToolExecutionEnd = mergeCallbacks(
1740
+ this.constructorOnToolExecutionEnd,
1741
+ options.onToolExecutionEnd,
1351
1742
  );
1352
1743
 
1353
1744
  // Determine effective tool choice
1354
1745
  const effectiveToolChoice = effectiveToolChoiceFromPrepare;
1355
1746
 
1356
- // Merge telemetry settings
1357
- const effectiveTelemetry = effectiveTelemetryFromPrepare;
1358
-
1359
1747
  // Filter tools if activeTools is specified (stream-level overrides constructor default)
1360
1748
  const effectiveActiveTools = options.activeTools ?? this.activeTools;
1361
1749
  const effectiveTools =
1362
1750
  effectiveActiveTools && effectiveActiveTools.length > 0
1363
- ? filterTools(this.tools, effectiveActiveTools as string[])
1751
+ ? (filterActiveTools({
1752
+ tools: this.tools,
1753
+ activeTools: effectiveActiveTools,
1754
+ }) ?? this.tools)
1364
1755
  : this.tools;
1756
+ const effectiveModelInfo = getModelInfo(effectiveModel);
1365
1757
 
1366
1758
  // Initialize context
1367
- let experimentalContext = effectiveExperimentalContext;
1759
+ let runtimeContext: TRuntimeContext = effectiveRuntimeContext;
1760
+ let toolsContext: Record<string, Context | undefined> =
1761
+ effectiveToolsContext;
1368
1762
 
1369
- const steps: StepResult<TTools, any>[] = [];
1763
+ const steps: StepResult<TTools, TRuntimeContext>[] = [];
1370
1764
 
1371
1765
  // Track tool calls and results from the last step for the result
1372
1766
  let lastStepToolCalls: ToolCall[] = [];
@@ -1377,17 +1771,45 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1377
1771
  await mergedOnStart({
1378
1772
  model: effectiveModel,
1379
1773
  messages: prompt.messages,
1774
+ runtimeContext,
1775
+ toolsContext: toolsContext as unknown as InferToolSetContext<TTools>,
1380
1776
  });
1381
1777
  }
1778
+ await telemetryDispatcher.onStart?.({
1779
+ callId: 'workflow-agent',
1780
+ operationId: 'ai.workflowAgent.stream',
1781
+ provider: effectiveModelInfo.provider,
1782
+ modelId: effectiveModelInfo.modelId,
1783
+ system: undefined,
1784
+ messages: prompt.messages,
1785
+ tools: effectiveTools,
1786
+ toolChoice: effectiveToolChoice,
1787
+ activeTools: effectiveActiveTools as never,
1788
+ maxOutputTokens: mergedGenerationSettings.maxOutputTokens,
1789
+ temperature: mergedGenerationSettings.temperature,
1790
+ topP: mergedGenerationSettings.topP,
1791
+ topK: mergedGenerationSettings.topK,
1792
+ presencePenalty: mergedGenerationSettings.presencePenalty,
1793
+ frequencyPenalty: mergedGenerationSettings.frequencyPenalty,
1794
+ stopSequences: mergedGenerationSettings.stopSequences,
1795
+ seed: mergedGenerationSettings.seed,
1796
+ maxRetries: mergedGenerationSettings.maxRetries ?? 2,
1797
+ timeout: undefined,
1798
+ headers: mergedGenerationSettings.headers,
1799
+ providerOptions: mergedGenerationSettings.providerOptions,
1800
+ output: (options.output ?? this.output) as never,
1801
+ runtimeContext,
1802
+ toolsContext: toolsContext as unknown as InferToolSetContext<TTools>,
1803
+ });
1382
1804
 
1383
- // Helper to wrap executeTool with onToolCallStart/onToolCallFinish callbacks
1805
+ // Helper to wrap executeTool with onToolExecutionStart/onToolExecutionEnd callbacks
1384
1806
  const executeToolWithCallbacks = async (
1385
1807
  toolCall: { toolCallId: string; toolName: string; input: unknown },
1386
1808
  tools: ToolSet,
1387
1809
  messages: LanguageModelV4Prompt,
1388
- context?: unknown,
1810
+ perToolContexts: Record<string, Context | undefined>,
1389
1811
  currentStepNumber: number = 0,
1390
- ): Promise<LanguageModelV4ToolResultPart> => {
1812
+ ): Promise<WorkflowToolExecutionResult> => {
1391
1813
  const toolCallEvent: ToolCall = {
1392
1814
  type: 'tool-call',
1393
1815
  toolCallId: toolCall.toolCallId,
@@ -1395,62 +1817,172 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1395
1817
  input: toolCall.input,
1396
1818
  };
1397
1819
 
1398
- if (mergedOnToolCallStart) {
1399
- await mergedOnToolCallStart({
1820
+ const tool = tools[toolCall.toolName];
1821
+ const resolvedContext = tool
1822
+ ? await resolveToolContext({
1823
+ toolName: toolCall.toolName,
1824
+ tool,
1825
+ toolsContext: perToolContexts,
1826
+ })
1827
+ : undefined;
1828
+ const modelMessages = getToolCallbackMessages(messages);
1829
+
1830
+ if (mergedOnToolExecutionStart) {
1831
+ await mergedOnToolExecutionStart({
1400
1832
  toolCall: toolCallEvent,
1401
1833
  stepNumber: currentStepNumber,
1834
+ messages: modelMessages,
1835
+ toolContext: resolvedContext as
1836
+ | InferToolSetContext<TTools>[keyof InferToolSetContext<TTools>]
1837
+ | undefined,
1402
1838
  });
1403
1839
  }
1840
+ await telemetryDispatcher.onToolExecutionStart?.({
1841
+ toolCall: toolCallEvent,
1842
+ stepNumber: currentStepNumber,
1843
+ messages: modelMessages,
1844
+ toolContext: resolvedContext as
1845
+ | InferToolSetContext<TTools>[keyof InferToolSetContext<TTools>]
1846
+ | undefined,
1847
+ });
1404
1848
 
1405
1849
  const startTime = Date.now();
1406
- let result: LanguageModelV4ToolResultPart;
1850
+ let result: WorkflowToolExecutionResult;
1407
1851
  try {
1408
- result = await executeTool(toolCall, tools, messages, context);
1852
+ const execute = () =>
1853
+ executeTool(toolCall, tools, messages, resolvedContext, download);
1854
+ result =
1855
+ telemetryDispatcher.executeTool != null
1856
+ ? await telemetryDispatcher.executeTool({
1857
+ callId: 'workflow-agent',
1858
+ toolCallId: toolCall.toolCallId,
1859
+ execute,
1860
+ })
1861
+ : await execute();
1409
1862
  } catch (err) {
1410
1863
  const durationMs = Date.now() - startTime;
1411
- if (mergedOnToolCallFinish) {
1412
- await mergedOnToolCallFinish({
1864
+ if (mergedOnToolExecutionEnd) {
1865
+ await mergedOnToolExecutionEnd({
1413
1866
  toolCall: toolCallEvent,
1414
1867
  stepNumber: currentStepNumber,
1415
1868
  durationMs,
1869
+ messages: modelMessages,
1870
+ toolContext: resolvedContext as
1871
+ | InferToolSetContext<TTools>[keyof InferToolSetContext<TTools>]
1872
+ | undefined,
1416
1873
  success: false,
1417
1874
  error: err,
1418
1875
  });
1419
1876
  }
1877
+ await telemetryDispatcher.onToolExecutionEnd?.({
1878
+ toolCall: toolCallEvent,
1879
+ stepNumber: currentStepNumber,
1880
+ durationMs,
1881
+ messages: modelMessages,
1882
+ toolContext: resolvedContext as
1883
+ | InferToolSetContext<TTools>[keyof InferToolSetContext<TTools>]
1884
+ | undefined,
1885
+ success: false,
1886
+ error: err,
1887
+ });
1420
1888
  throw err;
1421
1889
  }
1422
1890
 
1423
1891
  const durationMs = Date.now() - startTime;
1424
- if (mergedOnToolCallFinish) {
1425
- const isError =
1426
- result.output &&
1427
- 'type' in result.output &&
1428
- (result.output.type === 'error-text' ||
1429
- result.output.type === 'error-json');
1430
- if (isError) {
1431
- await mergedOnToolCallFinish({
1892
+ if (mergedOnToolExecutionEnd) {
1893
+ if (result.isError) {
1894
+ await mergedOnToolExecutionEnd({
1432
1895
  toolCall: toolCallEvent,
1433
1896
  stepNumber: currentStepNumber,
1434
1897
  durationMs,
1898
+ messages: modelMessages,
1899
+ toolContext: resolvedContext as
1900
+ | InferToolSetContext<TTools>[keyof InferToolSetContext<TTools>]
1901
+ | undefined,
1435
1902
  success: false,
1436
- error: 'value' in result.output ? result.output.value : undefined,
1903
+ error: result.rawOutput,
1437
1904
  });
1438
1905
  } else {
1439
- await mergedOnToolCallFinish({
1906
+ await mergedOnToolExecutionEnd({
1440
1907
  toolCall: toolCallEvent,
1441
1908
  stepNumber: currentStepNumber,
1442
1909
  durationMs,
1910
+ messages: modelMessages,
1911
+ toolContext: resolvedContext as
1912
+ | InferToolSetContext<TTools>[keyof InferToolSetContext<TTools>]
1913
+ | undefined,
1443
1914
  success: true,
1444
- output:
1445
- result.output && 'value' in result.output
1446
- ? result.output.value
1447
- : undefined,
1915
+ output: result.rawOutput,
1448
1916
  });
1449
1917
  }
1450
1918
  }
1919
+ if (result.isError) {
1920
+ await telemetryDispatcher.onToolExecutionEnd?.({
1921
+ toolCall: toolCallEvent,
1922
+ stepNumber: currentStepNumber,
1923
+ durationMs,
1924
+ messages: modelMessages,
1925
+ toolContext: resolvedContext as
1926
+ | InferToolSetContext<TTools>[keyof InferToolSetContext<TTools>]
1927
+ | undefined,
1928
+ success: false,
1929
+ error: result.rawOutput,
1930
+ });
1931
+ } else {
1932
+ await telemetryDispatcher.onToolExecutionEnd?.({
1933
+ toolCall: toolCallEvent,
1934
+ stepNumber: currentStepNumber,
1935
+ durationMs,
1936
+ messages: modelMessages,
1937
+ toolContext: resolvedContext as
1938
+ | InferToolSetContext<TTools>[keyof InferToolSetContext<TTools>]
1939
+ | undefined,
1940
+ success: true,
1941
+ output: result.rawOutput,
1942
+ });
1943
+ }
1451
1944
  return result;
1452
1945
  };
1453
1946
 
1947
+ const recordProviderExecutedToolTelemetry = async (
1948
+ toolCall: { toolCallId: string; toolName: string; input: unknown },
1949
+ result: WorkflowToolExecutionResult,
1950
+ messages: LanguageModelV4Prompt,
1951
+ currentStepNumber: number,
1952
+ ) => {
1953
+ const toolCallEvent: ToolCall = {
1954
+ type: 'tool-call',
1955
+ toolCallId: toolCall.toolCallId,
1956
+ toolName: toolCall.toolName,
1957
+ input: toolCall.input,
1958
+ };
1959
+ const modelMessages = getToolCallbackMessages(messages);
1960
+
1961
+ await telemetryDispatcher.onToolExecutionStart?.({
1962
+ toolCall: toolCallEvent,
1963
+ stepNumber: currentStepNumber,
1964
+ messages: modelMessages,
1965
+ toolContext: undefined,
1966
+ });
1967
+
1968
+ await telemetryDispatcher.onToolExecutionEnd?.({
1969
+ toolCall: toolCallEvent,
1970
+ stepNumber: currentStepNumber,
1971
+ durationMs: 0,
1972
+ messages: modelMessages,
1973
+ toolContext: undefined,
1974
+ ...(result.isError
1975
+ ? {
1976
+ success: false as const,
1977
+ error: result.rawOutput,
1978
+ }
1979
+ : {
1980
+ success: true as const,
1981
+ output: result.rawOutput,
1982
+ }),
1983
+ });
1984
+ };
1985
+
1454
1986
  // Check for abort before starting
1455
1987
  if (mergedGenerationSettings.abortSignal?.aborted) {
1456
1988
  if (options.onAbort) {
@@ -1471,17 +2003,19 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1471
2003
  writable: options.writable,
1472
2004
  prompt: modelPrompt,
1473
2005
  stopConditions: options.stopWhen ?? this.stopWhen,
1474
- maxSteps: options.maxSteps,
1475
- onStepFinish: mergedOnStepFinish,
1476
- onStepStart: mergedOnStepStart,
2006
+
2007
+ onStepEnd: mergedOnStepEnd as any,
2008
+ onStepStart: mergedOnStepStart as any,
1477
2009
  onError: options.onError,
1478
- prepareStep:
1479
- options.prepareStep ??
1480
- (this.prepareStep as PrepareStepCallback<ToolSet> | undefined),
2010
+ prepareStep: (options.prepareStep ??
2011
+ (this.prepareStep as
2012
+ | PrepareStepCallback<ToolSet, TRuntimeContext>
2013
+ | undefined)) as any,
1481
2014
  generationSettings: mergedGenerationSettings,
1482
2015
  toolChoice: effectiveToolChoice as ToolChoice<ToolSet>,
1483
- experimental_context: experimentalContext,
1484
- experimental_telemetry: effectiveTelemetry,
2016
+ runtimeContext,
2017
+ toolsContext,
2018
+ telemetry: effectiveTelemetry,
1485
2019
  includeRawChunks: options.includeRawChunks ?? false,
1486
2020
  repairToolCall: (options.experimental_repairToolCall ??
1487
2021
  this.experimentalRepairToolCall) as
@@ -1511,25 +2045,34 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1511
2045
  toolCalls,
1512
2046
  messages: iterMessages,
1513
2047
  step,
1514
- context,
2048
+ runtimeContext: yieldedRuntimeContext,
2049
+ toolsContext: yieldedToolsContext,
1515
2050
  providerExecutedToolResults,
1516
2051
  } = result.value;
1517
2052
  // Capture current step number before pushing (0-based)
1518
2053
  const currentStepNumber = steps.length;
1519
2054
  if (step) {
1520
- steps.push(step as unknown as StepResult<TTools, any>);
2055
+ steps.push(step as unknown as StepResult<TTools, TRuntimeContext>);
1521
2056
  }
1522
- if (context !== undefined) {
1523
- experimentalContext = context;
2057
+ if (yieldedRuntimeContext !== undefined) {
2058
+ runtimeContext = yieldedRuntimeContext as TRuntimeContext;
2059
+ }
2060
+ if (yieldedToolsContext !== undefined) {
2061
+ toolsContext = yieldedToolsContext;
1524
2062
  }
1525
2063
 
1526
2064
  // Only execute tools if there are tool calls
1527
2065
  if (toolCalls.length > 0) {
2066
+ const invalidToolCalls = toolCalls.filter(tc => tc.invalid === true);
2067
+ const validToolCalls = toolCalls.filter(tc => tc.invalid !== true);
2068
+
1528
2069
  // Separate provider-executed tool calls from client-executed ones
1529
- const nonProviderToolCalls = toolCalls.filter(
2070
+ const nonProviderToolCalls = validToolCalls.filter(
1530
2071
  tc => !tc.providerExecuted,
1531
2072
  );
1532
- const providerToolCalls = toolCalls.filter(tc => tc.providerExecuted);
2073
+ const providerToolCalls = validToolCalls.filter(
2074
+ tc => tc.providerExecuted,
2075
+ );
1533
2076
 
1534
2077
  // Check which tools need approval (can be async)
1535
2078
  const approvalNeeded = await Promise.all(
@@ -1539,10 +2082,16 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1539
2082
  if (tool.needsApproval == null) return false;
1540
2083
  if (typeof tool.needsApproval === 'boolean')
1541
2084
  return tool.needsApproval;
2085
+ const resolvedContext = await resolveToolContext({
2086
+ toolName: tc.toolName,
2087
+ tool,
2088
+ toolsContext:
2089
+ toolsContext as unknown as InferToolSetContext<TTools>,
2090
+ });
1542
2091
  return tool.needsApproval(tc.input, {
1543
2092
  toolCallId: tc.toolCallId,
1544
2093
  messages: iterMessages as unknown as ModelMessage[],
1545
- context: experimentalContext,
2094
+ context: resolvedContext,
1546
2095
  });
1547
2096
  }),
1548
2097
  );
@@ -1573,27 +2122,49 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1573
2122
  // Execute any executable tools that were also called in this step
1574
2123
  const executableResults = await Promise.all(
1575
2124
  executableToolCalls.map(
1576
- (toolCall): Promise<LanguageModelV4ToolResultPart> =>
2125
+ (toolCall): Promise<WorkflowToolExecutionResult> =>
1577
2126
  executeToolWithCallbacks(
1578
2127
  toolCall,
1579
2128
  effectiveTools as ToolSet,
1580
2129
  iterMessages,
1581
- experimentalContext,
2130
+ toolsContext,
1582
2131
  currentStepNumber,
1583
2132
  ),
1584
2133
  ),
1585
2134
  );
1586
2135
 
1587
2136
  // Collect provider tool results
1588
- const providerResults: LanguageModelV4ToolResultPart[] =
1589
- providerToolCalls.map(toolCall =>
1590
- resolveProviderToolResult(
1591
- toolCall,
1592
- providerExecutedToolResults,
2137
+ const providerResults: WorkflowToolExecutionResult[] =
2138
+ await Promise.all(
2139
+ providerToolCalls.map(toolCall =>
2140
+ resolveProviderToolResult(
2141
+ toolCall,
2142
+ providerExecutedToolResults,
2143
+ effectiveTools as ToolSet,
2144
+ download,
2145
+ ),
1593
2146
  ),
1594
2147
  );
2148
+ await Promise.all(
2149
+ providerToolCalls.map((toolCall, index) =>
2150
+ recordProviderExecutedToolTelemetry(
2151
+ toolCall,
2152
+ providerResults[index],
2153
+ iterMessages,
2154
+ currentStepNumber,
2155
+ ),
2156
+ ),
2157
+ );
1595
2158
 
1596
- const resolvedResults = [...executableResults, ...providerResults];
2159
+ const continuationInvalidResults = invalidToolCalls.map(
2160
+ createInvalidToolResult,
2161
+ );
2162
+ const resolvedResults: LanguageModelV4ToolResultPart[] = [
2163
+ ...executableResults.map(result => result.modelResult),
2164
+ ...providerResults.map(result => result.modelResult),
2165
+ ...continuationInvalidResults,
2166
+ ];
2167
+ const executedResults = [...executableResults, ...providerResults];
1597
2168
 
1598
2169
  const allToolCalls: ToolCall[] = toolCalls.map(tc => ({
1599
2170
  type: 'tool-call' as const,
@@ -1602,13 +2173,14 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1602
2173
  input: tc.input,
1603
2174
  }));
1604
2175
 
1605
- const allToolResults: ToolResult[] = resolvedResults.map(r => ({
2176
+ const allToolResults: ToolResult[] = executedResults.map(r => ({
1606
2177
  type: 'tool-result' as const,
1607
- toolCallId: r.toolCallId,
1608
- toolName: r.toolName,
1609
- input: toolCalls.find(tc => tc.toolCallId === r.toolCallId)
1610
- ?.input,
1611
- output: 'value' in r.output ? r.output.value : undefined,
2178
+ toolCallId: r.modelResult.toolCallId,
2179
+ toolName: r.modelResult.toolName,
2180
+ input: toolCalls.find(
2181
+ tc => tc.toolCallId === r.modelResult.toolCallId,
2182
+ )?.input,
2183
+ output: r.rawOutput,
1612
2184
  }));
1613
2185
 
1614
2186
  if (resolvedResults.length > 0) {
@@ -1620,18 +2192,33 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1620
2192
 
1621
2193
  const messages = iterMessages as unknown as ModelMessage[];
1622
2194
 
1623
- if (mergedOnFinish && !wasAborted) {
2195
+ if (mergedOnEnd && !wasAborted) {
1624
2196
  const lastStep = steps[steps.length - 1];
1625
- await mergedOnFinish({
2197
+ const totalUsage = aggregateUsage(steps);
2198
+ await mergedOnEnd({
1626
2199
  steps,
1627
2200
  messages,
1628
2201
  text: lastStep?.text ?? '',
1629
2202
  finishReason: lastStep?.finishReason ?? 'other',
1630
- totalUsage: aggregateUsage(steps),
1631
- experimental_context: experimentalContext,
2203
+ usage: totalUsage,
2204
+ totalUsage,
2205
+ runtimeContext,
2206
+ toolsContext:
2207
+ toolsContext as unknown as InferToolSetContext<TTools>,
1632
2208
  output: undefined as OUTPUT,
1633
2209
  });
1634
2210
  }
2211
+ if (!wasAborted && steps.length > 0) {
2212
+ const telemetrySteps = steps.map(normalizeStepForTelemetry);
2213
+ const lastStep = telemetrySteps[telemetrySteps.length - 1];
2214
+ const totalUsage = aggregateUsage(steps);
2215
+ await telemetryDispatcher.onEnd?.({
2216
+ ...lastStep,
2217
+ steps: telemetrySteps,
2218
+ usage: totalUsage,
2219
+ totalUsage,
2220
+ });
2221
+ }
1635
2222
 
1636
2223
  // Emit tool-approval-request chunks for tools that need approval
1637
2224
  // so useChat can show the approval UI
@@ -1674,40 +2261,67 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1674
2261
  // Execute client tools (all have execute functions at this point)
1675
2262
  const clientToolResults = await Promise.all(
1676
2263
  nonProviderToolCalls.map(
1677
- (toolCall): Promise<LanguageModelV4ToolResultPart> =>
2264
+ (toolCall): Promise<WorkflowToolExecutionResult> =>
1678
2265
  executeToolWithCallbacks(
1679
2266
  toolCall,
1680
2267
  effectiveTools as ToolSet,
1681
2268
  iterMessages,
1682
- experimentalContext,
2269
+ toolsContext,
1683
2270
  currentStepNumber,
1684
2271
  ),
1685
2272
  ),
1686
2273
  );
1687
2274
 
1688
2275
  // For provider-executed tools, use the results from the stream
1689
- const providerToolResults: LanguageModelV4ToolResultPart[] =
1690
- providerToolCalls.map(toolCall =>
1691
- resolveProviderToolResult(toolCall, providerExecutedToolResults),
2276
+ const providerToolResults: WorkflowToolExecutionResult[] =
2277
+ await Promise.all(
2278
+ providerToolCalls.map(toolCall =>
2279
+ resolveProviderToolResult(
2280
+ toolCall,
2281
+ providerExecutedToolResults,
2282
+ effectiveTools as ToolSet,
2283
+ download,
2284
+ ),
2285
+ ),
1692
2286
  );
2287
+ await Promise.all(
2288
+ providerToolCalls.map((toolCall, index) =>
2289
+ recordProviderExecutedToolTelemetry(
2290
+ toolCall,
2291
+ providerToolResults[index],
2292
+ iterMessages,
2293
+ currentStepNumber,
2294
+ ),
2295
+ ),
2296
+ );
2297
+ const continuationInvalidToolResults = invalidToolCalls.map(
2298
+ createInvalidToolResult,
2299
+ );
1693
2300
 
1694
- // Combine results in the original order
1695
- const toolResults = toolCalls.map(tc => {
2301
+ // Combine executable/provider results in the original order,
2302
+ // while preserving invalid tool calls as error results for the
2303
+ // next model step without emitting them as synthetic UI success.
2304
+ const executedToolResults = toolCalls.flatMap(tc => {
1696
2305
  const clientResult = clientToolResults.find(
1697
- r => r.toolCallId === tc.toolCallId,
2306
+ r => r.modelResult.toolCallId === tc.toolCallId,
1698
2307
  );
1699
- if (clientResult) return clientResult;
2308
+ if (clientResult) return [clientResult];
1700
2309
  const providerResult = providerToolResults.find(
2310
+ r => r.modelResult.toolCallId === tc.toolCallId,
2311
+ );
2312
+ if (providerResult) return [providerResult];
2313
+ return [];
2314
+ });
2315
+ const continuationToolResults = toolCalls.flatMap(tc => {
2316
+ const invalidResult = continuationInvalidToolResults.find(
1701
2317
  r => r.toolCallId === tc.toolCallId,
1702
2318
  );
1703
- if (providerResult) return providerResult;
1704
- // This should never happen, but return empty result as fallback
1705
- return {
1706
- type: 'tool-result' as const,
1707
- toolCallId: tc.toolCallId,
1708
- toolName: tc.toolName,
1709
- output: { type: 'text' as const, value: '' },
1710
- };
2319
+ if (invalidResult) return [invalidResult];
2320
+ const executedResult = executedToolResults.find(
2321
+ r => r.modelResult.toolCallId === tc.toolCallId,
2322
+ );
2323
+ if (executedResult) return [executedResult.modelResult];
2324
+ return [];
1711
2325
  });
1712
2326
 
1713
2327
  // Write tool results and step boundaries to the stream so the
@@ -1716,12 +2330,13 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1716
2330
  if (options.writable) {
1717
2331
  await writeToolResultsWithStepBoundary(
1718
2332
  options.writable,
1719
- toolResults.map(r => ({
1720
- toolCallId: r.toolCallId,
1721
- toolName: r.toolName,
1722
- input: toolCalls.find(tc => tc.toolCallId === r.toolCallId)
1723
- ?.input,
1724
- output: 'value' in r.output ? r.output.value : undefined,
2333
+ executedToolResults.map(r => ({
2334
+ toolCallId: r.modelResult.toolCallId,
2335
+ toolName: r.modelResult.toolName,
2336
+ input: toolCalls.find(
2337
+ tc => tc.toolCallId === r.modelResult.toolCallId,
2338
+ )?.input,
2339
+ output: r.rawOutput,
1725
2340
  })),
1726
2341
  );
1727
2342
  }
@@ -1733,15 +2348,17 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1733
2348
  toolName: tc.toolName,
1734
2349
  input: tc.input,
1735
2350
  }));
1736
- lastStepToolResults = toolResults.map(r => ({
2351
+ lastStepToolResults = executedToolResults.map(r => ({
1737
2352
  type: 'tool-result' as const,
1738
- toolCallId: r.toolCallId,
1739
- toolName: r.toolName,
1740
- input: toolCalls.find(tc => tc.toolCallId === r.toolCallId)?.input,
1741
- output: 'value' in r.output ? r.output.value : undefined,
2353
+ toolCallId: r.modelResult.toolCallId,
2354
+ toolName: r.modelResult.toolName,
2355
+ input: toolCalls.find(
2356
+ tc => tc.toolCallId === r.modelResult.toolCallId,
2357
+ )?.input,
2358
+ output: r.rawOutput,
1742
2359
  }));
1743
2360
 
1744
- result = await iterator.next(toolResults);
2361
+ result = await iterator.next(continuationToolResults);
1745
2362
  } else {
1746
2363
  // Final step with no tool calls - reset tracking
1747
2364
  lastStepToolCalls = [];
@@ -1766,7 +2383,8 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1766
2383
  // Call onError for non-abort errors (including tool execution errors)
1767
2384
  await options.onError({ error });
1768
2385
  }
1769
- // Don't throw yet - we want to call onFinish first
2386
+ await telemetryDispatcher.onError?.(error);
2387
+ // Don't throw yet - we want to call onEnd first
1770
2388
  }
1771
2389
 
1772
2390
  // Use the final messages from the iterator, or fall back to standardized messages
@@ -1799,19 +2417,33 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1799
2417
  }
1800
2418
  }
1801
2419
 
1802
- // Call onFinish callback if provided (always call, even on errors, but not on abort)
1803
- if (mergedOnFinish && !wasAborted) {
2420
+ // Call onEnd callback if provided (always call, even on errors, but not on abort)
2421
+ if (mergedOnEnd && !wasAborted) {
1804
2422
  const lastStep = steps[steps.length - 1];
1805
- await mergedOnFinish({
2423
+ const totalUsage = aggregateUsage(steps);
2424
+ await mergedOnEnd({
1806
2425
  steps,
1807
2426
  messages: messages as ModelMessage[],
1808
2427
  text: lastStep?.text ?? '',
1809
2428
  finishReason: lastStep?.finishReason ?? 'other',
1810
- totalUsage: aggregateUsage(steps),
1811
- experimental_context: experimentalContext,
2429
+ usage: totalUsage,
2430
+ totalUsage,
2431
+ runtimeContext,
2432
+ toolsContext: toolsContext as unknown as InferToolSetContext<TTools>,
1812
2433
  output: experimentalOutput,
1813
2434
  });
1814
2435
  }
2436
+ if (!wasAborted && steps.length > 0) {
2437
+ const telemetrySteps = steps.map(normalizeStepForTelemetry);
2438
+ const lastStep = telemetrySteps[telemetrySteps.length - 1];
2439
+ const totalUsage = aggregateUsage(steps);
2440
+ await telemetryDispatcher.onEnd?.({
2441
+ ...lastStep,
2442
+ steps: telemetrySteps,
2443
+ usage: totalUsage,
2444
+ totalUsage,
2445
+ });
2446
+ }
1815
2447
 
1816
2448
  // Re-throw any error that occurred
1817
2449
  if (encounteredError) {
@@ -1845,6 +2477,25 @@ export class WorkflowAgent<TBaseTools extends ToolSet = ToolSet> {
1845
2477
  }
1846
2478
  }
1847
2479
 
2480
+ function getModelInfo(model: LanguageModel): {
2481
+ provider: string;
2482
+ modelId: string;
2483
+ } {
2484
+ return typeof model === 'string'
2485
+ ? { provider: model.split('/')[0] ?? 'gateway', modelId: model }
2486
+ : { provider: model.provider, modelId: model.modelId };
2487
+ }
2488
+
2489
+ function normalizeStepForTelemetry<
2490
+ TOOLS extends ToolSet,
2491
+ RUNTIME_CONTEXT extends Context,
2492
+ >(step: StepResult<TOOLS, RUNTIME_CONTEXT>) {
2493
+ return {
2494
+ ...step,
2495
+ model: step.model ?? { provider: 'unknown', modelId: 'unknown' },
2496
+ };
2497
+ }
2498
+
1848
2499
  /**
1849
2500
  * Filter tools to only include the specified active tools.
1850
2501
  */
@@ -1965,6 +2616,33 @@ async function writeApprovalToolResults(
1965
2616
  }
1966
2617
  }
1967
2618
 
2619
+ /**
2620
+ * Resolve the per-tool context that gets passed into a tool's `execute`
2621
+ * (and `needsApproval`) function. When the tool declares a `contextSchema`,
2622
+ * the entry is validated against it.
2623
+ */
2624
+ async function resolveToolContext({
2625
+ toolName,
2626
+ tool,
2627
+ toolsContext,
2628
+ }: {
2629
+ toolName: string;
2630
+ tool: ToolSet[string];
2631
+ toolsContext: Record<string, Context | undefined> | undefined;
2632
+ }): Promise<unknown> {
2633
+ const contextSchema = (tool as { contextSchema?: unknown }).contextSchema;
2634
+ const entry = toolsContext?.[toolName];
2635
+ if (contextSchema == null) {
2636
+ return entry;
2637
+ }
2638
+
2639
+ return await validateTypes({
2640
+ value: entry,
2641
+ schema: contextSchema as Parameters<typeof validateTypes>[0]['schema'],
2642
+ context: { field: 'tool context', entityName: toolName },
2643
+ });
2644
+ }
2645
+
1968
2646
  function aggregateUsage(steps: StepResult<any, any>[]): LanguageModelUsage {
1969
2647
  let inputTokens = 0;
1970
2648
  let outputTokens = 0;
@@ -1979,43 +2657,15 @@ function aggregateUsage(steps: StepResult<any, any>[]): LanguageModelUsage {
1979
2657
  } as LanguageModelUsage;
1980
2658
  }
1981
2659
 
1982
- function filterTools<TTools extends ToolSet>(
1983
- tools: TTools,
1984
- activeTools: string[],
1985
- ): ToolSet {
1986
- const filtered: ToolSet = {};
1987
- for (const toolName of activeTools) {
1988
- if (toolName in tools) {
1989
- filtered[toolName] = tools[toolName];
1990
- }
1991
- }
1992
- return filtered;
1993
- }
1994
-
1995
- // Matches AI SDK's getErrorMessage from @ai-sdk/provider-utils
1996
- function getErrorMessage(error: unknown): string {
1997
- if (error == null) {
1998
- return 'unknown error';
1999
- }
2000
-
2001
- if (typeof error === 'string') {
2002
- return error;
2003
- }
2004
-
2005
- if (error instanceof Error) {
2006
- return error.message;
2007
- }
2008
-
2009
- return JSON.stringify(error);
2010
- }
2011
-
2012
- function resolveProviderToolResult(
2013
- toolCall: { toolCallId: string; toolName: string },
2660
+ async function resolveProviderToolResult(
2661
+ toolCall: { toolCallId: string; toolName: string; input: unknown },
2014
2662
  providerExecutedToolResults?: Map<
2015
2663
  string,
2016
2664
  { toolCallId: string; toolName: string; result: unknown; isError?: boolean }
2017
2665
  >,
2018
- ): LanguageModelV4ToolResultPart {
2666
+ tools?: ToolSet,
2667
+ download?: DownloadFunction,
2668
+ ): Promise<WorkflowToolExecutionResult> {
2019
2669
  const streamResult = providerExecutedToolResults?.get(toolCall.toolCallId);
2020
2670
  if (!streamResult) {
2021
2671
  console.warn(
@@ -2023,45 +2673,79 @@ function resolveProviderToolResult(
2023
2673
  `did not receive a result from the stream. This may indicate a provider issue.`,
2024
2674
  );
2025
2675
  return {
2026
- type: 'tool-result' as const,
2027
- toolCallId: toolCall.toolCallId,
2028
- toolName: toolCall.toolName,
2029
- output: {
2030
- type: 'text' as const,
2031
- value: '',
2676
+ modelResult: {
2677
+ type: 'tool-result' as const,
2678
+ toolCallId: toolCall.toolCallId,
2679
+ toolName: toolCall.toolName,
2680
+ output: {
2681
+ type: 'text' as const,
2682
+ value: '',
2683
+ },
2032
2684
  },
2685
+ rawOutput: '',
2686
+ isError: false,
2033
2687
  };
2034
2688
  }
2035
2689
 
2036
2690
  const result = streamResult.result;
2037
- const isString = typeof result === 'string';
2691
+ const errorMode = streamResult.isError
2692
+ ? typeof result === 'string'
2693
+ ? 'text'
2694
+ : 'json'
2695
+ : 'none';
2696
+
2697
+ return {
2698
+ modelResult: {
2699
+ type: 'tool-result' as const,
2700
+ toolCallId: toolCall.toolCallId,
2701
+ toolName: toolCall.toolName,
2702
+ output: await createLanguageModelToolResultOutput({
2703
+ toolCallId: toolCall.toolCallId,
2704
+ toolName: toolCall.toolName,
2705
+ input: toolCall.input,
2706
+ output: result,
2707
+ tool: tools?.[toolCall.toolName],
2708
+ errorMode,
2709
+ supportedUrls: {},
2710
+ download,
2711
+ }),
2712
+ },
2713
+ rawOutput: result,
2714
+ isError: streamResult.isError === true,
2715
+ };
2716
+ }
2038
2717
 
2718
+ function createInvalidToolResult(toolCall: {
2719
+ toolCallId: string;
2720
+ toolName: string;
2721
+ error?: unknown;
2722
+ }): LanguageModelV4ToolResultPart {
2039
2723
  return {
2040
2724
  type: 'tool-result' as const,
2041
2725
  toolCallId: toolCall.toolCallId,
2042
2726
  toolName: toolCall.toolName,
2043
- output: isString
2044
- ? streamResult.isError
2045
- ? { type: 'error-text' as const, value: result }
2046
- : { type: 'text' as const, value: result }
2047
- : streamResult.isError
2048
- ? {
2049
- type: 'error-json' as const,
2050
- value: result as JSONValue,
2051
- }
2052
- : {
2053
- type: 'json' as const,
2054
- value: result as JSONValue,
2055
- },
2727
+ output: {
2728
+ type: 'error-text' as const,
2729
+ value: getErrorMessage(toolCall.error),
2730
+ },
2056
2731
  };
2057
2732
  }
2058
2733
 
2734
+ function getToolCallbackMessages(
2735
+ messages: LanguageModelV4Prompt,
2736
+ ): ModelMessage[] {
2737
+ const withoutAssistantToolCall =
2738
+ messages.at(-1)?.role === 'assistant' ? messages.slice(0, -1) : messages;
2739
+ return withoutAssistantToolCall as unknown as ModelMessage[];
2740
+ }
2741
+
2059
2742
  async function executeTool(
2060
2743
  toolCall: { toolCallId: string; toolName: string; input: unknown },
2061
2744
  tools: ToolSet,
2062
2745
  messages: LanguageModelV4Prompt,
2063
- experimentalContext?: unknown,
2064
- ): Promise<LanguageModelV4ToolResultPart> {
2746
+ context?: unknown,
2747
+ download?: DownloadFunction,
2748
+ ): Promise<WorkflowToolExecutionResult> {
2065
2749
  const tool = tools[toolCall.toolName];
2066
2750
  if (!tool) throw new Error(`Tool "${toolCall.toolName}" not found`);
2067
2751
  if (typeof tool.execute !== 'function') {
@@ -2072,6 +2756,7 @@ async function executeTool(
2072
2756
  }
2073
2757
  // Input is already parsed and validated by streamModelCall's parseToolCall
2074
2758
  const parsedInput = toolCall.input;
2759
+ let toolResult: unknown;
2075
2760
 
2076
2761
  try {
2077
2762
  // Extract execute function to avoid binding `this` to the tool object.
@@ -2080,149 +2765,56 @@ async function executeTool(
2080
2765
  // When the execute function is a workflow step (marked with 'use step'),
2081
2766
  // the step system captures `this` for serialization, causing failures.
2082
2767
  const { execute } = tool;
2083
- const toolResult = await execute(parsedInput, {
2768
+ toolResult = await execute(parsedInput, {
2084
2769
  toolCallId: toolCall.toolCallId,
2085
2770
  // Pass the conversation messages to the tool so it has context about the conversation
2086
2771
  messages,
2087
- // Pass context to the tool
2088
- context: experimentalContext,
2772
+ // Pass per-tool context to the tool (resolved from `toolsContext`)
2773
+ context,
2089
2774
  });
2090
-
2091
- // Use the appropriate output type based on the result
2092
- // AI SDK supports 'text' for strings and 'json' for objects
2093
- const output =
2094
- typeof toolResult === 'string'
2095
- ? { type: 'text' as const, value: toolResult }
2096
- : { type: 'json' as const, value: toolResult };
2097
-
2098
- return {
2099
- type: 'tool-result' as const,
2100
- toolCallId: toolCall.toolCallId,
2101
- toolName: toolCall.toolName,
2102
- output,
2103
- };
2104
2775
  } catch (error) {
2105
2776
  // Convert tool errors to error-text results sent back to the model,
2106
2777
  // allowing the agent to recover rather than killing the entire stream.
2107
2778
  // This aligns with AI SDK's streamText behavior for individual tool failures.
2779
+ const errorMessage = getErrorMessage(error);
2108
2780
  return {
2109
- type: 'tool-result',
2110
- toolCallId: toolCall.toolCallId,
2111
- toolName: toolCall.toolName,
2112
- output: {
2113
- type: 'error-text',
2114
- value: getErrorMessage(error),
2781
+ modelResult: {
2782
+ type: 'tool-result',
2783
+ toolCallId: toolCall.toolCallId,
2784
+ toolName: toolCall.toolName,
2785
+ output: await createLanguageModelToolResultOutput({
2786
+ toolCallId: toolCall.toolCallId,
2787
+ toolName: toolCall.toolName,
2788
+ input: parsedInput,
2789
+ output: errorMessage,
2790
+ tool,
2791
+ errorMode: 'text',
2792
+ supportedUrls: {},
2793
+ download,
2794
+ }),
2115
2795
  },
2796
+ rawOutput: errorMessage,
2797
+ isError: true,
2116
2798
  };
2117
2799
  }
2118
- }
2119
2800
 
2120
- /**
2121
- * Collected tool approval information for a single tool call.
2122
- */
2123
- interface CollectedApproval {
2124
- toolCallId: string;
2125
- toolName: string;
2126
- input: unknown;
2127
- approvalId: string;
2128
- reason?: string;
2129
- }
2130
-
2131
- /**
2132
- * Collect tool approval responses from model messages.
2133
- * Mirrors the logic from `collectToolApprovals` in the AI SDK core
2134
- * (`packages/ai/src/generate-text/collect-tool-approvals.ts`).
2135
- *
2136
- * Scans the last tool message for `tool-approval-response` parts,
2137
- * matches them with `tool-approval-request` parts in assistant messages
2138
- * and the corresponding `tool-call` parts.
2139
- */
2140
- function collectToolApprovalsFromMessages(messages: ModelMessage[]): {
2141
- approvedToolApprovals: CollectedApproval[];
2142
- deniedToolApprovals: CollectedApproval[];
2143
- } {
2144
- const lastMessage = messages.at(-1);
2145
-
2146
- if (lastMessage?.role !== 'tool') {
2147
- return { approvedToolApprovals: [], deniedToolApprovals: [] };
2148
- }
2149
-
2150
- // Gather tool calls from assistant messages
2151
- const toolCallsByToolCallId: Record<
2152
- string,
2153
- { toolName: string; input: unknown }
2154
- > = {};
2155
- for (const message of messages) {
2156
- if (message.role === 'assistant' && Array.isArray(message.content)) {
2157
- for (const part of message.content as any[]) {
2158
- if (part.type === 'tool-call') {
2159
- toolCallsByToolCallId[part.toolCallId] = {
2160
- toolName: part.toolName,
2161
- input: part.input ?? part.args,
2162
- };
2163
- }
2164
- }
2165
- }
2166
- }
2167
-
2168
- // Gather approval requests from assistant messages
2169
- const approvalRequestsByApprovalId: Record<
2170
- string,
2171
- { approvalId: string; toolCallId: string }
2172
- > = {};
2173
- for (const message of messages) {
2174
- if (message.role === 'assistant' && Array.isArray(message.content)) {
2175
- for (const part of message.content as any[]) {
2176
- if (part.type === 'tool-approval-request') {
2177
- approvalRequestsByApprovalId[part.approvalId] = {
2178
- approvalId: part.approvalId,
2179
- toolCallId: part.toolCallId,
2180
- };
2181
- }
2182
- }
2183
- }
2184
- }
2185
-
2186
- // Gather existing tool results to avoid re-executing
2187
- const existingToolResults = new Set<string>();
2188
- for (const part of lastMessage.content as any[]) {
2189
- if (part.type === 'tool-result') {
2190
- existingToolResults.add(part.toolCallId);
2191
- }
2192
- }
2193
-
2194
- const approvedToolApprovals: CollectedApproval[] = [];
2195
- const deniedToolApprovals: CollectedApproval[] = [];
2196
-
2197
- // Collect approval responses from the last tool message
2198
- const approvalResponses = (lastMessage.content as any[]).filter(
2199
- (part: any) => part.type === 'tool-approval-response',
2200
- );
2201
-
2202
- for (const response of approvalResponses) {
2203
- const approvalRequest = approvalRequestsByApprovalId[response.approvalId];
2204
- if (approvalRequest == null) continue;
2205
-
2206
- // Skip if there's already a tool result for this tool call
2207
- if (existingToolResults.has(approvalRequest.toolCallId)) continue;
2208
-
2209
- const toolCall = toolCallsByToolCallId[approvalRequest.toolCallId];
2210
- if (toolCall == null) continue;
2211
-
2212
- const approval: CollectedApproval = {
2213
- toolCallId: approvalRequest.toolCallId,
2801
+ return {
2802
+ modelResult: {
2803
+ type: 'tool-result' as const,
2804
+ toolCallId: toolCall.toolCallId,
2214
2805
  toolName: toolCall.toolName,
2215
- input: toolCall.input,
2216
- approvalId: response.approvalId,
2217
- reason: response.reason,
2218
- };
2219
-
2220
- if (response.approved) {
2221
- approvedToolApprovals.push(approval);
2222
- } else {
2223
- deniedToolApprovals.push(approval);
2224
- }
2225
- }
2226
-
2227
- return { approvedToolApprovals, deniedToolApprovals };
2806
+ output: await createLanguageModelToolResultOutput({
2807
+ toolCallId: toolCall.toolCallId,
2808
+ toolName: toolCall.toolName,
2809
+ input: parsedInput,
2810
+ output: toolResult,
2811
+ tool,
2812
+ errorMode: 'none',
2813
+ supportedUrls: {},
2814
+ download,
2815
+ }),
2816
+ },
2817
+ rawOutput: toolResult,
2818
+ isError: false,
2819
+ };
2228
2820
  }