@positronic/template-new-project 0.0.61 → 0.0.63

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.61';
57
- let cloudflareVersion = '^0.0.61';
58
- let clientVercelVersion = '^0.0.61';
59
- let genUIComponentsVersion = '^0.0.61';
56
+ let coreVersion = '^0.0.63';
57
+ let cloudflareVersion = '^0.0.63';
58
+ let clientVercelVersion = '^0.0.63';
59
+ let genUIComponentsVersion = '^0.0.63';
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.61",
3
+ "version": "0.0.63",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -6,6 +6,9 @@
6
6
  "compatibility_flags": ["nodejs_compat", "nodejs_compat_populate_process_env"],
7
7
  "workers_dev": true,
8
8
  "preview_urls": true,
9
+ "limits": {
10
+ "cpu_ms": 300000
11
+ },
9
12
  "migrations": [
10
13
  {
11
14
  "tag": "v1",
@@ -101,7 +101,7 @@ export default approvalWebhook;
101
101
 
102
102
  ### Using Webhooks in Brains
103
103
 
104
- Import the webhook and use it with `waitFor`:
104
+ Import the webhook and use `.wait()` to pause execution:
105
105
 
106
106
  ```typescript
107
107
  import { brain } from '../brain.js';
@@ -109,9 +109,9 @@ import approvalWebhook from '../webhooks/approval.js';
109
109
 
110
110
  export default brain('approval-workflow')
111
111
  .step('Request approval', ({ state }) => ({
112
- state: { ...state, status: 'pending' },
113
- waitFor: [approvalWebhook(state.requestId)], // pause and wait
112
+ ...state, status: 'pending',
114
113
  }))
114
+ .wait('Wait for approval', ({ state }) => approvalWebhook(state.requestId))
115
115
  .step('Process approval', ({ state, response }) => ({
116
116
  ...state,
117
117
  status: response.approved ? 'approved' : 'rejected',
@@ -157,6 +157,43 @@ const mainBrain = brain('Main Process')
157
157
  );
158
158
  ```
159
159
 
160
+ ## Guard Clauses
161
+
162
+ Use `.guard()` to short-circuit a brain when a condition isn't met. If the predicate returns `true`, execution continues normally. If it returns `false`, all remaining steps are skipped and the brain completes with the current state.
163
+
164
+ ```typescript
165
+ brain('email-checker')
166
+ .step('Check Emails', async ({ state, client }) => {
167
+ const emails = await analyzeEmails(client, state);
168
+ return { ...state, emails };
169
+ })
170
+ .guard(({ state }) => state.emails.some(e => e.important))
171
+ // everything below only runs if guard passes
172
+ .ui('Review emails', { ... })
173
+ .step('Notify and wait', ...)
174
+ .step('Handle response', ...);
175
+ ```
176
+
177
+ Key points:
178
+ - The predicate is synchronous and receives `{ state, options }`
179
+ - Returns `true` to continue, `false` to skip all remaining steps
180
+ - The guard doesn't transform state — if you need to set "early exit" fields, do it in the step before the guard
181
+ - State type is unchanged after a guard (subsequent steps see the same type)
182
+ - Multiple guards can be chained — the first one that fails skips everything after it
183
+ - Halted steps appear as "halted" in the CLI watch view
184
+ - An optional title can be passed as the second argument: `.guard(predicate, 'Check emails exist')`
185
+
186
+ ### Multiple Guards
187
+
188
+ ```typescript
189
+ brain('processor')
190
+ .step('Init', () => ({ data: [], validated: false }))
191
+ .guard(({ state }) => state.data.length > 0, 'Has data')
192
+ .step('Validate', ({ state }) => ({ ...state, validated: true }))
193
+ .guard(({ state }) => state.validated, 'Is valid')
194
+ .step('Process', ({ state }) => ({ ...state, processed: true }));
195
+ ```
196
+
160
197
  ## Step Parameters
161
198
 
162
199
  Each step receives these parameters:
@@ -165,7 +202,7 @@ Each step receives these parameters:
165
202
  - `client` - AI client for generating structured objects
166
203
  - `resources` - Loaded resources (files, documents, etc.)
167
204
  - `options` - Runtime options passed to the brain
168
- - `response` - Webhook response data (available after `waitFor` completes)
205
+ - `response` - Webhook response data (available after `.wait()` completes)
169
206
  - `page` - Generated page object (available after `.ui()` step)
170
207
  - `pages` - Pages service for HTML page management
171
208
  - `env` - Runtime environment containing `origin` (base URL) and `secrets` (typed secrets object)
@@ -831,12 +868,7 @@ brain('Batch Processor')
831
868
  over: (state) => state.items, // Array to iterate over
832
869
  concurrency: 10, // Parallel requests (default: 10)
833
870
  stagger: 100, // Delay between requests in ms
834
- retry: {
835
- maxRetries: 3,
836
- backoff: 'exponential',
837
- initialDelay: 1000,
838
- maxDelay: 30000,
839
- },
871
+ maxRetries: 3,
840
872
  error: (item, error) => ({ summary: 'Failed to summarize' }) // Fallback on error
841
873
  })
842
874
  .step('Process Results', ({ state }) => ({
@@ -854,7 +886,7 @@ brain('Batch Processor')
854
886
  - `over: (state) => T[]` - Function returning the array to iterate over
855
887
  - `concurrency: number` - Maximum parallel requests (default: 10)
856
888
  - `stagger: number` - Milliseconds to wait between starting requests
857
- - `retry: RetryConfig` - Retry configuration for failed requests
889
+ - `maxRetries: number` - Maximum number of retries for failed requests (passed to the AI client SDK)
858
890
  - `error: (item, error) => Response` - Fallback function when a request fails
859
891
 
860
892
  ### Result Format
@@ -917,7 +949,60 @@ brain('Research Assistant')
917
949
  Each tool requires:
918
950
  - `description: string` - What the tool does
919
951
  - `inputSchema: ZodSchema` - Zod schema for the tool's input
920
- - `execute: (input) => Promise<any>` - Function to execute when the tool is called
952
+ - `execute: (input, context) => Promise<any>` - Function to execute when the tool is called
953
+ - `terminal?: boolean` - If true, calling this tool ends the agent loop
954
+
955
+ ### Tool Webhooks (waitFor)
956
+
957
+ Tools can pause agent execution and wait for external events by returning `{ waitFor: webhook(...) }` from their `execute` function. This is useful for human-in-the-loop workflows where the agent needs to wait for approval, external API callbacks, or other asynchronous events.
958
+
959
+ ```typescript
960
+ import approvalWebhook from '../webhooks/approval.js';
961
+
962
+ brain('Support Ticket Handler')
963
+ .brain('Handle Support Request', {
964
+ system: 'You are a support agent. Escalate complex issues for human review.',
965
+ prompt: ({ ticket }) => `Handle this support ticket: <%= '${ticket.description}' %>`,
966
+ tools: {
967
+ escalateToHuman: {
968
+ description: 'Escalate the ticket to a human reviewer for approval',
969
+ inputSchema: z.object({
970
+ summary: z.string().describe('Summary of the issue'),
971
+ recommendation: z.string().describe('Your recommended action'),
972
+ }),
973
+ execute: async ({ summary, recommendation }, context) => {
974
+ // Send notification to human reviewer (e.g., via Slack, email)
975
+ await notifyReviewer({ summary, recommendation, ticketId: context.state.ticketId });
976
+
977
+ // Return waitFor to pause until the webhook fires
978
+ return {
979
+ waitFor: approvalWebhook(context.state.ticketId),
980
+ };
981
+ },
982
+ },
983
+ resolveTicket: {
984
+ description: 'Mark the ticket as resolved',
985
+ inputSchema: z.object({
986
+ resolution: z.string().describe('How the ticket was resolved'),
987
+ }),
988
+ terminal: true,
989
+ },
990
+ },
991
+ })
992
+ .step('Process Result', ({ state, response }) => ({
993
+ ...state,
994
+ // response contains the webhook data (e.g., { approved: true, reviewerNote: '...' })
995
+ approved: response?.approved,
996
+ reviewerNote: response?.reviewerNote,
997
+ }));
998
+ ```
999
+
1000
+ Key points about tool `waitFor`:
1001
+ - Return `{ waitFor: webhook(...) }` to pause the agent and wait for an external event
1002
+ - The webhook response is available in the next step via the `response` parameter
1003
+ - You can wait for multiple webhooks (first response wins): `{ waitFor: [webhook1(...), webhook2(...)] }`
1004
+ - The `execute` function receives a `context` parameter with access to `state`, `options`, `env`, etc.
1005
+ - Use this pattern for approvals, external API callbacks, or any human-in-the-loop workflow
921
1006
 
922
1007
  ### Agent Output Schema
923
1008
 
@@ -1020,7 +1105,7 @@ The created page object contains:
1020
1105
 
1021
1106
  ## UI Steps
1022
1107
 
1023
- 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.
1108
+ 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 `.wait()` to pause until the form is submitted.
1024
1109
 
1025
1110
  ### Basic UI Step
1026
1111
 
@@ -1043,14 +1128,13 @@ brain('Feedback Collector')
1043
1128
  comments: z.string(),
1044
1129
  }),
1045
1130
  })
1046
- // Notify user and wait for submission
1047
- .step('Notify and Wait', async ({ state, page, slack }) => {
1131
+ // Notify user
1132
+ .step('Notify', async ({ state, page, slack }) => {
1048
1133
  await slack.post('#feedback', `Please fill out: <%= '${page.url}' %>`);
1049
- return {
1050
- state,
1051
- waitFor: [page.webhook],
1052
- };
1134
+ return state;
1053
1135
  })
1136
+ // Wait for form submission
1137
+ .wait('Wait for submission', ({ page }) => page.webhook)
1054
1138
  // Process the form data (comes through response, not page)
1055
1139
  .step('Process Feedback', ({ state, response }) => ({
1056
1140
  ...state,
@@ -1066,8 +1150,8 @@ brain('Feedback Collector')
1066
1150
  2. **AI Generation**: The AI creates a component tree based on the prompt
1067
1151
  3. **Page Object**: Next step receives `page` with `url` and `webhook`
1068
1152
  4. **Notification**: You notify users however you want (Slack, email, etc.)
1069
- 5. **Wait**: Use `waitFor: [page.webhook]` to pause until form submission
1070
- 6. **Form Data**: Step after `waitFor` receives form data via `response`
1153
+ 5. **Wait**: Use `.wait('title', ({ page }) => page.webhook)` to pause until form submission
1154
+ 6. **Form Data**: Step after `.wait()` receives form data via `response`
1071
1155
 
1072
1156
  ### The `page` Object
1073
1157
 
@@ -1140,10 +1224,11 @@ brain('User Onboarding')
1140
1224
  dob: z.string(),
1141
1225
  }),
1142
1226
  })
1143
- .step('Wait for Personal', async ({ state, page, notify }) => {
1227
+ .step('Notify Personal', async ({ state, page, notify }) => {
1144
1228
  await notify(`Step 1: <%= '${page.url}' %>`);
1145
- return { state, waitFor: [page.webhook] };
1229
+ return state;
1146
1230
  })
1231
+ .wait('Wait for Personal', ({ page }) => page.webhook)
1147
1232
  .step('Save Personal', ({ state, response }) => ({
1148
1233
  ...state,
1149
1234
  userData: { ...state.userData, ...response },
@@ -1162,10 +1247,11 @@ brain('User Onboarding')
1162
1247
  contactMethod: z.enum(['email', 'phone', 'sms']),
1163
1248
  }),
1164
1249
  })
1165
- .step('Wait for Preferences', async ({ state, page, notify }) => {
1250
+ .step('Notify Preferences', async ({ state, page, notify }) => {
1166
1251
  await notify(`Step 2: <%= '${page.url}' %>`);
1167
- return { state, waitFor: [page.webhook] };
1252
+ return state;
1168
1253
  })
1254
+ .wait('Wait for Preferences', ({ page }) => page.webhook)
1169
1255
  .step('Complete', ({ state, response }) => ({
1170
1256
  ...state,
1171
1257
  userData: { ...state.userData, preferences: response },
@@ -105,6 +105,26 @@ kill $(lsof -ti:38291)
105
105
  - Always clean up by killing the server process when done
106
106
  - The log file contains timestamped entries with [INFO], [ERROR], and [WARN] prefixes
107
107
 
108
+ ## Guard Clauses
109
+
110
+ Use `.guard()` to short-circuit a brain when a condition isn't met:
111
+
112
+ ```typescript
113
+ brain('approval-example')
114
+ .step('Init', () => ({ needsApproval: true, data: [] }))
115
+ .guard(({ state }) => state.data.length > 0, 'Has data')
116
+ // everything below only runs if guard passes
117
+ .step('Process', ({ state }) => ({ ...state, processed: true }))
118
+ .step('Continue', ({ state }) => ({ ...state, done: true }));
119
+ ```
120
+
121
+ Key rules:
122
+ - Predicate returns `true` to continue, `false` to skip all remaining steps
123
+ - The predicate is synchronous and receives `{ state, options }`
124
+ - State type is unchanged after a guard
125
+ - Optional title as second argument: `.guard(predicate, 'Check condition')`
126
+ - See `/docs/brain-dsl-guide.md` for more details
127
+
108
128
  ## Brain DSL Type Inference
109
129
 
110
130
  The Brain DSL has very strong type inference capabilities. **Important**: You should NOT explicitly specify types on the state object as it flows through steps. The types are automatically inferred from the previous step.
@@ -187,8 +207,8 @@ Most generated brains should not have try-catch blocks. Only use them when the e
187
207
  When you need to collect user input, use the `.ui()` method. The pattern is:
188
208
  1. `.ui()` generates the page
189
209
  2. Next step gets `page.url` and `page.webhook`
190
- 3. Notify users and use `waitFor: [page.webhook]`
191
- 4. Step after `waitFor` gets form data in `response`
210
+ 3. Notify users, then use `.wait()` with `page.webhook`
211
+ 4. Step after `.wait()` gets form data in `response`
192
212
 
193
213
  ```typescript
194
214
  import { z } from 'zod';
@@ -211,11 +231,13 @@ brain('feedback-collector')
211
231
  comments: z.string(),
212
232
  }),
213
233
  })
214
- // Notify and wait for submission
234
+ // Notify users
215
235
  .step('Notify', async ({ state, page, slack }) => {
216
236
  await slack.post('#feedback', `Fill out: <%= '${page.url}' %>`);
217
- return { state, waitFor: [page.webhook] };
237
+ return state;
218
238
  })
239
+ // Wait for form submission
240
+ .wait('Wait for submission', ({ page }) => page.webhook)
219
241
  // Form data comes through response (not page)
220
242
  .step('Process', ({ state, response }) => ({
221
243
  ...state,
@@ -226,8 +248,8 @@ brain('feedback-collector')
226
248
 
227
249
  Key points:
228
250
  - `page.url` - where to send users
229
- - `page.webhook` - use with `waitFor` to pause for submission
230
- - `response` - form data arrives here (in step after `waitFor`)
251
+ - `page.webhook` - use with `.wait()` to pause for submission
252
+ - `response` - form data arrives here (in step after `.wait()`)
231
253
  - You control how users are notified (Slack, email, etc.)
232
254
 
233
255
  See `/docs/brain-dsl-guide.md` for more UI step examples.