@electric-ax/agents 0.2.3 → 0.2.4
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/dist/entrypoint.js +40 -12
- package/dist/index.cjs +40 -12
- package/dist/index.js +40 -12
- package/docs/entities/agents/coder.md +99 -0
- package/docs/entities/agents/horton.md +16 -13
- package/docs/entities/agents/worker.md +18 -18
- package/docs/entities/patterns/blackboard.md +6 -6
- package/docs/entities/patterns/dispatcher.md +1 -1
- package/docs/entities/patterns/manager-worker.md +1 -1
- package/docs/entities/patterns/map-reduce.md +1 -1
- package/docs/entities/patterns/pipeline.md +1 -1
- package/docs/entities/patterns/reactive-observers.md +2 -2
- package/docs/examples/playground.md +42 -26
- package/docs/index.md +23 -23
- package/docs/quickstart.md +13 -13
- package/docs/reference/agent-config.md +20 -12
- package/docs/reference/agent-tool.md +1 -1
- package/docs/reference/built-in-collections.md +21 -21
- package/docs/reference/cli.md +39 -30
- package/docs/reference/entity-definition.md +9 -9
- package/docs/reference/entity-handle.md +2 -2
- package/docs/reference/entity-registry.md +1 -1
- package/docs/reference/handler-context.md +69 -18
- package/docs/reference/runtime-handler.md +25 -23
- package/docs/reference/shared-state-handle.md +7 -7
- package/docs/reference/state-collection-proxy.md +1 -1
- package/docs/reference/wake-event.md +23 -23
- package/docs/usage/app-setup.md +24 -23
- package/docs/usage/clients-and-react.md +44 -36
- package/docs/usage/configuring-the-agent.md +25 -19
- package/docs/usage/context-composition.md +12 -12
- package/docs/usage/defining-entities.md +36 -36
- package/docs/usage/defining-tools.md +45 -45
- package/docs/usage/embedded-builtins.md +48 -47
- package/docs/usage/managing-state.md +12 -12
- package/docs/usage/overview.md +52 -45
- package/docs/usage/programmatic-runtime-client.md +50 -47
- package/docs/usage/shared-state.md +32 -32
- package/docs/usage/spawning-and-coordinating.md +9 -9
- package/docs/usage/testing.md +14 -14
- package/docs/usage/waking-entities.md +13 -13
- package/docs/usage/writing-handlers.md +57 -26
- package/package.json +4 -1
- package/scripts/sync-docs.mjs +42 -0
- package/docs/examples/mega-draw.md +0 -106
package/docs/usage/testing.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Testing
|
|
3
|
-
titleTemplate:
|
|
3
|
+
titleTemplate: "... - Electric Agents"
|
|
4
4
|
description: >-
|
|
5
5
|
Test entity handlers with testResponses for LLM mocking, plus unit and integration testing patterns.
|
|
6
6
|
outline: [2, 3]
|
|
@@ -14,10 +14,10 @@ Test agent handlers without calling the LLM by providing canned responses:
|
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
ctx.useAgent({
|
|
17
|
-
systemPrompt:
|
|
18
|
-
model:
|
|
17
|
+
systemPrompt: "...",
|
|
18
|
+
model: "claude-sonnet-4-5-20250929",
|
|
19
19
|
tools: [...ctx.electricTools],
|
|
20
|
-
testResponses: [
|
|
20
|
+
testResponses: ["Hello! How can I help?"],
|
|
21
21
|
})
|
|
22
22
|
await ctx.agent.run()
|
|
23
23
|
```
|
|
@@ -31,9 +31,9 @@ For dynamic test responses, provide a function instead of an array:
|
|
|
31
31
|
```ts
|
|
32
32
|
testResponses: async (message, bridge) => {
|
|
33
33
|
bridge.onTextStart()
|
|
34
|
-
bridge.onTextDelta(
|
|
34
|
+
bridge.onTextDelta("Test response")
|
|
35
35
|
bridge.onTextEnd()
|
|
36
|
-
return
|
|
36
|
+
return "Test response"
|
|
37
37
|
}
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -46,28 +46,28 @@ The runtime wraps your `TestResponseFn` with `bridge.onRunStart()` / `bridge.onR
|
|
|
46
46
|
## Unit testing entity registration
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
|
-
import { createEntityRegistry } from
|
|
49
|
+
import { createEntityRegistry } from "@electric-ax/agents-runtime"
|
|
50
50
|
|
|
51
51
|
const registry = createEntityRegistry()
|
|
52
52
|
registerAssistant(registry)
|
|
53
53
|
|
|
54
|
-
test(
|
|
55
|
-
const entry = registry.get(
|
|
54
|
+
test("registers assistant", () => {
|
|
55
|
+
const entry = registry.get("assistant")
|
|
56
56
|
expect(entry).toBeDefined()
|
|
57
|
-
expect(entry!.definition.handler).toBeTypeOf(
|
|
57
|
+
expect(entry!.definition.handler).toBeTypeOf("function")
|
|
58
58
|
})
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
## Unit testing runtime creation
|
|
62
62
|
|
|
63
63
|
```ts
|
|
64
|
-
test(
|
|
64
|
+
test("creates runtime with types", () => {
|
|
65
65
|
const runtime = createRuntimeHandler({
|
|
66
|
-
baseUrl:
|
|
67
|
-
serveEndpoint:
|
|
66
|
+
baseUrl: "http://localhost:4437",
|
|
67
|
+
serveEndpoint: "http://localhost:3000/webhook",
|
|
68
68
|
registry,
|
|
69
69
|
})
|
|
70
|
-
expect(runtime.typeNames).toContain(
|
|
70
|
+
expect(runtime.typeNames).toContain("assistant")
|
|
71
71
|
})
|
|
72
72
|
```
|
|
73
73
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Waking entities
|
|
3
|
-
titleTemplate:
|
|
3
|
+
titleTemplate: "... - Electric Agents"
|
|
4
4
|
description: >-
|
|
5
5
|
How entity handlers get invoked - the triggers that produce wakes, how wake config threads through spawn/observe/observe(db(...)), and how to read a WakeEvent in a handler.
|
|
6
6
|
outline: [2, 3]
|
|
@@ -23,7 +23,7 @@ external event ─► wake entry (persisted) ─► handler invocation ─
|
|
|
23
23
|
3. **Handler is invoked.** The runtime picks up the wake, loads the entity's state, and calls your handler with a `WakeEvent` describing what triggered this invocation.
|
|
24
24
|
4. **Handler runs.** You read `ctx.events`, inspect `wake`, configure the agent, emit new events. When the handler returns (or calls `ctx.sleep()`), the entity goes idle until the next wake.
|
|
25
25
|
|
|
26
|
-
This means handlers are re-entrant: the same handler function is called fresh on every wake. Use `ctx.
|
|
26
|
+
This means handlers are re-entrant: the same handler function is called fresh on every wake. Use `ctx.db.actions` / `ctx.db.collections` to carry state across wakes, and make one-time writes idempotent by checking existing state. `ctx.firstWake` is for the initial setup pass while no manifest entries exist.
|
|
27
27
|
|
|
28
28
|
## What produces a wake
|
|
29
29
|
|
|
@@ -34,7 +34,7 @@ There are five things that can wake an entity:
|
|
|
34
34
|
Any external `/send` (via the CLI, HTTP, or another entity's `ctx.send()`) appends a `message_received` event to the entity's stream, which wakes the handler:
|
|
35
35
|
|
|
36
36
|
```ts
|
|
37
|
-
ctx.send(
|
|
37
|
+
ctx.send("/assistant/peer", { text: "hello" })
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
The receiving handler sees `wake.type === "message_received"` and finds the payload on `wake.payload`.
|
|
@@ -45,12 +45,12 @@ Pass `wake` when spawning a child to control when the parent wakes:
|
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
47
|
const child = await ctx.spawn(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{ systemPrompt:
|
|
48
|
+
"worker",
|
|
49
|
+
"analysis-1",
|
|
50
|
+
{ systemPrompt: "Analyse this input.", tools: ["read"] },
|
|
51
51
|
{
|
|
52
|
-
initialMessage:
|
|
53
|
-
wake: { on:
|
|
52
|
+
initialMessage: "begin",
|
|
53
|
+
wake: { on: "runFinished", includeResponse: true },
|
|
54
54
|
}
|
|
55
55
|
)
|
|
56
56
|
```
|
|
@@ -62,10 +62,10 @@ See the full catalog of `Wake` values in [WakeEvent](../reference/wake-event#wak
|
|
|
62
62
|
`ctx.observe()` subscribes to another entity's stream without spawning it. Pair it with a `wake` option to re-invoke this handler when the observed stream changes:
|
|
63
63
|
|
|
64
64
|
```ts
|
|
65
|
-
import { entity } from
|
|
65
|
+
import { entity } from "@electric-ax/agents-runtime"
|
|
66
66
|
|
|
67
67
|
await ctx.observe(entity(someEntityUrl), {
|
|
68
|
-
wake: { on:
|
|
68
|
+
wake: { on: "change", collections: ["status"], debounceMs: 250 },
|
|
69
69
|
})
|
|
70
70
|
```
|
|
71
71
|
|
|
@@ -76,8 +76,8 @@ The `entity()` helper wraps a raw URL string into the correct observe target typ
|
|
|
76
76
|
`observe(db(...))` connects to a shared-state stream and, with `wake`, re-wakes the connecting entity when its collections change:
|
|
77
77
|
|
|
78
78
|
```ts
|
|
79
|
-
await ctx.observe(db(
|
|
80
|
-
wake: { on:
|
|
79
|
+
await ctx.observe(db("board-1", schema), {
|
|
80
|
+
wake: { on: "change", collections: ["findings"] },
|
|
81
81
|
})
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -123,7 +123,7 @@ Multiple external events that arrive while an entity is busy (or between acks) a
|
|
|
123
123
|
|
|
124
124
|
- A wake covers a contiguous range of offsets in the source stream (`wake.fromOffset`..`wake.toOffset`).
|
|
125
125
|
- `wake.eventCount` tells you how many new events this wake represents.
|
|
126
|
-
- Handlers must be safe to re-run with the same input — at-least-once delivery.
|
|
126
|
+
- Handlers must be safe to re-run with the same input — at-least-once delivery. Prefer idempotent writes to collections over side effects on each wake.
|
|
127
127
|
|
|
128
128
|
If you need to deduplicate explicitly, key your writes by something stable (the child's entity URL, the message's producer/epoch/seq headers, etc.) and let the collection's primary key do the dedup.
|
|
129
129
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Writing handlers
|
|
3
|
-
titleTemplate:
|
|
3
|
+
titleTemplate: "... - Electric Agents"
|
|
4
4
|
description: >-
|
|
5
5
|
Implement entity handlers using HandlerContext and WakeEvent, with patterns for first wake, messaging, and tool use.
|
|
6
6
|
outline: [2, 3]
|
|
@@ -57,11 +57,16 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
|
|
|
57
57
|
id: string,
|
|
58
58
|
schema: T
|
|
59
59
|
) => SharedStateHandle<T>
|
|
60
|
+
useCodingAgent: (
|
|
61
|
+
sessionId: string,
|
|
62
|
+
opts: UseCodingAgentOptions
|
|
63
|
+
) => Promise<CodingSessionHandle>
|
|
60
64
|
send: (
|
|
61
65
|
entityUrl: string,
|
|
62
66
|
payload: unknown,
|
|
63
67
|
opts?: { type?: string; afterMs?: number }
|
|
64
68
|
) => void
|
|
69
|
+
recordRun: () => RunHandle
|
|
65
70
|
setTag: (key: string, value: string) => Promise<void>
|
|
66
71
|
removeTag: (key: string) => Promise<void>
|
|
67
72
|
sleep: () => void
|
|
@@ -72,7 +77,7 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
|
|
|
72
77
|
|
|
73
78
|
| Property | Description |
|
|
74
79
|
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
|
-
| `firstWake` | `true`
|
|
80
|
+
| `firstWake` | `true` during the initial setup pass while the entity has no persisted manifest entries. Use state checks for one-time plain state initialization. |
|
|
76
81
|
| `tags` | Entity tags -- key/value metadata associated with this entity. |
|
|
77
82
|
| `entityUrl` | The entity's URL path, e.g. `"/assistant/my-chat"`. |
|
|
78
83
|
| `entityType` | The registered type name, e.g. `"assistant"`. |
|
|
@@ -81,7 +86,7 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
|
|
|
81
86
|
| `state` | Proxy object keyed by collection name. Each property is a [`StateCollectionProxy`](../reference/state-collection-proxy). |
|
|
82
87
|
| `events` | Change events that triggered this wake. |
|
|
83
88
|
| `actions` | Custom non-CRUD action functions from the entity definition's `actions` factory. |
|
|
84
|
-
| `electricTools`
|
|
89
|
+
| `electricTools` | Host-provided runtime-level tools to pass to `useAgent` when needed. May be empty. |
|
|
85
90
|
| `useAgent` | Configures the LLM agent. Returns an `AgentHandle`. See [Configuring the agent](./configuring-the-agent). |
|
|
86
91
|
| `useContext` | Declares context sources with token budgets and cache tiers. See [Context composition](./context-composition). |
|
|
87
92
|
| `timelineMessages` | Projects the entity timeline into LLM messages. See [Context composition](./context-composition#timelinemessages). |
|
|
@@ -93,7 +98,9 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
|
|
|
93
98
|
| `spawn` | Creates a child entity. See [Spawning and coordinating](./spawning-and-coordinating). |
|
|
94
99
|
| `observe` | Connects to another entity's stream or shared db. See [Reactive observers](../entities/patterns/reactive-observers) and [Shared state](./shared-state). |
|
|
95
100
|
| `mkdb` | Creates a new shared state stream. See [Shared state](./shared-state). |
|
|
101
|
+
| `useCodingAgent` | Spawns or attaches to a built-in `coder` entity backed by Claude Code or Codex. |
|
|
96
102
|
| `send` | Sends a message to another entity's inbox. Supports delayed delivery via `afterMs`. |
|
|
103
|
+
| `recordRun` | Records non-LLM work in the built-in `runs` collection so `runFinished` observers are woken. |
|
|
97
104
|
| `setTag` | Sets a tag on this entity. |
|
|
98
105
|
| `removeTag` | Removes a tag from this entity. |
|
|
99
106
|
| `sleep` | Returns the entity to idle without re-waking. |
|
|
@@ -115,36 +122,36 @@ type WakeEvent = {
|
|
|
115
122
|
}
|
|
116
123
|
```
|
|
117
124
|
|
|
118
|
-
| Field | Description
|
|
119
|
-
| ------------ |
|
|
120
|
-
| `source` | The stream or entity that caused the wake.
|
|
125
|
+
| Field | Description |
|
|
126
|
+
| ------------ | -------------------------------------------------------------- |
|
|
127
|
+
| `source` | The stream or entity that caused the wake. |
|
|
121
128
|
| `type` | The wake type: `"message_received"` for inbox messages or `"wake"` for child completion, observed changes, cron, and timeouts. |
|
|
122
|
-
| `fromOffset` | Start offset of the events that triggered this wake.
|
|
123
|
-
| `toOffset` | End offset of the events that triggered this wake.
|
|
124
|
-
| `eventCount` | Number of new events since last wake.
|
|
125
|
-
| `payload` | Optional payload from the trigger event.
|
|
126
|
-
| `summary` | Optional human-readable summary.
|
|
127
|
-
| `fullRef` | Optional full reference string for the trigger.
|
|
129
|
+
| `fromOffset` | Start offset of the events that triggered this wake. |
|
|
130
|
+
| `toOffset` | End offset of the events that triggered this wake. |
|
|
131
|
+
| `eventCount` | Number of new events since last wake. |
|
|
132
|
+
| `payload` | Optional payload from the trigger event. |
|
|
133
|
+
| `summary` | Optional human-readable summary. |
|
|
134
|
+
| `fullRef` | Optional full reference string for the trigger. |
|
|
128
135
|
|
|
129
136
|
## Typical handler pattern
|
|
130
137
|
|
|
131
|
-
Most handlers follow the same structure: initialize state
|
|
138
|
+
Most LLM handlers follow the same structure: initialize missing state idempotently, configure the agent, run the agent.
|
|
132
139
|
|
|
133
140
|
```ts
|
|
134
|
-
registry.define(
|
|
135
|
-
description:
|
|
141
|
+
registry.define("assistant", {
|
|
142
|
+
description: "A general-purpose assistant",
|
|
136
143
|
state: {
|
|
137
|
-
status: { primaryKey:
|
|
144
|
+
status: { primaryKey: "key" },
|
|
138
145
|
},
|
|
139
146
|
|
|
140
147
|
async handler(ctx) {
|
|
141
|
-
if (ctx.
|
|
142
|
-
ctx.db.actions.status_insert({ row: { key:
|
|
148
|
+
if (!ctx.db.collections.status.get("current")) {
|
|
149
|
+
ctx.db.actions.status_insert({ row: { key: "current", value: "idle" } })
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
ctx.useAgent({
|
|
146
|
-
systemPrompt:
|
|
147
|
-
model:
|
|
153
|
+
systemPrompt: "You are a helpful assistant.",
|
|
154
|
+
model: "claude-sonnet-4-5-20250929",
|
|
148
155
|
tools: [...ctx.electricTools],
|
|
149
156
|
})
|
|
150
157
|
await ctx.agent.run()
|
|
@@ -163,25 +170,31 @@ interface AgentConfig {
|
|
|
163
170
|
provider?: KnownProvider
|
|
164
171
|
tools: AgentTool[]
|
|
165
172
|
streamFn?: StreamFn
|
|
173
|
+
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined
|
|
174
|
+
onPayload?: SimpleStreamOptions["onPayload"]
|
|
166
175
|
testResponses?: string[] | TestResponseFn
|
|
167
176
|
}
|
|
168
177
|
```
|
|
169
178
|
|
|
170
|
-
## firstWake
|
|
179
|
+
## firstWake and initialization
|
|
171
180
|
|
|
172
|
-
`ctx.firstWake` is `true`
|
|
181
|
+
`ctx.firstWake` is `true` during the initial setup pass while the entity has no persisted manifest entries. It is useful for setup that creates manifest-backed resources such as `ctx.spawn()`, `ctx.observe()`, `ctx.mkdb()`, context entries, or schedules.
|
|
182
|
+
|
|
183
|
+
For plain state rows, prefer checking the collection itself so initialization stays idempotent even for entities that do not create manifest entries:
|
|
173
184
|
|
|
174
185
|
```ts
|
|
175
186
|
async handler(ctx) {
|
|
176
|
-
if (ctx.
|
|
187
|
+
if (!ctx.db.collections.status.get("current")) {
|
|
177
188
|
ctx.db.actions.status_insert({ row: { key: 'current', value: 'idle' } })
|
|
189
|
+
}
|
|
190
|
+
if (!ctx.db.collections.counters.get("runs")) {
|
|
178
191
|
ctx.db.actions.counters_insert({ row: { key: 'runs', value: 0 } })
|
|
179
192
|
}
|
|
180
193
|
// ...
|
|
181
194
|
}
|
|
182
195
|
```
|
|
183
196
|
|
|
184
|
-
|
|
197
|
+
After an entity persists manifest entries, subsequent wakes set `firstWake` to `false`.
|
|
185
198
|
|
|
186
199
|
## sleep
|
|
187
200
|
|
|
@@ -200,6 +213,24 @@ async handler(ctx, wake) {
|
|
|
200
213
|
}
|
|
201
214
|
```
|
|
202
215
|
|
|
216
|
+
## recordRun
|
|
217
|
+
|
|
218
|
+
Call `ctx.recordRun()` when a handler does work without `ctx.agent.run()` but still needs to publish run lifecycle events. This is how non-LLM entities can wake parents observing them with `wake: "runFinished"`.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
async handler(ctx) {
|
|
222
|
+
const run = ctx.recordRun()
|
|
223
|
+
try {
|
|
224
|
+
const result = await runExternalJob()
|
|
225
|
+
run.attachResponse(result.summary)
|
|
226
|
+
run.end({ status: "completed" })
|
|
227
|
+
} catch (error) {
|
|
228
|
+
run.end({ status: "failed", finishReason: "error" })
|
|
229
|
+
throw error
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
203
234
|
## Using spawn args
|
|
204
235
|
|
|
205
236
|
Arguments passed at spawn time are available as `ctx.args`. This is how you parameterize entity behavior:
|
|
@@ -260,8 +291,8 @@ async handler(ctx) {
|
|
|
260
291
|
Use `ctx.send()` to deliver a message to another entity's inbox:
|
|
261
292
|
|
|
262
293
|
```ts
|
|
263
|
-
ctx.send(
|
|
264
|
-
ctx.send(
|
|
294
|
+
ctx.send("/worker/task-1", { action: "process", data: payload })
|
|
295
|
+
ctx.send("/worker/task-1", payload, { type: "custom_type" })
|
|
265
296
|
```
|
|
266
297
|
|
|
267
298
|
The target entity will be woken to process the message.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Built-in Electric Agents runtimes such as Horton and worker",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"files": [
|
|
54
54
|
"dist",
|
|
55
55
|
"docs",
|
|
56
|
+
"scripts",
|
|
56
57
|
"skills"
|
|
57
58
|
],
|
|
58
59
|
"sideEffects": false,
|
|
@@ -60,6 +61,8 @@
|
|
|
60
61
|
"scripts": {
|
|
61
62
|
"build": "tsdown",
|
|
62
63
|
"dev": "tsdown --watch",
|
|
64
|
+
"docs:sync": "node scripts/sync-docs.mjs",
|
|
65
|
+
"docs:clean": "node scripts/sync-docs.mjs --clean",
|
|
63
66
|
"test": "vitest run",
|
|
64
67
|
"coverage": "pnpm exec vitest --coverage",
|
|
65
68
|
"typecheck": "tsc --noEmit",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const packageRoot = path.resolve(
|
|
6
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
7
|
+
`..`
|
|
8
|
+
)
|
|
9
|
+
const repoRoot = path.resolve(packageRoot, `../..`)
|
|
10
|
+
const source = path.join(repoRoot, `website/docs/agents`)
|
|
11
|
+
const target = path.join(packageRoot, `docs`)
|
|
12
|
+
|
|
13
|
+
async function pathExists(value) {
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(value)
|
|
16
|
+
return true
|
|
17
|
+
} catch {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function clean() {
|
|
23
|
+
await fs.rm(target, { recursive: true, force: true })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (process.argv.includes(`--clean`)) {
|
|
27
|
+
if (await pathExists(path.join(source, `index.md`))) {
|
|
28
|
+
await clean()
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
if (!(await pathExists(path.join(source, `index.md`)))) {
|
|
32
|
+
if (await pathExists(path.join(target, `index.md`))) {
|
|
33
|
+
console.log(`Agents docs source not found; preserving ${target}`)
|
|
34
|
+
process.exit(0)
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Agents docs source not found at ${source}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await clean()
|
|
40
|
+
await fs.cp(source, target, { recursive: true })
|
|
41
|
+
console.log(`Synced agents docs from ${source} to ${target}`)
|
|
42
|
+
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Mega Draw
|
|
3
|
-
titleTemplate: '... - Electric Agents'
|
|
4
|
-
description: >-
|
|
5
|
-
Multi-agent collaborative drawing example with coordinator-worker patterns and 100 tile agents.
|
|
6
|
-
outline: [2, 3]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Mega Draw
|
|
10
|
-
|
|
11
|
-
A collaborative multi-agent drawing app where 100 AI agents each own a tile of a shared 1000x1000 pixel canvas and work together to produce a drawing from a single text prompt. Located at `examples/mega-draw/` in the repository.
|
|
12
|
-
|
|
13
|
-
## What it demonstrates
|
|
14
|
-
|
|
15
|
-
- **Coordinator + worker pattern** at scale (1 coordinator spawning 100 tile agents)
|
|
16
|
-
- **Custom drawing tools** --- `fill_rect`, `draw_line`, `draw_circle`, `fill_gradient`, `set_pixels`
|
|
17
|
-
- **Shared canvas** --- in-memory pixel buffer flushed to PNG, served via a live viewer
|
|
18
|
-
- **Follow-up instructions** --- send a new prompt and only affected tiles get re-instructed
|
|
19
|
-
- **Two-pass workflow** --- coordinator does a quick first pass for backgrounds, then a detail pass
|
|
20
|
-
|
|
21
|
-
## Architecture
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
User
|
|
25
|
-
│
|
|
26
|
-
│ spawn /coordinator/my-drawing
|
|
27
|
-
│ send "Draw a sunset over mountains"
|
|
28
|
-
▼
|
|
29
|
-
┌────────────────────────────────┐
|
|
30
|
-
│ Coordinator Agent │
|
|
31
|
-
│ - Receives prompt │
|
|
32
|
-
│ - Plans composition + palette │
|
|
33
|
-
│ - Spawns 100 tile agents │
|
|
34
|
-
│ - Can re-instruct tiles │
|
|
35
|
-
└──────────┬─────────────────────┘
|
|
36
|
-
│ spawn tile-agent (10×10 grid)
|
|
37
|
-
▼
|
|
38
|
-
┌────────┐ ┌────────┐
|
|
39
|
-
│Tile 0,0│ │Tile 1,0│ ... (10 columns)
|
|
40
|
-
└────────┘ └────────┘
|
|
41
|
-
┌────────┐ ┌────────┐
|
|
42
|
-
│Tile 0,1│ │Tile 1,1│ ...
|
|
43
|
-
└────────┘ └────────┘
|
|
44
|
-
... ... (10 rows = 100 tiles)
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Each tile agent:
|
|
48
|
-
|
|
49
|
-
- Owns a 100x100 pixel region
|
|
50
|
-
- Can **see** 50px beyond its borders (200x200 viewport) for edge coordination
|
|
51
|
-
- Can only **draw** within its own tile
|
|
52
|
-
- Receives drawing instructions from the coordinator
|
|
53
|
-
|
|
54
|
-
## Key files
|
|
55
|
-
|
|
56
|
-
### `src/server.ts`
|
|
57
|
-
|
|
58
|
-
Entry point. Creates the registry, runtime handler, and two HTTP servers (one for the Electric Agents webhook, one for the canvas viewer).
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
const registry = createEntityRegistry()
|
|
62
|
-
registerCoordinator(registry, WEB_PORT)
|
|
63
|
-
registerTileAgent(registry)
|
|
64
|
-
|
|
65
|
-
const runtime = createRuntimeHandler({
|
|
66
|
-
baseUrl: ELECTRIC_AGENTS_URL,
|
|
67
|
-
serveEndpoint: `${SERVE_URL}/webhook`,
|
|
68
|
-
registry,
|
|
69
|
-
})
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### `src/coordinator.ts`
|
|
73
|
-
|
|
74
|
-
The coordinator entity. Defines two custom tools:
|
|
75
|
-
|
|
76
|
-
- `set_drawing_plan` --- sets the composition description and color palette
|
|
77
|
-
- `instruct_tile` --- spawns or re-instructs a tile agent with drawing directions
|
|
78
|
-
|
|
79
|
-
### `src/tile-agent.ts`
|
|
80
|
-
|
|
81
|
-
The tile agent entity. Each instance gets drawing tools scoped to its tile:
|
|
82
|
-
|
|
83
|
-
- `read_viewport` --- see current pixel state (own tile + neighbors)
|
|
84
|
-
- `fill_rect`, `draw_line`, `draw_circle`, `fill_gradient`, `set_pixels`
|
|
85
|
-
|
|
86
|
-
All coordinates are tile-relative (0--99) and automatically clipped to tile bounds.
|
|
87
|
-
|
|
88
|
-
## Running it
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
cd examples/mega-draw
|
|
92
|
-
pnpm install
|
|
93
|
-
cp ../../.env.template .env # Set ANTHROPIC_API_KEY
|
|
94
|
-
pnpm dev
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Requires a running Electric Agents runtime server at `http://localhost:4437`.
|
|
98
|
-
|
|
99
|
-
Then in another terminal:
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
npx electric-ax agents spawn /coordinator/my-drawing
|
|
103
|
-
npx electric-ax agents send /coordinator/my-drawing 'Draw a sunset over mountains'
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
View the canvas live at `http://localhost:3000/my-drawing` --- it auto-refreshes as tiles draw.
|