@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.
Files changed (49) hide show
  1. package/dist/entrypoint.js +5 -3
  2. package/dist/index.cjs +5 -3
  3. package/dist/index.js +5 -3
  4. package/docs/entities/agents/horton.md +89 -0
  5. package/docs/entities/agents/worker.md +102 -0
  6. package/docs/entities/patterns/blackboard.md +111 -0
  7. package/docs/entities/patterns/dispatcher.md +77 -0
  8. package/docs/entities/patterns/manager-worker.md +127 -0
  9. package/docs/entities/patterns/map-reduce.md +81 -0
  10. package/docs/entities/patterns/pipeline.md +101 -0
  11. package/docs/entities/patterns/reactive-observers.md +125 -0
  12. package/docs/examples/mega-draw.md +106 -0
  13. package/docs/examples/playground.md +46 -0
  14. package/docs/index.md +208 -0
  15. package/docs/quickstart.md +201 -0
  16. package/docs/reference/agent-config.md +82 -0
  17. package/docs/reference/agent-tool.md +58 -0
  18. package/docs/reference/built-in-collections.md +334 -0
  19. package/docs/reference/cli.md +238 -0
  20. package/docs/reference/entity-definition.md +57 -0
  21. package/docs/reference/entity-handle.md +63 -0
  22. package/docs/reference/entity-registry.md +73 -0
  23. package/docs/reference/handler-context.md +108 -0
  24. package/docs/reference/runtime-handler.md +136 -0
  25. package/docs/reference/shared-state-handle.md +74 -0
  26. package/docs/reference/state-collection-proxy.md +41 -0
  27. package/docs/reference/wake-event.md +132 -0
  28. package/docs/usage/app-setup.md +165 -0
  29. package/docs/usage/clients-and-react.md +191 -0
  30. package/docs/usage/configuring-the-agent.md +136 -0
  31. package/docs/usage/context-composition.md +204 -0
  32. package/docs/usage/defining-entities.md +181 -0
  33. package/docs/usage/defining-tools.md +229 -0
  34. package/docs/usage/embedded-builtins.md +180 -0
  35. package/docs/usage/managing-state.md +93 -0
  36. package/docs/usage/overview.md +284 -0
  37. package/docs/usage/programmatic-runtime-client.md +216 -0
  38. package/docs/usage/shared-state.md +169 -0
  39. package/docs/usage/spawning-and-coordinating.md +165 -0
  40. package/docs/usage/testing.md +76 -0
  41. package/docs/usage/waking-entities.md +148 -0
  42. package/docs/usage/writing-handlers.md +267 -0
  43. package/package.json +2 -1
  44. package/skills/quickstart/scaffold/package.json +16 -3
  45. package/skills/quickstart/scaffold/tsconfig.json +8 -3
  46. package/skills/quickstart/scaffold/vite.config.ts +21 -0
  47. package/skills/quickstart/scaffold-ui/index.html +12 -0
  48. package/skills/quickstart/scaffold-ui/main.tsx +235 -0
  49. 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.