@positronic/template-new-project 0.0.53 → 0.0.55

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,10 +53,10 @@ module.exports = {
53
53
  ],
54
54
  setup: async ctx => {
55
55
  const devRootPath = process.env.POSITRONIC_LOCAL_PATH;
56
- let coreVersion = '^0.0.53';
57
- let cloudflareVersion = '^0.0.53';
58
- let clientVercelVersion = '^0.0.53';
59
- let genUIComponentsVersion = '^0.0.53';
56
+ let coreVersion = '^0.0.55';
57
+ let cloudflareVersion = '^0.0.55';
58
+ let clientVercelVersion = '^0.0.55';
59
+ let genUIComponentsVersion = '^0.0.55';
60
60
 
61
61
  // Map backend selection to package names
62
62
  const backendPackageMap = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@positronic/template-new-project",
3
- "version": "0.0.53",
3
+ "version": "0.0.55",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Bundle entry point for client-side rendering.
3
3
  *
4
- * This file is bundled by esbuild into dist/components.js which exposes
4
+ * This file is bundled by esbuild into .positronic/dist/components.js which exposes
5
5
  * React components to window.PositronicComponents for use by generated pages.
6
6
  *
7
- * When you add custom components to ./index.ts, they will automatically
7
+ * When you add custom components to components/index.ts, they will automatically
8
8
  * be included in the bundle.
9
9
  */
10
- import { components } from './index.js';
10
+ import { components } from '../components/index.js';
11
11
 
12
12
  // Extract the React component from each UIComponent and expose to window
13
13
  const PositronicComponents: Record<string, React.ComponentType<any>> = {};
@@ -38,7 +38,8 @@ For testing guidance, see `/docs/brain-testing-guide.md`
38
38
  The Brain DSL provides a fluent API for defining AI workflows:
39
39
 
40
40
  ```typescript
41
- import { brain } from '@positronic/core';
41
+ // Import from the project brain wrapper (see positronic-guide.md)
42
+ import { brain } from '../brain.js';
42
43
 
43
44
  const myBrain = brain('my-brain')
44
45
  .step('Initialize', ({ state }) => ({
@@ -46,12 +47,12 @@ const myBrain = brain('my-brain')
46
47
  initialized: true
47
48
  }))
48
49
  .step('Process', async ({ state, resources }) => {
49
- // Access resources
50
- const doc = await resources.get('example.md');
50
+ // Access resources with type-safe API
51
+ const content = await resources.example.loadText();
51
52
  return {
52
53
  ...state,
53
54
  processed: true,
54
- content: doc.content
55
+ content
55
56
  };
56
57
  });
57
58
 
package/template/_env CHANGED
@@ -1,6 +1,10 @@
1
1
  # Development Environment
2
2
  NODE_ENV=development
3
3
 
4
+ # Google AI API Key (required for running brains)
5
+ # Get your API key at https://aistudio.google.com/apikey
6
+ GOOGLE_GENERATIVE_AI_API_KEY=
7
+
4
8
  # Cloudflare R2 Configuration
5
9
  R2_BUCKET_NAME=<%= projectName %>
6
10
 
@@ -16,12 +16,11 @@ node_modules/
16
16
  # Mac specific
17
17
  .DS_Store
18
18
 
19
- dist/
20
-
21
19
  # Test coverage
22
20
  coverage/
23
21
 
24
22
  .positronic-server.log
25
23
  .positronic-server.pid
26
24
 
27
- resources.d.ts
25
+ resources.d.ts
26
+ secrets.d.ts
package/template/brain.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { createBrain } from '@positronic/core';
1
+ import { createBrain, defaultTools } from '@positronic/core';
2
2
  import { components } from './components/index.js';
3
3
 
4
4
  /**
5
- * Project-level brain function with pre-configured components.
5
+ * Project-level brain function with pre-configured components and tools.
6
6
  *
7
7
  * All brains in your project should import from this file:
8
8
  *
@@ -13,10 +13,15 @@ import { components } from './components/index.js';
13
13
  * .step('Do something', ({ state }) => ({ ...state, done: true }));
14
14
  * ```
15
15
  *
16
+ * Default tools available in agent steps:
17
+ * - generateUI: Generate interactive UI components
18
+ * - consoleLog: Log messages for debugging
19
+ * - done: Complete the agent and return a result
20
+ *
16
21
  * To add services (e.g., Slack, Gmail, database clients):
17
22
  *
18
23
  * ```typescript
19
- * import { createBrain } from '@positronic/core';
24
+ * import { createBrain, defaultTools } from '@positronic/core';
20
25
  * import { components } from './components/index.js';
21
26
  * import slack from './services/slack.js';
22
27
  * import gmail from './services/gmail.js';
@@ -24,6 +29,7 @@ import { components } from './components/index.js';
24
29
  * export const brain = createBrain({
25
30
  * services: { slack, gmail },
26
31
  * components,
32
+ * defaultTools,
27
33
  * });
28
34
  * ```
29
35
  *
@@ -37,27 +43,24 @@ import { components } from './components/index.js';
37
43
  * });
38
44
  * ```
39
45
  *
40
- * You can also create agents directly:
46
+ * You can also create agents directly with access to default tools:
41
47
  *
42
48
  * ```typescript
43
- * export default brain('my-agent', ({ slack, env }) => ({
49
+ * export default brain('my-agent', ({ slack, tools }) => ({
44
50
  * system: 'You are a helpful assistant',
45
51
  * prompt: 'Help the user with their request',
46
52
  * tools: {
53
+ * ...tools, // includes generateUI, consoleLog, done
47
54
  * notify: {
48
55
  * description: 'Send a Slack notification',
49
56
  * inputSchema: z.object({ message: z.string() }),
50
57
  * execute: ({ message }) => slack.postMessage('#general', message),
51
58
  * },
52
- * done: {
53
- * description: 'Complete the task',
54
- * inputSchema: z.object({ result: z.string() }),
55
- * terminal: true,
56
- * },
57
59
  * },
58
60
  * }));
59
61
  * ```
60
62
  */
61
63
  export const brain = createBrain({
62
64
  components,
65
+ defaultTools,
63
66
  });
@@ -0,0 +1,33 @@
1
+ import { brain } from '../brain.js';
2
+
3
+ /**
4
+ * A simple agent brain that demonstrates the default tools.
5
+ *
6
+ * This brain uses only a system prompt and tools - no explicit user prompt needed.
7
+ * When prompt is omitted, the agent automatically starts with "Begin."
8
+ *
9
+ * This brain:
10
+ * 1. Uses generateUI to create a form asking for the user's name
11
+ * 2. Waits for the user to submit the form
12
+ * 3. Uses consoleLog to log a personalized greeting
13
+ * 4. Uses done to complete with a welcome message
14
+ *
15
+ * Run with: px brain run hello
16
+ */
17
+ export default brain('hello', ({ tools }) => ({
18
+ system: `You are a friendly greeter for the Positronic framework.
19
+ Your job is to welcome new users and make them feel excited about building AI workflows.
20
+
21
+ When you start:
22
+ 1. Use generateUI to create a simple, welcoming form that asks for the user's name
23
+ (use a friendly title and a single text input)
24
+ 2. After receiving their name, use consoleLog to log a warm, personalized greeting
25
+ 3. Use done to complete with an encouraging message about what they can build
26
+
27
+ You have access to these tools:
28
+ - generateUI: Create a form to collect the user's name
29
+ - consoleLog: Log messages to the console
30
+ - done: Complete the greeting with a final message`,
31
+
32
+ tools,
33
+ }));
@@ -165,7 +165,11 @@ Each step receives these parameters:
165
165
  - `client` - AI client for generating structured objects
166
166
  - `resources` - Loaded resources (files, documents, etc.)
167
167
  - `options` - Runtime options passed to the brain
168
- - Custom services (if configured with `.withServices()`)
168
+ - `response` - Webhook response data (available after `waitFor` completes)
169
+ - `page` - Generated page object (available after `.ui()` step)
170
+ - `pages` - Pages service for HTML page management
171
+ - `env` - Runtime environment containing `origin` (base URL) and `secrets` (typed secrets object)
172
+ - Custom services (if configured with `.withServices()` or `createBrain()`)
169
173
 
170
174
  ## Configuration Methods
171
175
 
@@ -473,15 +477,141 @@ expect(result.finalState.data).toEqual({ id: '123', name: 'Test' });
473
477
  - Services are not serialized - they're for side effects and external interactions
474
478
  - Each brain instance maintains its own service references
475
479
 
480
+ ### Tool Configuration with `withTools()`
481
+
482
+ The `withTools()` method registers tools that can be used by agent steps:
483
+
484
+ ```typescript
485
+ import { z } from 'zod';
486
+
487
+ const brainWithTools = brain('Tool Brain')
488
+ .withTools({
489
+ fetchData: {
490
+ description: 'Fetch data from an external API',
491
+ inputSchema: z.object({
492
+ endpoint: z.string(),
493
+ params: z.record(z.string()).optional()
494
+ }),
495
+ execute: async ({ endpoint, params }) => {
496
+ const url = new URL(endpoint);
497
+ if (params) {
498
+ Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
499
+ }
500
+ const response = await fetch(url);
501
+ return response.json();
502
+ }
503
+ },
504
+ saveToDatabase: {
505
+ description: 'Save data to the database',
506
+ inputSchema: z.object({
507
+ table: z.string(),
508
+ data: z.any()
509
+ }),
510
+ execute: async ({ table, data }) => {
511
+ // Database save logic
512
+ return { success: true, id: 'generated-id' };
513
+ }
514
+ }
515
+ })
516
+ .brain('Data Agent', {
517
+ system: 'You can fetch and save data.',
518
+ prompt: 'Fetch user data and save the summary.'
519
+ // Tools defined with withTools() are automatically available
520
+ });
521
+ ```
522
+
523
+ ### Component Configuration with `withComponents()`
524
+
525
+ The `withComponents()` method registers custom UI components for use in `.ui()` steps:
526
+
527
+ ```typescript
528
+ const brainWithComponents = brain('Custom UI Brain')
529
+ .withComponents({
530
+ CustomCard: {
531
+ description: 'A styled card component for displaying content',
532
+ props: z.object({
533
+ title: z.string(),
534
+ content: z.string(),
535
+ variant: z.enum(['default', 'highlighted', 'warning']).default('default')
536
+ }),
537
+ render: (props) => `
538
+ <div class="card card-<%= '${props.variant}' %>">
539
+ <h3><%= '${props.title}' %></h3>
540
+ <p><%= '${props.content}' %></p>
541
+ </div>
542
+ `
543
+ },
544
+ DataTable: {
545
+ description: 'A table for displaying structured data',
546
+ props: z.object({
547
+ headers: z.array(z.string()),
548
+ rows: z.array(z.array(z.string()))
549
+ }),
550
+ render: (props) => {
551
+ // Build table HTML from headers and rows
552
+ const headerRow = props.headers.map(h => '<th>' + h + '</th>').join('');
553
+ const bodyRows = props.rows.map(row =>
554
+ '<tr>' + row.map(cell => '<td>' + cell + '</td>').join('') + '</tr>'
555
+ ).join('');
556
+ return '<table><thead><tr>' + headerRow + '</tr></thead><tbody>' + bodyRows + '</tbody></table>';
557
+ }
558
+ }
559
+ })
560
+ .ui('Dashboard', {
561
+ template: (state) => `
562
+ Create a dashboard using CustomCard components to display:
563
+ - User name: <%= '${state.userName}' %>
564
+ - Account status: <%= '${state.status}' %>
565
+ Use DataTable to show recent activity.
566
+ `,
567
+ responseSchema: z.object({
568
+ acknowledged: z.boolean()
569
+ })
570
+ });
571
+ ```
572
+
573
+ ### Using `createBrain()` for Project Configuration
574
+
575
+ For project-wide configuration, use `createBrain()` in your `brain.ts` file:
576
+
577
+ ```typescript
578
+ // brain.ts
579
+ import { createBrain } from '@positronic/core';
580
+ import { z } from 'zod';
581
+
582
+ export const brain = createBrain({
583
+ services: {
584
+ logger: console,
585
+ api: apiClient
586
+ },
587
+ tools: {
588
+ search: {
589
+ description: 'Search the web',
590
+ inputSchema: z.object({ query: z.string() }),
591
+ execute: async ({ query }) => searchWeb(query)
592
+ }
593
+ },
594
+ components: {
595
+ Alert: {
596
+ description: 'Alert banner',
597
+ props: z.object({ message: z.string(), type: z.enum(['info', 'warning', 'error']) }),
598
+ render: (props) => `<div class="alert alert-<%= '${props.type}' %>"><%= '${props.message}' %></div>`
599
+ }
600
+ }
601
+ });
602
+ ```
603
+
604
+ All brains created with this factory will have access to the configured services, tools, and components.
605
+
476
606
  ## Running Brains
477
607
 
478
608
  ### Basic Execution
479
609
 
480
610
  ```typescript
481
- const brain = brain('Simple').step('Process', () => ({ result: 'done' }));
611
+ const myBrain = brain('Simple').step('Process', () => ({ result: 'done' }));
482
612
 
483
613
  // Run and collect events
484
- for await (const event of brain.run({ client: aiClient })) {
614
+ for await (const event of myBrain.run({ client: aiClient })) {
485
615
  console.log(event.type); // START, STEP_START, STEP_COMPLETE, etc.
486
616
  }
487
617
  ```
@@ -678,6 +808,178 @@ Extract prompts to separate files when:
678
808
  - The prompt might be reused in other brains
679
809
  - You want to test the prompt logic separately
680
810
 
811
+ ## Batch Prompt Mode
812
+
813
+ When you need to run the same prompt over multiple items, use batch mode with the `over` option:
814
+
815
+ ```typescript
816
+ brain('Batch Processor')
817
+ .step('Initialize', () => ({
818
+ items: [
819
+ { id: 1, title: 'First item' },
820
+ { id: 2, title: 'Second item' },
821
+ { id: 3, title: 'Third item' }
822
+ ]
823
+ }))
824
+ .prompt('Summarize Items', {
825
+ template: (item) => `Summarize this item: <%= '${item.title}' %>`,
826
+ outputSchema: {
827
+ schema: z.object({ summary: z.string() }),
828
+ name: 'summaries' as const
829
+ }
830
+ }, {
831
+ over: (state) => state.items, // Array to iterate over
832
+ concurrency: 10, // Parallel requests (default: 10)
833
+ stagger: 100, // Delay between requests in ms
834
+ retry: {
835
+ maxRetries: 3,
836
+ backoff: 'exponential',
837
+ initialDelay: 1000,
838
+ maxDelay: 30000,
839
+ },
840
+ error: (item, error) => ({ summary: 'Failed to summarize' }) // Fallback on error
841
+ })
842
+ .step('Process Results', ({ state }) => ({
843
+ ...state,
844
+ // summaries is [item, response][] - array of tuples
845
+ processedSummaries: state.summaries.map(([item, response]) => ({
846
+ id: item.id,
847
+ summary: response.summary
848
+ }))
849
+ }));
850
+ ```
851
+
852
+ ### Batch Options
853
+
854
+ - `over: (state) => T[]` - Function returning the array to iterate over
855
+ - `concurrency: number` - Maximum parallel requests (default: 10)
856
+ - `stagger: number` - Milliseconds to wait between starting requests
857
+ - `retry: RetryConfig` - Retry configuration for failed requests
858
+ - `error: (item, error) => Response` - Fallback function when a request fails
859
+
860
+ ### Result Format
861
+
862
+ The result is stored as an array of `[item, response]` tuples, preserving the relationship between each input item and its generated response.
863
+
864
+ ## Agent Steps
865
+
866
+ For complex AI workflows that require tool use, use the `.brain()` method with an agent configuration:
867
+
868
+ ```typescript
869
+ brain('Research Assistant')
870
+ .step('Initialize', () => ({
871
+ query: 'What are the latest developments in AI?'
872
+ }))
873
+ .brain('Research Agent', {
874
+ system: 'You are a helpful research assistant with access to search tools.',
875
+ prompt: ({ query }) => `Research this topic: <%= '${query}' %>`,
876
+ tools: {
877
+ search: {
878
+ description: 'Search the web for information',
879
+ inputSchema: z.object({
880
+ query: z.string().describe('The search query')
881
+ }),
882
+ execute: async ({ query }) => {
883
+ // Implement search logic
884
+ const results = await searchWeb(query);
885
+ return { results };
886
+ }
887
+ },
888
+ summarize: {
889
+ description: 'Summarize a piece of text',
890
+ inputSchema: z.object({
891
+ text: z.string().describe('Text to summarize')
892
+ }),
893
+ execute: async ({ text }) => {
894
+ return { summary: text.slice(0, 100) + '...' };
895
+ }
896
+ }
897
+ },
898
+ maxTokens: 10000,
899
+ })
900
+ .step('Format Results', ({ state, brainState }) => ({
901
+ ...state,
902
+ researchResults: brainState.response
903
+ }));
904
+ ```
905
+
906
+ ### Agent Configuration Options
907
+
908
+ - `system: string` - System prompt for the agent
909
+ - `prompt: string | ((state) => string)` - User prompt (can be a function)
910
+ - `tools: Record<string, ToolDefinition>` - Tools available to the agent
911
+ - `maxTokens: number` - Maximum tokens for the agent response
912
+
913
+ ### Tool Definition
914
+
915
+ Each tool requires:
916
+ - `description: string` - What the tool does
917
+ - `inputSchema: ZodSchema` - Zod schema for the tool's input
918
+ - `execute: (input) => Promise<any>` - Function to execute when the tool is called
919
+
920
+ ## Environment and Pages Service
921
+
922
+ ### The `env` Parameter
923
+
924
+ Steps have access to the runtime environment via the `env` parameter:
925
+
926
+ ```typescript
927
+ brain('Environment Example')
928
+ .step('Use Environment', ({ state, env }) => {
929
+ // env.origin - Base URL of the deployment
930
+ console.log('Running at:', env.origin);
931
+
932
+ // env.secrets - Type-augmented secrets object
933
+ const apiKey = env.secrets.EXTERNAL_API_KEY;
934
+
935
+ return {
936
+ ...state,
937
+ baseUrl: env.origin,
938
+ configured: true
939
+ };
940
+ });
941
+ ```
942
+
943
+ ### The `pages` Service
944
+
945
+ The `pages` service allows you to create and manage HTML pages programmatically:
946
+
947
+ ```typescript
948
+ brain('Page Creator')
949
+ .step('Create Custom Page', async ({ state, pages, env }) => {
950
+ // Create a page with HTML content
951
+ const page = await pages.create(
952
+ `<html>
953
+ <body>
954
+ <h1>Hello, <%= '${state.userName}' %>!</h1>
955
+ <p>Your dashboard is ready.</p>
956
+ </body>
957
+ </html>`,
958
+ { persist: true } // Keep the page after brain completes
959
+ );
960
+
961
+ return {
962
+ ...state,
963
+ dashboardUrl: page.url, // URL where users can view the page
964
+ pageWebhook: page.webhook // Webhook for form submissions (if any)
965
+ };
966
+ })
967
+ .step('Notify User', async ({ state, slack }) => {
968
+ await slack.post('#general', `Your dashboard: <%= '${state.dashboardUrl}' %>`);
969
+ return state;
970
+ });
971
+ ```
972
+
973
+ ### Page Options
974
+
975
+ - `persist: boolean` - If true, the page remains accessible after the brain completes
976
+
977
+ ### Page Object
978
+
979
+ The created page object contains:
980
+ - `url: string` - Public URL to access the page
981
+ - `webhook: WebhookConfig` - Webhook configuration for handling form submissions
982
+
681
983
  ## UI Steps
682
984
 
683
985
  UI steps allow brains to generate dynamic user interfaces using AI. The `.ui()` step generates a page and provides a `page` object to the next step. You then notify users and use `waitFor` to pause until the form is submitted.
@@ -56,7 +56,7 @@ const result = await runBrainTest(brain, {
56
56
  client: mockClient, // Optional: defaults to createMockClient()
57
57
  initialState: { count: 0 }, // Optional: initial state
58
58
  resources: resourceLoader, // Optional: resources
59
- brainOptions: { mode: 'test' } // Optional: brain-specific options
59
+ options: { mode: 'test' } // Optional: brain-specific options
60
60
  });
61
61
  ```
62
62
 
@@ -64,9 +64,9 @@ const result = await runBrainTest(brain, {
64
64
  - `completed: boolean` - Whether the brain completed successfully
65
65
  - `error: Error | null` - Any error that occurred
66
66
  - `finalState: State` - The final state after all steps
67
- - `steps: string[]` - Names of executed steps in order
67
+ - `events: BrainEvent[]` - All emitted events during execution
68
68
 
69
- ## MockObjectGenerator API
69
+ ## MockClient API
70
70
 
71
71
  ### Creating a Mock Client
72
72
 
@@ -76,44 +76,42 @@ const mockClient = createMockClient();
76
76
 
77
77
  ### Mocking Responses
78
78
 
79
- #### Single Response
80
- ```typescript
81
- mockClient.mockNextResponse({
82
- answer: 'The capital of France is Paris',
83
- confidence: 0.95
84
- });
85
- ```
79
+ Queue responses that will be consumed in order by `generateObject` calls:
86
80
 
87
- #### Multiple Responses
88
81
  ```typescript
82
+ // Queue one or more responses
89
83
  mockClient.mockResponses(
90
84
  { step1: 'completed' },
91
85
  { step2: 'processed' },
92
86
  { finalResult: 'success' }
93
87
  );
94
- ```
95
88
 
96
- #### Error Responses
97
- ```typescript
98
- mockClient.mockNextError('API rate limit exceeded');
99
- // or
100
- mockClient.mockNextError(new Error('Connection timeout'));
89
+ // Clear all mocked responses
90
+ mockClient.clearMocks();
101
91
  ```
102
92
 
103
93
  ### Assertions
104
94
 
95
+ Use standard Jest assertions on the mock:
96
+
105
97
  ```typescript
106
98
  // Check call count
107
- mockClient.expectCallCount(3);
99
+ expect(mockClient.generateObject).toHaveBeenCalledTimes(3);
108
100
 
109
101
  // Check call parameters
110
- mockClient.expectCalledWith({
111
- prompt: 'Generate a summary',
112
- schemaName: 'summarySchema'
113
- });
102
+ expect(mockClient.generateObject).toHaveBeenCalledWith(
103
+ expect.objectContaining({
104
+ prompt: expect.stringContaining('Generate a summary')
105
+ })
106
+ );
114
107
 
115
- // Get call history
116
- const calls = mockClient.getCalls();
108
+ // Check specific call (0-indexed)
109
+ expect(mockClient.generateObject).toHaveBeenNthCalledWith(
110
+ 1,
111
+ expect.objectContaining({
112
+ prompt: expect.stringContaining('first prompt')
113
+ })
114
+ );
117
115
  ```
118
116
 
119
117
  ## Testing Patterns
@@ -145,13 +143,19 @@ it('should generate personalized recommendations', async () => {
145
143
 
146
144
  ### Testing Error Cases
147
145
 
146
+ To test error handling, create a mock client that throws:
147
+
148
148
  ```typescript
149
149
  it('should handle API failures gracefully', async () => {
150
- // Arrange
151
- mockClient.mockNextError('Service temporarily unavailable');
150
+ // Arrange - create a mock that throws on the first call
151
+ const errorClient = {
152
+ generateObject: jest.fn().mockRejectedValue(
153
+ new Error('Service temporarily unavailable')
154
+ )
155
+ };
152
156
 
153
157
  // Act
154
- const result = await runBrainTest(processingBrain, { client: mockClient });
158
+ const result = await runBrainTest(processingBrain, { client: errorClient });
155
159
 
156
160
  // Assert
157
161
  expect(result.completed).toBe(false);
@@ -167,6 +171,7 @@ Verify that data flows correctly through your brain:
167
171
  it('should use customer data to generate personalized content', async () => {
168
172
  // Arrange
169
173
  const customerName = 'Alice';
174
+ const mockClient = createMockClient();
170
175
  mockClient.mockResponses(
171
176
  { greeting: 'Hello Alice!', tone: 'friendly' },
172
177
  { email: 'Personalized email content...' }
@@ -179,8 +184,11 @@ it('should use customer data to generate personalized content', async () => {
179
184
  });
180
185
 
181
186
  // Assert that the AI used the customer data
182
- const calls = mockClient.getCalls();
183
- expect(calls[0].params.prompt).toContain(customerName);
187
+ expect(mockClient.generateObject).toHaveBeenCalledWith(
188
+ expect.objectContaining({
189
+ prompt: expect.stringContaining(customerName)
190
+ })
191
+ );
184
192
  expect(result.finalState.email).toContain('Personalized email content');
185
193
  });
186
194
  ```
@@ -227,28 +235,23 @@ Following testing best practices, avoid testing:
227
235
  ## Complete Example
228
236
 
229
237
  ```typescript
230
- import { createMockClient, runBrainTest } from '@positronic/core';
231
- import analysisBrain from './analysis-brain.js';
238
+ import { createMockClient, runBrainTest } from './test-utils.js';
239
+ import analysisBrain from '../brains/analysis-brain.js';
232
240
 
233
241
  describe('analysis-brain', () => {
234
- let mockClient;
235
-
236
- beforeEach(() => {
237
- mockClient = createMockClient();
238
- });
239
-
240
242
  it('should analyze customer feedback and generate insights', async () => {
241
243
  // Arrange: Set up AI to return analysis
242
- mockClient.mockNextResponse({
244
+ const mockClient = createMockClient();
245
+ mockClient.mockResponses({
243
246
  sentiment: 'positive',
244
247
  keywords: ['innovation', 'quality', 'service'],
245
248
  summary: 'Customers appreciate product quality and innovation'
246
249
  });
247
250
 
248
251
  // Act: Run analysis on customer feedback
249
- const result = await runBrainTest(analysisBrain, {
252
+ const result = await runBrainTest(analysisBrain, {
250
253
  client: mockClient,
251
- initialState: {
254
+ initialState: {
252
255
  feedback: 'Your product is innovative and high quality...'
253
256
  }
254
257
  });
@@ -265,14 +268,18 @@ describe('analysis-brain', () => {
265
268
 
266
269
  it('should handle analysis service outages', async () => {
267
270
  // Arrange: Simulate service failure
268
- mockClient.mockNextError('Analysis service unavailable');
271
+ const errorClient = {
272
+ generateObject: jest.fn().mockRejectedValue(
273
+ new Error('Analysis service unavailable')
274
+ )
275
+ };
269
276
 
270
277
  // Act: Attempt analysis
271
- const result = await runBrainTest(analysisBrain, {
272
- client: mockClient,
278
+ const result = await runBrainTest(analysisBrain, {
279
+ client: errorClient,
273
280
  initialState: { feedback: 'Some feedback...' }
274
281
  });
275
-
282
+
276
283
  // Assert: Verify graceful failure
277
284
  expect(result.completed).toBe(false);
278
285
  expect(result.error?.message).toBe('Analysis service unavailable');
@@ -300,12 +307,12 @@ expect(result.finalState.myProperty).toBe('value');
300
307
  ### Debugging Tips
301
308
 
302
309
  ```typescript
303
- // See what prompts are being sent to AI
304
- const calls = mockClient.getCalls();
305
- console.log('AI prompts:', calls.map(c => c.params.prompt));
310
+ // See what prompts were sent to AI
311
+ const calls = mockClient.generateObject.mock.calls;
312
+ console.log('AI prompts:', calls.map(c => c[0].prompt));
306
313
 
307
- // Check execution order
308
- console.log('Steps executed:', result.steps);
314
+ // Check events that occurred during execution
315
+ console.log('Events:', result.events.map(e => e.type));
309
316
  ```
310
317
 
311
318
  ## Next Steps
@@ -42,10 +42,10 @@ import { brain } from '@positronic/core';
42
42
 
43
43
  ### Configuring Services
44
44
 
45
- To add project-wide services, modify the `brain.ts` file in the root directory:
45
+ To add project-wide services, modify the `brain.ts` file in the root directory using `createBrain()`:
46
46
 
47
47
  ```typescript
48
- import { brain as coreBrain, type Brain } from '@positronic/core';
48
+ import { createBrain } from '@positronic/core';
49
49
 
50
50
  // Define your services
51
51
  interface ProjectServices {
@@ -59,28 +59,25 @@ interface ProjectServices {
59
59
  };
60
60
  }
61
61
 
62
- // Export the wrapped brain function
63
- export function brain(
64
- brainConfig: string | { title: string; description?: string }
65
- ) {
66
- return coreBrain(brainConfig)
67
- .withServices({
68
- logger: {
69
- info: (msg) => console.log(`[<%= '${new Date().toISOString()}' %>] INFO: <%= '${msg}' %>`),
70
- error: (msg) => console.error(`[<%= '${new Date().toISOString()}' %>] ERROR: <%= '${msg}' %>`)
62
+ // Export the project brain factory
63
+ 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);
71
73
  },
72
- database: {
73
- get: async (key) => {
74
- // Your database implementation
75
- return localStorage.getItem(key);
76
- },
77
- set: async (key, value) => {
78
- // Your database implementation
79
- localStorage.setItem(key, JSON.stringify(value));
80
- }
74
+ set: async (key, value) => {
75
+ // Your database implementation
76
+ localStorage.setItem(key, JSON.stringify(value));
81
77
  }
82
- });
83
- }
78
+ }
79
+ }
80
+ });
84
81
  ```
85
82
 
86
83
  Now all brains automatically have access to these services:
@@ -158,9 +155,9 @@ Use `.env` files for configuration:
158
155
  ANTHROPIC_API_KEY=your-key-here
159
156
  OPENAI_API_KEY=your-key-here
160
157
 
161
- # Backend-specific
162
- <% if (backend === 'cloudflare') { %>CLOUDFLARE_ACCOUNT_ID=your-account-id
163
- CLOUDFLARE_API_TOKEN=your-api-token<% } %>
158
+ # Backend-specific (Cloudflare example)
159
+ CLOUDFLARE_ACCOUNT_ID=your-account-id
160
+ CLOUDFLARE_API_TOKEN=your-api-token
164
161
  ```
165
162
 
166
163
  ## Best Practices
@@ -224,7 +221,7 @@ const api = {
224
221
  post: async (path: string, data: any) => {
225
222
  const response = await fetch(`https://api.example.com<%= '${path}' %>`, {
226
223
  method: 'POST',
227
- headers: {
224
+ headers: {
228
225
  'Authorization': `Bearer <%= '${process.env.API_KEY}' %>`,
229
226
  'Content-Type': 'application/json'
230
227
  },
@@ -247,22 +247,20 @@ services/
247
247
  Then in your `brain.ts` (at the project root):
248
248
 
249
249
  ```typescript
250
+ import { createBrain } from '@positronic/core';
250
251
  import gmail from './services/gmail.js';
251
252
  import slack from './services/slack.js';
252
253
  import database from './services/database.js';
253
254
  import analytics from './services/analytics.js';
254
255
 
255
- export function brain(
256
- brainConfig: string | { title: string; description?: string }
257
- ) {
258
- return coreBrain(brainConfig)
259
- .withServices({
260
- gmail,
261
- slack,
262
- database,
263
- analytics
264
- });
265
- }
256
+ export const brain = createBrain({
257
+ services: {
258
+ gmail,
259
+ slack,
260
+ database,
261
+ analytics
262
+ }
263
+ });
266
264
  ```
267
265
 
268
266
  This keeps your service implementations separate from your brain logic and makes them easier to test and maintain.
@@ -452,7 +450,7 @@ describe('FeedbackProcessor', () => {
452
450
  });
453
451
 
454
452
  // Step 2: Create minimal brain implementation
455
- import { brain } from '@positronic/core';
453
+ import { brain } from '../brain.js';
456
454
  import { z } from 'zod';
457
455
 
458
456
  const feedbackBrain = brain('feedback-processor')
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * esbuild configuration for bundling UI components.
3
3
  *
4
- * This bundles the components from ./components/bundle.ts into a single
4
+ * This bundles the components from .positronic/bundle.ts into a single
5
5
  * JavaScript file that can be served to the browser.
6
6
  *
7
7
  * Run: npm run build:components
@@ -10,11 +10,11 @@
10
10
  import * as esbuild from 'esbuild';
11
11
 
12
12
  await esbuild.build({
13
- entryPoints: ['components/bundle.ts'],
13
+ entryPoints: ['.positronic/bundle.ts'],
14
14
  bundle: true,
15
15
  external: ['react', 'react-dom'],
16
16
  format: 'iife',
17
- outfile: 'dist/components.js',
17
+ outfile: '.positronic/dist/components.js',
18
18
  jsx: 'transform',
19
19
  jsxFactory: 'React.createElement',
20
20
  jsxFragment: 'React.Fragment',
@@ -15,7 +15,8 @@
15
15
  "dependencies": {
16
16
  "zod": "^3.24.1",
17
17
  "@positronic/client-vercel": "<%= positronicClientVercelVersion %>",
18
- "@ai-sdk/openai": "^1.3.22",
18
+ "@ai-sdk/google": "^3.0.13",
19
+ "ai": "^6.0.48",
19
20
  "@positronic/core": "<%= positronicCoreVersion %>",
20
21
  "@positronic/gen-ui-components": "<%= positronicGenUIComponentsVersion %>"<% if (backend === 'cloudflare') { %>,
21
22
  "@positronic/cloudflare": "<%= positronicCloudflareVersion %>"<% } %>
@@ -27,7 +28,7 @@
27
28
  "@jest/globals": "^30.0.4",
28
29
  "ts-jest": "^29.2.6",
29
30
  "@types/jest": "^30.0.0",
30
- "esbuild": "^0.24.0",
31
+ "esbuild": "^0.27.2",
31
32
  "@types/react": "^18.2.0"
32
33
  }
33
34
  }
@@ -1,9 +1,9 @@
1
1
  import { BrainRunner } from '@positronic/core';
2
2
  import { VercelClient } from '@positronic/client-vercel';
3
- import { openai } from '@ai-sdk/openai';
3
+ import { google } from '@ai-sdk/google';
4
4
 
5
5
  export const runner = new BrainRunner({
6
6
  adapters: [],
7
- client: new VercelClient(openai('gpt-4o-mini')),
7
+ client: new VercelClient(google('gemini-3-pro-preview')),
8
8
  resources: {},
9
9
  });