@ai-sdk/workflow 2.0.58 → 2.0.59

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.
@@ -6,13 +6,11 @@ import type {
6
6
  } from '@ai-sdk/provider';
7
7
  import type { Context } from '@ai-sdk/provider-utils';
8
8
  import {
9
- DefaultGeneratedFile,
10
9
  experimental_filterActiveTools as filterActiveTools,
11
10
  type ActiveTools,
12
11
  type Experimental_SandboxSession as SandboxSession,
13
12
  type Instructions,
14
13
  type LanguageModel,
15
- type LanguageModelUsage,
16
14
  type ModelMessage,
17
15
  type StepResult,
18
16
  type ToolCallRepairFunction,
@@ -23,20 +21,22 @@ import {
23
21
  createRestrictedTelemetryDispatcher,
24
22
  createToolSearchState,
25
23
  } from 'ai/internal';
26
- import {
27
- type DoStreamStepRawResult,
28
- doStreamStep,
29
- type ModelCallStreamPart,
30
- type ModelStopCondition,
31
- type ParsedToolCall,
32
- type ProviderExecutedToolResult,
33
- type StreamFinish,
34
- type ToolInputLifecycleEvent,
35
- } from './do-stream-step.js';
24
+ import { buildModelStepResult } from './build-model-step-result.js';
25
+ import { doGenerateStep } from './do-generate-step.js';
26
+ import { doStreamStep } from './do-stream-step.js';
27
+ import type {
28
+ ModelCallStreamPart,
29
+ ModelCallOptions,
30
+ ModelStopCondition,
31
+ ParsedToolCall,
32
+ ProviderExecutedToolResult,
33
+ ToolInputLifecycleEvent,
34
+ } from './model-call.js';
36
35
  import {
37
36
  addToolResultsToConversation,
38
37
  type ProviderExecutedToolResultPosition,
39
38
  } from './add-tool-results-to-conversation.js';
39
+ import { toModelResponseMessages } from './to-model-response-messages.js';
40
40
  import { resolveToolContext } from './resolve-tool-context.js';
41
41
  import { serializeToolSet } from './serializable-schema.js';
42
42
  import type {
@@ -51,7 +51,7 @@ import type {
51
51
  } from './workflow-agent.js';
52
52
 
53
53
  // Re-export for consumers
54
- export type { ProviderExecutedToolResult } from './do-stream-step.js';
54
+ export type { ProviderExecutedToolResult } from './model-call.js';
55
55
 
56
56
  const prepareStepGenerationSettingKeys = [
57
57
  'maxOutputTokens',
@@ -87,7 +87,7 @@ function mergePrepareStepGenerationSettings(
87
87
  * The value yielded by the stream text iterator when tool calls are requested.
88
88
  * Contains both the tool calls and the current conversation messages.
89
89
  */
90
- export interface StreamTextIteratorYieldValue {
90
+ export interface ModelCallIteratorYieldValue {
91
91
  /** The tool calls requested by the model (parsed with typed inputs) */
92
92
  toolCalls: ParsedToolCall[];
93
93
  /** The tools available for execution in the current step. */
@@ -108,19 +108,21 @@ export interface StreamTextIteratorYieldValue {
108
108
  experimental_sandbox?: SandboxSession;
109
109
  }
110
110
 
111
- export interface StreamTextIteratorAbortedValue {
111
+ export interface ModelCallIteratorAbortedValue {
112
112
  aborted: true;
113
113
  messages: LanguageModelV4Prompt;
114
114
  }
115
115
 
116
- export interface StreamTextIteratorErrorValue {
116
+ export interface ModelCallIteratorErrorValue {
117
117
  error: unknown;
118
118
  messages: LanguageModelV4Prompt;
119
119
  }
120
120
 
121
121
  // This runs in the workflow context
122
- export async function* streamTextIterator({
122
+ export async function* modelCallIterator({
123
123
  prompt,
124
+ mode = 'stream',
125
+ include,
124
126
  initialInstructions,
125
127
  initialMessages = prompt as unknown as ModelMessage[],
126
128
  tools = {},
@@ -145,6 +147,8 @@ export async function* streamTextIterator({
145
147
  experimental_sandbox: sandbox,
146
148
  }: {
147
149
  prompt: LanguageModelV4Prompt;
150
+ mode?: 'generate' | 'stream';
151
+ include?: ModelCallOptions['include'];
148
152
  initialInstructions?: Instructions;
149
153
  initialMessages?: Array<ModelMessage>;
150
154
  tools: ToolSet;
@@ -171,10 +175,10 @@ export async function* streamTextIterator({
171
175
  | Array<StreamTextTransform<ToolSet>>;
172
176
  experimental_sandbox?: SandboxSession;
173
177
  }): AsyncGenerator<
174
- StreamTextIteratorYieldValue,
178
+ ModelCallIteratorYieldValue,
175
179
  | LanguageModelV4Prompt
176
- | StreamTextIteratorAbortedValue
177
- | StreamTextIteratorErrorValue,
180
+ | ModelCallIteratorAbortedValue
181
+ | ModelCallIteratorErrorValue,
178
182
  LanguageModelV4ToolResultPart[]
179
183
  > {
180
184
  let conversationPrompt = [...prompt]; // Create a mutable copy
@@ -202,7 +206,7 @@ export async function* streamTextIterator({
202
206
  });
203
207
 
204
208
  // TODO(#12164): replace this AI-core telemetry bridge with a
205
- // WorkflowAgent-specific typed dispatcher. `streamTextIterator` widens
209
+ // WorkflowAgent-specific typed dispatcher. `modelCallIterator` widens
206
210
  // tools/runtime context and emits Workflow-shaped events that are only
207
211
  // approximately compatible with generateText telemetry event types.
208
212
  const telemetryDispatcher = createRestrictedTelemetryDispatcher<
@@ -218,6 +222,8 @@ export async function* streamTextIterator({
218
222
  while (!done) {
219
223
  // Check for abort signal
220
224
  if (currentGenerationSettings.abortSignal?.aborted) {
225
+ if (mode === 'generate')
226
+ currentGenerationSettings.abortSignal.throwIfAborted();
221
227
  break;
222
228
  }
223
229
 
@@ -362,30 +368,43 @@ export async function* streamTextIterator({
362
368
  headers: currentGenerationSettings.headers,
363
369
  } as never);
364
370
 
365
- const stepInputMessages = conversationPrompt as unknown as ModelMessage[];
366
- const streamStepResult = await doStreamStep(
367
- conversationPrompt,
368
- currentModel,
369
- writable,
370
- serializedTools,
371
- {
372
- ...currentGenerationSettings,
373
- toolChoice: currentToolChoice,
374
- includeRawChunks,
375
- timeoutAt,
376
- repairToolCall,
377
- responseFormat,
378
- experimental_transform,
379
- },
380
- );
381
-
382
- if (streamStepResult.aborted) {
371
+ const stepInputMessages = (mode === 'generate'
372
+ ? [...conversationPrompt]
373
+ : conversationPrompt) as unknown as ModelMessage[];
374
+ const callOptions = {
375
+ ...currentGenerationSettings,
376
+ toolChoice: currentToolChoice,
377
+ includeRawChunks,
378
+ timeoutAt,
379
+ repairToolCall,
380
+ responseFormat,
381
+ include,
382
+ experimental_transform,
383
+ };
384
+ const modelCallResult =
385
+ mode === 'generate'
386
+ ? await doGenerateStep(
387
+ conversationPrompt,
388
+ currentModel,
389
+ serializedTools,
390
+ callOptions,
391
+ )
392
+ : await doStreamStep(
393
+ conversationPrompt,
394
+ currentModel,
395
+ writable,
396
+ serializedTools,
397
+ callOptions,
398
+ );
399
+
400
+ if (modelCallResult.aborted) {
383
401
  wasAborted = true;
384
402
  break;
385
403
  }
386
404
 
387
- if ('terminalError' in streamStepResult) {
388
- terminalError = streamStepResult.terminalError;
405
+ if ('terminalError' in modelCallResult) {
406
+ if (mode === 'generate') throw modelCallResult.terminalError;
407
+ terminalError = modelCallResult.terminalError;
389
408
  hasTerminalError = true;
390
409
  }
391
410
 
@@ -395,7 +414,7 @@ export async function* streamTextIterator({
395
414
  raw,
396
415
  providerExecutedToolResults,
397
416
  toolInputLifecycleEvents,
398
- } = streamStepResult;
417
+ } = modelCallResult;
399
418
  await invokeToolInputLifecycleCallbacks({
400
419
  events: toolInputLifecycleEvents ?? [],
401
420
  toolCalls,
@@ -408,12 +427,17 @@ export async function* streamTextIterator({
408
427
  // Reconstruct the full StepResult outside the step boundary so the
409
428
  // durable event log doesn't carry StepResult's redundant copies (or the
410
429
  // per-chunk snapshot the step used to return).
411
- const step = buildStepResult(
430
+ const step = await buildModelStepResult(
412
431
  raw,
413
432
  toolCalls,
414
433
  finish,
415
434
  providerExecutedToolResults,
416
435
  {
436
+ tools: effectiveTools,
437
+ requestMessages:
438
+ mode === 'generate' && include?.requestMessages
439
+ ? stepInputMessages
440
+ : undefined,
417
441
  stepNumber,
418
442
  runtimeContext: currentRuntimeContext,
419
443
  toolsContext: currentToolsContext,
@@ -471,7 +495,7 @@ export async function* streamTextIterator({
471
495
  const {
472
496
  content: assistantContent,
473
497
  providerExecutedToolResultPositions,
474
- } = getAssistantMessageContent(step);
498
+ } = getAssistantMessageContent(step, mode);
475
499
  const includedToolCallIds = new Set(
476
500
  assistantContent.flatMap(part =>
477
501
  part.type === 'tool-call' ? [part.toolCallId] : [],
@@ -484,7 +508,7 @@ export async function* streamTextIterator({
484
508
  // Note: providerMetadata from the tool call is mapped to providerOptions
485
509
  // in the prompt format, following the AI SDK convention. This is critical
486
510
  // for providers like Gemini that require thoughtSignature to be preserved
487
- // across multi-turn tool calls.
511
+ // across multi-turn tool calls. Some fields are sanitized before mapping.
488
512
  conversationPrompt.push({
489
513
  role: 'assistant',
490
514
  content: [
@@ -522,7 +546,9 @@ export async function* streamTextIterator({
522
546
  providerExecutedToolResultPositions,
523
547
  });
524
548
  step.response.messages.push(
525
- ...(responseMessages as unknown as typeof step.response.messages),
549
+ ...(mode === 'generate'
550
+ ? toModelResponseMessages(responseMessages)
551
+ : (responseMessages as unknown as typeof step.response.messages)),
526
552
  );
527
553
 
528
554
  const stopConditionList =
@@ -541,9 +567,17 @@ export async function* streamTextIterator({
541
567
  done =
542
568
  stopConditionMet ||
543
569
  (!hasClientToolCalls && pendingDeferredToolCallIds.size === 0);
544
- } else if (finishReason === 'stop' || finishReason === 'tool-calls') {
570
+ } else if (
571
+ mode === 'generate' ||
572
+ finishReason === 'stop' ||
573
+ finishReason === 'tool-calls' ||
574
+ finishReason === 'length'
575
+ ) {
545
576
  // Add assistant response content to the conversation
546
- const { content: assistantContent } = getAssistantMessageContent(step);
577
+ const { content: assistantContent } = getAssistantMessageContent(
578
+ step,
579
+ mode,
580
+ );
547
581
 
548
582
  if (assistantContent.length > 0) {
549
583
  const assistantMessage = {
@@ -552,13 +586,14 @@ export async function* streamTextIterator({
552
586
  } as const;
553
587
  conversationPrompt.push(assistantMessage);
554
588
  step.response.messages.push(
555
- assistantMessage as unknown as (typeof step.response.messages)[number],
589
+ ...(mode === 'generate'
590
+ ? toModelResponseMessages([assistantMessage])
591
+ : [
592
+ assistantMessage as unknown as (typeof step.response.messages)[number],
593
+ ]),
556
594
  );
557
595
  }
558
596
 
559
- done = true;
560
- } else if (finishReason === 'length') {
561
- // Model hit max tokens - stop but don't throw
562
597
  done = true;
563
598
  } else if (finishReason === 'content-filter') {
564
599
  // Content filter triggered - stop but don't throw
@@ -716,239 +751,11 @@ function normalizeStepForTelemetry(step: StepResult<any, any>) {
716
751
  };
717
752
  }
718
753
 
719
- /**
720
- * Reconstruct a full `StepResult` from the minimal aggregates returned by
721
- * `doStreamStep`. Runs outside the step boundary so StepResult's redundant
722
- * fields (duplicate tool-call lists, `text`, `files`, `sources`, and
723
- * `reasoningText`) and the per-chunk snapshot don't cross it. Tool-result
724
- * arrays are initialized here and populated after execution. The shape matches
725
- * what the AI SDK's `streamText` exposes to callers.
726
- */
727
- function buildStepResult(
728
- raw: DoStreamStepRawResult,
729
- toolCalls: ParsedToolCall[],
730
- finish: StreamFinish | undefined,
731
- providerExecutedToolResults: Map<string, ProviderExecutedToolResult>,
732
- opts: {
733
- stepNumber: number;
734
- runtimeContext: Context;
735
- toolsContext: Record<string, Context | undefined>;
736
- },
737
- ): StepResult<ToolSet, any> {
738
- const {
739
- content: rawContent,
740
- reasoning: reasoningParts,
741
- responseMetadata,
742
- warnings,
743
- } = raw;
744
- const reasoningText = reasoningParts.map(r => r.text).join('') || undefined;
745
- const validToolCallsByIndex = new Map(
746
- toolCalls.flatMap((tc, index) =>
747
- tc.invalid
748
- ? []
749
- : [
750
- [
751
- index,
752
- {
753
- type: 'tool-call' as const,
754
- toolCallId: tc.toolCallId,
755
- toolName: tc.toolName,
756
- input: tc.input,
757
- ...(tc.providerExecuted != null
758
- ? { providerExecuted: tc.providerExecuted }
759
- : {}),
760
- ...(tc.title != null ? { title: tc.title } : {}),
761
- ...(tc.toolMetadata != null
762
- ? { toolMetadata: tc.toolMetadata }
763
- : {}),
764
- ...(tc.dynamic ? { dynamic: true as const } : {}),
765
- ...(tc.providerExecuted ? { providerExecuted: true } : {}),
766
- ...(tc.providerMetadata != null
767
- ? { providerMetadata: tc.providerMetadata }
768
- : {}),
769
- },
770
- ] as const,
771
- ],
772
- ),
773
- );
774
- const validToolCalls = [...validToolCallsByIndex.values()];
775
- const content: StepResult<ToolSet, any>['content'] = [];
776
- const files: StepResult<ToolSet, any>['files'] = [];
777
- const sources: StepResult<ToolSet, any>['sources'] = [];
778
- let text = '';
779
-
780
- for (const part of rawContent) {
781
- switch (part.type) {
782
- case 'text':
783
- text += part.text;
784
- content.push({
785
- type: 'text',
786
- text: part.text,
787
- ...(part.providerMetadata != null
788
- ? { providerMetadata: part.providerMetadata }
789
- : {}),
790
- });
791
- break;
792
- case 'reasoning': {
793
- const reasoningPart = reasoningParts[part.reasoningIndex];
794
- if (reasoningPart != null) {
795
- content.push({
796
- type: 'reasoning',
797
- text: reasoningPart.text,
798
- ...(reasoningPart.providerMetadata != null
799
- ? { providerMetadata: reasoningPart.providerMetadata }
800
- : {}),
801
- });
802
- }
803
- break;
804
- }
805
- case 'file': {
806
- const file = new DefaultGeneratedFile({
807
- data: part.data,
808
- mediaType: part.mediaType,
809
- providerMetadata: part.providerMetadata,
810
- });
811
- files.push(file);
812
- content.push({
813
- type: 'file',
814
- file,
815
- ...(part.providerMetadata != null
816
- ? { providerMetadata: part.providerMetadata }
817
- : {}),
818
- });
819
- break;
820
- }
821
- case 'source':
822
- sources.push(part);
823
- content.push(part);
824
- break;
825
- case 'tool-call': {
826
- const toolCall = validToolCallsByIndex.get(part.toolCallIndex);
827
- if (toolCall != null) {
828
- content.push(toolCall);
829
- }
830
- break;
831
- }
832
- case 'provider-tool-result': {
833
- const result = providerExecutedToolResults.get(part.toolCallId);
834
- if (result == null) {
835
- break;
836
- }
837
-
838
- const toolCall = toolCalls.find(
839
- toolCall => toolCall.toolCallId === result.toolCallId,
840
- );
841
- const common = {
842
- toolCallId: result.toolCallId,
843
- toolName: result.toolName,
844
- input: toolCall?.input,
845
- providerExecuted: true as const,
846
- ...(result.dynamic === true || toolCall?.dynamic === true
847
- ? { dynamic: true as const }
848
- : {}),
849
- ...(result.providerMetadata != null
850
- ? { providerMetadata: result.providerMetadata }
851
- : {}),
852
- ...(toolCall?.toolMetadata != null
853
- ? { toolMetadata: toolCall.toolMetadata }
854
- : {}),
855
- };
856
-
857
- content.push(
858
- result.isError
859
- ? {
860
- type: 'tool-error',
861
- ...common,
862
- error: result.result,
863
- }
864
- : {
865
- type: 'tool-result',
866
- ...common,
867
- output: result.result,
868
- },
869
- );
870
- break;
871
- }
872
- }
873
- }
874
-
875
- const toolResults = content.filter(
876
- part => part.type === 'tool-result',
877
- ) as StepResult<ToolSet, any>['toolResults'];
878
-
879
- return {
880
- callId: 'workflow-agent',
881
- stepNumber: opts.stepNumber,
882
- model: {
883
- provider: responseMetadata?.modelId?.split(':')[0] ?? 'unknown',
884
- modelId: responseMetadata?.modelId ?? 'unknown',
885
- },
886
- functionId: undefined,
887
- metadata: undefined,
888
- runtimeContext: opts.runtimeContext ?? {},
889
- toolsContext: opts.toolsContext ?? {},
890
- content,
891
- text,
892
- reasoning: reasoningParts.map(r => ({
893
- type: 'reasoning' as const,
894
- text: r.text,
895
- ...(r.providerMetadata != null
896
- ? { providerMetadata: r.providerMetadata }
897
- : {}),
898
- })),
899
- reasoningText,
900
- files,
901
- sources,
902
- toolCalls: validToolCalls,
903
- staticToolCalls: validToolCalls.filter(tc => tc.dynamic !== true),
904
- dynamicToolCalls: validToolCalls.filter(tc => tc.dynamic),
905
- toolResults,
906
- staticToolResults: toolResults.filter(result => result.dynamic !== true),
907
- dynamicToolResults: toolResults.filter(result => result.dynamic === true),
908
- finishReason: finish?.finishReason ?? 'other',
909
- rawFinishReason: finish?.rawFinishReason,
910
- usage:
911
- finish?.usage ??
912
- ({
913
- inputTokens: 0,
914
- inputTokenDetails: {
915
- noCacheTokens: undefined,
916
- cacheReadTokens: undefined,
917
- cacheWriteTokens: undefined,
918
- },
919
- outputTokens: 0,
920
- outputTokenDetails: {
921
- textTokens: undefined,
922
- reasoningTokens: undefined,
923
- },
924
- totalTokens: 0,
925
- } as LanguageModelUsage),
926
- performance: {
927
- effectiveOutputTokensPerSecond: 0,
928
- outputTokensPerSecond: undefined,
929
- inputTokensPerSecond: undefined,
930
- effectiveTotalTokensPerSecond: 0,
931
- stepTimeMs: 0,
932
- responseTimeMs: 0,
933
- toolExecutionMs: {},
934
- timeToFirstOutputMs: undefined,
935
- },
936
- warnings,
937
- request: {
938
- body: '',
939
- messages: [], // TODO implement step request messages
940
- },
941
- response: {
942
- id: responseMetadata?.id ?? 'unknown',
943
- timestamp: responseMetadata?.timestamp ?? new Date(),
944
- modelId: responseMetadata?.modelId ?? 'unknown',
945
- messages: [],
946
- },
947
- providerMetadata: finish?.providerMetadata ?? {},
948
- } as StepResult<ToolSet, any>;
949
- }
950
-
951
- function getAssistantMessageContent(step: StepResult<any, any>): {
754
+ /** Preserve assistant ordering and positions for provider-executed results. */
755
+ function getAssistantMessageContent(
756
+ step: StepResult<any, any>,
757
+ mode: 'generate' | 'stream',
758
+ ): {
952
759
  content: Extract<
953
760
  LanguageModelV4Prompt[number],
954
761
  { role: 'assistant' }
@@ -965,28 +772,41 @@ function getAssistantMessageContent(step: StepResult<any, any>): {
965
772
 
966
773
  for (const part of step.content) {
967
774
  switch (part.type) {
968
- case 'reasoning':
775
+ case 'text':
776
+ if (part.text.length > 0) {
777
+ content.push({
778
+ type: 'text',
779
+ text: part.text,
780
+ ...(part.providerMetadata != null
781
+ ? { providerOptions: part.providerMetadata }
782
+ : {}),
783
+ });
784
+ contentIndex++;
785
+ }
786
+ break;
787
+ case 'reasoning': {
788
+ const { providerMetadata, ...messagePart } = part;
969
789
  content.push({
970
- type: 'reasoning',
971
- text: part.text,
972
- ...(part.providerMetadata != null
973
- ? {
974
- providerOptions:
975
- part.providerMetadata as SharedV4ProviderOptions,
976
- }
790
+ ...messagePart,
791
+ ...(providerMetadata != null
792
+ ? { providerOptions: providerMetadata }
977
793
  : {}),
978
794
  });
979
795
  contentIndex++;
980
796
  break;
981
- case 'text':
982
- if (part.text.length > 0) {
983
- content.push({ type: 'text', text: part.text });
797
+ }
798
+ case 'custom':
799
+ if (mode === 'generate') {
800
+ const { providerMetadata, ...messagePart } = part;
801
+ content.push({ ...messagePart, providerOptions: providerMetadata });
984
802
  contentIndex++;
985
803
  }
986
804
  break;
805
+ case 'reasoning-file':
806
+ if (mode !== 'generate') break;
987
807
  case 'file':
988
808
  content.push({
989
- type: 'file',
809
+ type: part.type,
990
810
  data: { type: 'data', data: part.file.base64 },
991
811
  mediaType: part.file.mediaType,
992
812
  ...(part.providerMetadata != null