@electric-ax/agents 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entrypoint.js +5 -3
- package/dist/index.cjs +5 -3
- package/dist/index.js +5 -3
- package/docs/entities/agents/horton.md +89 -0
- package/docs/entities/agents/worker.md +102 -0
- package/docs/entities/patterns/blackboard.md +111 -0
- package/docs/entities/patterns/dispatcher.md +77 -0
- package/docs/entities/patterns/manager-worker.md +127 -0
- package/docs/entities/patterns/map-reduce.md +81 -0
- package/docs/entities/patterns/pipeline.md +101 -0
- package/docs/entities/patterns/reactive-observers.md +125 -0
- package/docs/examples/mega-draw.md +106 -0
- package/docs/examples/playground.md +46 -0
- package/docs/index.md +208 -0
- package/docs/quickstart.md +201 -0
- package/docs/reference/agent-config.md +82 -0
- package/docs/reference/agent-tool.md +58 -0
- package/docs/reference/built-in-collections.md +334 -0
- package/docs/reference/cli.md +238 -0
- package/docs/reference/entity-definition.md +57 -0
- package/docs/reference/entity-handle.md +63 -0
- package/docs/reference/entity-registry.md +73 -0
- package/docs/reference/handler-context.md +108 -0
- package/docs/reference/runtime-handler.md +136 -0
- package/docs/reference/shared-state-handle.md +74 -0
- package/docs/reference/state-collection-proxy.md +41 -0
- package/docs/reference/wake-event.md +132 -0
- package/docs/usage/app-setup.md +165 -0
- package/docs/usage/clients-and-react.md +191 -0
- package/docs/usage/configuring-the-agent.md +136 -0
- package/docs/usage/context-composition.md +204 -0
- package/docs/usage/defining-entities.md +181 -0
- package/docs/usage/defining-tools.md +229 -0
- package/docs/usage/embedded-builtins.md +180 -0
- package/docs/usage/managing-state.md +93 -0
- package/docs/usage/overview.md +284 -0
- package/docs/usage/programmatic-runtime-client.md +216 -0
- package/docs/usage/shared-state.md +169 -0
- package/docs/usage/spawning-and-coordinating.md +165 -0
- package/docs/usage/testing.md +76 -0
- package/docs/usage/waking-entities.md +148 -0
- package/docs/usage/writing-handlers.md +267 -0
- package/package.json +2 -1
- package/skills/quickstart/scaffold/package.json +16 -3
- package/skills/quickstart/scaffold/tsconfig.json +8 -3
- package/skills/quickstart/scaffold/vite.config.ts +21 -0
- package/skills/quickstart/scaffold-ui/index.html +12 -0
- package/skills/quickstart/scaffold-ui/main.tsx +235 -0
- package/skills/quickstart.md +244 -334
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Defining entities
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Register entity types with the EntityRegistry, define custom state collections, typed schemas, and handler functions.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Defining entities
|
|
10
|
+
|
|
11
|
+
An entity type is registered with an `EntityRegistry`. The registry maps type names to `EntityDefinition` objects that declare the entity's state, schemas, and handler.
|
|
12
|
+
|
|
13
|
+
## Registry
|
|
14
|
+
|
|
15
|
+
`createEntityRegistry()` returns an `EntityRegistry`. Register types with `registry.define(name, definition)`.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createEntityRegistry } from '@electric-ax/agents-runtime'
|
|
19
|
+
|
|
20
|
+
const registry = createEntityRegistry()
|
|
21
|
+
|
|
22
|
+
registry.define('assistant', {
|
|
23
|
+
description: 'A general-purpose AI assistant',
|
|
24
|
+
async handler(ctx) {
|
|
25
|
+
ctx.useAgent({
|
|
26
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
27
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
28
|
+
tools: [...ctx.electricTools],
|
|
29
|
+
})
|
|
30
|
+
await ctx.agent.run()
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Calling `registry.define()` with a name that is already registered throws an error.
|
|
36
|
+
|
|
37
|
+
## EntityDefinition
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
interface EntityDefinition {
|
|
41
|
+
description?: string
|
|
42
|
+
state?: Record<string, CollectionDefinition>
|
|
43
|
+
actions?: (
|
|
44
|
+
collections: Record<string, unknown>
|
|
45
|
+
) => Record<string, (...args: unknown[]) => void>
|
|
46
|
+
creationSchema?: StandardJSONSchemaV1
|
|
47
|
+
inboxSchemas?: Record<string, StandardJSONSchemaV1>
|
|
48
|
+
outputSchemas?: Record<string, StandardJSONSchemaV1>
|
|
49
|
+
handler: (ctx: HandlerContext, wake: WakeEvent) => void | Promise<void>
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Field | Purpose |
|
|
54
|
+
| ---------------- | --------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| `description` | Human-readable description. Shown in the Electric Agents UI and CLI. |
|
|
56
|
+
| `state` | Custom persistent collections accessed via `ctx.state`, `ctx.db.actions`, and `ctx.db.collections`. |
|
|
57
|
+
| `actions` | Factory that returns custom non-CRUD action functions exposed on `ctx.actions`. |
|
|
58
|
+
| `creationSchema` | JSON Schema for arguments passed when the entity is spawned. |
|
|
59
|
+
| `inboxSchemas` | JSON Schemas for typed inbox message categories. |
|
|
60
|
+
| `outputSchemas` | JSON Schemas for typed output message categories. |
|
|
61
|
+
| `handler` | The function that runs each time the entity wakes. Required. |
|
|
62
|
+
|
|
63
|
+
## Custom state
|
|
64
|
+
|
|
65
|
+
Declare named collections in the `state` field. Each collection is a `CollectionDefinition`:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
interface CollectionDefinition {
|
|
69
|
+
schema?: StandardSchemaV1
|
|
70
|
+
type?: string
|
|
71
|
+
primaryKey?: string
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Field | Default | Purpose |
|
|
76
|
+
| ------------ | ---------------- | ----------------------------------------------------------------------- |
|
|
77
|
+
| `schema` | none | Optional Standard Schema validator (e.g. Zod). Validates rows on write. |
|
|
78
|
+
| `type` | `"state:{name}"` | Event type string used in the durable stream. |
|
|
79
|
+
| `primaryKey` | `"key"` | The field used as the primary key for the collection. |
|
|
80
|
+
|
|
81
|
+
Declared collections become available via `ctx.state` proxies and the lower-level `ctx.db.actions` / `ctx.db.collections` APIs:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { z } from 'zod'
|
|
85
|
+
|
|
86
|
+
const childSchema = z.object({
|
|
87
|
+
key: z.string(),
|
|
88
|
+
url: z.string(),
|
|
89
|
+
kind: z.string(),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
registry.define('coordinator', {
|
|
93
|
+
description: 'Spawns and tracks child entities',
|
|
94
|
+
state: {
|
|
95
|
+
status: { primaryKey: 'key' },
|
|
96
|
+
children: { schema: childSchema, primaryKey: 'key' },
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async handler(ctx) {
|
|
100
|
+
if (ctx.firstWake) {
|
|
101
|
+
ctx.db.actions.status_insert({ row: { key: 'current', value: 'idle' } })
|
|
102
|
+
}
|
|
103
|
+
// Convenience proxy:
|
|
104
|
+
ctx.state.children.insert({
|
|
105
|
+
key: 'child-1',
|
|
106
|
+
url: '/worker/child-1',
|
|
107
|
+
kind: 'worker',
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Lower-level APIs:
|
|
111
|
+
// Writes: ctx.db.actions.children_insert(), .children_update(), .children_delete()
|
|
112
|
+
// Reads: ctx.db.collections.children?.get(key), .children?.toArray
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For entity state, `ctx.state.<name>.insert/update/delete/get/toArray` is the convenience API. Lower-level writes use `ctx.db.actions.<name>_insert/update/delete`, and reads use `ctx.db.collections.<name>?.get(key)` and `ctx.db.collections.<name>?.toArray`.
|
|
118
|
+
|
|
119
|
+
::: info
|
|
120
|
+
`StateCollectionProxy` is used for both entity-local `ctx.state` and shared state handles. See [StateCollectionProxy](../reference/state-collection-proxy) for details.
|
|
121
|
+
:::
|
|
122
|
+
|
|
123
|
+
## Registry pattern
|
|
124
|
+
|
|
125
|
+
For projects with multiple entity types, keep a separate registry file and import register functions:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// entities/registry.ts
|
|
129
|
+
import { createEntityRegistry } from '@electric-ax/agents-runtime'
|
|
130
|
+
import { registerAssistant } from './assistant'
|
|
131
|
+
import { registerWorker } from './worker'
|
|
132
|
+
|
|
133
|
+
export const registry = createEntityRegistry()
|
|
134
|
+
registerAssistant(registry)
|
|
135
|
+
registerWorker(registry)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// entities/assistant.ts
|
|
140
|
+
import type { EntityRegistry } from '@electric-ax/agents-runtime'
|
|
141
|
+
|
|
142
|
+
export function registerAssistant(registry: EntityRegistry) {
|
|
143
|
+
registry.define('assistant', {
|
|
144
|
+
description: 'General-purpose assistant',
|
|
145
|
+
async handler(ctx) {
|
|
146
|
+
ctx.useAgent({
|
|
147
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
148
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
149
|
+
tools: [...ctx.electricTools],
|
|
150
|
+
})
|
|
151
|
+
await ctx.agent.run()
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
This keeps each entity type isolated and the registry composition explicit.
|
|
158
|
+
|
|
159
|
+
## Schemas
|
|
160
|
+
|
|
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.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { z } from 'zod/v4'
|
|
165
|
+
|
|
166
|
+
registry.define('processor', {
|
|
167
|
+
description: 'Processes structured tasks',
|
|
168
|
+
creationSchema: z.object({
|
|
169
|
+
priority: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
170
|
+
}),
|
|
171
|
+
inboxSchemas: {
|
|
172
|
+
task: z.object({
|
|
173
|
+
title: z.string(),
|
|
174
|
+
body: z.string().optional(),
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
async handler(ctx) {
|
|
178
|
+
// ctx.args.priority is available from creationSchema
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
```
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Defining tools
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Create stateless, stateful, and handler-scoped tools for the LLM agent loop.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Defining tools
|
|
10
|
+
|
|
11
|
+
Tools are functions the LLM can call during the agent loop. Each tool has a name, description, typed parameters, and an execute function.
|
|
12
|
+
|
|
13
|
+
## AgentTool interface
|
|
14
|
+
|
|
15
|
+
Re-exported from [`@mariozechner/pi-agent-core`](https://github.com/badlogic/pi-mono):
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any> {
|
|
19
|
+
name: string
|
|
20
|
+
label: string
|
|
21
|
+
description: string
|
|
22
|
+
parameters: TParameters
|
|
23
|
+
execute: (
|
|
24
|
+
toolCallId: string,
|
|
25
|
+
params: Static<TParameters>,
|
|
26
|
+
signal?: AbortSignal,
|
|
27
|
+
onUpdate?: AgentToolUpdateCallback<TDetails>
|
|
28
|
+
) => Promise<AgentToolResult<TDetails>>
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The return type:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
interface AgentToolResult<T = any> {
|
|
36
|
+
content: Array<{ type: 'text'; text: string }>
|
|
37
|
+
details: T
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Parameters
|
|
42
|
+
|
|
43
|
+
Defined using [TypeBox](https://github.com/sinclairzx81/typebox) (`@sinclair/typebox`). The schema is used for LLM function calling and argument validation.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Type } from '@sinclair/typebox'
|
|
47
|
+
|
|
48
|
+
parameters: Type.Object({
|
|
49
|
+
expression: Type.String({ description: 'Math expression to evaluate' }),
|
|
50
|
+
precision: Type.Optional(Type.Number({ description: 'Decimal places' })),
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Stateless tools
|
|
55
|
+
|
|
56
|
+
Pure functions with no side effects beyond what they compute. Define directly as an `AgentTool` object.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { Type } from '@sinclair/typebox'
|
|
60
|
+
import type { AgentTool } from '@electric-ax/agents-runtime'
|
|
61
|
+
|
|
62
|
+
const calculatorTool: AgentTool = {
|
|
63
|
+
name: 'calculator',
|
|
64
|
+
label: 'Calculator',
|
|
65
|
+
description: 'Evaluate mathematical expressions.',
|
|
66
|
+
parameters: Type.Object({
|
|
67
|
+
expression: Type.String({ description: 'The expression to evaluate' }),
|
|
68
|
+
}),
|
|
69
|
+
execute: async (_toolCallId, params) => {
|
|
70
|
+
const { expression } = params as { expression: string }
|
|
71
|
+
const result = evaluate(expression)
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: 'text', text: String(result) }],
|
|
74
|
+
details: {},
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Stateful tools
|
|
81
|
+
|
|
82
|
+
Use a factory function that receives the `HandlerContext`. The state persists across wakes -- it is backed by the entity's durable stream. Reads go through `ctx.db.collections` and writes go through `ctx.db.actions`.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { Type } from '@sinclair/typebox'
|
|
86
|
+
import type { AgentTool, HandlerContext } from '@electric-ax/agents-runtime'
|
|
87
|
+
|
|
88
|
+
function createMemoryStoreTool(ctx: HandlerContext): AgentTool {
|
|
89
|
+
return {
|
|
90
|
+
name: 'memory_store',
|
|
91
|
+
label: 'Memory Store',
|
|
92
|
+
description: 'Persistent key-value store.',
|
|
93
|
+
parameters: Type.Object({
|
|
94
|
+
operation: Type.Union([
|
|
95
|
+
Type.Literal('get'),
|
|
96
|
+
Type.Literal('set'),
|
|
97
|
+
Type.Literal('delete'),
|
|
98
|
+
Type.Literal('list'),
|
|
99
|
+
]),
|
|
100
|
+
key: Type.Optional(Type.String()),
|
|
101
|
+
value: Type.Optional(Type.String()),
|
|
102
|
+
}),
|
|
103
|
+
execute: async (_, params) => {
|
|
104
|
+
const { operation, key, value } = params as {
|
|
105
|
+
operation: string
|
|
106
|
+
key?: string
|
|
107
|
+
value?: string
|
|
108
|
+
}
|
|
109
|
+
if (operation === 'set') {
|
|
110
|
+
const existing = ctx.db.collections.kv?.get(key!)
|
|
111
|
+
if (existing) {
|
|
112
|
+
ctx.db.actions.kv_update({
|
|
113
|
+
key: key!,
|
|
114
|
+
updater: (draft) => {
|
|
115
|
+
draft.value = value!
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
} else {
|
|
119
|
+
ctx.db.actions.kv_insert({ row: { key: key!, value: value! } })
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: 'text', text: `Stored "${key}"` }],
|
|
123
|
+
details: {},
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (operation === 'get') {
|
|
127
|
+
const entry = ctx.db.collections.kv?.get(key!)
|
|
128
|
+
const text = entry ? entry.value : `No value found for "${key}"`
|
|
129
|
+
return { content: [{ type: 'text', text }], details: {} }
|
|
130
|
+
}
|
|
131
|
+
if (operation === 'delete') {
|
|
132
|
+
ctx.db.actions.kv_delete({ key: key! })
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: 'text', text: `Deleted "${key}"` }],
|
|
135
|
+
details: {},
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// list
|
|
139
|
+
const entries = ctx.db.collections.kv?.toArray ?? []
|
|
140
|
+
const text = entries.map((e) => `${e.key}: ${e.value}`).join('\n')
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: 'text', text: text || '(empty)' }],
|
|
143
|
+
details: {},
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The entity state API:
|
|
151
|
+
|
|
152
|
+
| Operation | Write (via `ctx.db.actions`) | Read (via `ctx.db.collections`) |
|
|
153
|
+
| --------- | ------------------------------------------------------------------ | ------------------------------------- |
|
|
154
|
+
| Insert | `ctx.db.actions.<coll>_insert({ row: {...} })` | - |
|
|
155
|
+
| Update | `ctx.db.actions.<coll>_update({ key, updater: (draft) => {...} })` | - |
|
|
156
|
+
| Delete | `ctx.db.actions.<coll>_delete({ key })` | - |
|
|
157
|
+
| Get | - | `ctx.db.collections.<coll>?.get(key)` |
|
|
158
|
+
| List | - | `ctx.db.collections.<coll>?.toArray` |
|
|
159
|
+
|
|
160
|
+
## Handler-scoped tools
|
|
161
|
+
|
|
162
|
+
Use a factory that receives the `HandlerContext`. These tools can spawn entities, observe streams, send messages, and use any other `ctx` primitive.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { Type } from '@sinclair/typebox'
|
|
166
|
+
import type { AgentTool, HandlerContext } from '@electric-ax/agents-runtime'
|
|
167
|
+
|
|
168
|
+
function createDispatchTool(ctx: HandlerContext): AgentTool {
|
|
169
|
+
return {
|
|
170
|
+
name: 'dispatch',
|
|
171
|
+
label: 'Dispatch',
|
|
172
|
+
description: 'Spawn a child agent and wait for its response.',
|
|
173
|
+
parameters: Type.Object({
|
|
174
|
+
type: Type.String({ description: 'Entity type to spawn' }),
|
|
175
|
+
systemPrompt: Type.String({ description: 'System prompt for the child' }),
|
|
176
|
+
task: Type.String({ description: 'Task to send to the child' }),
|
|
177
|
+
}),
|
|
178
|
+
execute: async (_, params) => {
|
|
179
|
+
const { type, systemPrompt, task } = params as {
|
|
180
|
+
type: string
|
|
181
|
+
systemPrompt: string
|
|
182
|
+
task: string
|
|
183
|
+
}
|
|
184
|
+
const child = await ctx.spawn(
|
|
185
|
+
type,
|
|
186
|
+
`dispatch-${Date.now()}`,
|
|
187
|
+
{ systemPrompt },
|
|
188
|
+
{
|
|
189
|
+
initialMessage: task,
|
|
190
|
+
wake: 'runFinished',
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
const text = (await child.text()).join('\n\n')
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: 'text', text }],
|
|
196
|
+
details: {},
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`ctx.spawn` returns an `EntityHandle`. Passing `wake: 'runFinished'` means the parent will be woken when the child's agent run completes. `child.text()` returns all text outputs from the child's stream.
|
|
204
|
+
|
|
205
|
+
## Wiring tools together
|
|
206
|
+
|
|
207
|
+
Tools are constructed in the handler and passed to `useAgent`. Include `ctx.electricTools` when your runtime host provides runtime-level tools that the LLM should be able to call:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
registry.define('assistant', {
|
|
211
|
+
description: 'An assistant with memory and delegation',
|
|
212
|
+
state: {
|
|
213
|
+
kv: { primaryKey: 'key' },
|
|
214
|
+
},
|
|
215
|
+
async handler(ctx) {
|
|
216
|
+
const memoryTool = createMemoryStoreTool(ctx)
|
|
217
|
+
const dispatchTool = createDispatchTool(ctx)
|
|
218
|
+
|
|
219
|
+
ctx.useAgent({
|
|
220
|
+
systemPrompt: 'You are a helpful assistant with persistent memory.',
|
|
221
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
222
|
+
tools: [...ctx.electricTools, memoryTool, dispatchTool, calculatorTool],
|
|
223
|
+
})
|
|
224
|
+
await ctx.agent.run()
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
When you include `ctx.electricTools`, spread them before your custom tools so host-provided primitives keep their expected order.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Embedded built-ins
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Embed the built-in Horton and worker runtime in your own process using
|
|
6
|
+
@electric-ax/agents, BuiltinAgentsServer, or the entrypoint helpers.
|
|
7
|
+
outline: [2, 3]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Embedded built-ins
|
|
11
|
+
|
|
12
|
+
The CLI commands `electric agents start-builtin` and `electric agents quickstart` run the built-in Horton and worker runtime for you. If you need to host those built-ins inside your own process, use the exported APIs from `@electric-ax/agents`.
|
|
13
|
+
|
|
14
|
+
## BuiltinAgentsServer
|
|
15
|
+
|
|
16
|
+
`BuiltinAgentsServer` starts an HTTP webhook server, registers `horton` and `worker`, and forwards Electric Agents webhook wakes to the built-in handler.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { BuiltinAgentsServer } from '@electric-ax/agents'
|
|
20
|
+
|
|
21
|
+
const server = new BuiltinAgentsServer({
|
|
22
|
+
agentServerUrl: 'http://localhost:4437',
|
|
23
|
+
port: 4448,
|
|
24
|
+
workingDirectory: process.cwd(),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await server.start()
|
|
28
|
+
|
|
29
|
+
console.log(server.url)
|
|
30
|
+
console.log(server.registeredBaseUrl)
|
|
31
|
+
|
|
32
|
+
// Later, during shutdown:
|
|
33
|
+
await server.stop()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Options
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import type { RuntimeRouterConfig } from '@electric-ax/agents-runtime'
|
|
40
|
+
|
|
41
|
+
type CreateElectricTools = RuntimeRouterConfig['createElectricTools']
|
|
42
|
+
|
|
43
|
+
interface BuiltinAgentsServerOptions {
|
|
44
|
+
agentServerUrl: string
|
|
45
|
+
baseUrl?: string
|
|
46
|
+
port: number
|
|
47
|
+
host?: string
|
|
48
|
+
workingDirectory?: string
|
|
49
|
+
mockStreamFn?: StreamFn
|
|
50
|
+
webhookPath?: string
|
|
51
|
+
createElectricTools?: CreateElectricTools
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Field | Description |
|
|
56
|
+
| --------------------- | ---------------------------------------------------------------------------- |
|
|
57
|
+
| `agentServerUrl` | Electric Agents coordinator server URL. |
|
|
58
|
+
| `baseUrl` | Public base URL used when registering the webhook. Defaults to local URL. |
|
|
59
|
+
| `port` | Local webhook server port. |
|
|
60
|
+
| `host` | Bind host. Defaults to `127.0.0.1`. |
|
|
61
|
+
| `workingDirectory` | Directory used by Horton and worker file tools. Defaults to `process.cwd()`. |
|
|
62
|
+
| `mockStreamFn` | Optional test stream function. Lets you run without `ANTHROPIC_API_KEY`. |
|
|
63
|
+
| `webhookPath` | Webhook path. Defaults to `/_electric/builtin-agent-handler`. |
|
|
64
|
+
| `createElectricTools` | Optional factory for extra tools injected into built-in agent handlers. |
|
|
65
|
+
|
|
66
|
+
Without `mockStreamFn`, `ANTHROPIC_API_KEY` must be present before the built-in handler starts.
|
|
67
|
+
|
|
68
|
+
## createBuiltinAgentHandler
|
|
69
|
+
|
|
70
|
+
Use `createBuiltinAgentHandler()` when you already have an HTTP server and only need the request handler and runtime objects.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import {
|
|
74
|
+
createBuiltinAgentHandler,
|
|
75
|
+
registerBuiltinAgentTypes,
|
|
76
|
+
} from '@electric-ax/agents'
|
|
77
|
+
|
|
78
|
+
const bootstrap = await createBuiltinAgentHandler({
|
|
79
|
+
agentServerUrl: 'http://localhost:4437',
|
|
80
|
+
serveEndpoint: 'https://example.com/_electric/builtin-agent-handler',
|
|
81
|
+
workingDirectory: process.cwd(),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
if (!bootstrap) {
|
|
85
|
+
throw new Error('ANTHROPIC_API_KEY is required for built-in agents')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await registerBuiltinAgentTypes(bootstrap)
|
|
89
|
+
|
|
90
|
+
// In your HTTP server:
|
|
91
|
+
await bootstrap.handler(req, res)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Result
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
interface AgentHandlerResult {
|
|
98
|
+
handler(req: IncomingMessage, res: ServerResponse): Promise<void>
|
|
99
|
+
runtime: RuntimeHandler
|
|
100
|
+
registry: EntityRegistry
|
|
101
|
+
typeNames: string[]
|
|
102
|
+
skillsRegistry: SkillsRegistry | null
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Extra Electric Tools
|
|
107
|
+
|
|
108
|
+
Both `BuiltinAgentsServer` and `createBuiltinAgentHandler()` accept `createElectricTools`. The factory receives the same context shape as `RuntimeRouterConfig.createElectricTools` and can add host-specific tools to Horton.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { Type } from '@sinclair/typebox'
|
|
112
|
+
|
|
113
|
+
const server = new BuiltinAgentsServer({
|
|
114
|
+
agentServerUrl: 'http://localhost:4437',
|
|
115
|
+
port: 4448,
|
|
116
|
+
createElectricTools: ({ entityUrl, upsertCronSchedule }) => [
|
|
117
|
+
{
|
|
118
|
+
name: 'schedule_daily_summary',
|
|
119
|
+
label: 'Schedule daily summary',
|
|
120
|
+
description: 'Schedule a daily summary wake for this entity.',
|
|
121
|
+
parameters: Type.Object({
|
|
122
|
+
hour: Type.Number(),
|
|
123
|
+
}),
|
|
124
|
+
execute: async (_id, params) => {
|
|
125
|
+
const { hour } = params as { hour: number }
|
|
126
|
+
await upsertCronSchedule({
|
|
127
|
+
id: 'daily-summary',
|
|
128
|
+
expression: `0 ${hour} * * *`,
|
|
129
|
+
payload: `Run daily summary for ${entityUrl}`,
|
|
130
|
+
})
|
|
131
|
+
return { content: [{ type: 'text', text: 'Scheduled.' }], details: {} }
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Entrypoint Helpers
|
|
139
|
+
|
|
140
|
+
`runBuiltinAgentsEntrypoint()` reads environment variables, creates a `BuiltinAgentsServer`, and starts it. This is what the `electric-agents` package binary uses.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import {
|
|
144
|
+
resolveBuiltinAgentsEntrypointOptions,
|
|
145
|
+
runBuiltinAgentsEntrypoint,
|
|
146
|
+
} from '@electric-ax/agents'
|
|
147
|
+
|
|
148
|
+
const options = resolveBuiltinAgentsEntrypointOptions(process.env)
|
|
149
|
+
const { server, url } = await runBuiltinAgentsEntrypoint()
|
|
150
|
+
|
|
151
|
+
console.log(options.agentServerUrl, url)
|
|
152
|
+
await server.stop()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Environment variables:
|
|
156
|
+
|
|
157
|
+
| Variable | Description |
|
|
158
|
+
| ----------------------------------- | ------------------------------------------------ |
|
|
159
|
+
| `ELECTRIC_AGENTS_SERVER_URL` | Required coordinator server URL. |
|
|
160
|
+
| `ELECTRIC_AGENTS_BUILTIN_BASE_URL` | Public webhook base URL for the built-in server. |
|
|
161
|
+
| `ELECTRIC_AGENTS_BUILTIN_HOST` | Bind host. |
|
|
162
|
+
| `ELECTRIC_AGENTS_BUILTIN_PORT` | Built-in server port. Defaults to `4448`. |
|
|
163
|
+
| `ELECTRIC_AGENTS_WORKING_DIRECTORY` | Working directory for file tools. |
|
|
164
|
+
|
|
165
|
+
## Built-in Agent APIs
|
|
166
|
+
|
|
167
|
+
The built-in agent exports are also available if you want to compose your own runtime:
|
|
168
|
+
|
|
169
|
+
| Export | Purpose |
|
|
170
|
+
| --------------------------- | ----------------------------------------------------- |
|
|
171
|
+
| `registerHorton()` | Register the `horton` type on an `EntityRegistry`. |
|
|
172
|
+
| `registerWorker()` | Register the `worker` type on an `EntityRegistry`. |
|
|
173
|
+
| `HORTON_MODEL` | Default model id used by Horton and worker. |
|
|
174
|
+
| `buildHortonSystemPrompt()` | Build Horton's system prompt for a working directory. |
|
|
175
|
+
| `createHortonTools()` | Create Horton's base shell/file/search/worker tools. |
|
|
176
|
+
| `createSpawnWorkerTool()` | Create the `spawn_worker` tool for another agent. |
|
|
177
|
+
| `WORKER_TOOL_NAMES` | Valid primitive tool names for workers. |
|
|
178
|
+
| `createHortonDocsSupport()` | Create Horton's docs knowledge-base support. |
|
|
179
|
+
|
|
180
|
+
For the behavior of `horton` and `worker`, see [Horton](../entities/agents/horton) and [Worker](../entities/agents/worker).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Managing state
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Declare and manage persistent entity state using custom collections with typed CRUD operations.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Managing state
|
|
10
|
+
|
|
11
|
+
Entities can declare custom persistent collections. The convenience API is `ctx.state.<name>.insert/update/delete/get/toArray`; the lower-level APIs are `ctx.db.actions.<name>_insert/update/delete` for writes and `ctx.db.collections.<name>` for reads. State is backed by the entity's durable stream. Values survive process restarts and are available on every handler invocation.
|
|
12
|
+
|
|
13
|
+
## Declaring state
|
|
14
|
+
|
|
15
|
+
Define collections in the `state` field of the entity definition:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
registry.define('my-entity', {
|
|
19
|
+
state: {
|
|
20
|
+
status: { primaryKey: 'key' },
|
|
21
|
+
items: {
|
|
22
|
+
schema: z.object({
|
|
23
|
+
key: z.string(),
|
|
24
|
+
name: z.string(),
|
|
25
|
+
count: z.number(),
|
|
26
|
+
}),
|
|
27
|
+
primaryKey: 'key',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
async handler(ctx) {
|
|
31
|
+
/* ... */
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Each key in `state` becomes a collection exposed on `ctx.state`, `ctx.db.actions`, and `ctx.db.collections`.
|
|
37
|
+
|
|
38
|
+
## CollectionDefinition
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
interface CollectionDefinition {
|
|
42
|
+
schema?: StandardSchemaV1 // Zod or any Standard Schema validator
|
|
43
|
+
type?: string // Event type in the stream. Defaults to "state:{name}"
|
|
44
|
+
primaryKey?: string // Key field. Defaults to "key"
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
All fields are optional. A minimal collection like `{ primaryKey: 'key' }` works without a schema — rows are untyped.
|
|
49
|
+
|
|
50
|
+
## Writing and reading state
|
|
51
|
+
|
|
52
|
+
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.
|
|
53
|
+
|
|
54
|
+
The lower-level `ctx.db.actions` object exposes action methods named `<collection>_insert`, `<collection>_update`, and `<collection>_delete`. Reads go through `ctx.db.collections`, which exposes TanStack DB collection objects with `.get(key)` and `.toArray`.
|
|
55
|
+
|
|
56
|
+
Write helpers return a Transaction. Reads query the underlying TanStack DB collection.
|
|
57
|
+
|
|
58
|
+
## CRUD operations
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// Convenience API
|
|
62
|
+
ctx.state.items.insert({ key: 'item-1', name: 'Widget', count: 5 })
|
|
63
|
+
const itemViaState = ctx.state.items.get('item-1')
|
|
64
|
+
const allViaState = ctx.state.items.toArray
|
|
65
|
+
ctx.state.items.update('item-1', (draft) => {
|
|
66
|
+
draft.count += 1
|
|
67
|
+
})
|
|
68
|
+
ctx.state.items.delete('item-1')
|
|
69
|
+
|
|
70
|
+
// Lower-level insert
|
|
71
|
+
ctx.db.actions.items_insert({
|
|
72
|
+
row: { key: 'item-1', name: 'Widget', count: 5 },
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Lower-level read
|
|
76
|
+
const item = ctx.db.collections.items?.get('item-1')
|
|
77
|
+
const all = ctx.db.collections.items?.toArray
|
|
78
|
+
|
|
79
|
+
// Lower-level update (Immer-style draft)
|
|
80
|
+
ctx.db.actions.items_update({
|
|
81
|
+
key: 'item-1',
|
|
82
|
+
updater: (draft) => {
|
|
83
|
+
draft.count += 1
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Lower-level delete
|
|
88
|
+
ctx.db.actions.items_delete({ key: 'item-1' })
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Built-in collections
|
|
92
|
+
|
|
93
|
+
Every entity also has `ctx.db.collections` with runtime-managed collections: `runs`, `steps`, `texts`, `toolCalls`, `errors`, `inbox`, and more. These are read-only from the handler's perspective — the runtime writes to them as the agent operates. See [Built-in collections](../reference/built-in-collections) for details.
|