@electric-ax/agents 0.1.4 → 0.1.5
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/package.json +3 -2
- package/skills/tutorial/scaffold/entities/.gitkeep +0 -0
- package/skills/tutorial/scaffold/lib/electric-tools.ts +80 -0
- package/skills/tutorial/scaffold/package.json +17 -0
- package/skills/tutorial/scaffold/server.ts +51 -0
- package/skills/tutorial/scaffold/tsconfig.json +15 -0
- package/skills/tutorial.md +282 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Built-in Electric Agents runtimes such as Horton and worker",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
"vitest": "^4.1.0"
|
|
56
56
|
},
|
|
57
57
|
"files": [
|
|
58
|
-
"dist"
|
|
58
|
+
"dist",
|
|
59
|
+
"skills"
|
|
59
60
|
],
|
|
60
61
|
"sideEffects": false,
|
|
61
62
|
"license": "Apache-2.0",
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox'
|
|
2
|
+
import type { AgentTool } from '@electric-ax/agents-runtime'
|
|
3
|
+
|
|
4
|
+
type CreateElectricToolsContext = {
|
|
5
|
+
entityUrl: string
|
|
6
|
+
entityType: string
|
|
7
|
+
args: Readonly<Record<string, unknown>>
|
|
8
|
+
upsertCronSchedule: (opts: {
|
|
9
|
+
id: string
|
|
10
|
+
expression: string
|
|
11
|
+
timezone?: string
|
|
12
|
+
payload?: unknown
|
|
13
|
+
debounceMs?: number
|
|
14
|
+
timeoutMs?: number
|
|
15
|
+
}) => Promise<{ txid: string }>
|
|
16
|
+
upsertFutureSendSchedule: (opts: {
|
|
17
|
+
id: string
|
|
18
|
+
payload: unknown
|
|
19
|
+
targetUrl?: string
|
|
20
|
+
fireAt: string
|
|
21
|
+
from?: string
|
|
22
|
+
messageType?: string
|
|
23
|
+
}) => Promise<{ txid: string }>
|
|
24
|
+
deleteSchedule: (opts: { id: string }) => Promise<{ txid: string }>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createElectricTools(
|
|
28
|
+
ctx: CreateElectricToolsContext
|
|
29
|
+
): Array<AgentTool> {
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
name: `upsert_cron_schedule`,
|
|
33
|
+
label: `Upsert Cron`,
|
|
34
|
+
description: `Create or update a recurring cron wake schedule.`,
|
|
35
|
+
parameters: Type.Object({
|
|
36
|
+
id: Type.String({ description: `Stable schedule identifier` }),
|
|
37
|
+
expression: Type.String({ description: `Cron expression` }),
|
|
38
|
+
timezone: Type.Optional(Type.String({ description: `IANA timezone` })),
|
|
39
|
+
payload: Type.Any({ description: `Instruction for the agent` }),
|
|
40
|
+
}),
|
|
41
|
+
execute: async (_toolCallId, params) => {
|
|
42
|
+
const { id, expression, timezone, payload } = params as any
|
|
43
|
+
const tz = timezone ?? `UTC`
|
|
44
|
+
const { txid } = await ctx.upsertCronSchedule({
|
|
45
|
+
id,
|
|
46
|
+
expression,
|
|
47
|
+
timezone: tz,
|
|
48
|
+
payload,
|
|
49
|
+
})
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{ type: `text` as const, text: `Cron "${id}" set. txid=${txid}` },
|
|
53
|
+
],
|
|
54
|
+
details: { txid },
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: `delete_schedule`,
|
|
60
|
+
label: `Delete Schedule`,
|
|
61
|
+
description: `Delete a schedule by id.`,
|
|
62
|
+
parameters: Type.Object({
|
|
63
|
+
id: Type.String({ description: `Schedule identifier` }),
|
|
64
|
+
}),
|
|
65
|
+
execute: async (_toolCallId, params) => {
|
|
66
|
+
const { id } = params as { id: string }
|
|
67
|
+
const { txid } = await ctx.deleteSchedule({ id })
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: `text` as const,
|
|
72
|
+
text: `Schedule "${id}" deleted. txid=${txid}`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
details: { txid },
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-electric-agents-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "tsx server.ts",
|
|
7
|
+
"dev": "tsx --watch server.ts"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@electric-ax/agents-runtime": "latest",
|
|
11
|
+
"@sinclair/typebox": "^0.34.49"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"tsx": "^4.19.0",
|
|
15
|
+
"typescript": "^5.7.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import {
|
|
5
|
+
createEntityRegistry,
|
|
6
|
+
createRuntimeHandler,
|
|
7
|
+
} from '@electric-ax/agents-runtime'
|
|
8
|
+
import { createElectricTools } from './lib/electric-tools'
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
process.loadEnvFile(path.resolve(here, `.env`))
|
|
13
|
+
} catch {}
|
|
14
|
+
|
|
15
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
16
|
+
console.warn(
|
|
17
|
+
`[app] ANTHROPIC_API_KEY is not set — agent.run() will throw on the first wake.`
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ELECTRIC_AGENTS_URL =
|
|
22
|
+
process.env.ELECTRIC_AGENTS_URL ?? `http://localhost:4437`
|
|
23
|
+
const PORT = Number(process.env.PORT ?? 3000)
|
|
24
|
+
const SERVE_URL = process.env.SERVE_URL ?? `http://localhost:${PORT}`
|
|
25
|
+
|
|
26
|
+
const registry = createEntityRegistry()
|
|
27
|
+
|
|
28
|
+
// Register your entity types here:
|
|
29
|
+
// import { registerMyEntity } from "./entities/my-entity"
|
|
30
|
+
// registerMyEntity(registry)
|
|
31
|
+
|
|
32
|
+
const runtime = createRuntimeHandler({
|
|
33
|
+
baseUrl: ELECTRIC_AGENTS_URL,
|
|
34
|
+
serveEndpoint: `${SERVE_URL}/webhook`,
|
|
35
|
+
registry,
|
|
36
|
+
createElectricTools,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const server = http.createServer(async (req, res) => {
|
|
40
|
+
if (req.url === `/webhook` && req.method === `POST`) {
|
|
41
|
+
await runtime.onEnter(req, res)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
res.writeHead(404)
|
|
45
|
+
res.end()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
server.listen(PORT, async () => {
|
|
49
|
+
await runtime.registerTypes()
|
|
50
|
+
console.log(`App server ready on port ${PORT}`)
|
|
51
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"allowImportingTsExtensions": false,
|
|
11
|
+
"noEmit": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["**/*.ts"],
|
|
14
|
+
"exclude": ["node_modules"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Interactive tutorial — build a perspectives analyzer entity with the manager-worker pattern
|
|
3
|
+
whenToUse: User asks about building entities, wants a tutorial, is new to Electric Agents, or wants to learn multi-agent patterns
|
|
4
|
+
keywords:
|
|
5
|
+
- tutorial
|
|
6
|
+
- getting started
|
|
7
|
+
- learn
|
|
8
|
+
- multi-agent
|
|
9
|
+
- manager-worker
|
|
10
|
+
- perspectives
|
|
11
|
+
- entity
|
|
12
|
+
user-invocable: true
|
|
13
|
+
max: 25000
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Tutorial: Build a Perspectives Analyzer
|
|
17
|
+
|
|
18
|
+
Build a `perspectives` entity that analyzes questions from an optimist and a critic using the manager-worker pattern. Use the exact code below — do not invent different code.
|
|
19
|
+
|
|
20
|
+
## Core Concepts
|
|
21
|
+
|
|
22
|
+
### What is Electric Agents?
|
|
23
|
+
|
|
24
|
+
Electric Agents is a runtime for spawning and orchestrating collaborative AI agents on serverless compute.
|
|
25
|
+
|
|
26
|
+
The core idea: agent sessions and communication are backed by **durable streams**. Each agent is an **entity** with its own stream of events. All agent activity — runs, tool calls, text output — is persisted to this stream. This means agents can scale to zero, survive restarts, and maintain full session history.
|
|
27
|
+
|
|
28
|
+
**Why this matters for multi-agent systems**: Because everything is durable and observable, agents can spawn children, wait for results (even across restarts), observe each other's state changes, and coordinate through structured primitives — all without worrying about losing state.
|
|
29
|
+
|
|
30
|
+
### Entities
|
|
31
|
+
|
|
32
|
+
An entity is a durable, addressable unit of computation. Each entity has:
|
|
33
|
+
|
|
34
|
+
- A **type** (e.g., `assistant`, `worker`, `research-team`) — defined once, instantiated many times
|
|
35
|
+
- A **URL** (e.g., `/research-team/my-team`) — its unique address
|
|
36
|
+
- A **handler** — the function that runs each time the entity wakes up
|
|
37
|
+
- **State** — persistent collections that survive across wakes
|
|
38
|
+
|
|
39
|
+
You define entity types with `registry.define()` and create instances by spawning them.
|
|
40
|
+
|
|
41
|
+
### Handlers and Wakes
|
|
42
|
+
|
|
43
|
+
An entity's handler runs in response to **wake events**:
|
|
44
|
+
|
|
45
|
+
- A message arrives in the entity's inbox
|
|
46
|
+
- A child entity finishes its run
|
|
47
|
+
- A cron schedule fires
|
|
48
|
+
- A state change in an observed entity
|
|
49
|
+
|
|
50
|
+
The handler is **not** a long-running process. It wakes, does its work (usually running an LLM agent loop), and goes back to sleep.
|
|
51
|
+
|
|
52
|
+
### The Agent Loop
|
|
53
|
+
|
|
54
|
+
`ctx.useAgent()` configures an LLM agent and `ctx.agent.run()` starts it. The agent receives conversation history, calls tools as needed, and generates a response — all persisted to the entity's durable stream.
|
|
55
|
+
|
|
56
|
+
### Spawning Children
|
|
57
|
+
|
|
58
|
+
Any entity can spawn child entities. When a child finishes (and the parent registered `wake: "runFinished"`), the parent's handler runs again. The wake event includes the child's response and the status of sibling children.
|
|
59
|
+
|
|
60
|
+
### The Worker Entity
|
|
61
|
+
|
|
62
|
+
The built-in `worker` type is a generic agent substrate. You configure it at spawn time with a `systemPrompt` and `tools` array (at least one tool required).
|
|
63
|
+
|
|
64
|
+
### State Collections
|
|
65
|
+
|
|
66
|
+
Entities can declare persistent state collections that survive across wakes, allowing coordination patterns like tracking which children have completed.
|
|
67
|
+
|
|
68
|
+
## Before starting
|
|
69
|
+
|
|
70
|
+
Read `server.ts` in the working directory:
|
|
71
|
+
|
|
72
|
+
- **Has `registerPerspectives`**: resume from where they left off (read `entities/perspectives.ts` to determine the step)
|
|
73
|
+
- **Has `server.ts` but no perspectives**: go to Step 1
|
|
74
|
+
- **No `server.ts`**: scaffold the project — spawn a worker (`tools: ["bash"]`, systemPrompt: `"Set up an Electric Agents app project."`, initialMessage: `"mkdir -p TARGET/lib TARGET/entities && cp SKILL_DIR/scaffold/* TARGET/ && cp SKILL_DIR/scaffold/lib/* TARGET/lib/ && cp SKILL_DIR/scaffold/.env TARGET/ && cd TARGET && pnpm install && pnpm dev &"` — replace SKILL_DIR and TARGET). Then proceed to Step 1 while the worker runs. Wait for the worker to finish before writing files.
|
|
75
|
+
|
|
76
|
+
## Steps
|
|
77
|
+
|
|
78
|
+
**Step 1 — Welcome + first entity.** In one message: introduce Electric Agents using the Core Concepts above, preview the perspectives analyzer, and show the Step 1 code. Ask to write.
|
|
79
|
+
|
|
80
|
+
**Step 2 — After confirmation:** write `entities/perspectives.ts` with Step 1 code. Give CLI commands. Explain spawning briefly, show Step 2 code (adds one worker). Ask to write.
|
|
81
|
+
|
|
82
|
+
**Step 3 — After confirmation:** write the updated file. Give CLI commands. Explain coordination, show Step 3 code (adds critic + state). Ask to write.
|
|
83
|
+
|
|
84
|
+
**Step 4 — After confirmation:** write the updated file. Give CLI commands.
|
|
85
|
+
|
|
86
|
+
**Step 5 — Wire up.** Read `server.ts`, show the import change, ask to write, update it.
|
|
87
|
+
|
|
88
|
+
**Step 6 — Recap.**
|
|
89
|
+
|
|
90
|
+
## Rules
|
|
91
|
+
|
|
92
|
+
- Use the exact code below. Write files with your write tool.
|
|
93
|
+
- `server.ts` is at the working directory root. Entity files go in `entities/`.
|
|
94
|
+
- Worker spawn args MUST include `tools` array (e.g. `tools: ["bash", "read"]`).
|
|
95
|
+
- Prefer showing what changed between steps rather than repeating the entire file.
|
|
96
|
+
- Use `edit` tool for small changes (like updating server.ts). Use `write` for full entity file updates.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
# Code
|
|
101
|
+
|
|
102
|
+
## Step 1: Minimal entity
|
|
103
|
+
|
|
104
|
+
`entities/perspectives.ts`:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import type { EntityRegistry } from '@electric-ax/agents-runtime'
|
|
108
|
+
|
|
109
|
+
export function registerPerspectives(registry: EntityRegistry) {
|
|
110
|
+
registry.define('perspectives', {
|
|
111
|
+
description: 'Analyzes questions from multiple perspectives',
|
|
112
|
+
async handler(ctx) {
|
|
113
|
+
ctx.useAgent({
|
|
114
|
+
systemPrompt:
|
|
115
|
+
'You are a balanced analyst. When given a question, provide a thoughtful analysis.',
|
|
116
|
+
model: 'claude-sonnet-4-6',
|
|
117
|
+
tools: [...ctx.electricTools],
|
|
118
|
+
})
|
|
119
|
+
await ctx.agent.run()
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`server.ts` additions:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { registerPerspectives } from './entities/perspectives'
|
|
129
|
+
registerPerspectives(registry)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Test: `pnpm electric-agents spawn /perspectives/test-1 && pnpm electric-agents send /perspectives/test-1 "Is remote work better than office work?" && pnpm electric-agents observe /perspectives/test-1`
|
|
133
|
+
|
|
134
|
+
## Step 2: One worker
|
|
135
|
+
|
|
136
|
+
Full `entities/perspectives.ts`:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import type {
|
|
140
|
+
EntityRegistry,
|
|
141
|
+
HandlerContext,
|
|
142
|
+
} from '@electric-ax/agents-runtime'
|
|
143
|
+
import { Type } from '@sinclair/typebox'
|
|
144
|
+
|
|
145
|
+
function createAnalyzeTool(ctx: HandlerContext) {
|
|
146
|
+
return {
|
|
147
|
+
name: 'analyze_question',
|
|
148
|
+
label: 'Analyze Question',
|
|
149
|
+
description: 'Spawns an optimist worker to analyze a question.',
|
|
150
|
+
parameters: Type.Object({
|
|
151
|
+
question: Type.String({ description: 'The question to analyze' }),
|
|
152
|
+
}),
|
|
153
|
+
execute: async (_toolCallId: string, params: unknown) => {
|
|
154
|
+
const { question } = params as { question: string }
|
|
155
|
+
const parentId = ctx.entityUrl.split('/').pop()
|
|
156
|
+
await ctx.spawn(
|
|
157
|
+
'worker',
|
|
158
|
+
`${parentId}-optimist`,
|
|
159
|
+
{
|
|
160
|
+
systemPrompt:
|
|
161
|
+
'You are an optimist analyst. Provide an enthusiastic, positive analysis focusing on opportunities and benefits.',
|
|
162
|
+
tools: ['bash', 'read'],
|
|
163
|
+
},
|
|
164
|
+
{ initialMessage: question, wake: 'runFinished' }
|
|
165
|
+
)
|
|
166
|
+
return {
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: 'text' as const,
|
|
170
|
+
text: "Spawned optimist worker. You'll be woken when it finishes.",
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
details: {},
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function registerPerspectives(registry: EntityRegistry) {
|
|
180
|
+
registry.define('perspectives', {
|
|
181
|
+
description: 'Analyzes questions from multiple perspectives',
|
|
182
|
+
async handler(ctx) {
|
|
183
|
+
ctx.useAgent({
|
|
184
|
+
systemPrompt: `You are a balanced analyst.\n\nWhen given a question:\n1. Call analyze_question with the question.\n2. End your turn. You'll be woken when the worker finishes.\n3. When woken, finished_child.response contains the analysis.\n4. Present it to the user.`,
|
|
185
|
+
model: 'claude-sonnet-4-6',
|
|
186
|
+
tools: [...ctx.electricTools, createAnalyzeTool(ctx)],
|
|
187
|
+
})
|
|
188
|
+
await ctx.agent.run()
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Test: `pnpm electric-agents spawn /perspectives/test-2 && pnpm electric-agents send /perspectives/test-2 "Is remote work better than office work?" && pnpm electric-agents observe /perspectives/test-2`
|
|
195
|
+
|
|
196
|
+
## Step 3: Two workers + state
|
|
197
|
+
|
|
198
|
+
Full `entities/perspectives.ts`:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import type {
|
|
202
|
+
EntityRegistry,
|
|
203
|
+
HandlerContext,
|
|
204
|
+
} from '@electric-ax/agents-runtime'
|
|
205
|
+
import { Type } from '@sinclair/typebox'
|
|
206
|
+
|
|
207
|
+
const PERSPECTIVES = [
|
|
208
|
+
{
|
|
209
|
+
id: 'optimist',
|
|
210
|
+
systemPrompt:
|
|
211
|
+
'You are an optimist analyst. Provide an enthusiastic, positive analysis focusing on opportunities and benefits.',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'critic',
|
|
215
|
+
systemPrompt:
|
|
216
|
+
'You are a critical analyst. Provide a sharp analysis focusing on risks, downsides, and challenges.',
|
|
217
|
+
},
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
function createAnalyzeTool(ctx: HandlerContext) {
|
|
221
|
+
return {
|
|
222
|
+
name: 'analyze_question',
|
|
223
|
+
label: 'Analyze Question',
|
|
224
|
+
description: 'Spawns optimist and critic workers to analyze a question.',
|
|
225
|
+
parameters: Type.Object({
|
|
226
|
+
question: Type.String({ description: 'The question to analyze' }),
|
|
227
|
+
}),
|
|
228
|
+
execute: async (_toolCallId: string, params: unknown) => {
|
|
229
|
+
const { question } = params as { question: string }
|
|
230
|
+
const parentId = ctx.entityUrl.split('/').pop()
|
|
231
|
+
for (const p of PERSPECTIVES) {
|
|
232
|
+
const childId = `${parentId}-${p.id}`
|
|
233
|
+
await ctx.spawn(
|
|
234
|
+
'worker',
|
|
235
|
+
childId,
|
|
236
|
+
{ systemPrompt: p.systemPrompt, tools: ['bash', 'read'] },
|
|
237
|
+
{ initialMessage: question, wake: 'runFinished' }
|
|
238
|
+
)
|
|
239
|
+
ctx.db.actions.children_insert({
|
|
240
|
+
row: { key: p.id, url: `/worker/${childId}` },
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: 'text' as const,
|
|
247
|
+
text: 'Spawned optimist and critic workers.',
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
details: {},
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function registerPerspectives(registry: EntityRegistry) {
|
|
257
|
+
registry.define('perspectives', {
|
|
258
|
+
description:
|
|
259
|
+
'Analyzes questions from two perspectives: optimist and critic',
|
|
260
|
+
state: { children: { primaryKey: 'key' } },
|
|
261
|
+
async handler(ctx) {
|
|
262
|
+
ctx.useAgent({
|
|
263
|
+
systemPrompt: `You are a balanced analyst.\n\n1. Call analyze_question with the question.\n2. End your turn. You'll be woken as each worker finishes.\n3. Each wake includes finished_child.response and other_children.\n4. Once both are done, synthesize a balanced response.`,
|
|
264
|
+
model: 'claude-sonnet-4-6',
|
|
265
|
+
tools: [...ctx.electricTools, createAnalyzeTool(ctx)],
|
|
266
|
+
})
|
|
267
|
+
await ctx.agent.run()
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Test: `pnpm electric-agents spawn /perspectives/test-3 && pnpm electric-agents send /perspectives/test-3 "Is remote work better than office work?" && pnpm electric-agents observe /perspectives/test-3`
|
|
274
|
+
|
|
275
|
+
## What you learned
|
|
276
|
+
|
|
277
|
+
- `registry.define()` — entity types with description, state, handler
|
|
278
|
+
- `ctx.useAgent()` + `ctx.agent.run()` — configure and run an LLM agent
|
|
279
|
+
- `ctx.spawn()` — spawn child entities with custom prompts
|
|
280
|
+
- Wake events — parents wake when children finish
|
|
281
|
+
- State collections — track data across wakes
|
|
282
|
+
- The worker pattern — one generic type, many roles
|