@positronic/template-new-project 0.0.76 → 0.0.78

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.
@@ -7,23 +7,28 @@ This guide covers project-level patterns and best practices for Positronic appli
7
7
  A typical Positronic project has the following structure:
8
8
 
9
9
  ```
10
- ├── brain.ts # Project brain wrapper
11
- ├── brains/ # Brain definitions
10
+ ├── src/
11
+ ├── brain.ts # Project brain wrapper
12
+ │ ├── brains/ # Brain definitions
13
+ │ ├── plugins/ # Plugin definitions (webhooks, tools, services)
14
+ │ ├── runner.ts # Local runner for development
15
+ │ ├── services/ # Service implementations
16
+ │ ├── utils/ # Shared utilities
17
+ │ └── components/ # Reusable UI/prompt components
12
18
  ├── resources/ # Files accessible to brains
13
19
  ├── tests/ # Test files
14
20
  ├── docs/ # Documentation
15
- ├── runner.ts # Local runner for development
16
21
  └── positronic.config.json # Project configuration
17
22
  ```
18
23
 
19
24
  ## The Project Brain Pattern
20
25
 
21
- Every Positronic project includes a `brain.ts` file in the root directory. This file exports a custom `brain` function that wraps the core Positronic brain function.
26
+ Every Positronic project includes a `src/brain.ts` file. This file exports a custom `brain` function that wraps the core Positronic brain function.
22
27
 
23
28
  ### Why Use a Project Brain?
24
29
 
25
30
  The project brain pattern provides a single place to:
26
- - Configure services that all brains can access
31
+ - Configure plugins that all brains can access
27
32
  - Set up logging, monitoring, or analytics
28
33
  - Add authentication or API clients
29
34
  - Establish project-wide conventions
@@ -33,54 +38,61 @@ The project brain pattern provides a single place to:
33
38
  All brains in your project should import from `../brain.js` instead of `@positronic/core`:
34
39
 
35
40
  ```typescript
36
- // ✅ DO THIS (from a file in the brains/ directory)
41
+ // ✅ DO THIS (from a file in src/brains/)
37
42
  import { brain } from '../brain.js';
38
43
 
39
44
  // ❌ NOT THIS
40
45
  import { brain } from '@positronic/core';
41
46
  ```
42
47
 
43
- ### Configuring Services
48
+ ### Configuring Plugins
44
49
 
45
- To add project-wide services, modify the `brain.ts` file in the root directory using `createBrain()`:
50
+ To add project-wide plugins, modify the `src/brain.ts` file using `createBrain()`. Plugins are defined with `definePlugin` and passed to `createBrain({ plugins: [...] })`:
46
51
 
47
52
  ```typescript
48
- import { createBrain } from '@positronic/core';
53
+ // src/plugins/logger.ts
54
+ import { definePlugin } from '@positronic/core';
55
+
56
+ export const logger = definePlugin({
57
+ name: 'logger',
58
+ create: () => ({
59
+ info: (msg: string) => console.log(`[<%= '${new Date().toISOString()}' %>] INFO: <%= '${msg}' %>`),
60
+ error: (msg: string) => console.error(`[<%= '${new Date().toISOString()}' %>] ERROR: <%= '${msg}' %>`),
61
+ }),
62
+ });
63
+ ```
49
64
 
50
- // Define your services
51
- interface ProjectServices {
52
- logger: {
53
- info: (message: string) => void;
54
- error: (message: string) => void;
55
- };
56
- database: {
57
- get: (key: string) => Promise<any>;
58
- set: (key: string, value: any) => Promise<void>;
59
- };
60
- }
65
+ ```typescript
66
+ // src/plugins/database.ts
67
+ import { definePlugin } from '@positronic/core';
68
+
69
+ export const database = definePlugin({
70
+ name: 'database',
71
+ create: () => ({
72
+ get: async (key: string) => {
73
+ // Your database implementation
74
+ return localStorage.getItem(key);
75
+ },
76
+ set: async (key: string, value: any) => {
77
+ // Your database implementation
78
+ localStorage.setItem(key, JSON.stringify(value));
79
+ },
80
+ }),
81
+ });
82
+ ```
83
+
84
+ ```typescript
85
+ // src/brain.ts
86
+ import { createBrain } from '@positronic/core';
87
+ import { logger } from './plugins/logger.js';
88
+ import { database } from './plugins/database.js';
61
89
 
62
- // Export the project brain factory
63
90
  export const brain = createBrain({
64
- services: {
65
- logger: {
66
- info: (msg) => console.log(`[<%= '${new Date().toISOString()}' %>] INFO: <%= '${msg}' %>`),
67
- error: (msg) => console.error(`[<%= '${new Date().toISOString()}' %>] ERROR: <%= '${msg}' %>`)
68
- },
69
- database: {
70
- get: async (key) => {
71
- // Your database implementation
72
- return localStorage.getItem(key);
73
- },
74
- set: async (key, value) => {
75
- // Your database implementation
76
- localStorage.setItem(key, JSON.stringify(value));
77
- }
78
- }
79
- }
91
+ plugins: [logger, database],
80
92
  });
81
93
  ```
82
94
 
83
- Now all brains automatically have access to these services:
95
+ Now all brains automatically have access to these plugins:
84
96
 
85
97
  ```typescript
86
98
  import { brain } from '../brain.js';
@@ -120,7 +132,7 @@ See `/docs/brain-testing-guide.md` for detailed testing guidance.
120
132
  ## Development Workflow
121
133
 
122
134
  1. **Start the development server**: `px server -d`
123
- 2. **Create or modify brains**: Always import from `./brain.js`
135
+ 2. **Create or modify brains**: Always import from `../brain.js` (from files in `src/brains/`)
124
136
  3. **Test locally**:
125
137
  ```bash
126
138
  # Basic run
@@ -162,7 +174,7 @@ CLOUDFLARE_API_TOKEN=your-api-token
162
174
 
163
175
  ## Best Practices
164
176
 
165
- 1. **Services**: Configure once in `brain.ts`, use everywhere
177
+ 1. **Plugins**: Configure once in `src/brain.ts`, use everywhere
166
178
  2. **Resources**: Use for content that non-developers should be able to edit
167
179
  3. **Secrets**: Never commit API keys; use environment variables
168
180
  4. **Organization**: Group related brains in folders as your project grows
@@ -174,14 +186,25 @@ CLOUDFLARE_API_TOKEN=your-api-token
174
186
  ### Logging and Monitoring
175
187
 
176
188
  ```typescript
177
- // In brain.ts
178
- interface ProjectServices {
179
- metrics: {
180
- track: (event: string, properties?: any) => void;
181
- time: (label: string) => () => void;
182
- };
183
- }
189
+ // src/plugins/metrics.ts
190
+ import { definePlugin } from '@positronic/core';
191
+
192
+ export const metrics = definePlugin({
193
+ name: 'metrics',
194
+ create: () => ({
195
+ track: (event: string, properties?: any) => {
196
+ // Your analytics implementation
197
+ console.log('track', event, properties);
198
+ },
199
+ time: (label: string) => {
200
+ const start = Date.now();
201
+ return () => console.log(label, Date.now() - start, 'ms');
202
+ },
203
+ }),
204
+ });
205
+ ```
184
206
 
207
+ ```typescript
185
208
  // In your brain
186
209
  export default brain('Analytics Brain')
187
210
  .step('Start Timer', ({ metrics }) => {
@@ -202,34 +225,32 @@ export default brain('Analytics Brain')
202
225
  ### API Integration
203
226
 
204
227
  ```typescript
205
- // In brain.ts
206
- interface ProjectServices {
207
- api: {
208
- get: (path: string) => Promise<any>;
209
- post: (path: string, data: any) => Promise<any>;
210
- };
211
- }
212
-
213
- // Configure with base URL and auth
214
- const api = {
215
- get: async (path: string) => {
216
- const response = await fetch(`https://api.example.com<%= '${path}' %>`, {
217
- headers: { 'Authorization': `Bearer <%= '${process.env.API_KEY}' %>` }
218
- });
219
- return response.json();
220
- },
221
- post: async (path: string, data: any) => {
222
- const response = await fetch(`https://api.example.com<%= '${path}' %>`, {
223
- method: 'POST',
224
- headers: {
225
- 'Authorization': `Bearer <%= '${process.env.API_KEY}' %>`,
226
- 'Content-Type': 'application/json'
227
- },
228
- body: JSON.stringify(data)
229
- });
230
- return response.json();
231
- }
232
- };
228
+ // src/plugins/api.ts
229
+ import { definePlugin } from '@positronic/core';
230
+
231
+ export const api = definePlugin({
232
+ name: 'api',
233
+ setup: (config: { baseUrl: string; apiKey: string }) => config,
234
+ create: ({ config }) => ({
235
+ get: async (path: string) => {
236
+ const response = await fetch(`<%= '${config!.baseUrl}${path}' %>`, {
237
+ headers: { 'Authorization': `Bearer <%= '${config!.apiKey}' %>` }
238
+ });
239
+ return response.json();
240
+ },
241
+ post: async (path: string, data: any) => {
242
+ const response = await fetch(`<%= '${config!.baseUrl}${path}' %>`, {
243
+ method: 'POST',
244
+ headers: {
245
+ 'Authorization': `Bearer <%= '${config!.apiKey}' %>`,
246
+ 'Content-Type': 'application/json'
247
+ },
248
+ body: JSON.stringify(data)
249
+ });
250
+ return response.json();
251
+ },
252
+ }),
253
+ });
233
254
  ```
234
255
 
235
256
  ## currentUser
@@ -247,8 +268,8 @@ The way `currentUser` is provided depends on how the brain is running:
247
268
  **Local development with `runner.ts`**: When calling `runner.run()` directly, you must pass `currentUser`:
248
269
 
249
270
  ```typescript
250
- import { runner } from './runner.js';
251
- import myBrain from './brains/my-brain.js';
271
+ import { runner } from './src/runner.js';
272
+ import myBrain from './src/brains/my-brain.js';
252
273
 
253
274
  await runner.run(myBrain, {
254
275
  currentUser: { name: 'local-dev-user' },
@@ -286,6 +307,6 @@ export default brain('greet')
286
307
 
287
308
  - **Documentation**: https://positronic.dev
288
309
  - **CLI Help**: `px --help`
289
- - **Brain DSL Guide**: `/docs/brain-dsl-guide.md` (includes UI steps for generating forms)
310
+ - **Brain DSL Guide**: `/docs/brain-dsl-guide.md` (includes page steps for generating forms)
290
311
  - **Memory Guide**: `/docs/memory-guide.md`
291
312
  - **Testing Guide**: `/docs/brain-testing-guide.md`
@@ -31,11 +31,11 @@ This also applies to variable declarations and function parameters:
31
31
  ```typescript
32
32
  // ❌ DON'T DO THIS
33
33
  const names: string[] = options.notify.split(',');
34
- template: (state: any) => { ... }
34
+ message: ({ state }: any) => { ... }
35
35
 
36
36
  // ✅ DO THIS
37
37
  const names = options.notify.split(',');
38
- template: (state) => { ... }
38
+ message: ({ state }) => { ... }
39
39
  ```
40
40
 
41
41
  If you genuinely need a cast to fix a type error, prefer the narrowest cast possible and add it only after seeing the error.
@@ -199,10 +199,10 @@ The same applies to prompt templates:
199
199
 
200
200
  ```typescript
201
201
  // ❌ DON'T DO THIS
202
- template: (state) => `Hello <%= '${state.user.name}' %>, your order <%= '${state.order.id}' %> is ready.`,
202
+ message: ({ state }) => `Hello <%= '${state.user.name}' %>, your order <%= '${state.order.id}' %> is ready.`,
203
203
 
204
204
  // ✅ DO THIS
205
- template: ({ user, order }) => `Hello <%= '${user.name}' %>, your order <%= '${order.id}' %> is ready.`,
205
+ message: ({ state: { user, order } }) => `Hello <%= '${user.name}' %>, your order <%= '${order.id}' %> is ready.`,
206
206
  ```
207
207
 
208
208
  When you still need `state` (e.g. for `...state` in the return value), destructure in the function body instead:
@@ -224,6 +224,27 @@ When you still need `state` (e.g. for `...state` in the return value), destructu
224
224
  })
225
225
  ```
226
226
 
227
+ ## JSX for Prompt Templates
228
+
229
+ For complex, multi-line prompts, use JSX instead of template literals. Rename the file to `.tsx` and return JSX from the template function:
230
+
231
+ ```tsx
232
+ // Before (template literal — hard to read when indented in builder chain)
233
+ message: ({ state: { user, order } }) =>
234
+ `Hello <%= '${user.name}' %>, your order <%= '${order.id}' %> is ready.
235
+ <%= '${order.isExpress ? "\\nThis is an express order." : ""}' %>`,
236
+
237
+ // After (JSX — Prettier manages indentation, conditionals are clean)
238
+ message: ({ state: { user, order } }) => (
239
+ <>
240
+ Hello {user.name}, your order {order.id} is ready.
241
+ {order.isExpress && <>This is an express order.</>}
242
+ </>
243
+ )
244
+ ```
245
+
246
+ See `/docs/brain-dsl-guide.md` for full JSX template documentation including loops, reusable components, and async components for resource loading.
247
+
227
248
  ## State Shape
228
249
 
229
250
  ### Each step should have one clear purpose, and add one thing to state
@@ -296,7 +317,53 @@ Use `.values` for simple extraction, `.filter()` for correlated filtering, and `
296
317
  }))
297
318
  ```
298
319
 
299
- **Name the `outputKey` after the content.** If results contain analyses, use `outputKey: 'analyses' as const`, not `outputKey: 'processedItems' as const`.
320
+ **Name the `stateKey` after the content.** The stateKey is now the 2nd argument to `.map()`. If results contain analyses, use `.map('title', 'analyses', { ... })`, not `.map('title', 'processedItems', { ... })`.
321
+
322
+ ### Naming convention for filter/map parameters
323
+
324
+ `IterateResult.filter()` and `.map()` take two parameters: the input item and the AI result. **Name them after what they represent**, not generic names like `item` and `r`:
325
+
326
+ ```typescript
327
+ // ❌ DON'T DO THIS - generic parameter names
328
+ state.validations.filter((item, r) => r.matches)
329
+ state.transcripts.filter((t) => t.hasTranscript) // WRONG: single param is the item, not the result
330
+
331
+ // ✅ DO THIS - descriptive names that reflect the data
332
+ state.validations.filter((crawledResult, validation) => validation.matches)
333
+ state.transcripts.filter((match, extraction) => extraction.hasTranscript)
334
+ ```
335
+
336
+ The first parameter is always the input item (what you passed to `over`), and the second is the AI's output (what the `outputSchema` describes). A single-parameter callback only receives the item — if you need the AI result, you must use both parameters.
337
+
338
+ ### Nested brain state spreading
339
+
340
+ When a `.brain()` step runs an inner brain, the inner brain's final state is spread directly onto the outer state:
341
+
342
+ ```typescript
343
+ .brain('Search and validate', searchAndValidate)
344
+ ```
345
+
346
+ All properties from the inner brain's final state are merged onto the outer state. Subsequent steps can access those properties directly (e.g., `state.matches`). The inner brain's state type is fully inferred from its definition, so you get full type safety.
347
+
348
+ If you want namespacing (to avoid collisions with existing state properties), design the inner brain to return a namespaced state shape:
349
+
350
+ ```typescript
351
+ // Inner brain returns its results under a single key
352
+ const searchAndValidate = brain('search-and-validate')
353
+ .step('Search', ({ state }) => ({ ...state, matches: [] }))
354
+ .step('Package', ({ state }) => ({
355
+ searchResults: { matches: state.matches }
356
+ }));
357
+
358
+ // Outer brain accesses via state.searchResults.matches
359
+ brain('parent')
360
+ .step('Init', () => ({ query: 'test' }))
361
+ .brain('Search and validate', searchAndValidate)
362
+ .step('Use results', ({ state }) => ({
363
+ ...state,
364
+ count: state.searchResults.matches.length,
365
+ }));
366
+ ```
300
367
 
301
368
  ## Brain DSL Type Inference
302
369
 
@@ -328,6 +395,30 @@ brain('example')
328
395
 
329
396
  The type inference flows through the entire chain, making the code cleaner and more maintainable.
330
397
 
398
+ ### Brains that receive initial state from outside
399
+
400
+ If a brain receives its initial state from the outside — via `.map()`, `.brain()`, or `run({ initialState })` — declare the state type in the generic parameters:
401
+
402
+ ```typescript
403
+ // This brain is used inside .map() — it receives thread data as initial state
404
+ const categorizeBrain = brain<{}, RawThread>('categorize-thread')
405
+ .prompt('Categorize', {
406
+ message: ({ state }) => `Categorize: <%= '${state.subject}' %>`,
407
+ outputSchema: z.object({ category: z.string() }),
408
+ });
409
+
410
+ // The parent brain maps over threads
411
+ brain('email-digest')
412
+ .step('Fetch', async () => ({ threads: await fetchThreads() }))
413
+ .map('Categorize', 'categorized', {
414
+ run: categorizeBrain,
415
+ over: ({ state }) => state.threads,
416
+ initialState: (thread) => thread,
417
+ });
418
+ ```
419
+
420
+ Without the generic, `TState` defaults to `object` and steps can't see any properties. The generic tells TypeScript "this brain starts with this shape." If the brain builds its own state from nothing (first step returns the initial shape), skip the generic — inference handles it.
421
+
331
422
  ## Error Handling in Brains
332
423
 
333
424
  **Important**: Do NOT catch errors in brain steps unless error handling is specifically part of the brain's workflow logic. The brain runner handles all errors automatically.
@@ -375,13 +466,9 @@ brain('validation-example')
375
466
 
376
467
  Most generated brains should not have try-catch blocks. Only use them when the error state is meaningful to subsequent steps in the workflow.
377
468
 
378
- ## UI Steps for Form Generation
469
+ ## Page Steps for Form Generation
379
470
 
380
- When you need to collect user input, use the `.ui()` method. The pattern is:
381
- 1. `.ui()` generates the page
382
- 2. Next step gets `page.url` and `page.webhook`
383
- 3. Notify users, then use `.wait()` with `page.webhook`
384
- 4. Step after `.wait()` gets form data in `response`
471
+ When you need to collect user input, use the `.page()` method with `formSchema`. The brain auto-suspends after generating the page, then auto-merges the form response directly onto state. Use the `onCreated` callback for side effects.
385
472
 
386
473
  ```typescript
387
474
  import { z } from 'zod';
@@ -391,74 +478,98 @@ brain('feedback-collector')
391
478
  ...state,
392
479
  userName: 'John',
393
480
  }))
394
- // Generate the form
395
- .ui('Collect Feedback', {
396
- template: (state) => <%= '\`' %>
481
+ // Generate the form, onCreated, auto-suspend, auto-merge response
482
+ .page('Collect Feedback', ({ state, slack }) => ({
483
+ prompt: <%= '\`' %>
397
484
  Create a feedback form for <%= '${state.userName}' %>:
398
485
  - Rating (1-5)
399
486
  - Comments textarea
400
487
  - Submit button
401
488
  <%= '\`' %>,
402
- responseSchema: z.object({
489
+ formSchema: z.object({
403
490
  rating: z.number().min(1).max(5),
404
491
  comments: z.string(),
405
492
  }),
406
- })
407
- // Notify users
408
- .step('Notify', async ({ state, page, slack }) => {
409
- await slack.post('#feedback', `Fill out: <%= '${page.url}' %>`);
410
- return state;
411
- })
412
- // Wait for form submission
413
- .wait('Wait for submission', ({ page }) => page.webhook)
414
- // Form data comes through response (not page)
415
- .step('Process', ({ state, response }) => ({
493
+ onCreated: async (page) => {
494
+ await slack.post('#feedback', `Fill out: <%= '${page.url}' %>`);
495
+ },
496
+ }))
497
+ // No .handle() needed — form data auto-merges directly onto state
498
+ .step('Process', ({ state }) => ({
416
499
  ...state,
417
- rating: response.rating, // Typed from responseSchema
418
- comments: response.comments,
500
+ // state.rating and state.comments are typed
501
+ processed: true,
419
502
  }));
420
503
  ```
421
504
 
422
505
  Key points:
506
+ - `page` is available inside the `onCreated` callback, not in a separate step
423
507
  - `page.url` - where to send users
424
- - `page.webhook` - use with `.wait()` to pause for submission
425
- - `response` - form data arrives here (in step after `.wait()`)
426
- - You control how users are notified (Slack, email, etc.)
508
+ - The brain auto-suspends after `.page()` with `formSchema`
509
+ - Form data is spread directly onto state (e.g., `state.rating`, `state.comments`)
510
+ - You control how users are notified (Slack, email, etc.) inside `onCreated`
511
+
512
+ See `/docs/brain-dsl-guide.md` for more page step examples.
427
513
 
428
- See `/docs/brain-dsl-guide.md` for more UI step examples.
514
+ ### Use `.page()` for Data Display, Not `generatePage` in Loops
429
515
 
430
- ## Service Organization
516
+ When a brain needs to display data and collect user input, prefer `.page()` steps over calling the `generatePage` tool inside a `.prompt()` loop. The `.page()` step passes data via `props` — the page generation LLM never sees the actual data, only its shape. This is more reliable and keeps data out of the LLM context.
431
517
 
432
- When implementing services for the project brain, consider creating a `services/` directory at the root of your project to keep service implementations organized and reusable:
518
+ ```typescript
519
+ // ❌ DON'T DO THIS - LLM fetches data then passes it to generatePage
520
+ brain('reader')
521
+ .prompt('Show Articles', () => ({
522
+ message: 'Fetch articles and create a page showing them',
523
+ outputSchema: z.object({ articlesShown: z.number() }),
524
+ loop: { tools: { fetchArticles, generatePage, waitForWebhook } },
525
+ }))
433
526
 
527
+ // ✅ DO THIS - fetch data in a step, display with .page()
528
+ brain('reader')
529
+ .step('Fetch Articles', async () => {
530
+ const articles = await fetchArticles();
531
+ return { articles };
532
+ })
533
+ .page('Show Articles', ({ state }) => ({
534
+ prompt: 'Create a reading list with checkboxes to mark articles as read',
535
+ props: { articles: state.articles },
536
+ formSchema: z.object({
537
+ readArticleIds: z.array(z.string()),
538
+ }),
539
+ }))
434
540
  ```
435
- services/
436
- ├── gmail.js # Gmail API integration
437
- ├── slack.js # Slack notifications
438
- ├── database.js # Database client
439
- └── analytics.js # Analytics tracking
541
+
542
+ The `.page()` step automatically suspends, waits for the form submission, and merges the response onto state. Use `.prompt()` loops for tasks that genuinely need LLM decision-making with tools (research, multi-step reasoning, human-in-the-loop via webhooks), not for data display.
543
+
544
+ ## Plugin Organization
545
+
546
+ When implementing plugins for the project brain, keep plugin implementations in the `src/plugins/` directory to stay organized and reusable:
547
+
548
+ ```
549
+ src/plugins/
550
+ ├── gmail.ts # Gmail API integration
551
+ ├── slack.ts # Slack notifications
552
+ ├── database.ts # Database client
553
+ └── analytics.ts # Analytics tracking
440
554
  ```
441
555
 
442
- Then in your `brain.ts` (at the project root):
556
+ Then in your `src/brain.ts`:
443
557
 
444
558
  ```typescript
445
559
  import { createBrain } from '@positronic/core';
446
- import gmail from './services/gmail.js';
447
- import slack from './services/slack.js';
448
- import database from './services/database.js';
449
- import analytics from './services/analytics.js';
560
+ import { gmail } from './plugins/gmail.js';
561
+ import { slack } from './plugins/slack.js';
562
+ import { database } from './plugins/database.js';
563
+ import { analytics } from './plugins/analytics.js';
450
564
 
451
565
  export const brain = createBrain({
452
- services: {
453
- gmail,
454
- slack,
455
- database,
456
- analytics
457
- }
566
+ plugins: [gmail, slack, database, analytics],
458
567
  });
459
568
  ```
460
569
 
461
- This keeps your service implementations separate from your brain logic and makes them easier to test and maintain.
570
+ Each plugin is defined with `definePlugin` from `@positronic/core`. See `/docs/plugin-guide.md` for how to create plugins with typed config, tools, and adapters.
571
+
572
+ This keeps your plugin implementations separate from your brain logic and makes them easier to test and maintain.
462
573
 
463
574
  ## Rate Limiting with bottleneck
464
575
 
@@ -495,7 +606,7 @@ const slow = bottleneck({ rpd: 1000 }); // 1000 per day
495
606
  Create one limiter per API and wrap all calls through it:
496
607
 
497
608
  ```typescript
498
- // services/github.ts
609
+ // src/services/github.ts
499
610
  import { bottleneck } from '../utils/bottleneck.js';
500
611
 
501
612
  const limit = bottleneck({ rps: 10 });
@@ -555,7 +666,7 @@ const alertSchema = z.object({
555
666
  });
556
667
 
557
668
  const alertBrain = brain('Alert System')
558
- .withOptionsSchema(alertSchema)
669
+ .withOptions(alertSchema)
559
670
  .step('Check Threshold', ({ state, options }) => ({
560
671
  ...state,
561
672
  shouldAlert: state.errorCount > parseInt(options.alertThreshold)
@@ -585,9 +696,9 @@ This project uses ES modules (ESM). **Always include the `.js` extension in your
585
696
 
586
697
  ```typescript
587
698
  // ✅ CORRECT - Include .js extension
588
- import { brain } from '../brain.js'; // From a file in brains/ directory
589
- import { analyzeData } from '../utils/analyzer.js';
590
- import gmail from '../services/gmail.js';
699
+ import { brain } from '../brain.js'; // From a file in src/brains/ (brain.ts is at src/brain.ts)
700
+ import { analyzeData } from '../utils/analyzer.js'; // From src/brains/ to src/utils/
701
+ import gmail from '../services/gmail.js'; // From src/brains/ to src/services/
591
702
 
592
703
  // ❌ WRONG - Missing .js extension
593
704
  import { brain } from '../brain';
@@ -615,7 +726,7 @@ Start by following the brain testing guide (`/docs/brain-testing-guide.md`) and
615
726
  // tests/my-new-brain.test.ts
616
727
  import { describe, it, expect } from '@jest/globals';
617
728
  import { createMockClient, runBrainTest } from './test-utils.js';
618
- import myNewBrain from '../brains/my-new-brain.js';
729
+ import myNewBrain from '../src/brains/my-new-brain.js';
619
730
 
620
731
  describe('MyNewBrain', () => {
621
732
  it('should process data and return expected result', async () => {
@@ -678,7 +789,7 @@ Build the brain one step at a time, testing as you go. **Actually run the brain
678
789
 
679
790
  ```bash
680
791
  # 1. Create the brain with just the first step
681
- # Write minimal implementation in brains/my-new-brain.ts
792
+ # Write minimal implementation in src/brains/my-new-brain.ts
682
793
 
683
794
  # 2. Run the brain to test the first step
684
795
  px brain run my-new-brain
@@ -739,34 +850,23 @@ export default feedbackBrain;
739
850
  // Step 3: Run and check logs, see it doesn't analyze yet
740
851
  // Step 4: Add sentiment analysis step
741
852
  .prompt('Analyze sentiment', {
742
- template: ({ feedback }) =>
853
+ message: ({ state: { feedback } }) =>
743
854
  <%= '\`Analyze the sentiment of this feedback: "${feedback}"\`' %>,
744
- outputSchema: {
745
- schema: z.object({
746
- sentiment: z.enum(['positive', 'neutral', 'negative']),
747
- score: z.number().min(0).max(1)
748
- }),
749
- name: 'sentimentAnalysis' as const
750
- }
855
+ outputSchema: z.object({
856
+ sentiment: z.enum(['positive', 'neutral', 'negative']),
857
+ score: z.number().min(0).max(1)
858
+ }),
751
859
  })
752
860
 
753
861
  // Step 5: Run again, check logs, test still fails (no response)
754
862
  // Step 6: Add response generation
755
863
  .prompt('Generate response', {
756
- template: ({ sentimentAnalysis, feedback }) =>
757
- <%= '\`Generate a brief response to this ${sentimentAnalysis.sentiment} feedback: "${feedback}"\`' %>,
758
- outputSchema: {
759
- schema: z.object({
760
- response: z.string()
761
- }),
762
- name: 'responseData' as const
763
- }
764
- })
765
- .step('Format output', ({ state }) => ({
766
- ...state,
767
- sentiment: state.sentimentAnalysis.sentiment,
768
- response: state.responseData.response
769
- }));
864
+ message: ({ state: { sentiment, feedback } }) =>
865
+ <%= '\`Generate a brief response to this ${sentiment} feedback: "${feedback}"\`' %>,
866
+ outputSchema: z.object({
867
+ response: z.string()
868
+ }),
869
+ });
770
870
 
771
871
  // Step 7: Run test - it should pass now!
772
872
  ```