@ai-sdk/workflow 2.0.71 → 2.0.73
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 +19 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +241 -85
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/build-model-step-result.ts +6 -1
- package/src/do-generate-step.ts +15 -1
- package/src/do-stream-step.ts +28 -2
- package/src/model-call-iterator.ts +219 -4
- package/src/model-call-telemetry.ts +3 -3
- package/src/model-call.ts +14 -0
- package/src/serializable-schema.ts +18 -6
- package/src/test/concurrency-workflows.ts +66 -0
- package/src/test/telemetry-workflows.ts +30 -4
- package/src/to-ui-message-chunk.ts +3 -0
- package/src/workflow-agent.ts +175 -116
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/workflow",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.73",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WorkflowAgent for building AI agents with AI SDK",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@ai-sdk/provider": "4.0.27",
|
|
40
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
41
|
-
"ai": "7.0.
|
|
40
|
+
"@ai-sdk/provider-utils": "5.0.60",
|
|
41
|
+
"ai": "7.0.141",
|
|
42
42
|
"ajv": "^8.20.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@ai-sdk/otel": "1.0.
|
|
45
|
+
"@ai-sdk/otel": "1.0.141",
|
|
46
46
|
"@opentelemetry/api": "1.9.1",
|
|
47
47
|
"@opentelemetry/sdk-trace-base": "2.7.1",
|
|
48
48
|
"@types/node": "22.19.19",
|
|
@@ -42,7 +42,12 @@ export async function buildModelStepResult(
|
|
|
42
42
|
break;
|
|
43
43
|
case 'tool-call': {
|
|
44
44
|
const call = toolCalls[part.toolCallIndex];
|
|
45
|
-
|
|
45
|
+
const { inputSchemaInput: _inputSchemaInput, ...publicCall } = call;
|
|
46
|
+
providerContent.push({
|
|
47
|
+
...publicCall,
|
|
48
|
+
input: '',
|
|
49
|
+
type: 'tool-call',
|
|
50
|
+
});
|
|
46
51
|
break;
|
|
47
52
|
}
|
|
48
53
|
case 'provider-tool-result':
|
package/src/do-generate-step.ts
CHANGED
|
@@ -162,6 +162,17 @@ async function generateModelCall(
|
|
|
162
162
|
performance: { responseTimeMs },
|
|
163
163
|
});
|
|
164
164
|
abortSignal?.throwIfAborted();
|
|
165
|
+
const repairedToolCallIds = new Set<string>();
|
|
166
|
+
const repairToolCall = options.repairToolCall;
|
|
167
|
+
const trackedRepairToolCall = repairToolCall
|
|
168
|
+
? async (repairOptions: Parameters<typeof repairToolCall>[0]) => {
|
|
169
|
+
const repaired = await repairToolCall(repairOptions);
|
|
170
|
+
if (repaired != null) {
|
|
171
|
+
repairedToolCallIds.add(repairOptions.toolCall.toolCallId);
|
|
172
|
+
}
|
|
173
|
+
return repaired;
|
|
174
|
+
}
|
|
175
|
+
: undefined;
|
|
165
176
|
const toolCalls = await Promise.all(
|
|
166
177
|
response.content
|
|
167
178
|
.filter(part => part.type === 'tool-call')
|
|
@@ -169,7 +180,9 @@ async function generateModelCall(
|
|
|
169
180
|
parseToolCall({
|
|
170
181
|
toolCall,
|
|
171
182
|
tools,
|
|
172
|
-
repairToolCall:
|
|
183
|
+
repairToolCall: trackedRepairToolCall as Parameters<
|
|
184
|
+
typeof parseToolCall
|
|
185
|
+
>[0]['repairToolCall'],
|
|
173
186
|
instructions:
|
|
174
187
|
modelPrompt
|
|
175
188
|
.filter(message => message.role === 'system')
|
|
@@ -269,6 +282,7 @@ async function generateModelCall(
|
|
|
269
282
|
},
|
|
270
283
|
providerExecutedToolResults,
|
|
271
284
|
toolInputLifecycleEvents,
|
|
285
|
+
repairedToolCallIds: [...repairedToolCallIds],
|
|
272
286
|
};
|
|
273
287
|
}
|
|
274
288
|
|
package/src/do-stream-step.ts
CHANGED
|
@@ -116,6 +116,17 @@ async function streamModelStep(
|
|
|
116
116
|
// Tools are serialized before crossing the step boundary because zod schemas
|
|
117
117
|
// contain functions that can't be serialized by the workflow runtime.
|
|
118
118
|
const toolInputLifecycleEvents: ToolInputLifecycleEvent[] = [];
|
|
119
|
+
const repairedToolCallIds = new Set<string>();
|
|
120
|
+
const repairToolCall = options?.repairToolCall;
|
|
121
|
+
const trackedRepairToolCall = repairToolCall
|
|
122
|
+
? async (repairOptions: Parameters<typeof repairToolCall>[0]) => {
|
|
123
|
+
const repaired = await repairToolCall(repairOptions);
|
|
124
|
+
if (repaired != null) {
|
|
125
|
+
repairedToolCallIds.add(repairOptions.toolCall.toolCallId);
|
|
126
|
+
}
|
|
127
|
+
return repaired;
|
|
128
|
+
}
|
|
129
|
+
: undefined;
|
|
119
130
|
const tools = serializedTools
|
|
120
131
|
? resolveSerializableTools(serializedTools)
|
|
121
132
|
: undefined;
|
|
@@ -196,7 +207,7 @@ async function streamModelStep(
|
|
|
196
207
|
frequencyPenalty: options?.frequencyPenalty,
|
|
197
208
|
stopSequences: options?.stopSequences,
|
|
198
209
|
seed: options?.seed,
|
|
199
|
-
repairToolCall:
|
|
210
|
+
repairToolCall: trackedRepairToolCall,
|
|
200
211
|
download: options?.download,
|
|
201
212
|
...languageModelCallTelemetry,
|
|
202
213
|
}),
|
|
@@ -449,7 +460,21 @@ async function streamModelStep(
|
|
|
449
460
|
toolCallId: part.toolCall.toolCallId,
|
|
450
461
|
...(part.signature != null ? { signature: part.signature } : {}),
|
|
451
462
|
});
|
|
452
|
-
} else
|
|
463
|
+
} else if (
|
|
464
|
+
// The original schema must validate and normalize these calls outside
|
|
465
|
+
// the step before they become available to UI stream consumers. The
|
|
466
|
+
// matching `tool-error` for an invalid call (emitted by
|
|
467
|
+
// streamModelCall immediately after the `tool-call`) is deferred as
|
|
468
|
+
// well, so `writeDeferredToolCalls` stays the sole emitter.
|
|
469
|
+
// Otherwise the UI stream would receive a duplicate `tool-error`
|
|
470
|
+
// chunk, and the one written here would arrive before its deferred
|
|
471
|
+
// `tool-call`.
|
|
472
|
+
!(
|
|
473
|
+
(part.type === 'tool-call' ||
|
|
474
|
+
(part.type === 'tool-error' && !part.providerExecuted)) &&
|
|
475
|
+
serializedTools?.[part.toolName]?.hasOwnValidator
|
|
476
|
+
)
|
|
477
|
+
) {
|
|
453
478
|
await writer.write(part);
|
|
454
479
|
}
|
|
455
480
|
}
|
|
@@ -500,6 +525,7 @@ async function streamModelStep(
|
|
|
500
525
|
},
|
|
501
526
|
providerExecutedToolResults,
|
|
502
527
|
toolInputLifecycleEvents,
|
|
528
|
+
repairedToolCallIds: [...repairedToolCallIds],
|
|
503
529
|
...(hasTerminalError ? { terminalError } : {}),
|
|
504
530
|
};
|
|
505
531
|
}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
LanguageModelV4CallOptions,
|
|
3
3
|
LanguageModelV4Prompt,
|
|
4
|
+
LanguageModelV4ToolCall,
|
|
4
5
|
LanguageModelV4ToolResultPart,
|
|
5
6
|
SharedV4ProviderOptions,
|
|
6
7
|
} from '@ai-sdk/provider';
|
|
7
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
asSchema,
|
|
10
|
+
getErrorMessage,
|
|
11
|
+
safeParseJSON,
|
|
12
|
+
type Context,
|
|
13
|
+
} from '@ai-sdk/provider-utils';
|
|
8
14
|
import {
|
|
9
15
|
experimental_filterActiveTools as filterActiveTools,
|
|
16
|
+
InvalidToolInputError,
|
|
17
|
+
isDeepEqualData,
|
|
10
18
|
type ActiveTools,
|
|
11
19
|
type Experimental_SandboxSession as SandboxSession,
|
|
12
20
|
type Instructions,
|
|
@@ -20,6 +28,8 @@ import {
|
|
|
20
28
|
import {
|
|
21
29
|
createRestrictedTelemetryDispatcher,
|
|
22
30
|
createToolSearchState,
|
|
31
|
+
filterIncludedContext,
|
|
32
|
+
parseToolCall,
|
|
23
33
|
} from 'ai/internal';
|
|
24
34
|
import { buildModelStepResult } from './build-model-step-result.js';
|
|
25
35
|
import { doGenerateStep } from './do-generate-step.js';
|
|
@@ -39,7 +49,11 @@ import {
|
|
|
39
49
|
} from './add-tool-results-to-conversation.js';
|
|
40
50
|
import { toModelResponseMessages } from './to-model-response-messages.js';
|
|
41
51
|
import { resolveToolContext } from './resolve-tool-context.js';
|
|
42
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
resolveSerializableTools,
|
|
54
|
+
serializeToolSet,
|
|
55
|
+
type SerializableToolDef,
|
|
56
|
+
} from './serializable-schema.js';
|
|
43
57
|
import type {
|
|
44
58
|
GenerationSettings,
|
|
45
59
|
PrepareStepCallback,
|
|
@@ -392,6 +406,12 @@ export async function* modelCallIterator({
|
|
|
392
406
|
recordInputs: telemetry?.recordInputs,
|
|
393
407
|
recordOutputs: telemetry?.recordOutputs,
|
|
394
408
|
stepNumber,
|
|
409
|
+
includeRuntimeContext: telemetry?.includeRuntimeContext,
|
|
410
|
+
// Exclude unselected fields before serializing the step input.
|
|
411
|
+
runtimeContext: filterIncludedContext({
|
|
412
|
+
context: currentRuntimeContext,
|
|
413
|
+
includeContext: telemetry?.includeRuntimeContext,
|
|
414
|
+
}),
|
|
395
415
|
}
|
|
396
416
|
: undefined,
|
|
397
417
|
};
|
|
@@ -423,12 +443,41 @@ export async function* modelCallIterator({
|
|
|
423
443
|
}
|
|
424
444
|
|
|
425
445
|
const {
|
|
426
|
-
toolCalls,
|
|
446
|
+
toolCalls: serializedToolCalls,
|
|
427
447
|
finish,
|
|
428
448
|
raw,
|
|
429
449
|
providerExecutedToolResults,
|
|
430
450
|
toolInputLifecycleEvents,
|
|
451
|
+
repairedToolCallIds,
|
|
431
452
|
} = modelCallResult;
|
|
453
|
+
const toolCalls = await validateToolCallInputs({
|
|
454
|
+
toolCalls: serializedToolCalls,
|
|
455
|
+
tools: effectiveTools,
|
|
456
|
+
serializedTools,
|
|
457
|
+
repairToolCall,
|
|
458
|
+
repairedToolCallIds: new Set(repairedToolCallIds),
|
|
459
|
+
instructions:
|
|
460
|
+
conversationPrompt
|
|
461
|
+
.filter(message => message.role === 'system')
|
|
462
|
+
.map(message => message.content)
|
|
463
|
+
.join('\n') || initialInstructions,
|
|
464
|
+
messages: stepInputMessages.filter(
|
|
465
|
+
message => message.role !== 'system',
|
|
466
|
+
),
|
|
467
|
+
abortSignal: currentGenerationSettings.abortSignal,
|
|
468
|
+
});
|
|
469
|
+
if (mode === 'stream' && writable != null) {
|
|
470
|
+
// Replace the tool-call chunk deferred inside the model step with the
|
|
471
|
+
// original schema's validated, normalized, or repaired result.
|
|
472
|
+
await writeDeferredToolCalls(
|
|
473
|
+
writable,
|
|
474
|
+
toolCalls.filter(
|
|
475
|
+
(_, index) =>
|
|
476
|
+
serializedTools[serializedToolCalls[index].toolName]
|
|
477
|
+
?.hasOwnValidator,
|
|
478
|
+
),
|
|
479
|
+
);
|
|
480
|
+
}
|
|
432
481
|
await invokeToolInputLifecycleCallbacks({
|
|
433
482
|
events: toolInputLifecycleEvents ?? [],
|
|
434
483
|
toolCalls,
|
|
@@ -664,6 +713,172 @@ export async function* modelCallIterator({
|
|
|
664
713
|
return conversationPrompt;
|
|
665
714
|
}
|
|
666
715
|
|
|
716
|
+
async function validateToolCallInputs({
|
|
717
|
+
toolCalls,
|
|
718
|
+
tools,
|
|
719
|
+
serializedTools,
|
|
720
|
+
repairToolCall,
|
|
721
|
+
repairedToolCallIds,
|
|
722
|
+
instructions,
|
|
723
|
+
messages,
|
|
724
|
+
abortSignal,
|
|
725
|
+
}: {
|
|
726
|
+
toolCalls: ParsedToolCall[];
|
|
727
|
+
tools: ToolSet;
|
|
728
|
+
serializedTools: Record<string, SerializableToolDef>;
|
|
729
|
+
repairToolCall?: ToolCallRepairFunction<ToolSet>;
|
|
730
|
+
repairedToolCallIds: Set<string>;
|
|
731
|
+
instructions?: Instructions;
|
|
732
|
+
messages: ModelMessage[];
|
|
733
|
+
abortSignal?: AbortSignal;
|
|
734
|
+
}): Promise<ParsedToolCall[]> {
|
|
735
|
+
return Promise.all(
|
|
736
|
+
toolCalls.map(async toolCall => {
|
|
737
|
+
if (toolCall.invalid || toolCall.providerExecuted) {
|
|
738
|
+
return toolCall;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const tool = tools[toolCall.toolName];
|
|
742
|
+
if (tool == null) {
|
|
743
|
+
return toolCall;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const inputSchema = asSchema(tool.inputSchema);
|
|
747
|
+
if (inputSchema.validate == null) {
|
|
748
|
+
return toolCall;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
let repairedInput: { value: unknown } | undefined;
|
|
752
|
+
const parsedToolCall = await parseToolCall({
|
|
753
|
+
toolCall: {
|
|
754
|
+
type: 'tool-call',
|
|
755
|
+
toolCallId: toolCall.toolCallId,
|
|
756
|
+
toolName: toolCall.toolName,
|
|
757
|
+
input:
|
|
758
|
+
JSON.stringify(toolCall.input) ??
|
|
759
|
+
(toolCall.input == null ? '' : String(toolCall.input)),
|
|
760
|
+
providerExecuted: toolCall.providerExecuted,
|
|
761
|
+
providerMetadata: toolCall.providerMetadata,
|
|
762
|
+
} satisfies LanguageModelV4ToolCall,
|
|
763
|
+
tools,
|
|
764
|
+
// A call that was already repaired inside the model step is not
|
|
765
|
+
// repaired again, matching parseToolCall's single repair attempt.
|
|
766
|
+
repairToolCall:
|
|
767
|
+
repairToolCall == null || repairedToolCallIds.has(toolCall.toolCallId)
|
|
768
|
+
? undefined
|
|
769
|
+
: async options => {
|
|
770
|
+
const repairedToolCall = await repairToolCallInStep({
|
|
771
|
+
repairToolCall,
|
|
772
|
+
serializedTools,
|
|
773
|
+
toolCall: options.toolCall,
|
|
774
|
+
error: {
|
|
775
|
+
toolName: options.toolCall.toolName,
|
|
776
|
+
toolInput: options.toolCall.input,
|
|
777
|
+
message: getErrorMessage(options.error),
|
|
778
|
+
},
|
|
779
|
+
instructions,
|
|
780
|
+
messages: options.messages,
|
|
781
|
+
});
|
|
782
|
+
if (repairedToolCall != null) {
|
|
783
|
+
const parsedRepairedInput =
|
|
784
|
+
repairedToolCall.input.trim() === ''
|
|
785
|
+
? { success: true as const, value: {} }
|
|
786
|
+
: await safeParseJSON({
|
|
787
|
+
text: repairedToolCall.input,
|
|
788
|
+
});
|
|
789
|
+
if (parsedRepairedInput.success) {
|
|
790
|
+
repairedInput = { value: parsedRepairedInput.value };
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return repairedToolCall;
|
|
794
|
+
},
|
|
795
|
+
instructions,
|
|
796
|
+
messages,
|
|
797
|
+
abortSignal,
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
const inputSchemaInput = repairedInput ?? { value: toolCall.input };
|
|
801
|
+
|
|
802
|
+
return {
|
|
803
|
+
...toolCall,
|
|
804
|
+
...parsedToolCall,
|
|
805
|
+
...(!parsedToolCall.invalid &&
|
|
806
|
+
!isDeepEqualData(inputSchemaInput.value, parsedToolCall.input)
|
|
807
|
+
? { inputSchemaInput: inputSchemaInput.value }
|
|
808
|
+
: {}),
|
|
809
|
+
};
|
|
810
|
+
}),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Runs `repairToolCall` inside a step. The callback may be a `'use step'`
|
|
816
|
+
* function, so its arguments must be serializable: the tools and the
|
|
817
|
+
* `inputSchema` accessor are rebuilt here instead of crossing the boundary
|
|
818
|
+
* from the workflow body.
|
|
819
|
+
*/
|
|
820
|
+
async function repairToolCallInStep({
|
|
821
|
+
repairToolCall,
|
|
822
|
+
serializedTools,
|
|
823
|
+
toolCall,
|
|
824
|
+
error,
|
|
825
|
+
instructions,
|
|
826
|
+
messages,
|
|
827
|
+
}: {
|
|
828
|
+
repairToolCall: ToolCallRepairFunction<ToolSet>;
|
|
829
|
+
serializedTools: Record<string, SerializableToolDef>;
|
|
830
|
+
toolCall: LanguageModelV4ToolCall;
|
|
831
|
+
error: { toolName: string; toolInput: string; message: string };
|
|
832
|
+
instructions?: Instructions;
|
|
833
|
+
messages: ModelMessage[];
|
|
834
|
+
}): Promise<LanguageModelV4ToolCall | null> {
|
|
835
|
+
'use step';
|
|
836
|
+
const tools = resolveSerializableTools(serializedTools);
|
|
837
|
+
return repairToolCall({
|
|
838
|
+
toolCall,
|
|
839
|
+
tools,
|
|
840
|
+
inputSchema: async ({ toolName }) =>
|
|
841
|
+
asSchema(tools[toolName]?.inputSchema).jsonSchema,
|
|
842
|
+
instructions,
|
|
843
|
+
system: instructions,
|
|
844
|
+
messages,
|
|
845
|
+
// Errors are passed as plain data and rebuilt so that
|
|
846
|
+
// `InvalidToolInputError.isInstance` keeps working in the callback.
|
|
847
|
+
error: new InvalidToolInputError({
|
|
848
|
+
toolName: error.toolName,
|
|
849
|
+
toolInput: error.toolInput,
|
|
850
|
+
cause: new Error(error.message),
|
|
851
|
+
message: error.message,
|
|
852
|
+
}),
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function writeDeferredToolCalls(
|
|
857
|
+
writable: WritableStream<ModelCallStreamPart<ToolSet>>,
|
|
858
|
+
toolCalls: ParsedToolCall[],
|
|
859
|
+
) {
|
|
860
|
+
'use step';
|
|
861
|
+
const writer = writable.getWriter();
|
|
862
|
+
try {
|
|
863
|
+
for (const toolCall of toolCalls) {
|
|
864
|
+
await writer.write(toolCall as ModelCallStreamPart<ToolSet>);
|
|
865
|
+
// Match streamModelCall, which follows an invalid call with a tool-error.
|
|
866
|
+
if (toolCall.invalid && !toolCall.providerExecuted) {
|
|
867
|
+
await writer.write({
|
|
868
|
+
type: 'tool-error',
|
|
869
|
+
toolCallId: toolCall.toolCallId,
|
|
870
|
+
toolName: toolCall.toolName,
|
|
871
|
+
input: toolCall.input,
|
|
872
|
+
error: getErrorMessage(toolCall.error),
|
|
873
|
+
dynamic: true,
|
|
874
|
+
} as ModelCallStreamPart<ToolSet>);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
} finally {
|
|
878
|
+
writer.releaseLock();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
667
882
|
async function invokeToolInputLifecycleCallbacks({
|
|
668
883
|
events,
|
|
669
884
|
toolCalls,
|
|
@@ -734,7 +949,7 @@ async function invokeToolInputLifecycleCallbacks({
|
|
|
734
949
|
break;
|
|
735
950
|
case 'available': {
|
|
736
951
|
const toolCall = toolCallsById.get(toolCallId);
|
|
737
|
-
if (toolCall == null) {
|
|
952
|
+
if (toolCall == null || toolCall.invalid) {
|
|
738
953
|
break;
|
|
739
954
|
}
|
|
740
955
|
await tool.onInputAvailable?.({
|
|
@@ -104,7 +104,7 @@ export async function runModelCallWithTelemetry({
|
|
|
104
104
|
reasoning: options?.reasoning,
|
|
105
105
|
providerOptions: options?.providerOptions,
|
|
106
106
|
output: undefined,
|
|
107
|
-
runtimeContext: {},
|
|
107
|
+
runtimeContext: telemetry.runtimeContext ?? {},
|
|
108
108
|
toolsContext: {},
|
|
109
109
|
} as never);
|
|
110
110
|
await dispatcher.onStepStart?.({
|
|
@@ -120,7 +120,7 @@ export async function runModelCallWithTelemetry({
|
|
|
120
120
|
steps: [],
|
|
121
121
|
providerOptions: options?.providerOptions,
|
|
122
122
|
output: undefined,
|
|
123
|
-
runtimeContext: {},
|
|
123
|
+
runtimeContext: telemetry.runtimeContext ?? {},
|
|
124
124
|
toolsContext: {},
|
|
125
125
|
} as never);
|
|
126
126
|
|
|
@@ -157,7 +157,7 @@ export async function runModelCallWithTelemetry({
|
|
|
157
157
|
callId,
|
|
158
158
|
tools,
|
|
159
159
|
stepNumber: telemetry.stepNumber,
|
|
160
|
-
runtimeContext: {},
|
|
160
|
+
runtimeContext: telemetry.runtimeContext ?? {},
|
|
161
161
|
toolsContext: {},
|
|
162
162
|
},
|
|
163
163
|
);
|
package/src/model-call.ts
CHANGED
|
@@ -5,12 +5,14 @@ import type {
|
|
|
5
5
|
LanguageModelV4Source,
|
|
6
6
|
SharedV4ProviderMetadata,
|
|
7
7
|
} from '@ai-sdk/provider';
|
|
8
|
+
import type { Context } from '@ai-sdk/provider-utils';
|
|
8
9
|
import type {
|
|
9
10
|
Experimental_LanguageModelStreamPart,
|
|
10
11
|
FinishReason,
|
|
11
12
|
LanguageModelUsage,
|
|
12
13
|
StepResultPerformance,
|
|
13
14
|
StopCondition,
|
|
15
|
+
TelemetryOptions,
|
|
14
16
|
ToolCallRepairFunction,
|
|
15
17
|
ToolChoice,
|
|
16
18
|
ToolSet,
|
|
@@ -28,6 +30,8 @@ export type ModelCallStreamPart<TTools extends ToolSet = ToolSet> =
|
|
|
28
30
|
approvalId: string;
|
|
29
31
|
toolCallId: string;
|
|
30
32
|
signature?: string;
|
|
33
|
+
/** Raw input before the original tool schema transformed it. */
|
|
34
|
+
inputSchemaInput?: unknown;
|
|
31
35
|
}
|
|
32
36
|
| { type: 'reset-step' };
|
|
33
37
|
|
|
@@ -91,6 +95,9 @@ export interface ModelCallTelemetry {
|
|
|
91
95
|
recordInputs?: boolean;
|
|
92
96
|
recordOutputs?: boolean;
|
|
93
97
|
stepNumber: number;
|
|
98
|
+
includeRuntimeContext?: TelemetryOptions['includeRuntimeContext'];
|
|
99
|
+
/** Runtime context filtered before crossing the durable step boundary. */
|
|
100
|
+
runtimeContext?: Context;
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
/**
|
|
@@ -108,6 +115,8 @@ export interface ParsedToolCall {
|
|
|
108
115
|
dynamic?: boolean;
|
|
109
116
|
invalid?: boolean;
|
|
110
117
|
error?: unknown;
|
|
118
|
+
/** Raw input before the original tool schema validated and transformed it. */
|
|
119
|
+
inputSchemaInput?: unknown;
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
/**
|
|
@@ -219,6 +228,11 @@ export type ModelCallResult =
|
|
|
219
228
|
* tool input lifecycle callback replay was added.
|
|
220
229
|
*/
|
|
221
230
|
toolInputLifecycleEvents?: ToolInputLifecycleEvent[];
|
|
231
|
+
/**
|
|
232
|
+
* Tool call ids that `repairToolCall` already repaired inside the step,
|
|
233
|
+
* so deferred validation does not repair them a second time.
|
|
234
|
+
*/
|
|
235
|
+
repairedToolCallIds?: string[];
|
|
222
236
|
/** Present when the model stream emitted an error part or generation failed. */
|
|
223
237
|
terminalError?: unknown;
|
|
224
238
|
};
|
|
@@ -36,6 +36,8 @@ export type SerializableToolDef = {
|
|
|
36
36
|
inputExamples?: Array<{ input: unknown }>;
|
|
37
37
|
/** Provider-specific options attached to the tool definition. */
|
|
38
38
|
providerOptions?: Tool['providerOptions'];
|
|
39
|
+
/** Whether the original input schema has its own runtime validator. */
|
|
40
|
+
hasOwnValidator?: boolean;
|
|
39
41
|
/** Input lifecycle callbacks that must be invoked outside the step. */
|
|
40
42
|
hasOnInputStart?: boolean;
|
|
41
43
|
hasOnInputDelta?: boolean;
|
|
@@ -69,6 +71,7 @@ export function serializeToolSet<TOOLS extends ToolSet>(
|
|
|
69
71
|
): Record<string, SerializableToolDef> {
|
|
70
72
|
return Object.fromEntries(
|
|
71
73
|
Object.entries(tools).map(([name, t]) => {
|
|
74
|
+
const inputSchema = asSchema(t.inputSchema);
|
|
72
75
|
const def: SerializableToolDef = {
|
|
73
76
|
title: t.title,
|
|
74
77
|
metadata: t.metadata,
|
|
@@ -78,12 +81,16 @@ export function serializeToolSet<TOOLS extends ToolSet>(
|
|
|
78
81
|
toolsContext,
|
|
79
82
|
experimental_sandbox: sandbox,
|
|
80
83
|
}),
|
|
81
|
-
inputSchema:
|
|
84
|
+
inputSchema: inputSchema.jsonSchema as JSONSchema7,
|
|
82
85
|
strict: t.strict,
|
|
83
86
|
inputExamples: t.inputExamples,
|
|
84
87
|
providerOptions: t.providerOptions,
|
|
85
88
|
};
|
|
86
89
|
|
|
90
|
+
if (inputSchema.validate != null) {
|
|
91
|
+
def.hasOwnValidator = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
if (t.type === 'dynamic') {
|
|
88
95
|
def.type = 'dynamic';
|
|
89
96
|
}
|
|
@@ -159,9 +166,9 @@ function formatAjvError(error: ErrorObject): string {
|
|
|
159
166
|
/**
|
|
160
167
|
* Reconstructs tool objects from serializable tool definitions inside a step.
|
|
161
168
|
*
|
|
162
|
-
* Wraps each tool's JSON Schema with `jsonSchema()
|
|
163
|
-
*
|
|
164
|
-
*
|
|
169
|
+
* Wraps each tool's JSON Schema with `jsonSchema()`. Tools without their own
|
|
170
|
+
* validator are validated with Ajv. Tools with their own validator are
|
|
171
|
+
* validated after the workflow step, where the original schema is available.
|
|
165
172
|
*/
|
|
166
173
|
export function resolveSerializableTools(
|
|
167
174
|
tools: Record<string, SerializableToolDef>,
|
|
@@ -198,6 +205,11 @@ export function resolveSerializableTools(
|
|
|
198
205
|
});
|
|
199
206
|
};
|
|
200
207
|
|
|
208
|
+
const createInputSchema = (tool: SerializableToolDef) =>
|
|
209
|
+
tool.hasOwnValidator
|
|
210
|
+
? jsonSchema(tool.inputSchema)
|
|
211
|
+
: createValidatedInputSchema(tool.inputSchema);
|
|
212
|
+
|
|
201
213
|
return Object.fromEntries(
|
|
202
214
|
Object.entries(tools).map(([name, t]) => {
|
|
203
215
|
// Provider tools are executed server-side — pass them through
|
|
@@ -235,7 +247,7 @@ export function resolveSerializableTools(
|
|
|
235
247
|
description: t.description,
|
|
236
248
|
inputExamples: t.inputExamples,
|
|
237
249
|
providerOptions: t.providerOptions,
|
|
238
|
-
inputSchema:
|
|
250
|
+
inputSchema: createInputSchema(t),
|
|
239
251
|
}),
|
|
240
252
|
];
|
|
241
253
|
}
|
|
@@ -247,7 +259,7 @@ export function resolveSerializableTools(
|
|
|
247
259
|
strict: t.strict,
|
|
248
260
|
inputExamples: t.inputExamples,
|
|
249
261
|
providerOptions: t.providerOptions,
|
|
250
|
-
inputSchema:
|
|
262
|
+
inputSchema: createInputSchema(t),
|
|
251
263
|
};
|
|
252
264
|
|
|
253
265
|
return [
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { tool, type ModelMessage } from 'ai';
|
|
2
|
+
import { createHook } from 'workflow';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
import { mockTextModel } from '../providers/mock.js';
|
|
5
|
+
import { WorkflowAgent } from '../workflow-agent.js';
|
|
6
|
+
|
|
7
|
+
async function completeAction(id: string) {
|
|
8
|
+
'use step';
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function resumeSequentialApprovals(mode: 'generate' | 'stream') {
|
|
13
|
+
'use workflow';
|
|
14
|
+
const events: string[] = [];
|
|
15
|
+
const agent = new WorkflowAgent({
|
|
16
|
+
model: mockTextModel('Done.'),
|
|
17
|
+
toolCallConcurrency: 1,
|
|
18
|
+
tools: {
|
|
19
|
+
action: tool({
|
|
20
|
+
inputSchema: z.object({ id: z.string() }),
|
|
21
|
+
needsApproval: true,
|
|
22
|
+
execute: async ({ id }) => {
|
|
23
|
+
events.push(`start:${id}`);
|
|
24
|
+
if (id === 'first') {
|
|
25
|
+
using hook = createHook<string>();
|
|
26
|
+
await hook;
|
|
27
|
+
}
|
|
28
|
+
const result = await completeAction(id);
|
|
29
|
+
events.push(`end:${id}`);
|
|
30
|
+
return result;
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const ids = ['first', 'second'];
|
|
36
|
+
const messages: ModelMessage[] = [
|
|
37
|
+
{ role: 'user', content: 'Run the tools.' },
|
|
38
|
+
{
|
|
39
|
+
role: 'assistant',
|
|
40
|
+
content: ids.flatMap(id => [
|
|
41
|
+
{
|
|
42
|
+
type: 'tool-call' as const,
|
|
43
|
+
toolCallId: id,
|
|
44
|
+
toolName: 'action',
|
|
45
|
+
input: { id },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'tool-approval-request' as const,
|
|
49
|
+
toolCallId: id,
|
|
50
|
+
approvalId: `approval-${id}`,
|
|
51
|
+
},
|
|
52
|
+
]),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
role: 'tool',
|
|
56
|
+
content: [...ids].reverse().map(id => ({
|
|
57
|
+
type: 'tool-approval-response' as const,
|
|
58
|
+
approvalId: `approval-${id}`,
|
|
59
|
+
approved: true,
|
|
60
|
+
})),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
if (mode === 'generate') await agent.generate({ messages });
|
|
64
|
+
else await agent.stream({ messages });
|
|
65
|
+
return events;
|
|
66
|
+
}
|
|
@@ -10,11 +10,24 @@ async function getGlobalTelemetryIntegrationCount(): Promise<number | null> {
|
|
|
10
10
|
return globalThis.AI_SDK_TELEMETRY_INTEGRATIONS?.length ?? null;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export async function agentStreamWithGlobalTelemetry(
|
|
13
|
+
export async function agentStreamWithGlobalTelemetry(
|
|
14
|
+
prompt: string,
|
|
15
|
+
includeTraceIdentity?: boolean,
|
|
16
|
+
) {
|
|
14
17
|
'use workflow';
|
|
15
18
|
const agent = new WorkflowAgent({
|
|
16
19
|
model: mockTextModel(`Echo: ${prompt}`),
|
|
17
|
-
telemetry: {
|
|
20
|
+
telemetry: {
|
|
21
|
+
functionId: 'telemetry-test',
|
|
22
|
+
includeRuntimeContext:
|
|
23
|
+
includeTraceIdentity == null
|
|
24
|
+
? undefined
|
|
25
|
+
: { traceIdentity: includeTraceIdentity },
|
|
26
|
+
},
|
|
27
|
+
runtimeContext: {
|
|
28
|
+
traceIdentity: { sessionId: 'chat_1', userId: 'user_1' },
|
|
29
|
+
secret: 'must not cross the model-step boundary',
|
|
30
|
+
},
|
|
18
31
|
});
|
|
19
32
|
const result = await agent.stream({
|
|
20
33
|
messages: [{ role: 'user', content: prompt }],
|
|
@@ -29,11 +42,24 @@ export async function agentStreamWithGlobalTelemetry(prompt: string) {
|
|
|
29
42
|
};
|
|
30
43
|
}
|
|
31
44
|
|
|
32
|
-
export async function agentGenerateWithGlobalTelemetry(
|
|
45
|
+
export async function agentGenerateWithGlobalTelemetry(
|
|
46
|
+
prompt: string,
|
|
47
|
+
includeTraceIdentity?: boolean,
|
|
48
|
+
) {
|
|
33
49
|
'use workflow';
|
|
34
50
|
const agent = new WorkflowAgent({
|
|
35
51
|
model: mockTextModel(`Echo: ${prompt}`),
|
|
36
|
-
telemetry: {
|
|
52
|
+
telemetry: {
|
|
53
|
+
functionId: 'telemetry-test',
|
|
54
|
+
includeRuntimeContext:
|
|
55
|
+
includeTraceIdentity == null
|
|
56
|
+
? undefined
|
|
57
|
+
: { traceIdentity: includeTraceIdentity },
|
|
58
|
+
},
|
|
59
|
+
runtimeContext: {
|
|
60
|
+
traceIdentity: { sessionId: 'chat_1', userId: 'user_1' },
|
|
61
|
+
secret: 'must not cross the model-step boundary',
|
|
62
|
+
},
|
|
37
63
|
});
|
|
38
64
|
const result = await agent.generate({
|
|
39
65
|
messages: [{ role: 'user', content: prompt }],
|