@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.
- package/dist/entrypoint.js +474 -737
- package/dist/index.cjs +470 -733
- package/dist/index.d.cts +68 -35
- package/dist/index.d.ts +69 -36
- package/dist/index.js +489 -751
- package/docs/entities/agents/horton.md +12 -12
- 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 +25 -23
- package/docs/quickstart.md +12 -12
- 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 +34 -18
- package/docs/reference/mcp-registry.md +189 -0
- package/docs/reference/mcp-server-config.md +226 -0
- 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 +40 -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 +54 -43
- package/docs/usage/managing-state.md +12 -12
- package/docs/usage/mcp-servers.md +354 -0
- package/docs/usage/overview.md +50 -45
- package/docs/usage/programmatic-runtime-client.md +51 -48
- 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 +52 -26
- package/package.json +9 -4
- package/scripts/sync-docs.mjs +42 -0
- package/docs/examples/mega-draw.md +0 -106
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Programmatic runtime client
|
|
3
|
-
titleTemplate:
|
|
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
|
|
15
|
+
import { createRuntimeServerClient } from "@electric-ax/agents-runtime"
|
|
16
16
|
|
|
17
17
|
const client = createRuntimeServerClient({
|
|
18
|
-
baseUrl:
|
|
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:
|
|
45
|
-
id:
|
|
46
|
-
args: { timezone:
|
|
47
|
-
initialMessage:
|
|
48
|
-
tags: { project:
|
|
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
|
-
|
|
|
67
|
+
| "runFinished"
|
|
68
68
|
| {
|
|
69
|
-
on:
|
|
69
|
+
on: "change"
|
|
70
70
|
collections?: string[]
|
|
71
|
-
ops?: Array<
|
|
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(
|
|
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(
|
|
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:
|
|
100
|
-
payload:
|
|
101
|
-
from:
|
|
102
|
-
type:
|
|
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(
|
|
121
|
+
const streamPath = await client.ensureSharedStateStream("research-123")
|
|
122
122
|
// "/_electric/shared-state/research-123"
|
|
123
123
|
|
|
124
|
-
const samePath = client.getSharedStateStreamPath(
|
|
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:
|
|
138
|
-
sourceUrl:
|
|
139
|
-
condition:
|
|
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:
|
|
149
|
-
sourceUrl:
|
|
148
|
+
subscriberUrl: "/monitor/main",
|
|
149
|
+
sourceUrl: "/horton/onboarding/main",
|
|
150
150
|
condition: {
|
|
151
|
-
on:
|
|
152
|
-
collections: [
|
|
153
|
-
ops: [
|
|
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(
|
|
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:
|
|
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:
|
|
181
|
-
id:
|
|
182
|
-
expression:
|
|
183
|
-
timezone:
|
|
184
|
-
payload:
|
|
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:
|
|
189
|
-
id:
|
|
191
|
+
entityUrl: "/horton/onboarding",
|
|
192
|
+
id: "follow-up",
|
|
190
193
|
fireAt: new Date(Date.now() + 60_000).toISOString(),
|
|
191
|
-
payload:
|
|
194
|
+
payload: "Follow up now.",
|
|
192
195
|
})
|
|
193
196
|
|
|
194
197
|
await client.deleteSchedule({
|
|
195
|
-
entityUrl:
|
|
196
|
-
id:
|
|
198
|
+
entityUrl: "/horton/onboarding",
|
|
199
|
+
id: "follow-up",
|
|
197
200
|
})
|
|
198
201
|
```
|
|
199
202
|
|
|
200
203
|
## Tags
|
|
201
204
|
|
|
202
|
-
`setTag()` and `removeTag()`
|
|
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(
|
|
206
|
-
await client.removeTag(
|
|
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
|
|
212
|
-
|
|
|
213
|
-
| `ctx.spawn/send/observe`
|
|
214
|
-
| `createAgentsClient()`
|
|
215
|
-
| `createRuntimeServerClient()`
|
|
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:
|
|
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:
|
|
22
|
-
primaryKey:
|
|
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(
|
|
35
|
+
ctx.mkdb("research-123", researchSchema)
|
|
36
36
|
}
|
|
37
|
-
const shared = await ctx.observe(db(
|
|
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(
|
|
48
|
-
wake: { on:
|
|
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
|
-
|
|
59
|
-
|
|
58
|
+
"worker",
|
|
59
|
+
"specialist-1",
|
|
60
60
|
{
|
|
61
|
-
systemPrompt:
|
|
62
|
-
sharedDb: { id:
|
|
61
|
+
systemPrompt: "...",
|
|
62
|
+
sharedDb: { id: "research-123", schema: researchSchema },
|
|
63
63
|
},
|
|
64
|
-
{ initialMessage:
|
|
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:
|
|
86
|
-
domain:
|
|
87
|
-
text:
|
|
85
|
+
key: "f1",
|
|
86
|
+
domain: "physics",
|
|
87
|
+
text: "Finding text...",
|
|
88
88
|
})
|
|
89
89
|
|
|
90
90
|
// Read
|
|
91
|
-
shared.findings.get(
|
|
91
|
+
shared.findings.get("f1")
|
|
92
92
|
shared.findings.toArray
|
|
93
93
|
|
|
94
94
|
// Update
|
|
95
|
-
shared.findings.update(
|
|
96
|
-
draft.text =
|
|
95
|
+
shared.findings.update("f1", (draft) => {
|
|
96
|
+
draft.text = "Updated"
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
// Delete
|
|
100
|
-
shared.findings.delete(
|
|
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([
|
|
122
|
+
side: z.enum(["pro", "con"]),
|
|
123
123
|
text: z.string(),
|
|
124
124
|
round: z.number(),
|
|
125
125
|
}),
|
|
126
|
-
type:
|
|
127
|
-
primaryKey:
|
|
126
|
+
type: "shared:argument",
|
|
127
|
+
primaryKey: "key",
|
|
128
128
|
},
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
registry.define(
|
|
131
|
+
registry.define("debate", {
|
|
132
132
|
state: {
|
|
133
|
-
status: { primaryKey:
|
|
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
|
-
|
|
147
|
-
|
|
146
|
+
"worker",
|
|
147
|
+
"debate-pro",
|
|
148
148
|
{
|
|
149
|
-
systemPrompt:
|
|
149
|
+
systemPrompt: "Argue FOR the topic.",
|
|
150
150
|
sharedDb: { id: `debate-${ctx.entityUrl}`, schema: debateSchema },
|
|
151
151
|
},
|
|
152
|
-
{ initialMessage:
|
|
152
|
+
{ initialMessage: "The topic is: ...", wake: "runFinished" }
|
|
153
153
|
)
|
|
154
154
|
|
|
155
155
|
const con = await ctx.spawn(
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
"worker",
|
|
157
|
+
"debate-con",
|
|
158
158
|
{
|
|
159
|
-
systemPrompt:
|
|
159
|
+
systemPrompt: "Argue AGAINST the topic.",
|
|
160
160
|
sharedDb: { id: `debate-${ctx.entityUrl}`, schema: debateSchema },
|
|
161
161
|
},
|
|
162
|
-
{ initialMessage:
|
|
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:
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
97
|
-
ctx.send(
|
|
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
|
-
|
|
155
|
+
"worker",
|
|
156
156
|
id,
|
|
157
|
-
{ systemPrompt:
|
|
157
|
+
{ systemPrompt: "Summarise this data.", tools: ["read"] },
|
|
158
158
|
{
|
|
159
159
|
initialMessage: JSON.stringify(data),
|
|
160
|
-
wake:
|
|
160
|
+
wake: "runFinished",
|
|
161
161
|
}
|
|
162
162
|
)
|
|
163
163
|
```
|
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
|
|