@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.
Files changed (45) hide show
  1. package/dist/entrypoint.js +40 -12
  2. package/dist/index.cjs +40 -12
  3. package/dist/index.js +40 -12
  4. package/docs/entities/agents/coder.md +99 -0
  5. package/docs/entities/agents/horton.md +16 -13
  6. package/docs/entities/agents/worker.md +18 -18
  7. package/docs/entities/patterns/blackboard.md +6 -6
  8. package/docs/entities/patterns/dispatcher.md +1 -1
  9. package/docs/entities/patterns/manager-worker.md +1 -1
  10. package/docs/entities/patterns/map-reduce.md +1 -1
  11. package/docs/entities/patterns/pipeline.md +1 -1
  12. package/docs/entities/patterns/reactive-observers.md +2 -2
  13. package/docs/examples/playground.md +42 -26
  14. package/docs/index.md +23 -23
  15. package/docs/quickstart.md +13 -13
  16. package/docs/reference/agent-config.md +20 -12
  17. package/docs/reference/agent-tool.md +1 -1
  18. package/docs/reference/built-in-collections.md +21 -21
  19. package/docs/reference/cli.md +39 -30
  20. package/docs/reference/entity-definition.md +9 -9
  21. package/docs/reference/entity-handle.md +2 -2
  22. package/docs/reference/entity-registry.md +1 -1
  23. package/docs/reference/handler-context.md +69 -18
  24. package/docs/reference/runtime-handler.md +25 -23
  25. package/docs/reference/shared-state-handle.md +7 -7
  26. package/docs/reference/state-collection-proxy.md +1 -1
  27. package/docs/reference/wake-event.md +23 -23
  28. package/docs/usage/app-setup.md +24 -23
  29. package/docs/usage/clients-and-react.md +44 -36
  30. package/docs/usage/configuring-the-agent.md +25 -19
  31. package/docs/usage/context-composition.md +12 -12
  32. package/docs/usage/defining-entities.md +36 -36
  33. package/docs/usage/defining-tools.md +45 -45
  34. package/docs/usage/embedded-builtins.md +48 -47
  35. package/docs/usage/managing-state.md +12 -12
  36. package/docs/usage/overview.md +52 -45
  37. package/docs/usage/programmatic-runtime-client.md +50 -47
  38. package/docs/usage/shared-state.md +32 -32
  39. package/docs/usage/spawning-and-coordinating.md +9 -9
  40. package/docs/usage/testing.md +14 -14
  41. package/docs/usage/waking-entities.md +13 -13
  42. package/docs/usage/writing-handlers.md +57 -26
  43. package/package.json +4 -1
  44. package/scripts/sync-docs.mjs +42 -0
  45. package/docs/examples/mega-draw.md +0 -106
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Testing
3
- titleTemplate: '... - Electric Agents'
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: 'claude-sonnet-4-5-20250929',
17
+ systemPrompt: "...",
18
+ model: "claude-sonnet-4-5-20250929",
19
19
  tools: [...ctx.electricTools],
20
- testResponses: ['Hello! How can I help?'],
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('Test response')
34
+ bridge.onTextDelta("Test response")
35
35
  bridge.onTextEnd()
36
- return 'Test response'
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 '@electric-ax/agents-runtime'
49
+ import { createEntityRegistry } from "@electric-ax/agents-runtime"
50
50
 
51
51
  const registry = createEntityRegistry()
52
52
  registerAssistant(registry)
53
53
 
54
- test('registers assistant', () => {
55
- const entry = registry.get('assistant')
54
+ test("registers assistant", () => {
55
+ const entry = registry.get("assistant")
56
56
  expect(entry).toBeDefined()
57
- expect(entry!.definition.handler).toBeTypeOf('function')
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('creates runtime with types', () => {
64
+ test("creates runtime with types", () => {
65
65
  const runtime = createRuntimeHandler({
66
- baseUrl: 'http://localhost:4437',
67
- serveEndpoint: 'http://localhost:3000/webhook',
66
+ baseUrl: "http://localhost:4437",
67
+ serveEndpoint: "http://localhost:3000/webhook",
68
68
  registry,
69
69
  })
70
- expect(runtime.typeNames).toContain('assistant')
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: '... - Electric Agents'
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.firstWake` for one-time initialization, and `ctx.db.actions` / `ctx.db.collections` to carry state across wakes.
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('/assistant/peer', { text: 'hello' })
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
- 'worker',
49
- 'analysis-1',
50
- { systemPrompt: 'Analyse this input.', tools: ['read'] },
48
+ "worker",
49
+ "analysis-1",
50
+ { systemPrompt: "Analyse this input.", tools: ["read"] },
51
51
  {
52
- initialMessage: 'begin',
53
- wake: { on: 'runFinished', includeResponse: true },
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 '@electric-ax/agents-runtime'
65
+ import { entity } from "@electric-ax/agents-runtime"
66
66
 
67
67
  await ctx.observe(entity(someEntityUrl), {
68
- wake: { on: 'change', collections: ['status'], debounceMs: 250 },
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('board-1', schema), {
80
- wake: { on: 'change', collections: ['findings'] },
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. Use `ctx.firstWake` and idempotent writes to collections rather than side effects on each wake.
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: '... - Electric Agents'
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` on the entity's first activation ever. Use for initialization. |
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` | Host-provided runtime-level tools to pass to `useAgent` when needed. May be empty. |
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 on first wake, configure the agent, run the agent.
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('assistant', {
135
- description: 'A general-purpose assistant',
141
+ registry.define("assistant", {
142
+ description: "A general-purpose assistant",
136
143
  state: {
137
- status: { primaryKey: 'key' },
144
+ status: { primaryKey: "key" },
138
145
  },
139
146
 
140
147
  async handler(ctx) {
141
- if (ctx.firstWake) {
142
- ctx.db.actions.status_insert({ row: { key: 'current', value: 'idle' } })
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: 'You are a helpful assistant.',
147
- model: 'claude-sonnet-4-5-20250929',
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` only on the entity's very first activation. Use it for one-time initialization:
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.firstWake) {
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
- On subsequent wakes (new messages, child completion, etc.), `firstWake` is `false`.
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('/worker/task-1', { action: 'process', data: payload })
264
- ctx.send('/worker/task-1', payload, { type: 'custom_type' })
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",
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.