@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,165 @@
1
+ ---
2
+ title: Spawning & coordinating
3
+ titleTemplate: '... - Electric Agents'
4
+ description: >-
5
+ Spawn child entities, observe existing ones, send messages, and use EntityHandle for coordination.
6
+ outline: [2, 3]
7
+ ---
8
+
9
+ # Spawning & coordinating
10
+
11
+ Entities coordinate by spawning children, observing other entities, and sending messages.
12
+
13
+ ## spawn
14
+
15
+ Create a child entity:
16
+
17
+ ```ts
18
+ const child = await ctx.spawn(type, id, args?, opts?)
19
+ ```
20
+
21
+ | Parameter | Type | Description |
22
+ | --------------------- | ------------------------- | -------------------------------------- |
23
+ | `type` | `string` | Entity type name (must be registered) |
24
+ | `id` | `string` | Unique child ID |
25
+ | `args` | `Record<string, unknown>` | Passed to child handler as `ctx.args` |
26
+ | `opts.initialMessage` | `unknown` | First message delivered to child |
27
+ | `opts.wake` | `Wake` | When to wake the parent (see below) |
28
+ | `opts.tags` | `Record<string, string>` | Key-value tags applied to the child |
29
+ | `opts.observe` | `boolean` | Also observe the child (default: true) |
30
+
31
+ `spawn` is a creation-only operation. Calling it with a `(type, id)` pair that already exists in the entity's manifest throws an error. Use `observe(entity(url))` to get a handle to an existing child.
32
+
33
+ The `wake` option controls when the parent's handler is re-invoked:
34
+
35
+ - `'runFinished'` — wake when the child's agent run completes. The child's text response is included in the wake event by default.
36
+ - `{ on: 'runFinished', includeResponse?: boolean }` — same as above, but set `includeResponse: false` to omit the child's text response from the wake event.
37
+ - `{ on: 'change', collections?: string[], debounceMs?: number, timeoutMs?: number }` — wake when specified collections change.
38
+
39
+ Returns an [`EntityHandle`](#entityhandle).
40
+
41
+ ## EntityHandle
42
+
43
+ Returned by `spawn` and `observe`:
44
+
45
+ ```ts
46
+ interface EntityHandle {
47
+ entityUrl: string
48
+ type?: string
49
+ db: EntityStreamDB // Read-only TanStack DB
50
+ events: ChangeEvent[]
51
+ run: Promise<void> // Resolves when child's run completes
52
+ text(): Promise<string[]> // Get completed text outputs
53
+ send(msg: unknown): void // Send follow-up message
54
+ status(): ChildStatus | undefined
55
+ }
56
+ ```
57
+
58
+ `status()` returns a `ChildStatus` object (or `undefined` if no status is known yet) with `.status`, `.entity_url`, `.entity_type`, and `.key`.
59
+
60
+ ## Waiting for children
61
+
62
+ Wait for a single child:
63
+
64
+ ```ts
65
+ await child.run
66
+ const output = (await child.text()).join('\n\n')
67
+ ```
68
+
69
+ Wait for multiple children in parallel:
70
+
71
+ ```ts
72
+ const results = await Promise.all(
73
+ children.map(async ({ handle }) => ({
74
+ text: (await handle.text()).join('\n\n'),
75
+ }))
76
+ )
77
+ ```
78
+
79
+ ## observe
80
+
81
+ Subscribe to an existing entity without spawning it:
82
+
83
+ ```ts
84
+ const handle = await ctx.observe(entity(entityUrl), {
85
+ wake: { on: 'change', collections: ['runs', 'childStatus'] },
86
+ })
87
+ ```
88
+
89
+ Returns an `EntityHandle`. Use `wake` to re-invoke the parent handler when the observed entity changes.
90
+
91
+ ## send
92
+
93
+ Fire-and-forget message to another entity:
94
+
95
+ ```ts
96
+ ctx.send('/assistant/target-id', { text: 'Hello' })
97
+ ctx.send('/assistant/target-id', payload, { type: 'custom_type' })
98
+ ```
99
+
100
+ Messages appear in the target entity's `inbox` collection.
101
+
102
+ ## sleep
103
+
104
+ Return the entity to idle state, ending the current handler invocation:
105
+
106
+ ```ts
107
+ ctx.sleep()
108
+ ```
109
+
110
+ The entity remains alive and can be woken again by incoming messages or observed changes.
111
+
112
+ ## Working with existing children
113
+
114
+ After spawning children in `firstWake`, use `observe` on subsequent wakes to get handles:
115
+
116
+ ```ts
117
+ async handler(ctx) {
118
+ if (ctx.firstWake) {
119
+ await ctx.spawn(
120
+ "worker",
121
+ "analyst",
122
+ { systemPrompt: "...", tools: ["read"] },
123
+ {
124
+ initialMessage: "Initial task.",
125
+ wake: "runFinished",
126
+ }
127
+ )
128
+ }
129
+
130
+ const analyst = await ctx.observe(entity("/worker/analyst"))
131
+
132
+ if (wake.type === "message_received") {
133
+ analyst.send(wake.payload)
134
+ }
135
+ }
136
+ ```
137
+
138
+ `spawn` creates the child once. `observe` returns a handle on every wake — it's how you interact with children after creation.
139
+
140
+ ## Workers and authenticated APIs
141
+
142
+ Workers are least-privilege sandboxes. The built-in `worker` receives a `systemPrompt`, a selected `tools` subset, an optional `sharedDb` config, and the `initialMessage` delivered at spawn time. Never interpolate secrets (`process.env.API_KEY`, auth tokens) into a worker's prompt or message — they are persisted in the entity's durable stream.
143
+
144
+ **Manager-side prefetch** is the recommended pattern: the manager does the authenticated fetch and passes the raw data to the worker.
145
+
146
+ ```ts
147
+ // In the manager's tool:
148
+ const response = await fetch(apiUrl, {
149
+ headers: { Authorization: `Bearer ${process.env.API_KEY}` },
150
+ })
151
+ const data = await response.json()
152
+
153
+ // Pass data, not credentials, to the worker
154
+ await ctx.spawn(
155
+ 'worker',
156
+ id,
157
+ { systemPrompt: 'Summarise this data.', tools: ['read'] },
158
+ {
159
+ initialMessage: JSON.stringify(data),
160
+ wake: 'runFinished',
161
+ }
162
+ )
163
+ ```
164
+
165
+ When the worker needs to make follow-up authenticated calls (pagination, conditional fetches), register a custom worker entity type in your app that closes over the credential at registration time — don't use the built-in `worker` type for this.
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: Testing
3
+ titleTemplate: '... - Electric Agents'
4
+ description: >-
5
+ Test entity handlers with testResponses for LLM mocking, plus unit and integration testing patterns.
6
+ outline: [2, 3]
7
+ ---
8
+
9
+ # Testing
10
+
11
+ ## testResponses
12
+
13
+ Test agent handlers without calling the LLM by providing canned responses:
14
+
15
+ ```ts
16
+ ctx.useAgent({
17
+ systemPrompt: '...',
18
+ model: 'claude-sonnet-4-5-20250929',
19
+ tools: [...ctx.electricTools],
20
+ testResponses: ['Hello! How can I help?'],
21
+ })
22
+ await ctx.agent.run()
23
+ ```
24
+
25
+ For array responses, the runtime picks a response based on the number of prior runs for the entity, which makes repeated wakes deterministic without calling an LLM. The selected string becomes the agent's text output for that run.
26
+
27
+ ## TestResponseFn
28
+
29
+ For dynamic test responses, provide a function instead of an array:
30
+
31
+ ```ts
32
+ testResponses: async (message, bridge) => {
33
+ bridge.onTextStart()
34
+ bridge.onTextDelta('Test response')
35
+ bridge.onTextEnd()
36
+ return 'Test response'
37
+ }
38
+ ```
39
+
40
+ The `bridge` parameter gives control over text-level events, letting you simulate tool calls, reasoning steps, and multi-turn interactions. Returning a string emits it as a text block automatically; returning `undefined` emits no automatic text response.
41
+
42
+ ::: info Runtime-managed lifecycle
43
+ The runtime wraps your `TestResponseFn` with `bridge.onRunStart()` / `bridge.onRunEnd()` and step start/end calls automatically. Do not call these yourself — only use text-level bridge methods (e.g. `onTextStart`, `onTextDelta`, `onTextEnd`) or tool-level methods inside the function.
44
+ :::
45
+
46
+ ## Unit testing entity registration
47
+
48
+ ```ts
49
+ import { createEntityRegistry } from '@electric-ax/agents-runtime'
50
+
51
+ const registry = createEntityRegistry()
52
+ registerAssistant(registry)
53
+
54
+ test('registers assistant', () => {
55
+ const entry = registry.get('assistant')
56
+ expect(entry).toBeDefined()
57
+ expect(entry!.definition.handler).toBeTypeOf('function')
58
+ })
59
+ ```
60
+
61
+ ## Unit testing runtime creation
62
+
63
+ ```ts
64
+ test('creates runtime with types', () => {
65
+ const runtime = createRuntimeHandler({
66
+ baseUrl: 'http://localhost:4437',
67
+ serveEndpoint: 'http://localhost:3000/webhook',
68
+ registry,
69
+ })
70
+ expect(runtime.typeNames).toContain('assistant')
71
+ })
72
+ ```
73
+
74
+ ## Integration testing
75
+
76
+ Integration testing with the full Electric Agents server is possible using the `@electric-ax/agents-server-conformance-tests` package, which provides test server utilities for running against a live server instance.
@@ -0,0 +1,148 @@
1
+ ---
2
+ title: Waking entities
3
+ titleTemplate: '... - Electric Agents'
4
+ description: >-
5
+ How entity handlers get invoked - the triggers that produce wakes, how wake config threads through spawn/observe/observe(db(...)), and how to read a WakeEvent in a handler.
6
+ outline: [2, 3]
7
+ ---
8
+
9
+ # Waking entities
10
+
11
+ Entities in Electric Agents are driven by **wakes**. A wake is a single handler invocation triggered by something outside the handler: a new message, a child finishing, a change in an observed stream, or a schedule. Between wakes the entity is idle — no process, no memory, no running handler.
12
+
13
+ Everything you do to make an entity respond to something — `ctx.spawn(..., { wake })`, `ctx.observe(..., { wake })`, `ctx.send()`, `upsertCronSchedule()` — is ultimately a way to produce a wake.
14
+
15
+ ## The mental model
16
+
17
+ ```
18
+ external event ─► wake entry (persisted) ─► handler invocation ─► WakeEvent passed to handler
19
+ ```
20
+
21
+ 1. **External event.** A message arrives, a child transitions, a watched collection changes, a cron fires.
22
+ 2. **Wake entry is persisted** to the entity's stream. This is the durability guarantee — wakes survive process restarts, network blips, and crashes. A wake that was written will eventually be delivered to a handler.
23
+ 3. **Handler is invoked.** The runtime picks up the wake, loads the entity's state, and calls your handler with a `WakeEvent` describing what triggered this invocation.
24
+ 4. **Handler runs.** You read `ctx.events`, inspect `wake`, configure the agent, emit new events. When the handler returns (or calls `ctx.sleep()`), the entity goes idle until the next wake.
25
+
26
+ This means handlers are re-entrant: the same handler function is called fresh on every wake. Use `ctx.firstWake` for one-time initialization, and `ctx.db.actions` / `ctx.db.collections` to carry state across wakes.
27
+
28
+ ## What produces a wake
29
+
30
+ There are five things that can wake an entity:
31
+
32
+ ### 1. An incoming message
33
+
34
+ Any external `/send` (via the CLI, HTTP, or another entity's `ctx.send()`) appends a `message_received` event to the entity's stream, which wakes the handler:
35
+
36
+ ```ts
37
+ ctx.send('/assistant/peer', { text: 'hello' })
38
+ ```
39
+
40
+ The receiving handler sees `wake.type === "message_received"` and finds the payload on `wake.payload`.
41
+
42
+ ### 2. A spawned child
43
+
44
+ Pass `wake` when spawning a child to control when the parent wakes:
45
+
46
+ ```ts
47
+ const child = await ctx.spawn(
48
+ 'worker',
49
+ 'analysis-1',
50
+ { systemPrompt: 'Analyse this input.', tools: ['read'] },
51
+ {
52
+ initialMessage: 'begin',
53
+ wake: { on: 'runFinished', includeResponse: true },
54
+ }
55
+ )
56
+ ```
57
+
58
+ See the full catalog of `Wake` values in [WakeEvent](../reference/wake-event#wake).
59
+
60
+ ### 3. An observed entity
61
+
62
+ `ctx.observe()` subscribes to another entity's stream without spawning it. Pair it with a `wake` option to re-invoke this handler when the observed stream changes:
63
+
64
+ ```ts
65
+ import { entity } from '@electric-ax/agents-runtime'
66
+
67
+ await ctx.observe(entity(someEntityUrl), {
68
+ wake: { on: 'change', collections: ['status'], debounceMs: 250 },
69
+ })
70
+ ```
71
+
72
+ The `entity()` helper wraps a raw URL string into the correct observe target type.
73
+
74
+ ### 4. Shared state
75
+
76
+ `observe(db(...))` connects to a shared-state stream and, with `wake`, re-wakes the connecting entity when its collections change:
77
+
78
+ ```ts
79
+ await ctx.observe(db('board-1', schema), {
80
+ wake: { on: 'change', collections: ['findings'] },
81
+ })
82
+ ```
83
+
84
+ ### 5. A schedule
85
+
86
+ Runtime hosts can expose schedule-management tools through `ctx.electricTools`. The current schedule tool set is `list_schedules`, `upsert_cron_schedule`, `upsert_future_send`, and `delete_schedule`. Schedule entries live on the entity's manifest, so they survive restarts and can be updated or cancelled idempotently.
87
+
88
+ ## Reading a WakeEvent
89
+
90
+ Your handler signature is:
91
+
92
+ ```ts
93
+ handler(ctx: HandlerContext, wake: WakeEvent) => void | Promise<void>
94
+ ```
95
+
96
+ The minimum useful pattern is to branch on `wake.type`:
97
+
98
+ ```ts
99
+ async handler(ctx, wake) {
100
+ if (wake.type === "message_received") {
101
+ // external input - reply, dispatch, etc.
102
+ ctx.useAgent({ ... })
103
+ await ctx.agent.run()
104
+ return
105
+ }
106
+
107
+ // everything else (child finished, change, cron, timeout) arrives as type "wake".
108
+ // Inspect wake.payload for the specific sub-kind.
109
+ ctx.sleep()
110
+ }
111
+ ```
112
+
113
+ Two wake types reach handlers directly:
114
+
115
+ - `"message_received"` — an external message was delivered to this entity's inbox.
116
+ - `"wake"` — a synthesised wake for anything else (child finished, collection change, cron, timeout). The specifics are on `wake.payload`. A future-send schedule delivers a message, so it arrives as `"message_received"`.
117
+
118
+ For the full payload shape (`changes[]`, `finished_child`, `other_children`, `timeout`), see the [wake-type catalog](../reference/wake-event#wake-type-catalog) in the reference.
119
+
120
+ ## Coalescing and idempotency
121
+
122
+ Multiple external events that arrive while an entity is busy (or between acks) are coalesced into a single wake. The runtime guarantees that:
123
+
124
+ - A wake covers a contiguous range of offsets in the source stream (`wake.fromOffset`..`wake.toOffset`).
125
+ - `wake.eventCount` tells you how many new events this wake represents.
126
+ - Handlers must be safe to re-run with the same input — at-least-once delivery. Use `ctx.firstWake` and idempotent writes to collections rather than side effects on each wake.
127
+
128
+ If you need to deduplicate explicitly, key your writes by something stable (the child's entity URL, the message's producer/epoch/seq headers, etc.) and let the collection's primary key do the dedup.
129
+
130
+ ## Debounce and timeouts on `change` wakes
131
+
132
+ `{ on: 'change' }` has two knobs worth understanding:
133
+
134
+ - `debounceMs` — if set, rapid-fire changes are batched; the wake fires `debounceMs` after the last change.
135
+ - `timeoutMs` — if set, the wake fires after this interval **even if nothing changed**. Useful for heartbeat-style handlers that need to periodically check state without requiring external events.
136
+
137
+ Both are optional. If neither is set, every change produces a wake.
138
+
139
+ ## Sleeping between wakes
140
+
141
+ When the handler finishes (or calls `ctx.sleep()`), the entity returns to idle. The runtime persists the ack offset so the next wake starts from the right place. You don't have to — and shouldn't — hold resources across wakes.
142
+
143
+ ## See also
144
+
145
+ - [WakeEvent](../reference/wake-event) — full type reference and wake-type catalog.
146
+ - [Spawning & coordinating](./spawning-and-coordinating) — using `wake` with `spawn` and `observe`.
147
+ - [Shared state](./shared-state) — using `wake` with `observe(db(...))`.
148
+ - [Writing handlers](./writing-handlers) — `HandlerContext` and `firstWake` patterns.
@@ -0,0 +1,267 @@
1
+ ---
2
+ title: Writing handlers
3
+ titleTemplate: '... - Electric Agents'
4
+ description: >-
5
+ Implement entity handlers using HandlerContext and WakeEvent, with patterns for first wake, messaging, and tool use.
6
+ outline: [2, 3]
7
+ ---
8
+
9
+ # Writing handlers
10
+
11
+ The handler is the function that runs each time an entity wakes. It receives a `HandlerContext` and a `WakeEvent` describing what triggered the invocation.
12
+
13
+ ## Signature
14
+
15
+ ```ts
16
+ handler(ctx: HandlerContext, wake: WakeEvent) => void | Promise<void>
17
+ ```
18
+
19
+ ## HandlerContext
20
+
21
+ ```ts
22
+ interface HandlerContext<TState extends StateProxy = StateProxy> {
23
+ firstWake: boolean
24
+ tags: Readonly<EntityTags>
25
+ entityUrl: string
26
+ entityType: string
27
+ args: Readonly<Record<string, unknown>>
28
+ db: EntityStreamDBWithActions
29
+ state: TState
30
+ events: Array<ChangeEvent>
31
+ actions: Record<string, (...args: unknown[]) => unknown>
32
+ electricTools: AgentTool[]
33
+ useAgent: (config: AgentConfig) => AgentHandle
34
+ useContext: (config: UseContextConfig) => void
35
+ timelineMessages: (opts?: TimelineProjectionOpts) => Array<TimestampedMessage>
36
+ insertContext: (id: string, entry: ContextEntryInput) => void
37
+ removeContext: (id: string) => void
38
+ getContext: (id: string) => ContextEntry | undefined
39
+ listContext: () => Array<ContextEntry>
40
+ agent: AgentHandle
41
+ spawn: (
42
+ type: string,
43
+ id: string,
44
+ args?: Record<string, unknown>,
45
+ opts?: {
46
+ initialMessage?: unknown
47
+ wake?: Wake
48
+ tags?: Record<string, string>
49
+ observe?: boolean
50
+ }
51
+ ) => Promise<EntityHandle>
52
+ observe: (
53
+ source: ObservationSource,
54
+ opts?: { wake?: Wake }
55
+ ) => Promise<EntityHandle | SharedStateHandle | ObservationHandle>
56
+ mkdb: <T extends SharedStateSchemaMap>(
57
+ id: string,
58
+ schema: T
59
+ ) => SharedStateHandle<T>
60
+ send: (
61
+ entityUrl: string,
62
+ payload: unknown,
63
+ opts?: { type?: string; afterMs?: number }
64
+ ) => void
65
+ setTag: (key: string, value: string) => Promise<void>
66
+ removeTag: (key: string) => Promise<void>
67
+ sleep: () => void
68
+ }
69
+ ```
70
+
71
+ ### Property reference
72
+
73
+ | Property | Description |
74
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
75
+ | `firstWake` | `true` on the entity's first activation ever. Use for initialization. |
76
+ | `tags` | Entity tags -- key/value metadata associated with this entity. |
77
+ | `entityUrl` | The entity's URL path, e.g. `"/assistant/my-chat"`. |
78
+ | `entityType` | The registered type name, e.g. `"assistant"`. |
79
+ | `args` | Arguments passed when the entity was spawned. Immutable. |
80
+ | `db` | The entity's stream database. Use `db.actions` for writes and `db.collections` for reads. |
81
+ | `state` | Proxy object keyed by collection name. Each property is a [`StateCollectionProxy`](../reference/state-collection-proxy). |
82
+ | `events` | Change events that triggered this wake. |
83
+ | `actions` | Custom non-CRUD action functions from the entity definition's `actions` factory. |
84
+ | `electricTools` | Host-provided runtime-level tools to pass to `useAgent` when needed. May be empty. |
85
+ | `useAgent` | Configures the LLM agent. Returns an `AgentHandle`. See [Configuring the agent](./configuring-the-agent). |
86
+ | `useContext` | Declares context sources with token budgets and cache tiers. See [Context composition](./context-composition). |
87
+ | `timelineMessages` | Projects the entity timeline into LLM messages. See [Context composition](./context-composition#timelinemessages). |
88
+ | `insertContext` | Inserts a durable context entry. See [Context composition](./context-composition#context-entries). |
89
+ | `removeContext` | Removes a context entry by id. |
90
+ | `getContext` | Gets a context entry by id, or `undefined` if not found. |
91
+ | `listContext` | Lists all context entries. |
92
+ | `agent` | The configured agent handle. Call `agent.run()` to start the agent loop. |
93
+ | `spawn` | Creates a child entity. See [Spawning and coordinating](./spawning-and-coordinating). |
94
+ | `observe` | Connects to another entity's stream or shared db. See [Reactive observers](../entities/patterns/reactive-observers) and [Shared state](./shared-state). |
95
+ | `mkdb` | Creates a new shared state stream. See [Shared state](./shared-state). |
96
+ | `send` | Sends a message to another entity's inbox. Supports delayed delivery via `afterMs`. |
97
+ | `setTag` | Sets a tag on this entity. |
98
+ | `removeTag` | Removes a tag from this entity. |
99
+ | `sleep` | Returns the entity to idle without re-waking. |
100
+
101
+ ## WakeEvent
102
+
103
+ Describes what triggered this handler invocation.
104
+
105
+ ```ts
106
+ type WakeEvent = {
107
+ source: string
108
+ type: string
109
+ fromOffset: number
110
+ toOffset: number
111
+ eventCount: number
112
+ payload?: unknown
113
+ summary?: string
114
+ fullRef?: string
115
+ }
116
+ ```
117
+
118
+ | Field | Description |
119
+ | ------------ | ------------------------------------------------------------------------------------------------------------------------------ |
120
+ | `source` | The stream or entity that caused the wake. |
121
+ | `type` | The wake type: `"message_received"` for inbox messages or `"wake"` for child completion, observed changes, cron, and timeouts. |
122
+ | `fromOffset` | Start offset of the events that triggered this wake. |
123
+ | `toOffset` | End offset of the events that triggered this wake. |
124
+ | `eventCount` | Number of new events since last wake. |
125
+ | `payload` | Optional payload from the trigger event. |
126
+ | `summary` | Optional human-readable summary. |
127
+ | `fullRef` | Optional full reference string for the trigger. |
128
+
129
+ ## Typical handler pattern
130
+
131
+ Most handlers follow the same structure: initialize state on first wake, configure the agent, run the agent.
132
+
133
+ ```ts
134
+ registry.define('assistant', {
135
+ description: 'A general-purpose assistant',
136
+ state: {
137
+ status: { primaryKey: 'key' },
138
+ },
139
+
140
+ async handler(ctx) {
141
+ if (ctx.firstWake) {
142
+ ctx.db.actions.status_insert({ row: { key: 'current', value: 'idle' } })
143
+ }
144
+
145
+ ctx.useAgent({
146
+ systemPrompt: 'You are a helpful assistant.',
147
+ model: 'claude-sonnet-4-5-20250929',
148
+ tools: [...ctx.electricTools],
149
+ })
150
+ await ctx.agent.run()
151
+ },
152
+ })
153
+ ```
154
+
155
+ ## AgentConfig
156
+
157
+ Passed to `ctx.useAgent()`:
158
+
159
+ ```ts
160
+ interface AgentConfig {
161
+ systemPrompt: string
162
+ model: string | Model<any>
163
+ provider?: KnownProvider
164
+ tools: AgentTool[]
165
+ streamFn?: StreamFn
166
+ testResponses?: string[] | TestResponseFn
167
+ }
168
+ ```
169
+
170
+ ## firstWake
171
+
172
+ `ctx.firstWake` is `true` only on the entity's very first activation. Use it for one-time initialization:
173
+
174
+ ```ts
175
+ async handler(ctx) {
176
+ if (ctx.firstWake) {
177
+ ctx.db.actions.status_insert({ row: { key: 'current', value: 'idle' } })
178
+ ctx.db.actions.counters_insert({ row: { key: 'runs', value: 0 } })
179
+ }
180
+ // ...
181
+ }
182
+ ```
183
+
184
+ On subsequent wakes (new messages, child completion, etc.), `firstWake` is `false`.
185
+
186
+ ## sleep
187
+
188
+ Call `ctx.sleep()` to return the entity to idle without triggering a re-wake. The handler exits and the entity waits for the next external event.
189
+
190
+ ```ts
191
+ async handler(ctx, wake) {
192
+ if (wake.type === 'some-condition') {
193
+ // Nothing to do right now
194
+ ctx.sleep()
195
+ return
196
+ }
197
+ // Otherwise, run the agent
198
+ ctx.useAgent({ ... })
199
+ await ctx.agent.run()
200
+ }
201
+ ```
202
+
203
+ ## Using spawn args
204
+
205
+ Arguments passed at spawn time are available as `ctx.args`. This is how you parameterize entity behavior:
206
+
207
+ ```ts
208
+ // Spawning side
209
+ const child = await ctx.spawn('worker', 'analysis-1', {
210
+ systemPrompt: 'You are an analyst.',
211
+ tools: ['read'],
212
+ })
213
+
214
+ // Worker handler
215
+ async handler(ctx) {
216
+ const { systemPrompt } = ctx.args as { systemPrompt: string }
217
+ ctx.useAgent({
218
+ systemPrompt,
219
+ model: 'claude-sonnet-4-5-20250929',
220
+ tools: [...ctx.electricTools],
221
+ })
222
+ await ctx.agent.run()
223
+ }
224
+ ```
225
+
226
+ ## Adding custom tools
227
+
228
+ Combine `ctx.electricTools` with custom tools:
229
+
230
+ ```ts
231
+ async handler(ctx) {
232
+ const myTool: AgentTool = {
233
+ name: 'lookup',
234
+ label: 'Lookup',
235
+ description: 'Looks up a value by key',
236
+ parameters: Type.Object({
237
+ key: Type.String({ description: 'The key to look up' }),
238
+ }),
239
+ execute: async (_toolCallId, params) => {
240
+ const { key } = params as { key: string }
241
+ const row = ctx.db.collections.kv?.get(key)
242
+ return {
243
+ content: [{ type: 'text', text: row ? JSON.stringify(row) : 'Not found' }],
244
+ details: {},
245
+ }
246
+ },
247
+ }
248
+
249
+ ctx.useAgent({
250
+ systemPrompt: 'You are an assistant with lookup capabilities.',
251
+ model: 'claude-sonnet-4-5-20250929',
252
+ tools: [...ctx.electricTools, myTool],
253
+ })
254
+ await ctx.agent.run()
255
+ }
256
+ ```
257
+
258
+ ## Sending messages
259
+
260
+ Use `ctx.send()` to deliver a message to another entity's inbox:
261
+
262
+ ```ts
263
+ ctx.send('/worker/task-1', { action: 'process', data: payload })
264
+ ctx.send('/worker/task-1', payload, { type: 'custom_type' })
265
+ ```
266
+
267
+ The target entity will be woken to process the message.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electric-ax/agents",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Built-in Electric Agents runtimes such as Horton and worker",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,6 +52,7 @@
52
52
  },
53
53
  "files": [
54
54
  "dist",
55
+ "docs",
55
56
  "skills"
56
57
  ],
57
58
  "sideEffects": false,