@ai-sdk/workflow 0.0.0-6b196531-20260710185421

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.
@@ -0,0 +1,661 @@
1
+ import type {
2
+ LanguageModelV4CallOptions,
3
+ LanguageModelV4Prompt,
4
+ LanguageModelV4ToolResultPart,
5
+ } from '@ai-sdk/provider';
6
+ import type { Context } from '@ai-sdk/provider-utils';
7
+ import {
8
+ experimental_filterActiveTools as filterActiveTools,
9
+ type Experimental_LanguageModelStreamPart as ModelCallStreamPart,
10
+ type Experimental_SandboxSession as SandboxSession,
11
+ type LanguageModel,
12
+ type LanguageModelUsage,
13
+ type ModelMessage,
14
+ type StepResult,
15
+ type ToolCallRepairFunction,
16
+ type ToolChoice,
17
+ type ToolSet,
18
+ } from 'ai';
19
+ import { createRestrictedTelemetryDispatcher } from 'ai/internal';
20
+ import {
21
+ type DoStreamStepRawResult,
22
+ doStreamStep,
23
+ type ModelStopCondition,
24
+ type ParsedToolCall,
25
+ type ProviderExecutedToolResult,
26
+ type StreamFinish,
27
+ } from './do-stream-step.js';
28
+ import { serializeToolSet } from './serializable-schema.js';
29
+ import type {
30
+ GenerationSettings,
31
+ PrepareStepCallback,
32
+ WorkflowAgentOnErrorCallback,
33
+ WorkflowAgentOnStepEndCallback,
34
+ WorkflowAgentOnStepFinishCallback,
35
+ TelemetryOptions,
36
+ WorkflowAgentOnStepStartCallback,
37
+ } from './workflow-agent.js';
38
+
39
+ // Re-export for consumers
40
+ export type { ProviderExecutedToolResult } from './do-stream-step.js';
41
+
42
+ /**
43
+ * The value yielded by the stream text iterator when tool calls are requested.
44
+ * Contains both the tool calls and the current conversation messages.
45
+ */
46
+ export interface StreamTextIteratorYieldValue {
47
+ /** The tool calls requested by the model (parsed with typed inputs) */
48
+ toolCalls: ParsedToolCall[];
49
+ /** The conversation messages up to (and including) the tool call request */
50
+ messages: LanguageModelV4Prompt;
51
+ /** The step result from the current step */
52
+ step?: StepResult<ToolSet, any>;
53
+ /** The current runtime context shared across the agent loop */
54
+ runtimeContext?: Context;
55
+ /** The current per-tool context, keyed by tool name */
56
+ toolsContext?: Record<string, Context | undefined>;
57
+ /** Provider-executed tool results (keyed by tool call ID) */
58
+ providerExecutedToolResults?: Map<string, ProviderExecutedToolResult>;
59
+ /** The sandbox selected for the current step. */
60
+ experimental_sandbox?: SandboxSession;
61
+ }
62
+
63
+ // This runs in the workflow context
64
+ export async function* streamTextIterator({
65
+ prompt,
66
+ tools = {},
67
+ writable,
68
+ model,
69
+ stopConditions,
70
+ onStepEnd,
71
+ onStepFinish,
72
+ onStepStart,
73
+ onError,
74
+ prepareStep,
75
+ generationSettings,
76
+ toolChoice,
77
+ runtimeContext,
78
+ toolsContext,
79
+ telemetry,
80
+ includeRawChunks = false,
81
+ repairToolCall,
82
+ responseFormat,
83
+ experimental_sandbox: sandbox,
84
+ }: {
85
+ prompt: LanguageModelV4Prompt;
86
+ tools: ToolSet;
87
+ writable?: WritableStream<ModelCallStreamPart<ToolSet>>;
88
+ model: LanguageModel;
89
+ stopConditions?: ModelStopCondition[] | ModelStopCondition;
90
+ onStepEnd?: WorkflowAgentOnStepEndCallback<any>;
91
+ /** @deprecated Use `onStepEnd` instead. */
92
+ onStepFinish?: WorkflowAgentOnStepFinishCallback<any>;
93
+ onStepStart?: WorkflowAgentOnStepStartCallback;
94
+ onError?: WorkflowAgentOnErrorCallback;
95
+ prepareStep?: PrepareStepCallback<any>;
96
+ generationSettings?: GenerationSettings;
97
+ toolChoice?: ToolChoice<ToolSet>;
98
+ runtimeContext?: Context;
99
+ toolsContext?: Record<string, Context | undefined>;
100
+ telemetry?: TelemetryOptions<Context, ToolSet>;
101
+ includeRawChunks?: boolean;
102
+ repairToolCall?: ToolCallRepairFunction<ToolSet>;
103
+ responseFormat?: LanguageModelV4CallOptions['responseFormat'];
104
+ experimental_sandbox?: SandboxSession;
105
+ }): AsyncGenerator<
106
+ StreamTextIteratorYieldValue,
107
+ LanguageModelV4Prompt,
108
+ LanguageModelV4ToolResultPart[]
109
+ > {
110
+ let conversationPrompt = [...prompt]; // Create a mutable copy
111
+ let currentModel: LanguageModel = model;
112
+ let currentGenerationSettings = generationSettings ?? {};
113
+ let currentToolChoice = toolChoice;
114
+ let currentRuntimeContext: Context = runtimeContext ?? {};
115
+ let currentToolsContext: Record<string, Context | undefined> =
116
+ toolsContext ?? {};
117
+ let currentActiveTools: string[] | undefined;
118
+
119
+ const steps: StepResult<any, any>[] = [];
120
+ let done = false;
121
+ let _isFirstIteration = true;
122
+ let stepNumber = 0;
123
+ let lastStep: StepResult<any, any> | undefined;
124
+ let lastStepWasToolCalls = false;
125
+
126
+ // TODO(#12164): replace this AI-core telemetry bridge with a
127
+ // WorkflowAgent-specific typed dispatcher. `streamTextIterator` widens
128
+ // tools/runtime context and emits Workflow-shaped events that are only
129
+ // approximately compatible with generateText telemetry event types.
130
+ const telemetryDispatcher = createRestrictedTelemetryDispatcher<
131
+ any,
132
+ any,
133
+ any
134
+ >({
135
+ telemetry: telemetry as any,
136
+ includeRuntimeContext: telemetry?.includeRuntimeContext,
137
+ includeToolsContext: telemetry?.includeToolsContext,
138
+ }) as any;
139
+
140
+ while (!done) {
141
+ // Check for abort signal
142
+ if (currentGenerationSettings.abortSignal?.aborted) {
143
+ break;
144
+ }
145
+
146
+ let stepSandbox = sandbox;
147
+
148
+ // Call prepareStep callback before each step if provided
149
+ if (prepareStep) {
150
+ const prepareResult = await prepareStep({
151
+ model: currentModel,
152
+ stepNumber,
153
+ steps,
154
+ messages: conversationPrompt,
155
+ runtimeContext: currentRuntimeContext,
156
+ toolsContext: currentToolsContext as never,
157
+ experimental_sandbox: sandbox,
158
+ });
159
+
160
+ stepSandbox = prepareResult?.experimental_sandbox ?? sandbox;
161
+
162
+ // Apply any overrides from prepareStep
163
+ if (prepareResult?.model !== undefined) {
164
+ currentModel = prepareResult.model;
165
+ }
166
+ // Apply messages override BEFORE system so the system message
167
+ // isn't lost when messages replaces the prompt.
168
+ if (prepareResult?.messages !== undefined) {
169
+ conversationPrompt = [...prepareResult.messages];
170
+ }
171
+ if (prepareResult?.system !== undefined) {
172
+ // Update or prepend system message in the conversation prompt.
173
+ // Applied AFTER messages override so the system message isn't
174
+ // lost when messages replaces the prompt.
175
+ if (
176
+ conversationPrompt.length > 0 &&
177
+ conversationPrompt[0].role === 'system'
178
+ ) {
179
+ // Replace existing system message
180
+ conversationPrompt[0] = {
181
+ role: 'system',
182
+ content: prepareResult.system,
183
+ };
184
+ } else {
185
+ // Prepend new system message
186
+ conversationPrompt.unshift({
187
+ role: 'system',
188
+ content: prepareResult.system,
189
+ });
190
+ }
191
+ }
192
+ if (prepareResult?.runtimeContext !== undefined) {
193
+ currentRuntimeContext = prepareResult.runtimeContext;
194
+ }
195
+ if (prepareResult?.toolsContext !== undefined) {
196
+ currentToolsContext = prepareResult.toolsContext as Record<
197
+ string,
198
+ Context | undefined
199
+ >;
200
+ }
201
+ if (prepareResult?.activeTools !== undefined) {
202
+ currentActiveTools = prepareResult.activeTools;
203
+ }
204
+ // Apply generation settings overrides
205
+ if (prepareResult?.maxOutputTokens !== undefined) {
206
+ currentGenerationSettings = {
207
+ ...currentGenerationSettings,
208
+ maxOutputTokens: prepareResult.maxOutputTokens,
209
+ };
210
+ }
211
+ if (prepareResult?.temperature !== undefined) {
212
+ currentGenerationSettings = {
213
+ ...currentGenerationSettings,
214
+ temperature: prepareResult.temperature,
215
+ };
216
+ }
217
+ if (prepareResult?.topP !== undefined) {
218
+ currentGenerationSettings = {
219
+ ...currentGenerationSettings,
220
+ topP: prepareResult.topP,
221
+ };
222
+ }
223
+ if (prepareResult?.topK !== undefined) {
224
+ currentGenerationSettings = {
225
+ ...currentGenerationSettings,
226
+ topK: prepareResult.topK,
227
+ };
228
+ }
229
+ if (prepareResult?.presencePenalty !== undefined) {
230
+ currentGenerationSettings = {
231
+ ...currentGenerationSettings,
232
+ presencePenalty: prepareResult.presencePenalty,
233
+ };
234
+ }
235
+ if (prepareResult?.frequencyPenalty !== undefined) {
236
+ currentGenerationSettings = {
237
+ ...currentGenerationSettings,
238
+ frequencyPenalty: prepareResult.frequencyPenalty,
239
+ };
240
+ }
241
+ if (prepareResult?.stopSequences !== undefined) {
242
+ currentGenerationSettings = {
243
+ ...currentGenerationSettings,
244
+ stopSequences: prepareResult.stopSequences,
245
+ };
246
+ }
247
+ if (prepareResult?.seed !== undefined) {
248
+ currentGenerationSettings = {
249
+ ...currentGenerationSettings,
250
+ seed: prepareResult.seed,
251
+ };
252
+ }
253
+ if (prepareResult?.maxRetries !== undefined) {
254
+ currentGenerationSettings = {
255
+ ...currentGenerationSettings,
256
+ maxRetries: prepareResult.maxRetries,
257
+ };
258
+ }
259
+ if (prepareResult?.headers !== undefined) {
260
+ currentGenerationSettings = {
261
+ ...currentGenerationSettings,
262
+ headers: prepareResult.headers,
263
+ };
264
+ }
265
+ if (prepareResult?.reasoning !== undefined) {
266
+ currentGenerationSettings = {
267
+ ...currentGenerationSettings,
268
+ reasoning: prepareResult.reasoning,
269
+ };
270
+ }
271
+ if (prepareResult?.providerOptions !== undefined) {
272
+ currentGenerationSettings = {
273
+ ...currentGenerationSettings,
274
+ providerOptions: prepareResult.providerOptions,
275
+ };
276
+ }
277
+ if (prepareResult?.toolChoice !== undefined) {
278
+ currentToolChoice = prepareResult.toolChoice;
279
+ }
280
+ }
281
+
282
+ if (onStepStart) {
283
+ await onStepStart({
284
+ stepNumber,
285
+ model: currentModel,
286
+ messages: conversationPrompt as unknown as ModelMessage[],
287
+ steps: [...steps],
288
+ runtimeContext: currentRuntimeContext,
289
+ toolsContext: currentToolsContext as never,
290
+ });
291
+ }
292
+
293
+ const stepStartModelInfo = getModelInfo(currentModel);
294
+ await telemetryDispatcher.onStepStart?.({
295
+ callId: 'workflow-agent',
296
+ provider: stepStartModelInfo.provider,
297
+ modelId: stepStartModelInfo.modelId,
298
+ stepNumber,
299
+ system: undefined,
300
+ messages: conversationPrompt as unknown as ModelMessage[],
301
+ tools,
302
+ toolChoice: currentToolChoice,
303
+ activeTools: currentActiveTools as never,
304
+ steps: steps.map(normalizeStepForTelemetry),
305
+ providerOptions: currentGenerationSettings.providerOptions,
306
+ output: undefined,
307
+ runtimeContext: currentRuntimeContext,
308
+ toolsContext: currentToolsContext as never,
309
+ });
310
+
311
+ try {
312
+ // Filter tools if activeTools is specified
313
+ const effectiveTools =
314
+ currentActiveTools && currentActiveTools.length > 0
315
+ ? (filterActiveTools({
316
+ tools,
317
+ activeTools: currentActiveTools,
318
+ }) ?? tools)
319
+ : tools;
320
+
321
+ // Serialize tools before crossing the step boundary — zod schemas
322
+ // contain functions that can't be serialized by the workflow runtime.
323
+ // Tools are reconstructed with Ajv validation inside doStreamStep.
324
+ const serializedTools = serializeToolSet(effectiveTools);
325
+ const modelCallInfo = getModelInfo(currentModel);
326
+
327
+ await telemetryDispatcher.onLanguageModelCallStart?.({
328
+ callId: 'workflow-agent',
329
+ provider: modelCallInfo.provider,
330
+ modelId: modelCallInfo.modelId,
331
+ system: undefined,
332
+ messages: conversationPrompt as unknown as ModelMessage[],
333
+ tools:
334
+ serializedTools == null
335
+ ? undefined
336
+ : Object.values(serializedTools).map(tool => ({ ...tool })),
337
+ maxOutputTokens: currentGenerationSettings.maxOutputTokens,
338
+ temperature: currentGenerationSettings.temperature,
339
+ topP: currentGenerationSettings.topP,
340
+ topK: currentGenerationSettings.topK,
341
+ presencePenalty: currentGenerationSettings.presencePenalty,
342
+ frequencyPenalty: currentGenerationSettings.frequencyPenalty,
343
+ stopSequences: currentGenerationSettings.stopSequences,
344
+ seed: currentGenerationSettings.seed,
345
+ reasoning: currentGenerationSettings.reasoning,
346
+ providerOptions: currentGenerationSettings.providerOptions,
347
+ headers: currentGenerationSettings.headers,
348
+ } as never);
349
+
350
+ const { toolCalls, finish, raw, providerExecutedToolResults } =
351
+ await doStreamStep(
352
+ conversationPrompt,
353
+ currentModel,
354
+ writable,
355
+ serializedTools,
356
+ {
357
+ ...currentGenerationSettings,
358
+ toolChoice: currentToolChoice,
359
+ includeRawChunks,
360
+ repairToolCall,
361
+ responseFormat,
362
+ },
363
+ );
364
+ // Reconstruct the full StepResult outside the step boundary so the
365
+ // durable event log doesn't carry StepResult's redundant copies (or the
366
+ // per-chunk snapshot the step used to return).
367
+ const step = buildStepResult(raw, toolCalls, finish, {
368
+ stepNumber,
369
+ runtimeContext: currentRuntimeContext,
370
+ toolsContext: currentToolsContext,
371
+ });
372
+
373
+ await telemetryDispatcher.onLanguageModelCallEnd?.({
374
+ callId: step.callId,
375
+ provider: step.model?.provider ?? 'unknown',
376
+ modelId: step.model?.modelId ?? 'unknown',
377
+ finishReason: step.finishReason,
378
+ usage: step.usage,
379
+ content: step.content,
380
+ responseId: step.response.id,
381
+ });
382
+
383
+ _isFirstIteration = false;
384
+ stepNumber++;
385
+ steps.push(step);
386
+ lastStep = step;
387
+ lastStepWasToolCalls = false;
388
+
389
+ const finishReason = finish?.finishReason;
390
+
391
+ if (finishReason === 'tool-calls') {
392
+ lastStepWasToolCalls = true;
393
+
394
+ // Add assistant message with tool calls to the conversation
395
+ // Note: providerMetadata from the tool call is mapped to providerOptions
396
+ // in the prompt format, following the AI SDK convention. This is critical
397
+ // for providers like Gemini that require thoughtSignature to be preserved
398
+ // across multi-turn tool calls. Some fields are sanitized before mapping.
399
+ conversationPrompt.push({
400
+ role: 'assistant',
401
+ content: toolCalls.map(toolCall => {
402
+ const sanitizedMetadata = sanitizeProviderMetadataForToolCall(
403
+ toolCall.providerMetadata,
404
+ );
405
+ return {
406
+ type: 'tool-call',
407
+ toolCallId: toolCall.toolCallId,
408
+ toolName: toolCall.toolName,
409
+ input: toolCall.input,
410
+ ...(sanitizedMetadata != null
411
+ ? { providerOptions: sanitizedMetadata }
412
+ : {}),
413
+ };
414
+ }) as typeof toolCalls,
415
+ });
416
+
417
+ // Yield the tool calls along with the current conversation messages
418
+ // This allows executeTool to pass the conversation context to tool execute functions
419
+ // Also include provider-executed tool results so they can be used instead of local execution
420
+ const toolResults = yield {
421
+ toolCalls,
422
+ messages: conversationPrompt,
423
+ step,
424
+ runtimeContext: currentRuntimeContext,
425
+ toolsContext: currentToolsContext,
426
+ experimental_sandbox: stepSandbox,
427
+ providerExecutedToolResults,
428
+ };
429
+
430
+ conversationPrompt.push({
431
+ role: 'tool',
432
+ content: toolResults,
433
+ });
434
+
435
+ if (stopConditions) {
436
+ const stopConditionList = Array.isArray(stopConditions)
437
+ ? stopConditions
438
+ : [stopConditions];
439
+ if (stopConditionList.some(test => test({ steps }))) {
440
+ done = true;
441
+ }
442
+ }
443
+ } else if (finishReason === 'stop') {
444
+ // Add assistant message with text content to the conversation
445
+ const textContent = step.content.filter(
446
+ item => item.type === 'text',
447
+ ) as Array<{ type: 'text'; text: string }>;
448
+
449
+ if (textContent.length > 0) {
450
+ conversationPrompt.push({
451
+ role: 'assistant',
452
+ content: textContent,
453
+ });
454
+ }
455
+
456
+ done = true;
457
+ } else if (finishReason === 'length') {
458
+ // Model hit max tokens - stop but don't throw
459
+ done = true;
460
+ } else if (finishReason === 'content-filter') {
461
+ // Content filter triggered - stop but don't throw
462
+ done = true;
463
+ } else if (finishReason === 'error') {
464
+ // Model error - stop but don't throw
465
+ done = true;
466
+ } else if (finishReason === 'other') {
467
+ // Other reason - stop but don't throw
468
+ done = true;
469
+ } else if (finishReason === 'unknown') {
470
+ // Unknown reason - stop but don't throw
471
+ done = true;
472
+ } else if (!finishReason) {
473
+ // No finish reason - this might happen on incomplete streams
474
+ done = true;
475
+ } else {
476
+ throw new Error(
477
+ `Unexpected finish reason: ${typeof finish?.finishReason === 'object' ? JSON.stringify(finish?.finishReason) : finish?.finishReason}`,
478
+ );
479
+ }
480
+
481
+ const resolvedOnStepEnd = onStepEnd ?? onStepFinish;
482
+ if (resolvedOnStepEnd) {
483
+ await resolvedOnStepEnd(step);
484
+ }
485
+ await telemetryDispatcher.onStepEnd?.(normalizeStepForTelemetry(step));
486
+ } catch (error) {
487
+ if (onError) {
488
+ await onError({ error });
489
+ }
490
+ throw error;
491
+ }
492
+ }
493
+
494
+ // Yield the final step if it wasn't already yielded (tool-calls steps are yielded inside the loop)
495
+ if (lastStep && !lastStepWasToolCalls) {
496
+ yield {
497
+ toolCalls: [],
498
+ messages: conversationPrompt,
499
+ step: lastStep,
500
+ runtimeContext: currentRuntimeContext,
501
+ toolsContext: currentToolsContext,
502
+ experimental_sandbox: sandbox,
503
+ };
504
+ }
505
+
506
+ return conversationPrompt;
507
+ }
508
+
509
+ function getModelInfo(model: LanguageModel): {
510
+ provider: string;
511
+ modelId: string;
512
+ } {
513
+ return typeof model === 'string'
514
+ ? { provider: model.split('/')[0] ?? 'gateway', modelId: model }
515
+ : { provider: model.provider, modelId: model.modelId };
516
+ }
517
+
518
+ function normalizeStepForTelemetry(step: StepResult<any, any>) {
519
+ return {
520
+ ...step,
521
+ model: step.model ?? { provider: 'unknown', modelId: 'unknown' },
522
+ };
523
+ }
524
+
525
+ /**
526
+ * Reconstruct a full `StepResult` from the minimal aggregates returned by
527
+ * `doStreamStep`. Runs outside the step boundary so StepResult's redundant
528
+ * fields (duplicate tool-call lists, `content`, `reasoningText`, the
529
+ * always-empty `*ToolResults` arrays) and the per-chunk snapshot don't cross
530
+ * it. The shape matches what the AI SDK's `streamText` exposes to callers.
531
+ */
532
+ function buildStepResult(
533
+ raw: DoStreamStepRawResult,
534
+ toolCalls: ParsedToolCall[],
535
+ finish: StreamFinish | undefined,
536
+ opts: {
537
+ stepNumber: number;
538
+ runtimeContext: Context;
539
+ toolsContext: Record<string, Context | undefined>;
540
+ },
541
+ ): StepResult<ToolSet, any> {
542
+ const { text, reasoning: reasoningParts, responseMetadata, warnings } = raw;
543
+ const reasoningText = reasoningParts.map(r => r.text).join('') || undefined;
544
+
545
+ const validToolCalls = toolCalls
546
+ .filter(tc => !tc.invalid)
547
+ .map(tc => ({
548
+ type: 'tool-call' as const,
549
+ toolCallId: tc.toolCallId,
550
+ toolName: tc.toolName,
551
+ input: tc.input,
552
+ ...(tc.dynamic ? { dynamic: true as const } : {}),
553
+ }));
554
+
555
+ return {
556
+ callId: 'workflow-agent',
557
+ stepNumber: opts.stepNumber,
558
+ model: {
559
+ provider: responseMetadata?.modelId?.split(':')[0] ?? 'unknown',
560
+ modelId: responseMetadata?.modelId ?? 'unknown',
561
+ },
562
+ functionId: undefined,
563
+ metadata: undefined,
564
+ runtimeContext: opts.runtimeContext ?? {},
565
+ toolsContext: opts.toolsContext ?? {},
566
+ content: [
567
+ ...(text ? [{ type: 'text' as const, text }] : []),
568
+ ...validToolCalls,
569
+ ],
570
+ text,
571
+ reasoning: reasoningParts.map(r => ({
572
+ type: 'reasoning' as const,
573
+ text: r.text,
574
+ })),
575
+ reasoningText,
576
+ files: [],
577
+ sources: [],
578
+ toolCalls: validToolCalls,
579
+ staticToolCalls: [],
580
+ dynamicToolCalls: validToolCalls.filter(tc => tc.dynamic),
581
+ toolResults: [],
582
+ staticToolResults: [],
583
+ dynamicToolResults: [],
584
+ finishReason: finish?.finishReason ?? 'other',
585
+ rawFinishReason: finish?.rawFinishReason,
586
+ usage:
587
+ finish?.usage ??
588
+ ({
589
+ inputTokens: 0,
590
+ inputTokenDetails: {
591
+ noCacheTokens: undefined,
592
+ cacheReadTokens: undefined,
593
+ cacheWriteTokens: undefined,
594
+ },
595
+ outputTokens: 0,
596
+ outputTokenDetails: {
597
+ textTokens: undefined,
598
+ reasoningTokens: undefined,
599
+ },
600
+ totalTokens: 0,
601
+ } as LanguageModelUsage),
602
+ performance: {
603
+ effectiveOutputTokensPerSecond: 0,
604
+ outputTokensPerSecond: undefined,
605
+ inputTokensPerSecond: undefined,
606
+ effectiveTotalTokensPerSecond: 0,
607
+ stepTimeMs: 0,
608
+ responseTimeMs: 0,
609
+ toolExecutionMs: {},
610
+ timeToFirstOutputMs: undefined,
611
+ },
612
+ warnings,
613
+ request: {
614
+ body: '',
615
+ messages: [], // TODO implement step request messages
616
+ },
617
+ response: {
618
+ id: responseMetadata?.id ?? 'unknown',
619
+ timestamp: responseMetadata?.timestamp ?? new Date(),
620
+ modelId: responseMetadata?.modelId ?? 'unknown',
621
+ messages: [],
622
+ },
623
+ providerMetadata: finish?.providerMetadata ?? {},
624
+ } as StepResult<ToolSet, any>;
625
+ }
626
+
627
+ /**
628
+ * Strip OpenAI's itemId from providerMetadata (requires reasoning items we don't preserve).
629
+ * Preserves all other provider metadata (e.g., Gemini's thoughtSignature).
630
+ */
631
+ function sanitizeProviderMetadataForToolCall(
632
+ metadata: unknown,
633
+ ): Record<string, unknown> | undefined {
634
+ if (metadata == null) return undefined;
635
+
636
+ const meta = metadata as Record<string, unknown>;
637
+
638
+ // Check if OpenAI metadata exists and needs sanitization
639
+ if ('openai' in meta && meta.openai != null) {
640
+ const { openai, ...restProviders } = meta;
641
+ const openaiMeta = openai as Record<string, unknown>;
642
+
643
+ // Remove itemId from OpenAI metadata - it requires reasoning items we don't preserve
644
+ const { itemId: _itemId, ...restOpenai } = openaiMeta;
645
+
646
+ // Reconstruct metadata without itemId
647
+ const hasOtherOpenaiFields = Object.keys(restOpenai).length > 0;
648
+ const hasOtherProviders = Object.keys(restProviders).length > 0;
649
+
650
+ if (hasOtherOpenaiFields && hasOtherProviders) {
651
+ return { ...restProviders, openai: restOpenai };
652
+ } else if (hasOtherOpenaiFields) {
653
+ return { openai: restOpenai };
654
+ } else if (hasOtherProviders) {
655
+ return restProviders;
656
+ }
657
+ return undefined;
658
+ }
659
+
660
+ return meta;
661
+ }