@electric-ax/agents 0.2.2 → 0.2.4

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 +40 -12
  2. package/dist/index.cjs +40 -12
  3. package/dist/index.js +40 -12
  4. package/docs/entities/agents/coder.md +99 -0
  5. package/docs/entities/agents/horton.md +16 -13
  6. package/docs/entities/agents/worker.md +18 -18
  7. package/docs/entities/patterns/blackboard.md +6 -6
  8. package/docs/entities/patterns/dispatcher.md +1 -1
  9. package/docs/entities/patterns/manager-worker.md +1 -1
  10. package/docs/entities/patterns/map-reduce.md +1 -1
  11. package/docs/entities/patterns/pipeline.md +1 -1
  12. package/docs/entities/patterns/reactive-observers.md +2 -2
  13. package/docs/examples/playground.md +42 -26
  14. package/docs/index.md +23 -23
  15. package/docs/quickstart.md +13 -13
  16. package/docs/reference/agent-config.md +20 -12
  17. package/docs/reference/agent-tool.md +1 -1
  18. package/docs/reference/built-in-collections.md +21 -21
  19. package/docs/reference/cli.md +39 -30
  20. package/docs/reference/entity-definition.md +9 -9
  21. package/docs/reference/entity-handle.md +2 -2
  22. package/docs/reference/entity-registry.md +1 -1
  23. package/docs/reference/handler-context.md +69 -18
  24. package/docs/reference/runtime-handler.md +25 -23
  25. package/docs/reference/shared-state-handle.md +7 -7
  26. package/docs/reference/state-collection-proxy.md +1 -1
  27. package/docs/reference/wake-event.md +23 -23
  28. package/docs/usage/app-setup.md +24 -23
  29. package/docs/usage/clients-and-react.md +44 -36
  30. package/docs/usage/configuring-the-agent.md +25 -19
  31. package/docs/usage/context-composition.md +12 -12
  32. package/docs/usage/defining-entities.md +36 -36
  33. package/docs/usage/defining-tools.md +45 -45
  34. package/docs/usage/embedded-builtins.md +48 -47
  35. package/docs/usage/managing-state.md +12 -12
  36. package/docs/usage/overview.md +52 -45
  37. package/docs/usage/programmatic-runtime-client.md +50 -47
  38. package/docs/usage/shared-state.md +32 -32
  39. package/docs/usage/spawning-and-coordinating.md +9 -9
  40. package/docs/usage/testing.md +14 -14
  41. package/docs/usage/waking-entities.md +13 -13
  42. package/docs/usage/writing-handlers.md +57 -26
  43. package/package.json +5 -2
  44. package/scripts/sync-docs.mjs +42 -0
  45. package/skills/quickstart/scaffold/package.json +1 -0
  46. package/skills/quickstart/scaffold-ui/index.html +1 -1
  47. package/skills/quickstart/scaffold-ui/main.tsx +221 -16
  48. package/skills/quickstart.md +49 -94
  49. package/docs/examples/mega-draw.md +0 -106
@@ -48,6 +48,10 @@ function formatArgs(args) {
48
48
  };
49
49
  }
50
50
  const serverLog = {
51
+ debug(...args) {
52
+ const { msg } = formatArgs(args);
53
+ logger.debug(msg);
54
+ },
51
55
  info(...args) {
52
56
  const { msg } = formatArgs(args);
53
57
  logger.info(msg);
@@ -792,15 +796,38 @@ function findLatestQuestion(items) {
792
796
  return void 0;
793
797
  }
794
798
  function resolveDocsRoot(workingDirectory) {
799
+ const envDocsRoot = process.env.HORTON_DOCS_ROOT;
795
800
  const candidates = [
796
- process.env.HORTON_DOCS_ROOT,
797
- path.resolve(workingDirectory, `electric-agents-docs/docs`),
798
- path.resolve(process.cwd(), `electric-agents-docs/docs`),
799
- path.resolve(MODULE_DIR, `../docs`),
800
- path.resolve(MODULE_DIR, `../../docs`),
801
- path.resolve(MODULE_DIR, `../../../../../electric-agents-docs/docs`)
802
- ].filter((value) => typeof value === `string`);
803
- for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
801
+ envDocsRoot ? {
802
+ path: envDocsRoot,
803
+ requireIndex: false
804
+ } : null,
805
+ {
806
+ path: path.resolve(workingDirectory, `electric-agents-docs/docs`),
807
+ requireIndex: false
808
+ },
809
+ {
810
+ path: path.resolve(process.cwd(), `electric-agents-docs/docs`),
811
+ requireIndex: false
812
+ },
813
+ {
814
+ path: path.resolve(MODULE_DIR, `../docs`),
815
+ requireIndex: true
816
+ },
817
+ {
818
+ path: path.resolve(MODULE_DIR, `../../docs`),
819
+ requireIndex: true
820
+ },
821
+ {
822
+ path: path.resolve(MODULE_DIR, `../../../../website/docs/agents`),
823
+ requireIndex: true
824
+ },
825
+ {
826
+ path: path.resolve(MODULE_DIR, `../../../../../electric-agents-docs/docs`),
827
+ requireIndex: false
828
+ }
829
+ ].filter((value) => Boolean(value));
830
+ for (const candidate of candidates) if (fs.existsSync(candidate.path) && (!candidate.requireIndex || fs.existsSync(path.join(candidate.path, `index.md`)))) return candidate.path;
804
831
  return null;
805
832
  }
806
833
  var DocsKnowledgeBase = class {
@@ -830,7 +857,7 @@ var DocsKnowledgeBase = class {
830
857
  return db$1;
831
858
  } catch (error) {
832
859
  const message = error instanceof Error ? error.message : String(error);
833
- console.warn(`${this.logPrefix} falling back to in-memory docs index: ${message}`);
860
+ serverLog.debug(`${this.logPrefix} falling back to in-memory docs index: ${message}`);
834
861
  return null;
835
862
  }
836
863
  }
@@ -917,7 +944,7 @@ var DocsKnowledgeBase = class {
917
944
  }
918
945
  this.fallbackFingerprint = fingerprint;
919
946
  const stats$1 = this.stats();
920
- console.log(`${this.logPrefix} indexed ${stats$1.docCount} docs into ${stats$1.chunkCount} chunks (${stats$1.fingerprint.slice(0, 12)}...)`);
947
+ serverLog.debug(`${this.logPrefix} indexed ${stats$1.docCount} docs into ${stats$1.chunkCount} chunks (${stats$1.fingerprint.slice(0, 12)}...)`);
921
948
  return stats$1;
922
949
  }
923
950
  const db$1 = this.db;
@@ -954,7 +981,7 @@ var DocsKnowledgeBase = class {
954
981
  });
955
982
  reset();
956
983
  const stats = this.stats();
957
- console.log(`${this.logPrefix} indexed ${stats.docCount} docs into ${stats.chunkCount} chunks (${stats.fingerprint.slice(0, 12)}...)`);
984
+ serverLog.debug(`${this.logPrefix} indexed ${stats.docCount} docs into ${stats.chunkCount} chunks (${stats.fingerprint.slice(0, 12)}...)`);
958
985
  return stats;
959
986
  }
960
987
  hybridSearch(query, limit = DEFAULT_K) {
@@ -1094,7 +1121,7 @@ function renderSearchResults(query, results, docsRoot) {
1094
1121
  return lines.join(`\n`);
1095
1122
  }
1096
1123
  function logSearchResults(kind, query, output) {
1097
- console.log(`[horton-docs] ${kind} search for "${query}"\n${output}\n`);
1124
+ serverLog.debug(`[horton-docs] ${kind} search for "${query}"\n${output}\n`);
1098
1125
  }
1099
1126
  function createHortonDocsSupport(workingDirectory, opts = {}) {
1100
1127
  const docsRoot = opts.docsRoot ?? resolveDocsRoot(workingDirectory);
@@ -2267,6 +2294,7 @@ async function saveCache(cachePath, catalog, cacheDir) {
2267
2294
  const obj = {};
2268
2295
  for (const [name, meta] of catalog) obj[name] = meta;
2269
2296
  fs.mkdirSync(cacheDir, { recursive: true });
2297
+ await fs$1.writeFile(path.join(cacheDir, `.gitignore`), `*\n`, `utf-8`);
2270
2298
  await fs$1.writeFile(cachePath, JSON.stringify(obj, null, 2), `utf-8`);
2271
2299
  }
2272
2300
  function sha256(content) {
package/dist/index.cjs CHANGED
@@ -71,6 +71,10 @@ function formatArgs(args) {
71
71
  };
72
72
  }
73
73
  const serverLog = {
74
+ debug(...args) {
75
+ const { msg } = formatArgs(args);
76
+ logger.debug(msg);
77
+ },
74
78
  info(...args) {
75
79
  const { msg } = formatArgs(args);
76
80
  logger.info(msg);
@@ -815,15 +819,38 @@ function findLatestQuestion(items) {
815
819
  return void 0;
816
820
  }
817
821
  function resolveDocsRoot(workingDirectory) {
822
+ const envDocsRoot = process.env.HORTON_DOCS_ROOT;
818
823
  const candidates = [
819
- process.env.HORTON_DOCS_ROOT,
820
- node_path.default.resolve(workingDirectory, `electric-agents-docs/docs`),
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`),
824
- node_path.default.resolve(MODULE_DIR, `../../../../../electric-agents-docs/docs`)
825
- ].filter((value) => typeof value === `string`);
826
- for (const candidate of candidates) if (node_fs.default.existsSync(candidate)) return candidate;
824
+ envDocsRoot ? {
825
+ path: envDocsRoot,
826
+ requireIndex: false
827
+ } : null,
828
+ {
829
+ path: node_path.default.resolve(workingDirectory, `electric-agents-docs/docs`),
830
+ requireIndex: false
831
+ },
832
+ {
833
+ path: node_path.default.resolve(process.cwd(), `electric-agents-docs/docs`),
834
+ requireIndex: false
835
+ },
836
+ {
837
+ path: node_path.default.resolve(MODULE_DIR, `../docs`),
838
+ requireIndex: true
839
+ },
840
+ {
841
+ path: node_path.default.resolve(MODULE_DIR, `../../docs`),
842
+ requireIndex: true
843
+ },
844
+ {
845
+ path: node_path.default.resolve(MODULE_DIR, `../../../../website/docs/agents`),
846
+ requireIndex: true
847
+ },
848
+ {
849
+ path: node_path.default.resolve(MODULE_DIR, `../../../../../electric-agents-docs/docs`),
850
+ requireIndex: false
851
+ }
852
+ ].filter((value) => Boolean(value));
853
+ for (const candidate of candidates) if (node_fs.default.existsSync(candidate.path) && (!candidate.requireIndex || node_fs.default.existsSync(node_path.default.join(candidate.path, `index.md`)))) return candidate.path;
827
854
  return null;
828
855
  }
829
856
  var DocsKnowledgeBase = class {
@@ -853,7 +880,7 @@ var DocsKnowledgeBase = class {
853
880
  return db$1;
854
881
  } catch (error) {
855
882
  const message = error instanceof Error ? error.message : String(error);
856
- console.warn(`${this.logPrefix} falling back to in-memory docs index: ${message}`);
883
+ serverLog.debug(`${this.logPrefix} falling back to in-memory docs index: ${message}`);
857
884
  return null;
858
885
  }
859
886
  }
@@ -940,7 +967,7 @@ var DocsKnowledgeBase = class {
940
967
  }
941
968
  this.fallbackFingerprint = fingerprint;
942
969
  const stats$1 = this.stats();
943
- console.log(`${this.logPrefix} indexed ${stats$1.docCount} docs into ${stats$1.chunkCount} chunks (${stats$1.fingerprint.slice(0, 12)}...)`);
970
+ serverLog.debug(`${this.logPrefix} indexed ${stats$1.docCount} docs into ${stats$1.chunkCount} chunks (${stats$1.fingerprint.slice(0, 12)}...)`);
944
971
  return stats$1;
945
972
  }
946
973
  const db$1 = this.db;
@@ -977,7 +1004,7 @@ var DocsKnowledgeBase = class {
977
1004
  });
978
1005
  reset();
979
1006
  const stats = this.stats();
980
- console.log(`${this.logPrefix} indexed ${stats.docCount} docs into ${stats.chunkCount} chunks (${stats.fingerprint.slice(0, 12)}...)`);
1007
+ serverLog.debug(`${this.logPrefix} indexed ${stats.docCount} docs into ${stats.chunkCount} chunks (${stats.fingerprint.slice(0, 12)}...)`);
981
1008
  return stats;
982
1009
  }
983
1010
  hybridSearch(query, limit = DEFAULT_K) {
@@ -1117,7 +1144,7 @@ function renderSearchResults(query, results, docsRoot) {
1117
1144
  return lines.join(`\n`);
1118
1145
  }
1119
1146
  function logSearchResults(kind, query, output) {
1120
- console.log(`[horton-docs] ${kind} search for "${query}"\n${output}\n`);
1147
+ serverLog.debug(`[horton-docs] ${kind} search for "${query}"\n${output}\n`);
1121
1148
  }
1122
1149
  function createHortonDocsSupport(workingDirectory, opts = {}) {
1123
1150
  const docsRoot = opts.docsRoot ?? resolveDocsRoot(workingDirectory);
@@ -2290,6 +2317,7 @@ async function saveCache(cachePath, catalog, cacheDir) {
2290
2317
  const obj = {};
2291
2318
  for (const [name, meta] of catalog) obj[name] = meta;
2292
2319
  node_fs.default.mkdirSync(cacheDir, { recursive: true });
2320
+ await node_fs_promises.default.writeFile(node_path.default.join(cacheDir, `.gitignore`), `*\n`, `utf-8`);
2293
2321
  await node_fs_promises.default.writeFile(cachePath, JSON.stringify(obj, null, 2), `utf-8`);
2294
2322
  }
2295
2323
  function sha256(content) {
package/dist/index.js CHANGED
@@ -47,6 +47,10 @@ function formatArgs(args) {
47
47
  };
48
48
  }
49
49
  const serverLog = {
50
+ debug(...args) {
51
+ const { msg } = formatArgs(args);
52
+ logger.debug(msg);
53
+ },
50
54
  info(...args) {
51
55
  const { msg } = formatArgs(args);
52
56
  logger.info(msg);
@@ -791,15 +795,38 @@ function findLatestQuestion(items) {
791
795
  return void 0;
792
796
  }
793
797
  function resolveDocsRoot(workingDirectory) {
798
+ const envDocsRoot = process.env.HORTON_DOCS_ROOT;
794
799
  const candidates = [
795
- process.env.HORTON_DOCS_ROOT,
796
- path.resolve(workingDirectory, `electric-agents-docs/docs`),
797
- path.resolve(process.cwd(), `electric-agents-docs/docs`),
798
- path.resolve(MODULE_DIR, `../docs`),
799
- path.resolve(MODULE_DIR, `../../docs`),
800
- path.resolve(MODULE_DIR, `../../../../../electric-agents-docs/docs`)
801
- ].filter((value) => typeof value === `string`);
802
- for (const candidate of candidates) if (fsSync.existsSync(candidate)) return candidate;
800
+ envDocsRoot ? {
801
+ path: envDocsRoot,
802
+ requireIndex: false
803
+ } : null,
804
+ {
805
+ path: path.resolve(workingDirectory, `electric-agents-docs/docs`),
806
+ requireIndex: false
807
+ },
808
+ {
809
+ path: path.resolve(process.cwd(), `electric-agents-docs/docs`),
810
+ requireIndex: false
811
+ },
812
+ {
813
+ path: path.resolve(MODULE_DIR, `../docs`),
814
+ requireIndex: true
815
+ },
816
+ {
817
+ path: path.resolve(MODULE_DIR, `../../docs`),
818
+ requireIndex: true
819
+ },
820
+ {
821
+ path: path.resolve(MODULE_DIR, `../../../../website/docs/agents`),
822
+ requireIndex: true
823
+ },
824
+ {
825
+ path: path.resolve(MODULE_DIR, `../../../../../electric-agents-docs/docs`),
826
+ requireIndex: false
827
+ }
828
+ ].filter((value) => Boolean(value));
829
+ for (const candidate of candidates) if (fsSync.existsSync(candidate.path) && (!candidate.requireIndex || fsSync.existsSync(path.join(candidate.path, `index.md`)))) return candidate.path;
803
830
  return null;
804
831
  }
805
832
  var DocsKnowledgeBase = class {
@@ -829,7 +856,7 @@ var DocsKnowledgeBase = class {
829
856
  return db$1;
830
857
  } catch (error) {
831
858
  const message = error instanceof Error ? error.message : String(error);
832
- console.warn(`${this.logPrefix} falling back to in-memory docs index: ${message}`);
859
+ serverLog.debug(`${this.logPrefix} falling back to in-memory docs index: ${message}`);
833
860
  return null;
834
861
  }
835
862
  }
@@ -916,7 +943,7 @@ var DocsKnowledgeBase = class {
916
943
  }
917
944
  this.fallbackFingerprint = fingerprint;
918
945
  const stats$1 = this.stats();
919
- console.log(`${this.logPrefix} indexed ${stats$1.docCount} docs into ${stats$1.chunkCount} chunks (${stats$1.fingerprint.slice(0, 12)}...)`);
946
+ serverLog.debug(`${this.logPrefix} indexed ${stats$1.docCount} docs into ${stats$1.chunkCount} chunks (${stats$1.fingerprint.slice(0, 12)}...)`);
920
947
  return stats$1;
921
948
  }
922
949
  const db$1 = this.db;
@@ -953,7 +980,7 @@ var DocsKnowledgeBase = class {
953
980
  });
954
981
  reset();
955
982
  const stats = this.stats();
956
- console.log(`${this.logPrefix} indexed ${stats.docCount} docs into ${stats.chunkCount} chunks (${stats.fingerprint.slice(0, 12)}...)`);
983
+ serverLog.debug(`${this.logPrefix} indexed ${stats.docCount} docs into ${stats.chunkCount} chunks (${stats.fingerprint.slice(0, 12)}...)`);
957
984
  return stats;
958
985
  }
959
986
  hybridSearch(query, limit = DEFAULT_K) {
@@ -1093,7 +1120,7 @@ function renderSearchResults(query, results, docsRoot) {
1093
1120
  return lines.join(`\n`);
1094
1121
  }
1095
1122
  function logSearchResults(kind, query, output) {
1096
- console.log(`[horton-docs] ${kind} search for "${query}"\n${output}\n`);
1123
+ serverLog.debug(`[horton-docs] ${kind} search for "${query}"\n${output}\n`);
1097
1124
  }
1098
1125
  function createHortonDocsSupport(workingDirectory, opts = {}) {
1099
1126
  const docsRoot = opts.docsRoot ?? resolveDocsRoot(workingDirectory);
@@ -2266,6 +2293,7 @@ async function saveCache(cachePath, catalog, cacheDir) {
2266
2293
  const obj = {};
2267
2294
  for (const [name, meta] of catalog) obj[name] = meta;
2268
2295
  fsSync.mkdirSync(cacheDir, { recursive: true });
2296
+ await fs.writeFile(path.join(cacheDir, `.gitignore`), `*\n`, `utf-8`);
2269
2297
  await fs.writeFile(cachePath, JSON.stringify(obj, null, 2), `utf-8`);
2270
2298
  }
2271
2299
  function sha256(content) {
@@ -0,0 +1,99 @@
1
+ ---
2
+ title: Coder
3
+ titleTemplate: "... - Electric Agents"
4
+ description: >-
5
+ Built-in coding-session entity backed by Claude Code or Codex CLI.
6
+ outline: [2, 3]
7
+ ---
8
+
9
+ # Coder
10
+
11
+ `coder` is the built-in coding-session entity. It runs a Claude Code or Codex CLI session in a working directory, mirrors the normalized session event stream into entity state, and can be prompted repeatedly across many turns.
12
+
13
+ **Source:** [`packages/agents/src/agents/coding-session.ts`](https://github.com/electric-sql/electric/blob/main/packages/agents/src/agents/coding-session.ts)
14
+
15
+ ## Spawn args
16
+
17
+ ```ts
18
+ interface CoderArgs {
19
+ agent: "claude" | "codex"
20
+ cwd?: string
21
+ nativeSessionId?: string
22
+ importFrom?: { agent: "claude" | "codex"; sessionId: string }
23
+ }
24
+ ```
25
+
26
+ | Field | Required | Description |
27
+ | ----------------- | -------- | ----------- |
28
+ | `agent` | Yes | CLI backend to run: `"claude"` or `"codex"`. |
29
+ | `cwd` | No | Working directory for the CLI. Defaults to the built-in runtime working directory. |
30
+ | `nativeSessionId` | No | Attach to an existing local Claude/Codex session. |
31
+ | `importFrom` | No | Import an existing local session into a new session for the selected backend. |
32
+
33
+ The built-in runtime registers `coder` during bootstrap. Handler code can also call `registerCodingSession(registry, { defaultWorkingDirectory, cliRunner? })` from `@electric-ax/agents`.
34
+
35
+ ## Prompt messages
36
+
37
+ The preferred inbox message type is `prompt` with a payload shaped like:
38
+
39
+ ```ts
40
+ interface PromptMessage {
41
+ text: string
42
+ }
43
+ ```
44
+
45
+ Generic messages with the same `{ text }` payload are also processed, so the dashboard and CLI can send prompts without a custom message type.
46
+
47
+ ## State collections
48
+
49
+ `coder` adds three custom state collections:
50
+
51
+ | Collection | Event type | Description |
52
+ | --------------- | ----------------------- | ----------- |
53
+ | `sessionMeta` | `coding_session_meta` | Current session metadata: selected backend, cwd, status, native session id, and errors. |
54
+ | `cursorState` | `coding_session_cursor` | Serialized tail cursor and the last processed inbox key. |
55
+ | `events` | `coding_session_event` | Normalized `agent-session-protocol` events mirrored from the CLI session. |
56
+
57
+ ## Handler behavior
58
+
59
+ 1. Initializes session metadata and cursor state if needed.
60
+ 2. Mirrors existing local session history when attaching or importing.
61
+ 3. Processes pending prompt messages in inbox order.
62
+ 4. Calls `ctx.recordRun()` around each CLI invocation so parents observing with `wake: "runFinished"` are notified.
63
+ 5. Mirrors new CLI events into the `events` collection and appends assistant text as the run response.
64
+ 6. Updates `sessionMeta.status` to `idle` or `error`.
65
+
66
+ ## Handler API
67
+
68
+ Inside another entity handler, use `ctx.useCodingAgent()` to spawn or attach to a coder:
69
+
70
+ ```ts
71
+ const coder = await ctx.useCodingAgent("feature-work", {
72
+ agent: "claude",
73
+ cwd: process.cwd(),
74
+ })
75
+
76
+ coder.send("Implement the requested feature and run the tests.")
77
+ await coder.run
78
+ ```
79
+
80
+ `useCodingAgent()` returns a `CodingSessionHandle` with `entityUrl`, `status()`, `meta()`, `send(prompt)`, `run`, `events`, and `messages`.
81
+
82
+ ## Horton tools
83
+
84
+ Horton usually interacts with coders through:
85
+
86
+ | Tool | Purpose |
87
+ | -------------- | ------- |
88
+ | `spawn_coder` | Creates a new long-lived `coder`, sends the first prompt, and wakes Horton when the reply lands. |
89
+ | `prompt_coder` | Sends a follow-up prompt to an existing coder URL. |
90
+
91
+ ## Details
92
+
93
+ | Property | Value |
94
+ | ----------------- | ----- |
95
+ | Type name | `coder` |
96
+ | Backends | Claude Code and Codex CLI |
97
+ | State | `sessionMeta`, `cursorState`, `events` |
98
+ | Wake support | Uses `ctx.recordRun()` so `runFinished` observers work |
99
+ | Working directory | From spawn args or `registerCodingSession` default |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Horton agent
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  The built-in Horton assistant - chat, research, code, and dispatch subagents in one entity type.
6
6
  outline: [2, 3]
@@ -8,7 +8,7 @@ outline: [2, 3]
8
8
 
9
9
  # Horton agent
10
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.
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, dispatch workers for isolated subtasks, and spawn long-lived coders for code changes.
12
12
 
13
13
  **Source:** [`packages/agents/src/agents/horton.ts`](https://github.com/electric-sql/electric/blob/main/packages/agents/src/agents/horton.ts)
14
14
 
@@ -35,6 +35,8 @@ Horton is configured with `ctx.electricTools` plus the base Horton tool set:
35
35
  | `brave_search` | Web search via the Brave Search API. |
36
36
  | `fetch_url` | Fetch a URL and return it as markdown. |
37
37
  | `spawn_worker` | Dispatch a subagent for an isolated subtask. |
38
+ | `spawn_coder` | Spawn a long-lived `coder` entity backed by Claude Code or Codex. |
39
+ | `prompt_coder` | Send a follow-up prompt to an existing coder. |
38
40
 
39
41
  `brave_search` requires `BRAVE_SEARCH_API_KEY` in the environment; without it the tool errors at call time.
40
42
 
@@ -46,14 +48,14 @@ After the first agent run completes, Horton calls `generateTitle()` (Haiku) to s
46
48
 
47
49
  ## Details
48
50
 
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 |
51
+ | Property | Value |
52
+ | ----------------- | ------------------------------------------------- |
53
+ | Type name | `horton` |
54
+ | Model | `HORTON_MODEL` (`claude-sonnet-4-5-20250929`) |
55
+ | Title model | `claude-haiku-4-5-20251001` |
56
+ | Tools | `ctx.electricTools` + base Horton tool set, coder tools, plus docs/skill tools when configured |
57
+ | Working directory | Passed at bootstrap (defaults to `process.cwd()`) |
58
+ | Title generation | Yes, after the first run if no title tag exists |
57
59
 
58
60
  ## Extending Horton
59
61
 
@@ -64,10 +66,10 @@ import {
64
66
  HORTON_MODEL,
65
67
  buildHortonSystemPrompt,
66
68
  createHortonTools,
67
- } from '@electric-ax/agents'
69
+ } from "@electric-ax/agents"
68
70
 
69
- registry.define('my-assistant', {
70
- description: 'Horton with an extra custom tool',
71
+ registry.define("my-assistant", {
72
+ description: "Horton with an extra custom tool",
71
73
  async handler(ctx) {
72
74
  const readSet = new Set<string>()
73
75
  ctx.useAgent({
@@ -87,3 +89,4 @@ registry.define('my-assistant', {
87
89
  ## Related
88
90
 
89
91
  - [Worker](./worker) — the subagent type Horton dispatches via `spawn_worker`.
92
+ - [Coder](./coder) — the coding-session entity Horton dispatches via `spawn_coder`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Worker
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
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
6
  outline: [2, 3]
@@ -19,7 +19,7 @@ interface WorkerArgs {
19
19
  systemPrompt: string
20
20
  tools?: Array<WorkerToolName>
21
21
  sharedDb?: { id: string; schema: SharedStateSchemaMap }
22
- sharedDbToolMode?: 'full' | 'write-only'
22
+ sharedDbToolMode?: "full" | "write-only"
23
23
  }
24
24
  ```
25
25
 
@@ -36,13 +36,13 @@ interface WorkerArgs {
36
36
 
37
37
  ```ts
38
38
  type WorkerToolName =
39
- | 'bash'
40
- | 'read'
41
- | 'write'
42
- | 'edit'
43
- | 'brave_search'
44
- | 'fetch_url'
45
- | 'spawn_worker'
39
+ | "bash"
40
+ | "read"
41
+ | "write"
42
+ | "edit"
43
+ | "brave_search"
44
+ | "fetch_url"
45
+ | "spawn_worker"
46
46
  ```
47
47
 
48
48
  These are the same primitives Horton uses. Pick the smallest subset the worker needs — tools are the worker's permission set.
@@ -56,9 +56,9 @@ The canonical way to spawn a worker is the `spawn_worker` tool, which Horton cal
56
56
  ```ts
57
57
  spawn_worker({
58
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.',
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
62
  })
63
63
  ```
64
64
 
@@ -93,10 +93,10 @@ When you finish, respond with a concise report covering what was done and any ke
93
93
 
94
94
  ## Details
95
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 |
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
102
  | Description | `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).` |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Blackboard (shared state)
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  Multi-agent coordination using shared state as a common data structure for reads and writes.
6
6
  outline: [2, 3]
@@ -23,12 +23,12 @@ const debateSchema = {
23
23
  arguments: {
24
24
  schema: z.object({
25
25
  key: z.string(),
26
- side: z.enum(['pro', 'con']),
26
+ side: z.enum(["pro", "con"]),
27
27
  text: z.string(),
28
28
  round: z.number(),
29
29
  }),
30
- type: 'shared:argument',
31
- primaryKey: 'key',
30
+ type: "shared:argument",
31
+ primaryKey: "key",
32
32
  },
33
33
  }
34
34
  ```
@@ -36,7 +36,7 @@ const debateSchema = {
36
36
  ### Registration
37
37
 
38
38
  ```ts
39
- import { db } from '@electric-ax/agents-runtime'
39
+ import { db } from "@electric-ax/agents-runtime"
40
40
 
41
41
  export function registerDebate(registry: EntityRegistry) {
42
42
  registry.define(`debate`, {
@@ -97,7 +97,7 @@ const args = shared.arguments.toArray
97
97
  ### State transitions
98
98
 
99
99
  ```ts
100
- type DebateStatus = 'idle' | 'debating' | 'ruling' | 'done'
100
+ type DebateStatus = "idle" | "debating" | "ruling" | "done"
101
101
  ```
102
102
 
103
103
  ## Other blackboard variants
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Dispatcher
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  Message routing pattern that classifies incoming messages and dispatches to specialist agents.
6
6
  outline: [2, 3]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Manager-Worker
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  Coordination pattern where a parent spawns specialist children, waits for completion, and synthesizes results.
6
6
  outline: [2, 3]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Map-Reduce
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  Parallel processing pattern that splits work into chunks, processes simultaneously, and reduces results.
6
6
  outline: [2, 3]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Pipeline
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  Sequential processing pattern where each stage's output feeds into the next via state transitions.
6
6
  outline: [2, 3]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Reactive observers
3
- titleTemplate: '... - Electric Agents'
3
+ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  Pattern for entities that watch others and react to changes using ctx.observe() with wake conditions.
6
6
  outline: [2, 3]
@@ -66,7 +66,7 @@ The monitor wraps the base observe tool to also transition its own state to `obs
66
66
  The `observe_entity` tool lets the LLM decide what to watch:
67
67
 
68
68
  ```ts
69
- import { entity } from '@electric-ax/agents-runtime'
69
+ import { entity } from "@electric-ax/agents-runtime"
70
70
 
71
71
  export function createObserveTool(ctx: HandlerContext): AgentTool {
72
72
  return {