@ai-sdk/workflow 2.0.19 → 2.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/index.d.ts +10 -9
- package/dist/index.js +480 -106
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/do-stream-step.ts +165 -12
- package/src/resolve-tool-context.ts +28 -0
- package/src/serializable-schema.ts +99 -30
- package/src/stream-text-iterator.ts +273 -51
- package/src/workflow-agent.ts +169 -88
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from '@ai-sdk/provider';
|
|
7
7
|
import type { Context } from '@ai-sdk/provider-utils';
|
|
8
8
|
import {
|
|
9
|
+
DefaultGeneratedFile,
|
|
9
10
|
experimental_filterActiveTools as filterActiveTools,
|
|
10
11
|
type ActiveTools,
|
|
11
12
|
type Experimental_SandboxSession as SandboxSession,
|
|
@@ -27,7 +28,9 @@ import {
|
|
|
27
28
|
type ParsedToolCall,
|
|
28
29
|
type ProviderExecutedToolResult,
|
|
29
30
|
type StreamFinish,
|
|
31
|
+
type ToolInputLifecycleEvent,
|
|
30
32
|
} from './do-stream-step.js';
|
|
33
|
+
import { resolveToolContext } from './resolve-tool-context.js';
|
|
31
34
|
import { serializeToolSet } from './serializable-schema.js';
|
|
32
35
|
import type {
|
|
33
36
|
GenerationSettings,
|
|
@@ -337,6 +340,7 @@ export async function* streamTextIterator({
|
|
|
337
340
|
headers: currentGenerationSettings.headers,
|
|
338
341
|
} as never);
|
|
339
342
|
|
|
343
|
+
const stepInputMessages = conversationPrompt as unknown as ModelMessage[];
|
|
340
344
|
const streamStepResult = await doStreamStep(
|
|
341
345
|
conversationPrompt,
|
|
342
346
|
currentModel,
|
|
@@ -362,8 +366,22 @@ export async function* streamTextIterator({
|
|
|
362
366
|
hasTerminalError = true;
|
|
363
367
|
}
|
|
364
368
|
|
|
365
|
-
const {
|
|
366
|
-
|
|
369
|
+
const {
|
|
370
|
+
toolCalls,
|
|
371
|
+
finish,
|
|
372
|
+
raw,
|
|
373
|
+
providerExecutedToolResults,
|
|
374
|
+
toolInputLifecycleEvents,
|
|
375
|
+
} = streamStepResult;
|
|
376
|
+
await invokeToolInputLifecycleCallbacks({
|
|
377
|
+
events: toolInputLifecycleEvents ?? [],
|
|
378
|
+
toolCalls,
|
|
379
|
+
tools: effectiveTools,
|
|
380
|
+
messages: stepInputMessages,
|
|
381
|
+
abortSignal: currentGenerationSettings.abortSignal,
|
|
382
|
+
toolsContext: currentToolsContext,
|
|
383
|
+
experimental_sandbox: stepSandbox,
|
|
384
|
+
});
|
|
367
385
|
// Reconstruct the full StepResult outside the step boundary so the
|
|
368
386
|
// durable event log doesn't carry StepResult's redundant copies (or the
|
|
369
387
|
// per-chunk snapshot the step used to return).
|
|
@@ -402,11 +420,16 @@ export async function* streamTextIterator({
|
|
|
402
420
|
} else if (finishReason === 'tool-calls') {
|
|
403
421
|
lastStepWasToolCalls = true;
|
|
404
422
|
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
423
|
+
const assistantContent = getAssistantMessageContent(step);
|
|
424
|
+
const includedToolCallIds = new Set(
|
|
425
|
+
assistantContent.flatMap(part =>
|
|
426
|
+
part.type === 'tool-call' ? [part.toolCallId] : [],
|
|
427
|
+
),
|
|
428
|
+
);
|
|
408
429
|
|
|
409
|
-
// Add assistant message
|
|
430
|
+
// Add assistant message content in provider emission order. Invalid
|
|
431
|
+
// tool calls are not part of StepResult.content, so retain the previous
|
|
432
|
+
// behavior of appending them to the prompt.
|
|
410
433
|
// Note: providerMetadata from the tool call is mapped to providerOptions
|
|
411
434
|
// in the prompt format, following the AI SDK convention. This is critical
|
|
412
435
|
// for providers like Gemini that require thoughtSignature to be preserved
|
|
@@ -414,24 +437,10 @@ export async function* streamTextIterator({
|
|
|
414
437
|
conversationPrompt.push({
|
|
415
438
|
role: 'assistant',
|
|
416
439
|
content: [
|
|
417
|
-
...
|
|
418
|
-
...toolCalls
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
);
|
|
422
|
-
return {
|
|
423
|
-
type: 'tool-call' as const,
|
|
424
|
-
toolCallId: toolCall.toolCallId,
|
|
425
|
-
toolName: toolCall.toolName,
|
|
426
|
-
input: toolCall.input,
|
|
427
|
-
...(sanitizedMetadata != null
|
|
428
|
-
? {
|
|
429
|
-
providerOptions:
|
|
430
|
-
sanitizedMetadata as SharedV4ProviderOptions,
|
|
431
|
-
}
|
|
432
|
-
: {}),
|
|
433
|
-
};
|
|
434
|
-
}),
|
|
440
|
+
...assistantContent,
|
|
441
|
+
...toolCalls
|
|
442
|
+
.filter(toolCall => !includedToolCallIds.has(toolCall.toolCallId))
|
|
443
|
+
.map(toAssistantToolCallContent),
|
|
435
444
|
],
|
|
436
445
|
});
|
|
437
446
|
|
|
@@ -462,15 +471,13 @@ export async function* streamTextIterator({
|
|
|
462
471
|
}
|
|
463
472
|
}
|
|
464
473
|
} else if (finishReason === 'stop') {
|
|
465
|
-
// Add assistant
|
|
466
|
-
const
|
|
467
|
-
item => item.type === 'text',
|
|
468
|
-
) as Array<{ type: 'text'; text: string }>;
|
|
474
|
+
// Add assistant response content to the conversation
|
|
475
|
+
const assistantContent = getAssistantMessageContent(step);
|
|
469
476
|
|
|
470
|
-
if (
|
|
477
|
+
if (assistantContent.length > 0) {
|
|
471
478
|
conversationPrompt.push({
|
|
472
479
|
role: 'assistant',
|
|
473
|
-
content:
|
|
480
|
+
content: assistantContent,
|
|
474
481
|
});
|
|
475
482
|
}
|
|
476
483
|
|
|
@@ -535,6 +542,89 @@ export async function* streamTextIterator({
|
|
|
535
542
|
return conversationPrompt;
|
|
536
543
|
}
|
|
537
544
|
|
|
545
|
+
async function invokeToolInputLifecycleCallbacks({
|
|
546
|
+
events,
|
|
547
|
+
toolCalls,
|
|
548
|
+
tools,
|
|
549
|
+
messages,
|
|
550
|
+
abortSignal,
|
|
551
|
+
toolsContext,
|
|
552
|
+
experimental_sandbox,
|
|
553
|
+
}: {
|
|
554
|
+
events: ToolInputLifecycleEvent[];
|
|
555
|
+
toolCalls: ParsedToolCall[];
|
|
556
|
+
tools: ToolSet;
|
|
557
|
+
messages: ModelMessage[];
|
|
558
|
+
abortSignal?: AbortSignal;
|
|
559
|
+
toolsContext: Record<string, Context | undefined>;
|
|
560
|
+
experimental_sandbox?: SandboxSession;
|
|
561
|
+
}) {
|
|
562
|
+
const toolNamesByCallId = new Map<string, string>();
|
|
563
|
+
const toolCallsById = new Map(
|
|
564
|
+
toolCalls.map(toolCall => [toolCall.toolCallId, toolCall]),
|
|
565
|
+
);
|
|
566
|
+
const resolvedContexts = new Map<string, Promise<unknown>>();
|
|
567
|
+
|
|
568
|
+
for (const event of events) {
|
|
569
|
+
const [type, toolCallId, value] = event;
|
|
570
|
+
if (type === 'start') {
|
|
571
|
+
toolNamesByCallId.set(toolCallId, value);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const toolName =
|
|
575
|
+
type === 'start' ? value : toolNamesByCallId.get(toolCallId);
|
|
576
|
+
if (toolName == null) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const tool = tools[toolName];
|
|
581
|
+
if (tool == null) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
let resolvedContext = resolvedContexts.get(toolName);
|
|
586
|
+
if (resolvedContext == null) {
|
|
587
|
+
resolvedContext = resolveToolContext({
|
|
588
|
+
toolName,
|
|
589
|
+
tool,
|
|
590
|
+
toolsContext,
|
|
591
|
+
});
|
|
592
|
+
resolvedContexts.set(toolName, resolvedContext);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const options = {
|
|
596
|
+
toolCallId,
|
|
597
|
+
messages,
|
|
598
|
+
abortSignal,
|
|
599
|
+
context: await resolvedContext,
|
|
600
|
+
experimental_sandbox,
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
switch (type) {
|
|
604
|
+
case 'start':
|
|
605
|
+
await tool.onInputStart?.(options);
|
|
606
|
+
break;
|
|
607
|
+
case 'delta':
|
|
608
|
+
await tool.onInputDelta?.({
|
|
609
|
+
...options,
|
|
610
|
+
inputTextDelta: value,
|
|
611
|
+
});
|
|
612
|
+
break;
|
|
613
|
+
case 'available': {
|
|
614
|
+
const toolCall = toolCallsById.get(toolCallId);
|
|
615
|
+
if (toolCall == null) {
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
await tool.onInputAvailable?.({
|
|
619
|
+
...options,
|
|
620
|
+
input: toolCall.input,
|
|
621
|
+
});
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
538
628
|
function getModelInfo(model: LanguageModel): {
|
|
539
629
|
provider: string;
|
|
540
630
|
modelId: string;
|
|
@@ -554,9 +644,10 @@ function normalizeStepForTelemetry(step: StepResult<any, any>) {
|
|
|
554
644
|
/**
|
|
555
645
|
* Reconstruct a full `StepResult` from the minimal aggregates returned by
|
|
556
646
|
* `doStreamStep`. Runs outside the step boundary so StepResult's redundant
|
|
557
|
-
* fields (duplicate tool-call lists, `
|
|
558
|
-
*
|
|
559
|
-
*
|
|
647
|
+
* fields (duplicate tool-call lists, `text`, `files`, `sources`, and
|
|
648
|
+
* `reasoningText`) and the per-chunk snapshot don't cross it. Tool-result
|
|
649
|
+
* arrays are initialized here and populated after execution. The shape matches
|
|
650
|
+
* what the AI SDK's `streamText` exposes to callers.
|
|
560
651
|
*/
|
|
561
652
|
function buildStepResult(
|
|
562
653
|
raw: DoStreamStepRawResult,
|
|
@@ -568,18 +659,89 @@ function buildStepResult(
|
|
|
568
659
|
toolsContext: Record<string, Context | undefined>;
|
|
569
660
|
},
|
|
570
661
|
): StepResult<ToolSet, any> {
|
|
571
|
-
const {
|
|
662
|
+
const {
|
|
663
|
+
content: rawContent,
|
|
664
|
+
reasoning: reasoningParts,
|
|
665
|
+
responseMetadata,
|
|
666
|
+
warnings,
|
|
667
|
+
} = raw;
|
|
572
668
|
const reasoningText = reasoningParts.map(r => r.text).join('') || undefined;
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
669
|
+
const validToolCallsByIndex = new Map(
|
|
670
|
+
toolCalls.flatMap((tc, index) =>
|
|
671
|
+
tc.invalid
|
|
672
|
+
? []
|
|
673
|
+
: [
|
|
674
|
+
[
|
|
675
|
+
index,
|
|
676
|
+
{
|
|
677
|
+
type: 'tool-call' as const,
|
|
678
|
+
toolCallId: tc.toolCallId,
|
|
679
|
+
toolName: tc.toolName,
|
|
680
|
+
input: tc.input,
|
|
681
|
+
...(tc.providerExecuted != null
|
|
682
|
+
? { providerExecuted: tc.providerExecuted }
|
|
683
|
+
: {}),
|
|
684
|
+
...(tc.title != null ? { title: tc.title } : {}),
|
|
685
|
+
...(tc.toolMetadata != null
|
|
686
|
+
? { toolMetadata: tc.toolMetadata }
|
|
687
|
+
: {}),
|
|
688
|
+
...(tc.dynamic ? { dynamic: true as const } : {}),
|
|
689
|
+
...(tc.providerExecuted ? { providerExecuted: true } : {}),
|
|
690
|
+
...(tc.providerMetadata != null
|
|
691
|
+
? { providerMetadata: tc.providerMetadata }
|
|
692
|
+
: {}),
|
|
693
|
+
},
|
|
694
|
+
] as const,
|
|
695
|
+
],
|
|
696
|
+
),
|
|
697
|
+
);
|
|
698
|
+
const validToolCalls = [...validToolCallsByIndex.values()];
|
|
699
|
+
const content: StepResult<ToolSet, any>['content'] = [];
|
|
700
|
+
const files: StepResult<ToolSet, any>['files'] = [];
|
|
701
|
+
const sources: StepResult<ToolSet, any>['sources'] = [];
|
|
702
|
+
let text = '';
|
|
703
|
+
|
|
704
|
+
for (const part of rawContent) {
|
|
705
|
+
switch (part.type) {
|
|
706
|
+
case 'text':
|
|
707
|
+
text += part.text;
|
|
708
|
+
content.push({
|
|
709
|
+
type: 'text',
|
|
710
|
+
text: part.text,
|
|
711
|
+
...(part.providerMetadata != null
|
|
712
|
+
? { providerMetadata: part.providerMetadata }
|
|
713
|
+
: {}),
|
|
714
|
+
});
|
|
715
|
+
break;
|
|
716
|
+
case 'file': {
|
|
717
|
+
const file = new DefaultGeneratedFile({
|
|
718
|
+
data: part.data,
|
|
719
|
+
mediaType: part.mediaType,
|
|
720
|
+
providerMetadata: part.providerMetadata,
|
|
721
|
+
});
|
|
722
|
+
files.push(file);
|
|
723
|
+
content.push({
|
|
724
|
+
type: 'file',
|
|
725
|
+
file,
|
|
726
|
+
...(part.providerMetadata != null
|
|
727
|
+
? { providerMetadata: part.providerMetadata }
|
|
728
|
+
: {}),
|
|
729
|
+
});
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
case 'source':
|
|
733
|
+
sources.push(part);
|
|
734
|
+
content.push(part);
|
|
735
|
+
break;
|
|
736
|
+
case 'tool-call': {
|
|
737
|
+
const toolCall = validToolCallsByIndex.get(part.toolCallIndex);
|
|
738
|
+
if (toolCall != null) {
|
|
739
|
+
content.push(toolCall);
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
583
745
|
|
|
584
746
|
return {
|
|
585
747
|
callId: 'workflow-agent',
|
|
@@ -592,20 +754,17 @@ function buildStepResult(
|
|
|
592
754
|
metadata: undefined,
|
|
593
755
|
runtimeContext: opts.runtimeContext ?? {},
|
|
594
756
|
toolsContext: opts.toolsContext ?? {},
|
|
595
|
-
content
|
|
596
|
-
...(text ? [{ type: 'text' as const, text }] : []),
|
|
597
|
-
...validToolCalls,
|
|
598
|
-
],
|
|
757
|
+
content,
|
|
599
758
|
text,
|
|
600
759
|
reasoning: reasoningParts.map(r => ({
|
|
601
760
|
type: 'reasoning' as const,
|
|
602
761
|
text: r.text,
|
|
603
762
|
})),
|
|
604
763
|
reasoningText,
|
|
605
|
-
files
|
|
606
|
-
sources
|
|
764
|
+
files,
|
|
765
|
+
sources,
|
|
607
766
|
toolCalls: validToolCalls,
|
|
608
|
-
staticToolCalls:
|
|
767
|
+
staticToolCalls: validToolCalls.filter(tc => tc.dynamic !== true),
|
|
609
768
|
dynamicToolCalls: validToolCalls.filter(tc => tc.dynamic),
|
|
610
769
|
toolResults: [],
|
|
611
770
|
staticToolResults: [],
|
|
@@ -653,6 +812,69 @@ function buildStepResult(
|
|
|
653
812
|
} as StepResult<ToolSet, any>;
|
|
654
813
|
}
|
|
655
814
|
|
|
815
|
+
function getAssistantMessageContent(
|
|
816
|
+
step: StepResult<any, any>,
|
|
817
|
+
): Extract<LanguageModelV4Prompt[number], { role: 'assistant' }>['content'] {
|
|
818
|
+
const content: Extract<
|
|
819
|
+
LanguageModelV4Prompt[number],
|
|
820
|
+
{ role: 'assistant' }
|
|
821
|
+
>['content'] = [];
|
|
822
|
+
|
|
823
|
+
for (const part of step.content) {
|
|
824
|
+
switch (part.type) {
|
|
825
|
+
case 'text':
|
|
826
|
+
if (part.text.length > 0) {
|
|
827
|
+
content.push({ type: 'text', text: part.text });
|
|
828
|
+
}
|
|
829
|
+
break;
|
|
830
|
+
case 'file':
|
|
831
|
+
content.push({
|
|
832
|
+
type: 'file',
|
|
833
|
+
data: { type: 'data', data: part.file.base64 },
|
|
834
|
+
mediaType: part.file.mediaType,
|
|
835
|
+
...(part.providerMetadata != null
|
|
836
|
+
? {
|
|
837
|
+
providerOptions:
|
|
838
|
+
part.providerMetadata as SharedV4ProviderOptions,
|
|
839
|
+
}
|
|
840
|
+
: {}),
|
|
841
|
+
});
|
|
842
|
+
break;
|
|
843
|
+
case 'tool-call':
|
|
844
|
+
content.push(toAssistantToolCallContent(part));
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return content;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function toAssistantToolCallContent(toolCall: {
|
|
853
|
+
toolCallId: string;
|
|
854
|
+
toolName: string;
|
|
855
|
+
input: unknown;
|
|
856
|
+
providerExecuted?: boolean;
|
|
857
|
+
providerMetadata?: unknown;
|
|
858
|
+
}) {
|
|
859
|
+
const sanitizedMetadata = sanitizeProviderMetadataForToolCall(
|
|
860
|
+
toolCall.providerMetadata,
|
|
861
|
+
);
|
|
862
|
+
return {
|
|
863
|
+
type: 'tool-call' as const,
|
|
864
|
+
toolCallId: toolCall.toolCallId,
|
|
865
|
+
toolName: toolCall.toolName,
|
|
866
|
+
input: toolCall.input,
|
|
867
|
+
...(toolCall.providerExecuted != null
|
|
868
|
+
? { providerExecuted: toolCall.providerExecuted }
|
|
869
|
+
: {}),
|
|
870
|
+
...(sanitizedMetadata != null
|
|
871
|
+
? {
|
|
872
|
+
providerOptions: sanitizedMetadata as SharedV4ProviderOptions,
|
|
873
|
+
}
|
|
874
|
+
: {}),
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
656
878
|
/**
|
|
657
879
|
* Strip OpenAI's itemId from providerMetadata (requires reasoning items we don't preserve).
|
|
658
880
|
* Preserves all other provider metadata (e.g., Gemini's thoughtSignature).
|