@ai-sdk/workflow 2.0.20 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/workflow",
3
- "version": "2.0.20",
3
+ "version": "2.0.21",
4
4
  "type": "module",
5
5
  "description": "WorkflowAgent for building AI agents with AI SDK",
6
6
  "license": "Apache-2.0",
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@ai-sdk/provider": "4.0.10",
35
- "@ai-sdk/provider-utils": "5.0.35",
36
- "ai": "7.0.89",
35
+ "@ai-sdk/provider-utils": "5.0.36",
36
+ "ai": "7.0.90",
37
37
  "ajv": "^8.20.0"
38
38
  },
39
39
  "devDependencies": {
@@ -1,6 +1,9 @@
1
1
  import type {
2
+ JSONObject,
2
3
  LanguageModelV4CallOptions,
3
4
  LanguageModelV4Prompt,
5
+ LanguageModelV4Source,
6
+ SharedV4ProviderMetadata,
4
7
  } from '@ai-sdk/provider';
5
8
  import { isAbortError } from '@ai-sdk/provider-utils';
6
9
  import {
@@ -78,7 +81,9 @@ export interface ParsedToolCall {
78
81
  toolName: string;
79
82
  input: unknown;
80
83
  providerExecuted?: boolean;
81
- providerMetadata?: Record<string, unknown>;
84
+ providerMetadata?: SharedV4ProviderMetadata;
85
+ title?: string;
86
+ toolMetadata?: JSONObject;
82
87
  dynamic?: boolean;
83
88
  invalid?: boolean;
84
89
  error?: unknown;
@@ -94,18 +99,46 @@ export interface StreamFinish {
94
99
  providerMetadata?: Record<string, unknown>;
95
100
  }
96
101
 
102
+ export type DoStreamStepRawContentPart =
103
+ | {
104
+ type: 'text';
105
+ text: string;
106
+ providerMetadata?: SharedV4ProviderMetadata;
107
+ }
108
+ | {
109
+ type: 'file';
110
+ data: string;
111
+ mediaType: string;
112
+ providerMetadata?: SharedV4ProviderMetadata;
113
+ }
114
+ | LanguageModelV4Source
115
+ | {
116
+ type: 'tool-call';
117
+ toolCallIndex: number;
118
+ };
119
+
120
+ /**
121
+ * Compact callback replay data. The start event establishes the tool name for
122
+ * a call, so delta events do not repeat it and available events reuse the
123
+ * parsed input already present in `toolCalls`.
124
+ */
125
+ export type ToolInputLifecycleEvent =
126
+ | ['start', toolCallId: string, toolName: string]
127
+ | ['delta', toolCallId: string, inputTextDelta: string]
128
+ | ['available', toolCallId: string];
129
+
97
130
  /**
98
131
  * Minimal aggregates needed to reconstruct a `StepResult` outside the step
99
132
  * boundary. By returning only these fields (instead of a fully-populated
100
133
  * StepResult plus the raw `chunks[]` array), the durable event log doesn't
101
- * carry StepResult's redundant copies — `content`, the duplicate
102
- * `toolCalls`/`dynamicToolCalls` lists, `reasoningText`, the always-empty
103
- * `*ToolResults` arrays, and the per-chunk `chunks[]` snapshot the iterator
104
- * never reads. The caller reconstructs the full StepResult via
134
+ * carry StepResult's redundant derived fields — duplicate tool-call lists,
135
+ * `text`, `files`, `sources`, `reasoningText`, or the tool-result arrays
136
+ * populated after execution. It also avoids the per-chunk `chunks[]` snapshot
137
+ * the iterator never reads. The caller reconstructs the full StepResult via
105
138
  * `buildStepResult`.
106
139
  */
107
140
  export interface DoStreamStepRawResult {
108
- text: string;
141
+ content: DoStreamStepRawContentPart[];
109
142
  reasoning: Array<{ text: string }>;
110
143
  responseMetadata?: { id?: string; timestamp?: Date; modelId?: string };
111
144
  warnings?: unknown[];
@@ -119,6 +152,11 @@ export type DoStreamStepResult =
119
152
  finish: StreamFinish | undefined;
120
153
  raw: DoStreamStepRawResult;
121
154
  providerExecutedToolResults: Map<string, ProviderExecutedToolResult>;
155
+ /**
156
+ * Optional for compatibility with model-step results persisted before
157
+ * tool input lifecycle callback replay was added.
158
+ */
159
+ toolInputLifecycleEvents?: ToolInputLifecycleEvent[];
122
160
  /** Present when the model stream emitted an error part. */
123
161
  terminalError?: unknown;
124
162
  };
@@ -158,6 +196,7 @@ export async function doStreamStep(
158
196
  // Reconstruct tools from serializable definitions with Ajv validation.
159
197
  // Tools are serialized before crossing the step boundary because zod schemas
160
198
  // contain functions that can't be serialized by the workflow runtime.
199
+ const toolInputLifecycleEvents: ToolInputLifecycleEvent[] = [];
161
200
  const tools = serializedTools
162
201
  ? resolveSerializableTools(serializedTools)
163
202
  : undefined;
@@ -244,7 +283,8 @@ export async function doStreamStep(
244
283
  let finish: StreamFinish | undefined;
245
284
 
246
285
  // Minimal aggregation — only what buildStepResult needs outside the step.
247
- let text = '';
286
+ const content: DoStreamStepRawContentPart[] = [];
287
+ const textPartIndexes = new Map<string, number>();
248
288
  const reasoningParts: Array<{ text: string }> = [];
249
289
  let responseMetadata:
250
290
  | { id?: string; timestamp?: Date; modelId?: string }
@@ -252,6 +292,7 @@ export async function doStreamStep(
252
292
  let warnings: unknown[] | undefined;
253
293
  let terminalError: unknown;
254
294
  let hasTerminalError = false;
295
+ const ongoingToolCallToolNames = new Map<string, string>();
255
296
 
256
297
  // Acquire writer once before the loop to avoid per-chunk lock overhead
257
298
  const writer = writable?.getWriter();
@@ -264,28 +305,99 @@ export async function doStreamStep(
264
305
 
265
306
  for await (const part of modelStream) {
266
307
  switch (part.type) {
308
+ case 'tool-input-start':
309
+ ongoingToolCallToolNames.set(part.id, part.toolName);
310
+ if (
311
+ serializedTools?.[part.toolName]?.hasOnInputStart ||
312
+ serializedTools?.[part.toolName]?.hasOnInputDelta ||
313
+ serializedTools?.[part.toolName]?.hasOnInputAvailable
314
+ ) {
315
+ toolInputLifecycleEvents.push(['start', part.id, part.toolName]);
316
+ }
317
+ break;
318
+ case 'tool-input-delta': {
319
+ const toolName = ongoingToolCallToolNames.get(part.id);
320
+ if (
321
+ toolName != null &&
322
+ serializedTools?.[toolName]?.hasOnInputDelta
323
+ ) {
324
+ toolInputLifecycleEvents.push(['delta', part.id, part.delta]);
325
+ }
326
+ break;
327
+ }
328
+ case 'text-start':
329
+ upsertTextContentPart({
330
+ content,
331
+ textPartIndexes,
332
+ id: part.id,
333
+ providerMetadata: part.providerMetadata,
334
+ });
335
+ break;
267
336
  case 'text-delta':
268
- text += part.text;
337
+ upsertTextContentPart({
338
+ content,
339
+ textPartIndexes,
340
+ id: part.id,
341
+ textDelta: part.text,
342
+ providerMetadata: part.providerMetadata,
343
+ });
344
+ break;
345
+ case 'text-end':
346
+ upsertTextContentPart({
347
+ content,
348
+ textPartIndexes,
349
+ id: part.id,
350
+ providerMetadata: part.providerMetadata,
351
+ });
352
+ textPartIndexes.delete(part.id);
269
353
  break;
270
354
  case 'reasoning-delta':
271
355
  reasoningParts.push({ text: part.text });
272
356
  break;
357
+ case 'file':
358
+ content.push({
359
+ type: 'file',
360
+ data: part.file.base64,
361
+ mediaType: part.file.mediaType,
362
+ ...(part.providerMetadata != null
363
+ ? { providerMetadata: part.providerMetadata }
364
+ : {}),
365
+ });
366
+ break;
367
+ case 'source':
368
+ content.push(part);
369
+ break;
273
370
  case 'tool-call': {
274
371
  // parseToolCall adds dynamic/invalid/error at runtime
275
372
  const toolCallPart = part as typeof part & Partial<ParsedToolCall>;
373
+ const toolCallIndex = toolCalls.length;
374
+ const lifecycleToolName = ongoingToolCallToolNames.get(
375
+ toolCallPart.toolCallId,
376
+ );
377
+ ongoingToolCallToolNames.delete(toolCallPart.toolCallId);
378
+ if (
379
+ lifecycleToolName != null &&
380
+ serializedTools?.[lifecycleToolName]?.hasOnInputAvailable
381
+ ) {
382
+ toolInputLifecycleEvents.push([
383
+ 'available',
384
+ toolCallPart.toolCallId,
385
+ ]);
386
+ }
276
387
  toolCalls.push({
277
388
  type: 'tool-call',
278
389
  toolCallId: toolCallPart.toolCallId,
279
390
  toolName: toolCallPart.toolName,
280
391
  input: toolCallPart.input,
281
392
  providerExecuted: toolCallPart.providerExecuted,
282
- providerMetadata: toolCallPart.providerMetadata as
283
- | Record<string, unknown>
284
- | undefined,
393
+ providerMetadata: toolCallPart.providerMetadata,
394
+ title: toolCallPart.title,
395
+ toolMetadata: toolCallPart.toolMetadata,
285
396
  dynamic: toolCallPart.dynamic,
286
397
  invalid: toolCallPart.invalid,
287
398
  error: toolCallPart.error,
288
399
  });
400
+ content.push({ type: 'tool-call', toolCallIndex });
289
401
  break;
290
402
  }
291
403
  case 'tool-result':
@@ -374,12 +486,13 @@ export async function doStreamStep(
374
486
  toolCalls,
375
487
  finish,
376
488
  raw: {
377
- text,
489
+ content,
378
490
  reasoning: reasoningParts,
379
491
  responseMetadata,
380
492
  warnings,
381
493
  },
382
494
  providerExecutedToolResults,
495
+ toolInputLifecycleEvents,
383
496
  ...(hasTerminalError ? { terminalError } : {}),
384
497
  };
385
498
  }
@@ -387,3 +500,43 @@ export async function doStreamStep(
387
500
  // Model-call retries are handled above so the workflow runtime must not add
388
501
  // another retry layer around the durable step.
389
502
  doStreamStep.maxRetries = 0;
503
+
504
+ function upsertTextContentPart({
505
+ content,
506
+ textPartIndexes,
507
+ id,
508
+ textDelta,
509
+ providerMetadata,
510
+ }: {
511
+ content: DoStreamStepRawContentPart[];
512
+ textPartIndexes: Map<string, number>;
513
+ id: string;
514
+ textDelta?: string;
515
+ providerMetadata?: SharedV4ProviderMetadata;
516
+ }) {
517
+ let partIndex = textPartIndexes.get(id);
518
+
519
+ if (partIndex == null) {
520
+ partIndex =
521
+ content.push({
522
+ type: 'text',
523
+ text: '',
524
+ ...(providerMetadata != null ? { providerMetadata } : {}),
525
+ }) - 1;
526
+ textPartIndexes.set(id, partIndex);
527
+ }
528
+
529
+ const part = content[partIndex];
530
+
531
+ if (part.type !== 'text') {
532
+ throw new Error(`Expected text content at index ${partIndex}.`);
533
+ }
534
+
535
+ if (textDelta != null) {
536
+ part.text += textDelta;
537
+ }
538
+
539
+ if (providerMetadata != null) {
540
+ part.providerMetadata = providerMetadata;
541
+ }
542
+ }
@@ -0,0 +1,28 @@
1
+ import { validateTypes, type Context, type Tool } from '@ai-sdk/provider-utils';
2
+
3
+ /**
4
+ * Resolves the per-tool context passed to tool callbacks and execution.
5
+ * Context schemas validate and normalize the value before any tool-owned
6
+ * callback observes it.
7
+ */
8
+ export async function resolveToolContext({
9
+ toolName,
10
+ tool,
11
+ toolsContext,
12
+ }: {
13
+ toolName: string;
14
+ tool: Tool;
15
+ toolsContext: Record<string, Context | undefined> | undefined;
16
+ }): Promise<unknown> {
17
+ const contextSchema = tool.contextSchema;
18
+ const entry = toolsContext?.[toolName];
19
+ if (contextSchema == null) {
20
+ return entry;
21
+ }
22
+
23
+ return await validateTypes({
24
+ value: entry,
25
+ schema: contextSchema,
26
+ context: { field: 'tool context', entityName: toolName },
27
+ });
28
+ }
@@ -10,7 +10,7 @@
10
10
  * `~standard.jsonSchema` (Standard Schema v2), extraction can be simplified
11
11
  * to use that interface directly.
12
12
  */
13
- import type { JSONSchema7 } from '@ai-sdk/provider';
13
+ import type { JSONObject, JSONSchema7 } from '@ai-sdk/provider';
14
14
  import {
15
15
  asSchema,
16
16
  type Experimental_SandboxSession as SandboxSession,
@@ -18,23 +18,33 @@ import {
18
18
  jsonSchema,
19
19
  type Tool,
20
20
  } from '@ai-sdk/provider-utils';
21
- import { tool, type ToolSet } from 'ai';
21
+ import { dynamicTool, tool, type ToolSet } from 'ai';
22
22
  import Ajv from 'ajv';
23
23
 
24
24
  /**
25
25
  * Serializable tool definition — plain objects only, safe for workflow steps.
26
26
  */
27
27
  export type SerializableToolDef = {
28
+ title?: string;
29
+ metadata?: JSONObject;
28
30
  description?: string;
29
31
  inputSchema: JSONSchema7;
32
+ /** Whether providers should enforce strict tool input generation. */
33
+ strict?: boolean;
30
34
  /** Input examples forwarded to providers that support them. */
31
35
  inputExamples?: Array<{ input: unknown }>;
32
36
  /** Provider-specific options attached to the tool definition. */
33
37
  providerOptions?: Tool['providerOptions'];
34
- /** Present on provider tools (e.g. anthropic.tools.webSearch). */
35
- type?: 'provider';
38
+ /** Input lifecycle callbacks that must be invoked outside the step. */
39
+ hasOnInputStart?: boolean;
40
+ hasOnInputDelta?: boolean;
41
+ hasOnInputAvailable?: boolean;
42
+ /** Present on dynamic and provider tools. */
43
+ type?: 'dynamic' | 'provider';
36
44
  /** Provider tool is executed by the provider. */
37
45
  isProviderExecuted?: boolean;
46
+ /** Provider tool results may arrive in a later model response. */
47
+ supportsDeferredResults?: boolean;
38
48
  /** Provider tool ID, e.g. 'anthropic.web_search_20250305'. */
39
49
  id?: `${string}.${string}`;
40
50
  /** Provider tool configuration args (maxUses, allowedDomains, etc.). */
@@ -59,6 +69,8 @@ export function serializeToolSet<TOOLS extends ToolSet>(
59
69
  return Object.fromEntries(
60
70
  Object.entries(tools).map(([name, t]) => {
61
71
  const def: SerializableToolDef = {
72
+ title: t.title,
73
+ metadata: t.metadata,
62
74
  description: resolveToolDescription({
63
75
  tool: t,
64
76
  toolName: name,
@@ -66,17 +78,32 @@ export function serializeToolSet<TOOLS extends ToolSet>(
66
78
  experimental_sandbox: sandbox,
67
79
  }),
68
80
  inputSchema: asSchema(t.inputSchema).jsonSchema as JSONSchema7,
81
+ strict: t.strict,
69
82
  inputExamples: t.inputExamples,
70
83
  providerOptions: t.providerOptions,
71
84
  };
72
85
 
86
+ if (t.type === 'dynamic') {
87
+ def.type = 'dynamic';
88
+ }
89
+ if (t.onInputStart != null) {
90
+ def.hasOnInputStart = true;
91
+ }
92
+ if (t.onInputDelta != null) {
93
+ def.hasOnInputDelta = true;
94
+ }
95
+ if (t.onInputAvailable != null) {
96
+ def.hasOnInputAvailable = true;
97
+ }
98
+
73
99
  // Preserve provider tool identity so the Gateway can recognize
74
100
  // them as provider-executed tools (e.g. anthropic webSearch).
75
- if ((t as any).type === 'provider') {
101
+ if (t.type === 'provider') {
76
102
  def.type = 'provider';
77
- def.isProviderExecuted = (t as any).isProviderExecuted ?? false;
78
- def.id = (t as any).id;
79
- def.args = (t as any).args;
103
+ def.isProviderExecuted = t.isProviderExecuted ?? false;
104
+ def.supportsDeferredResults = t.supportsDeferredResults;
105
+ def.id = t.id;
106
+ def.args = t.args;
80
107
  }
81
108
 
82
109
  return [name, def];
@@ -122,39 +149,81 @@ export function resolveSerializableTools(
122
149
  // Provider tools are executed server-side — pass them through
123
150
  // with their identity intact, no client-side validation needed.
124
151
  if (t.type === 'provider') {
152
+ const providerTool = {
153
+ type: 'provider' as const,
154
+ title: t.title,
155
+ metadata: t.metadata,
156
+ id: t.id!,
157
+ args: t.args ?? {},
158
+ inputSchema: jsonSchema(t.inputSchema),
159
+ providerOptions: t.providerOptions,
160
+ };
161
+
125
162
  return [
126
163
  name,
127
- tool({
128
- type: 'provider' as const,
129
- id: t.id!,
130
- args: t.args ?? {},
131
- isProviderExecuted: t.isProviderExecuted ?? false,
132
- inputSchema: jsonSchema(t.inputSchema),
164
+ t.isProviderExecuted
165
+ ? tool({
166
+ ...providerTool,
167
+ isProviderExecuted: true,
168
+ supportsDeferredResults: t.supportsDeferredResults,
169
+ })
170
+ : tool({
171
+ ...providerTool,
172
+ isProviderExecuted: false,
173
+ }),
174
+ ];
175
+ }
176
+
177
+ if (t.type === 'dynamic') {
178
+ const validateFn = ajv.compile(t.inputSchema);
179
+
180
+ return [
181
+ name,
182
+ dynamicTool({
183
+ description: t.description,
184
+ inputExamples: t.inputExamples,
133
185
  providerOptions: t.providerOptions,
186
+ inputSchema: jsonSchema(t.inputSchema, {
187
+ validate: value => {
188
+ if (validateFn(value)) {
189
+ return { success: true, value };
190
+ }
191
+ return {
192
+ success: false,
193
+ error: new Error(ajv.errorsText(validateFn.errors)),
194
+ };
195
+ },
196
+ }),
134
197
  }),
135
198
  ];
136
199
  }
137
200
 
138
201
  const validateFn = ajv.compile(t.inputSchema);
202
+ const functionTool = {
203
+ title: t.title,
204
+ metadata: t.metadata,
205
+ description: t.description,
206
+ strict: t.strict,
207
+ inputExamples: t.inputExamples,
208
+ providerOptions: t.providerOptions,
209
+ inputSchema: jsonSchema(t.inputSchema, {
210
+ validate: value => {
211
+ if (validateFn(value)) {
212
+ return { success: true, value: value as any };
213
+ }
214
+ return {
215
+ success: false,
216
+ error: new Error(ajv.errorsText(validateFn.errors)),
217
+ };
218
+ },
219
+ }),
220
+ };
139
221
 
140
222
  return [
141
223
  name,
142
- tool({
143
- description: t.description,
144
- inputExamples: t.inputExamples,
145
- providerOptions: t.providerOptions,
146
- inputSchema: jsonSchema(t.inputSchema, {
147
- validate: value => {
148
- if (validateFn(value)) {
149
- return { success: true, value: value as any };
150
- }
151
- return {
152
- success: false,
153
- error: new Error(ajv.errorsText(validateFn.errors)),
154
- };
155
- },
156
- }),
157
- }),
224
+ t.type === 'dynamic'
225
+ ? tool({ ...functionTool, type: 'dynamic' })
226
+ : tool(functionTool),
158
227
  ];
159
228
  }),
160
229
  );