@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
@@ -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- You have built-in Durable Agents docs context plus a docs search tool. Use that before broad web search when the question is about this repo, Electric Agents, or Durable Agents.\n- The docs TOC and docs search results include concrete file paths under the docs tree. Use the normal read tool with those returned paths.\n- Use repo read/bash tools for non-doc files or when you need to inspect exact implementation code in the workspace.` : ``;
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 your docs tools or fetch_url. Only load the quickstart skill if the user explicitly asks for a hands-on guided tutorial.
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 your docs tools, fetch_url, or general coding knowledge.
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- You have built-in Durable Agents docs context plus a docs search tool. Use that before broad web search when the question is about this repo, Electric Agents, or Durable Agents.\n- The docs TOC and docs search results include concrete file paths under the docs tree. Use the normal read tool with those returned paths.\n- Use repo read/bash tools for non-doc files or when you need to inspect exact implementation code in the workspace.` : ``;
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 your docs tools or fetch_url. Only load the quickstart skill if the user explicitly asks for a hands-on guided tutorial.
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 your docs tools, fetch_url, or general coding knowledge.
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- You have built-in Durable Agents docs context plus a docs search tool. Use that before broad web search when the question is about this repo, Electric Agents, or Durable Agents.\n- The docs TOC and docs search results include concrete file paths under the docs tree. Use the normal read tool with those returned paths.\n- Use repo read/bash tools for non-doc files or when you need to inspect exact implementation code in the workspace.` : ``;
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 your docs tools or fetch_url. Only load the quickstart skill if the user explicitly asks for a hands-on guided tutorial.
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 your docs tools, fetch_url, or general coding knowledge.
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.