@positronic/core 0.0.3 → 0.0.5

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.
@@ -1,1981 +0,0 @@
1
- import { BRAIN_EVENTS, STATUS } from './constants.js';
2
- import { applyPatches, type JsonPatch } from './json-patch.js';
3
- import { State } from './types.js';
4
- import {
5
- brain,
6
- type BrainEvent,
7
- type BrainErrorEvent,
8
- type SerializedStep,
9
- type SerializedStepStatus,
10
- } from './brain.js';
11
- import { z } from 'zod';
12
- import { jest } from '@jest/globals';
13
- import { ObjectGenerator } from '../clients/types.js';
14
- import { createResources, type Resources } from '../resources/resources.js';
15
- import type { ResourceLoader } from '../resources/resource-loader.js';
16
-
17
- // Helper function to get the next value from an AsyncIterator
18
- const nextStep = async <T>(brainRun: AsyncIterator<T>): Promise<T> => {
19
- const result = await brainRun.next();
20
- if (result.done) throw new Error('Iterator is done');
21
- return result.value;
22
- };
23
-
24
- // Define a Logger interface for testing
25
- interface Logger {
26
- log: (message: string) => void;
27
- }
28
-
29
- // Mock services for testing
30
- const testLogger: Logger = {
31
- log: jest.fn(),
32
- };
33
-
34
- type AssertEquals<T, U> = 0 extends 1 & T
35
- ? false // fails if T is any
36
- : 0 extends 1 & U
37
- ? false // fails if U is any
38
- : [T] extends [U]
39
- ? [U] extends [T]
40
- ? true
41
- : false
42
- : false;
43
-
44
- // Mock ObjectGenerator for testing
45
- const mockGenerateObject = jest.fn<ObjectGenerator['generateObject']>();
46
- const mockClient: jest.Mocked<ObjectGenerator> = {
47
- generateObject: mockGenerateObject,
48
- };
49
-
50
- // Mock Resources for testing
51
- const mockResourceLoad = jest.fn(
52
- async (
53
- resourceName: string,
54
- type?: 'text' | 'binary'
55
- ): Promise<string | Buffer> => {
56
- if (type === 'binary')
57
- return Buffer.from(`mock ${resourceName} binary content`);
58
- return `mock ${resourceName} text content`;
59
- }
60
- ) as jest.MockedFunction<ResourceLoader['load']>;
61
-
62
- const mockResourceLoader: ResourceLoader = {
63
- load: mockResourceLoad,
64
- };
65
-
66
- const testManifest = {
67
- myFile: {
68
- type: 'text' as const,
69
- key: 'myFile',
70
- path: '/test/myFile.txt',
71
- },
72
- myBinaryFile: {
73
- type: 'binary' as const,
74
- key: 'myBinaryFile',
75
- path: '/test/myBinaryFile.bin',
76
- },
77
- nested: {
78
- anotherFile: {
79
- type: 'text' as const,
80
- key: 'anotherFile',
81
- path: '/test/anotherFile.txt',
82
- },
83
- },
84
- } as const;
85
- const mockResources = createResources(mockResourceLoader, testManifest);
86
-
87
- describe('brain creation', () => {
88
- beforeEach(() => {
89
- mockGenerateObject.mockClear();
90
- mockResourceLoad.mockClear();
91
- });
92
-
93
- it('should create a brain with steps and run through them', async () => {
94
- const testBrain = brain('test brain')
95
- .step('First step', () => {
96
- return { count: 1 };
97
- })
98
- .step('Second step', ({ state }) => ({
99
- ...state,
100
- doubled: state.count * 2,
101
- }));
102
-
103
- const brainRun = testBrain.run({
104
- client: mockClient,
105
- });
106
-
107
- // Check start event
108
- const startResult = await brainRun.next();
109
- expect(startResult.value).toEqual(
110
- expect.objectContaining({
111
- type: BRAIN_EVENTS.START,
112
- status: STATUS.RUNNING,
113
- brainTitle: 'test brain',
114
- brainDescription: undefined,
115
- })
116
- );
117
-
118
- // Skip initial step status event
119
- await nextStep(brainRun);
120
-
121
- // Check first step start
122
- const firstStepStartResult = await nextStep(brainRun);
123
- expect(firstStepStartResult).toEqual(
124
- expect.objectContaining({
125
- type: BRAIN_EVENTS.STEP_START,
126
- status: STATUS.RUNNING,
127
- stepTitle: 'First step',
128
- stepId: expect.any(String),
129
- })
130
- );
131
-
132
- // Check first step status (running)
133
- const firstStepStatusRunning = await nextStep(brainRun);
134
- expect(firstStepStatusRunning).toEqual(
135
- expect.objectContaining({
136
- type: BRAIN_EVENTS.STEP_STATUS,
137
- steps: expect.any(Array),
138
- })
139
- );
140
- if (firstStepStatusRunning.type === BRAIN_EVENTS.STEP_STATUS) {
141
- expect(firstStepStatusRunning.steps[0].status).toBe(STATUS.RUNNING);
142
- }
143
-
144
- // Check first step completion
145
- const firstStepResult = await nextStep(brainRun);
146
- expect(firstStepResult).toEqual(
147
- expect.objectContaining({
148
- type: BRAIN_EVENTS.STEP_COMPLETE,
149
- status: STATUS.RUNNING,
150
- stepTitle: 'First step',
151
- stepId: expect.any(String),
152
- patch: [
153
- {
154
- op: 'add',
155
- path: '/count',
156
- value: 1,
157
- },
158
- ],
159
- })
160
- );
161
-
162
- // Step Status Event
163
- await nextStep(brainRun);
164
-
165
- // Check second step start
166
- const secondStepStartResult = await nextStep(brainRun);
167
- expect(secondStepStartResult).toEqual(
168
- expect.objectContaining({
169
- type: BRAIN_EVENTS.STEP_START,
170
- status: STATUS.RUNNING,
171
- stepTitle: 'Second step',
172
- stepId: expect.any(String),
173
- })
174
- );
175
-
176
- // Check second step status (running)
177
- const secondStepStatusRunning = await nextStep(brainRun);
178
- expect(secondStepStatusRunning).toEqual(
179
- expect.objectContaining({
180
- type: BRAIN_EVENTS.STEP_STATUS,
181
- steps: expect.any(Array),
182
- })
183
- );
184
- if (secondStepStatusRunning.type === BRAIN_EVENTS.STEP_STATUS) {
185
- expect(secondStepStatusRunning.steps[1].status).toBe(STATUS.RUNNING);
186
- }
187
-
188
- // Check second step completion
189
- const secondStepResult = await nextStep(brainRun);
190
- expect(secondStepResult).toEqual(
191
- expect.objectContaining({
192
- type: BRAIN_EVENTS.STEP_COMPLETE,
193
- stepTitle: 'Second step',
194
- stepId: expect.any(String),
195
- patch: [
196
- {
197
- op: 'add',
198
- path: '/doubled',
199
- value: 2,
200
- },
201
- ],
202
- })
203
- );
204
-
205
- // Step Status Event
206
- const stepStatusResult = await nextStep(brainRun);
207
- expect(stepStatusResult).toEqual(
208
- expect.objectContaining({
209
- type: BRAIN_EVENTS.STEP_STATUS,
210
- steps: [
211
- expect.objectContaining({
212
- title: 'First step',
213
- status: STATUS.COMPLETE,
214
- id: expect.any(String),
215
- }),
216
- expect.objectContaining({
217
- title: 'Second step',
218
- status: STATUS.COMPLETE,
219
- id: expect.any(String),
220
- }),
221
- ],
222
- })
223
- );
224
-
225
- // Check brain completion
226
- const completeResult = await nextStep(brainRun);
227
- expect(completeResult).toEqual(
228
- expect.objectContaining({
229
- type: BRAIN_EVENTS.COMPLETE,
230
- status: STATUS.COMPLETE,
231
- brainTitle: 'test brain',
232
- brainDescription: undefined,
233
- })
234
- );
235
- });
236
-
237
- it('should create a brain with a name and description when passed an object', async () => {
238
- const testBrain = brain({
239
- title: 'my named brain',
240
- description: 'some description',
241
- });
242
-
243
- const brainRun = testBrain.run({
244
- client: mockClient,
245
- });
246
- const startResult = await brainRun.next();
247
- expect(startResult.value).toEqual(
248
- expect.objectContaining({
249
- type: BRAIN_EVENTS.START,
250
- status: STATUS.RUNNING,
251
- brainTitle: 'my named brain',
252
- brainDescription: 'some description',
253
- options: {},
254
- })
255
- );
256
- });
257
-
258
- it('should create a brain with just a name when passed a string', async () => {
259
- const testBrain = brain('simple brain');
260
- const brainRun = testBrain.run({
261
- client: mockClient,
262
- });
263
- const startResult = await brainRun.next();
264
- expect(startResult.value).toEqual(
265
- expect.objectContaining({
266
- type: BRAIN_EVENTS.START,
267
- status: STATUS.RUNNING,
268
- brainTitle: 'simple brain',
269
- brainDescription: undefined,
270
- options: {},
271
- })
272
- );
273
- });
274
-
275
- it('should allow overriding client per step', async () => {
276
- const overrideClient: jest.Mocked<ObjectGenerator> = {
277
- generateObject: jest
278
- .fn<ObjectGenerator['generateObject']>()
279
- .mockResolvedValue({ override: true }),
280
- };
281
-
282
- // Make sure that for the default prompt the default client returns a known value.
283
- mockClient.generateObject.mockResolvedValueOnce({ override: false });
284
-
285
- const testBrain = brain('Client Override Test')
286
- .prompt('Use default client', {
287
- template: () => 'prompt1',
288
- outputSchema: {
289
- schema: z.object({ override: z.boolean() }),
290
- name: 'overrideResponse',
291
- },
292
- })
293
- .prompt('Use override client', {
294
- template: () => 'prompt2',
295
- outputSchema: {
296
- schema: z.object({ override: z.boolean() }),
297
- name: 'overrideResponse',
298
- },
299
- client: overrideClient,
300
- });
301
-
302
- // Run the brain and capture all events
303
- const events = [];
304
- let finalState = {};
305
- for await (const event of testBrain.run({
306
- client: mockClient,
307
- })) {
308
- events.push(event);
309
- if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
310
- finalState = applyPatches(finalState, [event.patch]);
311
- }
312
- }
313
-
314
- // Final state should include both responses
315
- expect(finalState).toEqual({
316
- overrideResponse: { override: true },
317
- });
318
-
319
- // Verify that each client was used correctly based on the supplied prompt configuration.
320
- expect(mockClient.generateObject).toHaveBeenCalledWith({
321
- schema: expect.any(z.ZodObject),
322
- schemaName: 'overrideResponse',
323
- prompt: 'prompt1',
324
- });
325
- expect(overrideClient.generateObject).toHaveBeenCalledWith({
326
- schema: expect.any(z.ZodObject),
327
- schemaName: 'overrideResponse',
328
- prompt: 'prompt2',
329
- });
330
-
331
- // Verify that the state was updated correctly with values from both clients.
332
- });
333
-
334
- it('should use the provided brainRunId for the initial run if supplied', async () => {
335
- const testBrain = brain('Brain with Provided ID');
336
- const providedId = 'my-custom-run-id-123';
337
-
338
- const brainRun = testBrain.run({
339
- client: mockClient,
340
- brainRunId: providedId,
341
- });
342
-
343
- // Check start event
344
- const startResult = await brainRun.next();
345
- expect(startResult.value).toEqual(
346
- expect.objectContaining({
347
- type: BRAIN_EVENTS.START,
348
- status: STATUS.RUNNING,
349
- brainTitle: 'Brain with Provided ID',
350
- brainRunId: providedId, // Expect the provided ID here
351
- })
352
- );
353
- });
354
-
355
- describe('Resource Availability in Steps', () => {
356
- it('should make resources available in a simple step action', async () => {
357
- let loadedText: string | undefined;
358
- const testBrain = brain('Resource Step Test').step(
359
- 'Load My File',
360
- async ({ resources }) => {
361
- loadedText = await (resources.myFile as any).loadText();
362
- return { loadedText };
363
- }
364
- );
365
-
366
- const run = testBrain.run({
367
- client: mockClient,
368
- resources: mockResources,
369
- });
370
- // Iterate through to completion
371
- for await (const _ of run) {
372
- }
373
-
374
- expect(mockResourceLoad).toHaveBeenCalledWith('myFile', 'text');
375
- expect(loadedText).toBe('mock myFile text content');
376
- });
377
-
378
- it('should make resources available in a prompt action and reducer', async () => {
379
- mockGenerateObject.mockResolvedValue({ summary: 'Generated summary' });
380
- let promptTemplateString: string | undefined;
381
- let reduceContent: string | undefined;
382
-
383
- const testBrain = brain<
384
- { myOption: string },
385
- {
386
- promptResult?: any;
387
- reducedResult?: string;
388
- fileContentForPrompt?: string;
389
- }
390
- >('Resource Prompt Test')
391
- .step('Load File For Prompt', async ({ resources }) => {
392
- const fileContent = await (resources.myFile as any).loadText();
393
- return { fileContentForPrompt: fileContent };
394
- })
395
- .prompt(
396
- 'Summarize File',
397
- {
398
- template: (state) => {
399
- return 'Summarize: ' + state.fileContentForPrompt;
400
- },
401
- outputSchema: {
402
- schema: z.object({ summary: z.string() }),
403
- name: 'promptResult' as const,
404
- },
405
- },
406
- async ({ state, response, resources }) => {
407
- reduceContent = await (
408
- (resources.nested as any).anotherFile as any
409
- ).loadText();
410
- return {
411
- ...state,
412
- reducedResult: response.summary + ' based on ' + reduceContent,
413
- };
414
- }
415
- );
416
-
417
- const run = testBrain.run({
418
- client: mockClient,
419
- resources: mockResources,
420
- options: { myOption: 'test' },
421
- });
422
- let finalState: any = {};
423
- const allPatches: JsonPatch[] = [];
424
- const initialStateForReconstruction = {};
425
-
426
- for await (const event of run) {
427
- if (event.type === BRAIN_EVENTS.STEP_COMPLETE && event.patch) {
428
- allPatches.push(event.patch);
429
- }
430
- }
431
- finalState = applyPatches(initialStateForReconstruction, allPatches);
432
-
433
- expect(mockResourceLoad).toHaveBeenCalledWith('myFile', 'text');
434
- expect(mockResourceLoad).toHaveBeenCalledWith('anotherFile', 'text');
435
- expect(reduceContent).toBe('mock anotherFile text content');
436
- expect(finalState.reducedResult).toBe(
437
- 'Generated summary based on mock anotherFile text content'
438
- );
439
- });
440
-
441
- it('should pass resources to prompt template function', async () => {
442
- const testBrain = brain('Resource Prompt Template Test').prompt(
443
- 'Generate Summary',
444
- {
445
- template: async (state, resources) => {
446
- const templateContent = await (resources.myFile as any).loadText();
447
- return `Generate a summary for: ${templateContent}`;
448
- },
449
- outputSchema: {
450
- schema: z.object({ summary: z.string() }),
451
- name: 'promptResult' as const,
452
- },
453
- }
454
- );
455
-
456
- mockGenerateObject.mockResolvedValue({ summary: 'Test summary' });
457
-
458
- const run = testBrain.run({
459
- client: mockClient,
460
- resources: mockResources,
461
- });
462
-
463
- let finalState: any = {};
464
- for await (const event of run) {
465
- if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
466
- finalState = applyPatches(finalState, [event.patch]);
467
- }
468
- }
469
-
470
- // Verify resource was loaded in template
471
- expect(mockResourceLoad).toHaveBeenCalledWith('myFile', 'text');
472
-
473
- // Verify the generated prompt included the resource content
474
- expect(mockGenerateObject).toHaveBeenCalledWith(
475
- expect.objectContaining({
476
- prompt: 'Generate a summary for: mock myFile text content',
477
- })
478
- );
479
-
480
- // Verify final state
481
- expect(finalState).toEqual({
482
- promptResult: { summary: 'Test summary' },
483
- });
484
- });
485
-
486
- it('templates can use state', async () => {
487
- const testBrain = brain('State Template Test')
488
- .step('Set Data', () => ({ existingData: 'legacy data' }))
489
- .prompt('Analyze Data', {
490
- template: (state) => {
491
- return `Analyze this: ${state.existingData}`;
492
- },
493
- outputSchema: {
494
- schema: z.object({ analysis: z.string() }),
495
- name: 'promptResult' as const,
496
- },
497
- });
498
-
499
- mockGenerateObject.mockResolvedValue({
500
- analysis: 'Analysis result',
501
- });
502
-
503
- const run = testBrain.run({
504
- client: mockClient,
505
- resources: mockResources,
506
- });
507
-
508
- let finalState: any = {};
509
- for await (const event of run) {
510
- if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
511
- finalState = applyPatches(finalState, [event.patch]);
512
- }
513
- }
514
-
515
- // Verify the prompt was generated correctly
516
- expect(mockGenerateObject).toHaveBeenCalledWith(
517
- expect.objectContaining({
518
- prompt: 'Analyze this: legacy data',
519
- })
520
- );
521
-
522
- // Verify final state
523
- expect(finalState).toEqual({
524
- existingData: 'legacy data',
525
- promptResult: { analysis: 'Analysis result' },
526
- });
527
-
528
- // Verify no resources were loaded (since template didn't use them)
529
- expect(mockResourceLoad).not.toHaveBeenCalled();
530
- });
531
-
532
- it('should make resources available in a nested brain step', async () => {
533
- let nestedLoadedText: string | undefined;
534
-
535
- const innerBrain = brain('Inner Resource Brain').step(
536
- 'Inner Load Step',
537
- async ({ resources }) => {
538
- nestedLoadedText = await (resources.myBinaryFile as any)
539
- .loadBinary()
540
- .then((b: Buffer) => b.toString());
541
- return { nestedLoadedText };
542
- }
543
- );
544
-
545
- const outerBrain = brain('Outer Resource Brain').brain(
546
- 'Run Inner',
547
- innerBrain,
548
- ({ state, brainState }) => ({ ...state, ...brainState })
549
- );
550
-
551
- const run = outerBrain.run({
552
- client: mockClient,
553
- resources: mockResources,
554
- });
555
- for await (const _ of run) {
556
- }
557
-
558
- expect(mockResourceLoad).toHaveBeenCalledWith('myBinaryFile', 'binary');
559
- expect(nestedLoadedText).toBe('mock myBinaryFile binary content');
560
- });
561
- });
562
- });
563
-
564
- describe('error handling', () => {
565
- it('should handle errors in actions and maintain correct status', async () => {
566
- const errorBrain = brain('Error Brain')
567
- // Step 1: Normal step
568
- .step('First step', () => ({
569
- value: 1,
570
- }))
571
- // Step 2: Error step
572
- .step('Error step', () => {
573
- if (true) {
574
- throw new Error('Test error');
575
- }
576
- return {
577
- value: 1,
578
- };
579
- })
580
- // Step 3: Should never execute
581
- .step('Never reached', ({ state }) => ({
582
- value: state.value + 1,
583
- }));
584
-
585
- let errorEvent, finalStepStatusEvent;
586
- try {
587
- for await (const event of errorBrain.run({
588
- client: mockClient,
589
- })) {
590
- if (event.type === BRAIN_EVENTS.ERROR) {
591
- errorEvent = event;
592
- }
593
- if (event.type === BRAIN_EVENTS.STEP_STATUS) {
594
- finalStepStatusEvent = event;
595
- }
596
- }
597
- } catch (error) {
598
- // Error is expected to be thrown
599
- }
600
-
601
- // Verify final state
602
- expect(errorEvent?.status).toBe(STATUS.ERROR);
603
- expect(errorEvent?.error?.message).toBe('Test error');
604
-
605
- // Verify steps status
606
- if (!finalStepStatusEvent?.steps) {
607
- throw new Error('Steps not found');
608
- }
609
- expect(finalStepStatusEvent.steps[0].status).toBe(STATUS.COMPLETE);
610
- expect(finalStepStatusEvent.steps[1].status).toBe(STATUS.ERROR);
611
- expect(finalStepStatusEvent.steps[2].status).toBe(STATUS.PENDING);
612
-
613
- // Verify error event structure
614
- expect(errorEvent).toEqual(
615
- expect.objectContaining({
616
- type: BRAIN_EVENTS.ERROR,
617
- status: STATUS.ERROR,
618
- brainTitle: 'Error Brain',
619
- error: expect.objectContaining({
620
- name: expect.any(String),
621
- message: expect.any(String),
622
- }),
623
- })
624
- );
625
- });
626
-
627
- it('should handle errors in nested brains and propagate them up', async () => {
628
- // Create an inner brain that will throw an error
629
- const innerBrain = brain<{}, { inner?: boolean; value?: number }>(
630
- 'Failing Inner Brain'
631
- ).step('Throw error', (): { value: number } => {
632
- throw new Error('Inner brain error');
633
- });
634
-
635
- // Create outer brain that uses the failing inner brain
636
- const outerBrain = brain('Outer Brain')
637
- .step('First step', () => ({ step: 'first' }))
638
- .brain(
639
- 'Run inner brain',
640
- innerBrain,
641
- ({ state, brainState }) => ({
642
- ...state,
643
- step: 'second',
644
- innerResult: brainState.value,
645
- }),
646
- () => ({ value: 5 })
647
- );
648
-
649
- const events: BrainEvent<any>[] = [];
650
- let error: Error | undefined;
651
- let mainBrainId: string | undefined;
652
-
653
- try {
654
- for await (const event of outerBrain.run({
655
- client: mockClient,
656
- })) {
657
- events.push(event);
658
- if (event.type === BRAIN_EVENTS.START && !mainBrainId) {
659
- mainBrainId = event.brainRunId;
660
- }
661
- }
662
- } catch (e) {
663
- error = e as Error;
664
- }
665
-
666
- // Verify error was thrown
667
- expect(error?.message).toBe('Inner brain error');
668
-
669
- // Verify event sequence including error
670
- expect(events).toEqual([
671
- expect.objectContaining({
672
- type: BRAIN_EVENTS.START,
673
- brainTitle: 'Outer Brain',
674
- status: STATUS.RUNNING,
675
- brainRunId: mainBrainId,
676
- }),
677
- expect.objectContaining({
678
- type: BRAIN_EVENTS.STEP_STATUS,
679
- steps: expect.any(Array),
680
- }),
681
- expect.objectContaining({
682
- type: BRAIN_EVENTS.STEP_START,
683
- status: STATUS.RUNNING,
684
- stepTitle: 'First step',
685
- }),
686
- expect.objectContaining({
687
- type: BRAIN_EVENTS.STEP_STATUS,
688
- steps: expect.any(Array),
689
- }),
690
- expect.objectContaining({
691
- type: BRAIN_EVENTS.STEP_COMPLETE,
692
- status: STATUS.RUNNING,
693
- stepTitle: 'First step',
694
- }),
695
- expect.objectContaining({
696
- type: BRAIN_EVENTS.STEP_STATUS,
697
- steps: expect.any(Array),
698
- }),
699
- expect.objectContaining({
700
- type: BRAIN_EVENTS.STEP_START,
701
- status: STATUS.RUNNING,
702
- stepTitle: 'Run inner brain',
703
- }),
704
- expect.objectContaining({
705
- type: BRAIN_EVENTS.STEP_STATUS,
706
- steps: expect.any(Array),
707
- }),
708
- expect.objectContaining({
709
- type: BRAIN_EVENTS.START,
710
- brainTitle: 'Failing Inner Brain',
711
- status: STATUS.RUNNING,
712
- }),
713
- expect.objectContaining({
714
- type: BRAIN_EVENTS.STEP_STATUS,
715
- steps: expect.any(Array),
716
- }),
717
- expect.objectContaining({
718
- type: BRAIN_EVENTS.STEP_START,
719
- status: STATUS.RUNNING,
720
- stepTitle: 'Throw error',
721
- }),
722
- expect.objectContaining({
723
- type: BRAIN_EVENTS.STEP_STATUS,
724
- steps: expect.any(Array),
725
- }),
726
- expect.objectContaining({
727
- type: BRAIN_EVENTS.ERROR,
728
- brainTitle: 'Failing Inner Brain',
729
- status: STATUS.ERROR,
730
- error: expect.objectContaining({
731
- name: expect.any(String),
732
- message: expect.any(String),
733
- }),
734
- }),
735
- expect.objectContaining({
736
- type: BRAIN_EVENTS.STEP_STATUS,
737
- steps: expect.arrayContaining([
738
- expect.objectContaining({
739
- title: 'Throw error',
740
- status: STATUS.ERROR,
741
- }),
742
- ]),
743
- }),
744
- expect.objectContaining({
745
- type: BRAIN_EVENTS.ERROR,
746
- brainTitle: 'Outer Brain',
747
- status: STATUS.ERROR,
748
- error: expect.objectContaining({
749
- name: expect.any(String),
750
- message: expect.any(String),
751
- }),
752
- }),
753
- expect.objectContaining({
754
- type: BRAIN_EVENTS.STEP_STATUS,
755
- steps: expect.arrayContaining([
756
- expect.objectContaining({
757
- title: 'Run inner brain',
758
- status: STATUS.ERROR,
759
- }),
760
- ]),
761
- }),
762
- ]);
763
-
764
- // Find inner and outer error events by brainRunId
765
- const innerErrorEvent = events.find(
766
- (e) => e.type === BRAIN_EVENTS.ERROR && e.brainRunId !== mainBrainId
767
- ) as BrainErrorEvent<any>;
768
-
769
- const outerErrorEvent = events.find(
770
- (e) => e.type === BRAIN_EVENTS.ERROR && e.brainRunId === mainBrainId
771
- ) as BrainErrorEvent<any>;
772
-
773
- expect(innerErrorEvent.error).toEqual(
774
- expect.objectContaining({
775
- message: 'Inner brain error',
776
- })
777
- );
778
- expect(outerErrorEvent.error).toEqual(
779
- expect.objectContaining({
780
- message: 'Inner brain error',
781
- })
782
- );
783
- });
784
- });
785
-
786
- describe('step creation', () => {
787
- it('should create a step that updates state', async () => {
788
- const testBrain = brain('Simple Brain').step(
789
- 'Simple step',
790
- ({ state }) => ({
791
- ...state,
792
- count: 1,
793
- message: 'Count is now 1',
794
- })
795
- );
796
-
797
- const events = [];
798
- let finalState = {};
799
- for await (const event of testBrain.run({
800
- client: mockClient,
801
- })) {
802
- events.push(event);
803
- if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
804
- finalState = applyPatches(finalState, event.patch);
805
- }
806
- }
807
-
808
- // Skip checking events[0] (brain:start)
809
- // Skip checking events[1] (step:status)
810
-
811
- // Verify the step start event
812
- expect(events[2]).toEqual(
813
- expect.objectContaining({
814
- type: BRAIN_EVENTS.STEP_START,
815
- status: STATUS.RUNNING,
816
- stepTitle: 'Simple step',
817
- stepId: expect.any(String),
818
- options: {},
819
- })
820
- );
821
-
822
- // Verify the step status event (running)
823
- expect(events[3]).toEqual(
824
- expect.objectContaining({
825
- type: BRAIN_EVENTS.STEP_STATUS,
826
- steps: expect.any(Array),
827
- options: {},
828
- })
829
- );
830
- if (events[3].type === BRAIN_EVENTS.STEP_STATUS) {
831
- expect(events[3].steps[0].status).toBe(STATUS.RUNNING);
832
- }
833
-
834
- // Verify the step complete event
835
- expect(events[4]).toEqual(
836
- expect.objectContaining({
837
- type: BRAIN_EVENTS.STEP_COMPLETE,
838
- status: STATUS.RUNNING,
839
- stepTitle: 'Simple step',
840
- stepId: expect.any(String),
841
- patch: [
842
- {
843
- op: 'add',
844
- path: '/count',
845
- value: 1,
846
- },
847
- {
848
- op: 'add',
849
- path: '/message',
850
- value: 'Count is now 1',
851
- },
852
- ],
853
- options: {},
854
- })
855
- );
856
-
857
- expect(events[5]).toEqual(
858
- expect.objectContaining({
859
- type: BRAIN_EVENTS.STEP_STATUS,
860
- steps: [
861
- expect.objectContaining({
862
- title: 'Simple step',
863
- status: STATUS.COMPLETE,
864
- id: expect.any(String),
865
- }),
866
- ],
867
- options: {},
868
- })
869
- );
870
-
871
- // Verify the brain complete event
872
- expect(events[6]).toEqual(
873
- expect.objectContaining({
874
- type: BRAIN_EVENTS.COMPLETE,
875
- status: STATUS.COMPLETE,
876
- brainTitle: 'Simple Brain',
877
- options: {},
878
- })
879
- );
880
- // Verify the final state
881
- expect(finalState).toEqual({
882
- count: 1,
883
- message: 'Count is now 1',
884
- });
885
- });
886
-
887
- it('should maintain immutable results between steps', async () => {
888
- const testBrain = brain('Immutable Steps Brain')
889
- .step('First step', () => ({
890
- value: 1,
891
- }))
892
- .step('Second step', ({ state }) => {
893
- // Attempt to modify previous step's state
894
- state.value = 99;
895
- return {
896
- value: 2,
897
- };
898
- });
899
-
900
- let finalState = {};
901
- const patches = [];
902
- for await (const event of testBrain.run({
903
- client: mockClient,
904
- })) {
905
- if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
906
- patches.push(...event.patch);
907
- }
908
- }
909
-
910
- // Apply all patches to the initial state
911
- finalState = applyPatches(finalState, patches);
912
-
913
- // Verify the final state
914
- expect(finalState).toEqual({ value: 2 });
915
- });
916
- });
917
-
918
- describe('brain resumption', () => {
919
- const mockClient = {
920
- generateObject: jest.fn(),
921
- };
922
-
923
- it('should resume brain from the correct step when given initialCompletedSteps', async () => {
924
- const executedSteps: string[] = [];
925
- const threeStepBrain = brain('Three Step Brain')
926
- .step('Step 1', ({ state }) => {
927
- executedSteps.push('Step 1');
928
- return { ...state, value: 2 };
929
- })
930
- .step('Step 2', ({ state }) => {
931
- executedSteps.push('Step 2');
932
- return { ...state, value: state.value + 10 };
933
- })
934
- .step('Step 3', ({ state }) => {
935
- executedSteps.push('Step 3');
936
- return { ...state, value: state.value * 3 };
937
- });
938
-
939
- // First run to get the first step completed with initial state
940
- let initialCompletedSteps: SerializedStep[] = []; // Use the correct type
941
- const initialState = { initialValue: true };
942
- let firstStepState: State = initialState;
943
- let allStepsInfo: SerializedStepStatus[] = []; // Explicit type annotation needed
944
-
945
- // Run brain until we get the first step completed
946
- for await (const event of threeStepBrain.run({
947
- client: mockClient as ObjectGenerator,
948
- initialState,
949
- })) {
950
- // Capture the full step list from the first status event
951
- if (event.type === BRAIN_EVENTS.STEP_STATUS) {
952
- allStepsInfo = event.steps; // Direct assignment, type is SerializedStepStatus[]
953
- }
954
-
955
- if (
956
- event.type === BRAIN_EVENTS.STEP_COMPLETE &&
957
- event.stepTitle === 'Step 1'
958
- ) {
959
- firstStepState = applyPatches(firstStepState, [event.patch]);
960
- // Construct initialCompletedSteps with the full data for completed steps
961
- initialCompletedSteps = allStepsInfo.map((stepInfo, index) => {
962
- if (index === 0) {
963
- // If it's Step 1
964
- return { ...stepInfo, status: STATUS.COMPLETE, patch: event.patch };
965
- } else {
966
- return { ...stepInfo, status: STATUS.PENDING, patch: undefined };
967
- }
968
- });
969
- break; // Stop after first step
970
- }
971
- }
972
-
973
- // Clear executed steps array
974
- executedSteps.length = 0;
975
-
976
- // Resume brain with first step completed
977
- let resumedState: State | undefined;
978
- if (!initialCompletedSteps)
979
- throw new Error('Expected initialCompletedSteps');
980
-
981
- for await (const event of threeStepBrain.run({
982
- client: mockClient as ObjectGenerator,
983
- initialState,
984
- initialCompletedSteps,
985
- brainRunId: 'test-run-id',
986
- })) {
987
- if (event.type === BRAIN_EVENTS.RESTART) {
988
- resumedState = event.initialState;
989
- } else if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
990
- resumedState = applyPatches(resumedState!, [event.patch]);
991
- }
992
- }
993
-
994
- // Verify only steps 2 and 3 were executed
995
- expect(executedSteps).toEqual(['Step 2', 'Step 3']);
996
- expect(executedSteps).not.toContain('Step 1');
997
-
998
- // Verify the final state after all steps complete
999
- expect(resumedState).toEqual({
1000
- value: 36,
1001
- initialValue: true,
1002
- });
1003
- });
1004
- });
1005
-
1006
- describe('nested brains', () => {
1007
- it('should execute nested brains and yield all inner brain events', async () => {
1008
- // Create an inner brain that will be nested
1009
- const innerBrain = brain<{}, { value: number }>('Inner Brain').step(
1010
- 'Double value',
1011
- ({ state }) => ({
1012
- inner: true,
1013
- value: state.value * 2,
1014
- })
1015
- );
1016
-
1017
- // Create outer brain that uses the inner brain
1018
- const outerBrain = brain('Outer Brain')
1019
- .step('Set prefix', () => ({ prefix: 'test-' }))
1020
- .brain(
1021
- 'Run inner brain',
1022
- innerBrain,
1023
- ({ state, brainState }) => ({
1024
- ...state,
1025
- innerResult: brainState.value,
1026
- }),
1027
- () => ({ value: 5 })
1028
- );
1029
-
1030
- const events: BrainEvent<any>[] = [];
1031
- for await (const event of outerBrain.run({
1032
- client: mockClient,
1033
- })) {
1034
- events.push(event);
1035
- }
1036
-
1037
- // Verify all events are yielded in correct order
1038
- expect(
1039
- events.map((e) => ({
1040
- type: e.type,
1041
- brainTitle: 'brainTitle' in e ? e.brainTitle : undefined,
1042
- status: 'status' in e ? e.status : undefined,
1043
- stepTitle: 'stepTitle' in e ? e.stepTitle : undefined,
1044
- }))
1045
- ).toEqual([
1046
- // Outer brain start
1047
- {
1048
- type: BRAIN_EVENTS.START,
1049
- brainTitle: 'Outer Brain',
1050
- status: STATUS.RUNNING,
1051
- stepTitle: undefined,
1052
- },
1053
- // Initial step status for outer brain
1054
- {
1055
- type: BRAIN_EVENTS.STEP_STATUS,
1056
- brainTitle: undefined,
1057
- status: undefined,
1058
- stepTitle: undefined,
1059
- },
1060
- // First step of outer brain
1061
- {
1062
- type: BRAIN_EVENTS.STEP_START,
1063
- brainTitle: undefined,
1064
- status: STATUS.RUNNING,
1065
- stepTitle: 'Set prefix',
1066
- },
1067
- // First step status (running)
1068
- {
1069
- type: BRAIN_EVENTS.STEP_STATUS,
1070
- brainTitle: undefined,
1071
- status: undefined,
1072
- stepTitle: undefined,
1073
- },
1074
- {
1075
- type: BRAIN_EVENTS.STEP_COMPLETE,
1076
- brainTitle: undefined,
1077
- status: STATUS.RUNNING,
1078
- stepTitle: 'Set prefix',
1079
- },
1080
- {
1081
- type: BRAIN_EVENTS.STEP_STATUS,
1082
- brainTitle: undefined,
1083
- status: undefined,
1084
- stepTitle: undefined,
1085
- },
1086
- {
1087
- type: BRAIN_EVENTS.STEP_START,
1088
- brainTitle: undefined,
1089
- status: STATUS.RUNNING,
1090
- stepTitle: 'Run inner brain',
1091
- },
1092
- // Step status for inner brain (running)
1093
- {
1094
- type: BRAIN_EVENTS.STEP_STATUS,
1095
- brainTitle: undefined,
1096
- status: undefined,
1097
- stepTitle: undefined,
1098
- },
1099
- // Inner brain start
1100
- {
1101
- type: BRAIN_EVENTS.START,
1102
- brainTitle: 'Inner Brain',
1103
- status: STATUS.RUNNING,
1104
- stepTitle: undefined,
1105
- },
1106
- // Initial step status for inner brain
1107
- {
1108
- type: BRAIN_EVENTS.STEP_STATUS,
1109
- brainTitle: undefined,
1110
- status: undefined,
1111
- stepTitle: undefined,
1112
- },
1113
- // Inner brain step
1114
- {
1115
- type: BRAIN_EVENTS.STEP_START,
1116
- brainTitle: undefined,
1117
- status: STATUS.RUNNING,
1118
- stepTitle: 'Double value',
1119
- },
1120
- // Inner brain step status (running)
1121
- {
1122
- type: BRAIN_EVENTS.STEP_STATUS,
1123
- brainTitle: undefined,
1124
- status: undefined,
1125
- stepTitle: undefined,
1126
- },
1127
- {
1128
- type: BRAIN_EVENTS.STEP_COMPLETE,
1129
- brainTitle: undefined,
1130
- status: STATUS.RUNNING,
1131
- stepTitle: 'Double value',
1132
- },
1133
- {
1134
- type: BRAIN_EVENTS.STEP_STATUS,
1135
- brainTitle: undefined,
1136
- status: undefined,
1137
- stepTitle: undefined,
1138
- },
1139
- {
1140
- type: BRAIN_EVENTS.COMPLETE,
1141
- brainTitle: 'Inner Brain',
1142
- status: STATUS.COMPLETE,
1143
- stepTitle: undefined,
1144
- },
1145
- // Outer brain nested step completion
1146
- {
1147
- type: BRAIN_EVENTS.STEP_COMPLETE,
1148
- brainTitle: undefined,
1149
- status: STATUS.RUNNING,
1150
- stepTitle: 'Run inner brain',
1151
- },
1152
- {
1153
- type: BRAIN_EVENTS.STEP_STATUS,
1154
- brainTitle: undefined,
1155
- status: undefined,
1156
- stepTitle: undefined,
1157
- },
1158
- // Outer brain completion
1159
- {
1160
- type: BRAIN_EVENTS.COMPLETE,
1161
- brainTitle: 'Outer Brain',
1162
- status: STATUS.COMPLETE,
1163
- stepTitle: undefined,
1164
- },
1165
- ]);
1166
-
1167
- // Verify states are passed correctly
1168
- let innerState: State = { value: 5 }; // Match the initial state from the brain
1169
- let outerState = {};
1170
-
1171
- for (const event of events) {
1172
- if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
1173
- if (event.stepTitle === 'Double value') {
1174
- innerState = applyPatches(innerState, [event.patch]);
1175
- } else {
1176
- outerState = applyPatches(outerState, [event.patch]);
1177
- }
1178
- }
1179
- }
1180
-
1181
- // Verify final states
1182
- expect(innerState).toEqual({
1183
- inner: true,
1184
- value: 10,
1185
- });
1186
-
1187
- expect(outerState).toEqual({
1188
- prefix: 'test-',
1189
- innerResult: 10,
1190
- });
1191
- });
1192
-
1193
- it('should handle errors in nested brains and propagate them up', async () => {
1194
- // Create an inner brain that will throw an error
1195
- const innerBrain = brain<{}, { inner: boolean; value?: number }>(
1196
- 'Failing Inner Brain'
1197
- ).step('Throw error', (): { value: number } => {
1198
- throw new Error('Inner brain error');
1199
- });
1200
-
1201
- // Create outer brain that uses the failing inner brain
1202
- const outerBrain = brain('Outer Brain')
1203
- .step('First step', () => ({ step: 'first' }))
1204
- .brain(
1205
- 'Run inner brain',
1206
- innerBrain,
1207
- ({ state, brainState }) => ({
1208
- ...state,
1209
- step: 'second',
1210
- innerResult: brainState.value,
1211
- }),
1212
- () => ({ value: 5 })
1213
- );
1214
-
1215
- const events: BrainEvent<any>[] = [];
1216
- let error: Error | undefined;
1217
- let mainBrainId: string | undefined;
1218
-
1219
- try {
1220
- for await (const event of outerBrain.run({
1221
- client: mockClient,
1222
- })) {
1223
- events.push(event);
1224
- if (event.type === BRAIN_EVENTS.START && !mainBrainId) {
1225
- mainBrainId = event.brainRunId;
1226
- }
1227
- }
1228
- } catch (e) {
1229
- error = e as Error;
1230
- }
1231
-
1232
- // Verify error was thrown
1233
- expect(error?.message).toBe('Inner brain error');
1234
-
1235
- // Verify event sequence including error
1236
- expect(events).toEqual([
1237
- expect.objectContaining({
1238
- type: BRAIN_EVENTS.START,
1239
- brainTitle: 'Outer Brain',
1240
- status: STATUS.RUNNING,
1241
- brainRunId: mainBrainId,
1242
- }),
1243
- expect.objectContaining({
1244
- type: BRAIN_EVENTS.STEP_STATUS,
1245
- steps: expect.any(Array),
1246
- }),
1247
- expect.objectContaining({
1248
- type: BRAIN_EVENTS.STEP_START,
1249
- status: STATUS.RUNNING,
1250
- stepTitle: 'First step',
1251
- }),
1252
- expect.objectContaining({
1253
- type: BRAIN_EVENTS.STEP_STATUS,
1254
- steps: expect.any(Array),
1255
- }),
1256
- expect.objectContaining({
1257
- type: BRAIN_EVENTS.STEP_COMPLETE,
1258
- status: STATUS.RUNNING,
1259
- stepTitle: 'First step',
1260
- }),
1261
- expect.objectContaining({
1262
- type: BRAIN_EVENTS.STEP_STATUS,
1263
- steps: expect.any(Array),
1264
- }),
1265
- expect.objectContaining({
1266
- type: BRAIN_EVENTS.STEP_START,
1267
- status: STATUS.RUNNING,
1268
- stepTitle: 'Run inner brain',
1269
- }),
1270
- expect.objectContaining({
1271
- type: BRAIN_EVENTS.STEP_STATUS,
1272
- steps: expect.any(Array),
1273
- }),
1274
- expect.objectContaining({
1275
- type: BRAIN_EVENTS.START,
1276
- brainTitle: 'Failing Inner Brain',
1277
- status: STATUS.RUNNING,
1278
- }),
1279
- expect.objectContaining({
1280
- type: BRAIN_EVENTS.STEP_STATUS,
1281
- steps: expect.any(Array),
1282
- }),
1283
- expect.objectContaining({
1284
- type: BRAIN_EVENTS.STEP_START,
1285
- status: STATUS.RUNNING,
1286
- stepTitle: 'Throw error',
1287
- }),
1288
- expect.objectContaining({
1289
- type: BRAIN_EVENTS.STEP_STATUS,
1290
- steps: expect.any(Array),
1291
- }),
1292
- expect.objectContaining({
1293
- type: BRAIN_EVENTS.ERROR,
1294
- brainTitle: 'Failing Inner Brain',
1295
- status: STATUS.ERROR,
1296
- error: expect.objectContaining({
1297
- name: expect.any(String),
1298
- message: expect.any(String),
1299
- }),
1300
- }),
1301
- expect.objectContaining({
1302
- type: BRAIN_EVENTS.STEP_STATUS,
1303
- steps: expect.arrayContaining([
1304
- expect.objectContaining({
1305
- title: 'Throw error',
1306
- status: STATUS.ERROR,
1307
- }),
1308
- ]),
1309
- }),
1310
- expect.objectContaining({
1311
- type: BRAIN_EVENTS.ERROR,
1312
- brainTitle: 'Outer Brain',
1313
- status: STATUS.ERROR,
1314
- error: expect.objectContaining({
1315
- name: expect.any(String),
1316
- message: expect.any(String),
1317
- }),
1318
- }),
1319
- expect.objectContaining({
1320
- type: BRAIN_EVENTS.STEP_STATUS,
1321
- steps: expect.arrayContaining([
1322
- expect.objectContaining({
1323
- title: 'Run inner brain',
1324
- status: STATUS.ERROR,
1325
- }),
1326
- ]),
1327
- }),
1328
- ]);
1329
-
1330
- // Find inner and outer error events by brainRunId
1331
- const innerErrorEvent = events.find(
1332
- (e) => e.type === BRAIN_EVENTS.ERROR && e.brainRunId !== mainBrainId
1333
- ) as BrainErrorEvent<any>;
1334
-
1335
- const outerErrorEvent = events.find(
1336
- (e) => e.type === BRAIN_EVENTS.ERROR && e.brainRunId === mainBrainId
1337
- ) as BrainErrorEvent<any>;
1338
-
1339
- expect(innerErrorEvent.error).toEqual(
1340
- expect.objectContaining({
1341
- message: 'Inner brain error',
1342
- })
1343
- );
1344
- expect(outerErrorEvent.error).toEqual(
1345
- expect.objectContaining({
1346
- message: 'Inner brain error',
1347
- })
1348
- );
1349
- });
1350
-
1351
- it('should include patches in step status events for inner brain steps', async () => {
1352
- interface InnerState extends State {
1353
- value: number;
1354
- }
1355
-
1356
- interface OuterState extends State {
1357
- value: number;
1358
- result?: number;
1359
- }
1360
-
1361
- // Create an inner brain that modifies state
1362
- const innerBrain = brain<{}, InnerState>('Inner Brain').step(
1363
- 'Double value',
1364
- ({ state }) => ({
1365
- ...state,
1366
- value: state.value * 2,
1367
- })
1368
- );
1369
-
1370
- // Create outer brain that uses the inner brain
1371
- const outerBrain = brain<{}, OuterState>('Outer Brain')
1372
- .step('Set initial', () => ({
1373
- value: 5,
1374
- }))
1375
- .brain(
1376
- 'Run inner brain',
1377
- innerBrain,
1378
- ({ state, brainState }) => ({
1379
- ...state,
1380
- result: brainState.value,
1381
- }),
1382
- (state) => ({ value: state.value })
1383
- );
1384
-
1385
- // Run brain and collect step status events
1386
- let finalStepStatus;
1387
- for await (const event of outerBrain.run({
1388
- client: mockClient,
1389
- })) {
1390
- if (event.type === BRAIN_EVENTS.STEP_STATUS) {
1391
- finalStepStatus = event;
1392
- }
1393
- }
1394
-
1395
- // Verify step status contains patches for all steps including the inner brain step
1396
- expect(finalStepStatus?.steps).toEqual([
1397
- expect.objectContaining({
1398
- title: 'Set initial',
1399
- status: STATUS.COMPLETE,
1400
- }),
1401
- expect.objectContaining({
1402
- title: 'Run inner brain',
1403
- status: STATUS.COMPLETE,
1404
- }),
1405
- ]);
1406
- });
1407
- });
1408
-
1409
- describe('brain options', () => {
1410
- it('should pass options through to brain events', async () => {
1411
- const testBrain = brain<{ testOption: string }>('Options Brain').step(
1412
- 'Simple step',
1413
- ({ state, options }) => ({
1414
- value: 1,
1415
- passedOption: options.testOption,
1416
- })
1417
- );
1418
-
1419
- const brainOptions = {
1420
- testOption: 'test-value',
1421
- };
1422
-
1423
- let finalEvent, finalStepStatus;
1424
- for await (const event of testBrain.run({
1425
- client: mockClient,
1426
- options: brainOptions,
1427
- })) {
1428
- if (event.type === BRAIN_EVENTS.STEP_STATUS) {
1429
- finalStepStatus = event;
1430
- } else {
1431
- finalEvent = event;
1432
- }
1433
- }
1434
-
1435
- expect(finalEvent).toEqual(
1436
- expect.objectContaining({
1437
- type: BRAIN_EVENTS.COMPLETE,
1438
- status: STATUS.COMPLETE,
1439
- brainTitle: 'Options Brain',
1440
- brainDescription: undefined,
1441
- options: brainOptions,
1442
- })
1443
- );
1444
- expect(finalStepStatus).toEqual(
1445
- expect.objectContaining({
1446
- type: BRAIN_EVENTS.STEP_STATUS,
1447
- steps: [
1448
- expect.objectContaining({
1449
- title: 'Simple step',
1450
- status: STATUS.COMPLETE,
1451
- }),
1452
- ],
1453
- options: brainOptions,
1454
- })
1455
- );
1456
- });
1457
-
1458
- it('should provide empty object as default options', async () => {
1459
- const testBrain = brain('Default Options Brain').step(
1460
- 'Simple step',
1461
- ({ options }) => ({
1462
- hasOptions: Object.keys(options).length === 0,
1463
- })
1464
- );
1465
-
1466
- const brainRun = testBrain.run({
1467
- client: mockClient,
1468
- });
1469
-
1470
- // Skip start event
1471
- await brainRun.next();
1472
-
1473
- // Skip initial step status event
1474
- await brainRun.next();
1475
-
1476
- // Check step start
1477
- const stepStartResult = await brainRun.next();
1478
- expect(stepStartResult.value).toEqual(
1479
- expect.objectContaining({
1480
- options: {},
1481
- type: BRAIN_EVENTS.STEP_START,
1482
- })
1483
- );
1484
-
1485
- // Check step status (running)
1486
- const stepStatusRunning = await brainRun.next();
1487
- expect(stepStatusRunning.value).toEqual(
1488
- expect.objectContaining({
1489
- type: BRAIN_EVENTS.STEP_STATUS,
1490
- steps: expect.any(Array),
1491
- })
1492
- );
1493
- if (stepStatusRunning.value.type === BRAIN_EVENTS.STEP_STATUS) {
1494
- expect(stepStatusRunning.value.steps[0].status).toBe(STATUS.RUNNING);
1495
- }
1496
-
1497
- // Check step completion
1498
- const stepResult = await brainRun.next();
1499
- expect(stepResult.value).toEqual(
1500
- expect.objectContaining({
1501
- type: BRAIN_EVENTS.STEP_COMPLETE,
1502
- stepTitle: 'Simple step',
1503
- options: {},
1504
- })
1505
- );
1506
- });
1507
- });
1508
-
1509
- describe('services support', () => {
1510
- it('should allow adding custom services to brains', async () => {
1511
- // Create a brain with services
1512
- const testBrain = brain('Services Test')
1513
- .withServices({
1514
- logger: testLogger,
1515
- })
1516
- .step('Use service', ({ state, logger }) => {
1517
- logger.log('Test service called');
1518
- return { serviceUsed: true };
1519
- });
1520
-
1521
- // Run the brain and collect events
1522
- let finalState = {};
1523
- for await (const event of testBrain.run({
1524
- client: mockClient,
1525
- })) {
1526
- if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
1527
- finalState = applyPatches(finalState, [event.patch]);
1528
- }
1529
- }
1530
-
1531
- // Verify the service was called
1532
- expect(testLogger.log).toHaveBeenCalledWith('Test service called');
1533
-
1534
- // Verify the state was updated
1535
- expect(finalState).toEqual({ serviceUsed: true });
1536
- });
1537
- });
1538
-
1539
- describe('type inference', () => {
1540
- it('should correctly infer complex brain state types', async () => {
1541
- // Create an inner brain that uses the shared options type
1542
- const innerBrain = brain<{ features: string[] }>('Inner Type Test').step(
1543
- 'Process features',
1544
- ({ options }) => ({
1545
- processedValue: options.features.includes('fast') ? 100 : 42,
1546
- featureCount: options.features.length,
1547
- })
1548
- );
1549
-
1550
- // Create a complex brain using multiple features
1551
- const complexBrain = brain<{ features: string[] }>('Complex Type Test')
1552
- .step('First step', ({ options }) => ({
1553
- initialFeatures: options.features,
1554
- value: 42,
1555
- }))
1556
- .brain(
1557
- 'Nested brain',
1558
- innerBrain,
1559
- ({ state, brainState }) => ({
1560
- ...state,
1561
- processedValue: brainState.processedValue,
1562
- totalFeatures: brainState.featureCount,
1563
- }),
1564
- () => ({
1565
- processedValue: 0,
1566
- featureCount: 0,
1567
- })
1568
- )
1569
- .step('Final step', ({ state }) => ({
1570
- ...state,
1571
- completed: true,
1572
- }));
1573
-
1574
- // Type test setup
1575
- type ExpectedState = {
1576
- initialFeatures: string[];
1577
- value: number;
1578
- processedValue: number;
1579
- totalFeatures: number;
1580
- completed: true;
1581
- };
1582
-
1583
- type ActualState = Parameters<
1584
- Parameters<(typeof complexBrain)['step']>[1]
1585
- >[0]['state'];
1586
-
1587
- type TypeTest = AssertEquals<ActualState, ExpectedState>;
1588
- const _typeAssert: TypeTest = true;
1589
-
1590
- // Collect all events
1591
- const events = [];
1592
- let finalStepStatus,
1593
- finalState = {};
1594
- let mainBrainId: string | undefined;
1595
-
1596
- for await (const event of complexBrain.run({
1597
- client: mockClient,
1598
- options: { features: ['fast', 'secure'] },
1599
- })) {
1600
- events.push(event);
1601
-
1602
- // Capture the main brain's ID from its start event
1603
- if (event.type === BRAIN_EVENTS.START && !mainBrainId) {
1604
- mainBrainId = event.brainRunId;
1605
- }
1606
-
1607
- if (event.type === BRAIN_EVENTS.STEP_STATUS) {
1608
- finalStepStatus = event;
1609
- } else if (
1610
- event.type === BRAIN_EVENTS.STEP_COMPLETE &&
1611
- event.brainRunId === mainBrainId // Only process events from main brain
1612
- ) {
1613
- finalState = applyPatches(finalState, [event.patch]);
1614
- }
1615
- }
1616
-
1617
- // Verify brain start event
1618
- expect(events[0]).toEqual(
1619
- expect.objectContaining({
1620
- type: BRAIN_EVENTS.START,
1621
- status: STATUS.RUNNING,
1622
- brainTitle: 'Complex Type Test',
1623
- brainDescription: undefined,
1624
- options: { features: ['fast', 'secure'] },
1625
- brainRunId: mainBrainId,
1626
- })
1627
- );
1628
-
1629
- // Verify inner brain events are included
1630
- const innerStartEvent = events.find(
1631
- (e) =>
1632
- e.type === BRAIN_EVENTS.START &&
1633
- 'brainRunId' in e &&
1634
- e.brainRunId !== mainBrainId
1635
- );
1636
- expect(innerStartEvent).toEqual(
1637
- expect.objectContaining({
1638
- type: BRAIN_EVENTS.START,
1639
- status: STATUS.RUNNING,
1640
- brainTitle: 'Inner Type Test',
1641
- options: { features: ['fast', 'secure'] },
1642
- })
1643
- );
1644
-
1645
- // Verify the final step status
1646
- if (!finalStepStatus) throw new Error('Expected final step status event');
1647
- const lastStep = finalStepStatus.steps[finalStepStatus.steps.length - 1];
1648
- expect(lastStep.status).toBe(STATUS.COMPLETE);
1649
- expect(lastStep.title).toBe('Final step');
1650
-
1651
- expect(finalState).toEqual({
1652
- initialFeatures: ['fast', 'secure'],
1653
- value: 42,
1654
- processedValue: 100,
1655
- totalFeatures: 2,
1656
- completed: true,
1657
- });
1658
- });
1659
-
1660
- it('should correctly infer brain reducer state types', async () => {
1661
- // Create an inner brain with a specific state shape
1662
- const innerBrain = brain('Inner State Test').step('Inner step', () => ({
1663
- innerValue: 42,
1664
- metadata: { processed: true },
1665
- }));
1666
-
1667
- // Create outer brain to test reducer type inference
1668
- const outerBrain = brain('Outer State Test')
1669
- .step('First step', () => ({
1670
- outerValue: 100,
1671
- status: 'ready',
1672
- }))
1673
- .brain(
1674
- 'Nested brain',
1675
- innerBrain,
1676
- ({ state, brainState }) => {
1677
- // Type assertion for outer state
1678
- type ExpectedOuterState = {
1679
- outerValue: number;
1680
- status: string;
1681
- };
1682
- type ActualOuterState = typeof state;
1683
- type OuterStateTest = AssertEquals<
1684
- ActualOuterState,
1685
- ExpectedOuterState
1686
- >;
1687
- const _outerAssert: OuterStateTest = true;
1688
-
1689
- // Type assertion for inner brain state
1690
- type ExpectedInnerState = {
1691
- innerValue: number;
1692
- metadata: { processed: true };
1693
- };
1694
- type ActualInnerState = typeof brainState;
1695
- type InnerStateTest = AssertEquals<
1696
- ActualInnerState,
1697
- ExpectedInnerState
1698
- >;
1699
- const _innerAssert: InnerStateTest = true;
1700
-
1701
- return {
1702
- ...state,
1703
- innerResult: brainState.innerValue,
1704
- processed: brainState.metadata.processed,
1705
- };
1706
- },
1707
- () => ({} as { innerValue: number; metadata: { processed: boolean } })
1708
- );
1709
-
1710
- // Run the brain to verify runtime behavior
1711
- let finalState = {};
1712
- let mainBrainId: string | undefined;
1713
-
1714
- for await (const event of outerBrain.run({
1715
- client: mockClient,
1716
- })) {
1717
- if (event.type === BRAIN_EVENTS.START && !mainBrainId) {
1718
- mainBrainId = event.brainRunId;
1719
- }
1720
- if (
1721
- event.type === BRAIN_EVENTS.STEP_COMPLETE &&
1722
- event.brainRunId === mainBrainId
1723
- ) {
1724
- finalState = applyPatches(finalState, [event.patch]);
1725
- }
1726
- }
1727
-
1728
- expect(finalState).toEqual({
1729
- outerValue: 100,
1730
- status: 'ready',
1731
- innerResult: 42,
1732
- processed: true,
1733
- });
1734
- });
1735
-
1736
- it('should correctly infer step action state types', async () => {
1737
- const testBrain = brain('Action State Test')
1738
- .step('First step', () => ({
1739
- count: 1,
1740
- metadata: { created: new Date().toISOString() },
1741
- }))
1742
- .step('Second step', ({ state }) => {
1743
- // Type assertion for action state
1744
- type ExpectedState = {
1745
- count: number;
1746
- metadata: { created: string };
1747
- };
1748
- type ActualState = typeof state;
1749
- type StateTest = AssertEquals<ActualState, ExpectedState>;
1750
- const _stateAssert: StateTest = true;
1751
-
1752
- return {
1753
- ...state,
1754
- count: state.count + 1,
1755
- metadata: {
1756
- ...state.metadata,
1757
- updated: new Date().toISOString(),
1758
- },
1759
- };
1760
- });
1761
-
1762
- // Run the brain to verify runtime behavior
1763
- let finalState = {};
1764
- let mainBrainId: string | undefined;
1765
-
1766
- for await (const event of testBrain.run({
1767
- client: mockClient,
1768
- })) {
1769
- if (event.type === BRAIN_EVENTS.START && !mainBrainId) {
1770
- mainBrainId = event.brainRunId;
1771
- }
1772
- if (
1773
- event.type === BRAIN_EVENTS.STEP_COMPLETE &&
1774
- event.brainRunId === mainBrainId
1775
- ) {
1776
- finalState = applyPatches(finalState, [event.patch]);
1777
- }
1778
- }
1779
-
1780
- expect(finalState).toMatchObject({
1781
- count: 2,
1782
- metadata: {
1783
- created: expect.any(String),
1784
- updated: expect.any(String),
1785
- },
1786
- });
1787
- });
1788
-
1789
- it('should correctly infer prompt response types in subsequent steps', async () => {
1790
- const testBrain = brain('Prompt Type Test')
1791
- .prompt('Get user info', {
1792
- template: () => "What is the user's info?",
1793
- outputSchema: {
1794
- schema: z.object({ name: z.string(), age: z.number() }),
1795
- name: 'userInfo' as const, // Must be const or type inference breaks
1796
- },
1797
- })
1798
- .step('Use response', ({ state }) => {
1799
- // Type assertion to verify state includes userInfo
1800
- type ExpectedState = {
1801
- userInfo: {
1802
- name: string;
1803
- age: number;
1804
- };
1805
- };
1806
- type ActualState = typeof state;
1807
- type StateTest = AssertEquals<ActualState, ExpectedState>;
1808
- const _stateAssert: StateTest = true;
1809
-
1810
- return {
1811
- ...state,
1812
- greeting: `Hello ${state.userInfo.name}, you are ${state.userInfo.age} years old`,
1813
- };
1814
- });
1815
-
1816
- // Mock the client response
1817
- mockClient.generateObject.mockResolvedValueOnce({
1818
- name: 'Test User',
1819
- age: 30,
1820
- });
1821
-
1822
- // Run brain and collect final state
1823
- let finalState = {};
1824
- for await (const event of testBrain.run({
1825
- client: mockClient,
1826
- })) {
1827
- if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
1828
- finalState = applyPatches(finalState, [event.patch]);
1829
- }
1830
- }
1831
-
1832
- // Verify the brain executed correctly
1833
- expect(finalState).toEqual({
1834
- userInfo: {
1835
- name: 'Test User',
1836
- age: 30,
1837
- },
1838
- greeting: 'Hello Test User, you are 30 years old',
1839
- });
1840
- });
1841
-
1842
- it('should correctly handle prompt reduce function', async () => {
1843
- const testBrain = brain('Prompt Reduce Test').prompt(
1844
- 'Get numbers',
1845
- {
1846
- template: () => 'Give me some numbers',
1847
- outputSchema: {
1848
- schema: z.object({ numbers: z.array(z.number()) }),
1849
- name: 'numbersResponse' as const,
1850
- },
1851
- },
1852
- ({ state, response, options }) => ({
1853
- ...state,
1854
- numbersResponse: response, // Include the response explicitly
1855
- sum: response.numbers.reduce((a, b) => a + b, 0),
1856
- count: response.numbers.length,
1857
- })
1858
- );
1859
-
1860
- // Mock the client response
1861
- mockClient.generateObject.mockResolvedValueOnce({
1862
- numbers: [1, 2, 3, 4, 5],
1863
- });
1864
-
1865
- // Run brain and collect final state
1866
- let finalState = {};
1867
- for await (const event of testBrain.run({
1868
- client: mockClient,
1869
- })) {
1870
- if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
1871
- finalState = applyPatches(finalState, [event.patch]);
1872
- }
1873
- }
1874
-
1875
- // Verify the brain executed correctly with reduced state
1876
- expect(finalState).toEqual({
1877
- numbersResponse: {
1878
- numbers: [1, 2, 3, 4, 5],
1879
- },
1880
- sum: 15,
1881
- count: 5,
1882
- });
1883
-
1884
- // Verify type inference works correctly
1885
- type ExpectedState = {
1886
- numbersResponse: {
1887
- numbers: number[];
1888
- };
1889
- sum: number;
1890
- count: number;
1891
- };
1892
-
1893
- type ActualState = Parameters<
1894
- Parameters<(typeof testBrain)['step']>[1]
1895
- >[0]['state'];
1896
-
1897
- type TypeTest = AssertEquals<ActualState, ExpectedState>;
1898
- const _typeAssert: TypeTest = true;
1899
- });
1900
- });
1901
-
1902
- describe('brain structure', () => {
1903
- it('should expose brain structure with steps', () => {
1904
- const testBrain = brain({
1905
- title: 'Test Brain',
1906
- description: 'A test brain description',
1907
- })
1908
- .step('First step', ({ state }) => ({ ...state, step1: true }))
1909
- .step('Second step', ({ state }) => ({ ...state, step2: true }))
1910
- .step('Third step', ({ state }) => ({ ...state, step3: true }));
1911
-
1912
- const structure = testBrain.structure;
1913
-
1914
- expect(structure).toEqual({
1915
- title: 'Test Brain',
1916
- description: 'A test brain description',
1917
- steps: [
1918
- { type: 'step', title: 'First step' },
1919
- { type: 'step', title: 'Second step' },
1920
- { type: 'step', title: 'Third step' },
1921
- ],
1922
- });
1923
- });
1924
-
1925
- it('should expose nested brain structure recursively', () => {
1926
- const innerBrain = brain({
1927
- title: 'Inner Brain',
1928
- description: 'An inner brain',
1929
- })
1930
- .step('Inner step 1', ({ state }) => ({ ...state, inner1: true }))
1931
- .step('Inner step 2', ({ state }) => ({ ...state, inner2: true }));
1932
-
1933
- const outerBrain = brain({
1934
- title: 'Outer Brain',
1935
- description: 'An outer brain',
1936
- })
1937
- .step('Outer step 1', ({ state }) => ({ ...state, outer1: true }))
1938
- .brain('Run inner brain', innerBrain, ({ brainState }) => ({
1939
- result: brainState,
1940
- }))
1941
- .step('Outer step 2', ({ state }) => ({ ...state, outer2: true }));
1942
-
1943
- const structure = outerBrain.structure;
1944
-
1945
- expect(structure).toEqual({
1946
- title: 'Outer Brain',
1947
- description: 'An outer brain',
1948
- steps: [
1949
- { type: 'step', title: 'Outer step 1' },
1950
- {
1951
- type: 'brain',
1952
- title: 'Run inner brain',
1953
- innerBrain: {
1954
- title: 'Inner Brain',
1955
- description: 'An inner brain',
1956
- steps: [
1957
- { type: 'step', title: 'Inner step 1' },
1958
- { type: 'step', title: 'Inner step 2' },
1959
- ],
1960
- },
1961
- },
1962
- { type: 'step', title: 'Outer step 2' },
1963
- ],
1964
- });
1965
- });
1966
-
1967
- it('should handle brain without description', () => {
1968
- const testBrain = brain('No Description Brain').step(
1969
- 'Only step',
1970
- ({ state }) => state
1971
- );
1972
-
1973
- const structure = testBrain.structure;
1974
-
1975
- expect(structure).toEqual({
1976
- title: 'No Description Brain',
1977
- description: undefined,
1978
- steps: [{ type: 'step', title: 'Only step' }],
1979
- });
1980
- });
1981
- });