@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,673 @@
1
+ /**
2
+ * Integration test workflows for WorkflowAgent using mock providers.
3
+ */
4
+ import { tool } from 'ai';
5
+ import { WorkflowAgent } from '../workflow-agent.js';
6
+ import { mockTextModel, mockSequenceModel } from '../providers/mock.js';
7
+ import { createTestSandbox } from './test-sandbox.js';
8
+ import { FatalError, getWritable } from 'workflow';
9
+ import { z } from 'zod';
10
+
11
+ // ============================================================================
12
+ // Tool step functions
13
+ // ============================================================================
14
+
15
+ async function addNumbers(input: { a: number; b: number }): Promise<number> {
16
+ 'use step';
17
+ return input.a + input.b;
18
+ }
19
+
20
+ async function echoStep(input: { step: number }): Promise<string> {
21
+ 'use step';
22
+ return `step-${input.step}-done`;
23
+ }
24
+
25
+ async function throwingStep(): Promise<string> {
26
+ 'use step';
27
+ throw new FatalError('Tool execution failed fatally');
28
+ }
29
+
30
+ // ============================================================================
31
+ // Core agent tests
32
+ // ============================================================================
33
+
34
+ export async function agentBasicE2e(prompt: string) {
35
+ 'use workflow';
36
+ const agent = new WorkflowAgent({
37
+ model: mockTextModel(`Echo: ${prompt}`),
38
+ instructions: 'You are a helpful assistant.',
39
+ });
40
+ const result = await agent.stream({
41
+ messages: [{ role: 'user', content: prompt }],
42
+ writable: getWritable(),
43
+ });
44
+ return {
45
+ stepCount: result.steps.length,
46
+ lastStepText: result.steps[result.steps.length - 1]?.text,
47
+ };
48
+ }
49
+
50
+ export async function agentToolCallE2e(a: number, b: number) {
51
+ 'use workflow';
52
+ const agent = new WorkflowAgent({
53
+ model: mockSequenceModel([
54
+ {
55
+ type: 'tool-call',
56
+ toolName: 'addNumbers',
57
+ input: JSON.stringify({ a, b }),
58
+ },
59
+ { type: 'text', text: `The sum is ${a + b}` },
60
+ ]),
61
+ tools: {
62
+ addNumbers: {
63
+ description: 'Add two numbers',
64
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
65
+ execute: addNumbers,
66
+ },
67
+ },
68
+ instructions: 'You are a calculator assistant.',
69
+ });
70
+ const result = await agent.stream({
71
+ messages: [{ role: 'user', content: `Add ${a} and ${b}` }],
72
+ writable: getWritable(),
73
+ });
74
+ return {
75
+ stepCount: result.steps.length,
76
+ toolResults: result.toolResults,
77
+ lastStepText: result.steps[result.steps.length - 1]?.text,
78
+ };
79
+ }
80
+
81
+ export async function agentMultiStepE2e() {
82
+ 'use workflow';
83
+ const agent = new WorkflowAgent({
84
+ model: mockSequenceModel([
85
+ {
86
+ type: 'tool-call',
87
+ toolName: 'echoStep',
88
+ input: JSON.stringify({ step: 1 }),
89
+ },
90
+ {
91
+ type: 'tool-call',
92
+ toolName: 'echoStep',
93
+ input: JSON.stringify({ step: 2 }),
94
+ },
95
+ {
96
+ type: 'tool-call',
97
+ toolName: 'echoStep',
98
+ input: JSON.stringify({ step: 3 }),
99
+ },
100
+ { type: 'text', text: 'All done!' },
101
+ ]),
102
+ tools: {
103
+ echoStep: {
104
+ description: 'Echo the step number',
105
+ inputSchema: z.object({ step: z.number() }),
106
+ execute: echoStep,
107
+ },
108
+ },
109
+ });
110
+ const result = await agent.stream({
111
+ messages: [{ role: 'user', content: 'Run 3 steps' }],
112
+ writable: getWritable(),
113
+ });
114
+ return {
115
+ stepCount: result.steps.length,
116
+ lastStepText: result.steps[result.steps.length - 1]?.text,
117
+ };
118
+ }
119
+
120
+ export async function agentErrorToolE2e() {
121
+ 'use workflow';
122
+ const agent = new WorkflowAgent({
123
+ model: mockSequenceModel([
124
+ { type: 'tool-call', toolName: 'throwingTool', input: '{}' },
125
+ { type: 'text', text: 'Tool failed but I recovered.' },
126
+ ]),
127
+ tools: {
128
+ throwingTool: {
129
+ description: 'A tool that always fails',
130
+ inputSchema: z.object({}),
131
+ execute: throwingStep,
132
+ },
133
+ },
134
+ });
135
+ const result = await agent.stream({
136
+ messages: [{ role: 'user', content: 'Call the throwing tool' }],
137
+ writable: getWritable(),
138
+ });
139
+ return {
140
+ stepCount: result.steps.length,
141
+ lastStepText: result.steps[result.steps.length - 1]?.text,
142
+ };
143
+ }
144
+
145
+ // ============================================================================
146
+ // repairToolCall serialization
147
+ // ============================================================================
148
+
149
+ async function repairToolCall({
150
+ toolCall,
151
+ }: {
152
+ toolCall: { toolCallId: string; toolName: string; input: string };
153
+ }) {
154
+ 'use step';
155
+ // Fix the malformed JSON
156
+ return { ...toolCall, input: '{"a": 3, "b": 7}' };
157
+ }
158
+
159
+ export async function agentRepairToolCallE2e() {
160
+ 'use workflow';
161
+ const agent = new WorkflowAgent({
162
+ model: mockSequenceModel([
163
+ {
164
+ type: 'tool-call',
165
+ toolName: 'addNumbers',
166
+ // Malformed input: missing closing brace
167
+ input: '{"a": 3, "b": 7',
168
+ },
169
+ { type: 'text', text: 'The sum is 10' },
170
+ ]),
171
+ tools: {
172
+ addNumbers: {
173
+ description: 'Add two numbers',
174
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
175
+ execute: addNumbers,
176
+ },
177
+ },
178
+ });
179
+ const result = await agent.stream({
180
+ messages: [{ role: 'user', content: 'add 3 and 7' }],
181
+ writable: getWritable(),
182
+ repairToolCall: repairToolCall as any,
183
+ });
184
+ return {
185
+ stepCount: result.steps.length,
186
+ lastStepText: result.steps[result.steps.length - 1]?.text,
187
+ repaired: result.steps.length === 2, // If repair worked, we get 2 steps (tool call + text)
188
+ };
189
+ }
190
+
191
+ // ============================================================================
192
+ // Callback tests — onStepFinish
193
+ // ============================================================================
194
+
195
+ export async function agentOnStepFinishE2e() {
196
+ 'use workflow';
197
+ const callSources: string[] = [];
198
+ let capturedStepResult: any = null;
199
+ const agent = new WorkflowAgent({
200
+ model: mockTextModel('hello'),
201
+ onStepFinish: async () => {
202
+ callSources.push('constructor');
203
+ },
204
+ });
205
+ const result = await agent.stream({
206
+ messages: [{ role: 'user', content: 'test' }],
207
+ writable: getWritable(),
208
+ onStepFinish: async stepResult => {
209
+ callSources.push('method');
210
+ capturedStepResult = {
211
+ text: stepResult.text,
212
+ finishReason: stepResult.finishReason,
213
+ stepNumber: (stepResult as any).stepNumber,
214
+ };
215
+ },
216
+ });
217
+ return { callSources, capturedStepResult, stepCount: result.steps.length };
218
+ }
219
+
220
+ // ============================================================================
221
+ // Callback tests — onFinish
222
+ // ============================================================================
223
+
224
+ export async function agentOnFinishE2e() {
225
+ 'use workflow';
226
+ const callSources: string[] = [];
227
+ let capturedEvent: any = null;
228
+ const agent = new WorkflowAgent({
229
+ model: mockTextModel('hello from finish'),
230
+ onFinish: async () => {
231
+ callSources.push('constructor');
232
+ },
233
+ });
234
+ const result = await agent.stream({
235
+ messages: [{ role: 'user', content: 'test' }],
236
+ writable: getWritable(),
237
+ onFinish: async event => {
238
+ callSources.push('method');
239
+ capturedEvent = {
240
+ text: (event as any).text,
241
+ finishReason: (event as any).finishReason,
242
+ stepsLength: event.steps.length,
243
+ hasMessages: event.messages.length > 0,
244
+ hasTotalUsage: (event as any).totalUsage != null,
245
+ };
246
+ },
247
+ });
248
+ return { callSources, capturedEvent, stepCount: result.steps.length };
249
+ }
250
+
251
+ // ============================================================================
252
+ // Instructions test
253
+ // ============================================================================
254
+
255
+ export async function agentInstructionsStringE2e() {
256
+ 'use workflow';
257
+ const agent = new WorkflowAgent({
258
+ model: mockTextModel('ok'),
259
+ instructions: 'You are a pirate.',
260
+ });
261
+ const result = await agent.stream({
262
+ messages: [{ role: 'user', content: 'ahoy' }],
263
+ writable: getWritable(),
264
+ });
265
+ return {
266
+ stepCount: result.steps.length,
267
+ lastStepText: result.steps[result.steps.length - 1]?.text,
268
+ };
269
+ }
270
+
271
+ // ============================================================================
272
+ // Timeout test
273
+ // ============================================================================
274
+
275
+ export async function agentTimeoutE2e() {
276
+ 'use workflow';
277
+ const agent = new WorkflowAgent({
278
+ model: mockTextModel('fast response'),
279
+ });
280
+ const result = await agent.stream({
281
+ messages: [{ role: 'user', content: 'test' }],
282
+ writable: getWritable(),
283
+ timeout: 30000,
284
+ });
285
+ return {
286
+ stepCount: result.steps.length,
287
+ lastStepText: result.steps[result.steps.length - 1]?.text,
288
+ };
289
+ }
290
+
291
+ // ============================================================================
292
+ // GAP tests — experimental_onStart
293
+ // ============================================================================
294
+
295
+ export async function agentOnStartE2e() {
296
+ 'use workflow';
297
+ const callSources: string[] = [];
298
+ const agent = new WorkflowAgent({
299
+ model: mockTextModel('hello'),
300
+ experimental_onStart: async () => {
301
+ callSources.push('constructor');
302
+ },
303
+ } as any);
304
+ await agent.stream({
305
+ messages: [{ role: 'user', content: 'test' }],
306
+ writable: getWritable(),
307
+ experimental_onStart: async () => {
308
+ callSources.push('method');
309
+ },
310
+ } as any);
311
+ return { callSources };
312
+ }
313
+
314
+ // ============================================================================
315
+ // GAP tests — experimental_onStepStart
316
+ // ============================================================================
317
+
318
+ export async function agentOnStepStartE2e() {
319
+ 'use workflow';
320
+ const callSources: string[] = [];
321
+ const agent = new WorkflowAgent({
322
+ model: mockTextModel('hello'),
323
+ experimental_onStepStart: async () => {
324
+ callSources.push('constructor');
325
+ },
326
+ } as any);
327
+ await agent.stream({
328
+ messages: [{ role: 'user', content: 'test' }],
329
+ writable: getWritable(),
330
+ experimental_onStepStart: async () => {
331
+ callSources.push('method');
332
+ },
333
+ } as any);
334
+ return { callSources };
335
+ }
336
+
337
+ // ============================================================================
338
+ // GAP tests — onToolExecutionStart
339
+ // ============================================================================
340
+
341
+ export async function agentonToolExecutionStartE2e() {
342
+ 'use workflow';
343
+ const calls: string[] = [];
344
+ const agent = new WorkflowAgent({
345
+ model: mockSequenceModel([
346
+ {
347
+ type: 'tool-call',
348
+ toolName: 'echoStep',
349
+ input: JSON.stringify({ step: 1 }),
350
+ },
351
+ { type: 'text', text: 'done' },
352
+ ]),
353
+ tools: {
354
+ echoStep: {
355
+ description: 'Echo',
356
+ inputSchema: z.object({ step: z.number() }),
357
+ execute: echoStep,
358
+ },
359
+ },
360
+ onToolExecutionStart: async () => {
361
+ calls.push('constructor');
362
+ },
363
+ } as any);
364
+ await agent.stream({
365
+ messages: [{ role: 'user', content: 'test' }],
366
+ writable: getWritable(),
367
+ onToolExecutionStart: async () => {
368
+ calls.push('method');
369
+ },
370
+ } as any);
371
+ return { calls };
372
+ }
373
+
374
+ // ============================================================================
375
+ // GAP tests — onToolExecutionEnd
376
+ // ============================================================================
377
+
378
+ export async function agentonToolExecutionEndE2e() {
379
+ 'use workflow';
380
+ const calls: string[] = [];
381
+ let capturedEvent: any = null;
382
+ const agent = new WorkflowAgent({
383
+ model: mockSequenceModel([
384
+ {
385
+ type: 'tool-call',
386
+ toolName: 'addNumbers',
387
+ input: JSON.stringify({ a: 1, b: 2 }),
388
+ },
389
+ { type: 'text', text: 'done' },
390
+ ]),
391
+ tools: {
392
+ addNumbers: {
393
+ description: 'Add two numbers',
394
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
395
+ execute: addNumbers,
396
+ },
397
+ },
398
+ onToolExecutionEnd: async () => {
399
+ calls.push('constructor');
400
+ },
401
+ } as any);
402
+ await agent.stream({
403
+ messages: [{ role: 'user', content: 'test' }],
404
+ writable: getWritable(),
405
+ onToolExecutionEnd: async (event: any) => {
406
+ calls.push('method');
407
+ capturedEvent = {
408
+ toolName: event?.toolCall?.toolName,
409
+ success: event?.success,
410
+ output: event?.output,
411
+ };
412
+ },
413
+ } as any);
414
+ return { calls, capturedEvent };
415
+ }
416
+
417
+ // ============================================================================
418
+ // GAP tests — prepareCall
419
+ // ============================================================================
420
+
421
+ export async function agentPrepareCallE2e() {
422
+ 'use workflow';
423
+ const agent = new WorkflowAgent({
424
+ model: mockTextModel('ok'),
425
+ prepareCall: ({ options, ...rest }: any) => ({
426
+ ...rest,
427
+ providerOptions: { test: { value: options?.value } },
428
+ }),
429
+ } as any);
430
+ const result = await agent.stream({
431
+ messages: [{ role: 'user', content: 'test' }],
432
+ writable: getWritable(),
433
+ });
434
+ return {
435
+ stepCount: result.steps.length,
436
+ lastStepText: result.steps[result.steps.length - 1]?.text,
437
+ };
438
+ }
439
+
440
+ // ============================================================================
441
+ // GAP tests — tool approval (needsApproval)
442
+ // ============================================================================
443
+
444
+ export async function agentToolApprovalE2e() {
445
+ 'use workflow';
446
+ const agent = new WorkflowAgent({
447
+ model: mockSequenceModel([
448
+ {
449
+ type: 'tool-call',
450
+ toolName: 'riskyTool',
451
+ input: JSON.stringify({ action: 'delete' }),
452
+ },
453
+ { type: 'text', text: 'done' },
454
+ ]),
455
+ tools: {
456
+ riskyTool: {
457
+ description: 'A dangerous tool that needs approval',
458
+ inputSchema: z.object({ action: z.string() }),
459
+ execute: echoStep as any,
460
+ needsApproval: true,
461
+ } as any,
462
+ },
463
+ });
464
+ const result = await agent.stream({
465
+ messages: [{ role: 'user', content: 'do something risky' }],
466
+ writable: getWritable(),
467
+ });
468
+ return {
469
+ toolCallsCount: result.toolCalls.length,
470
+ toolResultsCount: result.toolResults.length,
471
+ stepCount: result.steps.length,
472
+ firstToolCallName: result.toolCalls[0]?.toolName,
473
+ };
474
+ }
475
+
476
+ // ============================================================================
477
+ // Tool with input schema (tests serialization across step boundary)
478
+ // ============================================================================
479
+
480
+ export async function agentToolInputSchemaE2e(a: number, b: number) {
481
+ 'use workflow';
482
+ const agent = new WorkflowAgent({
483
+ model: mockSequenceModel([
484
+ {
485
+ type: 'tool-call',
486
+ toolName: 'addNumbers',
487
+ input: JSON.stringify({ a, b }),
488
+ },
489
+ { type: 'text', text: `The sum is ${a + b}` },
490
+ ]),
491
+ tools: {
492
+ addNumbers: tool({
493
+ description: 'Add two numbers',
494
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
495
+ execute: async (input: { a: number; b: number }) => input.a + input.b,
496
+ }),
497
+ },
498
+ instructions: 'You are a calculator.',
499
+ });
500
+ const result = await agent.stream({
501
+ messages: [{ role: 'user', content: `Add ${a} and ${b}` }],
502
+ writable: getWritable(),
503
+ });
504
+ return {
505
+ stepCount: result.steps.length,
506
+ lastStepText: result.steps[result.steps.length - 1]?.text,
507
+ };
508
+ }
509
+
510
+ // ============================================================================
511
+ // runtimeContext + toolsContext (end-to-end)
512
+ // ============================================================================
513
+
514
+ /**
515
+ * Demonstrates the full context flow:
516
+ *
517
+ * - `runtimeContext` holds shared agent state (`tenantId`, `requestId`).
518
+ * `prepareStep` reads it and tags it with the current step number;
519
+ * `onFinish` receives the final value.
520
+ * - `toolsContext` holds per-tool, schema-validated context. The
521
+ * `lookupCustomer` tool declares `contextSchema`, so its entry is
522
+ * validated and the tool's `execute` only sees its own context.
523
+ */
524
+ export async function agentRuntimeAndToolsContextE2e() {
525
+ 'use workflow';
526
+
527
+ let onFinishRuntimeContext: Record<string, unknown> | undefined;
528
+ let onFinishToolsContext: Record<string, unknown> | undefined;
529
+ let toolReceivedContext: unknown;
530
+
531
+ const agent = new WorkflowAgent({
532
+ model: mockSequenceModel([
533
+ {
534
+ type: 'tool-call',
535
+ toolName: 'lookupCustomer',
536
+ input: JSON.stringify({ customerId: 'cust_123' }),
537
+ },
538
+ { type: 'text', text: 'Customer cust_123 is eligible.' },
539
+ ]),
540
+ tools: {
541
+ lookupCustomer: tool({
542
+ description: 'Look up customer account details.',
543
+ inputSchema: z.object({ customerId: z.string() }),
544
+ contextSchema: z.object({
545
+ apiKey: z.string(),
546
+ region: z.enum(['us', 'eu']),
547
+ }),
548
+ execute: async (input, { context }) => {
549
+ toolReceivedContext = context;
550
+ return { customerId: input.customerId, eligible: true };
551
+ },
552
+ }),
553
+ },
554
+ instructions: 'You look up customers.',
555
+ runtimeContext: {
556
+ tenantId: 'tenant_123',
557
+ requestId: 'req_abc',
558
+ },
559
+ toolsContext: {
560
+ lookupCustomer: {
561
+ apiKey: 'sk-test-key',
562
+ region: 'us',
563
+ },
564
+ },
565
+ prepareStep: ({ stepNumber, runtimeContext }) => ({
566
+ runtimeContext: { ...runtimeContext, lastStep: stepNumber },
567
+ }),
568
+ onFinish: ({ runtimeContext, toolsContext }) => {
569
+ onFinishRuntimeContext = runtimeContext;
570
+ onFinishToolsContext = toolsContext;
571
+ },
572
+ });
573
+
574
+ const result = await agent.stream({
575
+ messages: [
576
+ { role: 'user', content: 'Is customer cust_123 eligible for support?' },
577
+ ],
578
+ writable: getWritable(),
579
+ });
580
+
581
+ return {
582
+ stepCount: result.steps.length,
583
+ lastStepText: result.steps[result.steps.length - 1]?.text,
584
+ toolReceivedContext,
585
+ onFinishRuntimeContext,
586
+ onFinishToolsContext,
587
+ };
588
+ }
589
+
590
+ export async function agentSandboxE2e() {
591
+ 'use workflow';
592
+
593
+ let constructorSandboxRanCommand = 'not-run';
594
+ let stepSandboxRanCommand = 'not-run';
595
+ let firstPrepareStepSawConstructorSandbox = false;
596
+ let secondPrepareStepSawConstructorSandbox = false;
597
+ let prepareStepSawStepSandbox = false;
598
+
599
+ // A live sandbox session passed through `experimental_sandbox`. The tool
600
+ // `execute` is inline (not a `'use step'`) so the handle never crosses a
601
+ // step boundary — matching the single-process use of `experimental_sandbox`.
602
+ const constructorSandbox = createTestSandbox({
603
+ run: async ({ command }) => {
604
+ constructorSandboxRanCommand = command;
605
+ return {
606
+ exitCode: 0,
607
+ stdout: `constructor ran: ${command}`,
608
+ stderr: '',
609
+ };
610
+ },
611
+ });
612
+ const stepSandbox = createTestSandbox({
613
+ run: async ({ command }) => {
614
+ stepSandboxRanCommand = command;
615
+ return { exitCode: 0, stdout: `ran: ${command}`, stderr: '' };
616
+ },
617
+ });
618
+
619
+ const agent = new WorkflowAgent({
620
+ model: mockSequenceModel([
621
+ {
622
+ type: 'tool-call',
623
+ toolName: 'runShell',
624
+ input: JSON.stringify({ command: 'echo hello' }),
625
+ },
626
+ { type: 'text', text: 'Command executed.' },
627
+ ]),
628
+ tools: {
629
+ runShell: tool({
630
+ description: 'Run a shell command in the sandbox.',
631
+ inputSchema: z.object({ command: z.string() }),
632
+ execute: async ({ command }, { experimental_sandbox }) => {
633
+ if (experimental_sandbox == null) {
634
+ throw new Error('Sandbox is not available');
635
+ }
636
+ return experimental_sandbox.run({ command });
637
+ },
638
+ }),
639
+ },
640
+ instructions: 'You run shell commands in a sandbox.',
641
+ experimental_sandbox: constructorSandbox,
642
+ prepareStep: ({ stepNumber, experimental_sandbox }) => {
643
+ if (stepNumber === 0) {
644
+ firstPrepareStepSawConstructorSandbox =
645
+ experimental_sandbox === constructorSandbox;
646
+ return { experimental_sandbox: stepSandbox };
647
+ }
648
+
649
+ if (experimental_sandbox === stepSandbox) {
650
+ prepareStepSawStepSandbox = true;
651
+ }
652
+ secondPrepareStepSawConstructorSandbox =
653
+ experimental_sandbox === constructorSandbox;
654
+ return {};
655
+ },
656
+ });
657
+
658
+ const result = await agent.stream({
659
+ messages: [{ role: 'user', content: 'Run echo hello' }],
660
+ writable: getWritable(),
661
+ });
662
+
663
+ return {
664
+ stepCount: result.steps.length,
665
+ lastStepText: result.steps[result.steps.length - 1]?.text,
666
+ toolResults: result.toolResults,
667
+ constructorSandboxRanCommand,
668
+ stepSandboxRanCommand,
669
+ firstPrepareStepSawConstructorSandbox,
670
+ secondPrepareStepSawConstructorSandbox,
671
+ prepareStepSawStepSandbox,
672
+ };
673
+ }
@@ -0,0 +1,19 @@
1
+ export async function calculateWorkflow(a: number, b: number) {
2
+ 'use workflow';
3
+
4
+ const sum = await add(a, b);
5
+ const product = await multiply(a, b);
6
+ const combined = await add(sum, product);
7
+
8
+ return { sum, product, combined };
9
+ }
10
+
11
+ async function add(a: number, b: number): Promise<number> {
12
+ 'use step';
13
+ return a + b;
14
+ }
15
+
16
+ async function multiply(a: number, b: number): Promise<number> {
17
+ 'use step';
18
+ return a * b;
19
+ }