@positronic/core 0.0.1 → 0.0.2

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
@@ -0,0 +1,740 @@
1
+ import { z } from 'zod';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import type { ObjectGenerator } from '../clients/types.js';
4
+ import type { State, JsonPatch } from './types.js';
5
+ import { STATUS, BRAIN_EVENTS } from './constants.js';
6
+ import { createPatch, applyPatches } from './json-patch.js';
7
+ import type { Resources } from '../resources/resources.js';
8
+
9
+ export type SerializedError = {
10
+ name: string;
11
+ message: string;
12
+ stack?: string;
13
+ };
14
+
15
+ // New Event Type System
16
+ // Base event interface with only type and options
17
+ interface BaseEvent<TOptions extends object = object> {
18
+ type: (typeof BRAIN_EVENTS)[keyof typeof BRAIN_EVENTS];
19
+ options: TOptions;
20
+ brainRunId: string;
21
+ }
22
+
23
+ // 1. Brain Events (all include brain title/description)
24
+ interface BrainBaseEvent<TOptions extends object = object>
25
+ extends BaseEvent<TOptions> {
26
+ brainTitle: string;
27
+ brainDescription?: string;
28
+ }
29
+
30
+ export interface BrainStartEvent<TOptions extends object = object>
31
+ extends BrainBaseEvent<TOptions> {
32
+ type: typeof BRAIN_EVENTS.START | typeof BRAIN_EVENTS.RESTART;
33
+ initialState: State;
34
+ status: typeof STATUS.RUNNING;
35
+ }
36
+
37
+ export interface BrainCompleteEvent<TOptions extends object = object>
38
+ extends BrainBaseEvent<TOptions> {
39
+ type: typeof BRAIN_EVENTS.COMPLETE;
40
+ status: typeof STATUS.COMPLETE;
41
+ }
42
+
43
+ export interface BrainErrorEvent<TOptions extends object = object>
44
+ extends BrainBaseEvent<TOptions> {
45
+ type: typeof BRAIN_EVENTS.ERROR;
46
+ status: typeof STATUS.ERROR;
47
+ error: SerializedError;
48
+ }
49
+
50
+ // 2. Step Status Event (just steps array and base event properties)
51
+ export interface StepStatusEvent<TOptions extends object = object>
52
+ extends BaseEvent<TOptions> {
53
+ type: typeof BRAIN_EVENTS.STEP_STATUS;
54
+ steps: SerializedStepStatus[];
55
+ }
56
+
57
+ // 3. Step Events (include step-specific properties)
58
+ export interface StepStartedEvent<TOptions extends object = object>
59
+ extends BaseEvent<TOptions> {
60
+ type: typeof BRAIN_EVENTS.STEP_START;
61
+ status: typeof STATUS.RUNNING;
62
+ stepTitle: string;
63
+ stepId: string;
64
+ }
65
+
66
+ export interface StepCompletedEvent<TOptions extends object = object>
67
+ extends BaseEvent<TOptions> {
68
+ type: typeof BRAIN_EVENTS.STEP_COMPLETE;
69
+ status: typeof STATUS.RUNNING;
70
+ stepTitle: string;
71
+ stepId: string;
72
+ patch: JsonPatch;
73
+ }
74
+
75
+ // Union type of all possible events
76
+ export type BrainEvent<TOptions extends object = object> =
77
+ | BrainStartEvent<TOptions>
78
+ | BrainCompleteEvent<TOptions>
79
+ | BrainErrorEvent<TOptions>
80
+ | StepStatusEvent<TOptions>
81
+ | StepStartedEvent<TOptions>
82
+ | StepCompletedEvent<TOptions>;
83
+
84
+ export interface SerializedStep {
85
+ title: string;
86
+ status: (typeof STATUS)[keyof typeof STATUS];
87
+ id: string;
88
+ patch?: JsonPatch;
89
+ }
90
+
91
+ // New type for Step Status Event, omitting the patch
92
+ export type SerializedStepStatus = Omit<SerializedStep, 'patch'>;
93
+
94
+ // Type for brain structure
95
+ export interface BrainStructure {
96
+ title: string;
97
+ description?: string;
98
+ steps: Array<{
99
+ type: 'step' | 'brain';
100
+ title: string;
101
+ innerBrain?: BrainStructure;
102
+ }>;
103
+ }
104
+
105
+ // Type for the brain function
106
+ export interface BrainFactory {
107
+ <
108
+ TOptions extends object = object,
109
+ TState extends State = object,
110
+ TServices extends object = object
111
+ >(
112
+ brainConfig: string | { title: string; description?: string }
113
+ ): Brain<TOptions, TState, TServices>;
114
+ }
115
+
116
+ type StepBlock<
117
+ TStateIn,
118
+ TStateOut,
119
+ TOptions extends object = object,
120
+ TServices extends object = object
121
+ > = {
122
+ type: 'step';
123
+ title: string;
124
+ action: (
125
+ params: {
126
+ state: TStateIn;
127
+ options: TOptions;
128
+ client: ObjectGenerator;
129
+ resources: Resources;
130
+ } & TServices
131
+ ) => TStateOut | Promise<TStateOut>;
132
+ };
133
+
134
+ type BrainBlock<
135
+ TOuterState,
136
+ TInnerState extends State,
137
+ TNewState,
138
+ TOptions extends object = object,
139
+ TServices extends object = object
140
+ > = {
141
+ type: 'brain';
142
+ title: string;
143
+ innerBrain: Brain<TOptions, TInnerState, TServices>;
144
+ initialState: State | ((outerState: TOuterState) => State);
145
+ action: (
146
+ outerState: TOuterState,
147
+ innerState: TInnerState,
148
+ services: TServices
149
+ ) => TNewState;
150
+ };
151
+
152
+ type Block<
153
+ TStateIn,
154
+ TStateOut,
155
+ TOptions extends object = object,
156
+ TServices extends object = object
157
+ > =
158
+ | StepBlock<TStateIn, TStateOut, TOptions, TServices>
159
+ | BrainBlock<TStateIn, any, TStateOut, TOptions, TServices>;
160
+
161
+ interface BaseRunParams<TOptions extends object = object> {
162
+ client: ObjectGenerator;
163
+ resources?: Resources;
164
+ options?: TOptions;
165
+ }
166
+
167
+ export interface InitialRunParams<TOptions extends object = object>
168
+ extends BaseRunParams<TOptions> {
169
+ initialState?: State;
170
+ initialCompletedSteps?: never;
171
+ brainRunId?: string;
172
+ }
173
+
174
+ export interface RerunParams<TOptions extends object = object>
175
+ extends BaseRunParams<TOptions> {
176
+ initialState: State;
177
+ initialCompletedSteps: SerializedStep[];
178
+ brainRunId: string;
179
+ }
180
+
181
+ export class Brain<
182
+ TOptions extends object = object,
183
+ TState extends State = object,
184
+ TServices extends object = object
185
+ > {
186
+ private blocks: Block<any, any, TOptions, TServices>[] = [];
187
+ public type: 'brain' = 'brain';
188
+ private defaultOptions: Partial<TOptions> = {};
189
+ private services: TServices = {} as TServices;
190
+
191
+ constructor(public readonly title: string, private description?: string) {}
192
+
193
+ get structure(): BrainStructure {
194
+ return {
195
+ title: this.title,
196
+ description: this.description,
197
+ steps: this.blocks.map((block) => {
198
+ if (block.type === 'step') {
199
+ return {
200
+ type: 'step' as const,
201
+ title: block.title,
202
+ };
203
+ } else {
204
+ // block.type === 'brain'
205
+ return {
206
+ type: 'brain' as const,
207
+ title: block.title,
208
+ innerBrain: block.innerBrain.structure,
209
+ };
210
+ }
211
+ }),
212
+ };
213
+ }
214
+
215
+ // New method to specify default options
216
+ withOptions(options: Partial<TOptions>): this {
217
+ this.defaultOptions = { ...this.defaultOptions, ...options };
218
+ return this;
219
+ }
220
+
221
+ // New method to add services
222
+ withServices<TNewServices extends object>(
223
+ services: TNewServices
224
+ ): Brain<TOptions, TState, TNewServices> {
225
+ const nextBrain = new Brain<TOptions, TState, TNewServices>(
226
+ this.title,
227
+ this.description
228
+ ).withBlocks(this.blocks as any);
229
+
230
+ // Copy default options
231
+ nextBrain.withOptions(this.defaultOptions);
232
+
233
+ // Set services
234
+ nextBrain.services = services;
235
+
236
+ return nextBrain;
237
+ }
238
+
239
+ step<TNewState extends State>(
240
+ title: string,
241
+ action: (
242
+ params: {
243
+ state: TState;
244
+ options: TOptions;
245
+ client: ObjectGenerator;
246
+ resources: Resources;
247
+ } & TServices
248
+ ) => TNewState | Promise<TNewState>
249
+ ) {
250
+ const stepBlock: StepBlock<TState, TNewState, TOptions, TServices> = {
251
+ type: 'step',
252
+ title,
253
+ action,
254
+ };
255
+ this.blocks.push(stepBlock);
256
+ return this.nextBrain<TNewState>();
257
+ }
258
+
259
+ brain<TInnerState extends State, TNewState extends State>(
260
+ title: string,
261
+ innerBrain: Brain<TOptions, TInnerState, TServices>,
262
+ action: (params: {
263
+ state: TState;
264
+ brainState: TInnerState;
265
+ services: TServices;
266
+ }) => TNewState,
267
+ initialState?: State | ((state: TState) => State)
268
+ ) {
269
+ const nestedBlock: BrainBlock<
270
+ TState,
271
+ TInnerState,
272
+ TNewState,
273
+ TOptions,
274
+ TServices
275
+ > = {
276
+ type: 'brain',
277
+ title,
278
+ innerBrain,
279
+ initialState: initialState || (() => ({} as State)),
280
+ action: (outerState, innerState, services) =>
281
+ action({ state: outerState, brainState: innerState, services }),
282
+ };
283
+ this.blocks.push(nestedBlock);
284
+ return this.nextBrain<TNewState>();
285
+ }
286
+
287
+ // TResponseKey:
288
+ // The response key must be a string literal, so if defining a response model
289
+ // a consumer of this brain must use "as const" to ensure the key is a string literal
290
+ // this type makes sure that the will get a ts error if they don't.
291
+ prompt<
292
+ TResponseKey extends string & { readonly brand?: unique symbol },
293
+ TSchema extends z.ZodObject<any>,
294
+ TNewState extends State = TState & {
295
+ [K in TResponseKey]: z.infer<TSchema>;
296
+ }
297
+ >(
298
+ title: string,
299
+ config: {
300
+ template: (
301
+ state: TState,
302
+ resources: Resources
303
+ ) => string | Promise<string>;
304
+ outputSchema: {
305
+ schema: TSchema;
306
+ name: TResponseKey & (string extends TResponseKey ? never : unknown);
307
+ };
308
+ client?: ObjectGenerator;
309
+ },
310
+ reduce?: (
311
+ params: {
312
+ state: TState;
313
+ response: z.infer<TSchema>;
314
+ options: TOptions;
315
+ prompt: string;
316
+ resources: Resources;
317
+ } & TServices
318
+ ) => TNewState | Promise<TNewState>
319
+ ) {
320
+ const promptBlock: StepBlock<TState, TNewState, TOptions, TServices> = {
321
+ type: 'step',
322
+ title,
323
+ action: async ({
324
+ state,
325
+ client: runClient,
326
+ options,
327
+ resources,
328
+ ...services
329
+ }) => {
330
+ const { template, outputSchema, client: stepClient } = config;
331
+ const { schema, name: schemaName } = outputSchema;
332
+ const client = stepClient ?? runClient;
333
+ const prompt = await template(state, resources);
334
+ const response = await client.generateObject({
335
+ schema,
336
+ schemaName,
337
+ prompt,
338
+ });
339
+ const stateWithResponse = {
340
+ ...state,
341
+ [config.outputSchema.name]: response,
342
+ };
343
+
344
+ return reduce
345
+ ? reduce({
346
+ state,
347
+ response,
348
+ options,
349
+ prompt,
350
+ resources,
351
+ ...(services as TServices),
352
+ })
353
+ : (stateWithResponse as unknown as TNewState);
354
+ },
355
+ };
356
+ this.blocks.push(promptBlock);
357
+ return this.nextBrain<TNewState>();
358
+ }
359
+
360
+ // Overload signatures
361
+ run(params: InitialRunParams<TOptions>): AsyncGenerator<BrainEvent<TOptions>>;
362
+ run(params: RerunParams<TOptions>): AsyncGenerator<BrainEvent<TOptions>>;
363
+
364
+ // Implementation signature
365
+ async *run(
366
+ params: InitialRunParams<TOptions> | RerunParams<TOptions>
367
+ ): AsyncGenerator<BrainEvent<TOptions>> {
368
+ const { title, description, blocks } = this;
369
+
370
+ // Merge default options with provided options
371
+ const mergedOptions = {
372
+ ...this.defaultOptions,
373
+ ...(params.options || {}),
374
+ } as TOptions;
375
+
376
+ const stream = new BrainEventStream({
377
+ title,
378
+ description,
379
+ blocks,
380
+ ...params,
381
+ options: mergedOptions,
382
+ services: this.services,
383
+ });
384
+
385
+ yield* stream.next();
386
+ }
387
+
388
+ private withBlocks(blocks: Block<any, any, TOptions, TServices>[]): this {
389
+ this.blocks = blocks;
390
+ return this;
391
+ }
392
+
393
+ private nextBrain<TNewState extends State>(): Brain<
394
+ TOptions,
395
+ TNewState,
396
+ TServices
397
+ > {
398
+ // Pass default options to the next brain
399
+ const nextBrain = new Brain<TOptions, TNewState, TServices>(
400
+ this.title,
401
+ this.description
402
+ ).withBlocks(this.blocks as any);
403
+
404
+ // Copy default options to the next brain
405
+ nextBrain.withOptions(this.defaultOptions);
406
+
407
+ // Copy services to the next brain
408
+ nextBrain.services = this.services;
409
+
410
+ return nextBrain;
411
+ }
412
+ }
413
+
414
+ class Step {
415
+ public id: string;
416
+ private patch?: JsonPatch;
417
+ private status: (typeof STATUS)[keyof typeof STATUS] = STATUS.PENDING;
418
+
419
+ constructor(public block: Block<any, any, any, any>, id?: string) {
420
+ this.id = id || uuidv4();
421
+ }
422
+
423
+ withPatch(patch: JsonPatch | undefined) {
424
+ this.patch = patch;
425
+ return this;
426
+ }
427
+
428
+ withStatus(status: (typeof STATUS)[keyof typeof STATUS]) {
429
+ this.status = status;
430
+ return this;
431
+ }
432
+
433
+ get serialized(): SerializedStep {
434
+ return {
435
+ id: this.id,
436
+ title: this.block.title,
437
+ status: this.status,
438
+ patch:
439
+ typeof this.patch === 'string' ? JSON.parse(this.patch) : this.patch,
440
+ };
441
+ }
442
+ }
443
+
444
+ class BrainEventStream<
445
+ TOptions extends object = object,
446
+ TState extends State = object,
447
+ TServices extends object = object
448
+ > {
449
+ private currentState: TState;
450
+ private steps: Step[];
451
+ private currentStepIndex: number = 0;
452
+ private initialState: TState;
453
+ private brainRunId: string;
454
+ private title: string;
455
+ private description?: string;
456
+ private client: ObjectGenerator;
457
+ private options: TOptions;
458
+ private services: TServices;
459
+ private resources: Resources;
460
+
461
+ constructor(
462
+ params: (InitialRunParams<TOptions> | RerunParams<TOptions>) & {
463
+ title: string;
464
+ description?: string;
465
+ blocks: Block<any, any, TOptions, TServices>[];
466
+ services: TServices;
467
+ }
468
+ ) {
469
+ const {
470
+ initialState = {} as TState,
471
+ initialCompletedSteps,
472
+ blocks,
473
+ title,
474
+ description,
475
+ brainRunId: providedBrainRunId,
476
+ options = {} as TOptions,
477
+ client,
478
+ services,
479
+ resources = {} as Resources,
480
+ } = params;
481
+
482
+ this.initialState = initialState as TState;
483
+ this.title = title;
484
+ this.description = description;
485
+ this.client = client;
486
+ this.options = options;
487
+ this.services = services;
488
+ this.resources = resources;
489
+ // Initialize steps array with UUIDs and pending status
490
+ this.steps = blocks.map((block, index) => {
491
+ const completedStep = initialCompletedSteps?.[index];
492
+ if (completedStep) {
493
+ return new Step(block, completedStep.id)
494
+ .withStatus(completedStep.status)
495
+ .withPatch(completedStep.patch);
496
+ }
497
+ return new Step(block);
498
+ });
499
+
500
+ this.currentState = clone(this.initialState);
501
+
502
+ for (const step of this.steps) {
503
+ if (step.serialized.status === STATUS.COMPLETE && step.serialized.patch) {
504
+ this.currentState = applyPatches(this.currentState, [
505
+ step.serialized.patch,
506
+ ]) as TState;
507
+ }
508
+ }
509
+
510
+ // Use provided ID if available, otherwise generate one
511
+ this.brainRunId = providedBrainRunId ?? uuidv4();
512
+ }
513
+
514
+ async *next(): AsyncGenerator<BrainEvent<TOptions>> {
515
+ const {
516
+ steps,
517
+ title: brainTitle,
518
+ description: brainDescription,
519
+ currentState,
520
+ options,
521
+ brainRunId,
522
+ } = this;
523
+
524
+ try {
525
+ const hasCompletedSteps = steps.some(
526
+ (step) => step.serialized.status !== STATUS.PENDING
527
+ );
528
+ yield {
529
+ type: hasCompletedSteps ? BRAIN_EVENTS.RESTART : BRAIN_EVENTS.START,
530
+ status: STATUS.RUNNING,
531
+ brainTitle,
532
+ brainDescription,
533
+ initialState: currentState,
534
+ options,
535
+ brainRunId,
536
+ };
537
+
538
+ // Emit initial step status after brain starts
539
+ yield {
540
+ type: BRAIN_EVENTS.STEP_STATUS,
541
+ steps: steps.map((step) => {
542
+ const { patch, ...rest } = step.serialized;
543
+ return rest;
544
+ }),
545
+ options,
546
+ brainRunId,
547
+ };
548
+
549
+ // Process each step
550
+ while (this.currentStepIndex < steps.length) {
551
+ const step = steps[this.currentStepIndex];
552
+
553
+ // Skip completed steps
554
+ if (step.serialized.status === STATUS.COMPLETE) {
555
+ this.currentStepIndex++;
556
+ continue;
557
+ }
558
+ // Step start event
559
+ yield {
560
+ type: BRAIN_EVENTS.STEP_START,
561
+ status: STATUS.RUNNING,
562
+ stepTitle: step.block.title,
563
+ stepId: step.id,
564
+ options,
565
+ brainRunId,
566
+ };
567
+
568
+ step.withStatus(STATUS.RUNNING);
569
+
570
+ // Step Status Event to indicate that the step is running
571
+ yield {
572
+ type: BRAIN_EVENTS.STEP_STATUS,
573
+ steps: steps.map((step) => {
574
+ const { patch, ...rest } = step.serialized;
575
+ return rest;
576
+ }),
577
+ options,
578
+ brainRunId,
579
+ };
580
+
581
+ // Execute step and yield the STEP_COMPLETE event and
582
+ // all events from inner brains if any
583
+ yield* this.executeStep(step);
584
+
585
+ // Step Status Event
586
+ yield {
587
+ type: BRAIN_EVENTS.STEP_STATUS,
588
+ steps: steps.map((step) => {
589
+ const { patch, ...rest } = step.serialized;
590
+ return rest;
591
+ }),
592
+ options,
593
+ brainRunId,
594
+ };
595
+
596
+ this.currentStepIndex++;
597
+ }
598
+
599
+ yield {
600
+ type: BRAIN_EVENTS.COMPLETE,
601
+ status: STATUS.COMPLETE,
602
+ brainTitle,
603
+ brainDescription,
604
+ brainRunId,
605
+ options,
606
+ };
607
+ } catch (err: any) {
608
+ const error = err as Error;
609
+ const currentStep = steps[this.currentStepIndex];
610
+ currentStep?.withStatus(STATUS.ERROR);
611
+
612
+ yield {
613
+ type: BRAIN_EVENTS.ERROR,
614
+ status: STATUS.ERROR,
615
+ brainTitle,
616
+ brainDescription,
617
+ brainRunId,
618
+ error: {
619
+ name: error.name,
620
+ message: error.message,
621
+ stack: error.stack,
622
+ },
623
+ options,
624
+ };
625
+
626
+ // Step Status Event
627
+ yield {
628
+ type: BRAIN_EVENTS.STEP_STATUS,
629
+ steps: steps.map((step) => {
630
+ const { patch, ...rest } = step.serialized;
631
+ return rest;
632
+ }),
633
+ options,
634
+ brainRunId,
635
+ };
636
+
637
+ throw error;
638
+ }
639
+ }
640
+
641
+ private async *executeStep(step: Step): AsyncGenerator<BrainEvent<TOptions>> {
642
+ const block = step.block as Block<any, any, TOptions, TServices>;
643
+
644
+ if (block.type === 'brain') {
645
+ const initialState =
646
+ typeof block.initialState === 'function'
647
+ ? block.initialState(this.currentState)
648
+ : block.initialState;
649
+
650
+ // Run inner brain and yield all its events
651
+ let patches: JsonPatch[] = [];
652
+ const innerRun = block.innerBrain.run({
653
+ resources: this.resources,
654
+ client: this.client,
655
+ initialState,
656
+ options: this.options ?? ({} as TOptions),
657
+ });
658
+
659
+ for await (const event of innerRun) {
660
+ yield event; // Forward all inner brain events
661
+ if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
662
+ patches.push(event.patch);
663
+ }
664
+ }
665
+
666
+ // Apply collected patches to get final inner state
667
+ const innerState = applyPatches(initialState, patches);
668
+
669
+ // Get previous state before action
670
+ const prevState = this.currentState;
671
+
672
+ // Update state with inner brain results
673
+ this.currentState = await block.action(
674
+ this.currentState,
675
+ innerState,
676
+ this.services
677
+ );
678
+ yield* this.completeStep(step, prevState);
679
+ } else {
680
+ // Get previous state before action
681
+ const prevState = this.currentState;
682
+
683
+ // Execute regular step
684
+ this.currentState = await block.action({
685
+ state: this.currentState,
686
+ options: this.options ?? ({} as TOptions),
687
+ client: this.client,
688
+ resources: this.resources,
689
+ ...this.services,
690
+ });
691
+ yield* this.completeStep(step, prevState);
692
+ }
693
+ }
694
+
695
+ private *completeStep(
696
+ step: Step,
697
+ prevState: TState
698
+ ): Generator<BrainEvent<TOptions>> {
699
+ step.withStatus(STATUS.COMPLETE);
700
+
701
+ // Create patch for the state change
702
+ const patch = createPatch(prevState, this.currentState);
703
+ step.withPatch(patch);
704
+
705
+ yield {
706
+ type: BRAIN_EVENTS.STEP_COMPLETE,
707
+ status: STATUS.RUNNING,
708
+ stepTitle: step.block.title,
709
+ stepId: step.id,
710
+ patch,
711
+ options: this.options ?? ({} as TOptions),
712
+ brainRunId: this.brainRunId,
713
+ };
714
+ }
715
+ }
716
+
717
+ const brainNamesAreUnique = process.env.NODE_ENV !== 'test';
718
+
719
+ const brainNames = new Set<string>();
720
+ export const brain: BrainFactory = function <
721
+ TOptions extends object = object,
722
+ TState extends State = object,
723
+ TServices extends object = object
724
+ >(brainConfig: string | { title: string; description?: string }) {
725
+ const title =
726
+ typeof brainConfig === 'string' ? brainConfig : brainConfig.title;
727
+ const description =
728
+ typeof brainConfig === 'string' ? undefined : brainConfig.description;
729
+ if (brainNamesAreUnique && brainNames.has(title)) {
730
+ throw new Error(
731
+ `Brain with name "${title}" already exists. Brain names must be unique.`
732
+ );
733
+ }
734
+ if (brainNamesAreUnique) {
735
+ brainNames.add(title);
736
+ }
737
+ return new Brain<TOptions, TState, TServices>(title, description);
738
+ };
739
+
740
+ const clone = <T>(value: T): T => structuredClone(value);