@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
package/dist/entrypoint.js
CHANGED
|
@@ -796,6 +796,8 @@ function resolveDocsRoot(workingDirectory) {
|
|
|
796
796
|
process.env.HORTON_DOCS_ROOT,
|
|
797
797
|
path.resolve(workingDirectory, `electric-agents-docs/docs`),
|
|
798
798
|
path.resolve(process.cwd(), `electric-agents-docs/docs`),
|
|
799
|
+
path.resolve(MODULE_DIR, `../docs`),
|
|
800
|
+
path.resolve(MODULE_DIR, `../../docs`),
|
|
799
801
|
path.resolve(MODULE_DIR, `../../../../../electric-agents-docs/docs`)
|
|
800
802
|
].filter((value) => typeof value === `string`);
|
|
801
803
|
for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
|
|
@@ -1637,7 +1639,7 @@ async function generateTitle(userMessage, llmCall = defaultHaikuCall) {
|
|
|
1637
1639
|
function buildHortonSystemPrompt(workingDirectory, opts = {}) {
|
|
1638
1640
|
const docsTools = opts.hasDocsSupport ? `\n- search_durable_agents_docs: hybrid search over the built-in Durable Agents docs index` : ``;
|
|
1639
1641
|
const skillsTools = opts.hasSkills ? `\n- use_skill: load a skill (knowledge, instructions, or a tutorial) into your context to help with the user's request\n- remove_skill: unload a skill from context when you're done with it` : ``;
|
|
1640
|
-
const docsGuidance = opts.hasDocsSupport ? `\n-
|
|
1642
|
+
const docsGuidance = opts.hasDocsSupport ? `\n- For ANY question about Electric Agents, Durable Agents, or this framework, ALWAYS use search_durable_agents_docs FIRST. Do not use brave_search or fetch_url for Electric Agents topics unless the docs search returns no useful results.\n- The search tool returns chunk content directly — you do not need to read the source files.\n- Use repo read/bash tools only for non-doc files or when you need to inspect exact implementation code in the workspace.` : ``;
|
|
1641
1643
|
const skillsGuidance = opts.hasSkills ? `\n# Skills\nYou have access to skills — specialized knowledge and guided workflows you can load on demand. Your context includes a skills catalog listing what's available. When the user's request matches a skill's description or keywords, load it with use_skill.
|
|
1642
1644
|
|
|
1643
1645
|
Some skills are user-invocable — the user can trigger them with a slash command like \`/quickstart\`. When you see a message starting with \`/\` followed by a skill name, load that skill immediately with use_skill. Pass any text after the skill name as args.
|
|
@@ -1656,7 +1658,7 @@ Do NOT load a skill and then ignore its instructions. The skill is there because
|
|
|
1656
1658
|
When a user is new or asks how to get started with Electric Agents, **don't assume a single path**. Present the options and let them choose:
|
|
1657
1659
|
|
|
1658
1660
|
- **Learn the concepts first** → Explain what Electric Agents is, answer questions, point to docs.
|
|
1659
|
-
Use
|
|
1661
|
+
Use search_durable_agents_docs to look up answers. Only load the quickstart skill if the user explicitly asks for a hands-on guided tutorial.
|
|
1660
1662
|
|
|
1661
1663
|
- **Hands-on guided tutorial** → Load the quickstart skill (or tell them to type \`/quickstart\`).
|
|
1662
1664
|
This is a step-by-step build that takes them from zero to a running app.
|
|
@@ -1666,7 +1668,7 @@ When a user is new or asks how to get started with Electric Agents, **don't assu
|
|
|
1666
1668
|
This sets up project structure and orients them in the codebase.
|
|
1667
1669
|
|
|
1668
1670
|
- **Have a specific question?** → Answer it directly.
|
|
1669
|
-
Use
|
|
1671
|
+
Use search_durable_agents_docs first, then fall back to fetch_url or general knowledge if needed.
|
|
1670
1672
|
|
|
1671
1673
|
Don't force onboarding. If someone just wants to chat or code, let them. When in doubt, ask what they'd like to do rather than picking a path for them.`;
|
|
1672
1674
|
const docsUrlGuidance = opts.docsUrl ? `\n# Electric Agents documentation
|
package/dist/index.cjs
CHANGED
|
@@ -819,6 +819,8 @@ function resolveDocsRoot(workingDirectory) {
|
|
|
819
819
|
process.env.HORTON_DOCS_ROOT,
|
|
820
820
|
node_path.default.resolve(workingDirectory, `electric-agents-docs/docs`),
|
|
821
821
|
node_path.default.resolve(process.cwd(), `electric-agents-docs/docs`),
|
|
822
|
+
node_path.default.resolve(MODULE_DIR, `../docs`),
|
|
823
|
+
node_path.default.resolve(MODULE_DIR, `../../docs`),
|
|
822
824
|
node_path.default.resolve(MODULE_DIR, `../../../../../electric-agents-docs/docs`)
|
|
823
825
|
].filter((value) => typeof value === `string`);
|
|
824
826
|
for (const candidate of candidates) if (node_fs.default.existsSync(candidate)) return candidate;
|
|
@@ -1660,7 +1662,7 @@ async function generateTitle(userMessage, llmCall = defaultHaikuCall) {
|
|
|
1660
1662
|
function buildHortonSystemPrompt(workingDirectory, opts = {}) {
|
|
1661
1663
|
const docsTools = opts.hasDocsSupport ? `\n- search_durable_agents_docs: hybrid search over the built-in Durable Agents docs index` : ``;
|
|
1662
1664
|
const skillsTools = opts.hasSkills ? `\n- use_skill: load a skill (knowledge, instructions, or a tutorial) into your context to help with the user's request\n- remove_skill: unload a skill from context when you're done with it` : ``;
|
|
1663
|
-
const docsGuidance = opts.hasDocsSupport ? `\n-
|
|
1665
|
+
const docsGuidance = opts.hasDocsSupport ? `\n- For ANY question about Electric Agents, Durable Agents, or this framework, ALWAYS use search_durable_agents_docs FIRST. Do not use brave_search or fetch_url for Electric Agents topics unless the docs search returns no useful results.\n- The search tool returns chunk content directly — you do not need to read the source files.\n- Use repo read/bash tools only for non-doc files or when you need to inspect exact implementation code in the workspace.` : ``;
|
|
1664
1666
|
const skillsGuidance = opts.hasSkills ? `\n# Skills\nYou have access to skills — specialized knowledge and guided workflows you can load on demand. Your context includes a skills catalog listing what's available. When the user's request matches a skill's description or keywords, load it with use_skill.
|
|
1665
1667
|
|
|
1666
1668
|
Some skills are user-invocable — the user can trigger them with a slash command like \`/quickstart\`. When you see a message starting with \`/\` followed by a skill name, load that skill immediately with use_skill. Pass any text after the skill name as args.
|
|
@@ -1679,7 +1681,7 @@ Do NOT load a skill and then ignore its instructions. The skill is there because
|
|
|
1679
1681
|
When a user is new or asks how to get started with Electric Agents, **don't assume a single path**. Present the options and let them choose:
|
|
1680
1682
|
|
|
1681
1683
|
- **Learn the concepts first** → Explain what Electric Agents is, answer questions, point to docs.
|
|
1682
|
-
Use
|
|
1684
|
+
Use search_durable_agents_docs to look up answers. Only load the quickstart skill if the user explicitly asks for a hands-on guided tutorial.
|
|
1683
1685
|
|
|
1684
1686
|
- **Hands-on guided tutorial** → Load the quickstart skill (or tell them to type \`/quickstart\`).
|
|
1685
1687
|
This is a step-by-step build that takes them from zero to a running app.
|
|
@@ -1689,7 +1691,7 @@ When a user is new or asks how to get started with Electric Agents, **don't assu
|
|
|
1689
1691
|
This sets up project structure and orients them in the codebase.
|
|
1690
1692
|
|
|
1691
1693
|
- **Have a specific question?** → Answer it directly.
|
|
1692
|
-
Use
|
|
1694
|
+
Use search_durable_agents_docs first, then fall back to fetch_url or general knowledge if needed.
|
|
1693
1695
|
|
|
1694
1696
|
Don't force onboarding. If someone just wants to chat or code, let them. When in doubt, ask what they'd like to do rather than picking a path for them.`;
|
|
1695
1697
|
const docsUrlGuidance = opts.docsUrl ? `\n# Electric Agents documentation
|
package/dist/index.js
CHANGED
|
@@ -795,6 +795,8 @@ function resolveDocsRoot(workingDirectory) {
|
|
|
795
795
|
process.env.HORTON_DOCS_ROOT,
|
|
796
796
|
path.resolve(workingDirectory, `electric-agents-docs/docs`),
|
|
797
797
|
path.resolve(process.cwd(), `electric-agents-docs/docs`),
|
|
798
|
+
path.resolve(MODULE_DIR, `../docs`),
|
|
799
|
+
path.resolve(MODULE_DIR, `../../docs`),
|
|
798
800
|
path.resolve(MODULE_DIR, `../../../../../electric-agents-docs/docs`)
|
|
799
801
|
].filter((value) => typeof value === `string`);
|
|
800
802
|
for (const candidate of candidates) if (fsSync.existsSync(candidate)) return candidate;
|
|
@@ -1636,7 +1638,7 @@ async function generateTitle(userMessage, llmCall = defaultHaikuCall) {
|
|
|
1636
1638
|
function buildHortonSystemPrompt(workingDirectory, opts = {}) {
|
|
1637
1639
|
const docsTools = opts.hasDocsSupport ? `\n- search_durable_agents_docs: hybrid search over the built-in Durable Agents docs index` : ``;
|
|
1638
1640
|
const skillsTools = opts.hasSkills ? `\n- use_skill: load a skill (knowledge, instructions, or a tutorial) into your context to help with the user's request\n- remove_skill: unload a skill from context when you're done with it` : ``;
|
|
1639
|
-
const docsGuidance = opts.hasDocsSupport ? `\n-
|
|
1641
|
+
const docsGuidance = opts.hasDocsSupport ? `\n- For ANY question about Electric Agents, Durable Agents, or this framework, ALWAYS use search_durable_agents_docs FIRST. Do not use brave_search or fetch_url for Electric Agents topics unless the docs search returns no useful results.\n- The search tool returns chunk content directly — you do not need to read the source files.\n- Use repo read/bash tools only for non-doc files or when you need to inspect exact implementation code in the workspace.` : ``;
|
|
1640
1642
|
const skillsGuidance = opts.hasSkills ? `\n# Skills\nYou have access to skills — specialized knowledge and guided workflows you can load on demand. Your context includes a skills catalog listing what's available. When the user's request matches a skill's description or keywords, load it with use_skill.
|
|
1641
1643
|
|
|
1642
1644
|
Some skills are user-invocable — the user can trigger them with a slash command like \`/quickstart\`. When you see a message starting with \`/\` followed by a skill name, load that skill immediately with use_skill. Pass any text after the skill name as args.
|
|
@@ -1655,7 +1657,7 @@ Do NOT load a skill and then ignore its instructions. The skill is there because
|
|
|
1655
1657
|
When a user is new or asks how to get started with Electric Agents, **don't assume a single path**. Present the options and let them choose:
|
|
1656
1658
|
|
|
1657
1659
|
- **Learn the concepts first** → Explain what Electric Agents is, answer questions, point to docs.
|
|
1658
|
-
Use
|
|
1660
|
+
Use search_durable_agents_docs to look up answers. Only load the quickstart skill if the user explicitly asks for a hands-on guided tutorial.
|
|
1659
1661
|
|
|
1660
1662
|
- **Hands-on guided tutorial** → Load the quickstart skill (or tell them to type \`/quickstart\`).
|
|
1661
1663
|
This is a step-by-step build that takes them from zero to a running app.
|
|
@@ -1665,7 +1667,7 @@ When a user is new or asks how to get started with Electric Agents, **don't assu
|
|
|
1665
1667
|
This sets up project structure and orients them in the codebase.
|
|
1666
1668
|
|
|
1667
1669
|
- **Have a specific question?** → Answer it directly.
|
|
1668
|
-
Use
|
|
1670
|
+
Use search_durable_agents_docs first, then fall back to fetch_url or general knowledge if needed.
|
|
1669
1671
|
|
|
1670
1672
|
Don't force onboarding. If someone just wants to chat or code, let them. When in doubt, ask what they'd like to do rather than picking a path for them.`;
|
|
1671
1673
|
const docsUrlGuidance = opts.docsUrl ? `\n# Electric Agents documentation
|
|
@@ -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.
|