@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: Programmatic runtime client
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  Use createRuntimeServerClient to spawn entities, send messages, register wakes,
6
6
  manage schedules, and connect shared state from application code.
@@ -12,10 +12,10 @@ outline: [2, 3]
12
12
  `createRuntimeServerClient()` is the lower-level HTTP client for the Electric Agents server. Handler code should usually use `ctx.spawn()`, `ctx.send()`, `ctx.observe()`, and `ctx.mkdb()` instead. Use this client from application services, tests, CLIs, and integration code that needs to manage entities from outside a handler.
13
13
 
14
14
  ```ts
15
- import { createRuntimeServerClient } from '@electric-ax/agents-runtime'
15
+ import { createRuntimeServerClient } from "@electric-ax/agents-runtime"
16
16
 
17
17
  const client = createRuntimeServerClient({
18
- baseUrl: 'http://localhost:4437',
18
+ baseUrl: "http://localhost:4437",
19
19
  })
20
20
  ```
21
21
 
@@ -41,11 +41,11 @@ interface RuntimeServerClientConfig {
41
41
 
42
42
  ```ts
43
43
  const info = await client.spawnEntity({
44
- type: 'horton',
45
- id: 'onboarding',
46
- args: { timezone: 'Europe/London' },
47
- initialMessage: 'Help me get started.',
48
- tags: { project: 'docs' },
44
+ type: "horton",
45
+ id: "onboarding",
46
+ args: { timezone: "Europe/London" },
47
+ initialMessage: "Help me get started.",
48
+ tags: { project: "docs" },
49
49
  })
50
50
 
51
51
  console.log(info.entityUrl) // "/horton/onboarding"
@@ -64,11 +64,11 @@ interface SpawnEntityOptions {
64
64
  wake?: {
65
65
  subscriberUrl: string
66
66
  condition:
67
- | 'runFinished'
67
+ | "runFinished"
68
68
  | {
69
- on: 'change'
69
+ on: "change"
70
70
  collections?: string[]
71
- ops?: Array<'insert' | 'update' | 'delete'>
71
+ ops?: Array<"insert" | "update" | "delete">
72
72
  }
73
73
  debounceMs?: number
74
74
  timeoutMs?: number
@@ -80,14 +80,14 @@ interface SpawnEntityOptions {
80
80
  ### getEntityInfo
81
81
 
82
82
  ```ts
83
- const info = await client.getEntityInfo('/horton/onboarding')
83
+ const info = await client.getEntityInfo("/horton/onboarding")
84
84
  // { entityUrl, entityType, streamPath }
85
85
  ```
86
86
 
87
87
  ### deleteEntity
88
88
 
89
89
  ```ts
90
- await client.deleteEntity('/horton/onboarding')
90
+ await client.deleteEntity("/horton/onboarding")
91
91
  ```
92
92
 
93
93
  Deleting an already-missing entity is treated as success.
@@ -96,10 +96,10 @@ Deleting an already-missing entity is treated as success.
96
96
 
97
97
  ```ts
98
98
  await client.sendEntityMessage({
99
- targetUrl: '/horton/onboarding',
100
- payload: 'What changed since last time?',
101
- from: 'support-ui',
102
- type: 'user_message',
99
+ targetUrl: "/horton/onboarding",
100
+ payload: "What changed since last time?",
101
+ from: "support-ui",
102
+ type: "user_message",
103
103
  })
104
104
  ```
105
105
 
@@ -118,10 +118,10 @@ interface SendEntityMessageOptions {
118
118
  ## Shared State
119
119
 
120
120
  ```ts
121
- const streamPath = await client.ensureSharedStateStream('research-123')
121
+ const streamPath = await client.ensureSharedStateStream("research-123")
122
122
  // "/_electric/shared-state/research-123"
123
123
 
124
- const samePath = client.getSharedStateStreamPath('research-123')
124
+ const samePath = client.getSharedStateStreamPath("research-123")
125
125
  ```
126
126
 
127
127
  Use `ensureSharedStateStream()` when app code needs to create a shared-state stream before entities connect to it.
@@ -134,9 +134,9 @@ Use `ensureSharedStateStream()` when app code needs to create a shared-state str
134
134
 
135
135
  ```ts
136
136
  await client.registerWake({
137
- subscriberUrl: '/coordinator/research',
138
- sourceUrl: '/worker/analyst/main',
139
- condition: 'runFinished',
137
+ subscriberUrl: "/coordinator/research",
138
+ sourceUrl: "/worker/analyst/main",
139
+ condition: "runFinished",
140
140
  includeResponse: true,
141
141
  })
142
142
  ```
@@ -145,12 +145,12 @@ For change wakes:
145
145
 
146
146
  ```ts
147
147
  await client.registerWake({
148
- subscriberUrl: '/monitor/main',
149
- sourceUrl: '/horton/onboarding/main',
148
+ subscriberUrl: "/monitor/main",
149
+ sourceUrl: "/horton/onboarding/main",
150
150
  condition: {
151
- on: 'change',
152
- collections: ['runs', 'texts'],
153
- ops: ['insert', 'update'],
151
+ on: "change",
152
+ collections: ["runs", "texts"],
153
+ ops: ["insert", "update"],
154
154
  },
155
155
  debounceMs: 250,
156
156
  })
@@ -159,13 +159,16 @@ await client.registerWake({
159
159
  ### registerCronSource
160
160
 
161
161
  ```ts
162
- const streamUrl = await client.registerCronSource('0 9 * * *', 'Europe/London')
162
+ const streamUrl = await client.registerCronSource(
163
+ "0 9 * * *",
164
+ "Europe/London"
165
+ )
163
166
  ```
164
167
 
165
168
  ### registerEntitiesSource
166
169
 
167
170
  ```ts
168
- const source = await client.registerEntitiesSource({ project: 'docs' })
171
+ const source = await client.registerEntitiesSource({ project: "docs" })
169
172
  // { streamUrl, sourceRef }
170
173
  ```
171
174
 
@@ -177,40 +180,40 @@ Schedules are stored on an entity manifest and return the write transaction id.
177
180
 
178
181
  ```ts
179
182
  await client.upsertCronSchedule({
180
- entityUrl: '/horton/onboarding',
181
- id: 'daily-checkin',
182
- expression: '0 9 * * *',
183
- timezone: 'Europe/London',
184
- payload: 'Run the daily check-in.',
183
+ entityUrl: "/horton/onboarding",
184
+ id: "daily-checkin",
185
+ expression: "0 9 * * *",
186
+ timezone: "Europe/London",
187
+ payload: "Run the daily check-in.",
185
188
  })
186
189
 
187
190
  await client.upsertFutureSendSchedule({
188
- entityUrl: '/horton/onboarding',
189
- id: 'follow-up',
191
+ entityUrl: "/horton/onboarding",
192
+ id: "follow-up",
190
193
  fireAt: new Date(Date.now() + 60_000).toISOString(),
191
- payload: 'Follow up now.',
194
+ payload: "Follow up now.",
192
195
  })
193
196
 
194
197
  await client.deleteSchedule({
195
- entityUrl: '/horton/onboarding',
196
- id: 'follow-up',
198
+ entityUrl: "/horton/onboarding",
199
+ id: "follow-up",
197
200
  })
198
201
  ```
199
202
 
200
203
  ## Tags
201
204
 
202
- `setTag()` and `removeTag()` require the entity write token. Handler code should prefer `ctx.setTag()` and `ctx.removeTag()` because the runtime already has the write token.
205
+ `setTag()` and `removeTag()` are primarily for handler/runtime-owned flows that already hold the current claim-scoped write token. External clients should prefer `send()` and write only to an entity's inbox rather than writing entity state directly.
203
206
 
204
207
  ```ts
205
- await client.setTag('/horton/onboarding', 'title', 'Onboarding', writeToken)
206
- await client.removeTag('/horton/onboarding', 'title', writeToken)
208
+ await client.setTag("/horton/onboarding", "title", "Onboarding", writeToken)
209
+ await client.removeTag("/horton/onboarding", "title", writeToken)
207
210
  ```
208
211
 
209
212
  ## Choosing a Client
210
213
 
211
- | API | Use when |
212
- | ------------------------------ | ---------------------------------------------------------------------------- |
213
- | `ctx.spawn/send/observe` | You are inside an entity handler. |
214
- | `createAgentsClient()` | You need to observe streams and drive UI state. |
215
- | `createRuntimeServerClient()` | You need to manage entities, messages, wakes, schedules, or tags externally. |
216
- | `electric-ax/entity-stream-db` | You need the CLI-style entity stream loader with `close()`. |
214
+ | API | Use when |
215
+ | --------------------------- | ------------------------------------------------------------------------ |
216
+ | `ctx.spawn/send/observe` | You are inside an entity handler. |
217
+ | `createAgentsClient()` | You need to observe streams and drive UI state. |
218
+ | `createRuntimeServerClient()` | You need to manage entities, messages, wakes, schedules, or tags externally. |
219
+ | `electric-ax/entity-stream-db` | You need the CLI-style entity stream loader with `close()`. |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Shared state
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  Coordinate across entities with shared state streams, schema definition, and cross-entity reads and writes.
6
6
  outline: [2, 3]
@@ -18,8 +18,8 @@ Define a `SharedStateSchemaMap` — a record of collection names to their schema
18
18
  const researchSchema = {
19
19
  findings: {
20
20
  schema: z.object({ key: z.string(), domain: z.string(), text: z.string() }),
21
- type: 'shared:finding',
22
- primaryKey: 'key',
21
+ type: "shared:finding",
22
+ primaryKey: "key",
23
23
  },
24
24
  }
25
25
  ```
@@ -32,9 +32,9 @@ The parent entity creates the shared DB stream, typically on `firstWake`:
32
32
 
33
33
  ```ts
34
34
  if (ctx.firstWake) {
35
- ctx.mkdb('research-123', researchSchema)
35
+ ctx.mkdb("research-123", researchSchema)
36
36
  }
37
- const shared = await ctx.observe(db('research-123', researchSchema))
37
+ const shared = await ctx.observe(db("research-123", researchSchema))
38
38
  ```
39
39
 
40
40
  `mkdb` creates the backing stream. It throws if the DB already exists — creation is always a one-time operation guarded by `firstWake` or your own state checks.
@@ -44,8 +44,8 @@ const shared = await ctx.observe(db('research-123', researchSchema))
44
44
  `observe` accepts an optional `wake` option to re-wake the entity when the shared state changes:
45
45
 
46
46
  ```ts
47
- const shared = await ctx.observe(db('research-123', researchSchema), {
48
- wake: { on: 'change', debounceMs: 500 },
47
+ const shared = await ctx.observe(db("research-123", researchSchema), {
48
+ wake: { on: "change", debounceMs: 500 },
49
49
  })
50
50
  ```
51
51
 
@@ -55,13 +55,13 @@ Pass the shared DB config to children via spawn args:
55
55
 
56
56
  ```ts
57
57
  const child = await ctx.spawn(
58
- 'worker',
59
- 'specialist-1',
58
+ "worker",
59
+ "specialist-1",
60
60
  {
61
- systemPrompt: '...',
62
- sharedDb: { id: 'research-123', schema: researchSchema },
61
+ systemPrompt: "...",
62
+ sharedDb: { id: "research-123", schema: researchSchema },
63
63
  },
64
- { initialMessage: 'Research topic X', wake: 'runFinished' }
64
+ { initialMessage: "Research topic X", wake: "runFinished" }
65
65
  )
66
66
  ```
67
67
 
@@ -82,22 +82,22 @@ async handler(ctx) {
82
82
  ```ts
83
83
  // Insert
84
84
  shared.findings.insert({
85
- key: 'f1',
86
- domain: 'physics',
87
- text: 'Finding text...',
85
+ key: "f1",
86
+ domain: "physics",
87
+ text: "Finding text...",
88
88
  })
89
89
 
90
90
  // Read
91
- shared.findings.get('f1')
91
+ shared.findings.get("f1")
92
92
  shared.findings.toArray
93
93
 
94
94
  // Update
95
- shared.findings.update('f1', (draft) => {
96
- draft.text = 'Updated'
95
+ shared.findings.update("f1", (draft) => {
96
+ draft.text = "Updated"
97
97
  })
98
98
 
99
99
  // Delete
100
- shared.findings.delete('f1')
100
+ shared.findings.delete("f1")
101
101
  ```
102
102
 
103
103
  ## SharedStateHandle type
@@ -119,18 +119,18 @@ const debateSchema = {
119
119
  arguments: {
120
120
  schema: z.object({
121
121
  key: z.string(),
122
- side: z.enum(['pro', 'con']),
122
+ side: z.enum(["pro", "con"]),
123
123
  text: z.string(),
124
124
  round: z.number(),
125
125
  }),
126
- type: 'shared:argument',
127
- primaryKey: 'key',
126
+ type: "shared:argument",
127
+ primaryKey: "key",
128
128
  },
129
129
  }
130
130
 
131
- registry.define('debate', {
131
+ registry.define("debate", {
132
132
  state: {
133
- status: { primaryKey: 'key' },
133
+ status: { primaryKey: "key" },
134
134
  },
135
135
 
136
136
  async handler(ctx) {
@@ -143,23 +143,23 @@ registry.define('debate', {
143
143
 
144
144
  // Spawn pro and con workers with shared state access
145
145
  const pro = await ctx.spawn(
146
- 'worker',
147
- 'debate-pro',
146
+ "worker",
147
+ "debate-pro",
148
148
  {
149
- systemPrompt: 'Argue FOR the topic.',
149
+ systemPrompt: "Argue FOR the topic.",
150
150
  sharedDb: { id: `debate-${ctx.entityUrl}`, schema: debateSchema },
151
151
  },
152
- { initialMessage: 'The topic is: ...', wake: 'runFinished' }
152
+ { initialMessage: "The topic is: ...", wake: "runFinished" }
153
153
  )
154
154
 
155
155
  const con = await ctx.spawn(
156
- 'worker',
157
- 'debate-con',
156
+ "worker",
157
+ "debate-con",
158
158
  {
159
- systemPrompt: 'Argue AGAINST the topic.',
159
+ systemPrompt: "Argue AGAINST the topic.",
160
160
  sharedDb: { id: `debate-${ctx.entityUrl}`, schema: debateSchema },
161
161
  },
162
- { initialMessage: 'The topic is: ...', wake: 'runFinished' }
162
+ { initialMessage: "The topic is: ...", wake: "runFinished" }
163
163
  )
164
164
 
165
165
  // Read all arguments written by both workers
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Spawning & coordinating
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  Spawn child entities, observe existing ones, send messages, and use EntityHandle for coordination.
6
6
  outline: [2, 3]
@@ -63,7 +63,7 @@ Wait for a single child:
63
63
 
64
64
  ```ts
65
65
  await child.run
66
- const output = (await child.text()).join('\n\n')
66
+ const output = (await child.text()).join("\n\n")
67
67
  ```
68
68
 
69
69
  Wait for multiple children in parallel:
@@ -71,7 +71,7 @@ Wait for multiple children in parallel:
71
71
  ```ts
72
72
  const results = await Promise.all(
73
73
  children.map(async ({ handle }) => ({
74
- text: (await handle.text()).join('\n\n'),
74
+ text: (await handle.text()).join("\n\n"),
75
75
  }))
76
76
  )
77
77
  ```
@@ -82,7 +82,7 @@ Subscribe to an existing entity without spawning it:
82
82
 
83
83
  ```ts
84
84
  const handle = await ctx.observe(entity(entityUrl), {
85
- wake: { on: 'change', collections: ['runs', 'childStatus'] },
85
+ wake: { on: "change", collections: ["runs", "childStatus"] },
86
86
  })
87
87
  ```
88
88
 
@@ -93,8 +93,8 @@ Returns an `EntityHandle`. Use `wake` to re-invoke the parent handler when the o
93
93
  Fire-and-forget message to another entity:
94
94
 
95
95
  ```ts
96
- ctx.send('/assistant/target-id', { text: 'Hello' })
97
- ctx.send('/assistant/target-id', payload, { type: 'custom_type' })
96
+ ctx.send("/assistant/target-id", { text: "Hello" })
97
+ ctx.send("/assistant/target-id", payload, { type: "custom_type" })
98
98
  ```
99
99
 
100
100
  Messages appear in the target entity's `inbox` collection.
@@ -152,12 +152,12 @@ const data = await response.json()
152
152
 
153
153
  // Pass data, not credentials, to the worker
154
154
  await ctx.spawn(
155
- 'worker',
155
+ "worker",
156
156
  id,
157
- { systemPrompt: 'Summarise this data.', tools: ['read'] },
157
+ { systemPrompt: "Summarise this data.", tools: ["read"] },
158
158
  {
159
159
  initialMessage: JSON.stringify(data),
160
- wake: 'runFinished',
160
+ wake: "runFinished",
161
161
  }
162
162
  )
163
163
  ```
@@ -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