@positronic/core 0.0.1 → 0.0.3

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.
Files changed (79) hide show
  1. package/CLAUDE.md +141 -0
  2. package/dist/src/adapters/types.js +1 -16
  3. package/dist/src/clients/types.js +4 -1
  4. package/dist/src/dsl/brain-runner.js +487 -0
  5. package/dist/src/dsl/brain-runner.test.js +733 -0
  6. package/dist/src/dsl/brain.js +1128 -0
  7. package/dist/src/dsl/brain.test.js +4225 -0
  8. package/dist/src/dsl/constants.js +6 -6
  9. package/dist/src/dsl/json-patch.js +37 -9
  10. package/dist/src/index.js +11 -10
  11. package/dist/src/resources/resources.js +371 -0
  12. package/dist/src/test-utils.js +474 -0
  13. package/dist/src/testing.js +3 -0
  14. package/dist/types/adapters/types.d.ts +3 -8
  15. package/dist/types/adapters/types.d.ts.map +1 -1
  16. package/dist/types/clients/types.d.ts +46 -6
  17. package/dist/types/clients/types.d.ts.map +1 -1
  18. package/dist/types/dsl/brain-runner.d.ts +24 -0
  19. package/dist/types/dsl/brain-runner.d.ts.map +1 -0
  20. package/dist/types/dsl/brain.d.ts +136 -0
  21. package/dist/types/dsl/brain.d.ts.map +1 -0
  22. package/dist/types/dsl/constants.d.ts +5 -5
  23. package/dist/types/dsl/constants.d.ts.map +1 -1
  24. package/dist/types/dsl/json-patch.d.ts +2 -1
  25. package/dist/types/dsl/json-patch.d.ts.map +1 -1
  26. package/dist/types/index.d.ts +13 -11
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/dist/types/resources/resource-loader.d.ts +6 -0
  29. package/dist/types/resources/resource-loader.d.ts.map +1 -0
  30. package/dist/types/resources/resources.d.ts +23 -0
  31. package/dist/types/resources/resources.d.ts.map +1 -0
  32. package/dist/types/test-utils.d.ts +94 -0
  33. package/dist/types/test-utils.d.ts.map +1 -0
  34. package/dist/types/testing.d.ts +2 -0
  35. package/dist/types/testing.d.ts.map +1 -0
  36. package/docs/core-testing-guide.md +289 -0
  37. package/package.json +26 -7
  38. package/src/adapters/types.ts +3 -22
  39. package/src/clients/types.ts +50 -10
  40. package/src/dsl/brain-runner.test.ts +384 -0
  41. package/src/dsl/brain-runner.ts +111 -0
  42. package/src/dsl/brain.test.ts +1981 -0
  43. package/src/dsl/brain.ts +740 -0
  44. package/src/dsl/constants.ts +6 -6
  45. package/src/dsl/json-patch.ts +24 -9
  46. package/src/dsl/types.ts +1 -1
  47. package/src/index.ts +30 -16
  48. package/src/resources/resource-loader.ts +8 -0
  49. package/src/resources/resources.ts +267 -0
  50. package/src/test-utils.ts +254 -0
  51. package/test/resources.test.ts +248 -0
  52. package/tsconfig.json +2 -2
  53. package/.swcrc +0 -31
  54. package/dist/src/dsl/extensions.js +0 -19
  55. package/dist/src/dsl/workflow-runner.js +0 -93
  56. package/dist/src/dsl/workflow.js +0 -308
  57. package/dist/src/file-stores/local-file-store.js +0 -12
  58. package/dist/src/utils/temp-files.js +0 -27
  59. package/dist/types/dsl/extensions.d.ts +0 -18
  60. package/dist/types/dsl/extensions.d.ts.map +0 -1
  61. package/dist/types/dsl/workflow-runner.d.ts +0 -28
  62. package/dist/types/dsl/workflow-runner.d.ts.map +0 -1
  63. package/dist/types/dsl/workflow.d.ts +0 -118
  64. package/dist/types/dsl/workflow.d.ts.map +0 -1
  65. package/dist/types/file-stores/local-file-store.d.ts +0 -7
  66. package/dist/types/file-stores/local-file-store.d.ts.map +0 -1
  67. package/dist/types/file-stores/types.d.ts +0 -4
  68. package/dist/types/file-stores/types.d.ts.map +0 -1
  69. package/dist/types/utils/temp-files.d.ts +0 -12
  70. package/dist/types/utils/temp-files.d.ts.map +0 -1
  71. package/src/dsl/extensions.ts +0 -58
  72. package/src/dsl/workflow-runner.test.ts +0 -203
  73. package/src/dsl/workflow-runner.ts +0 -146
  74. package/src/dsl/workflow.test.ts +0 -1435
  75. package/src/dsl/workflow.ts +0 -554
  76. package/src/file-stores/local-file-store.ts +0 -11
  77. package/src/file-stores/types.ts +0 -3
  78. package/src/utils/temp-files.ts +0 -46
  79. /package/dist/src/{file-stores/types.js → resources/resource-loader.js} +0 -0
@@ -1,1435 +0,0 @@
1
- import { WORKFLOW_EVENTS, STATUS } from './constants';
2
- import { applyPatches} from './json-patch';
3
- import { State } from './types';
4
- import { workflow, type WorkflowEvent, type WorkflowErrorEvent} from './workflow';
5
- import { z } from 'zod';
6
- import { nextStep } from '../../../../test-utils';
7
- import { FileStore } from '../file-stores/types';
8
-
9
- class TestFileStore implements FileStore {
10
- private files: Map<string, string> = new Map();
11
-
12
- setFile(path: string, content: string) {
13
- this.files.set(path, content);
14
- }
15
-
16
- async readFile(path: string): Promise<string> {
17
- const content = this.files.get(path);
18
- if (content === undefined) {
19
- throw new Error(`File not found: ${path}`);
20
- }
21
- return content;
22
- }
23
- }
24
-
25
- const testFileStore = new TestFileStore();
26
-
27
- type AssertEquals<T, U> =
28
- 0 extends (1 & T) ? false : // fails if T is any
29
- 0 extends (1 & U) ? false : // fails if U is any
30
- [T] extends [U] ? [U] extends [T] ? true : false : false;
31
-
32
- // Mock PromptClient for testing
33
- const mockClient = {
34
- execute: jest.fn()
35
- };
36
-
37
- describe('workflow creation', () => {
38
- it('should create a workflow with steps and run through them', async () => {
39
- const testWorkflow = workflow('test workflow')
40
- .step(
41
- "First step",
42
- () => {
43
- return { count: 1 };
44
- }
45
- )
46
- .step(
47
- "Second step",
48
- ({ state }) => ({ ...state, doubled: state.count * 2 })
49
- );
50
-
51
- const workflowRun = testWorkflow.run({ client: mockClient, fileStore: testFileStore });
52
-
53
- // Check start event
54
- const startResult = await workflowRun.next();
55
- expect(startResult.value).toEqual(expect.objectContaining({
56
- type: WORKFLOW_EVENTS.START,
57
- status: STATUS.RUNNING,
58
- workflowTitle: 'test workflow',
59
- workflowDescription: undefined
60
- }));
61
-
62
- // Skip initial step status event
63
- await nextStep(workflowRun);
64
-
65
- // Check first step start
66
- const firstStepStartResult = await nextStep(workflowRun);
67
- expect(firstStepStartResult).toEqual(expect.objectContaining({
68
- type: WORKFLOW_EVENTS.STEP_START,
69
- status: STATUS.RUNNING,
70
- stepTitle: 'First step',
71
- stepId: expect.any(String)
72
- }));
73
-
74
- // Check first step completion
75
- const firstStepResult = await nextStep(workflowRun);
76
- expect(firstStepResult).toEqual(expect.objectContaining({
77
- type: WORKFLOW_EVENTS.STEP_COMPLETE,
78
- status: STATUS.RUNNING,
79
- stepTitle: 'First step',
80
- stepId: expect.any(String),
81
- patch: [{
82
- op: 'add',
83
- path: '/count',
84
- value: 1
85
- }]
86
- }));
87
-
88
- // Step Status Event
89
- await nextStep(workflowRun);
90
-
91
- // Check second step start
92
- const secondStepStartResult = await nextStep(workflowRun);
93
- expect(secondStepStartResult).toEqual(expect.objectContaining({
94
- type: WORKFLOW_EVENTS.STEP_START,
95
- status: STATUS.RUNNING,
96
- stepTitle: 'Second step',
97
- stepId: expect.any(String)
98
- }));
99
-
100
- // Check second step completion
101
- const secondStepResult = await nextStep(workflowRun);
102
- expect(secondStepResult).toEqual(expect.objectContaining({
103
- type: WORKFLOW_EVENTS.STEP_COMPLETE,
104
- stepTitle: 'Second step',
105
- stepId: expect.any(String),
106
- patch: [{
107
- op: 'add',
108
- path: '/doubled',
109
- value: 2
110
- }]
111
- }));
112
-
113
- // Step Status Event
114
- const stepStatusResult = await nextStep(workflowRun);
115
- expect(stepStatusResult).toEqual(expect.objectContaining({
116
- type: WORKFLOW_EVENTS.STEP_STATUS,
117
- steps: [
118
- expect.objectContaining({
119
- title: 'First step',
120
- status: STATUS.COMPLETE,
121
- id: expect.any(String)
122
- }),
123
- expect.objectContaining({
124
- title: 'Second step',
125
- status: STATUS.COMPLETE,
126
- id: expect.any(String)
127
- })
128
- ]
129
- }));
130
-
131
- // Check workflow completion
132
- const completeResult = await nextStep(workflowRun);
133
- expect(completeResult).toEqual(expect.objectContaining({
134
- type: WORKFLOW_EVENTS.COMPLETE,
135
- status: STATUS.COMPLETE,
136
- workflowTitle: 'test workflow',
137
- workflowDescription: undefined,
138
- }));
139
- });
140
-
141
- it('should create a workflow with a name and description when passed an object', async () => {
142
- const testWorkflow = workflow({
143
- title: 'my named workflow',
144
- description: 'some description'
145
- });
146
-
147
- const workflowRun = testWorkflow.run({ client: mockClient, fileStore: testFileStore });
148
- const startResult = await workflowRun.next();
149
- expect(startResult.value).toEqual(expect.objectContaining({
150
- type: WORKFLOW_EVENTS.START,
151
- status: STATUS.RUNNING,
152
- workflowTitle: 'my named workflow',
153
- workflowDescription: 'some description',
154
- options: {}
155
- }));
156
- });
157
-
158
- it('should create a workflow with just a name when passed a string', async () => {
159
- const testWorkflow = workflow('simple workflow');
160
- const workflowRun = testWorkflow.run({ client: mockClient, fileStore: testFileStore });
161
- const startResult = await workflowRun.next();
162
- expect(startResult.value).toEqual(expect.objectContaining({
163
- type: WORKFLOW_EVENTS.START,
164
- status: STATUS.RUNNING,
165
- workflowTitle: 'simple workflow',
166
- workflowDescription: undefined,
167
- options: {}
168
- }));
169
- });
170
-
171
- it('should allow overriding client per step', async () => {
172
- const overrideClient = {
173
- execute: jest.fn().mockResolvedValue({ override: true })
174
- };
175
-
176
- // Make sure that for the default prompt the default client returns a known value.
177
- mockClient.execute.mockResolvedValueOnce({ override: false });
178
-
179
- const testWorkflow = workflow('Client Override Test')
180
- .prompt(
181
- "Use default client",
182
- {
183
- template: () => "prompt1",
184
- responseModel: {
185
- schema: z.object({ override: z.boolean() }),
186
- name: 'overrideResponse'
187
- }
188
- }
189
- )
190
- .prompt(
191
- "Use override client",
192
- {
193
- template: () => "prompt2",
194
- responseModel: {
195
- schema: z.object({ override: z.boolean() }),
196
- name: 'overrideResponse'
197
- },
198
- client: overrideClient
199
- }
200
- );
201
-
202
- // Run the workflow and capture all events
203
- const events = [];
204
- let finalState = {};
205
- for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
206
- events.push(event);
207
- if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
208
- finalState = applyPatches(finalState, [event.patch]);
209
- }
210
- }
211
-
212
- // Final state should include both responses
213
- expect(finalState).toEqual({
214
- overrideResponse: { override: true }
215
- });
216
-
217
- // Verify that each client was used correctly based on the supplied prompt configuration.
218
- expect(mockClient.execute).toHaveBeenCalledWith("prompt1", expect.any(Object));
219
- expect(overrideClient.execute).toHaveBeenCalledWith("prompt2", expect.any(Object));
220
- });
221
- });
222
-
223
- describe('error handling', () => {
224
- it('should handle errors in actions and maintain correct status', async () => {
225
- const errorWorkflow = workflow('Error Workflow')
226
- // Step 1: Normal step
227
- .step("First step", () => ({
228
- value: 1
229
- }))
230
- // Step 2: Error step
231
- .step("Error step", () => {
232
- if (true) {
233
- throw new Error('Test error');
234
- }
235
- return {
236
- value: 1
237
- };
238
- })
239
- // Step 3: Should never execute
240
- .step("Never reached", ({ state }) => ({
241
- value: state.value + 1
242
- }));
243
-
244
- let errorEvent, finalStepStatusEvent;
245
- try {
246
- for await (const event of errorWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
247
- if (event.type === WORKFLOW_EVENTS.ERROR) {
248
- errorEvent = event;
249
- }
250
- if (event.type === WORKFLOW_EVENTS.STEP_STATUS) {
251
- finalStepStatusEvent = event;
252
- }
253
- }
254
- } catch (error) {
255
- // Error is expected to be thrown
256
- }
257
-
258
- // Verify final state
259
- expect(errorEvent?.status).toBe(STATUS.ERROR);
260
- expect(errorEvent?.error?.message).toBe('Test error');
261
-
262
- // Verify steps status
263
- if (!finalStepStatusEvent?.steps) {
264
- throw new Error('Steps not found');
265
- }
266
- expect(finalStepStatusEvent.steps[0].status).toBe(STATUS.COMPLETE);
267
- expect(finalStepStatusEvent.steps[1].status).toBe(STATUS.ERROR);
268
- expect(finalStepStatusEvent.steps[2].status).toBe(STATUS.PENDING);
269
-
270
- // Verify error event structure
271
- expect(errorEvent).toEqual(expect.objectContaining({
272
- type: WORKFLOW_EVENTS.ERROR,
273
- status: STATUS.ERROR,
274
- workflowTitle: 'Error Workflow',
275
- error: expect.objectContaining({
276
- name: expect.any(String),
277
- message: expect.any(String)
278
- }),
279
- }));
280
- });
281
-
282
- it('should handle errors in nested workflows and propagate them up', async () => {
283
- // Create an inner workflow that will throw an error
284
- const innerWorkflow = workflow<{}, { inner?: boolean, value?: number }>('Failing Inner Workflow')
285
- .step(
286
- "Throw error",
287
- (): { value: number } => {
288
- throw new Error('Inner workflow error');
289
- }
290
- );
291
-
292
- // Create outer workflow that uses the failing inner workflow
293
- const outerWorkflow = workflow('Outer Workflow')
294
- .step(
295
- "First step",
296
- () => ({ step: "first" })
297
- )
298
- .workflow(
299
- "Run inner workflow",
300
- innerWorkflow,
301
- ({ state, workflowState }) => ({
302
- ...state,
303
- step: "second",
304
- innerResult: workflowState.value
305
- }),
306
- () => ({ value: 5 })
307
- );
308
-
309
- const events: WorkflowEvent<any>[] = [];
310
- let error: Error | undefined;
311
- let mainWorkflowId: string | undefined;
312
-
313
- try {
314
- for await (const event of outerWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
315
- events.push(event);
316
- if (event.type === WORKFLOW_EVENTS.START && !mainWorkflowId) {
317
- mainWorkflowId = event.workflowRunId;
318
- }
319
- }
320
- } catch (e) {
321
- error = e as Error;
322
- }
323
-
324
- // Verify error was thrown
325
- expect(error?.message).toBe('Inner workflow error');
326
-
327
- // Verify event sequence including error
328
- expect(events).toEqual([
329
- expect.objectContaining({
330
- type: WORKFLOW_EVENTS.START,
331
- workflowTitle: 'Outer Workflow',
332
- status: STATUS.RUNNING,
333
- workflowRunId: mainWorkflowId
334
- }),
335
- expect.objectContaining({
336
- type: WORKFLOW_EVENTS.STEP_STATUS,
337
- steps: expect.any(Array)
338
- }),
339
- expect.objectContaining({
340
- type: WORKFLOW_EVENTS.STEP_START,
341
- status: STATUS.RUNNING,
342
- stepTitle: 'First step'
343
- }),
344
- expect.objectContaining({
345
- type: WORKFLOW_EVENTS.STEP_COMPLETE,
346
- status: STATUS.RUNNING,
347
- stepTitle: 'First step'
348
- }),
349
- expect.objectContaining({
350
- type: WORKFLOW_EVENTS.STEP_STATUS,
351
- steps: expect.any(Array)
352
- }),
353
- expect.objectContaining({
354
- type: WORKFLOW_EVENTS.STEP_START,
355
- status: STATUS.RUNNING,
356
- stepTitle: 'Run inner workflow'
357
- }),
358
- expect.objectContaining({
359
- type: WORKFLOW_EVENTS.START,
360
- workflowTitle: 'Failing Inner Workflow',
361
- status: STATUS.RUNNING,
362
- }),
363
- expect.objectContaining({
364
- type: WORKFLOW_EVENTS.STEP_STATUS,
365
- steps: expect.any(Array)
366
- }),
367
- expect.objectContaining({
368
- type: WORKFLOW_EVENTS.STEP_START,
369
- status: STATUS.RUNNING,
370
- stepTitle: 'Throw error'
371
- }),
372
- expect.objectContaining({
373
- type: WORKFLOW_EVENTS.ERROR,
374
- workflowTitle: 'Failing Inner Workflow',
375
- status: STATUS.ERROR,
376
- error: expect.objectContaining({
377
- name: expect.any(String),
378
- message: expect.any(String)
379
- }),
380
- }),
381
- expect.objectContaining({
382
- type: WORKFLOW_EVENTS.STEP_STATUS,
383
- steps: expect.arrayContaining([
384
- expect.objectContaining({
385
- title: 'Throw error',
386
- status: STATUS.ERROR
387
- })
388
- ])
389
- }),
390
- expect.objectContaining({
391
- type: WORKFLOW_EVENTS.ERROR,
392
- workflowTitle: 'Outer Workflow',
393
- status: STATUS.ERROR,
394
- error: expect.objectContaining({
395
- name: expect.any(String),
396
- message: expect.any(String)
397
- })
398
- }),
399
- expect.objectContaining({
400
- type: WORKFLOW_EVENTS.STEP_STATUS,
401
- steps: expect.arrayContaining([
402
- expect.objectContaining({
403
- title: 'Run inner workflow',
404
- status: STATUS.ERROR
405
- })
406
- ])
407
- })
408
- ]);
409
-
410
- // Find inner and outer error events by workflowRunId
411
- const innerErrorEvent = events.find(e =>
412
- e.type === WORKFLOW_EVENTS.ERROR &&
413
- e.workflowRunId !== mainWorkflowId
414
- ) as WorkflowErrorEvent<any>;
415
-
416
- const outerErrorEvent = events.find(e =>
417
- e.type === WORKFLOW_EVENTS.ERROR &&
418
- e.workflowRunId === mainWorkflowId
419
- ) as WorkflowErrorEvent<any>;
420
-
421
- expect(innerErrorEvent.error).toEqual(expect.objectContaining({
422
- message: 'Inner workflow error'
423
- }));
424
- expect(outerErrorEvent.error).toEqual(expect.objectContaining({
425
- message: 'Inner workflow error'
426
- }));
427
- });
428
- });
429
-
430
- describe('step creation', () => {
431
- it('should create a step that updates state', async () => {
432
- const testWorkflow = workflow('Simple Workflow')
433
- .step("Simple step", ({ state }) => ({
434
- ...state,
435
- count: 1,
436
- message: 'Count is now 1'
437
- }));
438
-
439
- const events = [];
440
- let finalState = {};
441
- for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
442
- events.push(event);
443
- if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
444
- finalState = applyPatches(finalState, event.patch);
445
- }
446
- }
447
-
448
- // Skip checking events[0] (workflow:start)
449
- // Skip checking events[1] (step:status)
450
-
451
- // Verify the step start event
452
- expect(events[2]).toEqual(expect.objectContaining({
453
- type: WORKFLOW_EVENTS.STEP_START,
454
- status: STATUS.RUNNING,
455
- stepTitle: 'Simple step',
456
- stepId: expect.any(String),
457
- options: {}
458
- }));
459
-
460
- // Verify the step complete event
461
- expect(events[3]).toEqual(expect.objectContaining({
462
- type: WORKFLOW_EVENTS.STEP_COMPLETE,
463
- status: STATUS.RUNNING,
464
- stepTitle: 'Simple step',
465
- stepId: expect.any(String),
466
- patch: [{
467
- op: 'add',
468
- path: '/count',
469
- value: 1
470
- }, {
471
- op: 'add',
472
- path: '/message',
473
- value: 'Count is now 1'
474
- }],
475
- options: {}
476
- }));
477
-
478
- expect(events[4]).toEqual(expect.objectContaining({
479
- type: WORKFLOW_EVENTS.STEP_STATUS,
480
- steps: [
481
- expect.objectContaining({ title: 'Simple step', status: STATUS.COMPLETE, id: expect.any(String) })
482
- ],
483
- options: {}
484
- }));
485
-
486
- // Verify the workflow complete event
487
- expect(events[5]).toEqual(expect.objectContaining({
488
- type: WORKFLOW_EVENTS.COMPLETE,
489
- status: STATUS.COMPLETE,
490
- workflowTitle: 'Simple Workflow',
491
- options: {}
492
- }));
493
-
494
- // Verify the final state
495
- expect(finalState).toEqual({
496
- count: 1,
497
- message: 'Count is now 1'
498
- });
499
- });
500
-
501
- it('should maintain immutable results between steps', async () => {
502
- const testWorkflow = workflow('Immutable Steps Workflow')
503
- .step("First step", () => ({
504
- value: 1
505
- }))
506
- .step("Second step", ({ state }) => {
507
- // Attempt to modify previous step's state
508
- state.value = 99;
509
- return {
510
- value: 2
511
- };
512
- });
513
-
514
- let finalState = {};
515
- const patches = [];
516
- for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
517
- if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
518
- patches.push(...event.patch);
519
- }
520
- }
521
-
522
- // Apply all patches to the initial state
523
- finalState = applyPatches(finalState, patches);
524
-
525
- // Verify the final state
526
- expect(finalState).toEqual({ value: 2 });
527
- });
528
- });
529
-
530
- describe('workflow resumption', () => {
531
- const mockClient = {
532
- execute: jest.fn()
533
- };
534
-
535
- it('should resume workflow from the correct step when given initialCompletedSteps', async () => {
536
- const executedSteps: string[] = [];
537
- const threeStepWorkflow = workflow('Three Step Workflow')
538
- .step("Step 1", ({ state }) => {
539
- executedSteps.push("Step 1");
540
- return { ...state, value: 2 };
541
- })
542
- .step("Step 2", ({ state }) => {
543
- executedSteps.push("Step 2");
544
- return { ...state, value: state.value + 10 };
545
- })
546
- .step("Step 3", ({ state }) => {
547
- executedSteps.push("Step 3");
548
- return { ...state, value: state.value * 3 };
549
- });
550
-
551
- // First run to get the first step completed with initial state
552
- let initialCompletedSteps;
553
- const initialState = { initialValue: true };
554
- let firstStepState: State = initialState;
555
-
556
- // Run workflow until we get the first step completed
557
- for await (const event of threeStepWorkflow.run({
558
- client: mockClient,
559
- initialState,
560
- fileStore: testFileStore
561
- })) {
562
- if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
563
- firstStepState = applyPatches(firstStepState, [event.patch]);
564
- }
565
- if (event.type === WORKFLOW_EVENTS.STEP_STATUS && event.steps[0].status === STATUS.COMPLETE) {
566
- initialCompletedSteps = event.steps;
567
- break; // Stop after first step
568
- }
569
- }
570
-
571
- // Clear executed steps array
572
- executedSteps.length = 0;
573
-
574
- // Resume workflow with first step completed
575
- let resumedState: State | undefined;
576
- if (!initialCompletedSteps) throw new Error('Expected initialCompletedSteps');
577
-
578
- for await (const event of threeStepWorkflow.run({
579
- client: mockClient,
580
- initialState,
581
- initialCompletedSteps,
582
- workflowRunId: 'test-run-id',
583
- fileStore: testFileStore
584
- })) {
585
- if (event.type === WORKFLOW_EVENTS.RESTART) {
586
- resumedState = event.initialState;
587
- } else if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
588
- resumedState = applyPatches(resumedState!, [event.patch]);
589
- }
590
- }
591
-
592
- // Verify only steps 2 and 3 were executed
593
- expect(executedSteps).toEqual(["Step 2", "Step 3"]);
594
- expect(executedSteps).not.toContain("Step 1");
595
-
596
- // Verify the final state after all steps complete
597
- expect(resumedState).toEqual({
598
- value: 36,
599
- initialValue: true
600
- });
601
- });
602
- });
603
-
604
- describe('nested workflows', () => {
605
- it('should execute nested workflows and yield all inner workflow events', async () => {
606
- // Create an inner workflow that will be nested
607
- const innerWorkflow = workflow<{}, { value: number }>('Inner Workflow')
608
- .step(
609
- "Double value",
610
- ({ state }) => ({
611
- inner: true,
612
- value: state.value * 2
613
- })
614
- );
615
-
616
- // Create outer workflow that uses the inner workflow
617
- const outerWorkflow = workflow('Outer Workflow')
618
- .step(
619
- "Set prefix",
620
- () => ({ prefix: "test-" })
621
- )
622
- .workflow(
623
- "Run inner workflow",
624
- innerWorkflow,
625
- ({ state, workflowState }) => ({
626
- ...state,
627
- innerResult: workflowState.value
628
- }),
629
- () => ({ value: 5 })
630
- );
631
-
632
- const events: WorkflowEvent<any>[] = [];
633
- for await (const event of outerWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
634
- events.push(event);
635
- }
636
-
637
- // Verify all events are yielded in correct order
638
- expect(events.map(e => ({
639
- type: e.type,
640
- workflowTitle: 'workflowTitle' in e ? e.workflowTitle : undefined,
641
- status: 'status' in e ? e.status : undefined,
642
- stepTitle: 'stepTitle' in e ? e.stepTitle : undefined
643
- }))).toEqual([
644
- // Outer workflow start
645
- {
646
- type: WORKFLOW_EVENTS.START,
647
- workflowTitle: 'Outer Workflow',
648
- status: STATUS.RUNNING,
649
- stepTitle: undefined
650
- },
651
- // Initial step status for outer workflow
652
- {
653
- type: WORKFLOW_EVENTS.STEP_STATUS,
654
- workflowTitle: undefined,
655
- status: undefined,
656
- stepTitle: undefined
657
- },
658
- // First step of outer workflow
659
- {
660
- type: WORKFLOW_EVENTS.STEP_START,
661
- workflowTitle: undefined,
662
- status: STATUS.RUNNING,
663
- stepTitle: 'Set prefix'
664
- },
665
- {
666
- type: WORKFLOW_EVENTS.STEP_COMPLETE,
667
- workflowTitle: undefined,
668
- status: STATUS.RUNNING,
669
- stepTitle: 'Set prefix'
670
- },
671
- {
672
- type: WORKFLOW_EVENTS.STEP_STATUS,
673
- workflowTitle: undefined,
674
- status: undefined,
675
- stepTitle: undefined
676
- },
677
- {
678
- type: WORKFLOW_EVENTS.STEP_START,
679
- workflowTitle: undefined,
680
- status: STATUS.RUNNING,
681
- stepTitle: 'Run inner workflow'
682
- },
683
- // Inner workflow start
684
- {
685
- type: WORKFLOW_EVENTS.START,
686
- workflowTitle: 'Inner Workflow',
687
- status: STATUS.RUNNING,
688
- stepTitle: undefined
689
- },
690
- // Initial step status for inner workflow
691
- {
692
- type: WORKFLOW_EVENTS.STEP_STATUS,
693
- workflowTitle: undefined,
694
- status: undefined,
695
- stepTitle: undefined
696
- },
697
- // Inner workflow step
698
- {
699
- type: WORKFLOW_EVENTS.STEP_START,
700
- workflowTitle: undefined,
701
- status: STATUS.RUNNING,
702
- stepTitle: 'Double value'
703
- },
704
- {
705
- type: WORKFLOW_EVENTS.STEP_COMPLETE,
706
- workflowTitle: undefined,
707
- status: STATUS.RUNNING,
708
- stepTitle: 'Double value'
709
- },
710
- {
711
- type: WORKFLOW_EVENTS.STEP_STATUS,
712
- workflowTitle: undefined,
713
- status: undefined,
714
- stepTitle: undefined
715
- },
716
- {
717
- type: WORKFLOW_EVENTS.COMPLETE,
718
- workflowTitle: 'Inner Workflow',
719
- status: STATUS.COMPLETE,
720
- stepTitle: undefined
721
- },
722
- // Outer workflow nested step completion
723
- {
724
- type: WORKFLOW_EVENTS.STEP_COMPLETE,
725
- workflowTitle: undefined,
726
- status: STATUS.RUNNING,
727
- stepTitle: 'Run inner workflow'
728
- },
729
- {
730
- type: WORKFLOW_EVENTS.STEP_STATUS,
731
- workflowTitle: undefined,
732
- status: undefined,
733
- stepTitle: undefined
734
- },
735
- // Outer workflow completion
736
- {
737
- type: WORKFLOW_EVENTS.COMPLETE,
738
- workflowTitle: 'Outer Workflow',
739
- status: STATUS.COMPLETE,
740
- stepTitle: undefined
741
- }
742
- ]);
743
-
744
- // Verify states are passed correctly
745
- let innerState: State = { value: 5 }; // Match the initial state from the workflow
746
- let outerState = {};
747
-
748
- for (const event of events) {
749
- if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
750
- if (event.stepTitle === 'Double value') {
751
- innerState = applyPatches(innerState, [event.patch]);
752
- } else {
753
- outerState = applyPatches(outerState, [event.patch]);
754
- }
755
- }
756
- }
757
-
758
- // Verify final states
759
- expect(innerState).toEqual({
760
- inner: true,
761
- value: 10
762
- });
763
-
764
- expect(outerState).toEqual({
765
- prefix: "test-",
766
- innerResult: 10
767
- });
768
- });
769
-
770
- it('should handle errors in nested workflows and propagate them up', async () => {
771
- // Create an inner workflow that will throw an error
772
- const innerWorkflow = workflow<{}, { inner: boolean, value: number }>('Failing Inner Workflow')
773
- .step(
774
- "Throw error",
775
- (): { value: number } => {
776
- throw new Error('Inner workflow error');
777
- }
778
- );
779
-
780
- // Create outer workflow that uses the failing inner workflow
781
- const outerWorkflow = workflow('Outer Workflow')
782
- .step(
783
- "First step",
784
- () => ({ step: "first" })
785
- )
786
- .workflow(
787
- "Run inner workflow",
788
- innerWorkflow,
789
- ({ state, workflowState }) => ({
790
- ...state,
791
- step: "second",
792
- innerResult: workflowState.value
793
- }),
794
- () => ({ value: 5 })
795
- );
796
-
797
- const events: WorkflowEvent<any>[] = [];
798
- let error: Error | undefined;
799
- let mainWorkflowId: string | undefined;
800
-
801
- try {
802
- for await (const event of outerWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
803
- events.push(event);
804
- if (event.type === WORKFLOW_EVENTS.START && !mainWorkflowId) {
805
- mainWorkflowId = event.workflowRunId;
806
- }
807
- }
808
- } catch (e) {
809
- error = e as Error;
810
- }
811
-
812
- // Verify error was thrown
813
- expect(error?.message).toBe('Inner workflow error');
814
-
815
- // Verify event sequence including error
816
- expect(events).toEqual([
817
- expect.objectContaining({
818
- type: WORKFLOW_EVENTS.START,
819
- workflowTitle: 'Outer Workflow',
820
- status: STATUS.RUNNING,
821
- workflowRunId: mainWorkflowId
822
- }),
823
- expect.objectContaining({
824
- type: WORKFLOW_EVENTS.STEP_STATUS,
825
- steps: expect.any(Array)
826
- }),
827
- expect.objectContaining({
828
- type: WORKFLOW_EVENTS.STEP_START,
829
- status: STATUS.RUNNING,
830
- stepTitle: 'First step'
831
- }),
832
- expect.objectContaining({
833
- type: WORKFLOW_EVENTS.STEP_COMPLETE,
834
- status: STATUS.RUNNING,
835
- stepTitle: 'First step'
836
- }),
837
- expect.objectContaining({
838
- type: WORKFLOW_EVENTS.STEP_STATUS,
839
- steps: expect.any(Array)
840
- }),
841
- expect.objectContaining({
842
- type: WORKFLOW_EVENTS.STEP_START,
843
- status: STATUS.RUNNING,
844
- stepTitle: 'Run inner workflow'
845
- }),
846
- expect.objectContaining({
847
- type: WORKFLOW_EVENTS.START,
848
- workflowTitle: 'Failing Inner Workflow',
849
- status: STATUS.RUNNING,
850
- }),
851
- expect.objectContaining({
852
- type: WORKFLOW_EVENTS.STEP_STATUS,
853
- steps: expect.any(Array)
854
- }),
855
- expect.objectContaining({
856
- type: WORKFLOW_EVENTS.STEP_START,
857
- status: STATUS.RUNNING,
858
- stepTitle: 'Throw error'
859
- }),
860
- expect.objectContaining({
861
- type: WORKFLOW_EVENTS.ERROR,
862
- workflowTitle: 'Failing Inner Workflow',
863
- status: STATUS.ERROR,
864
- error: expect.objectContaining({
865
- name: expect.any(String),
866
- message: expect.any(String)
867
- }),
868
- }),
869
- expect.objectContaining({
870
- type: WORKFLOW_EVENTS.STEP_STATUS,
871
- steps: expect.arrayContaining([
872
- expect.objectContaining({
873
- title: 'Throw error',
874
- status: STATUS.ERROR
875
- })
876
- ])
877
- }),
878
- expect.objectContaining({
879
- type: WORKFLOW_EVENTS.ERROR,
880
- workflowTitle: 'Outer Workflow',
881
- status: STATUS.ERROR,
882
- error: expect.objectContaining({
883
- name: expect.any(String),
884
- message: expect.any(String)
885
- })
886
- }),
887
- expect.objectContaining({
888
- type: WORKFLOW_EVENTS.STEP_STATUS,
889
- steps: expect.arrayContaining([
890
- expect.objectContaining({
891
- title: 'Run inner workflow',
892
- status: STATUS.ERROR
893
- })
894
- ])
895
- })
896
- ]);
897
-
898
- // Find inner and outer error events by workflowRunId
899
- const innerErrorEvent = events.find(e =>
900
- e.type === WORKFLOW_EVENTS.ERROR &&
901
- e.workflowRunId !== mainWorkflowId
902
- ) as WorkflowErrorEvent<any>;
903
-
904
- const outerErrorEvent = events.find(e =>
905
- e.type === WORKFLOW_EVENTS.ERROR &&
906
- e.workflowRunId === mainWorkflowId
907
- ) as WorkflowErrorEvent<any>;
908
-
909
- expect(innerErrorEvent.error).toEqual(expect.objectContaining({
910
- message: 'Inner workflow error'
911
- }));
912
- expect(outerErrorEvent.error).toEqual(expect.objectContaining({
913
- message: 'Inner workflow error'
914
- }));
915
- });
916
-
917
- it('should include patches in step status events for inner workflow steps', async () => {
918
- interface InnerState extends State {
919
- value: number;
920
- }
921
-
922
- interface OuterState extends State {
923
- value: number;
924
- result?: number;
925
- }
926
-
927
- // Create an inner workflow that modifies state
928
- const innerWorkflow = workflow<{}, InnerState>('Inner Workflow')
929
- .step("Double value", ({ state }) => ({
930
- ...state,
931
- value: state.value * 2
932
- }));
933
-
934
- // Create outer workflow that uses the inner workflow
935
- const outerWorkflow = workflow<{}, OuterState>('Outer Workflow')
936
- .step("Set initial", () => ({
937
- value: 5
938
- }))
939
- .workflow(
940
- "Run inner workflow",
941
- innerWorkflow,
942
- ({ state, workflowState }) => ({
943
- ...state,
944
- result: workflowState.value
945
- }),
946
- (state) => ({ value: state.value })
947
- );
948
-
949
- // Run workflow and collect step status events
950
- let finalStepStatus;
951
- for await (const event of outerWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
952
- if (event.type === WORKFLOW_EVENTS.STEP_STATUS) {
953
- finalStepStatus = event;
954
- }
955
- }
956
-
957
- // Verify step status contains patches for all steps including the inner workflow step
958
- expect(finalStepStatus?.steps).toEqual([
959
- expect.objectContaining({
960
- title: 'Set initial',
961
- status: STATUS.COMPLETE,
962
- patch: [{
963
- op: 'add',
964
- path: '/value',
965
- value: 5
966
- }]
967
- }),
968
- expect.objectContaining({
969
- title: 'Run inner workflow',
970
- status: STATUS.COMPLETE,
971
- patch: [{
972
- op: 'add',
973
- path: '/result',
974
- value: 10
975
- }]
976
- })
977
- ]);
978
- });
979
- });
980
-
981
- describe('workflow options', () => {
982
- it('should pass options through to workflow events', async () => {
983
- const testWorkflow = workflow<{ testOption: string }>('Options Workflow')
984
- .step(
985
- "Simple step",
986
- ({ state, options }) => ({
987
- value: 1,
988
- passedOption: options.testOption
989
- })
990
- );
991
-
992
- const workflowOptions = {
993
- testOption: 'test-value'
994
- };
995
-
996
- let finalEvent, finalStepStatus;
997
- for await (const event of testWorkflow.run({
998
- client: mockClient,
999
- options: workflowOptions,
1000
- fileStore: testFileStore
1001
- })) {
1002
- if (event.type === WORKFLOW_EVENTS.STEP_STATUS) {
1003
- finalStepStatus = event;
1004
- } else {
1005
- finalEvent = event;
1006
- }
1007
- }
1008
-
1009
- expect(finalEvent).toEqual(expect.objectContaining({
1010
- type: WORKFLOW_EVENTS.COMPLETE,
1011
- status: STATUS.COMPLETE,
1012
- workflowTitle: 'Options Workflow',
1013
- workflowDescription: undefined,
1014
- options: workflowOptions,
1015
- }))
1016
- expect(finalStepStatus).toEqual(expect.objectContaining({
1017
- type: WORKFLOW_EVENTS.STEP_STATUS,
1018
- steps: [
1019
- expect.objectContaining({
1020
- title: 'Simple step',
1021
- patch: expect.any(Object),
1022
- status: STATUS.COMPLETE,
1023
- })
1024
- ],
1025
- options: workflowOptions,
1026
- }));
1027
- });
1028
-
1029
- it('should provide empty object as default options', async () => {
1030
- const testWorkflow = workflow('Default Options Workflow')
1031
- .step(
1032
- "Simple step",
1033
- ({ options }) => ({
1034
- hasOptions: Object.keys(options).length === 0
1035
- })
1036
- );
1037
-
1038
- const workflowRun = testWorkflow.run({ client: mockClient, fileStore: testFileStore });
1039
-
1040
- // Skip start event
1041
- await workflowRun.next();
1042
-
1043
- // Skip initial step status event
1044
- await workflowRun.next();
1045
-
1046
- // Check step start
1047
- const stepStartResult = await workflowRun.next();
1048
- expect(stepStartResult.value).toEqual(expect.objectContaining({
1049
- options: {},
1050
- type: WORKFLOW_EVENTS.STEP_START
1051
- }));
1052
-
1053
- // Check step completion
1054
- const stepResult = await workflowRun.next();
1055
- expect(stepResult.value).toEqual(expect.objectContaining({
1056
- type: WORKFLOW_EVENTS.STEP_COMPLETE,
1057
- stepTitle: 'Simple step',
1058
- options: {},
1059
- }));
1060
- });
1061
- });
1062
-
1063
- describe('type inference', () => {
1064
- it('should correctly infer complex workflow state types', async () => {
1065
- // Create an inner workflow that uses the shared options type
1066
- const innerWorkflow = workflow<{ features: string[] }>('Inner Type Test')
1067
- .step(
1068
- "Process features",
1069
- ({ options }) => ({
1070
- processedValue: options.features.includes('fast') ? 100 : 42,
1071
- featureCount: options.features.length
1072
- })
1073
- );
1074
-
1075
- // Create a complex workflow using multiple features
1076
- const complexWorkflow = workflow<{ features: string[] }>('Complex Type Test')
1077
- .step(
1078
- "First step",
1079
- ({ options }) => ({
1080
- initialFeatures: options.features,
1081
- value: 42
1082
- })
1083
- )
1084
- .workflow(
1085
- "Nested workflow",
1086
- innerWorkflow,
1087
- ({ state, workflowState }) => ({
1088
- ...state,
1089
- processedValue: workflowState.processedValue,
1090
- totalFeatures: workflowState.featureCount
1091
- }),
1092
- () => ({
1093
- processedValue: 0,
1094
- featureCount: 0
1095
- })
1096
- )
1097
- .step(
1098
- "Final step",
1099
- ({ state }) => ({
1100
- ...state,
1101
- completed: true
1102
- })
1103
- );
1104
-
1105
- // Type test setup
1106
- type ExpectedState = {
1107
- initialFeatures: string[];
1108
- value: number;
1109
- processedValue: number;
1110
- totalFeatures: number;
1111
- completed: true;
1112
- };
1113
-
1114
- type ActualState = Parameters<
1115
- Parameters<(typeof complexWorkflow)['step']>[1]
1116
- >[0]['state'];
1117
-
1118
- type TypeTest = AssertEquals<ActualState, ExpectedState>;
1119
- const _typeAssert: TypeTest = true;
1120
-
1121
- // Collect all events
1122
- const events = [];
1123
- let finalStepStatus, finalState = {};
1124
- let mainWorkflowId: string | undefined;
1125
-
1126
- for await (const event of complexWorkflow.run({
1127
- client: mockClient,
1128
- options: { features: ['fast', 'secure'] },
1129
- fileStore: testFileStore
1130
- })) {
1131
- events.push(event);
1132
-
1133
- // Capture the main workflow's ID from its start event
1134
- if (event.type === WORKFLOW_EVENTS.START && !mainWorkflowId) {
1135
- mainWorkflowId = event.workflowRunId;
1136
- }
1137
-
1138
- if (event.type === WORKFLOW_EVENTS.STEP_STATUS) {
1139
- finalStepStatus = event;
1140
- } else if (
1141
- event.type === WORKFLOW_EVENTS.STEP_COMPLETE &&
1142
- event.workflowRunId === mainWorkflowId // Only process events from main workflow
1143
- ) {
1144
- finalState = applyPatches(finalState, [event.patch]);
1145
- }
1146
- }
1147
-
1148
- // Verify workflow start event
1149
- expect(events[0]).toEqual(expect.objectContaining({
1150
- type: WORKFLOW_EVENTS.START,
1151
- status: STATUS.RUNNING,
1152
- workflowTitle: 'Complex Type Test',
1153
- workflowDescription: undefined,
1154
- options: { features: ['fast', 'secure'] },
1155
- workflowRunId: mainWorkflowId
1156
- }));
1157
-
1158
- // Verify inner workflow events are included
1159
- const innerStartEvent = events.find(e =>
1160
- e.type === WORKFLOW_EVENTS.START &&
1161
- 'workflowRunId' in e &&
1162
- e.workflowRunId !== mainWorkflowId
1163
- );
1164
- expect(innerStartEvent).toEqual(expect.objectContaining({
1165
- type: WORKFLOW_EVENTS.START,
1166
- status: STATUS.RUNNING,
1167
- workflowTitle: 'Inner Type Test',
1168
- options: { features: ['fast', 'secure'] }
1169
- }));
1170
-
1171
- // Verify the final step status
1172
- if (!finalStepStatus) throw new Error('Expected final step status event');
1173
- const lastStep = finalStepStatus.steps[finalStepStatus.steps.length - 1];
1174
- expect(lastStep.status).toBe(STATUS.COMPLETE);
1175
- expect(lastStep.title).toBe('Final step');
1176
-
1177
- expect(finalState).toEqual({
1178
- initialFeatures: ['fast', 'secure'],
1179
- value: 42,
1180
- processedValue: 100,
1181
- totalFeatures: 2,
1182
- completed: true
1183
- });
1184
- });
1185
-
1186
- it('should correctly infer workflow reducer state types', async () => {
1187
- // Create an inner workflow with a specific state shape
1188
- const innerWorkflow = workflow('Inner State Test')
1189
- .step(
1190
- "Inner step",
1191
- () => ({
1192
- innerValue: 42,
1193
- metadata: { processed: true }
1194
- })
1195
- );
1196
-
1197
- // Create outer workflow to test reducer type inference
1198
- const outerWorkflow = workflow('Outer State Test')
1199
- .step(
1200
- "First step",
1201
- () => ({
1202
- outerValue: 100,
1203
- status: 'ready'
1204
- })
1205
- )
1206
- .workflow(
1207
- "Nested workflow",
1208
- innerWorkflow,
1209
- ({ state, workflowState }) => {
1210
- // Type assertion for outer state
1211
- type ExpectedOuterState = {
1212
- outerValue: number;
1213
- status: string;
1214
- };
1215
- type ActualOuterState = typeof state;
1216
- type OuterStateTest = AssertEquals<
1217
- ActualOuterState,
1218
- ExpectedOuterState
1219
- >;
1220
- const _outerAssert: OuterStateTest = true;
1221
-
1222
- // Type assertion for inner workflow state
1223
- type ExpectedInnerState = {
1224
- innerValue: number;
1225
- metadata: { processed: true };
1226
- };
1227
- type ActualInnerState = typeof workflowState;
1228
- type InnerStateTest = AssertEquals<
1229
- ActualInnerState,
1230
- ExpectedInnerState
1231
- >;
1232
- const _innerAssert: InnerStateTest = true;
1233
-
1234
- return {
1235
- ...state,
1236
- innerResult: workflowState.innerValue,
1237
- processed: workflowState.metadata.processed
1238
- };
1239
- },
1240
- () => ({} as { innerValue: number; metadata: { processed: boolean } })
1241
- );
1242
-
1243
- // Run the workflow to verify runtime behavior
1244
- let finalState = {};
1245
- let mainWorkflowId: string | undefined;
1246
-
1247
- for await (const event of outerWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
1248
- if (event.type === WORKFLOW_EVENTS.START && !mainWorkflowId) {
1249
- mainWorkflowId = event.workflowRunId;
1250
- }
1251
- if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE && event.workflowRunId === mainWorkflowId) {
1252
- finalState = applyPatches(finalState, [event.patch]);
1253
- }
1254
- }
1255
-
1256
- expect(finalState).toEqual({
1257
- outerValue: 100,
1258
- status: 'ready',
1259
- innerResult: 42,
1260
- processed: true
1261
- });
1262
- });
1263
-
1264
- it('should correctly infer step action state types', async () => {
1265
- const testWorkflow = workflow('Action State Test')
1266
- .step(
1267
- "First step",
1268
- () => ({
1269
- count: 1,
1270
- metadata: { created: new Date().toISOString() }
1271
- })
1272
- )
1273
- .step(
1274
- "Second step",
1275
- ({ state }) => {
1276
- // Type assertion for action state
1277
- type ExpectedState = {
1278
- count: number;
1279
- metadata: { created: string };
1280
- };
1281
- type ActualState = typeof state;
1282
- type StateTest = AssertEquals<
1283
- ActualState,
1284
- ExpectedState
1285
- >;
1286
- const _stateAssert: StateTest = true;
1287
-
1288
- return {
1289
- ...state,
1290
- count: state.count + 1,
1291
- metadata: {
1292
- ...state.metadata,
1293
- updated: new Date().toISOString()
1294
- }
1295
- };
1296
- }
1297
- );
1298
-
1299
- // Run the workflow to verify runtime behavior
1300
- let finalState = {};
1301
- let mainWorkflowId: string | undefined;
1302
-
1303
- for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
1304
- if (event.type === WORKFLOW_EVENTS.START && !mainWorkflowId) {
1305
- mainWorkflowId = event.workflowRunId;
1306
- }
1307
- if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE && event.workflowRunId === mainWorkflowId) {
1308
- finalState = applyPatches(finalState, [event.patch]);
1309
- }
1310
- }
1311
-
1312
- expect(finalState).toMatchObject({
1313
- count: 2,
1314
- metadata: {
1315
- created: expect.any(String),
1316
- updated: expect.any(String)
1317
- }
1318
- });
1319
- });
1320
-
1321
- it('should correctly infer prompt response types in subsequent steps', async () => {
1322
- const testWorkflow = workflow('Prompt Type Test')
1323
- .prompt(
1324
- "Get user info",
1325
- {
1326
- template: () => "What is the user's info?",
1327
- responseModel: {
1328
- schema: z.object({ name: z.string(), age: z.number() }),
1329
- name: "userInfo" as const // Must be const or type inference breaks
1330
- }
1331
- }
1332
- )
1333
- .step(
1334
- "Use response",
1335
- ({ state }) => {
1336
- // Type assertion to verify state includes userInfo
1337
- type ExpectedState = {
1338
- userInfo: {
1339
- name: string;
1340
- age: number;
1341
- }
1342
- };
1343
- type ActualState = typeof state;
1344
- type StateTest = AssertEquals<ActualState, ExpectedState>;
1345
- const _stateAssert: StateTest = true;
1346
-
1347
- return {
1348
- ...state,
1349
- greeting: `Hello ${state.userInfo.name}, you are ${state.userInfo.age} years old`
1350
- };
1351
- }
1352
- );
1353
-
1354
- // Mock the client response
1355
- mockClient.execute.mockResolvedValueOnce({
1356
- name: "Test User",
1357
- age: 30
1358
- });
1359
-
1360
- // Run workflow and collect final state
1361
- let finalState = {};
1362
- for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
1363
- if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
1364
- finalState = applyPatches(finalState, [event.patch]);
1365
- }
1366
- }
1367
-
1368
- // Verify the workflow executed correctly
1369
- expect(finalState).toEqual({
1370
- userInfo: {
1371
- name: "Test User",
1372
- age: 30
1373
- },
1374
- greeting: "Hello Test User, you are 30 years old"
1375
- });
1376
- });
1377
-
1378
- it('should correctly handle prompt reduce function', async () => {
1379
- const testWorkflow = workflow('Prompt Reduce Test')
1380
- .prompt(
1381
- "Get numbers",
1382
- {
1383
- template: () => "Give me some numbers",
1384
- responseModel: {
1385
- schema: z.object({ numbers: z.array(z.number()) }),
1386
- name: "numbersResponse" as const
1387
- }
1388
- },
1389
- ({ state, response, options }) => ({
1390
- ...state,
1391
- numbersResponse: response, // Include the response explicitly
1392
- sum: response.numbers.reduce((a, b) => a + b, 0),
1393
- count: response.numbers.length
1394
- })
1395
- );
1396
-
1397
- // Mock the client response
1398
- mockClient.execute.mockResolvedValueOnce({
1399
- numbers: [1, 2, 3, 4, 5]
1400
- });
1401
-
1402
- // Run workflow and collect final state
1403
- let finalState = {};
1404
- for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
1405
- if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
1406
- finalState = applyPatches(finalState, [event.patch]);
1407
- }
1408
- }
1409
-
1410
- // Verify the workflow executed correctly with reduced state
1411
- expect(finalState).toEqual({
1412
- numbersResponse: {
1413
- numbers: [1, 2, 3, 4, 5]
1414
- },
1415
- sum: 15,
1416
- count: 5
1417
- });
1418
-
1419
- // Verify type inference works correctly
1420
- type ExpectedState = {
1421
- numbersResponse: {
1422
- numbers: number[];
1423
- };
1424
- sum: number;
1425
- count: number;
1426
- };
1427
-
1428
- type ActualState = Parameters<
1429
- Parameters<(typeof testWorkflow)['step']>[1]
1430
- >[0]['state'];
1431
-
1432
- type TypeTest = AssertEquals<ActualState, ExpectedState>;
1433
- const _typeAssert: TypeTest = true;
1434
- });
1435
- });