@positronic/template-new-project 0.0.77 → 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: ({ state: { 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,7 @@ 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', { ... })`.
300
321
 
301
322
  ### Naming convention for filter/map parameters
302
323
 
@@ -314,27 +335,35 @@ state.transcripts.filter((match, extraction) => extraction.hasTranscript)
314
335
 
315
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.
316
337
 
317
- ### Nested brain state mapping
338
+ ### Nested brain state spreading
318
339
 
319
- When a `.brain()` step runs an inner brain and maps its state into the outer brain, **destructure to select the fields you need** instead of casting:
340
+ When a `.brain()` step runs an inner brain, the inner brain's final state is spread directly onto the outer state:
320
341
 
321
342
  ```typescript
322
- // DON'T DO THIS - casting to force the type
323
- .brain(
324
- 'Search and validate',
325
- searchAndValidate,
326
- ({ brainState }) => brainState as { matches: Match[] },
327
- )
328
-
329
- // ✅ DO THIS - destructure to select and let inference work
330
- .brain(
331
- 'Search and validate',
332
- searchAndValidate,
333
- ({ brainState: { matches } }) => ({ matches }),
334
- )
343
+ .brain('Search and validate', searchAndValidate)
335
344
  ```
336
345
 
337
- The inner brain's state type is fully inferred from its definition. Destructuring picks the fields you want and TypeScript infers the outer state correctly — no casts needed. If the inner brain exports an interface for its output shape, import it for use in downstream steps (like prompt templates).
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
+ ```
338
367
 
339
368
  ## Brain DSL Type Inference
340
369
 
@@ -366,6 +395,30 @@ brain('example')
366
395
 
367
396
  The type inference flows through the entire chain, making the code cleaner and more maintainable.
368
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
+
369
422
  ## Error Handling in Brains
370
423
 
371
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.
@@ -413,13 +466,9 @@ brain('validation-example')
413
466
 
414
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.
415
468
 
416
- ## UI Steps for Form Generation
469
+ ## Page Steps for Form Generation
417
470
 
418
- When you need to collect user input, use the `.ui()` method. The pattern is:
419
- 1. `.ui()` generates the page
420
- 2. Next step gets `page.url` and `page.webhook`
421
- 3. Notify users, then use `.wait()` with `page.webhook`
422
- 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.
423
472
 
424
473
  ```typescript
425
474
  import { z } from 'zod';
@@ -429,74 +478,98 @@ brain('feedback-collector')
429
478
  ...state,
430
479
  userName: 'John',
431
480
  }))
432
- // Generate the form
433
- .ui('Collect Feedback', {
434
- template: ({ state }) => <%= '\`' %>
481
+ // Generate the form, onCreated, auto-suspend, auto-merge response
482
+ .page('Collect Feedback', ({ state, slack }) => ({
483
+ prompt: <%= '\`' %>
435
484
  Create a feedback form for <%= '${state.userName}' %>:
436
485
  - Rating (1-5)
437
486
  - Comments textarea
438
487
  - Submit button
439
488
  <%= '\`' %>,
440
- responseSchema: z.object({
489
+ formSchema: z.object({
441
490
  rating: z.number().min(1).max(5),
442
491
  comments: z.string(),
443
492
  }),
444
- })
445
- // Notify users
446
- .step('Notify', async ({ state, page, slack }) => {
447
- await slack.post('#feedback', `Fill out: <%= '${page.url}' %>`);
448
- return state;
449
- })
450
- // Wait for form submission
451
- .wait('Wait for submission', ({ page }) => page.webhook)
452
- // Form data comes through response (not page)
453
- .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 }) => ({
454
499
  ...state,
455
- rating: response.rating, // Typed from responseSchema
456
- comments: response.comments,
500
+ // state.rating and state.comments are typed
501
+ processed: true,
457
502
  }));
458
503
  ```
459
504
 
460
505
  Key points:
506
+ - `page` is available inside the `onCreated` callback, not in a separate step
461
507
  - `page.url` - where to send users
462
- - `page.webhook` - use with `.wait()` to pause for submission
463
- - `response` - form data arrives here (in step after `.wait()`)
464
- - 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`
465
511
 
466
- See `/docs/brain-dsl-guide.md` for more UI step examples.
512
+ See `/docs/brain-dsl-guide.md` for more page step examples.
467
513
 
468
- ## Service Organization
514
+ ### Use `.page()` for Data Display, Not `generatePage` in Loops
469
515
 
470
- 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:
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.
517
+
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
+ }))
471
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
+ }))
472
540
  ```
473
- services/
474
- ├── gmail.js # Gmail API integration
475
- ├── slack.js # Slack notifications
476
- ├── database.js # Database client
477
- └── 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
478
554
  ```
479
555
 
480
- Then in your `brain.ts` (at the project root):
556
+ Then in your `src/brain.ts`:
481
557
 
482
558
  ```typescript
483
559
  import { createBrain } from '@positronic/core';
484
- import gmail from './services/gmail.js';
485
- import slack from './services/slack.js';
486
- import database from './services/database.js';
487
- 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';
488
564
 
489
565
  export const brain = createBrain({
490
- services: {
491
- gmail,
492
- slack,
493
- database,
494
- analytics
495
- }
566
+ plugins: [gmail, slack, database, analytics],
496
567
  });
497
568
  ```
498
569
 
499
- 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.
500
573
 
501
574
  ## Rate Limiting with bottleneck
502
575
 
@@ -533,7 +606,7 @@ const slow = bottleneck({ rpd: 1000 }); // 1000 per day
533
606
  Create one limiter per API and wrap all calls through it:
534
607
 
535
608
  ```typescript
536
- // services/github.ts
609
+ // src/services/github.ts
537
610
  import { bottleneck } from '../utils/bottleneck.js';
538
611
 
539
612
  const limit = bottleneck({ rps: 10 });
@@ -593,7 +666,7 @@ const alertSchema = z.object({
593
666
  });
594
667
 
595
668
  const alertBrain = brain('Alert System')
596
- .withOptionsSchema(alertSchema)
669
+ .withOptions(alertSchema)
597
670
  .step('Check Threshold', ({ state, options }) => ({
598
671
  ...state,
599
672
  shouldAlert: state.errorCount > parseInt(options.alertThreshold)
@@ -623,9 +696,9 @@ This project uses ES modules (ESM). **Always include the `.js` extension in your
623
696
 
624
697
  ```typescript
625
698
  // ✅ CORRECT - Include .js extension
626
- import { brain } from '../brain.js'; // From a file in brains/ directory
627
- import { analyzeData } from '../utils/analyzer.js';
628
- 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/
629
702
 
630
703
  // ❌ WRONG - Missing .js extension
631
704
  import { brain } from '../brain';
@@ -653,7 +726,7 @@ Start by following the brain testing guide (`/docs/brain-testing-guide.md`) and
653
726
  // tests/my-new-brain.test.ts
654
727
  import { describe, it, expect } from '@jest/globals';
655
728
  import { createMockClient, runBrainTest } from './test-utils.js';
656
- import myNewBrain from '../brains/my-new-brain.js';
729
+ import myNewBrain from '../src/brains/my-new-brain.js';
657
730
 
658
731
  describe('MyNewBrain', () => {
659
732
  it('should process data and return expected result', async () => {
@@ -716,7 +789,7 @@ Build the brain one step at a time, testing as you go. **Actually run the brain
716
789
 
717
790
  ```bash
718
791
  # 1. Create the brain with just the first step
719
- # Write minimal implementation in brains/my-new-brain.ts
792
+ # Write minimal implementation in src/brains/my-new-brain.ts
720
793
 
721
794
  # 2. Run the brain to test the first step
722
795
  px brain run my-new-brain
@@ -777,34 +850,23 @@ export default feedbackBrain;
777
850
  // Step 3: Run and check logs, see it doesn't analyze yet
778
851
  // Step 4: Add sentiment analysis step
779
852
  .prompt('Analyze sentiment', {
780
- template: ({ state: { feedback } }) =>
853
+ message: ({ state: { feedback } }) =>
781
854
  <%= '\`Analyze the sentiment of this feedback: "${feedback}"\`' %>,
782
- outputSchema: {
783
- schema: z.object({
784
- sentiment: z.enum(['positive', 'neutral', 'negative']),
785
- score: z.number().min(0).max(1)
786
- }),
787
- name: 'sentimentAnalysis' as const
788
- }
855
+ outputSchema: z.object({
856
+ sentiment: z.enum(['positive', 'neutral', 'negative']),
857
+ score: z.number().min(0).max(1)
858
+ }),
789
859
  })
790
860
 
791
861
  // Step 5: Run again, check logs, test still fails (no response)
792
862
  // Step 6: Add response generation
793
863
  .prompt('Generate response', {
794
- template: ({ state: { sentimentAnalysis, feedback } }) =>
795
- <%= '\`Generate a brief response to this ${sentimentAnalysis.sentiment} feedback: "${feedback}"\`' %>,
796
- outputSchema: {
797
- schema: z.object({
798
- response: z.string()
799
- }),
800
- name: 'responseData' as const
801
- }
802
- })
803
- .step('Format output', ({ state }) => ({
804
- ...state,
805
- sentiment: state.sentimentAnalysis.sentiment,
806
- response: state.responseData.response
807
- }));
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
+ });
808
870
 
809
871
  // Step 7: Run test - it should pass now!
810
872
  ```