@ai-sdk/workflow 2.0.28 → 2.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/workflow",
3
- "version": "2.0.28",
3
+ "version": "2.0.29",
4
4
  "type": "module",
5
5
  "description": "WorkflowAgent for building AI agents with AI SDK",
6
6
  "license": "Apache-2.0",
@@ -31,9 +31,9 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
- "@ai-sdk/provider": "4.0.13",
35
- "@ai-sdk/provider-utils": "5.0.39",
36
- "ai": "7.0.97",
34
+ "@ai-sdk/provider": "4.0.14",
35
+ "@ai-sdk/provider-utils": "5.0.40",
36
+ "ai": "7.0.98",
37
37
  "ajv": "^8.20.0"
38
38
  },
39
39
  "devDependencies": {
@@ -0,0 +1,108 @@
1
+ import type {
2
+ LanguageModelV4Prompt,
3
+ LanguageModelV4ToolResultPart,
4
+ } from '@ai-sdk/provider';
5
+
6
+ export interface ProviderExecutedToolResultPosition {
7
+ toolCallId: string;
8
+ contentIndex: number;
9
+ }
10
+
11
+ export function addToolResultsToConversation({
12
+ messages,
13
+ toolResults,
14
+ providerExecutedToolCallIds,
15
+ providerExecutedToolResultPositions = [],
16
+ }: {
17
+ messages: LanguageModelV4Prompt;
18
+ toolResults: LanguageModelV4ToolResultPart[];
19
+ providerExecutedToolCallIds: Set<string>;
20
+ providerExecutedToolResultPositions?: ProviderExecutedToolResultPosition[];
21
+ }) {
22
+ const providerResultIds = new Set([
23
+ ...providerExecutedToolCallIds,
24
+ ...providerExecutedToolResultPositions.map(position => position.toolCallId),
25
+ ]);
26
+ const providerResults = new Map<string, LanguageModelV4ToolResultPart[]>();
27
+ const clientResults: LanguageModelV4ToolResultPart[] = [];
28
+ let assistantMessageIndex = -1;
29
+ let assistantMessage:
30
+ | Extract<LanguageModelV4Prompt[number], { role: 'assistant' }>
31
+ | undefined;
32
+
33
+ for (let index = messages.length - 1; index >= 0; index--) {
34
+ const message = messages[index];
35
+ if (message.role === 'assistant') {
36
+ assistantMessageIndex = index;
37
+ assistantMessage = message;
38
+ break;
39
+ }
40
+ }
41
+
42
+ for (const toolResult of toolResults) {
43
+ if (providerResultIds.has(toolResult.toolCallId)) {
44
+ const results = providerResults.get(toolResult.toolCallId) ?? [];
45
+ results.push(toolResult);
46
+ providerResults.set(toolResult.toolCallId, results);
47
+ } else {
48
+ clientResults.push(toolResult);
49
+ }
50
+ }
51
+
52
+ if (providerResults.size > 0) {
53
+ if (assistantMessage != null) {
54
+ let content = [...assistantMessage.content];
55
+
56
+ for (const position of [...providerExecutedToolResultPositions].sort(
57
+ (a, b) => a.contentIndex - b.contentIndex,
58
+ )) {
59
+ const results = providerResults.get(position.toolCallId);
60
+ if (results == null || results.length === 0) {
61
+ continue;
62
+ }
63
+
64
+ const [result, ...remainingResults] = results;
65
+ content.splice(position.contentIndex, 0, result);
66
+
67
+ if (remainingResults.length === 0) {
68
+ providerResults.delete(position.toolCallId);
69
+ } else {
70
+ providerResults.set(position.toolCallId, remainingResults);
71
+ }
72
+ }
73
+
74
+ const contentWithFallbackResults: typeof assistantMessage.content = [];
75
+
76
+ for (const part of content) {
77
+ contentWithFallbackResults.push(part);
78
+
79
+ if (part.type !== 'tool-call') {
80
+ continue;
81
+ }
82
+
83
+ const results = providerResults.get(part.toolCallId);
84
+ if (results == null) {
85
+ continue;
86
+ }
87
+
88
+ providerResults.delete(part.toolCallId);
89
+ contentWithFallbackResults.push(...results);
90
+ }
91
+
92
+ for (const results of providerResults.values()) {
93
+ contentWithFallbackResults.push(...results);
94
+ }
95
+
96
+ assistantMessage.content = contentWithFallbackResults;
97
+ }
98
+ }
99
+
100
+ if (clientResults.length > 0) {
101
+ messages.push({
102
+ role: 'tool',
103
+ content: clientResults,
104
+ });
105
+ }
106
+
107
+ return assistantMessageIndex < 0 ? [] : messages.slice(assistantMessageIndex);
108
+ }
@@ -46,6 +46,8 @@ export interface ProviderExecutedToolResult {
46
46
  toolName: string;
47
47
  result: unknown;
48
48
  isError?: boolean;
49
+ dynamic?: boolean;
50
+ providerMetadata?: SharedV4ProviderMetadata;
49
51
  }
50
52
 
51
53
  /**
@@ -115,6 +117,10 @@ export type DoStreamStepRawContentPart =
115
117
  | {
116
118
  type: 'tool-call';
117
119
  toolCallIndex: number;
120
+ }
121
+ | {
122
+ type: 'provider-tool-result';
123
+ toolCallId: string;
118
124
  };
119
125
 
120
126
  /**
@@ -407,6 +413,12 @@ export async function doStreamStep(
407
413
  toolName: part.toolName,
408
414
  result: part.output,
409
415
  isError: false,
416
+ dynamic: part.dynamic,
417
+ providerMetadata: part.providerMetadata,
418
+ });
419
+ content.push({
420
+ type: 'provider-tool-result',
421
+ toolCallId: part.toolCallId,
410
422
  });
411
423
  }
412
424
  break;
@@ -420,6 +432,12 @@ export async function doStreamStep(
420
432
  toolName: errorPart.toolName,
421
433
  result: errorPart.error,
422
434
  isError: true,
435
+ dynamic: errorPart.dynamic,
436
+ providerMetadata: errorPart.providerMetadata,
437
+ });
438
+ content.push({
439
+ type: 'provider-tool-result',
440
+ toolCallId: errorPart.toolCallId,
423
441
  });
424
442
  }
425
443
  break;
@@ -30,6 +30,10 @@ import {
30
30
  type StreamFinish,
31
31
  type ToolInputLifecycleEvent,
32
32
  } from './do-stream-step.js';
33
+ import {
34
+ addToolResultsToConversation,
35
+ type ProviderExecutedToolResultPosition,
36
+ } from './add-tool-results-to-conversation.js';
33
37
  import { resolveToolContext } from './resolve-tool-context.js';
34
38
  import { serializeToolSet } from './serializable-schema.js';
35
39
  import type {
@@ -92,6 +96,8 @@ export interface StreamTextIteratorYieldValue {
92
96
  toolsContext?: Record<string, Context | undefined>;
93
97
  /** Provider-executed tool results (keyed by tool call ID) */
94
98
  providerExecutedToolResults?: Map<string, ProviderExecutedToolResult>;
99
+ /** Original positions of provider-executed results in assistant content. */
100
+ providerExecutedToolResultPositions?: ProviderExecutedToolResultPosition[];
95
101
  /** The sandbox selected for the current step. */
96
102
  experimental_sandbox?: SandboxSession;
97
103
  }
@@ -175,7 +181,8 @@ export async function* streamTextIterator({
175
181
  let _isFirstIteration = true;
176
182
  let stepNumber = 0;
177
183
  let lastStep: StepResult<any, any> | undefined;
178
- let lastStepWasToolCalls = false;
184
+ let lastStepWasYielded = false;
185
+ const pendingDeferredToolCallIds = new Set<string>();
179
186
  let wasAborted = false;
180
187
  let terminalError: unknown;
181
188
  let hasTerminalError = false;
@@ -385,11 +392,17 @@ export async function* streamTextIterator({
385
392
  // Reconstruct the full StepResult outside the step boundary so the
386
393
  // durable event log doesn't carry StepResult's redundant copies (or the
387
394
  // per-chunk snapshot the step used to return).
388
- const step = buildStepResult(raw, toolCalls, finish, {
389
- stepNumber,
390
- runtimeContext: currentRuntimeContext,
391
- toolsContext: currentToolsContext,
392
- });
395
+ const step = buildStepResult(
396
+ raw,
397
+ toolCalls,
398
+ finish,
399
+ providerExecutedToolResults,
400
+ {
401
+ stepNumber,
402
+ runtimeContext: currentRuntimeContext,
403
+ toolsContext: currentToolsContext,
404
+ },
405
+ );
393
406
 
394
407
  await telemetryDispatcher.onLanguageModelCallEnd?.({
395
408
  callId: step.callId,
@@ -408,19 +421,41 @@ export async function* streamTextIterator({
408
421
  stepNumber++;
409
422
  steps.push(step);
410
423
  lastStep = step;
411
- lastStepWasToolCalls = false;
424
+ lastStepWasYielded = false;
412
425
 
413
426
  const finishReason = finish?.finishReason;
427
+ const isToolExecutionAllowed =
428
+ finishReason === 'tool-calls' || finishReason === 'stop';
429
+
430
+ for (const toolCall of toolCalls) {
431
+ if (
432
+ toolCall.providerExecuted &&
433
+ serializedTools[toolCall.toolName]?.supportsDeferredResults &&
434
+ !providerExecutedToolResults.has(toolCall.toolCallId)
435
+ ) {
436
+ pendingDeferredToolCallIds.add(toolCall.toolCallId);
437
+ }
438
+ }
439
+ for (const toolCallId of providerExecutedToolResults.keys()) {
440
+ pendingDeferredToolCallIds.delete(toolCallId);
441
+ }
442
+
443
+ const shouldProcessTools =
444
+ isToolExecutionAllowed &&
445
+ (toolCalls.length > 0 || providerExecutedToolResults.size > 0);
414
446
 
415
447
  if (hasTerminalError) {
416
448
  // The error crossed the durable step boundary as data. End the loop
417
449
  // without throwing so WorkflowAgent can preserve the existing
418
450
  // resolved-result contract and expose the original value.
419
451
  done = true;
420
- } else if (finishReason === 'tool-calls') {
421
- lastStepWasToolCalls = true;
452
+ } else if (shouldProcessTools) {
453
+ lastStepWasYielded = true;
422
454
 
423
- const assistantContent = getAssistantMessageContent(step);
455
+ const {
456
+ content: assistantContent,
457
+ providerExecutedToolResultPositions,
458
+ } = getAssistantMessageContent(step);
424
459
  const includedToolCallIds = new Set(
425
460
  assistantContent.flatMap(part =>
426
461
  part.type === 'tool-call' ? [part.toolCallId] : [],
@@ -455,30 +490,53 @@ export async function* streamTextIterator({
455
490
  toolsContext: currentToolsContext,
456
491
  experimental_sandbox: stepSandbox,
457
492
  providerExecutedToolResults,
493
+ providerExecutedToolResultPositions,
458
494
  };
459
495
 
460
- conversationPrompt.push({
461
- role: 'tool',
462
- content: toolResults,
496
+ const responseMessages = addToolResultsToConversation({
497
+ messages: conversationPrompt,
498
+ toolResults,
499
+ providerExecutedToolCallIds: new Set([
500
+ ...toolCalls.flatMap(toolCall =>
501
+ toolCall.providerExecuted ? [toolCall.toolCallId] : [],
502
+ ),
503
+ ...providerExecutedToolResults.keys(),
504
+ ]),
505
+ providerExecutedToolResultPositions,
463
506
  });
507
+ step.response.messages.push(
508
+ ...(responseMessages as unknown as typeof step.response.messages),
509
+ );
464
510
 
465
- if (stopConditions) {
466
- const stopConditionList = Array.isArray(stopConditions)
467
- ? stopConditions
468
- : [stopConditions];
469
- if (stopConditionList.some(test => test({ steps }))) {
470
- done = true;
471
- }
472
- }
473
- } else if (finishReason === 'stop') {
511
+ const stopConditionList =
512
+ stopConditions == null
513
+ ? []
514
+ : Array.isArray(stopConditions)
515
+ ? stopConditions
516
+ : [stopConditions];
517
+ const stopConditionMet = stopConditionList.some(test =>
518
+ test({ steps }),
519
+ );
520
+ const hasClientToolCalls = toolCalls.some(
521
+ toolCall => !toolCall.providerExecuted,
522
+ );
523
+
524
+ done =
525
+ stopConditionMet ||
526
+ (!hasClientToolCalls && pendingDeferredToolCallIds.size === 0);
527
+ } else if (finishReason === 'stop' || finishReason === 'tool-calls') {
474
528
  // Add assistant response content to the conversation
475
- const assistantContent = getAssistantMessageContent(step);
529
+ const { content: assistantContent } = getAssistantMessageContent(step);
476
530
 
477
531
  if (assistantContent.length > 0) {
478
- conversationPrompt.push({
532
+ const assistantMessage = {
479
533
  role: 'assistant',
480
534
  content: assistantContent,
481
- });
535
+ } as const;
536
+ conversationPrompt.push(assistantMessage);
537
+ step.response.messages.push(
538
+ assistantMessage as unknown as (typeof step.response.messages)[number],
539
+ );
482
540
  }
483
541
 
484
542
  done = true;
@@ -519,8 +577,8 @@ export async function* streamTextIterator({
519
577
  }
520
578
  }
521
579
 
522
- // Yield the final step if it wasn't already yielded (tool-calls steps are yielded inside the loop)
523
- if (lastStep && !lastStepWasToolCalls) {
580
+ // Yield the final step if it wasn't already yielded inside the loop.
581
+ if (lastStep && !lastStepWasYielded) {
524
582
  yield {
525
583
  toolCalls: [],
526
584
  messages: conversationPrompt,
@@ -653,6 +711,7 @@ function buildStepResult(
653
711
  raw: DoStreamStepRawResult,
654
712
  toolCalls: ParsedToolCall[],
655
713
  finish: StreamFinish | undefined,
714
+ providerExecutedToolResults: Map<string, ProviderExecutedToolResult>,
656
715
  opts: {
657
716
  stepNumber: number;
658
717
  runtimeContext: Context;
@@ -740,9 +799,53 @@ function buildStepResult(
740
799
  }
741
800
  break;
742
801
  }
802
+ case 'provider-tool-result': {
803
+ const result = providerExecutedToolResults.get(part.toolCallId);
804
+ if (result == null) {
805
+ break;
806
+ }
807
+
808
+ const toolCall = toolCalls.find(
809
+ toolCall => toolCall.toolCallId === result.toolCallId,
810
+ );
811
+ const common = {
812
+ toolCallId: result.toolCallId,
813
+ toolName: result.toolName,
814
+ input: toolCall?.input,
815
+ providerExecuted: true as const,
816
+ ...(result.dynamic === true || toolCall?.dynamic === true
817
+ ? { dynamic: true as const }
818
+ : {}),
819
+ ...(result.providerMetadata != null
820
+ ? { providerMetadata: result.providerMetadata }
821
+ : {}),
822
+ ...(toolCall?.toolMetadata != null
823
+ ? { toolMetadata: toolCall.toolMetadata }
824
+ : {}),
825
+ };
826
+
827
+ content.push(
828
+ result.isError
829
+ ? {
830
+ type: 'tool-error',
831
+ ...common,
832
+ error: result.result,
833
+ }
834
+ : {
835
+ type: 'tool-result',
836
+ ...common,
837
+ output: result.result,
838
+ },
839
+ );
840
+ break;
841
+ }
743
842
  }
744
843
  }
745
844
 
845
+ const toolResults = content.filter(
846
+ part => part.type === 'tool-result',
847
+ ) as StepResult<ToolSet, any>['toolResults'];
848
+
746
849
  return {
747
850
  callId: 'workflow-agent',
748
851
  stepNumber: opts.stepNumber,
@@ -766,9 +869,9 @@ function buildStepResult(
766
869
  toolCalls: validToolCalls,
767
870
  staticToolCalls: validToolCalls.filter(tc => tc.dynamic !== true),
768
871
  dynamicToolCalls: validToolCalls.filter(tc => tc.dynamic),
769
- toolResults: [],
770
- staticToolResults: [],
771
- dynamicToolResults: [],
872
+ toolResults,
873
+ staticToolResults: toolResults.filter(result => result.dynamic !== true),
874
+ dynamicToolResults: toolResults.filter(result => result.dynamic === true),
772
875
  finishReason: finish?.finishReason ?? 'other',
773
876
  rawFinishReason: finish?.rawFinishReason,
774
877
  usage:
@@ -812,19 +915,27 @@ function buildStepResult(
812
915
  } as StepResult<ToolSet, any>;
813
916
  }
814
917
 
815
- function getAssistantMessageContent(
816
- step: StepResult<any, any>,
817
- ): Extract<LanguageModelV4Prompt[number], { role: 'assistant' }>['content'] {
918
+ function getAssistantMessageContent(step: StepResult<any, any>): {
919
+ content: Extract<
920
+ LanguageModelV4Prompt[number],
921
+ { role: 'assistant' }
922
+ >['content'];
923
+ providerExecutedToolResultPositions: ProviderExecutedToolResultPosition[];
924
+ } {
818
925
  const content: Extract<
819
926
  LanguageModelV4Prompt[number],
820
927
  { role: 'assistant' }
821
928
  >['content'] = [];
929
+ const providerExecutedToolResultPositions: ProviderExecutedToolResultPosition[] =
930
+ [];
931
+ let contentIndex = 0;
822
932
 
823
933
  for (const part of step.content) {
824
934
  switch (part.type) {
825
935
  case 'text':
826
936
  if (part.text.length > 0) {
827
937
  content.push({ type: 'text', text: part.text });
938
+ contentIndex++;
828
939
  }
829
940
  break;
830
941
  case 'file':
@@ -839,14 +950,26 @@ function getAssistantMessageContent(
839
950
  }
840
951
  : {}),
841
952
  });
953
+ contentIndex++;
842
954
  break;
843
955
  case 'tool-call':
844
956
  content.push(toAssistantToolCallContent(part));
957
+ contentIndex++;
958
+ break;
959
+ case 'tool-result':
960
+ case 'tool-error':
961
+ if (part.providerExecuted) {
962
+ providerExecutedToolResultPositions.push({
963
+ toolCallId: part.toolCallId,
964
+ contentIndex,
965
+ });
966
+ contentIndex++;
967
+ }
845
968
  break;
846
969
  }
847
970
  }
848
971
 
849
- return content;
972
+ return { content, providerExecutedToolResultPositions };
850
973
  }
851
974
 
852
975
  function toAssistantToolCallContent(toolCall: {