@electric-ax/agents 0.4.18 → 0.6.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 (45) hide show
  1. package/dist/entrypoint.js +88 -14
  2. package/dist/index.cjs +87 -13
  3. package/dist/index.d.cts +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +88 -14
  6. package/docs/entities/agents/horton.md +22 -17
  7. package/docs/entities/agents/worker.md +13 -6
  8. package/docs/entities/patterns/blackboard.md +1 -1
  9. package/docs/entities/patterns/dispatcher.md +1 -1
  10. package/docs/entities/patterns/manager-worker.md +10 -5
  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 +1 -1
  14. package/docs/index.md +6 -4
  15. package/docs/quickstart.md +2 -2
  16. package/docs/reference/agent-config.md +13 -3
  17. package/docs/reference/built-in-collections.md +128 -9
  18. package/docs/reference/cli.md +34 -4
  19. package/docs/reference/entity-definition.md +39 -7
  20. package/docs/reference/entity-handle.md +19 -1
  21. package/docs/reference/handler-context.md +130 -5
  22. package/docs/reference/runtime-handler.md +42 -14
  23. package/docs/reference/wake-event.md +29 -1
  24. package/docs/usage/app-setup.md +38 -7
  25. package/docs/usage/attachments.md +129 -0
  26. package/docs/usage/clients-and-react.md +23 -2
  27. package/docs/usage/configuring-the-agent.md +15 -5
  28. package/docs/usage/context-composition.md +2 -1
  29. package/docs/usage/defining-entities.md +9 -5
  30. package/docs/usage/defining-tools.md +1 -1
  31. package/docs/usage/embedded-builtins.md +82 -31
  32. package/docs/usage/managing-state.md +5 -0
  33. package/docs/usage/mcp-servers.md +16 -8
  34. package/docs/usage/overview.md +39 -14
  35. package/docs/usage/permissions-and-principals.md +160 -0
  36. package/docs/usage/programmatic-runtime-client.md +158 -16
  37. package/docs/usage/sandboxing.md +162 -0
  38. package/docs/usage/signals.md +138 -0
  39. package/docs/usage/spawning-and-coordinating.md +30 -11
  40. package/docs/usage/testing.md +1 -1
  41. package/docs/usage/waking-entities.md +34 -6
  42. package/docs/usage/webhook-sources.md +171 -0
  43. package/docs/usage/writing-handlers.md +13 -55
  44. package/docs/walkthrough.md +13 -5
  45. package/package.json +3 -3
@@ -0,0 +1,129 @@
1
+ ---
2
+ title: Attachments
3
+ titleTemplate: "... - Electric Agents"
4
+ description: >-
5
+ Upload, reference, read, and hydrate files and images for Electric Agents entities.
6
+ outline: [2, 3]
7
+ ---
8
+
9
+ # Attachments
10
+
11
+ Attachments are files associated with an entity. They are uploaded through entity routes, stored in private attachment streams, and referenced by `manifest` rows on the entity stream.
12
+
13
+ Attachments are useful for image inputs, user-uploaded files, generated artifacts, and tool outputs that should be tracked alongside the entity timeline.
14
+
15
+ ## Upload from clients
16
+
17
+ Use `createRuntimeServerClient().createAttachment()`:
18
+
19
+ ```ts
20
+ import { createRuntimeServerClient } from "@electric-ax/agents-runtime"
21
+
22
+ const client = createRuntimeServerClient({
23
+ baseUrl: "http://localhost:4437",
24
+ principalKey: "user:sam",
25
+ })
26
+
27
+ const { attachment } = await client.createAttachment({
28
+ entityUrl: "/horton/onboarding",
29
+ attachment: {
30
+ bytes: imageBytes,
31
+ mimeType: "image/png",
32
+ filename: "screenshot.png",
33
+ subject: { type: "inbox", key: "message-1" },
34
+ role: "input",
35
+ meta: { source: "upload" },
36
+ },
37
+ })
38
+ ```
39
+
40
+ The server writes a manifest entry like:
41
+
42
+ ```ts
43
+ interface ManifestAttachmentEntry {
44
+ kind: "attachment"
45
+ id: string
46
+ streamPath: string
47
+ status: "pending" | "complete" | "failed"
48
+ subject: {
49
+ type: "inbox" | "run" | "text" | "tool_call" | "context"
50
+ key: string
51
+ }
52
+ role: "input" | "output"
53
+ mimeType: string
54
+ filename?: string
55
+ byteLength?: number
56
+ sha256?: string
57
+ createdAt: string
58
+ createdBy?: string
59
+ error?: string
60
+ meta?: Record<string, JsonValue>
61
+ }
62
+ ```
63
+
64
+ ## Read from clients
65
+
66
+ Read bytes by entity URL and attachment id:
67
+
68
+ ```ts
69
+ const bytes = await client.readAttachment({
70
+ entityUrl: "/horton/onboarding",
71
+ id: attachment.id,
72
+ })
73
+ ```
74
+
75
+ The caller needs read access to the entity.
76
+
77
+ ## Handler API
78
+
79
+ Handlers access attachments through `ctx.attachments`:
80
+
81
+ ```ts
82
+ async handler(ctx) {
83
+ const inputs = ctx.attachments.list({ role: "input" })
84
+ const first = inputs[0]
85
+ if (!first) return
86
+
87
+ const bytes = await ctx.attachments.read(first.id)
88
+ // Use bytes in a custom tool or external API call.
89
+ }
90
+ ```
91
+
92
+ Available operations:
93
+
94
+ | Method | Purpose |
95
+ | ------ | ------- |
96
+ | `list(filter?)` | List manifest-backed attachments, optionally by role or subject. |
97
+ | `get(id)` | Return one attachment manifest entry by id. |
98
+ | `read(id)` | Read attachment bytes. |
99
+ | `create(input)` | Create a new attachment associated with this entity. |
100
+
101
+ ## Subjects and roles
102
+
103
+ The `subject` links an attachment to the timeline object it belongs to:
104
+
105
+ | Subject type | Typical use |
106
+ | ------------ | ----------- |
107
+ | `inbox` | User-uploaded input attached to a message |
108
+ | `run` | Artifact associated with an agent run |
109
+ | `text` | File linked to generated text |
110
+ | `tool_call` | Tool input or output artifact |
111
+ | `context` | Durable context material |
112
+
113
+ `role` is either `input` or `output`. Input attachments are usually supplied by users or the host app. Output attachments are usually created by handlers or tools.
114
+
115
+ ## Images in agent context
116
+
117
+ When image attachments are associated with inbox messages, the runtime can hydrate supported image inputs into model messages. The UI should hide image upload controls for models that do not advertise image input support.
118
+
119
+ To keep context bounded, image hydration uses newest-first byte/count guardrails. Large or older images may remain as attachment descriptors rather than inline model content.
120
+
121
+ ## Failure and rollback
122
+
123
+ Attachment uploads can fail independently of message sends. UI flows should roll back uploaded attachments if the send that references them fails, or leave an explicit failed manifest row when the failure should be visible to the entity.
124
+
125
+ ## Related APIs
126
+
127
+ - [`HandlerContext`](../reference/handler-context) documents `ctx.attachments`.
128
+ - [`Built-in collections`](../reference/built-in-collections) documents attachment manifest rows.
129
+ - [`Programmatic runtime client`](./programmatic-runtime-client) documents `createAttachment()` and `readAttachment()`.
@@ -17,10 +17,10 @@ Use the client APIs when you need to observe agents from application code rather
17
17
 
18
18
  ```ts
19
19
  import {
20
- codingSession,
21
20
  createAgentsClient,
22
21
  entity,
23
22
  entities,
23
+ pgSync,
24
24
  } from "@electric-ax/agents-runtime"
25
25
 
26
26
  const client = createAgentsClient({ baseUrl: "http://localhost:4437" })
@@ -43,17 +43,27 @@ console.log(membersDb.collections.members.toArray)
43
43
  interface AgentsClientConfig {
44
44
  baseUrl: string
45
45
  fetch?: typeof globalThis.fetch
46
+ principalKey?: string
46
47
  }
47
48
 
48
49
  interface AgentsClient {
49
50
  observe(
50
51
  source: ObservationSource
51
52
  ): Promise<EntityStreamDB | ObservationStreamDB>
53
+ signal(options: {
54
+ entityUrl: string
55
+ signal: EntitySignal
56
+ reason?: string
57
+ payload?: unknown
58
+ }): Promise<{ txid: number }>
59
+ kill(entityUrl: string, reason?: string): Promise<{ txid: number }>
52
60
  }
53
61
  ```
54
62
 
55
63
  `observe(entity(url))` returns an `EntityStreamDB`. `observe(entities(...))` and `observe(db(...))` return an `ObservationStreamDB`.
56
64
 
65
+ Use `principalKey` when observing or signalling against a server that enforces principal-scoped access.
66
+
57
67
  :::: warning
58
68
  `client.observe(cron(...))` is not currently supported. Use cron sources from handler wake subscriptions, or schedule tools exposed through `ctx.electricTools`.
59
69
  ::::
@@ -67,12 +77,23 @@ The same source helpers used by `ctx.observe()` can be used with `AgentsClient`.
67
77
  | `entity(url)` | Observe one entity by URL. |
68
78
  | `entities({ tags })` | Observe the entity membership stream matching tags. |
69
79
  | `db(id, schema)` | Observe a shared-state stream. |
80
+ | `webhook(endpointKey, opts?)` | Observe a webhook-backed stream. |
81
+ | `pgSync(options)` | Observe an Electric Postgres shape stream. |
70
82
  | `cron(expression)` | Build a cron source for wake subscriptions. |
71
83
 
72
84
  ```ts
73
- import { db } from "@electric-ax/agents-runtime"
85
+ import { db, pgSync } from "@electric-ax/agents-runtime"
74
86
 
75
87
  const shared = await client.observe(db("research-123", researchSchema))
88
+
89
+ const todos = await client.observe(
90
+ pgSync({
91
+ url: "http://localhost:3000/v1/shape",
92
+ table: "todos",
93
+ where: "project_id = $1",
94
+ params: ["docs"],
95
+ })
96
+ )
76
97
  ```
77
98
 
78
99
  ## React useChat
@@ -16,11 +16,18 @@ Call `ctx.useAgent()` in your handler to set up the LLM, then `ctx.agent.run()`
16
16
  interface AgentConfig {
17
17
  systemPrompt: string
18
18
  model: string | Model<any>
19
- provider?: KnownProvider
19
+ provider?: Provider
20
20
  tools: AgentTool[]
21
21
  streamFn?: StreamFn
22
22
  getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined
23
23
  onPayload?: SimpleStreamOptions["onPayload"]
24
+ onStepEnd?: (stats: {
25
+ input: number
26
+ uncachedInput: number
27
+ output: number
28
+ }) => void
29
+ modelTimeoutMs?: number
30
+ modelMaxRetries?: number
24
31
  testResponses?: string[] | TestResponseFn
25
32
  }
26
33
  ```
@@ -29,11 +36,14 @@ interface AgentConfig {
29
36
  | --------------- | -------- | ----------------------------------------------------------------------- |
30
37
  | `systemPrompt` | Yes | The system prompt passed to the LLM. |
31
38
  | `model` | Yes | Model identifier string or resolved model object. |
32
- | `provider` | No | Provider to use when `model` is a string. Defaults to `"anthropic"`. |
39
+ | `provider` | No | pi-ai provider to use when `model` is a string. Defaults to `"anthropic"`. |
33
40
  | `tools` | Yes | Array of tools available to the agent. Spread `ctx.electricTools` when your runtime host provides runtime-level tools. |
34
41
  | `streamFn` | No | Optional streaming callback passed to the underlying agent. |
35
42
  | `getApiKey` | No | Optional API-key resolver passed through to the model layer. |
36
43
  | `onPayload` | No | Optional callback for raw streaming payloads from the model layer. |
44
+ | `onStepEnd` | No | Callback after each model step with provider-reported token counts. |
45
+ | `modelTimeoutMs` | No | Timeout for individual model calls, in milliseconds. |
46
+ | `modelMaxRetries` | No | Maximum retry count for model calls. |
37
47
  | `testResponses` | No | Mock responses for testing without calling the LLM. |
38
48
 
39
49
  ## Basic usage
@@ -42,7 +52,7 @@ interface AgentConfig {
42
52
  async handler(ctx) {
43
53
  ctx.useAgent({
44
54
  systemPrompt: 'You are a helpful assistant.',
45
- model: 'claude-sonnet-4-5-20250929',
55
+ model: 'claude-sonnet-4-6',
46
56
  tools: [...ctx.electricTools],
47
57
  })
48
58
  await ctx.agent.run()
@@ -106,7 +116,7 @@ You must call `useAgent` before calling `run()`. Calling `ctx.agent.run()` witho
106
116
  When `model` is a string, the runtime resolves it through the configured `provider` (default `"anthropic"`). You can also pass a resolved `Model` object directly.
107
117
 
108
118
  ```ts
109
- model: "claude-sonnet-4-5-20250929"
119
+ model: "claude-sonnet-4-6"
110
120
  provider: "anthropic"
111
121
  ```
112
122
 
@@ -119,7 +129,7 @@ For testing handlers without making LLM calls, pass `testResponses`. Two forms a
119
129
  ```ts
120
130
  ctx.useAgent({
121
131
  systemPrompt: "...",
122
- model: "claude-sonnet-4-5-20250929",
132
+ model: "claude-sonnet-4-6",
123
133
  tools: [...ctx.electricTools],
124
134
  testResponses: ["Hello! How can I help?", "Sure, I can do that."],
125
135
  })
@@ -17,6 +17,7 @@ Most entities don't need `useContext` -- the default timeline assembly works wel
17
17
  - **Budget token space** across multiple content sources (docs, conversation history, retrieved context)
18
18
  - **Mix static and dynamic content** with different caching behavior
19
19
  - **Inject external content** (documentation, search results, knowledge bases) alongside conversation history
20
+ - **Hydrate uploaded files or images** through manifest-backed [attachments](./attachments)
20
21
 
21
22
  ## UseContextConfig
22
23
 
@@ -190,7 +191,7 @@ async handler(ctx, wake) {
190
191
 
191
192
  ctx.useAgent({
192
193
  systemPrompt: "You are a helpful assistant.",
193
- model: "claude-sonnet-4-5-20250929",
194
+ model: "claude-sonnet-4-6",
194
195
  tools,
195
196
  })
196
197
  await ctx.agent.run()
@@ -24,7 +24,7 @@ registry.define("assistant", {
24
24
  async handler(ctx) {
25
25
  ctx.useAgent({
26
26
  systemPrompt: "You are a helpful assistant.",
27
- model: "claude-sonnet-4-5-20250929",
27
+ model: "claude-sonnet-4-6",
28
28
  tools: [...ctx.electricTools],
29
29
  })
30
30
  await ctx.agent.run()
@@ -45,7 +45,8 @@ interface EntityDefinition {
45
45
  ) => Record<string, (...args: unknown[]) => void>
46
46
  creationSchema?: StandardJSONSchemaV1
47
47
  inboxSchemas?: Record<string, StandardJSONSchemaV1>
48
- outputSchemas?: Record<string, StandardJSONSchemaV1>
48
+ stateSchemas?: Record<string, StandardJSONSchemaV1>
49
+ permissionGrants?: EntityTypePermissionGrantDefinition[]
49
50
  handler: (ctx: HandlerContext, wake: WakeEvent) => void | Promise<void>
50
51
  }
51
52
  ```
@@ -57,9 +58,12 @@ interface EntityDefinition {
57
58
  | `actions` | Factory that returns custom non-CRUD action functions exposed on `ctx.actions`. |
58
59
  | `creationSchema` | JSON Schema for arguments passed when the entity is spawned. |
59
60
  | `inboxSchemas` | JSON Schemas for typed inbox message categories. |
60
- | `outputSchemas` | JSON Schemas for typed output message categories. |
61
+ | `stateSchemas` | Additional JSON Schemas registered with the entity type's state schema map. |
62
+ | `permissionGrants` | Initial permission grants applied when this entity type is registered. |
61
63
  | `handler` | The function that runs each time the entity wakes. Required. |
62
64
 
65
+ See [Permissions & principals](./permissions-and-principals) for the access-control model behind `permissionGrants`.
66
+
63
67
  ## Custom state
64
68
 
65
69
  Declare named collections in the `state` field. Each collection is a `CollectionDefinition`:
@@ -145,7 +149,7 @@ export function registerAssistant(registry: EntityRegistry) {
145
149
  async handler(ctx) {
146
150
  ctx.useAgent({
147
151
  systemPrompt: "You are a helpful assistant.",
148
- model: "claude-sonnet-4-5-20250929",
152
+ model: "claude-sonnet-4-6",
149
153
  tools: [...ctx.electricTools],
150
154
  })
151
155
  await ctx.agent.run()
@@ -158,7 +162,7 @@ This keeps each entity type isolated and the registry composition explicit.
158
162
 
159
163
  ## Schemas
160
164
 
161
- `creationSchema`, `inboxSchemas`, and `outputSchemas` accept [`StandardJSONSchemaV1`](https://github.com/standard-schema/standard-schema) objects. Any schema library implementing the Standard JSON Schema interface works (e.g. Zod v4). These schemas are used for validation and for generating UI and documentation in the Electric Agents dashboard.
165
+ `creationSchema`, `inboxSchemas`, and `stateSchemas` accept [`StandardJSONSchemaV1`](https://github.com/standard-schema/standard-schema) objects. Any schema library implementing the Standard JSON Schema interface works (e.g. Zod v4). These schemas are used for validation and for generating UI and documentation in the Electric Agents dashboard.
162
166
 
163
167
  ```ts
164
168
  import { z } from "zod/v4"
@@ -217,7 +217,7 @@ registry.define("assistant", {
217
217
 
218
218
  ctx.useAgent({
219
219
  systemPrompt: "You are a helpful assistant with persistent memory.",
220
- model: "claude-sonnet-4-5-20250929",
220
+ model: "claude-sonnet-4-6",
221
221
  tools: [...ctx.electricTools, memoryTool, dispatchTool, calculatorTool],
222
222
  })
223
223
  await ctx.agent.run()
@@ -13,21 +13,24 @@ The CLI commands `electric agents start-builtin` and `electric agents quickstart
13
13
 
14
14
  ## BuiltinAgentsServer
15
15
 
16
- `BuiltinAgentsServer` starts an HTTP webhook server, registers `horton` and `worker`, and forwards Electric Agents webhook wakes to the built-in handler.
16
+ `BuiltinAgentsServer` registers `horton` and `worker`, advertises the runtime's sandbox profiles, and starts a pull-wake runner that claims wakes from the Electric Agents server. This is the same model used by the CLI and desktop app.
17
17
 
18
18
  ```ts
19
19
  import { BuiltinAgentsServer } from "@electric-ax/agents"
20
20
 
21
21
  const server = new BuiltinAgentsServer({
22
22
  agentServerUrl: "http://localhost:4437",
23
- port: 4448,
24
23
  workingDirectory: process.cwd(),
24
+ loadProjectMcpConfig: true,
25
+ pullWake: {
26
+ runnerId: "builtin-agents",
27
+ ownerPrincipal: "/principal/system%3Abuiltin-agents",
28
+ registerRunner: true,
29
+ },
25
30
  })
26
31
 
27
- await server.start()
28
-
29
- console.log(server.url)
30
- console.log(server.registeredBaseUrl)
32
+ const runtimeUrl = await server.start()
33
+ console.log(runtimeUrl) // "pull-wake:builtin-agents"
31
34
 
32
35
  // Later, during shutdown:
33
36
  await server.stop()
@@ -42,12 +45,23 @@ type CreateElectricTools = RuntimeRouterConfig["createElectricTools"]
42
45
 
43
46
  interface BuiltinAgentsServerOptions {
44
47
  agentServerUrl: string
45
- baseUrl?: string
46
- port: number
47
- host?: string
48
48
  workingDirectory?: string
49
49
  mockStreamFn?: StreamFn
50
- webhookPath?: string
50
+ durableStreamsFetchCache?: DurableStreamsFetchCacheOptions | false
51
+ pullWake: {
52
+ runnerId: string
53
+ ownerPrincipal?: string
54
+ label?: string
55
+ registerRunner?: boolean
56
+ headers?: HeadersProvider
57
+ claimHeaders?: HeadersProvider
58
+ claimTokenHeader?: ClaimTokenHeader
59
+ heartbeatIntervalMs?: number
60
+ eventHeartbeatThrottleMs?: number
61
+ leaseMs?: number
62
+ }
63
+ enabledModelValues?: readonly string[] | null
64
+ baseSkillsDir?: string
51
65
  createElectricTools?: CreateElectricTools
52
66
  // MCP integration
53
67
  extraMcpServers?: ReadonlyArray<McpServerConfig>
@@ -61,24 +75,49 @@ interface BuiltinAgentsServerOptions {
61
75
  | Field | Description |
62
76
  | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63
77
  | `agentServerUrl` | Electric Agents coordinator server URL. |
64
- | `baseUrl` | Public base URL used when registering the webhook. Defaults to local URL. |
65
- | `port` | Local webhook server port. |
66
- | `host` | Bind host. Defaults to `127.0.0.1`. |
67
78
  | `workingDirectory` | Directory used by Horton and worker file tools. Defaults to `process.cwd()`. |
68
- | `mockStreamFn` | Optional test stream function. Lets you run without `ANTHROPIC_API_KEY`. |
69
- | `webhookPath` | Webhook path. Defaults to `/_electric/builtin-agent-handler`. |
70
- | `createElectricTools` | Optional factory for extra tools injected into built-in agent handlers. |
79
+ | `mockStreamFn` | Optional test stream function. Lets you run without a real model provider. |
80
+ | `durableStreamsFetchCache` | Optional process-wide HTTP cache tuning for Undici-backed fetch calls. Pass `false` to leave the global dispatcher unchanged. |
81
+ | `pullWake` | Pull-wake runner configuration. `runnerId` identifies this runtime to the server. Set `registerRunner: true` when this process should create/update the runner record. |
82
+ | `enabledModelValues` | Optional allowlist of model values exposed by built-in agent creation schemas. Values use the model catalog's `provider:model` form. |
83
+ | `baseSkillsDir` | Override for the bundled skills directory, useful when an embedder packages `@electric-ax/agents`. |
84
+ | `createElectricTools` | Optional factory for tools injected into built-in agent handlers. The default built-in factory includes webhook-source and schedule tools; override or wrap it when adding host-specific tools. |
71
85
  | `extraMcpServers` | MCP servers contributed by the embedder. On name conflict with `mcp.json`, `mcp.json` wins. `authorizationCode` servers are auto-wired with `keychainPersistence`. |
72
- | `loadProjectMcpConfig` | Load `<workingDirectory>/mcp.json` (and watch it). Off by default stdio MCP servers can spawn local commands, so the embedder must opt in. The Electron desktop and `electric-ax` CLI opt in. |
73
- | `mcpOAuthRedirectBase` | Base for OAuth redirect URIs (full URI is `<base>/oauth/callback/<server-name>`). MUST be stable across restarts so DCR client info stays valid; required when listening on `port: 0`. The runtime never listens at this URI the embedder intercepts the redirect. |
86
+ | `loadProjectMcpConfig` | Load `<workingDirectory>/mcp.json` (and watch it). Off by default because stdio MCP servers can spawn local commands, so embedders must opt in. The Electron desktop and `electric-ax` CLI opt in. |
87
+ | `mcpOAuthRedirectBase` | Base for OAuth redirect URIs (full URI is `<base>/oauth/callback/<server-name>`). Must be stable across restarts so DCR client info stays valid. The runtime never listens at this URI; the embedder intercepts the redirect. |
74
88
  | `openAuthorizeUrl` | Hook invoked when an `authorizationCode` MCP server first needs user consent. Receives the SDK-generated authorize URL. The desktop opens it in a sandboxed `BrowserWindow`; headless embedders can read the URL from the `authenticating` envelope of `addServer` and surface it themselves. |
75
89
  | `onConfigError` | Invoked when applying an MCP config (initial boot or watcher reload) fails. Errors are always logged; this hook is for surfacing them programmatically. |
76
90
 
77
- Without `mockStreamFn`, `ANTHROPIC_API_KEY` must be present before the built-in handler starts.
91
+ Without `mockStreamFn`, at least one supported provider must be configured before the built-in handler starts: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `DEEPSEEK_API_KEY`, `MOONSHOT_API_KEY`, or a valid OpenAI Codex CLI auth file for the `openai-codex` provider.
92
+
93
+ ### Pull-wake headers and principals
94
+
95
+ When the server enforces principals or auth, pass the same headers to runner registration and wake claims:
96
+
97
+ ```ts
98
+ const serverHeaders = {
99
+ "electric-principal": "service:local-runtime",
100
+ authorization: `Bearer ${process.env.ELECTRIC_AGENTS_TOKEN}`,
101
+ }
102
+
103
+ const server = new BuiltinAgentsServer({
104
+ agentServerUrl: "http://localhost:4437",
105
+ pullWake: {
106
+ runnerId: "local-runtime",
107
+ ownerPrincipal: "/principal/service%3Alocal-runtime",
108
+ registerRunner: true,
109
+ headers: serverHeaders,
110
+ claimHeaders: serverHeaders,
111
+ claimTokenHeader: "electric-claim-token",
112
+ },
113
+ })
114
+ ```
115
+
116
+ Use `claimTokenHeader: "electric-claim-token"` when your `authorization` header is reserved for server auth. Otherwise the default claim token transport is the standard `Authorization: Bearer <claim-token>` header.
78
117
 
79
118
  ## createBuiltinAgentHandler
80
119
 
81
- Use `createBuiltinAgentHandler()` when you already have an HTTP server and only need the request handler and runtime objects.
120
+ Use `createBuiltinAgentHandler()` when you need the lower-level registry/runtime objects. If you pass `serveEndpoint`, `registerTypes()` registers webhook dispatch for the built-in types. If you are using pull-wake, prefer `BuiltinAgentsServer`, which wires runner registration, MCP, sandbox profiles, and wake claiming for you.
82
121
 
83
122
  ```ts
84
123
  import {
@@ -93,7 +132,7 @@ const bootstrap = await createBuiltinAgentHandler({
93
132
  })
94
133
 
95
134
  if (!bootstrap) {
96
- throw new Error("ANTHROPIC_API_KEY is required for built-in agents")
135
+ throw new Error("No supported model provider is configured")
97
136
  }
98
137
 
99
138
  await registerBuiltinAgentTypes(bootstrap)
@@ -111,19 +150,29 @@ interface AgentHandlerResult {
111
150
  registry: EntityRegistry
112
151
  typeNames: string[]
113
152
  skillsRegistry: SkillsRegistry | null
153
+ shutdownSandboxes: (() => Promise<void>) | null
154
+ modelCatalog: BuiltinModelCatalog
114
155
  }
115
156
  ```
116
157
 
158
+ Call `shutdownSandboxes()` during process shutdown when it is present. `modelCatalog` is the catalog used by the built-in Horton and Worker definitions; pass it along if you register sibling built-in-style types directly with `registerHorton()` or `registerWorker()`. Most embedders should use `registerBuiltinAgentTypes()` instead.
159
+
117
160
  ## Extra Electric Tools
118
161
 
119
162
  Both `BuiltinAgentsServer` and `createBuiltinAgentHandler()` accept `createElectricTools`. The factory receives the same context shape as `RuntimeRouterConfig.createElectricTools` and can add host-specific tools to Horton.
120
163
 
164
+ If you do not provide a custom factory, the built-in runtime injects webhook-source tools and schedule tools (`list_schedules`, `upsert_cron_schedule`, `upsert_future_send`, and `delete_schedule`). If you replace the factory entirely, include those tools yourself when Horton should keep that behavior.
165
+
121
166
  ```ts
167
+ import { BuiltinAgentsServer } from "@electric-ax/agents"
122
168
  import { Type } from "@sinclair/typebox"
123
169
 
124
170
  const server = new BuiltinAgentsServer({
125
171
  agentServerUrl: "http://localhost:4437",
126
- port: 4448,
172
+ pullWake: {
173
+ runnerId: "builtin-agents",
174
+ registerRunner: true,
175
+ },
127
176
  createElectricTools: ({ entityUrl, upsertCronSchedule }) => [
128
177
  {
129
178
  name: "schedule_daily_summary",
@@ -165,15 +214,17 @@ await server.stop()
165
214
 
166
215
  Environment variables:
167
216
 
168
- | Variable | Description |
169
- | -------------------------------- | ----------------------------------------------------- |
170
- | `ELECTRIC_AGENTS_SERVER_URL` | Required coordinator server URL. |
171
- | `ELECTRIC_AGENTS_PRINCIPAL` | Optional principal key sent as `Electric-Principal`. |
172
- | `ELECTRIC_AGENTS_SERVER_HEADERS` | Optional JSON object of additional server headers. |
173
- | `ELECTRIC_AGENTS_BUILTIN_BASE_URL` | Public webhook base URL for the built-in server. |
174
- | `ELECTRIC_AGENTS_BUILTIN_HOST` | Bind host. |
175
- | `ELECTRIC_AGENTS_BUILTIN_PORT` | Built-in server port. Defaults to `4448`. |
176
- | `ELECTRIC_AGENTS_WORKING_DIRECTORY` | Working directory for file tools. |
217
+ | Variable | Description |
218
+ | ------------------------------------------ | --------------------------------------------------------------------------- |
219
+ | `ELECTRIC_AGENTS_SERVER_URL` | Required coordinator server URL. |
220
+ | `ELECTRIC_AGENTS_BASE_URL` | Legacy alias for `ELECTRIC_AGENTS_SERVER_URL`. |
221
+ | `ELECTRIC_AGENTS_PULL_WAKE_RUNNER_ID` | Required pull-wake runner id. |
222
+ | `PULL_WAKE_RUNNER_ID` | Legacy alias for `ELECTRIC_AGENTS_PULL_WAKE_RUNNER_ID`. |
223
+ | `ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER` | Set to `true` or `1` to register/update the runner record before claiming. |
224
+ | `ELECTRIC_AGENTS_PRINCIPAL` | Optional principal key sent as `Electric-Principal`. |
225
+ | `ELECTRIC_AGENTS_SERVER_HEADERS` | Optional JSON object of additional server headers. |
226
+ | `ELECTRIC_AGENTS_WORKING_DIRECTORY` | Working directory for file tools. |
227
+ | `WORKING_DIRECTORY` | Legacy alias for `ELECTRIC_AGENTS_WORKING_DIRECTORY`. |
177
228
 
178
229
  ## Built-in Agent APIs
179
230
 
@@ -42,11 +42,16 @@ interface CollectionDefinition {
42
42
  schema?: StandardSchemaV1 // Zod or any Standard Schema validator
43
43
  type?: string // Event type in the stream. Defaults to "state:{name}"
44
44
  primaryKey?: string // Key field. Defaults to "key"
45
+ externallyWritable?: boolean // Opt in to HTTP writes for this collection
46
+ contract?: string // Well-known contract implemented by this collection
47
+ operations?: Array<"insert" | "update" | "delete"> // External write allowlist
45
48
  }
46
49
  ```
47
50
 
48
51
  All fields are optional. A minimal collection like `{ primaryKey: 'key' }` works without a schema — rows are untyped.
49
52
 
53
+ Set `externallyWritable: true` only for collections that should accept writes through the entity collection HTTP routes. When enabled, `operations` controls which external write operations are allowed; if omitted, the server permits inserts only.
54
+
50
55
  ## Writing and reading state
51
56
 
52
57
  Use `ctx.state.<collection>` for normal handler code. Its `insert`, `update`, and `delete` methods route through generated actions; its `get` and `toArray` members read from the underlying TanStack DB collection.
@@ -26,8 +26,11 @@ import { BuiltinAgentsServer } from "@electric-ax/agents"
26
26
 
27
27
  const server = new BuiltinAgentsServer({
28
28
  agentServerUrl: "http://localhost:4437",
29
- port: 4448,
30
29
  workingDirectory: process.cwd(),
30
+ pullWake: {
31
+ runnerId: "builtin-agents",
32
+ registerRunner: true,
33
+ },
31
34
  })
32
35
 
33
36
  await server.start()
@@ -44,7 +47,7 @@ const result = await server.mcpRegistry?.addServer({
44
47
  })
45
48
  ```
46
49
 
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).
50
+ `addServer` returns a discriminated [`AddServerResult`](/docs/agents/reference/mcp-registry#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
51
 
49
52
  The bulk methods are:
50
53
 
@@ -95,7 +98,7 @@ For static, project-scoped configuration the runtime can load `mcp.json` from th
95
98
  }
96
99
  ```
97
100
 
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.
101
+ For [`authorizationCode`](#authorizationcode-oauth) servers in `mcp.json`, the runtime auto-wires `keychainPersistence` so OAuth tokens survive process restarts via the OS keychain.
99
102
 
100
103
  ### Desktop settings layer
101
104
 
@@ -117,10 +120,15 @@ Example shape:
117
120
 
118
121
  ```jsonc
119
122
  {
120
- "servers": [...],
121
- "activeServer": {...},
123
+ "servers": [
124
+ {
125
+ "id": "local",
126
+ "name": "Local",
127
+ "url": "http://localhost:4437"
128
+ }
129
+ ],
130
+ "defaultServerId": "local",
122
131
  "workingDirectory": "/Users/me/workspace/foo",
123
- "apiKeys": {...},
124
132
  "mcp": {
125
133
  "servers": {
126
134
  "linear": {
@@ -137,10 +145,10 @@ Programmatic embedders (other than the desktop) pass the resolved set as an arra
137
145
 
138
146
  ## Per-agent allowlist
139
147
 
140
- Entity definitions opt into MCP servers explicitly via the `mcp.tools()` helper from `@electric-ax/agents-runtime`:
148
+ Entity definitions opt into MCP servers explicitly via the `mcp.tools()` helper from `@electric-ax/agents-mcp`:
141
149
 
142
150
  ```ts
143
- import { mcp } from "@electric-ax/agents-runtime"
151
+ import { mcp } from "@electric-ax/agents-mcp"
144
152
 
145
153
  registry.define("research-agent", {
146
154
  async handler(ctx) {