@electric-ax/agents 0.4.19 → 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.
- package/docs/entities/agents/horton.md +22 -17
- package/docs/entities/agents/worker.md +13 -6
- package/docs/entities/patterns/blackboard.md +1 -1
- package/docs/entities/patterns/dispatcher.md +1 -1
- package/docs/entities/patterns/manager-worker.md +10 -5
- package/docs/entities/patterns/map-reduce.md +1 -1
- package/docs/entities/patterns/pipeline.md +1 -1
- package/docs/entities/patterns/reactive-observers.md +1 -1
- package/docs/index.md +6 -4
- package/docs/quickstart.md +2 -2
- package/docs/reference/agent-config.md +13 -3
- package/docs/reference/built-in-collections.md +128 -9
- package/docs/reference/cli.md +34 -4
- package/docs/reference/entity-definition.md +39 -7
- package/docs/reference/entity-handle.md +19 -1
- package/docs/reference/handler-context.md +130 -5
- package/docs/reference/runtime-handler.md +42 -14
- package/docs/reference/wake-event.md +29 -1
- package/docs/usage/app-setup.md +38 -7
- package/docs/usage/attachments.md +129 -0
- package/docs/usage/clients-and-react.md +23 -2
- package/docs/usage/configuring-the-agent.md +15 -5
- package/docs/usage/context-composition.md +2 -1
- package/docs/usage/defining-entities.md +9 -5
- package/docs/usage/defining-tools.md +1 -1
- package/docs/usage/embedded-builtins.md +82 -31
- package/docs/usage/managing-state.md +5 -0
- package/docs/usage/mcp-servers.md +16 -8
- package/docs/usage/overview.md +39 -14
- package/docs/usage/permissions-and-principals.md +160 -0
- package/docs/usage/programmatic-runtime-client.md +158 -16
- package/docs/usage/sandboxing.md +162 -0
- package/docs/usage/signals.md +138 -0
- package/docs/usage/spawning-and-coordinating.md +30 -11
- package/docs/usage/testing.md +1 -1
- package/docs/usage/waking-entities.md +34 -6
- package/docs/usage/webhook-sources.md +171 -0
- package/docs/usage/writing-handlers.md +13 -55
- package/docs/walkthrough.md +13 -5
- 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?:
|
|
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 |
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
| `
|
|
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-
|
|
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 `
|
|
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-
|
|
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`
|
|
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
|
-
|
|
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
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
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
|
|
73
|
-
| `mcpOAuthRedirectBase` | Base for OAuth redirect URIs (full URI is `<base>/oauth/callback/<server-name>`).
|
|
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`,
|
|
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
|
|
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("
|
|
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
|
-
|
|
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
|
|
169
|
-
|
|
|
170
|
-
| `ELECTRIC_AGENTS_SERVER_URL`
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
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`](#
|
|
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
|
-
|
|
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-
|
|
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-
|
|
151
|
+
import { mcp } from "@electric-ax/agents-mcp"
|
|
144
152
|
|
|
145
153
|
registry.define("research-agent", {
|
|
146
154
|
async handler(ctx) {
|