@positronic/template-new-project 0.0.8 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -53,9 +53,9 @@ module.exports = {
53
53
  ],
54
54
  setup: async ctx => {
55
55
  const devRootPath = process.env.POSITRONIC_LOCAL_PATH;
56
- let coreVersion = '^0.0.8';
57
- let cloudflareVersion = '^0.0.8';
58
- let clientVercelVersion = '^0.0.8';
56
+ let coreVersion = '^0.0.11';
57
+ let cloudflareVersion = '^0.0.11';
58
+ let clientVercelVersion = '^0.0.11';
59
59
 
60
60
  // Map backend selection to package names
61
61
  const backendPackageMap = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@positronic/template-new-project",
3
- "version": "0.0.8",
3
+ "version": "0.0.11",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -26,7 +26,7 @@ This is a Positronic project - an AI-powered framework for building and running
26
26
 
27
27
  ### Testing & Building
28
28
 
29
- - `npm test` - Run tests (uses Jest with @positronic/core/testing utilities)
29
+ - `npm test` - Run tests (uses Jest with local test utilities)
30
30
  - `npm run build` - Build the project
31
31
  - `npm run dev` - Start development mode with hot reload
32
32
 
package/template/brain.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { brain as coreBrain, type BrainFunction } from '@positronic/core';
1
+ import { brain as coreBrain, type BrainFactory } from '@positronic/core';
2
2
 
3
3
  /**
4
4
  * Base brain factory for this project.
@@ -11,7 +11,7 @@ import { brain as coreBrain, type BrainFunction } from '@positronic/core';
11
11
  * 2. Create service instances
12
12
  * 3. Call .withServices() on the brain before returning it
13
13
  *
14
- * Example:
14
+ * Example with services:
15
15
  * ```typescript
16
16
  * interface ProjectServices {
17
17
  * logger: {
@@ -23,7 +23,7 @@ import { brain as coreBrain, type BrainFunction } from '@positronic/core';
23
23
  * };
24
24
  * }
25
25
  *
26
- * export const brain: BrainFunction = (brainConfig) => {
26
+ * export const brain: BrainFactory = (brainConfig) => {
27
27
  * return coreBrain(brainConfig)
28
28
  * .withServices({
29
29
  * logger: {
@@ -43,16 +43,29 @@ import { brain as coreBrain, type BrainFunction } from '@positronic/core';
43
43
  * Then in your brain files (in the brains/ directory):
44
44
  * ```typescript
45
45
  * import { brain } from '../brain.js';
46
+ * import { z } from 'zod';
47
+ *
48
+ * const optionsSchema = z.object({
49
+ * environment: z.string().default('prod'),
50
+ * verbose: z.string().default('false')
51
+ * });
46
52
  *
47
53
  * export default brain('My Brain')
48
- * .step('Use Services', async ({ logger, api }) => {
49
- * logger.info('Fetching data...');
50
- * const data = await api.fetch('/users');
54
+ * .withOptionsSchema(optionsSchema)
55
+ * .step('Use Services', async ({ state, options, logger, api }) => {
56
+ * if (options.verbose === 'true') {
57
+ * logger.info('Fetching data...');
58
+ * }
59
+ * const endpoint = options.environment === 'dev' ? '/users/test' : '/users';
60
+ * const data = await api.fetch(endpoint);
51
61
  * return { users: data };
52
62
  * });
53
63
  * ```
64
+ *
65
+ * Run with custom options from CLI:
66
+ * px brain run my-brain -o environment=dev -o verbose=true
54
67
  */
55
- export const brain: BrainFunction = (brainConfig) => {
68
+ export const brain: BrainFactory = (brainConfig) => {
56
69
  // For now, just return the core brain without any services.
57
70
  // Update this function to add your project-wide services.
58
71
  return coreBrain(brainConfig);
@@ -8,6 +8,31 @@ The Brain DSL provides a fluent, type-safe API for building stateful AI workflow
8
8
 
9
9
  **Note**: This project uses a custom brain function. Always import `brain` from `../brain.js`, not from `@positronic/core`. See positronic-guide.md for details.
10
10
 
11
+ ### Type Safety and Options
12
+
13
+ The brain function provides full type safety through its fluent API. State types are automatically inferred as you build your brain, and options can be validated at runtime using schemas.
14
+
15
+ For runtime options validation, use the `withOptionsSchema` method with a Zod schema:
16
+
17
+ ```typescript
18
+ import { z } from 'zod';
19
+
20
+ const optionsSchema = z.object({
21
+ environment: z.enum(['dev', 'staging', 'prod']),
22
+ verbose: z.boolean().default(false)
23
+ });
24
+
25
+ const myBrain = brain('My Brain')
26
+ .withOptionsSchema(optionsSchema)
27
+ .step('Process', ({ options }) => {
28
+ // options is fully typed based on the schema
29
+ if (options.verbose) {
30
+ console.log('Running in', options.environment);
31
+ }
32
+ return { status: 'complete' };
33
+ });
34
+ ```
35
+
11
36
  ## Basic Brain Structure
12
37
 
13
38
  ```typescript
@@ -57,7 +82,7 @@ brain('AI Education Assistant')
57
82
  }))
58
83
  .prompt('Generate explanation', {
59
84
  template: ({ topic, context }) =>
60
- <%= "`${context}. Please provide a brief, beginner-friendly explanation of ${topic}.`" %>,
85
+ `<%= '${context}' %>. Please provide a brief, beginner-friendly explanation of <%= '${topic}' %>.`,
61
86
  outputSchema: {
62
87
  schema: z.object({
63
88
  explanation: z.string().describe('A clear explanation of the topic'),
@@ -72,7 +97,7 @@ brain('AI Education Assistant')
72
97
  formattedOutput: {
73
98
  topic: state.topic,
74
99
  explanation: state.topicExplanation.explanation || '',
75
- summary: <%= "`This explanation covers ${state.topicExplanation.keyPoints?.length || 0} key points at a ${state.topicExplanation.difficulty || 'unknown'} level.`" %>,
100
+ summary: `This explanation covers <%= '${state.topicExplanation.keyPoints?.length || 0}' %> key points at a <%= '${state.topicExplanation.difficulty || \'unknown\'}' %> level.`,
76
101
  points: state.topicExplanation.keyPoints || [],
77
102
  },
78
103
  }))
@@ -80,7 +105,9 @@ brain('AI Education Assistant')
80
105
  'Generate follow-up questions',
81
106
  {
82
107
  template: ({ formattedOutput }) =>
83
- <%= "`Based on this explanation about ${formattedOutput.topic}: \"${formattedOutput.explanation}\"\n \n Generate 3 thoughtful follow-up questions that a student might ask.`" %>,
108
+ `Based on this explanation about <%= '${formattedOutput.topic}' %>: "<%= '${formattedOutput.explanation}' %>"
109
+
110
+ Generate 3 thoughtful follow-up questions that a student might ask.`,
84
111
  outputSchema: {
85
112
  schema: z.object({
86
113
  questions: z.array(z.string()).length(3).describe('Three follow-up questions'),
@@ -142,19 +169,144 @@ Each step receives these parameters:
142
169
 
143
170
  ## Configuration Methods
144
171
 
145
- ### Default Options
172
+ ### Brain Options
173
+
174
+ Options provide runtime configuration for your brains, allowing different behavior without changing code. They're perfect for settings like API endpoints, feature flags, output preferences, or channel identifiers.
146
175
 
147
- Set default options for all brain runs:
176
+ #### Typing Options
177
+
178
+ To use options in your brain, define a Zod schema with `withOptionsSchema`:
148
179
 
149
180
  ```typescript
150
- const configuredBrain = brain('My Brain')
151
- .withOptions({ debug: true, temperature: 0.7 })
152
- .step('Process', ({ state, options }) => {
153
- if (options.debug) console.log('Processing...');
181
+ import { z } from 'zod';
182
+
183
+ // Define your options schema
184
+ const notificationSchema = z.object({
185
+ slackChannel: z.string(),
186
+ priority: z.enum(['low', 'normal', 'high']),
187
+ includeTimestamp: z.boolean().default(true)
188
+ });
189
+
190
+ // Use withOptionsSchema to add runtime validation
191
+ const notificationBrain = brain('Notification Brain')
192
+ .withOptionsSchema(notificationSchema)
193
+ .step('Send Alert', async ({ state, options, slack }) => {
194
+ // TypeScript knows the exact shape of options from the schema
195
+ const message = options.includeTimestamp
196
+ ? `[<%= '${new Date().toISOString()}' %>] <%= '${state.alert}' %>`
197
+ : state.alert;
198
+
199
+ await slack.post(options.slackChannel, {
200
+ text: message,
201
+ priority: options.priority // Type-safe: must be 'low' | 'normal' | 'high'
202
+ });
203
+
154
204
  return state;
155
205
  });
156
206
  ```
157
207
 
208
+ The schema approach provides:
209
+ - Runtime validation of options
210
+ - Automatic TypeScript type inference
211
+ - Clear error messages for invalid options
212
+ - Support for default values in the schema
213
+
214
+ #### Passing Options from Command Line
215
+
216
+ Override default options when running brains from the CLI using the `-o` or `--options` flag:
217
+
218
+ ```bash
219
+ # Single option
220
+ px brain run my-brain -o debug=true
221
+
222
+ # Multiple options
223
+ px brain run my-brain -o slackChannel=#alerts -o temperature=0.9 -o verbose=true
224
+
225
+ # Options with spaces or special characters (use quotes)
226
+ px brain run my-brain -o "webhook=https://example.com/api?key=value"
227
+ ```
228
+
229
+ Options are passed as simple key=value pairs and are available as strings in your brain.
230
+
231
+ #### Options vs Services vs Initial State
232
+
233
+ Understanding when to use each:
234
+
235
+ - **Options**: Runtime configuration (channels, endpoints, feature flags)
236
+ - Override from CLI with `-o key=value`
237
+ - Don't change during execution
238
+ - Examples: `slackChannel`, `apiEndpoint`, `debugMode`
239
+
240
+ - **Services**: External dependencies and side effects (clients, loggers, databases)
241
+ - Configure once with `.withServices()`
242
+ - Available in all steps
243
+ - Not serializable
244
+ - Examples: `slackClient`, `database`, `logger`
245
+
246
+ - **Initial State**: Starting data for a specific run
247
+ - Pass to `brain.run()` or set via CLI/API
248
+ - Changes throughout execution
249
+ - Must be serializable
250
+ - Examples: `userId`, `orderData`, `inputText`
251
+
252
+ #### Real-World Example
253
+
254
+ ```typescript
255
+ // Define a brain that uses options for configuration
256
+ const notificationSchema = z.object({
257
+ channel: z.string(),
258
+ priority: z.string().default('normal'),
259
+ includeDetails: z.string().default('false')
260
+ });
261
+
262
+ const notificationBrain = brain('Smart Notifier')
263
+ .withOptionsSchema(notificationSchema)
264
+ .withServices({
265
+ slack: slackClient,
266
+ email: emailClient
267
+ })
268
+ .step('Process Alert', ({ state, options }) => ({
269
+ ...state,
270
+ formattedMessage: options.includeDetails === 'true'
271
+ ? `Alert: <%= '${state.message}' %> - Details: <%= '${state.details}' %>`
272
+ : `Alert: <%= '${state.message}' %>`,
273
+ isPriority: options.priority === 'high'
274
+ }))
275
+ .step('Send Notification', async ({ state, options, slack, email }) => {
276
+ // Use options to control behavior
277
+ if (state.isPriority) {
278
+ // High priority goes to email too
279
+ await email.send('admin@example.com', state.formattedMessage);
280
+ }
281
+
282
+ // Always send to Slack channel from options
283
+ await slack.post(options.channel, state.formattedMessage);
284
+
285
+ return { ...state, notified: true };
286
+ });
287
+
288
+ // Run with custom options from CLI:
289
+ // px brain run smart-notifier -o channel=#urgent -o priority=high -o includeDetails=true
290
+ ```
291
+
292
+ #### Testing with Options
293
+
294
+ ```typescript
295
+ // In your tests
296
+ const result = await runBrainTest(notificationBrain, {
297
+ client: mockClient,
298
+ initialState: { message: 'System down', details: 'Database unreachable' },
299
+ options: {
300
+ channel: '#test-channel',
301
+ priority: 'high',
302
+ includeDetails: true
303
+ }
304
+ });
305
+
306
+ expect(mockSlack.post).toHaveBeenCalledWith('#test-channel', expect.any(String));
307
+ expect(mockEmail.send).toHaveBeenCalled(); // High priority triggers email
308
+ ```
309
+
158
310
  ### Service Injection
159
311
 
160
312
  The `withServices` method provides dependency injection for your brains, making external services available throughout the workflow while maintaining testability.
@@ -249,7 +401,7 @@ const analysisBrain = brain('Data Analysis')
249
401
  return { ...state, data };
250
402
  })
251
403
  .prompt('Analyze Data', {
252
- template: ({ data }) => <%= "`Analyze this data: ${JSON.stringify(data)}`" %>,
404
+ template: ({ data }) => `Analyze this data: <%= '${JSON.stringify(data)}' %>`,
253
405
  outputSchema: {
254
406
  schema: z.object({
255
407
  insights: z.array(z.string()),
@@ -283,7 +435,7 @@ Services make testing easier by allowing you to inject mocks:
283
435
 
284
436
  ```typescript
285
437
  // In your test file
286
- import { createMockClient, runBrainTest } from '@positronic/core/testing';
438
+ import { createMockClient, runBrainTest } from '../tests/test-utils.js';
287
439
 
288
440
  const mockLogger = {
289
441
  info: jest.fn(),
@@ -377,7 +529,7 @@ const typedBrain = brain('Typed Example')
377
529
  name: 'Test', // TypeScript knows state has 'count'
378
530
  }))
379
531
  .step('Use Both', ({ state }) => ({
380
- message: <%= "`${state.name}: ${state.count}`" %>, // Both properties available
532
+ message: `<%= '${state.name}' %>: <%= '${state.count}' %>`, // Both properties available
381
533
  }));
382
534
  ```
383
535
 
@@ -482,7 +634,7 @@ export const aiFilterPrompt = {
482
634
 
483
635
  // Build the prompt with state data
484
636
  const articleList = state.articles
485
- .map((a, i) => <%= "`${i + 1}. ${a.title} (score: ${a.score})`" %>)
637
+ .map((a, i) => `<%= '${i + 1}' %>. <%= '${a.title}' %> (score: <%= '${a.score}' %>)`)
486
638
  .join('\n');
487
639
 
488
640
  return template
@@ -546,7 +698,6 @@ const completeBrain = brain({
546
698
  title: 'Complete Example',
547
699
  description: 'Demonstrates all Brain DSL features',
548
700
  })
549
- .withOptions({ temperature: 0.7 })
550
701
  .withServices<Services>({
551
702
  logger: console,
552
703
  analytics: {
@@ -574,11 +725,11 @@ const completeBrain = brain({
574
725
  },
575
726
  // Services available in reduce function too
576
727
  ({ state, response, logger }) => {
577
- logger.log(<%= "`Plan generated with ${response.tasks.length} tasks`" %>);
728
+ logger.log(`Plan generated with <%= '${response.tasks.length}' %> tasks`);
578
729
  return { ...state, plan: response };
579
730
  })
580
731
  .step('Process Plan', ({ state, logger, analytics }) => {
581
- logger.log(<%= "`Processing ${state.plan.tasks.length} tasks`" %>);
732
+ logger.log(`Processing <%= '${state.plan.tasks.length}' %> tasks`);
582
733
  analytics.track('plan_processed', {
583
734
  task_count: state.plan.tasks.length,
584
735
  duration: state.plan.duration
@@ -1,6 +1,14 @@
1
1
  # Brain Testing Guide
2
2
 
3
- This guide explains how to test Positronic brains using the testing utilities provided by `@positronic/core`.
3
+ This guide explains how to test Positronic brains using the testing utilities in the `tests/test-utils.ts` file.
4
+
5
+ ## Test Organization
6
+
7
+ All test files should be placed in the `tests/` directory at the root of your project. This keeps tests separate from your brain implementations and prevents them from being deployed with your application.
8
+
9
+ Test files should follow the naming convention `<brain-name>.test.ts`. For example:
10
+ - Brain file: `brains/customer-support.ts`
11
+ - Test file: `tests/customer-support.test.ts`
4
12
 
5
13
  ## Testing Philosophy
6
14
 
@@ -16,8 +24,8 @@ Testing brains is about verifying they produce the correct outputs given specifi
16
24
  ## Quick Start
17
25
 
18
26
  ```typescript
19
- import { createMockClient, runBrainTest } from '@positronic/core';
20
- import yourBrain from './your-brain.js';
27
+ import { createMockClient, runBrainTest } from '../tests/test-utils.js';
28
+ import yourBrain from '../brains/your-brain.js';
21
29
 
22
30
  describe('your-brain', () => {
23
31
  it('should process user data and generate a report', async () => {
@@ -60,13 +60,10 @@ interface ProjectServices {
60
60
  }
61
61
 
62
62
  // Export the wrapped brain function
63
- export function brain<
64
- TOptions extends object = object,
65
- TState extends object = object
66
- >(
63
+ export function brain(
67
64
  brainConfig: string | { title: string; description?: string }
68
- ): Brain<TOptions, TState, ProjectServices> {
69
- return coreBrain<TOptions, TState, ProjectServices>(brainConfig)
65
+ ) {
66
+ return coreBrain(brainConfig)
70
67
  .withServices({
71
68
  logger: {
72
69
  info: (msg) => console.log(`[<%= '${new Date().toISOString()}' %>] INFO: <%= '${msg}' %>`),
@@ -127,7 +124,17 @@ See `/docs/brain-testing-guide.md` for detailed testing guidance.
127
124
 
128
125
  1. **Start the development server**: `px server -d`
129
126
  2. **Create or modify brains**: Always import from `./brain.js`
130
- 3. **Test locally**: `px brain run <brain-name>`
127
+ 3. **Test locally**:
128
+ ```bash
129
+ # Basic run
130
+ px brain run <brain-name>
131
+
132
+ # Run with options
133
+ px brain run <brain-name> -o channel=#dev -o debug=true
134
+
135
+ # Watch execution in real-time
136
+ px brain run <brain-name> --watch
137
+ ```
131
138
  4. **Run tests**: `npm test`
132
139
  5. **Deploy**: Backend-specific commands (e.g., `px deploy` for Cloudflare)
133
140
 
@@ -202,10 +202,7 @@ import slack from './services/slack.js';
202
202
  import database from './services/database.js';
203
203
  import analytics from './services/analytics.js';
204
204
 
205
- export function brain<
206
- TOptions extends object = object,
207
- TState extends object = object
208
- >(
205
+ export function brain(
209
206
  brainConfig: string | { title: string; description?: string }
210
207
  ) {
211
208
  return coreBrain(brainConfig)
@@ -220,6 +217,45 @@ export function brain<
220
217
 
221
218
  This keeps your service implementations separate from your brain logic and makes them easier to test and maintain.
222
219
 
220
+ ## Brain Options Usage
221
+
222
+ When creating brains that need runtime configuration, use the options schema pattern:
223
+
224
+ ```typescript
225
+ import { z } from 'zod';
226
+
227
+ // Good example - configurable brain with validated options
228
+ const alertSchema = z.object({
229
+ slackChannel: z.string(),
230
+ emailEnabled: z.string().default('false'),
231
+ alertThreshold: z.string().default('10')
232
+ });
233
+
234
+ const alertBrain = brain('Alert System')
235
+ .withOptionsSchema(alertSchema)
236
+ .step('Check Threshold', ({ state, options }) => ({
237
+ ...state,
238
+ shouldAlert: state.errorCount > parseInt(options.alertThreshold)
239
+ }))
240
+ .step('Send Alerts', async ({ state, options, slack }) => {
241
+ if (!state.shouldAlert) return state;
242
+
243
+ await slack.post(options.slackChannel, state.message);
244
+
245
+ if (options.emailEnabled === 'true') {
246
+ // Note: CLI options come as strings
247
+ await email.send('admin@example.com', state.message);
248
+ }
249
+
250
+ return { ...state, alerted: true };
251
+ });
252
+ ```
253
+
254
+ Remember:
255
+ - Options from CLI are always strings (even numbers and booleans)
256
+ - Options are for configuration, not data
257
+ - Document available options in comments above the brain
258
+
223
259
  ## Important: ESM Module Imports
224
260
 
225
261
  This project uses ES modules (ESM). **Always include the `.js` extension in your imports**, even when importing TypeScript files:
@@ -255,8 +291,8 @@ Start by following the brain testing guide (`/docs/brain-testing-guide.md`) and
255
291
  ```typescript
256
292
  // tests/my-new-brain.test.ts
257
293
  import { describe, it, expect } from '@jest/globals';
258
- import { createMockClient, runBrainTest } from '@positronic/core/testing';
259
- import myNewBrain from '../brains/my-new-brain';
294
+ import { createMockClient, runBrainTest } from './test-utils.js';
295
+ import myNewBrain from '../brains/my-new-brain.js';
260
296
 
261
297
  describe('MyNewBrain', () => {
262
298
  it('should process data and return expected result', async () => {
@@ -381,7 +417,7 @@ export default feedbackBrain;
381
417
  // Step 4: Add sentiment analysis step
382
418
  .prompt('Analyze sentiment', {
383
419
  template: ({ feedback }) =>
384
- <%= "`Analyze the sentiment of this feedback: \"${feedback}\"`" %>,
420
+ <%= '\`Analyze the sentiment of this feedback: "${feedback}"\`' %>,
385
421
  outputSchema: {
386
422
  schema: z.object({
387
423
  sentiment: z.enum(['positive', 'neutral', 'negative']),
@@ -395,7 +431,7 @@ export default feedbackBrain;
395
431
  // Step 6: Add response generation
396
432
  .prompt('Generate response', {
397
433
  template: ({ sentimentAnalysis, feedback }) =>
398
- <%= "`Generate a brief response to this ${sentimentAnalysis.sentiment} feedback: \"${feedback}\"`" %>,
434
+ <%= '\`Generate a brief response to this ${sentimentAnalysis.sentiment} feedback: "${feedback}"\`' %>,
399
435
  outputSchema: {
400
436
  schema: z.object({
401
437
  response: z.string()
@@ -1,4 +1,4 @@
1
- import { createMockClient, runBrainTest } from '@positronic/core/testing';
1
+ import { createMockClient, runBrainTest } from './test-utils.js';
2
2
  import exampleBrain from '../brains/example.js';
3
3
 
4
4
  describe('example brain', () => {
@@ -0,0 +1,81 @@
1
+ import type { ObjectGenerator } from '@positronic/core';
2
+ import type { BrainEvent } from '@positronic/core';
3
+ import { BRAIN_EVENTS, applyPatches } from '@positronic/core';
4
+
5
+ export interface MockClient extends ObjectGenerator {
6
+ mockResponses: (...responses: any[]) => void;
7
+ clearMocks: () => void;
8
+ }
9
+
10
+ export function createMockClient(): MockClient {
11
+ const responses: any[] = [];
12
+ let responseIndex = 0;
13
+
14
+ const generateObject = jest.fn(async () => {
15
+ if (responseIndex >= responses.length) {
16
+ throw new Error('No more mock responses available');
17
+ }
18
+ return responses[responseIndex++];
19
+ });
20
+
21
+ return {
22
+ generateObject,
23
+ mockResponses: (...newResponses: any[]) => {
24
+ responses.push(...newResponses);
25
+ },
26
+ clearMocks: () => {
27
+ responses.length = 0;
28
+ responseIndex = 0;
29
+ generateObject.mockClear();
30
+ },
31
+ };
32
+ }
33
+
34
+ export interface BrainTestResult<TState> {
35
+ completed: boolean;
36
+ error: Error | null;
37
+ finalState: TState;
38
+ events: BrainEvent<any>[];
39
+ }
40
+
41
+ export async function runBrainTest<TOptions extends object, TState extends object>(
42
+ brain: any,
43
+ options?: {
44
+ client?: ObjectGenerator;
45
+ initialState?: Partial<TState>;
46
+ resources?: any;
47
+ }
48
+ ): Promise<BrainTestResult<TState>> {
49
+ const events: BrainEvent<any>[] = [];
50
+ let finalState: any = options?.initialState || {};
51
+ let error: Error | null = null;
52
+ let completed = false;
53
+
54
+ try {
55
+ const runOptions = {
56
+ ...options,
57
+ state: options?.initialState,
58
+ };
59
+
60
+ for await (const event of brain.run(runOptions)) {
61
+ events.push(event);
62
+
63
+ if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
64
+ finalState = applyPatches(finalState, [event.patch]);
65
+ } else if (event.type === BRAIN_EVENTS.ERROR) {
66
+ error = new Error(event.error.message);
67
+ } else if (event.type === BRAIN_EVENTS.COMPLETE) {
68
+ completed = true;
69
+ }
70
+ }
71
+ } catch (err) {
72
+ error = err instanceof Error ? err : new Error(String(err));
73
+ }
74
+
75
+ return {
76
+ completed,
77
+ error,
78
+ finalState,
79
+ events,
80
+ };
81
+ }