@electric-ax/agents 0.1.5 → 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 +653 -408
- package/dist/index.cjs +671 -419
- package/dist/index.d.cts +36 -3
- package/dist/index.d.ts +36 -3
- package/dist/index.js +656 -411
- 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 +6 -9
- package/skills/init.md +71 -0
- package/skills/quickstart/scaffold/package.json +30 -0
- package/skills/{tutorial → 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 +582 -0
- package/skills/tutorial/scaffold/package.json +0 -17
- package/skills/tutorial.md +0 -282
- /package/skills/{tutorial → quickstart}/scaffold/entities/.gitkeep +0 -0
- /package/skills/{tutorial → quickstart}/scaffold/lib/electric-tools.ts +0 -0
- /package/skills/{tutorial → quickstart}/scaffold/server.ts +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Horton agent
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
The built-in Horton assistant - chat, research, code, and dispatch subagents in one entity type.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Horton agent
|
|
10
|
+
|
|
11
|
+
The built-in assistant registered by the Electric Agents dev server. Horton can chat conversationally, search the web, read and edit files, run shell commands, and dispatch subagents (workers) for isolated subtasks.
|
|
12
|
+
|
|
13
|
+
**Source:** [`packages/agents/src/agents/horton.ts`](https://github.com/electric-sql/electric/blob/main/packages/agents/src/agents/horton.ts)
|
|
14
|
+
|
|
15
|
+
## Try it
|
|
16
|
+
|
|
17
|
+
With the dev server running (`npx electric-ax agents quickstart`):
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx electric-ax agents spawn /horton/my-horton
|
|
21
|
+
npx electric-ax agents send /horton/my-horton 'What's in this directory?'
|
|
22
|
+
npx electric-ax agents observe /horton/my-horton
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Tools
|
|
26
|
+
|
|
27
|
+
Horton is configured with `ctx.electricTools` plus the base Horton tool set:
|
|
28
|
+
|
|
29
|
+
| Tool | Purpose |
|
|
30
|
+
| -------------- | -------------------------------------------------------- |
|
|
31
|
+
| `bash` | Run shell commands in the working directory. |
|
|
32
|
+
| `read` | Read a file. Tracked in a per-wake `readSet`. |
|
|
33
|
+
| `write` | Create or overwrite a file. |
|
|
34
|
+
| `edit` | Targeted string replacement (file must be `read` first). |
|
|
35
|
+
| `brave_search` | Web search via the Brave Search API. |
|
|
36
|
+
| `fetch_url` | Fetch a URL and return it as markdown. |
|
|
37
|
+
| `spawn_worker` | Dispatch a subagent for an isolated subtask. |
|
|
38
|
+
|
|
39
|
+
`brave_search` requires `BRAVE_SEARCH_API_KEY` in the environment; without it the tool errors at call time.
|
|
40
|
+
|
|
41
|
+
When docs support or skills are available, Horton also adds the docs search tool and skill tools during bootstrap.
|
|
42
|
+
|
|
43
|
+
## Title generation
|
|
44
|
+
|
|
45
|
+
After the first agent run completes, Horton calls `generateTitle()` (Haiku) to summarise the user's first message into a 3-5 word session title and stores it via `ctx.setTag('title', title)`. Failures are logged and ignored — the entity continues without a title.
|
|
46
|
+
|
|
47
|
+
## Details
|
|
48
|
+
|
|
49
|
+
| Property | Value |
|
|
50
|
+
| ----------------- | --------------------------------------------------------------------------------- |
|
|
51
|
+
| Type name | `horton` |
|
|
52
|
+
| Model | `HORTON_MODEL` (`claude-sonnet-4-5-20250929`) |
|
|
53
|
+
| Title model | `claude-haiku-4-5-20251001` |
|
|
54
|
+
| Tools | `ctx.electricTools` + base Horton tool set, plus docs/skill tools when configured |
|
|
55
|
+
| Working directory | Passed at bootstrap (defaults to `process.cwd()`) |
|
|
56
|
+
| Title generation | Yes, after the first run if no title tag exists |
|
|
57
|
+
|
|
58
|
+
## Extending Horton
|
|
59
|
+
|
|
60
|
+
The system prompt and tool factory are exported so you can build your own variants:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import {
|
|
64
|
+
HORTON_MODEL,
|
|
65
|
+
buildHortonSystemPrompt,
|
|
66
|
+
createHortonTools,
|
|
67
|
+
} from '@electric-ax/agents'
|
|
68
|
+
|
|
69
|
+
registry.define('my-assistant', {
|
|
70
|
+
description: 'Horton with an extra custom tool',
|
|
71
|
+
async handler(ctx) {
|
|
72
|
+
const readSet = new Set<string>()
|
|
73
|
+
ctx.useAgent({
|
|
74
|
+
systemPrompt: buildHortonSystemPrompt(process.cwd()),
|
|
75
|
+
model: HORTON_MODEL,
|
|
76
|
+
tools: [
|
|
77
|
+
...ctx.electricTools,
|
|
78
|
+
...createHortonTools(process.cwd(), ctx, readSet),
|
|
79
|
+
myCustomTool,
|
|
80
|
+
],
|
|
81
|
+
})
|
|
82
|
+
await ctx.agent.run()
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Related
|
|
88
|
+
|
|
89
|
+
- [Worker](./worker) — the subagent type Horton dispatches via `spawn_worker`.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Worker
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Generic sandboxed subagent type. Spawned by Horton (or any agent) via the spawn_worker tool with a system prompt and a chosen tool subset.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Worker
|
|
10
|
+
|
|
11
|
+
A generic, sandboxed subagent type. Workers are spawned by other agents (typically [Horton](./horton)) via the `spawn_worker` tool — the spawner provides a system prompt and picks the subset of tools the worker should have access to.
|
|
12
|
+
|
|
13
|
+
**Source:** [`packages/agents/src/agents/worker.ts`](https://github.com/electric-sql/electric/blob/main/packages/agents/src/agents/worker.ts)
|
|
14
|
+
|
|
15
|
+
## Spawn args
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface WorkerArgs {
|
|
19
|
+
systemPrompt: string
|
|
20
|
+
tools?: Array<WorkerToolName>
|
|
21
|
+
sharedDb?: { id: string; schema: SharedStateSchemaMap }
|
|
22
|
+
sharedDbToolMode?: 'full' | 'write-only'
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Field | Required | Description |
|
|
27
|
+
| ------------------ | -------- | --------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `systemPrompt` | Yes | The worker's system prompt. Brief it like a colleague: file paths, line numbers, deliverable. |
|
|
29
|
+
| `tools` | No | Subset of valid tool names (see below). Unknown names throw at parse time. |
|
|
30
|
+
| `sharedDb` | No | Shared state stream id and schema to connect to. |
|
|
31
|
+
| `sharedDbToolMode` | No | Shared state tool mode: `"full"` (default) or `"write-only"`. |
|
|
32
|
+
|
|
33
|
+
`registerWorker(registry, { workingDirectory, streamFn? })` is called by the dev server during bootstrap; you don't usually call it yourself.
|
|
34
|
+
|
|
35
|
+
## Valid tool names
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
type WorkerToolName =
|
|
39
|
+
| 'bash'
|
|
40
|
+
| 'read'
|
|
41
|
+
| 'write'
|
|
42
|
+
| 'edit'
|
|
43
|
+
| 'brave_search'
|
|
44
|
+
| 'fetch_url'
|
|
45
|
+
| 'spawn_worker'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
These are the same primitives Horton uses. Pick the smallest subset the worker needs — tools are the worker's permission set.
|
|
49
|
+
|
|
50
|
+
The worker must receive at least one tool or a `sharedDb` config. If `sharedDb` is provided, the worker gets generated shared-state tools in addition to any selected primitives.
|
|
51
|
+
|
|
52
|
+
## Spawning a worker
|
|
53
|
+
|
|
54
|
+
The canonical way to spawn a worker is the `spawn_worker` tool, which Horton calls from inside its agent loop:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
spawn_worker({
|
|
58
|
+
systemPrompt:
|
|
59
|
+
'You are a focused researcher. Find the three most-cited papers on X and return their titles, authors, and DOIs as a markdown table.',
|
|
60
|
+
tools: ['brave_search', 'fetch_url'],
|
|
61
|
+
initialMessage: 'Begin research now.',
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| Field | Required | Notes |
|
|
66
|
+
| ---------------- | -------- | ----------------------------------------------------------------------------- |
|
|
67
|
+
| `systemPrompt` | Yes | Sets persona and constraints. |
|
|
68
|
+
| `tools` | Yes | Subset of `WorkerToolName`. Must contain at least one entry. |
|
|
69
|
+
| `initialMessage` | Yes | First user message. **Without this the worker idles** — nothing kicks it off. |
|
|
70
|
+
|
|
71
|
+
The spawn uses `wake: { on: 'runFinished', includeResponse: true }`, so the spawner wakes when the worker finishes its run and receives the worker's response in the wake message.
|
|
72
|
+
|
|
73
|
+
## What the handler does
|
|
74
|
+
|
|
75
|
+
1. Parses `ctx.args` into `WorkerArgs`. Throws if `systemPrompt` is empty, if `tools` contains an unknown name, or if neither `tools` nor `sharedDb` is provided.
|
|
76
|
+
2. Builds the requested tool instances against the worker's `workingDirectory` (and a fresh per-wake `readSet` for the read-first-then-edit guard).
|
|
77
|
+
3. If `sharedDb` is present, connects with `ctx.observe(db(id, schema))` and exposes generated `read_*`, `write_*`, `update_*`, and `delete_*` tools (`write_*` only in `"write-only"` mode).
|
|
78
|
+
4. Configures the agent with `HORTON_MODEL` (`claude-sonnet-4-5-20250929`), the provided system prompt (with a brief reporting-back footer appended), and the assembled tool list.
|
|
79
|
+
5. Runs the agent until the LLM stops.
|
|
80
|
+
|
|
81
|
+
::: warning Least-privilege sandbox
|
|
82
|
+
Workers deliberately do **not** receive `ctx.electricTools`. The spawner already picked the worker's tool subset; granting entity-runtime primitives (cron, schedule, send-to-arbitrary-entity) would let a worker escape that scope. If a worker needs those primitives, it must spawn its own subagent or report back to the spawner.
|
|
83
|
+
:::
|
|
84
|
+
|
|
85
|
+
## Reporting footer
|
|
86
|
+
|
|
87
|
+
The runtime appends this to every worker's system prompt:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
# Reporting back
|
|
91
|
+
When you finish, respond with a concise report covering what was done and any key findings. The caller will relay this to the user, so it only needs the essentials.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Details
|
|
95
|
+
|
|
96
|
+
| Property | Value |
|
|
97
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
98
|
+
| Type name | `worker` |
|
|
99
|
+
| Model | `HORTON_MODEL` (`claude-sonnet-4-5-20250929`) |
|
|
100
|
+
| Tools | Subset of 7 primitives plus optional shared-state tools. **No `ctx.electricTools`.** |
|
|
101
|
+
| Working directory | Provided to `registerWorker` at bootstrap |
|
|
102
|
+
| Description | `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).` |
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Blackboard (shared state)
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Multi-agent coordination using shared state as a common data structure for reads and writes.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Blackboard (shared state)
|
|
10
|
+
|
|
11
|
+
Pattern: multiple agents coordinate through a shared data structure. A parent creates shared state, spawns workers that connect to it, and workers read/write shared collections via auto-generated CRUD tools.
|
|
12
|
+
|
|
13
|
+
**Source:** [`packages/agents-runtime/skills/designing-entities/references/patterns/blackboard.md`](https://github.com/electric-sql/electric/blob/main/packages/agents-runtime/skills/designing-entities/references/patterns/blackboard.md)
|
|
14
|
+
|
|
15
|
+
## Debate example
|
|
16
|
+
|
|
17
|
+
The canonical blackboard example: a moderator runs a structured debate between pro and con workers via shared state.
|
|
18
|
+
|
|
19
|
+
### Schema
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const debateSchema = {
|
|
23
|
+
arguments: {
|
|
24
|
+
schema: z.object({
|
|
25
|
+
key: z.string(),
|
|
26
|
+
side: z.enum(['pro', 'con']),
|
|
27
|
+
text: z.string(),
|
|
28
|
+
round: z.number(),
|
|
29
|
+
}),
|
|
30
|
+
type: 'shared:argument',
|
|
31
|
+
primaryKey: 'key',
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Registration
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { db } from '@electric-ax/agents-runtime'
|
|
40
|
+
|
|
41
|
+
export function registerDebate(registry: EntityRegistry) {
|
|
42
|
+
registry.define(`debate`, {
|
|
43
|
+
description: `Debate moderator that creates shared state, spawns pro and con workers, and writes a final ruling based on arguments written to shared state`,
|
|
44
|
+
state: {
|
|
45
|
+
status: { primaryKey: `key` },
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async handler(ctx) {
|
|
49
|
+
if (ctx.firstWake) {
|
|
50
|
+
ctx.db.actions.status_insert({ row: { key: `current`, value: `idle` } })
|
|
51
|
+
ctx.mkdb(`debate-${ctx.entityUrl}`, debateSchema)
|
|
52
|
+
}
|
|
53
|
+
const shared = await ctx.observe(
|
|
54
|
+
db(`debate-${ctx.entityUrl}`, debateSchema)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// ... create tools that reference `shared` ...
|
|
58
|
+
|
|
59
|
+
ctx.useAgent({
|
|
60
|
+
systemPrompt: DEBATE_SYSTEM_PROMPT,
|
|
61
|
+
model: `claude-sonnet-4-5-20250929`,
|
|
62
|
+
tools: [...ctx.electricTools, startTool, checkTool, endTool],
|
|
63
|
+
})
|
|
64
|
+
await ctx.agent.run()
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### How it works
|
|
71
|
+
|
|
72
|
+
1. On first wake, the moderator creates shared state with `ctx.mkdb()`.
|
|
73
|
+
2. The moderator connects to the shared state with `ctx.observe(db(...))`.
|
|
74
|
+
3. The `start_debate` tool spawns pro and con workers, each connected to the same shared state:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const proWorker = await ctx.spawn(
|
|
78
|
+
`worker`,
|
|
79
|
+
`debate-pro-${Date.now()}-${spawnCounter}`,
|
|
80
|
+
{
|
|
81
|
+
systemPrompt: PRO_WORKER_PROMPT,
|
|
82
|
+
sharedDb: { id: `debate-${ctx.entityUrl}`, schema: debateSchema },
|
|
83
|
+
},
|
|
84
|
+
{ initialMessage: proInitialMessage, wake: `runFinished` }
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
4. Workers write arguments to the shared `arguments` collection using auto-generated `write_arguments` tools (see [Worker](../agents/worker.md)).
|
|
89
|
+
5. The `check_debate` tool reads the shared state to see current arguments:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const args = shared.arguments.toArray
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
6. The `end_debate` tool reads all arguments and transitions to `done`.
|
|
96
|
+
|
|
97
|
+
### State transitions
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
type DebateStatus = 'idle' | 'debating' | 'ruling' | 'done'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Other blackboard variants
|
|
104
|
+
|
|
105
|
+
The same structure applies to other blackboard workflows:
|
|
106
|
+
|
|
107
|
+
- **Wiki** -- specialist workers collaboratively build a knowledge base. Each writes articles to a shared `articles` collection.
|
|
108
|
+
- **Peer review** -- workers submit reviews with scores and feedback to a shared `reviews` collection. A coordinator summarizes the reviews.
|
|
109
|
+
- **Trading floor** -- trader agents submit buy/sell orders to a shared `orders` collection and transition through market sessions.
|
|
110
|
+
|
|
111
|
+
All follow the same structure: parent creates shared state, spawns workers with `sharedDb` in their args, workers use generated CRUD tools to coordinate.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Dispatcher
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Message routing pattern that classifies incoming messages and dispatches to specialist agents.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Dispatcher
|
|
10
|
+
|
|
11
|
+
Pattern: classify incoming messages and route to the appropriate agent type.
|
|
12
|
+
|
|
13
|
+
**Source:** [`packages/agents-runtime/skills/designing-entities/references/patterns/dispatcher.md`](https://github.com/electric-sql/electric/blob/main/packages/agents-runtime/skills/designing-entities/references/patterns/dispatcher.md)
|
|
14
|
+
|
|
15
|
+
## Registration
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
export function registerDispatcher(registry: EntityRegistry) {
|
|
19
|
+
registry.define(`dispatcher`, {
|
|
20
|
+
description: `Router agent that classifies incoming messages and dispatches to the appropriate specialist agent type`,
|
|
21
|
+
|
|
22
|
+
async handler(ctx) {
|
|
23
|
+
const dispatchTool = createDispatchTool(ctx)
|
|
24
|
+
|
|
25
|
+
ctx.useAgent({
|
|
26
|
+
systemPrompt: DISPATCHER_SYSTEM_PROMPT,
|
|
27
|
+
model: `claude-sonnet-4-5-20250929`,
|
|
28
|
+
tools: [...ctx.electricTools, dispatchTool],
|
|
29
|
+
})
|
|
30
|
+
await ctx.agent.run()
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
::: info No local state
|
|
37
|
+
The dispatcher entity defines no `state` collections. It is stateless -- it relies entirely on the wake mechanism to receive child completion events and forwards specialist output to the user.
|
|
38
|
+
:::
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
The dispatcher exposes a `dispatch` tool. When the LLM classifies an incoming message, it calls the tool with:
|
|
43
|
+
|
|
44
|
+
- `type` -- the entity type to spawn (e.g. `"horton"`, `"worker"`, or an app-defined type)
|
|
45
|
+
- `systemPrompt` -- a focused prompt crafted for the task
|
|
46
|
+
- `task` -- the original message to forward
|
|
47
|
+
|
|
48
|
+
The tool then:
|
|
49
|
+
|
|
50
|
+
1. Spawns the requested entity type with `wake: 'runFinished'`.
|
|
51
|
+
2. Returns immediately with a status message. The dispatcher is re-invoked when the specialist finishes.
|
|
52
|
+
|
|
53
|
+
## Dispatch tool
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
await ctx.spawn(
|
|
57
|
+
type,
|
|
58
|
+
id,
|
|
59
|
+
type === `worker` ? { systemPrompt, tools: [`read`] } : { systemPrompt },
|
|
60
|
+
{
|
|
61
|
+
initialMessage: task,
|
|
62
|
+
wake: `runFinished`,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: `text` as const,
|
|
70
|
+
text: `Dispatched to "${type}" specialist (${id}). You will be woken when it finishes.`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
details: { id, type },
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
When dispatching to the built-in `worker`, include its required tool subset in the spawn args.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Manager-Worker
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Coordination pattern where a parent spawns specialist children, waits for completion, and synthesizes results.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Manager-Worker
|
|
10
|
+
|
|
11
|
+
Pattern: a parent agent spawns multiple specialist children, waits for all to complete, and synthesizes results.
|
|
12
|
+
|
|
13
|
+
**Source:** [`packages/agents-runtime/skills/designing-entities/references/patterns/manager-worker.md`](https://github.com/electric-sql/electric/blob/main/packages/agents-runtime/skills/designing-entities/references/patterns/manager-worker.md)
|
|
14
|
+
|
|
15
|
+
## Registration
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
export function registerManagerWorker(registry: EntityRegistry) {
|
|
19
|
+
registry.define(`manager-worker`, {
|
|
20
|
+
description: `Manager agent that spawns optimist, pessimist, and pragmatist workers to analyze any question from multiple perspectives`,
|
|
21
|
+
state: {
|
|
22
|
+
children: { schema: managerChildSchema, primaryKey: `key` },
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async handler(ctx) {
|
|
26
|
+
const analyzeTool = createAnalyzeWithPerspectivesTool(ctx)
|
|
27
|
+
|
|
28
|
+
ctx.useAgent({
|
|
29
|
+
systemPrompt: MANAGER_SYSTEM_PROMPT,
|
|
30
|
+
model: `claude-sonnet-4-5-20250929`,
|
|
31
|
+
tools: [...ctx.electricTools, analyzeTool],
|
|
32
|
+
})
|
|
33
|
+
await ctx.agent.run()
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
The manager defines a handler-scoped tool called `analyze_with_perspectives`. When the LLM calls this tool, it:
|
|
42
|
+
|
|
43
|
+
1. Spawns 3 worker children -- optimist, pessimist, pragmatist -- each with a different system prompt.
|
|
44
|
+
2. Sends the same question to all three as `initialMessage`.
|
|
45
|
+
3. Uses `wake: 'runFinished'` to wait for each child to complete.
|
|
46
|
+
4. Collects results with `Promise.all` and `handle.text()`.
|
|
47
|
+
5. Returns the combined perspectives to the LLM for synthesis.
|
|
48
|
+
|
|
49
|
+
On subsequent calls, the tool reuses existing children via `ctx.observe()` and `child.send()` instead of spawning new ones.
|
|
50
|
+
|
|
51
|
+
## Spawn-or-reuse pattern
|
|
52
|
+
|
|
53
|
+
The core of the tool -- first-call spawns, subsequent calls reuse:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
for (const perspective of PERSPECTIVES) {
|
|
57
|
+
const existing = children.get(perspective.id)
|
|
58
|
+
const childId = `${parentId}-${perspective.id}`
|
|
59
|
+
|
|
60
|
+
if (!existing?.url) {
|
|
61
|
+
// First time: spawn a new worker
|
|
62
|
+
const child = await ctx.spawn(
|
|
63
|
+
`worker`,
|
|
64
|
+
childId,
|
|
65
|
+
{ systemPrompt: perspective.systemPrompt, tools: [`read`] },
|
|
66
|
+
{ initialMessage: question, wake: `runFinished` }
|
|
67
|
+
)
|
|
68
|
+
children.insert({
|
|
69
|
+
key: perspective.id,
|
|
70
|
+
url: child.entityUrl,
|
|
71
|
+
kind: perspective.id,
|
|
72
|
+
question,
|
|
73
|
+
})
|
|
74
|
+
handles.push({ id: perspective.id, handle: child })
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Subsequent calls: observe existing child and send new question
|
|
79
|
+
const child = await ctx.observe(entity(existing.url))
|
|
80
|
+
child.send(question)
|
|
81
|
+
children.update(perspective.id, (draft) => {
|
|
82
|
+
draft.question = question
|
|
83
|
+
})
|
|
84
|
+
handles.push({ id: perspective.id, handle: child })
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Collecting results
|
|
89
|
+
|
|
90
|
+
The `readLatestCompletedText` helper awaits the child's current run, reads all text outputs, and returns the last one:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
async function readLatestCompletedText(
|
|
94
|
+
handle: EntityHandle,
|
|
95
|
+
fallback: string
|
|
96
|
+
): Promise<string> {
|
|
97
|
+
await handle.run
|
|
98
|
+
const runs = await handle.text()
|
|
99
|
+
const latest = runs[runs.length - 1]?.trim()
|
|
100
|
+
return latest || fallback
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const results = await Promise.all(
|
|
104
|
+
handles.map(async ({ id, handle }) => ({
|
|
105
|
+
id,
|
|
106
|
+
text: await readLatestCompletedText(
|
|
107
|
+
handle,
|
|
108
|
+
`(no completed output from ${id})`
|
|
109
|
+
),
|
|
110
|
+
}))
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## State
|
|
115
|
+
|
|
116
|
+
The `children` collection tracks spawned workers:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const managerChildSchema = z.object({
|
|
120
|
+
key: z.string(),
|
|
121
|
+
url: z.string(),
|
|
122
|
+
kind: z.string(),
|
|
123
|
+
question: z.string(),
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This allows the manager to find and reuse children across handler invocations.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Map-Reduce
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Parallel processing pattern that splits work into chunks, processes simultaneously, and reduces results.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Map-Reduce
|
|
10
|
+
|
|
11
|
+
Pattern: split input into chunks, process all in parallel, collect results.
|
|
12
|
+
|
|
13
|
+
**Source:** [`packages/agents-runtime/skills/designing-entities/references/patterns/map-reduce.md`](https://github.com/electric-sql/electric/blob/main/packages/agents-runtime/skills/designing-entities/references/patterns/map-reduce.md)
|
|
14
|
+
|
|
15
|
+
## Registration
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
export function registerMapReduce(registry: EntityRegistry) {
|
|
19
|
+
registry.define(`map-reduce`, {
|
|
20
|
+
description: `Map-reduce orchestrator that splits input into chunks, processes them in parallel with worker agents, then synthesizes results`,
|
|
21
|
+
state: {
|
|
22
|
+
children: { primaryKey: `key` },
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async handler(ctx) {
|
|
26
|
+
ctx.useAgent({
|
|
27
|
+
systemPrompt: MAP_REDUCE_SYSTEM_PROMPT,
|
|
28
|
+
model: `claude-sonnet-4-5-20250929`,
|
|
29
|
+
tools: [...ctx.electricTools, createMapChunksTool(ctx)],
|
|
30
|
+
})
|
|
31
|
+
await ctx.agent.run()
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
The agent exposes a `map_chunks` tool. When called:
|
|
40
|
+
|
|
41
|
+
1. **Map phase** -- spawns one worker per chunk simultaneously. All workers run in parallel.
|
|
42
|
+
2. Returns immediately. The entity is re-invoked as each worker finishes.
|
|
43
|
+
3. The LLM synthesizes results once all workers have reported in via wake events.
|
|
44
|
+
|
|
45
|
+
## Core
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// Map phase - spawn all workers in parallel
|
|
49
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
50
|
+
spawnCounter++
|
|
51
|
+
const id = `chunk-${i}-${Date.now()}-${spawnCounter}`
|
|
52
|
+
const child = await ctx.spawn(
|
|
53
|
+
`worker`,
|
|
54
|
+
id,
|
|
55
|
+
{ systemPrompt: task, tools: [`read`] },
|
|
56
|
+
{
|
|
57
|
+
initialMessage: chunks[i],
|
|
58
|
+
wake: `runFinished`,
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
ctx.db.actions.children_insert({
|
|
62
|
+
row: { key: id, url: child.entityUrl, chunk: i },
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: `text` as const,
|
|
70
|
+
text: `Spawned ${chunks.length} parallel workers. You will be woken as each finishes with its output in finished_child.response.`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
details: { chunkCount: chunks.length },
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## State collections
|
|
78
|
+
|
|
79
|
+
| Collection | Purpose |
|
|
80
|
+
| ---------- | ---------------------------------------------- |
|
|
81
|
+
| `children` | Spawned chunk workers (key, URL, chunk index). |
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Pipeline
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Sequential processing pattern where each stage's output feeds into the next via state transitions.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Pipeline
|
|
10
|
+
|
|
11
|
+
Pattern: sequential stages where each stage's output feeds into the next.
|
|
12
|
+
|
|
13
|
+
**Source:** [`packages/agents-runtime/skills/designing-entities/references/patterns/pipeline.md`](https://github.com/electric-sql/electric/blob/main/packages/agents-runtime/skills/designing-entities/references/patterns/pipeline.md)
|
|
14
|
+
|
|
15
|
+
## Registration
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
export function registerPipeline(registry: EntityRegistry) {
|
|
19
|
+
registry.define(`pipeline`, {
|
|
20
|
+
description: `Pipeline orchestrator that chains sequential worker stages, feeding each stage output into the next`,
|
|
21
|
+
state: {
|
|
22
|
+
children: { primaryKey: `key` },
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async handler(ctx) {
|
|
26
|
+
ctx.useAgent({
|
|
27
|
+
systemPrompt: PIPELINE_SYSTEM_PROMPT,
|
|
28
|
+
model: `claude-sonnet-4-5-20250929`,
|
|
29
|
+
tools: [...ctx.electricTools, createRunStageTool(ctx)],
|
|
30
|
+
})
|
|
31
|
+
await ctx.agent.run()
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
The pipeline agent exposes a `run_stage` tool. The LLM drives the pipeline one stage at a time:
|
|
40
|
+
|
|
41
|
+
1. The LLM calls `run_stage` with an instruction and input for the current stage.
|
|
42
|
+
2. The tool spawns a worker with the instruction as its system prompt and the input as `initialMessage`, using `wake: 'runFinished'`.
|
|
43
|
+
3. The tool returns immediately. The pipeline entity is re-invoked when the worker finishes.
|
|
44
|
+
4. On each re-invocation, the wake event contains `finished_child.response` with the stage's output. The LLM then calls `run_stage` again with the next stage's instruction and the previous output as input.
|
|
45
|
+
5. This repeats until all stages are complete.
|
|
46
|
+
|
|
47
|
+
## Stage tool
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
function createRunStageTool(ctx: HandlerContext): AgentTool {
|
|
51
|
+
let stageCount = 0
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
name: `run_stage`,
|
|
55
|
+
label: `Run Stage`,
|
|
56
|
+
description: `Spawns a worker for one pipeline stage.`,
|
|
57
|
+
parameters: Type.Object({
|
|
58
|
+
instruction: Type.String({
|
|
59
|
+
description: `The instruction for this stage.`,
|
|
60
|
+
}),
|
|
61
|
+
input: Type.String({ description: `The input for this stage.` }),
|
|
62
|
+
}),
|
|
63
|
+
execute: async (_toolCallId, params) => {
|
|
64
|
+
const { instruction, input } = params as {
|
|
65
|
+
instruction: string
|
|
66
|
+
input: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
stageCount++
|
|
70
|
+
const parentId = entityIdFromUrl(ctx.entityUrl)
|
|
71
|
+
const id = `${parentId}-stage-${stageCount}`
|
|
72
|
+
|
|
73
|
+
const child = await ctx.spawn(
|
|
74
|
+
`worker`,
|
|
75
|
+
id,
|
|
76
|
+
{ systemPrompt: instruction, tools: [`read`] },
|
|
77
|
+
{ initialMessage: input, wake: `runFinished` }
|
|
78
|
+
)
|
|
79
|
+
ctx.db.actions.children_insert({
|
|
80
|
+
row: { key: id, url: child.entityUrl, stage: stageCount },
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: `text` as const,
|
|
87
|
+
text: `Stage ${stageCount} spawned. You will be woken when it finishes.`,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
details: { stage: stageCount },
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## State collections
|
|
98
|
+
|
|
99
|
+
| Collection | Purpose |
|
|
100
|
+
| ---------- | --------------------------------------------------- |
|
|
101
|
+
| `children` | Spawned worker references (key, URL, stage number). |
|