@electric-ax/agents 0.2.3 → 0.3.0

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 (49) hide show
  1. package/dist/entrypoint.js +474 -737
  2. package/dist/index.cjs +470 -733
  3. package/dist/index.d.cts +68 -35
  4. package/dist/index.d.ts +69 -36
  5. package/dist/index.js +489 -751
  6. package/docs/entities/agents/horton.md +12 -12
  7. package/docs/entities/agents/worker.md +18 -18
  8. package/docs/entities/patterns/blackboard.md +6 -6
  9. package/docs/entities/patterns/dispatcher.md +1 -1
  10. package/docs/entities/patterns/manager-worker.md +1 -1
  11. package/docs/entities/patterns/map-reduce.md +1 -1
  12. package/docs/entities/patterns/pipeline.md +1 -1
  13. package/docs/entities/patterns/reactive-observers.md +2 -2
  14. package/docs/examples/playground.md +42 -26
  15. package/docs/index.md +25 -23
  16. package/docs/quickstart.md +12 -12
  17. package/docs/reference/agent-config.md +20 -12
  18. package/docs/reference/agent-tool.md +1 -1
  19. package/docs/reference/built-in-collections.md +21 -21
  20. package/docs/reference/cli.md +39 -30
  21. package/docs/reference/entity-definition.md +9 -9
  22. package/docs/reference/entity-handle.md +2 -2
  23. package/docs/reference/entity-registry.md +1 -1
  24. package/docs/reference/handler-context.md +34 -18
  25. package/docs/reference/mcp-registry.md +189 -0
  26. package/docs/reference/mcp-server-config.md +226 -0
  27. package/docs/reference/runtime-handler.md +25 -23
  28. package/docs/reference/shared-state-handle.md +7 -7
  29. package/docs/reference/state-collection-proxy.md +1 -1
  30. package/docs/reference/wake-event.md +23 -23
  31. package/docs/usage/app-setup.md +24 -23
  32. package/docs/usage/clients-and-react.md +40 -36
  33. package/docs/usage/configuring-the-agent.md +25 -19
  34. package/docs/usage/context-composition.md +12 -12
  35. package/docs/usage/defining-entities.md +36 -36
  36. package/docs/usage/defining-tools.md +45 -45
  37. package/docs/usage/embedded-builtins.md +54 -43
  38. package/docs/usage/managing-state.md +12 -12
  39. package/docs/usage/mcp-servers.md +354 -0
  40. package/docs/usage/overview.md +50 -45
  41. package/docs/usage/programmatic-runtime-client.md +51 -48
  42. package/docs/usage/shared-state.md +32 -32
  43. package/docs/usage/spawning-and-coordinating.md +9 -9
  44. package/docs/usage/testing.md +14 -14
  45. package/docs/usage/waking-entities.md +13 -13
  46. package/docs/usage/writing-handlers.md +52 -26
  47. package/package.json +9 -4
  48. package/scripts/sync-docs.mjs +42 -0
  49. package/docs/examples/mega-draw.md +0 -106
@@ -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]
@@ -62,6 +62,7 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
62
62
  payload: unknown,
63
63
  opts?: { type?: string; afterMs?: number }
64
64
  ) => void
65
+ recordRun: () => RunHandle
65
66
  setTag: (key: string, value: string) => Promise<void>
66
67
  removeTag: (key: string) => Promise<void>
67
68
  sleep: () => void
@@ -72,7 +73,7 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
72
73
 
73
74
  | Property | Description |
74
75
  | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
75
- | `firstWake` | `true` on the entity's first activation ever. Use for initialization. |
76
+ | `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
77
  | `tags` | Entity tags -- key/value metadata associated with this entity. |
77
78
  | `entityUrl` | The entity's URL path, e.g. `"/assistant/my-chat"`. |
78
79
  | `entityType` | The registered type name, e.g. `"assistant"`. |
@@ -81,7 +82,7 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
81
82
  | `state` | Proxy object keyed by collection name. Each property is a [`StateCollectionProxy`](../reference/state-collection-proxy). |
82
83
  | `events` | Change events that triggered this wake. |
83
84
  | `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. |
85
+ | `electricTools` | Host-provided runtime-level tools to pass to `useAgent` when needed. May be empty. |
85
86
  | `useAgent` | Configures the LLM agent. Returns an `AgentHandle`. See [Configuring the agent](./configuring-the-agent). |
86
87
  | `useContext` | Declares context sources with token budgets and cache tiers. See [Context composition](./context-composition). |
87
88
  | `timelineMessages` | Projects the entity timeline into LLM messages. See [Context composition](./context-composition#timelinemessages). |
@@ -94,6 +95,7 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
94
95
  | `observe` | Connects to another entity's stream or shared db. See [Reactive observers](../entities/patterns/reactive-observers) and [Shared state](./shared-state). |
95
96
  | `mkdb` | Creates a new shared state stream. See [Shared state](./shared-state). |
96
97
  | `send` | Sends a message to another entity's inbox. Supports delayed delivery via `afterMs`. |
98
+ | `recordRun` | Records non-LLM work in the built-in `runs` collection so `runFinished` observers are woken. |
97
99
  | `setTag` | Sets a tag on this entity. |
98
100
  | `removeTag` | Removes a tag from this entity. |
99
101
  | `sleep` | Returns the entity to idle without re-waking. |
@@ -115,36 +117,36 @@ type WakeEvent = {
115
117
  }
116
118
  ```
117
119
 
118
- | Field | Description |
119
- | ------------ | ------------------------------------------------------------------------------------------------------------------------------ |
120
- | `source` | The stream or entity that caused the wake. |
120
+ | Field | Description |
121
+ | ------------ | -------------------------------------------------------------- |
122
+ | `source` | The stream or entity that caused the wake. |
121
123
  | `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. |
124
+ | `fromOffset` | Start offset of the events that triggered this wake. |
125
+ | `toOffset` | End offset of the events that triggered this wake. |
126
+ | `eventCount` | Number of new events since last wake. |
127
+ | `payload` | Optional payload from the trigger event. |
128
+ | `summary` | Optional human-readable summary. |
129
+ | `fullRef` | Optional full reference string for the trigger. |
128
130
 
129
131
  ## Typical handler pattern
130
132
 
131
- Most handlers follow the same structure: initialize state on first wake, configure the agent, run the agent.
133
+ Most LLM handlers follow the same structure: initialize missing state idempotently, configure the agent, run the agent.
132
134
 
133
135
  ```ts
134
- registry.define('assistant', {
135
- description: 'A general-purpose assistant',
136
+ registry.define("assistant", {
137
+ description: "A general-purpose assistant",
136
138
  state: {
137
- status: { primaryKey: 'key' },
139
+ status: { primaryKey: "key" },
138
140
  },
139
141
 
140
142
  async handler(ctx) {
141
- if (ctx.firstWake) {
142
- ctx.db.actions.status_insert({ row: { key: 'current', value: 'idle' } })
143
+ if (!ctx.db.collections.status.get("current")) {
144
+ ctx.db.actions.status_insert({ row: { key: "current", value: "idle" } })
143
145
  }
144
146
 
145
147
  ctx.useAgent({
146
- systemPrompt: 'You are a helpful assistant.',
147
- model: 'claude-sonnet-4-5-20250929',
148
+ systemPrompt: "You are a helpful assistant.",
149
+ model: "claude-sonnet-4-5-20250929",
148
150
  tools: [...ctx.electricTools],
149
151
  })
150
152
  await ctx.agent.run()
@@ -163,25 +165,31 @@ interface AgentConfig {
163
165
  provider?: KnownProvider
164
166
  tools: AgentTool[]
165
167
  streamFn?: StreamFn
168
+ getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined
169
+ onPayload?: SimpleStreamOptions["onPayload"]
166
170
  testResponses?: string[] | TestResponseFn
167
171
  }
168
172
  ```
169
173
 
170
- ## firstWake
174
+ ## firstWake and initialization
171
175
 
172
- `ctx.firstWake` is `true` only on the entity's very first activation. Use it for one-time initialization:
176
+ `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.
177
+
178
+ For plain state rows, prefer checking the collection itself so initialization stays idempotent even for entities that do not create manifest entries:
173
179
 
174
180
  ```ts
175
181
  async handler(ctx) {
176
- if (ctx.firstWake) {
182
+ if (!ctx.db.collections.status.get("current")) {
177
183
  ctx.db.actions.status_insert({ row: { key: 'current', value: 'idle' } })
184
+ }
185
+ if (!ctx.db.collections.counters.get("runs")) {
178
186
  ctx.db.actions.counters_insert({ row: { key: 'runs', value: 0 } })
179
187
  }
180
188
  // ...
181
189
  }
182
190
  ```
183
191
 
184
- On subsequent wakes (new messages, child completion, etc.), `firstWake` is `false`.
192
+ After an entity persists manifest entries, subsequent wakes set `firstWake` to `false`.
185
193
 
186
194
  ## sleep
187
195
 
@@ -200,6 +208,24 @@ async handler(ctx, wake) {
200
208
  }
201
209
  ```
202
210
 
211
+ ## recordRun
212
+
213
+ 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"`.
214
+
215
+ ```ts
216
+ async handler(ctx) {
217
+ const run = ctx.recordRun()
218
+ try {
219
+ const result = await runExternalJob()
220
+ run.attachResponse(result.summary)
221
+ run.end({ status: "completed" })
222
+ } catch (error) {
223
+ run.end({ status: "failed", finishReason: "error" })
224
+ throw error
225
+ }
226
+ }
227
+ ```
228
+
203
229
  ## Using spawn args
204
230
 
205
231
  Arguments passed at spawn time are available as `ctx.args`. This is how you parameterize entity behavior:
@@ -260,8 +286,8 @@ async handler(ctx) {
260
286
  Use `ctx.send()` to deliver a message to another entity's inbox:
261
287
 
262
288
  ```ts
263
- ctx.send('/worker/task-1', { action: 'process', data: payload })
264
- ctx.send('/worker/task-1', payload, { type: 'custom_type' })
289
+ ctx.send("/worker/task-1", { action: "process", data: payload })
290
+ ctx.send("/worker/task-1", payload, { type: "custom_type" })
265
291
  ```
266
292
 
267
293
  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.3.0",
4
4
  "description": "Built-in Electric Agents runtimes such as Horton and worker",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,31 +28,33 @@
28
28
  "./package.json": "./package.json"
29
29
  },
30
30
  "dependencies": {
31
- "@anthropic-ai/sdk": "^0.78.0",
32
31
  "@durable-streams/state": "npm:@electric-ax/durable-streams-state-beta@^0.3.1",
33
32
  "@mariozechner/pi-agent-core": "^0.70.2",
34
33
  "@mariozechner/pi-ai": "^0.70.2",
35
34
  "@sinclair/typebox": "^0.34.48",
36
- "agent-session-protocol": "^0.0.2",
37
35
  "better-sqlite3": "^11.10.0",
38
36
  "nanoid": "^3.3.11",
39
37
  "pino": "^10.3.1",
40
38
  "pino-pretty": "^13.0.0",
41
39
  "sqlite-vec": "^0.1.9",
42
40
  "zod": "^4.3.6",
43
- "@electric-ax/agents-runtime": "0.1.2"
41
+ "@electric-ax/agents-mcp": "0.2.0",
42
+ "@electric-ax/agents-runtime": "0.1.3"
44
43
  },
45
44
  "devDependencies": {
46
45
  "@types/better-sqlite3": "^7.6.13",
47
46
  "@types/node": "^22.19.15",
48
47
  "@vitest/coverage-v8": "^4.1.0",
48
+ "cross-env": "^10.1.0",
49
49
  "tsdown": "^0.9.0",
50
+ "tsx": "^4.19.0",
50
51
  "typescript": "^5.0.0",
51
52
  "vitest": "^4.1.0"
52
53
  },
53
54
  "files": [
54
55
  "dist",
55
56
  "docs",
57
+ "scripts",
56
58
  "skills"
57
59
  ],
58
60
  "sideEffects": false,
@@ -60,6 +62,9 @@
60
62
  "scripts": {
61
63
  "build": "tsdown",
62
64
  "dev": "tsdown --watch",
65
+ "start": "cross-env ELECTRIC_AGENTS_SERVER_URL=http://localhost:4437 tsx --watch src/entrypoint.ts",
66
+ "docs:sync": "node scripts/sync-docs.mjs",
67
+ "docs:clean": "node scripts/sync-docs.mjs --clean",
63
68
  "test": "vitest run",
64
69
  "coverage": "pnpm exec vitest --coverage",
65
70
  "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.