@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
@@ -0,0 +1,354 @@
1
+ ---
2
+ title: MCP servers
3
+ titleTemplate: "... - Electric Agents"
4
+ description: >-
5
+ Connect agents to external tools, resources, and prompts via the
6
+ Model Context Protocol. Register servers programmatically through the
7
+ Registry API, declaratively in mcp.json, or globally in the desktop
8
+ app's settings.
9
+ outline: [2, 3]
10
+ ---
11
+
12
+ # MCP servers
13
+
14
+ The runtime ships an embedded **MCP registry** that connects agents to external [Model Context Protocol](https://modelcontextprotocol.io) servers — both locally-spawned `stdio` servers and remote `Streamable HTTP` servers. Tools, resources, and prompts exposed by those servers become available to every entity at the next wake without per-agent wiring.
15
+
16
+ ## Registering servers
17
+
18
+ `Registry` is the primary API. Agent authors call into it directly when they're defining or hosting agents in code. `mcp.json` and the desktop app's `settings.json` are file-based convenience layers that the runtime turns into the same `Registry.applyConfig()` calls under the hood.
19
+
20
+ ### Programmatic — `Registry.addServer()` / `applyConfig()`
21
+
22
+ `BuiltinAgentsServer` exposes the registry through `mcpRegistry`. Add servers from code anywhere it's the right shape — at boot from your own config source, in response to user actions, or per-session for tools an agent should only see during a specific task:
23
+
24
+ ```ts
25
+ import { BuiltinAgentsServer } from "@electric-ax/agents"
26
+
27
+ const server = new BuiltinAgentsServer({
28
+ agentServerUrl: "http://localhost:4437",
29
+ port: 4448,
30
+ workingDirectory: process.cwd(),
31
+ })
32
+
33
+ await server.start()
34
+
35
+ const result = await server.mcpRegistry?.addServer({
36
+ name: "stripe",
37
+ transport: "http",
38
+ url: "https://mcp.stripe.com/mcp",
39
+ auth: {
40
+ mode: "apiKey",
41
+ headerName: "Authorization",
42
+ key: process.env.STRIPE_MCP_KEY!,
43
+ },
44
+ })
45
+ ```
46
+
47
+ `addServer` returns a discriminated [`AddServerResult`](#addserverresult) — `{ state: "ready" | "authenticating" | "error", … }`. The state landscape is described in [Server states](#server-states) below; the full lifecycle (hot-reload, reauthorize, timeouts) lives in [Lifecycle](#lifecycle).
48
+
49
+ The bulk methods are:
50
+
51
+ - `applyConfig(cfg)` — replace the full set of servers. Idempotent on unchanged entries; removes anything not in the supplied config. This is what file-based config layers compile down to.
52
+ - `subscribe(handler)` — push-based view of the live state, including `ready` / `authenticating` / `error` transitions. Useful when an embedder renders its own UI on top of the registry.
53
+ - `reauthorize(name)`, `disable(name)`, `enable(name)`, `removeServer(name)` — single-server lifecycle.
54
+
55
+ Static secrets (`apiKey.key`, `clientCredentials.clientId` / `clientSecret`) are passed inline at the call site — typically read from `process.env`. The runtime never reads environment variables on the embedder's behalf. See [`McpServerConfig`](/docs/agents/reference/mcp-server-config) for the full schema.
56
+
57
+ ### File-based — `mcp.json`
58
+
59
+ For static, project-scoped configuration the runtime can load `mcp.json` from the configured `workingDirectory`, watch it for changes, and hot-reload adds, removes, and reconfigurations through `applyConfig` — exactly as if you'd called the API yourself. In-flight tool calls finish on the old config; new calls pick up the new one.
60
+
61
+ `mcp.json` loading is opt-in: stdio MCP servers spawn local commands, so picking a working directory must not auto-execute config from it. The Electron desktop and the `electric-ax` CLI opt in by default. Library embedders that construct `BuiltinAgentsServer` directly enable it with `loadProjectMcpConfig: true` (which loads `<workingDirectory>/mcp.json` and watches it).
62
+
63
+ `mcp.json` carries structural shape only — no secrets:
64
+
65
+ ```jsonc
66
+ {
67
+ "servers": {
68
+ "honeycomb": {
69
+ "transport": "http",
70
+ "url": "https://mcp.honeycomb.io/mcp",
71
+ "auth": {
72
+ "mode": "authorizationCode",
73
+ "scopes": ["mcp:read", "mcp:write"]
74
+ }
75
+ },
76
+ "internal-api": {
77
+ "transport": "http",
78
+ "url": "https://api.example.com/mcp",
79
+ "auth": {
80
+ "mode": "apiKey",
81
+ "headerName": "X-Api-Key"
82
+ }
83
+ },
84
+ "git-local": {
85
+ "transport": "stdio",
86
+ "command": "npx",
87
+ "args": [
88
+ "-y",
89
+ "@modelcontextprotocol/server-git",
90
+ "--repository",
91
+ "${workspaceRoot}"
92
+ ]
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ For [`authorizationCode`](#authorization-code-oauth) servers in `mcp.json`, the runtime auto-wires `keychainPersistence` so OAuth tokens survive process restarts via the OS keychain.
99
+
100
+ ### Desktop settings layer
101
+
102
+ The Electron desktop app exposes a second file-based layer: a global `mcp.servers` block in its `settings.json`, applied to every workspace. The shape mirrors `mcp.json` — keyed by server name — so entries can be copy-pasted between the two files. It composes with the workspace `mcp.json` instead of replacing it:
103
+
104
+ - Servers from both files load together when their names don't collide.
105
+ - On a name collision, the workspace `mcp.json` wins (project scope overrides global).
106
+ - `keychainPersistence` is auto-wired for OAuth servers from either source.
107
+
108
+ The `settings.json` lives at:
109
+
110
+ | OS | Path |
111
+ | ------- | ------------------------------------------------------- |
112
+ | macOS | `~/Library/Application Support/Electric Agents/` |
113
+ | Linux | `~/.config/Electric Agents/` |
114
+ | Windows | `%APPDATA%\Electric Agents\` |
115
+
116
+ Example shape:
117
+
118
+ ```jsonc
119
+ {
120
+ "servers": [...],
121
+ "activeServer": {...},
122
+ "workingDirectory": "/Users/me/workspace/foo",
123
+ "apiKeys": {...},
124
+ "mcp": {
125
+ "servers": {
126
+ "linear": {
127
+ "transport": "http",
128
+ "url": "https://mcp.linear.app/sse",
129
+ "auth": { "mode": "authorizationCode", "scopes": ["mcp:read"] }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ Programmatic embedders (other than the desktop) pass the resolved set as an array via `BuiltinAgentsServer({ extraMcpServers })` — that's the in-memory shape `settings.json` is rewritten into when the desktop loads it.
137
+
138
+ ## Per-agent allowlist
139
+
140
+ Entity definitions opt into MCP servers explicitly via the `mcp.tools()` helper from `@electric-ax/agents-runtime`:
141
+
142
+ ```ts
143
+ import { mcp } from "@electric-ax/agents-runtime"
144
+
145
+ registry.define("research-agent", {
146
+ async handler(ctx) {
147
+ ctx.useAgent({
148
+ systemPrompt: "...",
149
+ tools: [
150
+ ...ctx.electricTools,
151
+ ...mcp.tools(["sentry", "github"]), // explicit list
152
+ // or: ...mcp.tools() // every registered server
153
+ ],
154
+ })
155
+ await ctx.agent.run()
156
+ },
157
+ })
158
+ ```
159
+
160
+ The resolved tool set is recorded in the agent's manifest at compose time. Tools are exposed to the model with always-prefixed names matching Anthropic's tool-name regex (`^[a-zA-Z0-9_-]{1,128}$`):
161
+
162
+ - Tools: `mcp__sentry__search`, `mcp__github__create_issue`, …
163
+ - Resources: `mcp__<server>__list_resources`, `mcp__<server>__read_resource`
164
+ - Prompts: `mcp__<server>__list_prompts`, `mcp__<server>__get_prompt`
165
+
166
+ Built-in entities `horton` and `worker` opt in to all registered servers via `mcp.tools()`.
167
+
168
+ ## Auth modes
169
+
170
+ Each server declares one auth mode. The runtime keeps a valid token in hand on every call: silent refresh when possible, or a structured `auth_unavailable` error to the agent's model when not.
171
+
172
+ ### `apiKey`
173
+
174
+ ```ts
175
+ auth: {
176
+ mode: "apiKey",
177
+ key: process.env.X_API_KEY!,
178
+ headerName: "X-Api-Key", // default "Authorization"
179
+ valuePrefix: "Bearer ", // optional
180
+ }
181
+ ```
182
+
183
+ The header is sent on every request. Rotate by editing the config; the registry's idempotency check picks up the change and rebuilds the transport on the next reload.
184
+
185
+ ### `clientCredentials`
186
+
187
+ ```ts
188
+ auth: {
189
+ mode: "clientCredentials",
190
+ tokenUrl: "https://auth.example.com/oauth/token",
191
+ clientId: process.env.X_CLIENT_ID!,
192
+ clientSecret: process.env.X_CLIENT_SECRET!,
193
+ scopes: ["mcp:read"],
194
+ }
195
+ ```
196
+
197
+ The runtime exchanges the client credentials for short-lived access tokens silently. No user interaction.
198
+
199
+ ### `authorizationCode` (OAuth)
200
+
201
+ ```ts
202
+ auth: {
203
+ mode: "authorizationCode",
204
+ scopes: ["mcp:read"],
205
+ // optional — pre-registered OAuth client (skips DCR)
206
+ client: { clientId: "...", clientSecret: "..." },
207
+ // optional — pre-existing tokens (skips OAuth flow on boot)
208
+ tokens: { accessToken: "...", refreshToken: "...", expiresAt: 1736e9 },
209
+ // fires on initial auth + every refresh — wire to your persistence
210
+ onTokensChanged: async (t) => { /* persist */ },
211
+ // fires once after RFC 7591 DCR completes
212
+ onClientRegistered: async (c) => { /* persist */ },
213
+ }
214
+ ```
215
+
216
+ The MCP SDK handles PKCE, RFC 7591 Dynamic Client Registration, RFC 9728 Protected Resource Metadata discovery, and 401-retry transparently. The first time a server is used:
217
+
218
+ 1. The runtime captures an authorize URL and surfaces it through the `openAuthorizeUrl(url, server)` hook on `BuiltinAgentsServer`.
219
+ 2. The Electron desktop opens the URL in a sandboxed `BrowserWindow` and intercepts the `redirect_uri` navigation client-side — the redirect URL is never actually fetched, so no HTTP listener is needed.
220
+ 3. The runtime exchanges the captured `code` + `state` for tokens and fires `onTokensChanged`.
221
+
222
+ Subsequent restarts re-seed from persisted tokens; refresh-token rotation happens silently on every call.
223
+
224
+ The redirect URI registered with the auth server during DCR is `<mcpOAuthRedirectBase>/oauth/callback/<server-name>`. Embedders that listen on an ephemeral port (the desktop runs on `port: 0`) MUST pass a stable `mcpOAuthRedirectBase` to `BuiltinAgentsServer` — otherwise the cached DCR client info goes stale on every restart and users have to re-authorize every launch. The desktop sets it to a fixed loopback literal (`http://127.0.0.1:53117`) per RFC 8252 §7.3; nothing actually listens at the port. Headless embedders that use port 0 with persisted credentials need to do the same.
225
+
226
+ #### Persistence helpers
227
+
228
+ `@electric-ax/agents-mcp` ships two opt-in helpers that produce the auth-config slice:
229
+
230
+ ```ts
231
+ import { keychainPersistence, filePersistence } from "@electric-ax/agents-mcp"
232
+
233
+ const honeycomb = await keychainPersistence({ server: "honeycomb" })
234
+
235
+ await mcpRegistry.addServer({
236
+ name: "honeycomb",
237
+ transport: "http",
238
+ url: "https://mcp.honeycomb.io/mcp",
239
+ auth: {
240
+ mode: "authorizationCode",
241
+ scopes: ["mcp:read"],
242
+ ...honeycomb,
243
+ },
244
+ })
245
+ ```
246
+
247
+ | Helper | Backing store | When to use |
248
+ | ---------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------- |
249
+ | `keychainPersistence({ server })` | OS keychain (macOS `security`, Linux `secret-tool`) | Local dev / desktop apps; tokens encrypted by the OS |
250
+ | `filePersistence({ path, server })` | Mode-`0600` JSON file | CI / containers without an OS keychain |
251
+
252
+ For Vault, SSM, or a custom secret system, write your own `onTokensChanged` and `onClientRegistered` directly. The contract is two callbacks and two optional values.
253
+
254
+ ## Server states
255
+
256
+ Every server entry the registry tracks is in exactly one of five states. The state is the `status` field on `ListedEntry` (returned by `Registry.list()` and emitted on every snapshot through `subscribe`), and it's the discriminator on the `AddServerResult` envelope returned from `addServer` / `applyConfig` / `finishAuth` / `enable`.
257
+
258
+ | State | Meaning | Side data |
259
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
260
+ | `connecting` | Transport is being built (RFC 9728 discovery, RFC 7591 DCR, stdio spawn, HTTP handshake) or rebuilt after `reauthorize` / `enable`. | — |
261
+ | `authenticating` | An `authorizationCode` server needs the user. The SDK has produced an authorize URL; the embedder's `openAuthorizeUrl` hook fired. | `authUrl: string` |
262
+ | `ready` | Connected. Tools listed. Calls succeed and stream through the bridge. | `toolCount: number`, `tools: [...]` |
263
+ | `error` | Transport, auth-config, or `addServer` validation failure. The entry stays in `list()` so the UI can surface the failure. | `error: { kind, message, details? }` |
264
+ | `disabled` | Operator paused the server via `Registry.disable(name)`. Transport closed; tokens stay in the cache. | — |
265
+
266
+ Transitions are driven by registry methods. The high-level shape:
267
+
268
+ ```
269
+ ┌──────────────┐ success ┌──────────┐
270
+ addServer ──────▶│ connecting │───────────────▶│ ready │
271
+ applyConfig └──────┬───────┘ └────┬─────┘
272
+ enable │ │
273
+ │ no tokens / 401 │
274
+ ▼ │
275
+ ┌──────────────┐ finishAuth │
276
+ │authenticating│───────────────────▶─┘
277
+ └──────┬───────┘ (retries connect)
278
+
279
+ │ unrecoverable
280
+
281
+ ┌──────────────┐
282
+ │ error │
283
+ └──────────────┘
284
+
285
+ reauthorize: any non-disabled ──▶ connecting ──▶ authenticating
286
+ disable: any ──▶ disabled
287
+ enable: disabled ──▶ connecting ──▶ ready (or authenticating, or error)
288
+ removeServer: any ──▶ (entry gone)
289
+ ```
290
+
291
+ A few specifics worth knowing:
292
+
293
+ - **`error` is sticky.** It doesn't auto-recover. Reach `ready` again by calling `addServer` with the same config (idempotency picks up changes), `reauthorize(name)`, or — for transient transport issues — re-running through `applyConfig`. The entry stays in the snapshot the whole time.
294
+ - **`reauthorize` always lands in `connecting` first**, then typically `authenticating` because tokens are intentionally cleared. The mutation is in-place — subscribers never see the entry disappear, so renderers don't flicker.
295
+ - **`disable` is recoverable.** It closes the transport but keeps tokens, hooks, and the entry. `enable` rebuilds the transport from the same config; if tokens are still valid, the next state is `ready` without an OAuth round-trip.
296
+ - **`removeServer` is destructive.** It clears tokens from the in-memory cache (persisted tokens via `onTokensChanged` stay where the operator put them) and removes the entry. There is no UI affordance for it on the desktop — Disable is the recoverable equivalent.
297
+
298
+ For the full per-method API (including `subscribe`, `RegistrySnapshot`, and `RegistryOpts`), see the [`McpRegistry` reference](/docs/agents/reference/mcp-registry).
299
+
300
+ ## Lifecycle
301
+
302
+ ### Hot-reload
303
+
304
+ Editing `mcp.json` (or calling `applyConfig` programmatically) takes effect immediately:
305
+
306
+ - **New server.** Tools available at the next tool-selection step in any active wake; manifests of agents using `mcp.tools()` update at the next compose.
307
+ - **Removed server.** In-flight tool calls complete or fail cleanly; no new calls dispatch; stdio subprocesses terminate after in-flight calls drain.
308
+ - **Reconfigured server.** Takes effect on the next tool call to that server. In-flight calls finish on the old config.
309
+
310
+ `addServer` and `applyConfig` are idempotent on unchanged config — they compare by `(name, url, transport, authMode, scopes, timeoutMs, command, args)` and short-circuit when nothing changed. Spurious file-system events from macOS reload watchers won't tear down healthy connections.
311
+
312
+ ### Re-authorize
313
+
314
+ Calling `Registry.reauthorize(name)` forces a fresh OAuth flow without removing the entry from the registry. The transport is closed, tokens are dropped from the in-memory cache (hooks remain registered), and the SDK produces a new authorize URL that fires through the `openAuthorizeUrl` hook. The entry stays in every snapshot throughout, so subscribers don't see it disappear and reappear.
315
+
316
+ The desktop's **Authorize** button routes through this method. It's enabled when the server is in `authenticating` (initial sign-in) or `error` (recover from a stale-token state). Once the server is `ready` the same button label switches to **Re-authorize** and forces a fresh OAuth flow — useful when refresh-token rotation has stopped working and you want to re-bootstrap without removing the server.
317
+
318
+ ### Per-call timeouts
319
+
320
+ Every MCP tool call has a timeout (default 30 seconds, overridable per server via `timeoutMs`). When exceeded, the bridge cancels the call (JSON-RPC cancellation for stdio servers; HTTP request abort for HTTP servers) and resolves it with a `timeout` error result. The agent's model decides what to do — retry, fall back, abort.
321
+
322
+ The timeout is a hygiene feature, not a long-running-call solution. Tool calls in v1 are synchronous within the wake.
323
+
324
+ ## Connected Services UI (desktop)
325
+
326
+ The Electron desktop ships a **Settings → MCP Servers** page that mirrors `Registry.subscribe` over Electron IPC. Each row shows:
327
+
328
+ - **Name and transport** (stdio / http).
329
+ - **Auth mode** (apiKey / clientCredentials / authorizationCode).
330
+ - **Status** — `connecting`, `authenticating`, `ready`, `error`, or `disabled`.
331
+ - **Tool count + expandable tool list.**
332
+ - **Per-row actions:** Authorize (only when a server is in `authenticating`), Reconnect, Disable / Enable.
333
+
334
+ The page is the operator's primary mechanism for noticing and fixing broken credentials, and the developer's primary surface for kicking off initial OAuth flows. There is no Disconnect action: removal of an entry happens via editing the config file. Disable pauses without losing state and is recoverable from the UI.
335
+
336
+ ## Failure modes
337
+
338
+ The runtime returns a structured error to the agent's model on any tool-call failure it can't transparently recover from:
339
+
340
+ | Kind | Meaning |
341
+ | ------------------- | -------------------------------------------------------------------------------------- |
342
+ | `auth_unavailable` | Silent refresh failed and no credential is usable; operator must reauthorize. |
343
+ | `transport_error` | Server unreachable, connection dropped, malformed response. |
344
+ | `timeout` | Call exceeded its per-call timeout. |
345
+ | `server_error` | The MCP server returned a structured error. |
346
+ | `tool_not_found` | Capability mismatch (e.g. server's tool list changed since compose). |
347
+
348
+ Agents handle these like any other tool error: retry, fall back, give up gracefully, or escalate to the user. The runtime doesn't block tool calls indefinitely waiting for out-of-band recovery.
349
+
350
+ ## Reference
351
+
352
+ - [`McpRegistry`](/docs/agents/reference/mcp-registry) — full API: `addServer`, `applyConfig`, `subscribe`, `reauthorize`, the lifecycle, snapshot envelope, and `RegistryOpts` for custom embedders.
353
+ - [`McpServerConfig`](/docs/agents/reference/mcp-server-config) — schema for the `cfg` argument to `addServer` / `applyConfig`.
354
+ - [`BuiltinAgentsServer` options](/docs/agents/usage/embedded-builtins) — the `extraMcpServers` and `openAuthorizeUrl` options used to wire embedder-specific MCP behavior.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Overview
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  High level overview of the Electric Agents system and developer APIs.
6
6
  outline: [2, 3]
@@ -31,7 +31,7 @@ The context API passed into the handler:
31
31
 
32
32
  | Property/Method | Purpose |
33
33
  | ----------------------------------- | --------------------------------------------------------------------- |
34
- | `ctx.firstWake` | Boolean -- is this the entity's first activation? |
34
+ | `ctx.firstWake` | Boolean -- initial setup pass while no manifest entries exist |
35
35
  | `ctx.entityUrl` | Identity -- `/type/id` |
36
36
  | `ctx.entityType` | Type name string |
37
37
  | `ctx.args` | Readonly spawn arguments |
@@ -44,13 +44,14 @@ The context API passed into the handler:
44
44
  | `ctx.timelineMessages()` | Project the entity timeline into LLM messages |
45
45
  | `ctx.insertContext(id, entry)` | Insert a durable context entry |
46
46
  | `ctx.agent.run()` | Execute the agent loop |
47
- | `ctx.electricTools` | Runtime-provided tools to spread into agent config |
47
+ | `ctx.electricTools` | Runtime-provided tools to spread into agent config |
48
48
  | `ctx.spawn(type, id, args, opts)` | Create child entity |
49
49
  | `ctx.observe(source, opts)` | Subscribe to a source via `entity()`, `cron()`, `entities()`, `db()` |
50
50
  | `ctx.send(url, payload, opts)` | Send message to an entity |
51
51
  | `ctx.sleep()` | Return to idle |
52
52
  | `ctx.mkdb(id, schema)` | Create cross-entity shared state |
53
53
  | `ctx.observe(db(id, schema), opts)` | Join existing shared state |
54
+ | `ctx.recordRun()` | Record non-LLM work as a run for `runFinished` observers |
54
55
  | `ctx.setTag(key, value)` | Set a tag on this entity |
55
56
  | `ctx.removeTag(key)` | Remove a tag from this entity |
56
57
 
@@ -65,7 +66,9 @@ ctx.useAgent({
65
66
  provider?: KnownProvider, // defaults to 'anthropic' for string models
66
67
  tools: AgentTool[], // [...ctx.electricTools, ...custom]
67
68
  streamFn?: StreamFn, // optional streaming callback
68
- testResponses?: string[] // for testing without LLM
69
+ getApiKey?: (provider: string) => string | Promise<string> | undefined,
70
+ onPayload?: SimpleStreamOptions["onPayload"],
71
+ testResponses?: string[] | TestResponseFn // for testing without LLM
69
72
  })
70
73
  await ctx.agent.run() // blocks until agent finishes
71
74
  ```
@@ -78,15 +81,15 @@ See [Configuring the agent](/docs/agents/usage/configuring-the-agent) and [Agent
78
81
 
79
82
  ```ts
80
83
  const myTool: AgentTool = {
81
- name: 'calculator',
82
- label: 'Calculator',
83
- description: 'Evaluate a mathematical expression.',
84
+ name: "calculator",
85
+ label: "Calculator",
86
+ description: "Evaluate a mathematical expression.",
84
87
  parameters: Type.Object({ expression: Type.String() }), // TypeBox
85
88
  execute: async (_toolCallId, params) => {
86
89
  const { expression } = params as { expression: string }
87
90
  const result = evaluate(expression)
88
91
  return {
89
- content: [{ type: 'text', text: String(result) }],
92
+ content: [{ type: "text", text: String(result) }],
90
93
  details: {},
91
94
  }
92
95
  },
@@ -98,9 +101,9 @@ const myTool: AgentTool = {
98
101
  ```ts
99
102
  function createMemoryTool(ctx: HandlerContext): AgentTool {
100
103
  return {
101
- name: 'memory_store',
102
- label: 'Memory Store',
103
- description: 'Persist a key-value memory row.',
104
+ name: "memory_store",
105
+ label: "Memory Store",
106
+ description: "Persist a key-value memory row.",
104
107
  parameters: Type.Object({
105
108
  key: Type.String(),
106
109
  value: Type.String(),
@@ -108,7 +111,7 @@ function createMemoryTool(ctx: HandlerContext): AgentTool {
108
111
  execute: async (_, params) => {
109
112
  const { key, value } = params as { key: string; value: string }
110
113
  ctx.db.actions.memory_insert({ row: { key, value } }) // writes to entity state
111
- return { content: [{ type: 'text', text: 'Stored.' }], details: {} }
114
+ return { content: [{ type: "text", text: "Stored." }], details: {} }
112
115
  },
113
116
  }
114
117
  }
@@ -119,9 +122,9 @@ function createMemoryTool(ctx: HandlerContext): AgentTool {
119
122
  ```ts
120
123
  function createDispatchTool(ctx: HandlerContext): AgentTool {
121
124
  return {
122
- name: 'dispatch',
123
- label: 'Dispatch',
124
- description: 'Spawn a worker and return its text output.',
125
+ name: "dispatch",
126
+ label: "Dispatch",
127
+ description: "Spawn a worker and return its text output.",
125
128
  parameters: Type.Object({
126
129
  id: Type.String(),
127
130
  systemPrompt: Type.String(),
@@ -134,13 +137,13 @@ function createDispatchTool(ctx: HandlerContext): AgentTool {
134
137
  task: string
135
138
  }
136
139
  const child = await ctx.spawn(
137
- 'worker',
140
+ "worker",
138
141
  id,
139
- { systemPrompt, tools: ['read'] },
140
- { initialMessage: task, wake: 'runFinished' }
142
+ { systemPrompt, tools: ["read"] },
143
+ { initialMessage: task, wake: "runFinished" }
141
144
  )
142
- const text = (await child.text()).join('\n\n')
143
- return { content: [{ type: 'text', text }], details: {} }
145
+ const text = (await child.text()).join("\n\n")
146
+ return { content: [{ type: "text", text }], details: {} }
144
147
  },
145
148
  }
146
149
  }
@@ -172,6 +175,7 @@ See [Managing state](/docs/agents/usage/managing-state).
172
175
  - `opts.wake` -- `'runFinished'`, `{ on: 'runFinished', includeResponse? }`, or `{ on: 'change', collections?, debounceMs?, timeoutMs? }`
173
176
  - **`observe(source, opts)`** -> `EntityHandle | ObservationHandle` -- subscribe via `entity()`, `cron()`, `entities()`, `db()`
174
177
  - **`send(url, payload, opts)`** -- fire-and-forget message
178
+ - **`recordRun()`** -> `RunHandle` -- publish run lifecycle for external work
175
179
  - **`sleep()`** -- go idle
176
180
 
177
181
  **EntityHandle** returned from spawn/observe:
@@ -192,15 +196,15 @@ Define a schema map, then create/connect:
192
196
  const schema = {
193
197
  findings: {
194
198
  schema: z.object({ key: z.string(), text: z.string() }),
195
- type: 'shared:finding',
196
- primaryKey: 'key',
199
+ type: "shared:finding",
200
+ primaryKey: "key",
197
201
  },
198
202
  }
199
203
  // Parent creates:
200
- ctx.mkdb('research-123', schema)
204
+ ctx.mkdb("research-123", schema)
201
205
  // Children connect:
202
- const shared = await ctx.observe(db('research-123', schema))
203
- shared.findings.insert({ key: 'f1', text: '...' })
206
+ const shared = await ctx.observe(db("research-123", schema))
207
+ shared.findings.insert({ key: "f1", text: "..." })
204
208
  ```
205
209
 
206
210
  See [Shared state](/docs/agents/usage/shared-state) and [SharedStateHandle reference](/docs/agents/reference/shared-state-handle).
@@ -235,20 +239,21 @@ See [Built-in collections](/docs/agents/reference/built-in-collections).
235
239
 
236
240
  Interact with the system using the Electric Agents CLI:
237
241
 
238
- | Command | Purpose |
239
- | ----------------------------------------------- | -------------------------------- |
240
- | `electric agents types` | List registered entity types |
241
- | `electric agents types inspect <name>` | Show type schema |
242
- | `electric agents spawn /type/id --args '{...}'` | Create entity |
243
- | `electric agents send /type/id 'message'` | Send message |
244
- | `electric agents observe /type/id` | Stream entity events |
245
- | `electric agents inspect /type/id` | Show entity state |
246
- | `electric agents ps [--type --status --parent]` | List entities |
247
- | `electric agents kill /type/id` | Delete entity |
248
- | `electric agents start` | Start local dev environment |
249
- | `electric agents start-builtin` | Start built-in Horton runtime |
250
- | `electric agents quickstart` | Start local server and built-ins |
251
- | `electric agents stop` | Stop local dev environment |
242
+ | Command | Purpose |
243
+ | -------------------------------------------- | ---------------------------- |
244
+ | `electric agents types` | List registered entity types |
245
+ | `electric agents types inspect <name>` | Show type schema |
246
+ | `electric agents spawn /type/id --args '{...}'` | Create entity |
247
+ | `electric agents send /type/id 'message'` | Send message |
248
+ | `electric agents observe /type/id` | Stream entity events |
249
+ | `electric agents inspect /type/id` | Show entity state |
250
+ | `electric agents ps [--type --status --parent]` | List entities |
251
+ | `electric agents kill /type/id` | Delete entity |
252
+ | `electric agents start` | Start local dev environment |
253
+ | `electric agents start-builtin` | Start built-in Horton runtime |
254
+ | `electric agents quickstart` | Start local server and built-ins |
255
+ | `electric agents stop` | Stop local dev environment |
256
+ | `electric agents init [project-name]` | Scaffold a starter app |
252
257
 
253
258
  See [CLI reference](/docs/agents/reference/cli).
254
259
 
@@ -274,11 +279,11 @@ See [App setup](/docs/agents/usage/app-setup) and [RuntimeHandler reference](/do
274
279
 
275
280
  Use the client and embedding APIs when you need to work with agents outside an entity handler:
276
281
 
277
- | API | Use case |
278
- | ----------------------------- | ----------------------------------------------------------------- |
279
- | `createAgentsClient()` | Observe entity, membership, or shared-state streams from app code |
280
- | `useChat()` | Render an observed `EntityStreamDB` in React |
281
- | `createRuntimeServerClient()` | Spawn, message, delete, tag, and schedule entities from services |
282
- | `BuiltinAgentsServer` | Host Horton and worker in your own process |
282
+ | API | Use case |
283
+ | --------------------------------- | --------------------------------------------- |
284
+ | `createAgentsClient()` | Observe entity, membership, or shared-state streams from app code |
285
+ | `useChat()` | Render an observed `EntityStreamDB` in React |
286
+ | `createRuntimeServerClient()` | Spawn, message, delete, tag, and schedule entities from services |
287
+ | `BuiltinAgentsServer` | Host Horton and worker in your own process |
283
288
 
284
289
  See [Clients & React](/docs/agents/usage/clients-and-react), [Programmatic runtime client](/docs/agents/usage/programmatic-runtime-client), and [Embedded built-ins](/docs/agents/usage/embedded-builtins).