@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.
|
|
57
|
-
let cloudflareVersion = '^0.0.
|
|
58
|
-
let clientVercelVersion = '^0.0.
|
|
59
|
-
let genUIComponentsVersion = '^0.0.
|
|
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
package/template/CLAUDE.md
CHANGED
|
@@ -101,7 +101,7 @@ export default approvalWebhook;
|
|
|
101
101
|
|
|
102
102
|
### Using Webhooks in Brains
|
|
103
103
|
|
|
104
|
-
Import the webhook and use
|
|
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
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
-
- `
|
|
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 `
|
|
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
|
|
1047
|
-
.step('Notify
|
|
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
|
|
1070
|
-
6. **Form Data**: Step after `
|
|
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('
|
|
1227
|
+
.step('Notify Personal', async ({ state, page, notify }) => {
|
|
1144
1228
|
await notify(`Step 1: <%= '${page.url}' %>`);
|
|
1145
|
-
return
|
|
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('
|
|
1250
|
+
.step('Notify Preferences', async ({ state, page, notify }) => {
|
|
1166
1251
|
await notify(`Step 2: <%= '${page.url}' %>`);
|
|
1167
|
-
return
|
|
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
|
|
191
|
-
4. Step after `
|
|
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
|
|
234
|
+
// Notify users
|
|
215
235
|
.step('Notify', async ({ state, page, slack }) => {
|
|
216
236
|
await slack.post('#feedback', `Fill out: <%= '${page.url}' %>`);
|
|
217
|
-
return
|
|
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 `
|
|
230
|
-
- `response` - form data arrives here (in step after `
|
|
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.
|