@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.
- package/CHANGELOG.md +1145 -0
- package/LICENSE +13 -0
- package/README.md +62 -0
- package/dist/index.d.ts +1188 -0
- package/dist/index.js +2522 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
- package/src/create-language-model-tool-result-output.ts +85 -0
- package/src/do-stream-step.ts +265 -0
- package/src/index.ts +49 -0
- package/src/normalize-ui-message-stream.ts +164 -0
- package/src/providers/mock-function-wrapper.ts +11 -0
- package/src/providers/mock.ts +110 -0
- package/src/serializable-schema.ts +114 -0
- package/src/stream-text-iterator.ts +661 -0
- package/src/test/agent-e2e-workflows.ts +673 -0
- package/src/test/calculate-workflow.ts +19 -0
- package/src/test/test-sandbox.ts +26 -0
- package/src/to-ui-message-chunk.ts +238 -0
- package/src/types.ts +11 -0
- package/src/workflow-agent.ts +2930 -0
- package/src/workflow-chat-transport.ts +534 -0
|
@@ -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
|
+
}
|