@electric-ax/agents 0.4.14 → 0.4.15

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.
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
- import { Agent, cacheStores, interceptors, setGlobalDispatcher } from "undici";
3
+ import { cacheStores, getGlobalDispatcher, interceptors, setGlobalDispatcher } from "undici";
4
4
  import fs from "node:fs";
5
5
  import pino from "pino";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, buildSkillSlashCommands, completeWithLowCostModel, createContextSkillLoader, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModel, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
8
- import { braveSearchTool, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
9
- import { chooseDefaultSandbox, isE2BAvailable, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
8
+ import { braveSearchTool, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createScheduleTools, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
9
+ import { chooseDefaultSandbox, isE2BAvailable, lazySandbox, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
10
10
  import { z } from "zod";
11
11
  import { createHash } from "node:crypto";
12
12
  import fs$1 from "node:fs/promises";
@@ -25,7 +25,7 @@ function installDurableStreamsFetchCache(options = {}) {
25
25
  location: options.sqliteLocation,
26
26
  maxCount: options.maxCount
27
27
  }) : new cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
28
- setGlobalDispatcher(new Agent().compose(interceptors.cache({ store })));
28
+ setGlobalDispatcher(getGlobalDispatcher().compose(interceptors.cache({ store })));
29
29
  }
30
30
 
31
31
  //#endregion
@@ -806,6 +806,60 @@ function createSpawnWorkerTool(ctx, modelConfig) {
806
806
  };
807
807
  }
808
808
 
809
+ //#endregion
810
+ //#region src/tools/fork.ts
811
+ function createForkTool(ctx) {
812
+ return {
813
+ name: `fork`,
814
+ label: `Fork`,
815
+ description: `Fork a session at its latest completed agent response, producing a child copy of the conversation up to that point. The new fork is YOUR child — same parent-ownership model as a spawned worker — and it reports back to you the same way: when its next run finishes you'll be woken with its response. End your turn after forking.
816
+
817
+ Prefer supplying an 'initialMessage' so the fork is dispatched immediately in a single call — no follow-up 'send' needed. If you omit it, the fork boots idle and you'll need to call 'send' afterwards. For chat-rendered messages use the shape \`{ "text": "..." }\` so the prompt shows up in the chat UI.
818
+
819
+ Use this to explore multiple alternative continuations in parallel from the same starting point. End your current turn first so the fork includes your latest response — the anchor is always the most recently completed run.
820
+
821
+ Omit 'entityUrl' to fork your own session. Pass a different session's URL to fork that session instead (the new fork is still your child). The optional 'id' names the new fork's instance — useful when you want stable, predictable URLs (e.g. labelling branches in a parallel exploration); omit to let the server mint one.`,
822
+ parameters: Type.Object({
823
+ entityUrl: Type.Optional(Type.String({ description: `URL of the session to fork. Omit to fork your own session.` })),
824
+ id: Type.Optional(Type.String({ description: `Instance id for the new fork (the \`<id>\` in \`/horton/<id>\`). Mirrors spawn_worker's id parameter. Omit to let the server assign one.` })),
825
+ initialMessage: Type.Optional(Type.Any({ description: `Initial inbox message delivered to the fork by the server in the same round-trip — the fork wakes and starts running immediately, no follow-up 'send' needed. Use the shape \`{ "text": "..." }\` for chat-rendered prompts. Omit to leave the fork idle (then call 'send' separately).` })),
826
+ tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: `Optional tags stamped on the new fork, on top of those copied from the source. Useful for labelling experiments (e.g. \`{ "experiment": "ecosystem-maturity" }\`).` }))
827
+ }),
828
+ execute: async (_toolCallId, params) => {
829
+ const { entityUrl, id, initialMessage, tags } = params;
830
+ try {
831
+ const opts = {
832
+ ...initialMessage !== void 0 && { initialMessage },
833
+ ...tags !== void 0 && { tags }
834
+ };
835
+ const forkId = id ?? `fork-${nanoid(10)}`;
836
+ const handle = entityUrl !== void 0 ? await ctx.fork(entityUrl, forkId, opts) : await ctx.forkSelf(forkId, opts);
837
+ const dispatchNote = initialMessage !== void 0 ? `The initial message has been delivered to the fork — it will start running.` : `The fork boots idle — use the 'send' tool to dispatch a follow-up prompt.`;
838
+ return {
839
+ content: [{
840
+ type: `text`,
841
+ text: `Forked at ${handle.entityUrl}. ${dispatchNote} End your turn; you'll wake with the fork's response when its next run finishes (same as a spawned worker).`
842
+ }],
843
+ details: {
844
+ forked: true,
845
+ forkUrl: handle.entityUrl
846
+ }
847
+ };
848
+ } catch (err) {
849
+ const message = err instanceof Error ? err.message : `Unknown error`;
850
+ serverLog.warn(`[fork tool] failed to fork ${entityUrl ?? `<self>`}: ${message}`, err instanceof Error ? err : void 0);
851
+ return {
852
+ content: [{
853
+ type: `text`,
854
+ text: `Error forking session: ${message}`
855
+ }],
856
+ details: { forked: false }
857
+ };
858
+ }
859
+ }
860
+ };
861
+ }
862
+
809
863
  //#endregion
810
864
  //#region src/model-catalog.ts
811
865
  const MODEL_INPUTS_SCHEMA_DEF = `electricModelInputs`;
@@ -1096,6 +1150,7 @@ async function generateTitle(userMessage, llmCall, onFallback) {
1096
1150
  function buildHortonSystemPrompt(workingDirectory, opts = {}) {
1097
1151
  const docsTools = opts.hasDocsSupport ? `\n- search_electric_agents_docs: hybrid search over the built-in Electric Agents docs index` : ``;
1098
1152
  const eventSourceTools = opts.hasEventSourceTools ? `\n- list_event_sources: list external webhook/event feeds you can subscribe to, including available buckets and parameters\n- subscribe_event_source: subscribe yourself to one of those feeds or buckets so matching future events wake you\n- list_event_source_subscriptions: list your active event source subscriptions\n- unsubscribe_event_source: remove one of your event source subscriptions by id` : ``;
1153
+ const scheduleTools = opts.hasScheduleTools ? `\n- upsert_cron_schedule: create or update a recurring cron wake for yourself. Always include payload with the concrete instruction/message you should receive when the cron fires.\n- delete_schedule: delete one of your cron or future-send schedules by stable id\n- list_schedules: list your manifest-backed cron and future-send schedules` : ``;
1099
1154
  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` : ``;
1100
1155
  const docsGuidance = opts.hasDocsSupport ? `\n- For ANY question about Electric Agents or this framework, ALWAYS use search_electric_agents_docs FIRST. Do not use web_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.` : ``;
1101
1156
  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.
@@ -1149,8 +1204,9 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
1149
1204
  - web_search: search the web
1150
1205
  - fetch_url: fetch and convert a URL to markdown
1151
1206
  - spawn_worker: dispatch a subagent for an isolated task
1207
+ - fork: spawn a child session that inherits this conversation's history up to the latest completed response. Same parent-ownership model as spawn_worker — when the fork's next run finishes, you'll wake with its response.
1152
1208
  - send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs.
1153
- ${eventSourceTools}${docsTools}${skillsTools}
1209
+ ${eventSourceTools}${scheduleTools}${docsTools}${skillsTools}
1154
1210
 
1155
1211
  # Working with files
1156
1212
  - Prefer edit over write when modifying existing files.
@@ -1177,6 +1233,20 @@ When you spawn a worker, write its system prompt the way you'd brief a colleague
1177
1233
 
1178
1234
  After spawning, end your turn (optionally with a brief "I've dispatched a worker for X; I'll respond when it finishes"). When the worker finishes, you'll receive a message describing which worker completed and what it returned. Multiple workers may finish at different times — check the message for the worker URL to know which one you're hearing about.
1179
1235
 
1236
+ # When to fork (vs spawn_worker)
1237
+ \`fork\` is the **sibling primitive to spawn_worker** — both create a child you own, both report back to you on a future wake. The difference is only in what the child boots with:
1238
+
1239
+ - **spawn_worker** → child boots with an **empty context**; you brief it from scratch via a system prompt + initial message. Use when the worker doesn't need to know what we've said so far.
1240
+ - **fork** → child boots with **a copy of THIS conversation's history** up to your latest completed response. Use when each child needs to know what we've already established (the user's framing, your prior analysis, an earlier decision, the constraints, etc.).
1241
+
1242
+ **Trigger pattern: prefer fork when generating multiple variants the user wants to compare.** If the user asks for "three different X" or "two takes on Y" or "evaluate these N approaches" and each variant should reflect the conversation we've had so far, **don't inline the variants in one response** — fork once per variant, send each a tailored follow-up, and synthesize when they report back. Inlining feels faster but the variants end up cross-contaminating in your single response; forks keep them honestly independent. The exception is trivial generation (a list of names, a couple of one-liners) where each variant takes a sentence — there, inline is fine.
1243
+
1244
+ Workflow when forking yourself for parallel exploration:
1245
+ 1. **End your current turn first.** The fork's history stops at your *latest completed* run. Anything you say mid-turn is NOT in the fork. If you want your analysis baked into each fork, finish it and end the turn before calling fork.
1246
+ 2. On the next wake, call \`fork\` once per branch with a different \`initialMessage\` per call — that's how the branches diverge from a shared starting point. Each fork is YOUR child, just like a spawned worker, and the server delivers \`initialMessage\` to the fork in the same round-trip, so the fork starts running immediately (no follow-up \`send\` needed). Use the shape \`{ text: "..." }\` for the message so it renders in the chat UI.
1247
+ 3. End your turn. You'll wake automatically when each fork's run finishes (same wake mechanism as spawn_worker); the wake message identifies the fork and includes its response.
1248
+ 4. If you're waiting on multiple forks, don't synthesize on the first wake — quietly end the turn with "got N of M, waiting" until you have what you need to compare.
1249
+
1180
1250
  # Reporting
1181
1251
  Report outcomes faithfully. If a command failed, say so with the relevant output. If you didn't run a verification step, say that rather than implying you did. Don't hedge confirmed results with unnecessary disclaimers.
1182
1252
 
@@ -1202,6 +1272,7 @@ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1202
1272
  logPrefix: opts.logPrefix ?? `[horton]`
1203
1273
  })] : [createFetchUrlTool(sandbox)],
1204
1274
  createSpawnWorkerTool(ctx, opts.modelConfig),
1275
+ createForkTool(ctx),
1205
1276
  createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
1206
1277
  ...opts.docsSearchTool ? [opts.docsSearchTool] : []
1207
1278
  ];
@@ -1294,6 +1365,7 @@ function createAssistantHandler(options) {
1294
1365
  ...mcp.tools()
1295
1366
  ];
1296
1367
  const hasEventSourceTools = tools.some((tool) => getToolName(tool) === `list_event_sources`);
1368
+ const hasScheduleTools = tools.some((tool) => getToolName(tool) === `upsert_cron_schedule`);
1297
1369
  const titlePromise = !ctx.tags.title ? (async () => {
1298
1370
  const firstUserMessage = await extractFirstUserMessage(ctx);
1299
1371
  if (!firstUserMessage) return;
@@ -1374,7 +1446,8 @@ function createAssistantHandler(options) {
1374
1446
  docsUrl,
1375
1447
  modelProvider: modelConfig.provider,
1376
1448
  modelId: String(modelConfig.model),
1377
- hasEventSourceTools
1449
+ hasEventSourceTools,
1450
+ hasScheduleTools
1378
1451
  }),
1379
1452
  ...modelConfig,
1380
1453
  tools,
@@ -1646,7 +1719,10 @@ function dedupeToolsByName(tools) {
1646
1719
  }
1647
1720
  function createBuiltinElectricTools(custom) {
1648
1721
  return async (context) => {
1649
- const builtinTools = createEventSourceTools(context);
1722
+ const builtinTools = [...createEventSourceTools(context), ...createScheduleTools({
1723
+ ...context,
1724
+ db: context.db
1725
+ })];
1650
1726
  const customTools = custom ? await custom(context) : [];
1651
1727
  return dedupeToolsByName([...builtinTools, ...customTools]);
1652
1728
  };
@@ -1688,7 +1764,7 @@ async function createBuiltinAgentHandler(options) {
1688
1764
  modelCatalog
1689
1765
  });
1690
1766
  typeNames.push(`worker`);
1691
- const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
1767
+ const { profiles: sandboxProfiles, shutdownSandboxes } = await buildBuiltinSandboxProfiles(cwd);
1692
1768
  const runtime = createRuntimeHandler({
1693
1769
  baseUrl: agentServerUrl,
1694
1770
  serveEndpoint,
@@ -1707,7 +1783,8 @@ async function createBuiltinAgentHandler(options) {
1707
1783
  runtime,
1708
1784
  registry,
1709
1785
  typeNames,
1710
- skillsRegistry
1786
+ skillsRegistry,
1787
+ shutdownSandboxes
1711
1788
  };
1712
1789
  }
1713
1790
  async function registerBuiltinAgentTypes(bootstrap) {
@@ -1718,18 +1795,22 @@ async function registerBuiltinAgentTypes(bootstrap) {
1718
1795
  * Guard so repeated `buildBuiltinSandboxProfiles` calls in one process don't
1719
1796
  * re-run the boot sweep.
1720
1797
  */
1721
- let dockerSweptOnBoot = false;
1798
+ let dockerBootSweep = null;
1722
1799
  function sweepOrphanedDockerSandboxesOnce(sweep) {
1723
- if (dockerSweptOnBoot) return;
1724
- dockerSweptOnBoot = true;
1725
- sweep().then((removed) => {
1726
- if (removed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep removed ${removed.length} leftover container(s)`);
1800
+ dockerBootSweep ??= sweep().then((reclaimed) => {
1801
+ if (reclaimed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep reclaimed ${reclaimed.length} leftover container(s)`);
1727
1802
  }).catch((err) => serverLog.warn(`[builtin-agents] docker sandbox boot sweep error: ${err instanceof Error ? err.message : String(err)}`));
1803
+ return dockerBootSweep;
1728
1804
  }
1729
1805
  /**
1730
1806
  * Built-in sandbox profiles. `local` is always available. `docker` is
1731
1807
  * gated on Docker being reachable so a user without Docker installed
1732
1808
  * sees only what works — the UI never offers a non-functional choice.
1809
+ *
1810
+ * Also returns `shutdownSandboxes` when a host-local provider registered: an
1811
+ * immediate teardown of this process's live containers that the embedding
1812
+ * server must run on shutdown (the providers' debounced idle teardowns die
1813
+ * with the process).
1733
1814
  */
1734
1815
  async function buildBuiltinSandboxProfiles(workingDirectory) {
1735
1816
  const profiles = [{
@@ -1738,29 +1819,36 @@ async function buildBuiltinSandboxProfiles(workingDirectory) {
1738
1819
  description: `Runs on the host without isolation. Full filesystem access.`,
1739
1820
  factory: ({ args }) => chooseDefaultSandbox(resolveCwd(args, workingDirectory))
1740
1821
  }];
1822
+ let shutdownSandboxes = null;
1741
1823
  try {
1742
1824
  const { isDockerAvailable } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1743
1825
  if (await isDockerAvailable()) {
1744
- const { dockerSandbox, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1745
- sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
1826
+ const { dockerSandbox, reclaimDockerSandboxByKey, shutdownAllDockerSandboxes, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1827
+ await sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
1828
+ shutdownSandboxes = shutdownAllDockerSandboxes;
1746
1829
  profiles.push({
1747
1830
  name: `docker`,
1748
1831
  label: `Docker`,
1749
1832
  description: `Runs in a hardened Docker container: dropped capabilities, no privilege escalation, and CPU/memory/process limits. The chosen working directory is mounted read-write and, by default, network egress is unrestricted (allow-all).`,
1750
- factory: ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
1833
+ factory: async ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
1751
1834
  const cwd = readWorkingDirectoryArg(args);
1752
- return dockerSandbox({
1753
- initialNetworkPolicy: { mode: `allow-all` },
1754
- extraMounts: cwd ? [{
1755
- hostPath: cwd,
1756
- containerPath: `/work`,
1757
- readOnly: false
1758
- }] : void 0,
1759
- sandboxKey,
1760
- persistent,
1761
- owner,
1762
- entityType,
1763
- entityUrl
1835
+ return lazySandbox({
1836
+ name: `docker`,
1837
+ workingDirectory: `/work`,
1838
+ factory: () => dockerSandbox({
1839
+ initialNetworkPolicy: { mode: `allow-all` },
1840
+ extraMounts: cwd ? [{
1841
+ hostPath: cwd,
1842
+ containerPath: `/work`,
1843
+ readOnly: false
1844
+ }] : void 0,
1845
+ sandboxKey,
1846
+ persistent,
1847
+ owner,
1848
+ entityType,
1849
+ entityUrl
1850
+ }),
1851
+ reclaim: owner ? () => reclaimDockerSandboxByKey(sandboxKey) : void 0
1764
1852
  });
1765
1853
  }
1766
1854
  });
@@ -1784,7 +1872,10 @@ async function buildBuiltinSandboxProfiles(workingDirectory) {
1784
1872
  });
1785
1873
  else serverLog.info(`[builtin-agents] E2B_API_KEY set but the "e2b" package is not installed — e2b sandbox profile not registered`);
1786
1874
  console.log(`[builtin-agents] sandbox profiles advertised: ${profiles.map((p) => p.name).join(`, `)}`);
1787
- return profiles;
1875
+ return {
1876
+ profiles,
1877
+ shutdownSandboxes
1878
+ };
1788
1879
  }
1789
1880
  function readWorkingDirectoryArg(args) {
1790
1881
  const v = args.workingDirectory;
@@ -1990,6 +2081,9 @@ var BuiltinAgentsServer = class {
1990
2081
  await Promise.race([this.bootstrap.runtime.drainWakes().catch((err) => {
1991
2082
  serverLog.error(`[builtin-agents] drainWakes failed during shutdown:`, err);
1992
2083
  }), new Promise((resolve) => setTimeout(resolve, 5e3))]);
2084
+ if (this.bootstrap.shutdownSandboxes) await Promise.race([this.bootstrap.shutdownSandboxes().catch((err) => {
2085
+ serverLog.error(`[builtin-agents] sandbox shutdown failed during shutdown:`, err);
2086
+ }), new Promise((resolve) => setTimeout(resolve, 5e3))]);
1993
2087
  this.bootstrap = null;
1994
2088
  }
1995
2089
  this.mcpStopping = true;
package/dist/index.cjs CHANGED
@@ -818,6 +818,60 @@ function createSpawnWorkerTool(ctx, modelConfig) {
818
818
  };
819
819
  }
820
820
 
821
+ //#endregion
822
+ //#region src/tools/fork.ts
823
+ function createForkTool(ctx) {
824
+ return {
825
+ name: `fork`,
826
+ label: `Fork`,
827
+ description: `Fork a session at its latest completed agent response, producing a child copy of the conversation up to that point. The new fork is YOUR child — same parent-ownership model as a spawned worker — and it reports back to you the same way: when its next run finishes you'll be woken with its response. End your turn after forking.
828
+
829
+ Prefer supplying an 'initialMessage' so the fork is dispatched immediately in a single call — no follow-up 'send' needed. If you omit it, the fork boots idle and you'll need to call 'send' afterwards. For chat-rendered messages use the shape \`{ "text": "..." }\` so the prompt shows up in the chat UI.
830
+
831
+ Use this to explore multiple alternative continuations in parallel from the same starting point. End your current turn first so the fork includes your latest response — the anchor is always the most recently completed run.
832
+
833
+ Omit 'entityUrl' to fork your own session. Pass a different session's URL to fork that session instead (the new fork is still your child). The optional 'id' names the new fork's instance — useful when you want stable, predictable URLs (e.g. labelling branches in a parallel exploration); omit to let the server mint one.`,
834
+ parameters: __sinclair_typebox.Type.Object({
835
+ entityUrl: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String({ description: `URL of the session to fork. Omit to fork your own session.` })),
836
+ id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String({ description: `Instance id for the new fork (the \`<id>\` in \`/horton/<id>\`). Mirrors spawn_worker's id parameter. Omit to let the server assign one.` })),
837
+ initialMessage: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Any({ description: `Initial inbox message delivered to the fork by the server in the same round-trip — the fork wakes and starts running immediately, no follow-up 'send' needed. Use the shape \`{ "text": "..." }\` for chat-rendered prompts. Omit to leave the fork idle (then call 'send' separately).` })),
838
+ tags: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String(), { description: `Optional tags stamped on the new fork, on top of those copied from the source. Useful for labelling experiments (e.g. \`{ "experiment": "ecosystem-maturity" }\`).` }))
839
+ }),
840
+ execute: async (_toolCallId, params) => {
841
+ const { entityUrl, id, initialMessage, tags } = params;
842
+ try {
843
+ const opts = {
844
+ ...initialMessage !== void 0 && { initialMessage },
845
+ ...tags !== void 0 && { tags }
846
+ };
847
+ const forkId = id ?? `fork-${(0, nanoid.nanoid)(10)}`;
848
+ const handle = entityUrl !== void 0 ? await ctx.fork(entityUrl, forkId, opts) : await ctx.forkSelf(forkId, opts);
849
+ const dispatchNote = initialMessage !== void 0 ? `The initial message has been delivered to the fork — it will start running.` : `The fork boots idle — use the 'send' tool to dispatch a follow-up prompt.`;
850
+ return {
851
+ content: [{
852
+ type: `text`,
853
+ text: `Forked at ${handle.entityUrl}. ${dispatchNote} End your turn; you'll wake with the fork's response when its next run finishes (same as a spawned worker).`
854
+ }],
855
+ details: {
856
+ forked: true,
857
+ forkUrl: handle.entityUrl
858
+ }
859
+ };
860
+ } catch (err) {
861
+ const message = err instanceof Error ? err.message : `Unknown error`;
862
+ serverLog.warn(`[fork tool] failed to fork ${entityUrl ?? `<self>`}: ${message}`, err instanceof Error ? err : void 0);
863
+ return {
864
+ content: [{
865
+ type: `text`,
866
+ text: `Error forking session: ${message}`
867
+ }],
868
+ details: { forked: false }
869
+ };
870
+ }
871
+ }
872
+ };
873
+ }
874
+
821
875
  //#endregion
822
876
  //#region src/model-catalog.ts
823
877
  const MODEL_INPUTS_SCHEMA_DEF = `electricModelInputs`;
@@ -1109,6 +1163,7 @@ async function generateTitle(userMessage, llmCall, onFallback) {
1109
1163
  function buildHortonSystemPrompt(workingDirectory, opts = {}) {
1110
1164
  const docsTools = opts.hasDocsSupport ? `\n- search_electric_agents_docs: hybrid search over the built-in Electric Agents docs index` : ``;
1111
1165
  const eventSourceTools = opts.hasEventSourceTools ? `\n- list_event_sources: list external webhook/event feeds you can subscribe to, including available buckets and parameters\n- subscribe_event_source: subscribe yourself to one of those feeds or buckets so matching future events wake you\n- list_event_source_subscriptions: list your active event source subscriptions\n- unsubscribe_event_source: remove one of your event source subscriptions by id` : ``;
1166
+ const scheduleTools = opts.hasScheduleTools ? `\n- upsert_cron_schedule: create or update a recurring cron wake for yourself. Always include payload with the concrete instruction/message you should receive when the cron fires.\n- delete_schedule: delete one of your cron or future-send schedules by stable id\n- list_schedules: list your manifest-backed cron and future-send schedules` : ``;
1112
1167
  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` : ``;
1113
1168
  const docsGuidance = opts.hasDocsSupport ? `\n- For ANY question about Electric Agents or this framework, ALWAYS use search_electric_agents_docs FIRST. Do not use web_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.` : ``;
1114
1169
  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.
@@ -1162,8 +1217,9 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
1162
1217
  - web_search: search the web
1163
1218
  - fetch_url: fetch and convert a URL to markdown
1164
1219
  - spawn_worker: dispatch a subagent for an isolated task
1220
+ - fork: spawn a child session that inherits this conversation's history up to the latest completed response. Same parent-ownership model as spawn_worker — when the fork's next run finishes, you'll wake with its response.
1165
1221
  - send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs.
1166
- ${eventSourceTools}${docsTools}${skillsTools}
1222
+ ${eventSourceTools}${scheduleTools}${docsTools}${skillsTools}
1167
1223
 
1168
1224
  # Working with files
1169
1225
  - Prefer edit over write when modifying existing files.
@@ -1190,6 +1246,20 @@ When you spawn a worker, write its system prompt the way you'd brief a colleague
1190
1246
 
1191
1247
  After spawning, end your turn (optionally with a brief "I've dispatched a worker for X; I'll respond when it finishes"). When the worker finishes, you'll receive a message describing which worker completed and what it returned. Multiple workers may finish at different times — check the message for the worker URL to know which one you're hearing about.
1192
1248
 
1249
+ # When to fork (vs spawn_worker)
1250
+ \`fork\` is the **sibling primitive to spawn_worker** — both create a child you own, both report back to you on a future wake. The difference is only in what the child boots with:
1251
+
1252
+ - **spawn_worker** → child boots with an **empty context**; you brief it from scratch via a system prompt + initial message. Use when the worker doesn't need to know what we've said so far.
1253
+ - **fork** → child boots with **a copy of THIS conversation's history** up to your latest completed response. Use when each child needs to know what we've already established (the user's framing, your prior analysis, an earlier decision, the constraints, etc.).
1254
+
1255
+ **Trigger pattern: prefer fork when generating multiple variants the user wants to compare.** If the user asks for "three different X" or "two takes on Y" or "evaluate these N approaches" and each variant should reflect the conversation we've had so far, **don't inline the variants in one response** — fork once per variant, send each a tailored follow-up, and synthesize when they report back. Inlining feels faster but the variants end up cross-contaminating in your single response; forks keep them honestly independent. The exception is trivial generation (a list of names, a couple of one-liners) where each variant takes a sentence — there, inline is fine.
1256
+
1257
+ Workflow when forking yourself for parallel exploration:
1258
+ 1. **End your current turn first.** The fork's history stops at your *latest completed* run. Anything you say mid-turn is NOT in the fork. If you want your analysis baked into each fork, finish it and end the turn before calling fork.
1259
+ 2. On the next wake, call \`fork\` once per branch with a different \`initialMessage\` per call — that's how the branches diverge from a shared starting point. Each fork is YOUR child, just like a spawned worker, and the server delivers \`initialMessage\` to the fork in the same round-trip, so the fork starts running immediately (no follow-up \`send\` needed). Use the shape \`{ text: "..." }\` for the message so it renders in the chat UI.
1260
+ 3. End your turn. You'll wake automatically when each fork's run finishes (same wake mechanism as spawn_worker); the wake message identifies the fork and includes its response.
1261
+ 4. If you're waiting on multiple forks, don't synthesize on the first wake — quietly end the turn with "got N of M, waiting" until you have what you need to compare.
1262
+
1193
1263
  # Reporting
1194
1264
  Report outcomes faithfully. If a command failed, say so with the relevant output. If you didn't run a verification step, say that rather than implying you did. Don't hedge confirmed results with unnecessary disclaimers.
1195
1265
 
@@ -1215,6 +1285,7 @@ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1215
1285
  logPrefix: opts.logPrefix ?? `[horton]`
1216
1286
  })] : [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox)],
1217
1287
  createSpawnWorkerTool(ctx, opts.modelConfig),
1288
+ createForkTool(ctx),
1218
1289
  (0, __electric_ax_agents_runtime_tools.createSendTool)(ctx.send, { selfEntityUrl: ctx.entityUrl }),
1219
1290
  ...opts.docsSearchTool ? [opts.docsSearchTool] : []
1220
1291
  ];
@@ -1307,6 +1378,7 @@ function createAssistantHandler(options) {
1307
1378
  ...__electric_ax_agents_mcp.mcp.tools()
1308
1379
  ];
1309
1380
  const hasEventSourceTools = tools.some((tool) => getToolName(tool) === `list_event_sources`);
1381
+ const hasScheduleTools = tools.some((tool) => getToolName(tool) === `upsert_cron_schedule`);
1310
1382
  const titlePromise = !ctx.tags.title ? (async () => {
1311
1383
  const firstUserMessage = await extractFirstUserMessage(ctx);
1312
1384
  if (!firstUserMessage) return;
@@ -1387,7 +1459,8 @@ function createAssistantHandler(options) {
1387
1459
  docsUrl,
1388
1460
  modelProvider: modelConfig.provider,
1389
1461
  modelId: String(modelConfig.model),
1390
- hasEventSourceTools
1462
+ hasEventSourceTools,
1463
+ hasScheduleTools
1391
1464
  }),
1392
1465
  ...modelConfig,
1393
1466
  tools,
@@ -1660,7 +1733,10 @@ function dedupeToolsByName(tools) {
1660
1733
  }
1661
1734
  function createBuiltinElectricTools(custom) {
1662
1735
  return async (context) => {
1663
- const builtinTools = (0, __electric_ax_agents_runtime_tools.createEventSourceTools)(context);
1736
+ const builtinTools = [...(0, __electric_ax_agents_runtime_tools.createEventSourceTools)(context), ...(0, __electric_ax_agents_runtime_tools.createScheduleTools)({
1737
+ ...context,
1738
+ db: context.db
1739
+ })];
1664
1740
  const customTools = custom ? await custom(context) : [];
1665
1741
  return dedupeToolsByName([...builtinTools, ...customTools]);
1666
1742
  };
@@ -1702,7 +1778,7 @@ async function createBuiltinAgentHandler(options) {
1702
1778
  modelCatalog
1703
1779
  });
1704
1780
  typeNames.push(`worker`);
1705
- const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
1781
+ const { profiles: sandboxProfiles, shutdownSandboxes } = await buildBuiltinSandboxProfiles(cwd);
1706
1782
  const runtime = (0, __electric_ax_agents_runtime.createRuntimeHandler)({
1707
1783
  baseUrl: agentServerUrl,
1708
1784
  serveEndpoint,
@@ -1721,7 +1797,8 @@ async function createBuiltinAgentHandler(options) {
1721
1797
  runtime,
1722
1798
  registry,
1723
1799
  typeNames,
1724
- skillsRegistry
1800
+ skillsRegistry,
1801
+ shutdownSandboxes
1725
1802
  };
1726
1803
  }
1727
1804
  async function createAgentHandler(agentServerUrl, workingDirectory, streamFn, createElectricTools, serveEndpoint) {
@@ -1742,18 +1819,22 @@ const registerAgentTypes = registerBuiltinAgentTypes;
1742
1819
  * Guard so repeated `buildBuiltinSandboxProfiles` calls in one process don't
1743
1820
  * re-run the boot sweep.
1744
1821
  */
1745
- let dockerSweptOnBoot = false;
1822
+ let dockerBootSweep = null;
1746
1823
  function sweepOrphanedDockerSandboxesOnce(sweep) {
1747
- if (dockerSweptOnBoot) return;
1748
- dockerSweptOnBoot = true;
1749
- sweep().then((removed) => {
1750
- if (removed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep removed ${removed.length} leftover container(s)`);
1824
+ dockerBootSweep ??= sweep().then((reclaimed) => {
1825
+ if (reclaimed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep reclaimed ${reclaimed.length} leftover container(s)`);
1751
1826
  }).catch((err) => serverLog.warn(`[builtin-agents] docker sandbox boot sweep error: ${err instanceof Error ? err.message : String(err)}`));
1827
+ return dockerBootSweep;
1752
1828
  }
1753
1829
  /**
1754
1830
  * Built-in sandbox profiles. `local` is always available. `docker` is
1755
1831
  * gated on Docker being reachable so a user without Docker installed
1756
1832
  * sees only what works — the UI never offers a non-functional choice.
1833
+ *
1834
+ * Also returns `shutdownSandboxes` when a host-local provider registered: an
1835
+ * immediate teardown of this process's live containers that the embedding
1836
+ * server must run on shutdown (the providers' debounced idle teardowns die
1837
+ * with the process).
1757
1838
  */
1758
1839
  async function buildBuiltinSandboxProfiles(workingDirectory) {
1759
1840
  const profiles = [{
@@ -1762,29 +1843,36 @@ async function buildBuiltinSandboxProfiles(workingDirectory) {
1762
1843
  description: `Runs on the host without isolation. Full filesystem access.`,
1763
1844
  factory: ({ args }) => (0, __electric_ax_agents_runtime_sandbox.chooseDefaultSandbox)(resolveCwd(args, workingDirectory))
1764
1845
  }];
1846
+ let shutdownSandboxes = null;
1765
1847
  try {
1766
1848
  const { isDockerAvailable } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1767
1849
  if (await isDockerAvailable()) {
1768
- const { dockerSandbox, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1769
- sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
1850
+ const { dockerSandbox, reclaimDockerSandboxByKey, shutdownAllDockerSandboxes, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1851
+ await sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
1852
+ shutdownSandboxes = shutdownAllDockerSandboxes;
1770
1853
  profiles.push({
1771
1854
  name: `docker`,
1772
1855
  label: `Docker`,
1773
1856
  description: `Runs in a hardened Docker container: dropped capabilities, no privilege escalation, and CPU/memory/process limits. The chosen working directory is mounted read-write and, by default, network egress is unrestricted (allow-all).`,
1774
- factory: ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
1857
+ factory: async ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
1775
1858
  const cwd = readWorkingDirectoryArg(args);
1776
- return dockerSandbox({
1777
- initialNetworkPolicy: { mode: `allow-all` },
1778
- extraMounts: cwd ? [{
1779
- hostPath: cwd,
1780
- containerPath: `/work`,
1781
- readOnly: false
1782
- }] : void 0,
1783
- sandboxKey,
1784
- persistent,
1785
- owner,
1786
- entityType,
1787
- entityUrl
1859
+ return (0, __electric_ax_agents_runtime_sandbox.lazySandbox)({
1860
+ name: `docker`,
1861
+ workingDirectory: `/work`,
1862
+ factory: () => dockerSandbox({
1863
+ initialNetworkPolicy: { mode: `allow-all` },
1864
+ extraMounts: cwd ? [{
1865
+ hostPath: cwd,
1866
+ containerPath: `/work`,
1867
+ readOnly: false
1868
+ }] : void 0,
1869
+ sandboxKey,
1870
+ persistent,
1871
+ owner,
1872
+ entityType,
1873
+ entityUrl
1874
+ }),
1875
+ reclaim: owner ? () => reclaimDockerSandboxByKey(sandboxKey) : void 0
1788
1876
  });
1789
1877
  }
1790
1878
  });
@@ -1808,7 +1896,10 @@ async function buildBuiltinSandboxProfiles(workingDirectory) {
1808
1896
  });
1809
1897
  else serverLog.info(`[builtin-agents] E2B_API_KEY set but the "e2b" package is not installed — e2b sandbox profile not registered`);
1810
1898
  console.log(`[builtin-agents] sandbox profiles advertised: ${profiles.map((p) => p.name).join(`, `)}`);
1811
- return profiles;
1899
+ return {
1900
+ profiles,
1901
+ shutdownSandboxes
1902
+ };
1812
1903
  }
1813
1904
  function readWorkingDirectoryArg(args) {
1814
1905
  const v = args.workingDirectory;
@@ -1827,7 +1918,7 @@ function installDurableStreamsFetchCache(options = {}) {
1827
1918
  location: options.sqliteLocation,
1828
1919
  maxCount: options.maxCount
1829
1920
  }) : new undici.cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
1830
- (0, undici.setGlobalDispatcher)(new undici.Agent().compose(undici.interceptors.cache({ store })));
1921
+ (0, undici.setGlobalDispatcher)((0, undici.getGlobalDispatcher)().compose(undici.interceptors.cache({ store })));
1831
1922
  }
1832
1923
 
1833
1924
  //#endregion
@@ -2026,6 +2117,9 @@ var BuiltinAgentsServer = class {
2026
2117
  await Promise.race([this.bootstrap.runtime.drainWakes().catch((err) => {
2027
2118
  serverLog.error(`[builtin-agents] drainWakes failed during shutdown:`, err);
2028
2119
  }), new Promise((resolve) => setTimeout(resolve, 5e3))]);
2120
+ if (this.bootstrap.shutdownSandboxes) await Promise.race([this.bootstrap.shutdownSandboxes().catch((err) => {
2121
+ serverLog.error(`[builtin-agents] sandbox shutdown failed during shutdown:`, err);
2122
+ }), new Promise((resolve) => setTimeout(resolve, 5e3))]);
2029
2123
  this.bootstrap = null;
2030
2124
  }
2031
2125
  this.mcpStopping = true;
@@ -2166,6 +2260,7 @@ exports.builtinModelProviderLabel = builtinModelProviderLabel
2166
2260
  exports.createAgentHandler = createAgentHandler
2167
2261
  exports.createBuiltinAgentHandler = createBuiltinAgentHandler
2168
2262
  exports.createBuiltinElectricTools = createBuiltinElectricTools
2263
+ exports.createForkTool = createForkTool
2169
2264
  exports.createHortonDocsSupport = createHortonDocsSupport
2170
2265
  exports.createHortonTools = createHortonTools
2171
2266
  exports.createSpawnWorkerTool = createSpawnWorkerTool
package/dist/index.d.cts CHANGED
@@ -14,6 +14,13 @@ interface AgentHandlerResult {
14
14
  registry: EntityRegistry;
15
15
  typeNames: Array<string>;
16
16
  skillsRegistry: SkillsRegistry | null;
17
+ /**
18
+ * Immediately tears down the idle sandboxes this process created (set when
19
+ * a provider with host-local state — docker — is registered). MUST be called
20
+ * on shutdown, after wakes drain: the providers' debounced idle teardowns
21
+ * die with the process, which would leave containers running.
22
+ */
23
+ shutdownSandboxes: (() => Promise<void>) | null;
17
24
  }
18
25
  type BuiltinElectricToolsFactory = NonNullable<ProcessWakeConfig[`createElectricTools`]>;
19
26
  interface BuiltinAgentHandlerOptions {
@@ -190,6 +197,7 @@ declare function generateTitle(userMessage: string, llmCall: (prompt: string) =>
190
197
  declare function buildHortonSystemPrompt(workingDirectory: string, opts?: {
191
198
  hasDocsSupport?: boolean;
192
199
  hasEventSourceTools?: boolean;
200
+ hasScheduleTools?: boolean;
193
201
  hasSkills?: boolean;
194
202
  docsUrl?: string;
195
203
  modelProvider?: string;
@@ -223,6 +231,10 @@ declare const WORKER_TOOL_NAMES: readonly ["bash", "read", "write", "edit", "web
223
231
  type WorkerToolName = (typeof WORKER_TOOL_NAMES)[number];
224
232
  declare function createSpawnWorkerTool(ctx: HandlerContext, modelConfig?: BuiltinAgentModelConfig): AgentTool$1;
225
233
 
234
+ //#endregion
235
+ //#region src/tools/fork.d.ts
236
+ declare function createForkTool(ctx: HandlerContext): AgentTool$1;
237
+
226
238
  //#endregion
227
239
  //#region src/docs/knowledge-base.d.ts
228
240
  interface HortonDocsSupport {
@@ -242,4 +254,4 @@ declare function createHortonDocsSupport(workingDirectory: string, opts?: {
242
254
  }): HortonDocsSupport | null;
243
255
 
244
256
  //#endregion
245
- export { AgentHandlerResult, BuiltinAgentHandlerOptions, BuiltinAgentsEntrypointOptions, BuiltinAgentsEntrypointServer, BuiltinAgentsServer, BuiltinAgentsServerOptions, BuiltinElectricToolsFactory, BuiltinModelCatalogOptions, BuiltinModelChoice, BuiltinModelProvider, DEFAULT_BUILTIN_AGENT_HANDLER_PATH, HORTON_MODEL, McpConfig, McpListedEntry, McpRegistry, McpServerConfig, RegistrySnapshot, RegistrySubscriber, RunBuiltinAgentsEntrypointOptions, WORKER_TOOL_NAMES, WorkerToolName, braveSearchTool, buildHortonSystemPrompt, builtinModelProviderLabel, createAgentHandler, createBuiltinAgentHandler, createBuiltinElectricTools, createHortonDocsSupport, createHortonTools, createSpawnWorkerTool, generateTitle, listBuiltinModelChoices, registerAgentTypes, registerBuiltinAgentTypes, registerHorton, registerWorker, resolveBuiltinAgentsEntrypointOptions, runBuiltinAgentsEntrypoint };
257
+ export { AgentHandlerResult, BuiltinAgentHandlerOptions, BuiltinAgentsEntrypointOptions, BuiltinAgentsEntrypointServer, BuiltinAgentsServer, BuiltinAgentsServerOptions, BuiltinElectricToolsFactory, BuiltinModelCatalogOptions, BuiltinModelChoice, BuiltinModelProvider, DEFAULT_BUILTIN_AGENT_HANDLER_PATH, HORTON_MODEL, McpConfig, McpListedEntry, McpRegistry, McpServerConfig, RegistrySnapshot, RegistrySubscriber, RunBuiltinAgentsEntrypointOptions, WORKER_TOOL_NAMES, WorkerToolName, braveSearchTool, buildHortonSystemPrompt, builtinModelProviderLabel, createAgentHandler, createBuiltinAgentHandler, createBuiltinElectricTools, createForkTool, createHortonDocsSupport, createHortonTools, createSpawnWorkerTool, generateTitle, listBuiltinModelChoices, registerAgentTypes, registerBuiltinAgentTypes, registerHorton, registerWorker, resolveBuiltinAgentsEntrypointOptions, runBuiltinAgentsEntrypoint };
package/dist/index.d.ts CHANGED
@@ -14,6 +14,13 @@ interface AgentHandlerResult {
14
14
  registry: EntityRegistry;
15
15
  typeNames: Array<string>;
16
16
  skillsRegistry: SkillsRegistry | null;
17
+ /**
18
+ * Immediately tears down the idle sandboxes this process created (set when
19
+ * a provider with host-local state — docker — is registered). MUST be called
20
+ * on shutdown, after wakes drain: the providers' debounced idle teardowns
21
+ * die with the process, which would leave containers running.
22
+ */
23
+ shutdownSandboxes: (() => Promise<void>) | null;
17
24
  }
18
25
  type BuiltinElectricToolsFactory = NonNullable<ProcessWakeConfig[`createElectricTools`]>;
19
26
  interface BuiltinAgentHandlerOptions {
@@ -190,6 +197,7 @@ declare function generateTitle(userMessage: string, llmCall: (prompt: string) =>
190
197
  declare function buildHortonSystemPrompt(workingDirectory: string, opts?: {
191
198
  hasDocsSupport?: boolean;
192
199
  hasEventSourceTools?: boolean;
200
+ hasScheduleTools?: boolean;
193
201
  hasSkills?: boolean;
194
202
  docsUrl?: string;
195
203
  modelProvider?: string;
@@ -223,6 +231,10 @@ declare const WORKER_TOOL_NAMES: readonly ["bash", "read", "write", "edit", "web
223
231
  type WorkerToolName = (typeof WORKER_TOOL_NAMES)[number];
224
232
  declare function createSpawnWorkerTool(ctx: HandlerContext, modelConfig?: BuiltinAgentModelConfig): AgentTool$1;
225
233
 
234
+ //#endregion
235
+ //#region src/tools/fork.d.ts
236
+ declare function createForkTool(ctx: HandlerContext): AgentTool$1;
237
+
226
238
  //#endregion
227
239
  //#region src/docs/knowledge-base.d.ts
228
240
  interface HortonDocsSupport {
@@ -242,4 +254,4 @@ declare function createHortonDocsSupport(workingDirectory: string, opts?: {
242
254
  }): HortonDocsSupport | null;
243
255
 
244
256
  //#endregion
245
- export { AgentHandlerResult, BuiltinAgentHandlerOptions, BuiltinAgentsEntrypointOptions, BuiltinAgentsEntrypointServer, BuiltinAgentsServer, BuiltinAgentsServerOptions, BuiltinElectricToolsFactory, BuiltinModelCatalogOptions, BuiltinModelChoice, BuiltinModelProvider, DEFAULT_BUILTIN_AGENT_HANDLER_PATH, HORTON_MODEL, McpConfig, McpListedEntry, McpRegistry, McpServerConfig, RegistrySnapshot, RegistrySubscriber, RunBuiltinAgentsEntrypointOptions, WORKER_TOOL_NAMES, WorkerToolName, braveSearchTool, buildHortonSystemPrompt, builtinModelProviderLabel, createAgentHandler, createBuiltinAgentHandler, createBuiltinElectricTools, createHortonDocsSupport, createHortonTools, createSpawnWorkerTool, generateTitle, listBuiltinModelChoices, registerAgentTypes, registerBuiltinAgentTypes, registerHorton, registerWorker, resolveBuiltinAgentsEntrypointOptions, runBuiltinAgentsEntrypoint };
257
+ export { AgentHandlerResult, BuiltinAgentHandlerOptions, BuiltinAgentsEntrypointOptions, BuiltinAgentsEntrypointServer, BuiltinAgentsServer, BuiltinAgentsServerOptions, BuiltinElectricToolsFactory, BuiltinModelCatalogOptions, BuiltinModelChoice, BuiltinModelProvider, DEFAULT_BUILTIN_AGENT_HANDLER_PATH, HORTON_MODEL, McpConfig, McpListedEntry, McpRegistry, McpServerConfig, RegistrySnapshot, RegistrySubscriber, RunBuiltinAgentsEntrypointOptions, WORKER_TOOL_NAMES, WorkerToolName, braveSearchTool, buildHortonSystemPrompt, builtinModelProviderLabel, createAgentHandler, createBuiltinAgentHandler, createBuiltinElectricTools, createForkTool, createHortonDocsSupport, createHortonTools, createSpawnWorkerTool, generateTitle, listBuiltinModelChoices, registerAgentTypes, registerBuiltinAgentTypes, registerHorton, registerWorker, resolveBuiltinAgentsEntrypointOptions, runBuiltinAgentsEntrypoint };
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import { mergeElectricPrincipalHeader } from "./server-headers-KD5yHFYT.js";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, buildSkillSlashCommands, completeWithLowCostModel, createContextSkillLoader, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModel, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
5
- import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
6
- import { chooseDefaultSandbox, isE2BAvailable, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
5
+ import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createScheduleTools, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
6
+ import { chooseDefaultSandbox, isE2BAvailable, lazySandbox, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
7
7
  import fsSync from "node:fs";
8
8
  import pino from "pino";
9
9
  import { z } from "zod";
@@ -15,7 +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
+ import { cacheStores, getGlobalDispatcher, interceptors, setGlobalDispatcher } from "undici";
19
19
 
20
20
  //#region src/log.ts
21
21
  const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
@@ -794,6 +794,60 @@ function createSpawnWorkerTool(ctx, modelConfig) {
794
794
  };
795
795
  }
796
796
 
797
+ //#endregion
798
+ //#region src/tools/fork.ts
799
+ function createForkTool(ctx) {
800
+ return {
801
+ name: `fork`,
802
+ label: `Fork`,
803
+ description: `Fork a session at its latest completed agent response, producing a child copy of the conversation up to that point. The new fork is YOUR child — same parent-ownership model as a spawned worker — and it reports back to you the same way: when its next run finishes you'll be woken with its response. End your turn after forking.
804
+
805
+ Prefer supplying an 'initialMessage' so the fork is dispatched immediately in a single call — no follow-up 'send' needed. If you omit it, the fork boots idle and you'll need to call 'send' afterwards. For chat-rendered messages use the shape \`{ "text": "..." }\` so the prompt shows up in the chat UI.
806
+
807
+ Use this to explore multiple alternative continuations in parallel from the same starting point. End your current turn first so the fork includes your latest response — the anchor is always the most recently completed run.
808
+
809
+ Omit 'entityUrl' to fork your own session. Pass a different session's URL to fork that session instead (the new fork is still your child). The optional 'id' names the new fork's instance — useful when you want stable, predictable URLs (e.g. labelling branches in a parallel exploration); omit to let the server mint one.`,
810
+ parameters: Type.Object({
811
+ entityUrl: Type.Optional(Type.String({ description: `URL of the session to fork. Omit to fork your own session.` })),
812
+ id: Type.Optional(Type.String({ description: `Instance id for the new fork (the \`<id>\` in \`/horton/<id>\`). Mirrors spawn_worker's id parameter. Omit to let the server assign one.` })),
813
+ initialMessage: Type.Optional(Type.Any({ description: `Initial inbox message delivered to the fork by the server in the same round-trip — the fork wakes and starts running immediately, no follow-up 'send' needed. Use the shape \`{ "text": "..." }\` for chat-rendered prompts. Omit to leave the fork idle (then call 'send' separately).` })),
814
+ tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: `Optional tags stamped on the new fork, on top of those copied from the source. Useful for labelling experiments (e.g. \`{ "experiment": "ecosystem-maturity" }\`).` }))
815
+ }),
816
+ execute: async (_toolCallId, params) => {
817
+ const { entityUrl, id, initialMessage, tags } = params;
818
+ try {
819
+ const opts = {
820
+ ...initialMessage !== void 0 && { initialMessage },
821
+ ...tags !== void 0 && { tags }
822
+ };
823
+ const forkId = id ?? `fork-${nanoid(10)}`;
824
+ const handle = entityUrl !== void 0 ? await ctx.fork(entityUrl, forkId, opts) : await ctx.forkSelf(forkId, opts);
825
+ const dispatchNote = initialMessage !== void 0 ? `The initial message has been delivered to the fork — it will start running.` : `The fork boots idle — use the 'send' tool to dispatch a follow-up prompt.`;
826
+ return {
827
+ content: [{
828
+ type: `text`,
829
+ text: `Forked at ${handle.entityUrl}. ${dispatchNote} End your turn; you'll wake with the fork's response when its next run finishes (same as a spawned worker).`
830
+ }],
831
+ details: {
832
+ forked: true,
833
+ forkUrl: handle.entityUrl
834
+ }
835
+ };
836
+ } catch (err) {
837
+ const message = err instanceof Error ? err.message : `Unknown error`;
838
+ serverLog.warn(`[fork tool] failed to fork ${entityUrl ?? `<self>`}: ${message}`, err instanceof Error ? err : void 0);
839
+ return {
840
+ content: [{
841
+ type: `text`,
842
+ text: `Error forking session: ${message}`
843
+ }],
844
+ details: { forked: false }
845
+ };
846
+ }
847
+ }
848
+ };
849
+ }
850
+
797
851
  //#endregion
798
852
  //#region src/model-catalog.ts
799
853
  const MODEL_INPUTS_SCHEMA_DEF = `electricModelInputs`;
@@ -1085,6 +1139,7 @@ async function generateTitle(userMessage, llmCall, onFallback) {
1085
1139
  function buildHortonSystemPrompt(workingDirectory, opts = {}) {
1086
1140
  const docsTools = opts.hasDocsSupport ? `\n- search_electric_agents_docs: hybrid search over the built-in Electric Agents docs index` : ``;
1087
1141
  const eventSourceTools = opts.hasEventSourceTools ? `\n- list_event_sources: list external webhook/event feeds you can subscribe to, including available buckets and parameters\n- subscribe_event_source: subscribe yourself to one of those feeds or buckets so matching future events wake you\n- list_event_source_subscriptions: list your active event source subscriptions\n- unsubscribe_event_source: remove one of your event source subscriptions by id` : ``;
1142
+ const scheduleTools = opts.hasScheduleTools ? `\n- upsert_cron_schedule: create or update a recurring cron wake for yourself. Always include payload with the concrete instruction/message you should receive when the cron fires.\n- delete_schedule: delete one of your cron or future-send schedules by stable id\n- list_schedules: list your manifest-backed cron and future-send schedules` : ``;
1088
1143
  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` : ``;
1089
1144
  const docsGuidance = opts.hasDocsSupport ? `\n- For ANY question about Electric Agents or this framework, ALWAYS use search_electric_agents_docs FIRST. Do not use web_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.` : ``;
1090
1145
  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.
@@ -1138,8 +1193,9 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
1138
1193
  - web_search: search the web
1139
1194
  - fetch_url: fetch and convert a URL to markdown
1140
1195
  - spawn_worker: dispatch a subagent for an isolated task
1196
+ - fork: spawn a child session that inherits this conversation's history up to the latest completed response. Same parent-ownership model as spawn_worker — when the fork's next run finishes, you'll wake with its response.
1141
1197
  - send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs.
1142
- ${eventSourceTools}${docsTools}${skillsTools}
1198
+ ${eventSourceTools}${scheduleTools}${docsTools}${skillsTools}
1143
1199
 
1144
1200
  # Working with files
1145
1201
  - Prefer edit over write when modifying existing files.
@@ -1166,6 +1222,20 @@ When you spawn a worker, write its system prompt the way you'd brief a colleague
1166
1222
 
1167
1223
  After spawning, end your turn (optionally with a brief "I've dispatched a worker for X; I'll respond when it finishes"). When the worker finishes, you'll receive a message describing which worker completed and what it returned. Multiple workers may finish at different times — check the message for the worker URL to know which one you're hearing about.
1168
1224
 
1225
+ # When to fork (vs spawn_worker)
1226
+ \`fork\` is the **sibling primitive to spawn_worker** — both create a child you own, both report back to you on a future wake. The difference is only in what the child boots with:
1227
+
1228
+ - **spawn_worker** → child boots with an **empty context**; you brief it from scratch via a system prompt + initial message. Use when the worker doesn't need to know what we've said so far.
1229
+ - **fork** → child boots with **a copy of THIS conversation's history** up to your latest completed response. Use when each child needs to know what we've already established (the user's framing, your prior analysis, an earlier decision, the constraints, etc.).
1230
+
1231
+ **Trigger pattern: prefer fork when generating multiple variants the user wants to compare.** If the user asks for "three different X" or "two takes on Y" or "evaluate these N approaches" and each variant should reflect the conversation we've had so far, **don't inline the variants in one response** — fork once per variant, send each a tailored follow-up, and synthesize when they report back. Inlining feels faster but the variants end up cross-contaminating in your single response; forks keep them honestly independent. The exception is trivial generation (a list of names, a couple of one-liners) where each variant takes a sentence — there, inline is fine.
1232
+
1233
+ Workflow when forking yourself for parallel exploration:
1234
+ 1. **End your current turn first.** The fork's history stops at your *latest completed* run. Anything you say mid-turn is NOT in the fork. If you want your analysis baked into each fork, finish it and end the turn before calling fork.
1235
+ 2. On the next wake, call \`fork\` once per branch with a different \`initialMessage\` per call — that's how the branches diverge from a shared starting point. Each fork is YOUR child, just like a spawned worker, and the server delivers \`initialMessage\` to the fork in the same round-trip, so the fork starts running immediately (no follow-up \`send\` needed). Use the shape \`{ text: "..." }\` for the message so it renders in the chat UI.
1236
+ 3. End your turn. You'll wake automatically when each fork's run finishes (same wake mechanism as spawn_worker); the wake message identifies the fork and includes its response.
1237
+ 4. If you're waiting on multiple forks, don't synthesize on the first wake — quietly end the turn with "got N of M, waiting" until you have what you need to compare.
1238
+
1169
1239
  # Reporting
1170
1240
  Report outcomes faithfully. If a command failed, say so with the relevant output. If you didn't run a verification step, say that rather than implying you did. Don't hedge confirmed results with unnecessary disclaimers.
1171
1241
 
@@ -1191,6 +1261,7 @@ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1191
1261
  logPrefix: opts.logPrefix ?? `[horton]`
1192
1262
  })] : [createFetchUrlTool(sandbox)],
1193
1263
  createSpawnWorkerTool(ctx, opts.modelConfig),
1264
+ createForkTool(ctx),
1194
1265
  createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
1195
1266
  ...opts.docsSearchTool ? [opts.docsSearchTool] : []
1196
1267
  ];
@@ -1283,6 +1354,7 @@ function createAssistantHandler(options) {
1283
1354
  ...mcp.tools()
1284
1355
  ];
1285
1356
  const hasEventSourceTools = tools.some((tool) => getToolName(tool) === `list_event_sources`);
1357
+ const hasScheduleTools = tools.some((tool) => getToolName(tool) === `upsert_cron_schedule`);
1286
1358
  const titlePromise = !ctx.tags.title ? (async () => {
1287
1359
  const firstUserMessage = await extractFirstUserMessage(ctx);
1288
1360
  if (!firstUserMessage) return;
@@ -1363,7 +1435,8 @@ function createAssistantHandler(options) {
1363
1435
  docsUrl,
1364
1436
  modelProvider: modelConfig.provider,
1365
1437
  modelId: String(modelConfig.model),
1366
- hasEventSourceTools
1438
+ hasEventSourceTools,
1439
+ hasScheduleTools
1367
1440
  }),
1368
1441
  ...modelConfig,
1369
1442
  tools,
@@ -1636,7 +1709,10 @@ function dedupeToolsByName(tools) {
1636
1709
  }
1637
1710
  function createBuiltinElectricTools(custom) {
1638
1711
  return async (context) => {
1639
- const builtinTools = createEventSourceTools(context);
1712
+ const builtinTools = [...createEventSourceTools(context), ...createScheduleTools({
1713
+ ...context,
1714
+ db: context.db
1715
+ })];
1640
1716
  const customTools = custom ? await custom(context) : [];
1641
1717
  return dedupeToolsByName([...builtinTools, ...customTools]);
1642
1718
  };
@@ -1678,7 +1754,7 @@ async function createBuiltinAgentHandler(options) {
1678
1754
  modelCatalog
1679
1755
  });
1680
1756
  typeNames.push(`worker`);
1681
- const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
1757
+ const { profiles: sandboxProfiles, shutdownSandboxes } = await buildBuiltinSandboxProfiles(cwd);
1682
1758
  const runtime = createRuntimeHandler({
1683
1759
  baseUrl: agentServerUrl,
1684
1760
  serveEndpoint,
@@ -1697,7 +1773,8 @@ async function createBuiltinAgentHandler(options) {
1697
1773
  runtime,
1698
1774
  registry,
1699
1775
  typeNames,
1700
- skillsRegistry
1776
+ skillsRegistry,
1777
+ shutdownSandboxes
1701
1778
  };
1702
1779
  }
1703
1780
  async function createAgentHandler(agentServerUrl, workingDirectory, streamFn, createElectricTools, serveEndpoint) {
@@ -1718,18 +1795,22 @@ const registerAgentTypes = registerBuiltinAgentTypes;
1718
1795
  * Guard so repeated `buildBuiltinSandboxProfiles` calls in one process don't
1719
1796
  * re-run the boot sweep.
1720
1797
  */
1721
- let dockerSweptOnBoot = false;
1798
+ let dockerBootSweep = null;
1722
1799
  function sweepOrphanedDockerSandboxesOnce(sweep) {
1723
- if (dockerSweptOnBoot) return;
1724
- dockerSweptOnBoot = true;
1725
- sweep().then((removed) => {
1726
- if (removed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep removed ${removed.length} leftover container(s)`);
1800
+ dockerBootSweep ??= sweep().then((reclaimed) => {
1801
+ if (reclaimed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep reclaimed ${reclaimed.length} leftover container(s)`);
1727
1802
  }).catch((err) => serverLog.warn(`[builtin-agents] docker sandbox boot sweep error: ${err instanceof Error ? err.message : String(err)}`));
1803
+ return dockerBootSweep;
1728
1804
  }
1729
1805
  /**
1730
1806
  * Built-in sandbox profiles. `local` is always available. `docker` is
1731
1807
  * gated on Docker being reachable so a user without Docker installed
1732
1808
  * sees only what works — the UI never offers a non-functional choice.
1809
+ *
1810
+ * Also returns `shutdownSandboxes` when a host-local provider registered: an
1811
+ * immediate teardown of this process's live containers that the embedding
1812
+ * server must run on shutdown (the providers' debounced idle teardowns die
1813
+ * with the process).
1733
1814
  */
1734
1815
  async function buildBuiltinSandboxProfiles(workingDirectory) {
1735
1816
  const profiles = [{
@@ -1738,29 +1819,36 @@ async function buildBuiltinSandboxProfiles(workingDirectory) {
1738
1819
  description: `Runs on the host without isolation. Full filesystem access.`,
1739
1820
  factory: ({ args }) => chooseDefaultSandbox(resolveCwd(args, workingDirectory))
1740
1821
  }];
1822
+ let shutdownSandboxes = null;
1741
1823
  try {
1742
1824
  const { isDockerAvailable } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1743
1825
  if (await isDockerAvailable()) {
1744
- const { dockerSandbox, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1745
- sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
1826
+ const { dockerSandbox, reclaimDockerSandboxByKey, shutdownAllDockerSandboxes, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1827
+ await sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
1828
+ shutdownSandboxes = shutdownAllDockerSandboxes;
1746
1829
  profiles.push({
1747
1830
  name: `docker`,
1748
1831
  label: `Docker`,
1749
1832
  description: `Runs in a hardened Docker container: dropped capabilities, no privilege escalation, and CPU/memory/process limits. The chosen working directory is mounted read-write and, by default, network egress is unrestricted (allow-all).`,
1750
- factory: ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
1833
+ factory: async ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
1751
1834
  const cwd = readWorkingDirectoryArg(args);
1752
- return dockerSandbox({
1753
- initialNetworkPolicy: { mode: `allow-all` },
1754
- extraMounts: cwd ? [{
1755
- hostPath: cwd,
1756
- containerPath: `/work`,
1757
- readOnly: false
1758
- }] : void 0,
1759
- sandboxKey,
1760
- persistent,
1761
- owner,
1762
- entityType,
1763
- entityUrl
1835
+ return lazySandbox({
1836
+ name: `docker`,
1837
+ workingDirectory: `/work`,
1838
+ factory: () => dockerSandbox({
1839
+ initialNetworkPolicy: { mode: `allow-all` },
1840
+ extraMounts: cwd ? [{
1841
+ hostPath: cwd,
1842
+ containerPath: `/work`,
1843
+ readOnly: false
1844
+ }] : void 0,
1845
+ sandboxKey,
1846
+ persistent,
1847
+ owner,
1848
+ entityType,
1849
+ entityUrl
1850
+ }),
1851
+ reclaim: owner ? () => reclaimDockerSandboxByKey(sandboxKey) : void 0
1764
1852
  });
1765
1853
  }
1766
1854
  });
@@ -1784,7 +1872,10 @@ async function buildBuiltinSandboxProfiles(workingDirectory) {
1784
1872
  });
1785
1873
  else serverLog.info(`[builtin-agents] E2B_API_KEY set but the "e2b" package is not installed — e2b sandbox profile not registered`);
1786
1874
  console.log(`[builtin-agents] sandbox profiles advertised: ${profiles.map((p) => p.name).join(`, `)}`);
1787
- return profiles;
1875
+ return {
1876
+ profiles,
1877
+ shutdownSandboxes
1878
+ };
1788
1879
  }
1789
1880
  function readWorkingDirectoryArg(args) {
1790
1881
  const v = args.workingDirectory;
@@ -1803,7 +1894,7 @@ function installDurableStreamsFetchCache(options = {}) {
1803
1894
  location: options.sqliteLocation,
1804
1895
  maxCount: options.maxCount
1805
1896
  }) : new cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
1806
- setGlobalDispatcher(new Agent().compose(interceptors.cache({ store })));
1897
+ setGlobalDispatcher(getGlobalDispatcher().compose(interceptors.cache({ store })));
1807
1898
  }
1808
1899
 
1809
1900
  //#endregion
@@ -2002,6 +2093,9 @@ var BuiltinAgentsServer = class {
2002
2093
  await Promise.race([this.bootstrap.runtime.drainWakes().catch((err) => {
2003
2094
  serverLog.error(`[builtin-agents] drainWakes failed during shutdown:`, err);
2004
2095
  }), new Promise((resolve) => setTimeout(resolve, 5e3))]);
2096
+ if (this.bootstrap.shutdownSandboxes) await Promise.race([this.bootstrap.shutdownSandboxes().catch((err) => {
2097
+ serverLog.error(`[builtin-agents] sandbox shutdown failed during shutdown:`, err);
2098
+ }), new Promise((resolve) => setTimeout(resolve, 5e3))]);
2005
2099
  this.bootstrap = null;
2006
2100
  }
2007
2101
  this.mcpStopping = true;
@@ -2127,4 +2221,4 @@ async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd
2127
2221
  }
2128
2222
 
2129
2223
  //#endregion
2130
- export { BuiltinAgentsServer, DEFAULT_BUILTIN_AGENT_HANDLER_PATH, HORTON_MODEL, WORKER_TOOL_NAMES, braveSearchTool, buildHortonSystemPrompt, builtinModelProviderLabel, createAgentHandler, createBuiltinAgentHandler, createBuiltinElectricTools, createHortonDocsSupport, createHortonTools, createSpawnWorkerTool, generateTitle, listBuiltinModelChoices, registerAgentTypes, registerBuiltinAgentTypes, registerHorton, registerWorker, resolveBuiltinAgentsEntrypointOptions, runBuiltinAgentsEntrypoint };
2224
+ export { BuiltinAgentsServer, DEFAULT_BUILTIN_AGENT_HANDLER_PATH, HORTON_MODEL, WORKER_TOOL_NAMES, braveSearchTool, buildHortonSystemPrompt, builtinModelProviderLabel, createAgentHandler, createBuiltinAgentHandler, createBuiltinElectricTools, createForkTool, createHortonDocsSupport, createHortonTools, createSpawnWorkerTool, generateTitle, listBuiltinModelChoices, registerAgentTypes, registerBuiltinAgentTypes, registerHorton, registerWorker, resolveBuiltinAgentsEntrypointOptions, runBuiltinAgentsEntrypoint };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electric-ax/agents",
3
- "version": "0.4.14",
3
+ "version": "0.4.15",
4
4
  "description": "Built-in Electric Agents runtimes such as Horton and worker",
5
5
  "repository": {
6
6
  "type": "git",
@@ -50,7 +50,7 @@
50
50
  "undici": "^7.24.7",
51
51
  "zod": "^4.3.6",
52
52
  "@electric-ax/agents-mcp": "0.2.2",
53
- "@electric-ax/agents-runtime": "0.3.10"
53
+ "@electric-ax/agents-runtime": "0.3.11"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/better-sqlite3": "^7.6.13",