@ai-sdk/workflow 1.0.0-beta.1 → 1.0.0-beta.100

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": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.100",
4
4
  "description": "WorkflowAgent for building AI agents with AI SDK",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.js",
@@ -26,17 +26,17 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "ajv": "^8.18.0",
30
- "@ai-sdk/provider": "4.0.0-beta.12",
31
- "@ai-sdk/provider-utils": "5.0.0-beta.20",
32
- "ai": "7.0.0-beta.90"
29
+ "ajv": "^8.20.0",
30
+ "@ai-sdk/provider": "4.0.0-beta.19",
31
+ "@ai-sdk/provider-utils": "5.0.0-beta.49",
32
+ "ai": "7.0.0-beta.182"
33
33
  },
34
34
  "devDependencies": {
35
- "@types/node": "20.17.24",
35
+ "@types/node": "22.19.19",
36
36
  "@workflow/vitest": "4.0.1",
37
- "tsup": "^8",
37
+ "tsup": "^8.5.1",
38
38
  "typescript": "5.8.3",
39
- "workflow": "4.2.0",
39
+ "workflow": "4.2.4",
40
40
  "zod": "3.25.76",
41
41
  "@vercel/ai-tsconfig": "0.0.0"
42
42
  },
@@ -44,15 +44,17 @@
44
44
  "zod": "^3.25.76 || ^4.1.8"
45
45
  },
46
46
  "engines": {
47
- "node": ">=18"
47
+ "node": ">=22"
48
48
  },
49
49
  "publishConfig": {
50
- "access": "public"
50
+ "access": "public",
51
+ "provenance": true
51
52
  },
52
53
  "homepage": "https://ai-sdk.dev/docs",
53
54
  "repository": {
54
55
  "type": "git",
55
- "url": "git+https://github.com/vercel/ai.git"
56
+ "url": "https://github.com/vercel/ai",
57
+ "directory": "packages/workflow"
56
58
  },
57
59
  "bugs": {
58
60
  "url": "https://github.com/vercel/ai/issues"
@@ -0,0 +1,85 @@
1
+ import type { LanguageModelV4ToolResultOutput } from '@ai-sdk/provider';
2
+ import type { Tool } from '@ai-sdk/provider-utils';
3
+ import type { ModelMessage } from 'ai';
4
+ import {
5
+ createDefaultDownloadFunction,
6
+ createToolModelOutput,
7
+ downloadAssets,
8
+ mapToolResultOutput,
9
+ type DownloadFunction,
10
+ } from 'ai/internal';
11
+
12
+ /**
13
+ * Converts a single tool result into a provider-level
14
+ * `LanguageModelV4ToolResultOutput`, honoring the tool's optional
15
+ * `toModelOutput` hook.
16
+ *
17
+ * Unlike `generateText`/`streamText`, `WorkflowAgent` assembles the
18
+ * `LanguageModelV4` prompt incrementally — appending one tool result at a time
19
+ * — instead of building AI-level `ModelMessage`s and converting the whole
20
+ * prompt once via `convertToLanguageModelPrompt`. This helper performs the
21
+ * equivalent per-result conversion using the shared `ai/internal` primitives:
22
+ *
23
+ * 1. `createToolModelOutput` — applies `tool.toModelOutput` (or the
24
+ * text/json/error fallback).
25
+ * 2. `downloadAssets` — for `content`-type outputs, downloads any file/image
26
+ * assets so URLs become bytes the provider can consume.
27
+ * 3. `mapToolResultOutput` — maps the AI-level `ToolResultOutput` to the
28
+ * provider-level output and converts legacy file types.
29
+ */
30
+ export async function createLanguageModelToolResultOutput({
31
+ toolCallId,
32
+ toolName,
33
+ input,
34
+ output,
35
+ tool,
36
+ errorMode,
37
+ supportedUrls,
38
+ download = createDefaultDownloadFunction(),
39
+ provider,
40
+ }: {
41
+ toolCallId: string;
42
+ toolName: string;
43
+ input: unknown;
44
+ output: unknown;
45
+ tool: Tool | undefined;
46
+ errorMode: 'none' | 'text' | 'json';
47
+ supportedUrls: Record<string, RegExp[]>;
48
+ download?: DownloadFunction;
49
+ provider?: string;
50
+ }): Promise<LanguageModelV4ToolResultOutput> {
51
+ const modelOutput = await createToolModelOutput({
52
+ toolCallId,
53
+ input,
54
+ output,
55
+ tool,
56
+ errorMode,
57
+ });
58
+
59
+ const downloadedAssets =
60
+ modelOutput.type === 'content'
61
+ ? await downloadAssets(
62
+ [
63
+ {
64
+ role: 'tool',
65
+ content: [
66
+ {
67
+ type: 'tool-result',
68
+ toolCallId,
69
+ toolName,
70
+ output: modelOutput,
71
+ },
72
+ ],
73
+ } satisfies ModelMessage,
74
+ ],
75
+ download,
76
+ supportedUrls,
77
+ )
78
+ : {};
79
+
80
+ return mapToolResultOutput({
81
+ output: modelOutput,
82
+ provider,
83
+ downloadedAssets,
84
+ });
85
+ }
@@ -2,9 +2,11 @@ import type {
2
2
  LanguageModelV4CallOptions,
3
3
  LanguageModelV4Prompt,
4
4
  } from '@ai-sdk/provider';
5
+ import type { Context } from '@ai-sdk/provider-utils';
5
6
  import {
6
- type Experimental_LanguageModelStreamPart as ModelCallStreamPart,
7
7
  experimental_streamLanguageModelCall as streamModelCall,
8
+ gateway,
9
+ type Experimental_LanguageModelStreamPart as ModelCallStreamPart,
8
10
  type FinishReason,
9
11
  type LanguageModel,
10
12
  type LanguageModelUsage,
@@ -15,14 +17,11 @@ import {
15
17
  type ToolChoice,
16
18
  type ToolSet,
17
19
  } from 'ai';
18
- import { gateway } from 'ai';
19
- import type { ProviderOptions, TelemetrySettings } from './workflow-agent.js';
20
+ import type { ProviderOptions } from './workflow-agent.js';
20
21
  import {
21
22
  resolveSerializableTools,
22
23
  type SerializableToolDef,
23
24
  } from './serializable-schema.js';
24
- import type { CompatibleLanguageModel } from './types.js';
25
-
26
25
  export type { Experimental_LanguageModelStreamPart as ModelCallStreamPart } from 'ai';
27
26
 
28
27
  export type ModelStopCondition = StopCondition<NoInfer<ToolSet>, any>;
@@ -55,9 +54,15 @@ export interface DoStreamStepOptions {
55
54
  providerOptions?: ProviderOptions;
56
55
  toolChoice?: ToolChoice<ToolSet>;
57
56
  includeRawChunks?: boolean;
58
- experimental_telemetry?: TelemetrySettings;
59
57
  repairToolCall?: ToolCallRepairFunction<ToolSet>;
60
58
  responseFormat?: LanguageModelV4CallOptions['responseFormat'];
59
+ runtimeContext?: Context;
60
+ toolsContext?: Record<string, Context | undefined>;
61
+ /**
62
+ * The step number for the returned StepResult. Defaults to 0 for direct
63
+ * callers; stream iterators pass their current index. See #15151.
64
+ */
65
+ stepNumber?: number;
61
66
  }
62
67
 
63
68
  /**
@@ -87,33 +92,24 @@ export interface StreamFinish {
87
92
 
88
93
  export async function doStreamStep(
89
94
  conversationPrompt: LanguageModelV4Prompt,
90
- modelInit:
91
- | string
92
- | CompatibleLanguageModel
93
- | (() => Promise<CompatibleLanguageModel>),
95
+ modelInit: LanguageModel,
94
96
  writable?: WritableStream<ModelCallStreamPart<ToolSet>>,
95
97
  serializedTools?: Record<string, SerializableToolDef>,
96
98
  options?: DoStreamStepOptions,
97
- ) {
99
+ ): Promise<{
100
+ toolCalls: ParsedToolCall[];
101
+ finish: StreamFinish | undefined;
102
+ step: StepResult<ToolSet, any>;
103
+ chunks?: unknown[];
104
+ providerExecutedToolResults: Map<string, ProviderExecutedToolResult>;
105
+ }> {
98
106
  'use step';
99
107
 
100
108
  // Resolve model inside step (must happen here for serialization boundary)
101
- let model: CompatibleLanguageModel;
102
- if (typeof modelInit === 'string') {
103
- model = gateway.languageModel(modelInit) as CompatibleLanguageModel;
104
- } else if (typeof modelInit === 'function') {
105
- model = await modelInit();
106
- } else if (
107
- typeof modelInit === 'object' &&
108
- modelInit !== null &&
109
- 'modelId' in modelInit
110
- ) {
111
- model = modelInit;
112
- } else {
113
- throw new Error(
114
- 'Invalid "model initialization" argument. Must be a string, a LanguageModel instance, or a function that returns a LanguageModel instance.',
115
- );
116
- }
109
+ const model: LanguageModel =
110
+ typeof modelInit === 'string'
111
+ ? gateway.languageModel(modelInit)
112
+ : modelInit;
117
113
 
118
114
  // Reconstruct tools from serializable definitions with Ajv validation.
119
115
  // Tools are serialized before crossing the step boundary because zod schemas
@@ -126,11 +122,12 @@ export async function doStreamStep(
126
122
  // model.doStream(), retry logic, and stream part transformation
127
123
  // (tool call parsing, finish reason mapping, file wrapping).
128
124
  const { stream: modelStream } = await streamModelCall({
129
- model: model as LanguageModel,
125
+ model,
130
126
  // streamModelCall expects Prompt (ModelMessage[]) but we pass the
131
127
  // pre-converted LanguageModelV4Prompt. standardizePrompt inside
132
128
  // streamModelCall handles both formats.
133
129
  messages: conversationPrompt as unknown as ModelMessage[],
130
+ allowSystemInMessages: true,
134
131
  tools,
135
132
  toolChoice: options?.toolChoice,
136
133
  includeRawChunks: options?.includeRawChunks,
@@ -159,6 +156,7 @@ export async function doStreamStep(
159
156
  // Aggregation for StepResult
160
157
  let text = '';
161
158
  const reasoningParts: Array<{ text: string }> = [];
159
+ const chunks: unknown[] = [];
162
160
  let responseMetadata:
163
161
  | { id?: string; timestamp?: Date; modelId?: string }
164
162
  | undefined;
@@ -169,6 +167,14 @@ export async function doStreamStep(
169
167
 
170
168
  try {
171
169
  for await (const part of modelStream) {
170
+ if (
171
+ part.type !== 'model-call-start' &&
172
+ part.type !== 'model-call-end' &&
173
+ part.type !== 'model-call-response-metadata'
174
+ ) {
175
+ chunks.push(part);
176
+ }
177
+
172
178
  switch (part.type) {
173
179
  case 'text-delta':
174
180
  text += part.text;
@@ -250,14 +256,15 @@ export async function doStreamStep(
250
256
 
251
257
  const step: StepResult<ToolSet, any> = {
252
258
  callId: 'workflow-agent',
253
- stepNumber: 0,
259
+ stepNumber: options?.stepNumber ?? 0,
254
260
  model: {
255
261
  provider: responseMetadata?.modelId?.split(':')[0] ?? 'unknown',
256
262
  modelId: responseMetadata?.modelId ?? 'unknown',
257
263
  },
258
264
  functionId: undefined,
259
265
  metadata: undefined,
260
- context: undefined,
266
+ runtimeContext: options?.runtimeContext ?? {},
267
+ toolsContext: options?.toolsContext ?? {},
261
268
  content: [
262
269
  ...(text ? [{ type: 'text' as const, text }] : []),
263
270
  ...toolCalls
@@ -318,8 +325,21 @@ export async function doStreamStep(
318
325
  },
319
326
  totalTokens: 0,
320
327
  } as LanguageModelUsage),
328
+ performance: {
329
+ effectiveOutputTokensPerSecond: 0,
330
+ outputTokensPerSecond: undefined,
331
+ inputTokensPerSecond: undefined,
332
+ effectiveTotalTokensPerSecond: 0,
333
+ stepTimeMs: 0,
334
+ responseTimeMs: 0,
335
+ toolExecutionMs: {},
336
+ timeToFirstOutputMs: undefined,
337
+ },
321
338
  warnings,
322
- request: { body: '' },
339
+ request: {
340
+ body: '',
341
+ messages: [], // TODO implement step request messages
342
+ },
323
343
  response: {
324
344
  id: responseMetadata?.id ?? 'unknown',
325
345
  timestamp: responseMetadata?.timestamp ?? new Date(),
@@ -333,6 +353,7 @@ export async function doStreamStep(
333
353
  toolCalls,
334
354
  finish,
335
355
  step,
356
+ chunks,
336
357
  providerExecutedToolResults,
337
358
  };
338
359
  }
package/src/index.ts CHANGED
@@ -17,16 +17,19 @@ export {
17
17
  type PrepareStepInfo,
18
18
  type PrepareStepResult,
19
19
  type ProviderOptions,
20
- type StreamTextOnAbortCallback,
21
- type StreamTextOnErrorCallback,
22
- type StreamTextOnFinishCallback,
20
+ type WorkflowAgentOnAbortCallback,
21
+ type WorkflowAgentOnEndCallback,
22
+ type WorkflowAgentOnErrorCallback,
23
+ type WorkflowAgentOnFinishCallback,
24
+ type WorkflowAgentOnStepEndCallback,
25
+ type WorkflowAgentOnStepFinishCallback,
23
26
  type StreamTextTransform,
24
- type TelemetrySettings,
27
+ type TelemetryOptions,
25
28
  type ToolCallRepairFunction,
26
29
  type WorkflowAgentOnStartCallback,
27
30
  type WorkflowAgentOnStepStartCallback,
28
- type WorkflowAgentOnToolCallStartCallback,
29
- type WorkflowAgentOnToolCallFinishCallback,
31
+ type WorkflowAgentOnToolExecutionStartCallback,
32
+ type WorkflowAgentOnToolExecutionEndCallback,
30
33
  } from './workflow-agent.js';
31
34
 
32
35
  export {
@@ -6,19 +6,55 @@ export type MockResponseDescriptor =
6
6
 
7
7
  /**
8
8
  * Mock model that returns a fixed text response.
9
- * Same 'use step' pattern as real providers (anthropic, openai, etc.).
10
- * Only captures `text` (string) — fully serializable across step boundary.
11
9
  */
12
10
  export function mockTextModel(text: string) {
13
- return async () => {
14
- 'use step';
15
- // Bind closure var at step body level so SWC plugin detects it
16
- const _text = text;
17
- return mockProvider({
18
- doStream: async () => ({
19
- stream: new ReadableStream({
20
- start(c) {
21
- for (const v of [
11
+ return mockProvider({
12
+ doStream: async () => ({
13
+ stream: new ReadableStream({
14
+ start(c) {
15
+ for (const v of [
16
+ { type: 'stream-start', warnings: [] },
17
+ {
18
+ type: 'response-metadata',
19
+ id: 'r',
20
+ modelId: 'mock',
21
+ timestamp: new Date(),
22
+ },
23
+ { type: 'text-start', id: '1' },
24
+ { type: 'text-delta', id: '1', delta: text },
25
+ { type: 'text-end', id: '1' },
26
+ {
27
+ type: 'finish',
28
+ finishReason: { unified: 'stop', raw: 'stop' },
29
+ usage: {
30
+ inputTokens: { total: 5, noCache: 5 },
31
+ outputTokens: { total: 10, text: 10 },
32
+ },
33
+ },
34
+ ] as any[])
35
+ c.enqueue(v);
36
+ c.close();
37
+ },
38
+ }),
39
+ }),
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Mock model that plays through a sequence of responses.
45
+ * Determines which response to return by counting assistant messages in the prompt.
46
+ */
47
+ export function mockSequenceModel(responses: MockResponseDescriptor[]) {
48
+ return mockProvider({
49
+ doStream: async (options: any) => {
50
+ const responseIndex = Math.min(
51
+ options.prompt.filter((m: any) => m.role === 'assistant').length,
52
+ responses.length - 1,
53
+ );
54
+ const selectedResponse = responses[responseIndex];
55
+ const parts =
56
+ selectedResponse.type === 'text'
57
+ ? [
22
58
  { type: 'stream-start', warnings: [] },
23
59
  {
24
60
  type: 'response-metadata',
@@ -27,7 +63,7 @@ export function mockTextModel(text: string) {
27
63
  timestamp: new Date(),
28
64
  },
29
65
  { type: 'text-start', id: '1' },
30
- { type: 'text-delta', id: '1', delta: _text },
66
+ { type: 'text-delta', id: '1', delta: selectedResponse.text },
31
67
  { type: 'text-end', id: '1' },
32
68
  {
33
69
  type: 'finish',
@@ -37,87 +73,38 @@ export function mockTextModel(text: string) {
37
73
  outputTokens: { total: 10, text: 10 },
38
74
  },
39
75
  },
40
- ] as any[])
41
- c.enqueue(v);
76
+ ]
77
+ : [
78
+ { type: 'stream-start', warnings: [] },
79
+ {
80
+ type: 'response-metadata',
81
+ id: 'r',
82
+ modelId: 'mock',
83
+ timestamp: new Date(),
84
+ },
85
+ {
86
+ type: 'tool-call',
87
+ toolCallId: `call-${responseIndex + 1}`,
88
+ toolName: selectedResponse.toolName,
89
+ input: selectedResponse.input,
90
+ },
91
+ {
92
+ type: 'finish',
93
+ finishReason: { unified: 'tool-calls', raw: undefined },
94
+ usage: {
95
+ inputTokens: { total: 5, noCache: 5 },
96
+ outputTokens: { total: 10, text: 10 },
97
+ },
98
+ },
99
+ ];
100
+ return {
101
+ stream: new ReadableStream({
102
+ start(c) {
103
+ for (const streamPart of parts as any[]) c.enqueue(streamPart);
42
104
  c.close();
43
105
  },
44
106
  }),
45
- }),
46
- });
47
- };
48
- }
49
-
50
- /**
51
- * Mock model that plays through a sequence of responses.
52
- * Determines which response to return by counting assistant messages in the prompt.
53
- * Only captures `responses` (array of plain objects) — fully serializable.
54
- */
55
- export function mockSequenceModel(responses: MockResponseDescriptor[]) {
56
- return async () => {
57
- 'use step';
58
- // Bind closure var at step body level so SWC plugin detects it
59
- const _responses = responses;
60
- return mockProvider({
61
- doStream: async (options: any) => {
62
- const idx = Math.min(
63
- options.prompt.filter((m: any) => m.role === 'assistant').length,
64
- _responses.length - 1,
65
- );
66
- const r = _responses[idx];
67
- const parts =
68
- r.type === 'text'
69
- ? [
70
- { type: 'stream-start', warnings: [] },
71
- {
72
- type: 'response-metadata',
73
- id: 'r',
74
- modelId: 'mock',
75
- timestamp: new Date(),
76
- },
77
- { type: 'text-start', id: '1' },
78
- { type: 'text-delta', id: '1', delta: r.text },
79
- { type: 'text-end', id: '1' },
80
- {
81
- type: 'finish',
82
- finishReason: { unified: 'stop', raw: 'stop' },
83
- usage: {
84
- inputTokens: { total: 5, noCache: 5 },
85
- outputTokens: { total: 10, text: 10 },
86
- },
87
- },
88
- ]
89
- : [
90
- { type: 'stream-start', warnings: [] },
91
- {
92
- type: 'response-metadata',
93
- id: 'r',
94
- modelId: 'mock',
95
- timestamp: new Date(),
96
- },
97
- {
98
- type: 'tool-call',
99
- toolCallId: `call-${idx + 1}`,
100
- toolName: r.toolName,
101
- input: r.input,
102
- },
103
- {
104
- type: 'finish',
105
- finishReason: { unified: 'tool-calls', raw: undefined },
106
- usage: {
107
- inputTokens: { total: 5, noCache: 5 },
108
- outputTokens: { total: 10, text: 10 },
109
- },
110
- },
111
- ];
112
- return {
113
- stream: new ReadableStream({
114
- start(c) {
115
- for (const p of parts as any[]) c.enqueue(p);
116
- c.close();
117
- },
118
- }),
119
- };
120
- },
121
- });
122
- };
107
+ };
108
+ },
109
+ });
123
110
  }
@@ -23,6 +23,8 @@ export type SerializableToolDef = {
23
23
  inputSchema: JSONSchema7;
24
24
  /** Present on provider tools (e.g. anthropic.tools.webSearch). */
25
25
  type?: 'provider';
26
+ /** Provider tool is executed by the provider. */
27
+ isProviderExecuted?: boolean;
26
28
  /** Provider tool ID, e.g. 'anthropic.web_search_20250305'. */
27
29
  id?: `${string}.${string}`;
28
30
  /** Provider tool configuration args (maxUses, allowedDomains, etc.). */
@@ -41,7 +43,7 @@ export function serializeToolSet(
41
43
  return Object.fromEntries(
42
44
  Object.entries(tools).map(([name, t]) => {
43
45
  const def: SerializableToolDef = {
44
- description: t.description,
46
+ description: t.description as string, // TODO support tools with function descriptions
45
47
  inputSchema: asSchema(t.inputSchema).jsonSchema as JSONSchema7,
46
48
  };
47
49
 
@@ -49,6 +51,7 @@ export function serializeToolSet(
49
51
  // them as provider-executed tools (e.g. anthropic webSearch).
50
52
  if ((t as any).type === 'provider') {
51
53
  def.type = 'provider';
54
+ def.isProviderExecuted = (t as any).isProviderExecuted ?? false;
52
55
  def.id = (t as any).id;
53
56
  def.args = (t as any).args;
54
57
  }
@@ -81,6 +84,7 @@ export function resolveSerializableTools(
81
84
  type: 'provider' as const,
82
85
  id: t.id!,
83
86
  args: t.args ?? {},
87
+ isProviderExecuted: t.isProviderExecuted ?? false,
84
88
  inputSchema: jsonSchema(t.inputSchema),
85
89
  }),
86
90
  ];