@electric-ax/agents 0.4.12 → 0.4.13

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/index.d.ts CHANGED
@@ -36,12 +36,26 @@ declare function createAgentHandler(agentServerUrl: string, workingDirectory?: s
36
36
  declare function registerBuiltinAgentTypes(bootstrap: AgentHandlerResult): Promise<void>;
37
37
  declare const registerAgentTypes: typeof registerBuiltinAgentTypes;
38
38
 
39
+ //#endregion
40
+ //#region src/durable-streams-cache.d.ts
41
+ type DurableStreamsFetchCacheOptions = false | {
42
+ store?: `memory` | `sqlite`;
43
+ sqliteLocation?: string;
44
+ maxCount?: number;
45
+ };
46
+
39
47
  //#endregion
40
48
  //#region src/server.d.ts
41
49
  interface BuiltinAgentsServerOptions {
42
50
  agentServerUrl: string;
43
51
  workingDirectory?: string;
44
52
  mockStreamFn?: StreamFn;
53
+ /**
54
+ * Configure the process-wide HTTP cache used by Undici-backed fetch calls.
55
+ * Defaults to a 100 MiB in-memory cache. Pass `false` to leave the global
56
+ * dispatcher unchanged.
57
+ */
58
+ durableStreamsFetchCache?: DurableStreamsFetchCacheOptions;
45
59
  /** Pull-wake runner configuration for built-in agents. */
46
60
  pullWake: {
47
61
  runnerId: string;
@@ -92,11 +106,24 @@ declare class BuiltinAgentsServer {
92
106
  private mcpToolProviderName;
93
107
  private mcpApplyInFlight;
94
108
  private mcpStopping;
109
+ private mcpExtras;
110
+ private mcpLastJsonConfig;
95
111
  private pullWakeRunner;
96
112
  readonly options: BuiltinAgentsServerOptions;
97
113
  constructor(options: BuiltinAgentsServerOptions);
98
114
  /** Embedded MCP registry. `null` until `start()` has run. */
99
115
  get mcpRegistry(): Registry | null;
116
+ /**
117
+ * Replace the in-memory `extras` list and re-apply the merged config
118
+ * against the last-known workspace `mcp.json` state. Workspace
119
+ * `mcp.json` still wins on name collision. No-op once `stop()` has
120
+ * latched `mcpStopping`.
121
+ */
122
+ setExtraMcpServers(extras: ReadonlyArray<McpServerConfig$1>): Promise<void>;
123
+ private wirePersistence;
124
+ private mergeMcp;
125
+ private runApply;
126
+ private applyMerged;
100
127
  start(): Promise<string>;
101
128
  stop(): Promise<void>;
102
129
  private registerPullWakeRunner;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { mergeElectricPrincipalHeader } from "./server-headers-KD5yHFYT.js";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillTools, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
4
+ import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillTools, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModel, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
5
5
  import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
6
6
  import { chooseDefaultSandbox, isE2BAvailable, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
7
7
  import fsSync from "node:fs";
@@ -15,6 +15,7 @@ import { load } from "sqlite-vec";
15
15
  import { nanoid } from "nanoid";
16
16
  import { getModels } from "@mariozechner/pi-ai";
17
17
  import { bridgeMcpTool, buildPromptTools, buildResourceTools, createRegistry, keychainPersistence, loadConfig, mcp, watchConfig } from "@electric-ax/agents-mcp";
18
+ import { Agent, cacheStores, interceptors, setGlobalDispatcher } from "undici";
18
19
 
19
20
  //#region src/log.ts
20
21
  const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
@@ -865,6 +866,15 @@ async function fetchAvailableModelIds(provider) {
865
866
  function knownModelsForProvider(provider) {
866
867
  return provider === MOONSHOT_PROVIDER ? getMoonshotModels() : getModels(provider);
867
868
  }
869
+ function resolveBuiltinModelContextWindow(modelConfig) {
870
+ const modelId = String(modelConfig.model);
871
+ if (modelConfig.provider === MOONSHOT_PROVIDER) return getMoonshotModel(modelId)?.contextWindow ?? null;
872
+ if (!modelConfig.provider) return null;
873
+ return knownModelsForProvider(modelConfig.provider).find((model) => model.id === modelId)?.contextWindow ?? null;
874
+ }
875
+ function resolveBuiltinModelSourceBudget(modelConfig) {
876
+ return resolveBuiltinModelContextWindow(modelConfig) ?? 1e5;
877
+ }
868
878
  function choiceForKnownModel(provider, model) {
869
879
  return {
870
880
  provider,
@@ -1239,6 +1249,7 @@ function createAssistantHandler(options) {
1239
1249
  return async function assistantHandler(ctx, wake) {
1240
1250
  const readSet = new Set();
1241
1251
  const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
1252
+ const sourceBudget = resolveBuiltinModelSourceBudget(modelConfig);
1242
1253
  const sandboxCwd = ctx.sandbox.workingDirectory;
1243
1254
  const agentsMd = await readAgentsMd(ctx.sandbox);
1244
1255
  const tools = [
@@ -1272,7 +1283,7 @@ function createAssistantHandler(options) {
1272
1283
  }
1273
1284
  })() : Promise.resolve();
1274
1285
  if (docsSupport) ctx.useContext({
1275
- sourceBudget: 1e5,
1286
+ sourceBudget,
1276
1287
  sources: {
1277
1288
  docs_toc: {
1278
1289
  content: () => docsSupport.renderCompressedToc(),
@@ -1301,7 +1312,7 @@ function createAssistantHandler(options) {
1301
1312
  }
1302
1313
  });
1303
1314
  else if (skillsRegistry && skillsRegistry.catalog.size > 0) ctx.useContext({
1304
- sourceBudget: 1e5,
1315
+ sourceBudget,
1305
1316
  sources: {
1306
1317
  skills_catalog: {
1307
1318
  content: () => skillsRegistry.renderCatalog(2e3),
@@ -1320,7 +1331,7 @@ function createAssistantHandler(options) {
1320
1331
  }
1321
1332
  });
1322
1333
  else if (agentsMd) ctx.useContext({
1323
- sourceBudget: 1e5,
1334
+ sourceBudget,
1324
1335
  sources: {
1325
1336
  conversation: {
1326
1337
  content: () => ctx.timelineMessages(),
@@ -1377,6 +1388,15 @@ function registerHorton(registry, options) {
1377
1388
  registry.define(`horton`, {
1378
1389
  description: `Friendly capable assistant — chat, code, research, dispatch`,
1379
1390
  creationSchema: hortonCreationSchema,
1391
+ permissionGrants: [{
1392
+ subject_kind: `principal_kind`,
1393
+ subject_value: `user`,
1394
+ permission: `spawn`
1395
+ }, {
1396
+ subject_kind: `principal_kind`,
1397
+ subject_value: `user`,
1398
+ permission: `manage`
1399
+ }],
1380
1400
  handler: assistantHandler
1381
1401
  });
1382
1402
  return [`horton`];
@@ -1418,7 +1438,7 @@ function parseWorkerArgs(value) {
1418
1438
  if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
1419
1439
  return args;
1420
1440
  }
1421
- function buildToolsForWorker(tools, sandbox, ctx, readSet) {
1441
+ function buildToolsForWorker(tools, sandbox, ctx, readSet, opts) {
1422
1442
  const out = [];
1423
1443
  for (const name of tools) switch (name) {
1424
1444
  case `bash`:
@@ -1437,7 +1457,10 @@ function buildToolsForWorker(tools, sandbox, ctx, readSet) {
1437
1457
  out.push(braveSearchTool$1);
1438
1458
  break;
1439
1459
  case `fetch_url`:
1440
- out.push(createFetchUrlTool(sandbox));
1460
+ out.push(createFetchUrlTool(sandbox, {
1461
+ catalog: opts.modelCatalog,
1462
+ modelConfig: opts.modelConfig
1463
+ }));
1441
1464
  break;
1442
1465
  case `spawn_worker`:
1443
1466
  out.push(createSpawnWorkerTool(ctx));
@@ -1548,11 +1571,23 @@ function registerWorker(registry, options) {
1548
1571
  const { streamFn, modelCatalog } = options;
1549
1572
  registry.define(`worker`, {
1550
1573
  description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
1574
+ permissionGrants: [{
1575
+ subject_kind: `principal_kind`,
1576
+ subject_value: `user`,
1577
+ permission: `spawn`
1578
+ }, {
1579
+ subject_kind: `principal_kind`,
1580
+ subject_value: `user`,
1581
+ permission: `manage`
1582
+ }],
1551
1583
  async handler(ctx) {
1552
1584
  const args = parseWorkerArgs(ctx.args);
1553
1585
  const readSet = new Set();
1554
- const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
1555
1586
  const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
1587
+ const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet, {
1588
+ modelCatalog,
1589
+ modelConfig
1590
+ });
1556
1591
  const sharedStateTools = [];
1557
1592
  if (args.sharedDb) {
1558
1593
  const shared = await ctx.observe(db(args.sharedDb.id, args.sharedDb.schema));
@@ -1746,6 +1781,18 @@ function resolveCwd(args, fallback) {
1746
1781
  return readWorkingDirectoryArg(args) ?? fallback;
1747
1782
  }
1748
1783
 
1784
+ //#endregion
1785
+ //#region src/durable-streams-cache.ts
1786
+ const MEMORY_CACHE_SIZE_BYTES = 100 * 1024 * 1024;
1787
+ function installDurableStreamsFetchCache(options = {}) {
1788
+ if (options === false) return;
1789
+ const store = options.store === `sqlite` || options.sqliteLocation ? new cacheStores.SqliteCacheStore({
1790
+ location: options.sqliteLocation,
1791
+ maxCount: options.maxCount
1792
+ }) : new cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
1793
+ setGlobalDispatcher(new Agent().compose(interceptors.cache({ store })));
1794
+ }
1795
+
1749
1796
  //#endregion
1750
1797
  //#region src/server.ts
1751
1798
  var BuiltinAgentsServer = class {
@@ -1755,6 +1802,8 @@ var BuiltinAgentsServer = class {
1755
1802
  mcpToolProviderName = null;
1756
1803
  mcpApplyInFlight = new Set();
1757
1804
  mcpStopping = false;
1805
+ mcpExtras = [];
1806
+ mcpLastJsonConfig = null;
1758
1807
  pullWakeRunner = null;
1759
1808
  options;
1760
1809
  constructor(options) {
@@ -1764,8 +1813,70 @@ var BuiltinAgentsServer = class {
1764
1813
  get mcpRegistry() {
1765
1814
  return this._mcpRegistry;
1766
1815
  }
1816
+ /**
1817
+ * Replace the in-memory `extras` list and re-apply the merged config
1818
+ * against the last-known workspace `mcp.json` state. Workspace
1819
+ * `mcp.json` still wins on name collision. No-op once `stop()` has
1820
+ * latched `mcpStopping`.
1821
+ */
1822
+ async setExtraMcpServers(extras) {
1823
+ if (!this._mcpRegistry || this.mcpStopping) return;
1824
+ this.mcpExtras = extras;
1825
+ await this.applyMerged(this.mcpLastJsonConfig);
1826
+ }
1827
+ async wirePersistence(cfg) {
1828
+ const servers = [];
1829
+ for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
1830
+ const persist = await keychainPersistence({ server: s.name });
1831
+ servers.push({
1832
+ ...s,
1833
+ auth: {
1834
+ ...s.auth,
1835
+ ...persist
1836
+ }
1837
+ });
1838
+ } else servers.push(s);
1839
+ return {
1840
+ ...cfg,
1841
+ servers
1842
+ };
1843
+ }
1844
+ mergeMcp(jsonCfg) {
1845
+ const jsonServers = jsonCfg?.servers ?? [];
1846
+ const jsonNames = new Set(jsonServers.map((s) => s.name));
1847
+ const filteredExtras = this.mcpExtras.filter((s) => !jsonNames.has(s.name));
1848
+ return {
1849
+ servers: [...filteredExtras, ...jsonServers],
1850
+ raw: jsonCfg?.raw
1851
+ };
1852
+ }
1853
+ async runApply(jsonCfg) {
1854
+ if (this.mcpStopping) return;
1855
+ const registry = this._mcpRegistry;
1856
+ if (!registry) return;
1857
+ try {
1858
+ const wired = await this.wirePersistence(this.mergeMcp(jsonCfg));
1859
+ if (this.mcpStopping) return;
1860
+ await registry.applyConfig(wired);
1861
+ } catch (e) {
1862
+ serverLog.error(`[mcp] applyConfig:`, e);
1863
+ try {
1864
+ this.options.onConfigError?.(e);
1865
+ } catch (cbErr) {
1866
+ serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
1867
+ }
1868
+ }
1869
+ }
1870
+ applyMerged(jsonCfg) {
1871
+ this.mcpLastJsonConfig = jsonCfg;
1872
+ const p = this.runApply(jsonCfg);
1873
+ this.mcpApplyInFlight.add(p);
1874
+ p.finally(() => this.mcpApplyInFlight.delete(p));
1875
+ return p;
1876
+ }
1767
1877
  async start() {
1768
1878
  if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
1879
+ installDurableStreamsFetchCache(this.options.durableStreamsFetchCache);
1769
1880
  const pullWake = this.options.pullWake;
1770
1881
  if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
1771
1882
  try {
@@ -1776,76 +1887,28 @@ var BuiltinAgentsServer = class {
1776
1887
  });
1777
1888
  this._mcpRegistry = mcpRegistry;
1778
1889
  const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
1779
- const extras = this.options.extraMcpServers ?? [];
1780
- const wirePersistence = async (cfg) => {
1781
- const servers = [];
1782
- for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
1783
- const persist = await keychainPersistence({ server: s.name });
1784
- servers.push({
1785
- ...s,
1786
- auth: {
1787
- ...s.auth,
1788
- ...persist
1789
- }
1790
- });
1791
- } else servers.push(s);
1792
- return {
1793
- ...cfg,
1794
- servers
1795
- };
1796
- };
1797
- const merge = (jsonCfg) => {
1798
- const jsonServers = jsonCfg?.servers ?? [];
1799
- const jsonNames = new Set(jsonServers.map((s) => s.name));
1800
- const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
1801
- return {
1802
- servers: [...filteredExtras, ...jsonServers],
1803
- raw: jsonCfg?.raw
1804
- };
1805
- };
1806
- const onConfigError = this.options.onConfigError;
1807
- const runApply = async (jsonCfg) => {
1808
- if (this.mcpStopping) return;
1809
- try {
1810
- const wired = await wirePersistence(merge(jsonCfg));
1811
- if (this.mcpStopping) return;
1812
- await mcpRegistry.applyConfig(wired);
1813
- } catch (e) {
1814
- serverLog.error(`[mcp] applyConfig:`, e);
1815
- try {
1816
- onConfigError?.(e);
1817
- } catch (cbErr) {
1818
- serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
1819
- }
1820
- }
1821
- };
1822
- const applyMerged = (jsonCfg) => {
1823
- const p = runApply(jsonCfg);
1824
- this.mcpApplyInFlight.add(p);
1825
- p.finally(() => this.mcpApplyInFlight.delete(p));
1826
- return p;
1827
- };
1890
+ this.mcpExtras = this.options.extraMcpServers ?? [];
1828
1891
  if (mcpConfigPath) {
1829
1892
  try {
1830
1893
  const cfg = await loadConfig(mcpConfigPath, process.env);
1831
- applyMerged(cfg);
1894
+ this.applyMerged(cfg);
1832
1895
  } catch (err) {
1833
1896
  if (err.code !== `ENOENT`) throw err;
1834
- if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
1835
- else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
1836
- applyMerged(null);
1897
+ if (this.mcpExtras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
1898
+ else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${this.mcpExtras.length} server(s) from extras`);
1899
+ this.applyMerged(null);
1837
1900
  }
1838
1901
  try {
1839
1902
  this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
1840
- onChange: (cfg) => void applyMerged(cfg),
1903
+ onChange: (cfg) => void this.applyMerged(cfg),
1841
1904
  onError: (e) => serverLog.error(`[mcp] config error:`, e)
1842
1905
  });
1843
1906
  } catch (e) {
1844
1907
  serverLog.error(`[mcp] config watcher failed to start:`, e);
1845
1908
  }
1846
1909
  } else {
1847
- if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
1848
- applyMerged(null);
1910
+ if (this.mcpExtras.length > 0) serverLog.info(`[mcp] starting with ${this.mcpExtras.length} server(s) from extras`);
1911
+ this.applyMerged(null);
1849
1912
  }
1850
1913
  this.mcpToolProviderName = `mcp`;
1851
1914
  registerToolProvider({
@@ -81,7 +81,7 @@ const proWorker = await ctx.spawn(
81
81
  systemPrompt: PRO_WORKER_PROMPT,
82
82
  sharedDb: { id: `debate-${ctx.entityUrl}`, schema: debateSchema },
83
83
  },
84
- { initialMessage: proInitialMessage, wake: `runFinished` }
84
+ { initialMessage: proInitialMessage, wake: { on: `runFinished`, includeResponse: true } }
85
85
  )
86
86
  ```
87
87
 
@@ -47,7 +47,7 @@ The dispatcher exposes a `dispatch` tool. When the LLM classifies an incoming me
47
47
 
48
48
  The tool then:
49
49
 
50
- 1. Spawns the requested entity type with `wake: 'runFinished'`.
50
+ 1. Spawns the requested entity type with `wake: { on: 'runFinished', includeResponse: true }`.
51
51
  2. Returns immediately with a status message. The dispatcher is re-invoked when the specialist finishes.
52
52
 
53
53
  ## Dispatch tool
@@ -59,7 +59,7 @@ await ctx.spawn(
59
59
  type === `worker` ? { systemPrompt, tools: [`read`] } : { systemPrompt },
60
60
  {
61
61
  initialMessage: task,
62
- wake: `runFinished`,
62
+ wake: { on: `runFinished`, includeResponse: true },
63
63
  }
64
64
  )
65
65
 
@@ -67,7 +67,7 @@ return {
67
67
  content: [
68
68
  {
69
69
  type: `text` as const,
70
- text: `Dispatched to "${type}" specialist (${id}). You will be woken when it finishes.`,
70
+ text: `Dispatched to "${type}" specialist (${id}). The dispatcher will continue when it finishes.`,
71
71
  },
72
72
  ],
73
73
  details: { id, type },
@@ -42,9 +42,9 @@ The manager defines a handler-scoped tool called `analyze_with_perspectives`. Wh
42
42
 
43
43
  1. Spawns 3 worker children -- optimist, pessimist, pragmatist -- each with a different system prompt.
44
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.
45
+ 3. Uses `wake: { on: 'runFinished', includeResponse: true }` so the manager is re-invoked as each child completes.
46
+ 4. Collects results from `runFinished` wake payloads or shared state after workers finish.
47
+ 5. Runs a synthesis step after all child-completion wakes have been recorded.
48
48
 
49
49
  On subsequent calls, the tool reuses existing children via `ctx.observe()` and `child.send()` instead of spawning new ones.
50
50
 
@@ -63,7 +63,7 @@ for (const perspective of PERSPECTIVES) {
63
63
  `worker`,
64
64
  childId,
65
65
  { systemPrompt: perspective.systemPrompt, tools: [`read`] },
66
- { initialMessage: question, wake: `runFinished` }
66
+ { initialMessage: question, wake: { on: `runFinished`, includeResponse: true } }
67
67
  )
68
68
  children.insert({
69
69
  key: perspective.id,
@@ -87,28 +87,16 @@ for (const perspective of PERSPECTIVES) {
87
87
 
88
88
  ## Collecting results
89
89
 
90
- The `readLatestCompletedText` helper awaits the child's current run, reads all text outputs, and returns the last one:
90
+ Do not wait for worker output inside the same wake. Spawn workers with `wake: { on: "runFinished", includeResponse: true }`, record each worker URL in manager state, and return. On each later child-completion wake, store `wake.payload.finished_child.response` (or read structured output from shared state). Once all workers have reported, run the reduce/synthesis step.
91
91
 
92
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
93
+ const finished = wake.payload?.finished_child
94
+ if (finished) {
95
+ ctx.state.workers.update(finished.url, (draft) => {
96
+ draft.status = finished.run_status
97
+ draft.output = finished.response ?? ""
98
+ })
101
99
  }
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
100
  ```
113
101
 
114
102
  ## State
@@ -55,7 +55,7 @@ for (let i = 0; i < chunks.length; i++) {
55
55
  { systemPrompt: task, tools: [`read`] },
56
56
  {
57
57
  initialMessage: chunks[i],
58
- wake: `runFinished`,
58
+ wake: { on: `runFinished`, includeResponse: true },
59
59
  }
60
60
  )
61
61
  ctx.db.actions.children_insert({
@@ -39,7 +39,7 @@ export function registerPipeline(registry: EntityRegistry) {
39
39
  The pipeline agent exposes a `run_stage` tool. The LLM drives the pipeline one stage at a time:
40
40
 
41
41
  1. The LLM calls `run_stage` with an instruction and input for the current stage.
42
- 2. The tool spawns a worker with the instruction as its system prompt and the input as `initialMessage`, using `wake: 'runFinished'`.
42
+ 2. The tool spawns a worker with the instruction as its system prompt and the input as `initialMessage`, using `wake: { on: 'runFinished', includeResponse: true }`.
43
43
  3. The tool returns immediately. The pipeline entity is re-invoked when the worker finishes.
44
44
  4. On each re-invocation, the wake event contains `finished_child.response` with the stage's output. The LLM then calls `run_stage` again with the next stage's instruction and the previous output as input.
45
45
  5. This repeats until all stages are complete.
@@ -74,7 +74,7 @@ function createRunStageTool(ctx: HandlerContext): AgentTool {
74
74
  `worker`,
75
75
  id,
76
76
  { systemPrompt: instruction, tools: [`read`] },
77
- { initialMessage: input, wake: `runFinished` }
77
+ { initialMessage: input, wake: { on: `runFinished`, includeResponse: true } }
78
78
  )
79
79
  ctx.db.actions.children_insert({
80
80
  row: { key: id, url: child.entityUrl, stage: stageCount },
@@ -84,7 +84,7 @@ function createRunStageTool(ctx: HandlerContext): AgentTool {
84
84
  content: [
85
85
  {
86
86
  type: `text` as const,
87
- text: `Stage ${stageCount} spawned. You will be woken when it finishes.`,
87
+ text: `Stage ${stageCount} spawned. The pipeline will continue when it finishes.`,
88
88
  },
89
89
  ],
90
90
  details: { stage: stageCount },
package/docs/index.md CHANGED
@@ -4,6 +4,9 @@ titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
5
  The durable runtime for long-lived agents — entities, handlers, wakes, agent loops, and coordination, built on Electric Streams, TanStack DB, and pi.
6
6
  outline: [2, 3]
7
+ next:
8
+ text: 'Quickstart'
9
+ link: '/docs/agents/quickstart'
7
10
  ---
8
11
 
9
12
  <script setup>
@@ -12,7 +15,16 @@ import EntityOverviewDiagram from '../../src/components/agents-home/EntityOvervi
12
15
 
13
16
  # Electric Agents
14
17
 
15
- Electric Agents is **the durable runtime for long-lived agents**. It's a runtime and communication fabric for spawning and scaling collaborative agents on serverless compute, using your existing web and AI&nbsp;frameworks.
18
+ Electric Agents is **the durable runtime for long-lived agents**.
19
+
20
+ It's a runtime and communication fabric for spawning and scaling collaborative agents <span class="no-wrap-sm">[on serverless compute](/blog/2026/06/04/serverless-agents)</span> using your existing web systems.
21
+
22
+ > [!Warning] ✨&nbsp; Start using Electric Agents now
23
+ > See the [Quickstart](/docs/agents/quickstart) to fire the system up and try the built-in agents.
24
+ >
25
+ > Dive into the [Walkthrough](/docs/agents/walkthrough) for a step-by-step guide to building a multi-agent system.
26
+
27
+ ## System overview
16
28
 
17
29
  Each agent is an **entity** — an addressable, schema-typed unit of state at `/{type}/{id}`. An entity's session and state live on a durable [Electric&nbsp;Stream](/streams/) of events.
18
30
 
@@ -22,7 +34,7 @@ Every step — runs, tool calls, text deltas, state changes — is appended to t
22
34
 
23
35
  <EntityOverviewDiagram />
24
36
 
25
- Start with the [Quickstart](/docs/agents/quickstart) to run the built-in `horton` and `worker` entities and connect your own app in a few minutes. The [Usage overview](/docs/agents/usage/overview) summarises the full developer surface in a single page.
37
+ See the [Usage overview](/docs/agents/usage/overview) for a summary of the developer surface in a single page.
26
38
 
27
39
  ## How it works
28
40
 
@@ -38,7 +50,7 @@ The runtime SDK is a layer over three foundations:
38
50
 
39
51
  **Outside the handler.** Any app or other entity can call [`createAgentsClient().observe(entity('/type/id'))`](/docs/agents/usage/clients-and-react) to load an entity's stream into a local DB and react to changes in real time, with the same schemas and types as the handler.
40
52
 
41
- ## Entities
53
+ ### Entities
42
54
 
43
55
  Use entities to model anything long-lived and addressable — an agent session, a chat thread, a research job, a coordinator, a worker. You register a **type** with [`registry.define()`](/docs/agents/reference/entity-registry) and spawn **instances** at `/{type}/{id}`. Each instance has its own state, handler, and event stream. See [Defining entities](/docs/agents/usage/defining-entities).
44
56
 
@@ -53,7 +65,7 @@ registry.define("assistant", {
53
65
  })
54
66
  ```
55
67
 
56
- ## Handlers
68
+ ### Handlers
57
69
 
58
70
  The function that runs when an entity wakes. Receives a [`HandlerContext`](/docs/agents/reference/handler-context) (`ctx`) and a [`WakeEvent`](/docs/agents/reference/wake-event) (`wake`). The handler decides how to respond: configure an agent, update state, spawn children, or any combination. See [Writing handlers](/docs/agents/usage/writing-handlers).
59
71
 
@@ -72,9 +84,9 @@ registry.define("support", {
72
84
  })
73
85
  ```
74
86
 
75
- ## Wakes
87
+ ### Waking and notifications
76
88
 
77
- Events that trigger a handler invocation. Wake sources include incoming messages, child completion, state changes, and timers (scheduled sends, cron, timeouts). The [`WakeEvent`](/docs/agents/reference/wake-event) tells the handler why it was woken. See [Waking entities](/docs/agents/usage/waking-entities).
89
+ Events that trigger a handler invocation. Wake sources include incoming messages, child completion, state changes, and timers (scheduled sends, cron, timeouts). The [`WakeEvent`](/docs/agents/reference/wake-event) tells the handler why it was woken.
78
90
 
79
91
  ```ts
80
92
  async handler(ctx, wake) {
@@ -89,35 +101,9 @@ async handler(ctx, wake) {
89
101
  }
90
102
  ```
91
103
 
92
- ## State
104
+ See [Waking entities](/docs/agents/usage/waking-entities) for more information.
93
105
 
94
- Custom persistent collections on the entity. Defined as part of the [entity definition](/docs/agents/reference/entity-definition) and accessed through `ctx.db` alongside the [built-in collections](#built-in-collections). State is local to the entity, typed, and survives restarts. Use it for things that belong to the entity but aren't part of the agent's event stream — an order's items, a research job's findings, a chat session's TODOs. See [Managing state](/docs/agents/usage/managing-state).
95
-
96
- ```ts
97
- registry.define("tracker", {
98
- state: {
99
- items: {
100
- schema: z.object({
101
- key: z.string(),
102
- name: z.string(),
103
- done: z.boolean(),
104
- }),
105
- primaryKey: "key",
106
- },
107
- },
108
- async handler(ctx) {
109
- // read
110
- const item = ctx.db.collections.items.get("item-1")
111
-
112
- // write
113
- ctx.db.actions.items_insert({
114
- row: { key: "item-2", name: "New", done: false },
115
- })
116
- },
117
- })
118
- ```
119
-
120
- ## Agent loop
106
+ ### The agent loop
121
107
 
122
108
  The core pattern is [`ctx.useAgent()`](/docs/agents/reference/agent-config) followed by `ctx.agent.run()`. This runs the LLM in a loop — it generates text, calls tools, and continues until it has nothing left to do. All activity is automatically persisted to the entity's stream. See [Configuring the agent](/docs/agents/usage/configuring-the-agent).
123
109
 
@@ -131,7 +117,7 @@ ctx.useAgent({
131
117
  await ctx.agent.run()
132
118
  ```
133
119
 
134
- ## Tools
120
+ ### Tools
135
121
 
136
122
  Functions the LLM can call during the agent loop. Each tool has a name, description, parameters (defined with [TypeBox](https://github.com/sinclairzx81/typebox) or any [Standard Schema](https://standardschema.dev) validator), and an execute function. Tools run in the handler's context and have access to the entity's state and coordination primitives. See [Defining tools](/docs/agents/usage/defining-tools) and the [`AgentTool` reference](/docs/agents/reference/agent-tool).
137
123
 
@@ -156,7 +142,7 @@ const searchKbTool: AgentTool = {
156
142
  }
157
143
  ```
158
144
 
159
- ## Coordination
145
+ ### Coordination
160
146
 
161
147
  Entities interact through structured primitives. An entity can `spawn` children, `observe` other entities, `send` messages, and [share state](/docs/agents/usage/shared-state). These operations are all durable — they survive restarts and are tracked in the event stream. See [Spawning and coordinating](/docs/agents/usage/spawning-and-coordinating).
162
148
 
@@ -170,7 +156,7 @@ async handler(ctx) {
170
156
  systemPrompt: "Analyse the report",
171
157
  tools: ["read"],
172
158
  },
173
- { initialMessage: "Find the top three issues", wake: "runFinished" }
159
+ { initialMessage: "Find the top three issues", wake: { on: "runFinished", includeResponse: true } }
174
160
  )
175
161
 
176
162
  // send a message to another entity
@@ -183,7 +169,7 @@ async handler(ctx) {
183
169
  }
184
170
  ```
185
171
 
186
- ## Built-in collections
172
+ ### Built-in collections
187
173
 
188
174
  Every entity automatically has collections for runs, steps, texts, tool calls, errors, inbox, and more. These are populated by the runtime as the agent operates and give you live observability into every step of the agent loop — useful for chat UIs, debugging tools, dashboards, and analytics. Query them from the handler or observe them externally. See the [Built-in collections reference](/docs/agents/reference/built-in-collections).
189
175
 
@@ -198,9 +184,45 @@ const db = await client.observe(entity("/support/ticket-42"))
198
184
  console.log(db.collections.texts.toArray)
199
185
  ```
200
186
 
187
+
188
+ ### Custom collections
189
+
190
+ Define custom persistent collections on the entity.
191
+
192
+ Defined as part of the [entity definition](/docs/agents/reference/entity-definition) and accessed through `ctx.db` alongside the [built-in collections](#built-in-collections).
193
+
194
+ ```ts
195
+ registry.define("tracker", {
196
+ state: {
197
+ items: {
198
+ schema: z.object({
199
+ key: z.string(),
200
+ name: z.string(),
201
+ done: z.boolean(),
202
+ }),
203
+ primaryKey: "key",
204
+ },
205
+ },
206
+ async handler(ctx) {
207
+ // read
208
+ const item = ctx.db.collections.items.get("item-1")
209
+
210
+ // write
211
+ ctx.db.actions.items_insert({
212
+ row: { key: "item-2", name: "New", done: false },
213
+ })
214
+ },
215
+ })
216
+ ```
217
+
218
+ State is local to the entity, typed, and survives restarts. Use it for things that belong to the entity but aren't part of the agent's event stream — an order's items, a research job's findings, a chat session's TODOs.
219
+
220
+ See [Managing state](/docs/agents/usage/managing-state) for more information.
221
+
201
222
  ## Next steps
202
223
 
203
- - [Quickstart](/docs/agents/quickstart) — run the built-in `horton` and `worker` entities and connect your own app.
224
+ - [Quickstart](/docs/agents/quickstart) — run the built-in `horton` and `worker` entities and connect your own app
225
+ - [Walkthrough](./walkthrough) — go from a web or mobile app to a <span class="no-wrap">multi-agent</span> system
204
226
  - [Usage overview](/docs/agents/usage/overview) — the full developer surface on one page.
205
227
  - [Defining entities](/docs/agents/usage/defining-entities) — entity types, schemas, and configuration.
206
228
  - [Writing handlers](/docs/agents/usage/writing-handlers) — handler lifecycle and the `ctx` API.