@electric-ax/agents 0.4.17 → 0.4.19

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.cjs CHANGED
@@ -820,7 +820,7 @@ function createSpawnWorkerTool(ctx, modelConfig) {
820
820
 
821
821
  //#endregion
822
822
  //#region src/tools/observe-pg-sync.ts
823
- function asToolResult(value) {
823
+ function asToolResult$1(value) {
824
824
  return {
825
825
  content: [{
826
826
  type: `text`,
@@ -838,9 +838,9 @@ function createObservePgSyncTool(ctx) {
838
838
  return {
839
839
  name: `observe_pg_sync`,
840
840
  label: `Observe Postgres Sync`,
841
- description: `Observe an Electric Postgres shape stream and wake this agent when matching row changes arrive.`,
841
+ description: `Observe an Electric Postgres shape stream and wake this agent when matching row changes arrive. Requires the HTTP(S) URL of an Electric shape endpoint — ask the user for it if you don't know it. Registration validates the endpoint up front and fails with Electric's error if the shape can't be fetched.`,
842
842
  parameters: __sinclair_typebox.Type.Object({
843
- url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String({ description: `Optional Electric shape endpoint URL. Defaults to the server-configured pg-sync URL.` })),
843
+ url: __sinclair_typebox.Type.String({ description: `HTTP(S) URL of the Electric shape endpoint, e.g. http://localhost:3000/v1/shape. Not a postgres:// connection string. Never guess this — ask the user if it hasn't been provided.` }),
844
844
  table: __sinclair_typebox.Type.String({
845
845
  minLength: 1,
846
846
  pattern: `\\S`,
@@ -857,6 +857,7 @@ function createObservePgSyncTool(ctx) {
857
857
  }),
858
858
  execute: async (_toolCallId, params) => {
859
859
  const args = params;
860
+ if (typeof args.url !== `string` || args.url.trim().length === 0) throw new Error(`url is required`);
860
861
  if (typeof args.table !== `string` || args.table.trim().length === 0) throw new Error(`table is required`);
861
862
  const source = (0, __electric_ax_agents_runtime.pgSync)({
862
863
  url: args.url,
@@ -871,16 +872,79 @@ function createObservePgSyncTool(ctx) {
871
872
  ...args.wake?.ops ? { ops: args.wake.ops } : {},
872
873
  ...args.wake?.debounceMs !== void 0 ? { debounceMs: args.wake.debounceMs } : {}
873
874
  };
874
- await ctx.observe(source, { wake });
875
- return asToolResult({
876
- sourceRef: source.sourceRef,
877
- streamUrl: source.streamUrl,
875
+ const handle = await ctx.observe(source, { wake });
876
+ if (!handle.streamUrl) throw new Error(`pg-sync observation did not return a stream URL for ${handle.sourceRef}`);
877
+ return asToolResult$1({
878
+ sourceRef: handle.sourceRef,
879
+ streamUrl: handle.streamUrl,
878
880
  wake
879
881
  });
880
882
  }
881
883
  };
882
884
  }
883
885
 
886
+ //#endregion
887
+ //#region src/tools/unobserve-pg-sync.ts
888
+ function asToolResult(value) {
889
+ return {
890
+ content: [{
891
+ type: `text`,
892
+ text: typeof value === `string` ? value : JSON.stringify(value, null, 2)
893
+ }],
894
+ details: {}
895
+ };
896
+ }
897
+ function isRecord$1(value) {
898
+ return typeof value === `object` && value !== null && !Array.isArray(value);
899
+ }
900
+ function listPgSyncObservations(ctx) {
901
+ const manifests = ctx.db.collections.manifests?.toArray;
902
+ if (!Array.isArray(manifests)) return [];
903
+ const observations = [];
904
+ for (const entry of manifests) {
905
+ if (!isRecord$1(entry) || entry.kind !== `source` || entry.sourceType !== `pgSync` || typeof entry.sourceRef !== `string`) continue;
906
+ const config = isRecord$1(entry.config) ? entry.config : {};
907
+ observations.push({
908
+ sourceRef: entry.sourceRef,
909
+ ...typeof config.table === `string` ? { table: config.table } : {},
910
+ ...typeof config.url === `string` ? { url: config.url } : {},
911
+ ...typeof entry.streamUrl === `string` ? { streamUrl: entry.streamUrl } : {}
912
+ });
913
+ }
914
+ return observations.sort((left, right) => left.sourceRef.localeCompare(right.sourceRef));
915
+ }
916
+ function createUnobservePgSyncTool(ctx) {
917
+ return {
918
+ name: `unobserve_pg_sync`,
919
+ label: `Stop Observing Postgres Sync`,
920
+ description: `Stop being woken by a Postgres shape stream you previously observed with observe_pg_sync. Identify the observation by its sourceRef (preferred) or table. Call with no arguments to list your active pg-sync observations. This only removes your own subscription; any other agents observing the same shape keep their stream.`,
921
+ parameters: __sinclair_typebox.Type.Object({
922
+ sourceRef: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String({ description: `The sourceRef returned by observe_pg_sync. Preferred — unambiguous.` })),
923
+ table: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String({ description: `The observed table name. Used only when sourceRef is not given; fails if more than one observation matches.` }))
924
+ }),
925
+ execute: async (_toolCallId, params) => {
926
+ const args = params;
927
+ const observations = listPgSyncObservations(ctx);
928
+ if (!args.sourceRef && !args.table) return asToolResult(observations.length > 0 ? { observations } : `You have no active pg-sync observations.`);
929
+ let sourceRef = args.sourceRef;
930
+ if (!sourceRef) {
931
+ const matches = observations.filter((o) => o.table === args.table);
932
+ if (matches.length === 0) return asToolResult(`No active pg-sync observation found for table "${args.table}".`);
933
+ if (matches.length > 1) return asToolResult({
934
+ error: `Multiple pg-sync observations match table "${args.table}"; pass a sourceRef instead.`,
935
+ matches
936
+ });
937
+ sourceRef = matches[0].sourceRef;
938
+ } else if (!observations.some((o) => o.sourceRef === sourceRef)) return asToolResult(`No active pg-sync observation found for sourceRef "${sourceRef}".`);
939
+ await ctx.unobserve(sourceRef);
940
+ return asToolResult({
941
+ unobserved: true,
942
+ sourceRef
943
+ });
944
+ }
945
+ };
946
+ }
947
+
884
948
  //#endregion
885
949
  //#region src/tools/fork.ts
886
950
  function createForkTool(ctx) {
@@ -1093,25 +1157,66 @@ function filterChoicesByEnabledModels(choices, values) {
1093
1157
  const filtered = choices.filter((choice) => enabled.has(choice.value));
1094
1158
  return filtered.length > 0 ? filtered : choices;
1095
1159
  }
1160
+ /**
1161
+ * Anthropic-specific budget mapping for `reasoningEffort`.
1162
+ *
1163
+ * Anthropic's `thinking.budget_tokens` is a hard cap on tokens spent
1164
+ * inside the thinking block before the model must commit to its
1165
+ * answer. Docs require ≥ 1024; we scale from there. Numbers tuned so
1166
+ * `medium` is the spot most "show your work" requests land, and
1167
+ * `high` covers tougher reasoning without uncapped spend.
1168
+ *
1169
+ * Keep in sync with provider doc updates — Anthropic has shifted the
1170
+ * minimum once already (older models capped lower).
1171
+ */
1172
+ const ANTHROPIC_THINKING_BUDGET_BY_EFFORT = {
1173
+ minimal: 1024,
1174
+ low: 2048,
1175
+ medium: 8192,
1176
+ high: 24576
1177
+ };
1096
1178
  function withProviderPayloadDefaults(config, choice, reasoningEffort) {
1097
- if (choice.provider !== `openai` && choice.provider !== `openai-codex` || !choice.reasoning) return config;
1098
- const defaultEffort = choice.provider === `openai-codex` ? `low` : `minimal`;
1099
- const effort = reasoningEffort === `minimal` && choice.provider === `openai-codex` ? `low` : reasoningEffort ?? defaultEffort;
1100
- return {
1101
- ...config,
1102
- onPayload: (payload) => {
1103
- if (typeof payload !== `object` || payload === null) return void 0;
1104
- const body = payload;
1105
- const existingReasoning = typeof body.reasoning === `object` && body.reasoning !== null ? body.reasoning : {};
1106
- return {
1107
- ...body,
1108
- reasoning: {
1109
- ...existingReasoning,
1110
- effort
1111
- }
1112
- };
1113
- }
1114
- };
1179
+ if (!choice.reasoning) return config;
1180
+ if (choice.provider === `openai` || choice.provider === `openai-codex`) {
1181
+ const defaultEffort = choice.provider === `openai-codex` ? `low` : `minimal`;
1182
+ const effort = reasoningEffort === `minimal` && choice.provider === `openai-codex` ? `low` : reasoningEffort ?? defaultEffort;
1183
+ return {
1184
+ ...config,
1185
+ onPayload: (payload) => {
1186
+ if (typeof payload !== `object` || payload === null) return void 0;
1187
+ const body = payload;
1188
+ const existingReasoning = typeof body.reasoning === `object` && body.reasoning !== null ? body.reasoning : {};
1189
+ return {
1190
+ ...body,
1191
+ reasoning: {
1192
+ ...existingReasoning,
1193
+ effort
1194
+ }
1195
+ };
1196
+ }
1197
+ };
1198
+ }
1199
+ if (choice.provider === `anthropic`) {
1200
+ const effectiveEffort = reasoningEffort ?? `minimal`;
1201
+ const budgetTokens = ANTHROPIC_THINKING_BUDGET_BY_EFFORT[effectiveEffort];
1202
+ return {
1203
+ ...config,
1204
+ onPayload: (payload) => {
1205
+ if (typeof payload !== `object` || payload === null) return void 0;
1206
+ const body = payload;
1207
+ const existingThinking = typeof body.thinking === `object` && body.thinking !== null ? body.thinking : {};
1208
+ return {
1209
+ ...body,
1210
+ thinking: {
1211
+ ...existingThinking,
1212
+ type: `enabled`,
1213
+ budget_tokens: budgetTokens
1214
+ }
1215
+ };
1216
+ }
1217
+ };
1218
+ }
1219
+ return config;
1115
1220
  }
1116
1221
  function parseReasoningEffort(value) {
1117
1222
  return value === `minimal` || value === `low` || value === `medium` || value === `high` ? value : null;
@@ -1272,7 +1377,7 @@ async function generateTitle(userMessage, llmCall, onFallback) {
1272
1377
  }
1273
1378
  function buildHortonSystemPrompt(workingDirectory, opts = {}) {
1274
1379
  const docsTools = opts.hasDocsSupport ? `\n- search_electric_agents_docs: hybrid search over the built-in Electric Agents docs index` : ``;
1275
- 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` : ``;
1380
+ const webhookSourceTools = opts.hasWebhookSourceTools ? `\n- list_webhook_sources: list external webhook feeds you can subscribe to, including available buckets and parameters\n- subscribe_webhook_source: subscribe yourself to one of those feeds or buckets so matching future webhooks wake you\n- list_webhook_source_subscriptions: list your active webhook source subscriptions\n- unsubscribe_webhook_source: remove one of your webhook source subscriptions by id` : ``;
1276
1381
  const titleTool = `\n- set_title: set or rename this chat session's UI title`;
1277
1382
  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` : ``;
1278
1383
  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` : ``;
@@ -1329,9 +1434,10 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
1329
1434
  - fetch_url: fetch and convert a URL to markdown
1330
1435
  - spawn_worker: dispatch a subagent for an isolated task
1331
1436
  - 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.
1332
- - observe_pg_sync: observe an Electric Postgres sync stream and wake on matching changes
1437
+ - observe_pg_sync: observe an Electric Postgres sync stream and wake on matching changes (see "Observing Postgres tables")
1438
+ - unobserve_pg_sync: stop being woken by a pg-sync stream you previously observed (see "Observing Postgres tables")
1333
1439
  - send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs.
1334
- ${eventSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1440
+ ${webhookSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1335
1441
 
1336
1442
  # Working with files
1337
1443
  - Prefer edit over write when modifying existing files.
@@ -1339,6 +1445,14 @@ ${eventSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1339
1445
  - Use absolute paths or paths relative to the current working directory.
1340
1446
  ${modelGuidance}${docsGuidance}${skillsGuidance}${onboardingGuidance}${docsUrlGuidance}
1341
1447
 
1448
+ # Observing Postgres tables
1449
+ observe_pg_sync subscribes you to row changes in a Postgres table via an Electric shape stream:
1450
+ - The \`url\` parameter is the HTTP(S) URL of an Electric shape endpoint (e.g. \`http://localhost:3000/v1/shape\`). It is NOT a \`postgres://\` connection string and there is no default — if the user hasn't given you the endpoint URL, ask for it. Never guess or invent one.
1451
+ - Registration validates the endpoint by fetching the shape log first. If it fails, the error includes Electric's response or the failure reason — use it to correct the table name, where clause, or URL, or relay it to the user.
1452
+ - Use \`where\` and \`columns\` to narrow the shape so you only wake on changes you care about; use \`wake.ops\` to filter by operation and \`wake.debounceMs\` to batch bursts.
1453
+ - The observation persists across wakes — register it once, don't re-register on every wake.
1454
+ - To stop, call unobserve_pg_sync with the sourceRef from observe_pg_sync (or the table name). Call it with no arguments to list your active observations. This only ends your own subscription.
1455
+
1342
1456
  # Risky actions
1343
1457
  Pause and confirm with the user before:
1344
1458
  - Destructive operations (deleting files, rm -rf, dropping data, force-pushing)
@@ -1376,7 +1490,18 @@ Workflow when forking yourself for parallel exploration:
1376
1490
  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.
1377
1491
 
1378
1492
  Working directory: ${workingDirectory}
1379
- The current year is ${new Date().getFullYear()}.`;
1493
+ The current year is ${new Date().getFullYear()}.${buildGoalGuidance(opts.activeGoal)}`;
1494
+ }
1495
+ function buildGoalGuidance(goal) {
1496
+ if (!goal) return ``;
1497
+ const budgetLine = goal.tokenBudget === null ? `unlimited` : `${goal.tokensUsed} / ${goal.tokenBudget} tokens used`;
1498
+ return `
1499
+
1500
+ # Active goal
1501
+ - Objective: ${goal.objective}
1502
+ - Token budget: ${budgetLine}
1503
+
1504
+ The user set this goal with /goal set. Work autonomously toward it: do NOT ask the user clarifying questions or pause for confirmation — make reasonable assumptions and proceed. When you believe the goal is met, call the \`mark_goal_complete\` tool. If you hit a blocker that genuinely requires the user (e.g. credentials, a destructive action), call \`mark_goal_complete\` with a summary explaining what's needed. The runtime will abort this run automatically if you exceed the token budget.`;
1380
1505
  }
1381
1506
  function getToolName(tool) {
1382
1507
  if (typeof tool !== `object` || tool === null) return null;
@@ -1399,8 +1524,10 @@ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1399
1524
  createSpawnWorkerTool(ctx, opts.modelConfig),
1400
1525
  createForkTool(ctx),
1401
1526
  createObservePgSyncTool(ctx),
1527
+ createUnobservePgSyncTool(ctx),
1402
1528
  createSetTitleTool(ctx),
1403
1529
  (0, __electric_ax_agents_runtime_tools.createSendTool)(ctx.send, { selfEntityUrl: ctx.entityUrl }),
1530
+ ...ctx.getGoal()?.status === `active` ? [(0, __electric_ax_agents_runtime_tools.createMarkGoalCompleteTool)(ctx)] : [],
1404
1531
  ...opts.docsSearchTool ? [opts.docsSearchTool] : []
1405
1532
  ];
1406
1533
  }
@@ -1469,11 +1596,58 @@ async function readAgentsMd(sandbox) {
1469
1596
  return null;
1470
1597
  }
1471
1598
  }
1599
+ function extractWakeText(wake) {
1600
+ if (wake.type !== `inbox`) return null;
1601
+ const payload = wake.payload;
1602
+ if (typeof payload === `string`) return payload;
1603
+ if (payload && typeof payload === `object`) {
1604
+ const record = payload;
1605
+ if (typeof record.text === `string`) return record.text;
1606
+ if (typeof record.source === `string`) return record.source;
1607
+ }
1608
+ return null;
1609
+ }
1610
+ async function tryHandleSlashCommand(ctx, wake) {
1611
+ const text = extractWakeText(wake);
1612
+ if (text === null) return false;
1613
+ if ((0, __electric_ax_agents_runtime.isGoalCommandText)(text)) {
1614
+ const command = (0, __electric_ax_agents_runtime.parseGoalCommand)(text);
1615
+ const result = (0, __electric_ax_agents_runtime.dispatchGoalCommand)(ctx, command);
1616
+ if (result.message) {
1617
+ serverLog.info(`[horton ${ctx.entityUrl}] ${result.message}`);
1618
+ writeSlashCommandReply(ctx, result.message);
1619
+ }
1620
+ if (command.kind === `set`) await kickoffGoalRun(ctx);
1621
+ return result.handled;
1622
+ }
1623
+ return false;
1624
+ }
1625
+ const GOAL_KICKOFF_TEXT = `Start working toward the active goal now. Call \`mark_goal_complete\` when you believe it is done.`;
1626
+ async function kickoffGoalRun(ctx) {
1627
+ const goal = ctx.getGoal();
1628
+ if (!goal || goal.status !== `active`) return;
1629
+ try {
1630
+ await ctx.send(ctx.entityUrl, {
1631
+ kind: `goal_kickoff`,
1632
+ text: GOAL_KICKOFF_TEXT
1633
+ }, { type: `inbox` });
1634
+ } catch (err) {
1635
+ serverLog.warn(`[horton ${ctx.entityUrl}] failed to enqueue goal kickoff: ${err instanceof Error ? err.message : String(err)}`);
1636
+ }
1637
+ }
1638
+ function writeSlashCommandReply(ctx, text) {
1639
+ try {
1640
+ ctx.replyText(text);
1641
+ } catch (err) {
1642
+ serverLog.warn(`[horton ${ctx.entityUrl}] failed to render slash command reply: ${err instanceof Error ? err.message : String(err)}`);
1643
+ }
1644
+ }
1472
1645
  function createAssistantHandler(options) {
1473
1646
  const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
1474
1647
  const skillLoader = (0, __electric_ax_agents_runtime.createContextSkillLoader)(skillsRegistry, { slashCommandOwner: HORTON_SKILLS_SLASH_COMMAND_OWNER });
1475
1648
  const hasSkills = skillLoader.hasSkills;
1476
1649
  return async function assistantHandler(ctx, wake) {
1650
+ if (await tryHandleSlashCommand(ctx, wake)) return;
1477
1651
  const loadedSkills = await skillLoader.load(ctx);
1478
1652
  const readSet = new Set();
1479
1653
  const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
@@ -1491,7 +1665,7 @@ function createAssistantHandler(options) {
1491
1665
  ...loadedSkills.tools,
1492
1666
  ...__electric_ax_agents_mcp.mcp.tools()
1493
1667
  ];
1494
- const hasEventSourceTools = tools.some((tool) => getToolName(tool) === `list_event_sources`);
1668
+ const hasWebhookSourceTools = tools.some((tool) => getToolName(tool) === `list_webhook_sources`);
1495
1669
  const hasScheduleTools = tools.some((tool) => getToolName(tool) === `upsert_cron_schedule`);
1496
1670
  const titlePromise = !ctx.tags.title ? (async () => {
1497
1671
  const firstUserMessage = await extractFirstUserMessage(ctx);
@@ -1566,6 +1740,26 @@ function createAssistantHandler(options) {
1566
1740
  }
1567
1741
  }
1568
1742
  });
1743
+ const goal = ctx.getGoal();
1744
+ const enforcedGoal = goal && goal.status === `active` ? goal : void 0;
1745
+ const activeGoalPromptInfo = enforcedGoal ? {
1746
+ objective: enforcedGoal.objective,
1747
+ tokenBudget: enforcedGoal.tokenBudget,
1748
+ tokensUsed: enforcedGoal.tokensUsed
1749
+ } : void 0;
1750
+ const budgetAbort = new AbortController();
1751
+ let runTokensUsed = enforcedGoal?.tokensUsed ?? 0;
1752
+ let budgetTripped = false;
1753
+ const onStepEnd = enforcedGoal ? (stats) => {
1754
+ if (budgetTripped) return;
1755
+ runTokensUsed += stats.uncachedInput + stats.output;
1756
+ ctx.updateGoalUsage(runTokensUsed);
1757
+ if (enforcedGoal.tokenBudget !== null && runTokensUsed >= enforcedGoal.tokenBudget) {
1758
+ budgetTripped = true;
1759
+ serverLog.info(`[horton ${ctx.entityUrl}] goal budget exhausted (${runTokensUsed} tokens) — aborting run`);
1760
+ budgetAbort.abort();
1761
+ }
1762
+ } : void 0;
1569
1763
  ctx.useAgent({
1570
1764
  systemPrompt: buildHortonSystemPrompt(sandboxCwd, {
1571
1765
  hasDocsSupport: Boolean(docsSupport),
@@ -1573,14 +1767,27 @@ function createAssistantHandler(options) {
1573
1767
  docsUrl,
1574
1768
  modelProvider: modelConfig.provider,
1575
1769
  modelId: String(modelConfig.model),
1576
- hasEventSourceTools,
1577
- hasScheduleTools
1770
+ hasWebhookSourceTools,
1771
+ hasScheduleTools,
1772
+ ...activeGoalPromptInfo && { activeGoal: activeGoalPromptInfo }
1578
1773
  }),
1579
1774
  ...modelConfig,
1580
1775
  tools,
1581
- ...streamFn && { streamFn }
1776
+ ...streamFn && { streamFn },
1777
+ ...onStepEnd && { onStepEnd }
1582
1778
  });
1583
- await ctx.agent.run();
1779
+ try {
1780
+ await ctx.agent.run(void 0, budgetAbort.signal);
1781
+ } catch (err) {
1782
+ if (!budgetTripped) throw err;
1783
+ serverLog.info(`[horton ${ctx.entityUrl}] agent.run aborted by budget enforcement`);
1784
+ }
1785
+ if (enforcedGoal) ctx.updateGoalUsage(runTokensUsed, budgetTripped ? { status: `budget_limited` } : void 0);
1786
+ if (budgetTripped && enforcedGoal && enforcedGoal.tokenBudget !== null) {
1787
+ const budget = enforcedGoal.tokenBudget;
1788
+ const suggestedNext = Math.max(budget * 2, budget + 1e4);
1789
+ writeSlashCommandReply(ctx, `⚠️ Stopped — goal hit the token budget (${(0, __electric_ax_agents_runtime.formatTokenCount)(runTokensUsed)} / ${(0, __electric_ax_agents_runtime.formatTokenCount)(budget)} tokens used). Raise the budget with \`/goal set "..." --tokens ${(0, __electric_ax_agents_runtime.formatTokenCount)(suggestedNext)}\`, or call \`/goal complete\` to finalize.`);
1790
+ }
1584
1791
  await titlePromise;
1585
1792
  };
1586
1793
  }
@@ -1620,7 +1827,8 @@ function registerHorton(registry, options) {
1620
1827
  subject_value: `user`,
1621
1828
  permission: `manage`
1622
1829
  }],
1623
- slashCommands: (0, __electric_ax_agents_runtime.buildSkillSlashCommands)(skillsRegistry),
1830
+ state: { comments: __electric_ax_agents_runtime.commentsCollection },
1831
+ slashCommands: [__electric_ax_agents_runtime.GOAL_SLASH_COMMAND, ...(0, __electric_ax_agents_runtime.buildSkillSlashCommands)(skillsRegistry)],
1624
1832
  handler: assistantHandler
1625
1833
  });
1626
1834
  return [`horton`];
@@ -1804,6 +2012,7 @@ function registerWorker(registry, options) {
1804
2012
  subject_value: `user`,
1805
2013
  permission: `manage`
1806
2014
  }],
2015
+ state: { comments: __electric_ax_agents_runtime.commentsCollection },
1807
2016
  async handler(ctx) {
1808
2017
  const args = parseWorkerArgs(ctx.args);
1809
2018
  const readSet = new Set();
@@ -1847,7 +2056,7 @@ function dedupeToolsByName(tools) {
1847
2056
  }
1848
2057
  function createBuiltinElectricTools(custom) {
1849
2058
  return async (context) => {
1850
- const builtinTools = [...(0, __electric_ax_agents_runtime_tools.createEventSourceTools)(context), ...(0, __electric_ax_agents_runtime_tools.createScheduleTools)({
2059
+ const builtinTools = [...(0, __electric_ax_agents_runtime_tools.createWebhookSourceTools)(context), ...(0, __electric_ax_agents_runtime_tools.createScheduleTools)({
1851
2060
  ...context,
1852
2061
  db: context.db
1853
2062
  })];
@@ -1856,7 +2065,7 @@ function createBuiltinElectricTools(custom) {
1856
2065
  };
1857
2066
  }
1858
2067
  async function createBuiltinAgentHandler(options) {
1859
- const { agentServerUrl, serveEndpoint, workingDirectory, streamFn, enabledModelValues, createElectricTools, publicUrl, runtimeName, baseSkillsDir: baseSkillsDirOverride, serverHeaders, defaultDispatchPolicyForType } = options;
2068
+ const { agentServerUrl, serveEndpoint, workingDirectory, streamFn, enabledModelValues, createElectricTools, publicUrl, runtimeName, baseSkillsDir: baseSkillsDirOverride, serverHeaders, defaultDispatchPolicyForType, dockerSandbox: dockerSandboxOpts } = options;
1860
2069
  const modelCatalog = await createBuiltinModelCatalog({
1861
2070
  allowMockFallback: Boolean(streamFn),
1862
2071
  enabledModelValues
@@ -1892,7 +2101,7 @@ async function createBuiltinAgentHandler(options) {
1892
2101
  modelCatalog
1893
2102
  });
1894
2103
  typeNames.push(`worker`);
1895
- const { profiles: sandboxProfiles, shutdownSandboxes } = await buildBuiltinSandboxProfiles(cwd);
2104
+ const { profiles: sandboxProfiles, shutdownSandboxes } = await buildBuiltinSandboxProfiles(cwd, dockerSandboxOpts);
1896
2105
  const runtime = (0, __electric_ax_agents_runtime.createRuntimeHandler)({
1897
2106
  baseUrl: agentServerUrl,
1898
2107
  serveEndpoint,
@@ -1912,7 +2121,8 @@ async function createBuiltinAgentHandler(options) {
1912
2121
  registry,
1913
2122
  typeNames,
1914
2123
  skillsRegistry,
1915
- shutdownSandboxes
2124
+ shutdownSandboxes,
2125
+ modelCatalog
1916
2126
  };
1917
2127
  }
1918
2128
  async function createAgentHandler(agentServerUrl, workingDirectory, streamFn, createElectricTools, serveEndpoint) {
@@ -1941,6 +2151,21 @@ function sweepOrphanedDockerSandboxesOnce(sweep) {
1941
2151
  return dockerBootSweep;
1942
2152
  }
1943
2153
  /**
2154
+ * Merge the profile's working-directory mount with embedder docker options
2155
+ * into the option fragment spread into `dockerSandbox()`. An internal helper:
2156
+ * exported from this module so the unit test can import it, but intentionally
2157
+ * not re-exported from `index.ts` (not part of the package's public API).
2158
+ */
2159
+ function resolveDockerSandboxOpts(cwdMount, custom) {
2160
+ const extraMounts = [...cwdMount ? [cwdMount] : [], ...custom?.extraMounts ?? []];
2161
+ return {
2162
+ ...custom?.image !== void 0 && { image: custom.image },
2163
+ ...custom?.allowFloatingTag !== void 0 && { allowFloatingTag: custom.allowFloatingTag },
2164
+ ...custom?.env !== void 0 && { env: custom.env },
2165
+ ...extraMounts.length > 0 && { extraMounts }
2166
+ };
2167
+ }
2168
+ /**
1944
2169
  * Built-in sandbox profiles. `local` is always available. `docker` is
1945
2170
  * gated on Docker being reachable so a user without Docker installed
1946
2171
  * sees only what works — the UI never offers a non-functional choice.
@@ -1950,7 +2175,7 @@ function sweepOrphanedDockerSandboxesOnce(sweep) {
1950
2175
  * server must run on shutdown (the providers' debounced idle teardowns die
1951
2176
  * with the process).
1952
2177
  */
1953
- async function buildBuiltinSandboxProfiles(workingDirectory) {
2178
+ async function buildBuiltinSandboxProfiles(workingDirectory, dockerOpts) {
1954
2179
  const profiles = [{
1955
2180
  name: `local`,
1956
2181
  label: `Local`,
@@ -1975,11 +2200,11 @@ async function buildBuiltinSandboxProfiles(workingDirectory) {
1975
2200
  workingDirectory: `/work`,
1976
2201
  factory: () => dockerSandbox({
1977
2202
  initialNetworkPolicy: { mode: `allow-all` },
1978
- extraMounts: cwd ? [{
2203
+ ...resolveDockerSandboxOpts(cwd ? {
1979
2204
  hostPath: cwd,
1980
2205
  containerPath: `/work`,
1981
2206
  readOnly: false
1982
- }] : void 0,
2207
+ } : void 0, dockerOpts),
1983
2208
  sandboxKey,
1984
2209
  persistent,
1985
2210
  owner,
@@ -2391,4 +2616,5 @@ exports.registerBuiltinAgentTypes = registerBuiltinAgentTypes
2391
2616
  exports.registerHorton = registerHorton
2392
2617
  exports.registerWorker = registerWorker
2393
2618
  exports.resolveBuiltinAgentsEntrypointOptions = resolveBuiltinAgentsEntrypointOptions
2619
+ exports.resolveBuiltinModelConfig = resolveBuiltinModelConfig
2394
2620
  exports.runBuiltinAgentsEntrypoint = runBuiltinAgentsEntrypoint
package/dist/index.d.cts CHANGED
@@ -6,6 +6,36 @@ import { Sandbox } from "@electric-ax/agents-runtime/sandbox";
6
6
  import { ChangeEvent } from "@durable-streams/state";
7
7
  import { braveSearchTool } from "@electric-ax/agents-runtime/tools";
8
8
 
9
+ //#region src/model-catalog.d.ts
10
+ type BuiltinModelProvider = AvailableProvider;
11
+ type BuiltinModelInput = `text` | `image`;
12
+ interface BuiltinModelChoice {
13
+ provider: BuiltinModelProvider;
14
+ id: string;
15
+ label: string;
16
+ value: string;
17
+ reasoning: boolean;
18
+ input: Array<BuiltinModelInput>;
19
+ }
20
+ interface BuiltinModelCatalog {
21
+ choices: Array<BuiltinModelChoice>;
22
+ defaultChoice: BuiltinModelChoice;
23
+ }
24
+ interface BuiltinModelCatalogOptions {
25
+ allowMockFallback?: boolean;
26
+ enabledModelValues?: ReadonlyArray<string> | null;
27
+ }
28
+ declare const REASONING_EFFORT_VALUES: readonly ["auto", "minimal", "low", "medium", "high"];
29
+ type BuiltinReasoningEffort = (typeof REASONING_EFFORT_VALUES)[number];
30
+ type ExplicitReasoningEffort = Exclude<BuiltinReasoningEffort, `auto`>;
31
+ type BuiltinAgentModelConfig = Pick<AgentConfig, `model` | `provider` | `onPayload` | `getApiKey`> & {
32
+ reasoningEffort?: ExplicitReasoningEffort;
33
+ };
34
+ declare function builtinModelProviderLabel(provider: BuiltinModelProvider): string;
35
+ declare function listBuiltinModelChoices(providers: ReadonlyArray<BuiltinModelProvider>): Array<BuiltinModelChoice>;
36
+ declare function resolveBuiltinModelConfig(catalog: BuiltinModelCatalog, args: Readonly<Record<string, unknown>>): BuiltinAgentModelConfig;
37
+
38
+ //#endregion
9
39
  //#region src/bootstrap.d.ts
10
40
  declare const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = "/_electric/builtin-agent-handler";
11
41
  interface AgentHandlerResult {
@@ -21,8 +51,38 @@ interface AgentHandlerResult {
21
51
  * die with the process, which would leave containers running.
22
52
  */
23
53
  shutdownSandboxes: (() => Promise<void>) | null;
54
+ /**
55
+ * Model catalog the built-in agents resolve `model` args against — lets
56
+ * embedders register sibling agent types with the same model resolution.
57
+ */
58
+ modelCatalog: BuiltinModelCatalog;
24
59
  }
25
60
  type BuiltinElectricToolsFactory = NonNullable<ProcessWakeConfig[`createElectricTools`]>;
61
+ /** Mount spec mirroring `DockerSandboxOpts['extraMounts']` items. */
62
+ interface BuiltinDockerSandboxMount {
63
+ hostPath: string;
64
+ containerPath: string;
65
+ readOnly?: boolean;
66
+ }
67
+ /**
68
+ * Embedder customization for the built-in `docker` sandbox profile.
69
+ * Threads straight into `dockerSandbox()` (which already supports these);
70
+ * custom `extraMounts` are appended after the working-directory mount.
71
+ * These are embedder/operator-trust inputs: `extraMounts` is subject to the
72
+ * runtime's docker-socket guard, and `env` is passed verbatim into the
73
+ * container.
74
+ *
75
+ * Note: custom `extraMounts` must not target the working-directory container
76
+ * path (`/work`) — it collides with the cwd mount and fails at container-create
77
+ * time with an opaque docker error.
78
+ */
79
+ interface BuiltinDockerSandboxOptions {
80
+ /** Digest-pinned image unless `allowFloatingTag` is set. */
81
+ image?: string;
82
+ allowFloatingTag?: boolean;
83
+ env?: Record<string, string>;
84
+ extraMounts?: Array<BuiltinDockerSandboxMount>;
85
+ }
26
86
  interface BuiltinAgentHandlerOptions {
27
87
  agentServerUrl: string;
28
88
  serveEndpoint?: string;
@@ -36,6 +96,8 @@ interface BuiltinAgentHandlerOptions {
36
96
  serverHeaders?: HeadersProvider;
37
97
  defaultDispatchPolicyForType?: (typeName: string) => DispatchPolicy | undefined;
38
98
  createElectricTools?: BuiltinElectricToolsFactory;
99
+ /** Customize the built-in `docker` sandbox profile (image, env, mounts). */
100
+ dockerSandbox?: BuiltinDockerSandboxOptions;
39
101
  }
40
102
  declare function createBuiltinElectricTools(custom?: BuiltinElectricToolsFactory): BuiltinElectricToolsFactory;
41
103
  declare function createBuiltinAgentHandler(options: BuiltinAgentHandlerOptions): Promise<AgentHandlerResult | null>;
@@ -45,6 +107,12 @@ declare const registerAgentTypes: typeof registerBuiltinAgentTypes;
45
107
 
46
108
  //#endregion
47
109
  //#region src/durable-streams-cache.d.ts
110
+ /**
111
+ * Merge the profile's working-directory mount with embedder docker options
112
+ * into the option fragment spread into `dockerSandbox()`. An internal helper:
113
+ * exported from this module so the unit test can import it, but intentionally
114
+ * not re-exported from `index.ts` (not part of the package's public API).
115
+ */
48
116
  type DurableStreamsFetchCacheOptions = false | {
49
117
  store?: `memory` | `sqlite`;
50
118
  sqliteLocation?: string;
@@ -160,48 +228,24 @@ declare function runBuiltinAgentsEntrypoint({
160
228
  url: string;
161
229
  }>;
162
230
 
163
- //#endregion
164
- //#region src/model-catalog.d.ts
165
- type BuiltinModelProvider = AvailableProvider;
166
- type BuiltinModelInput = `text` | `image`;
167
- interface BuiltinModelChoice {
168
- provider: BuiltinModelProvider;
169
- id: string;
170
- label: string;
171
- value: string;
172
- reasoning: boolean;
173
- input: Array<BuiltinModelInput>;
174
- }
175
- interface BuiltinModelCatalog {
176
- choices: Array<BuiltinModelChoice>;
177
- defaultChoice: BuiltinModelChoice;
178
- }
179
- interface BuiltinModelCatalogOptions {
180
- allowMockFallback?: boolean;
181
- enabledModelValues?: ReadonlyArray<string> | null;
182
- }
183
- declare const REASONING_EFFORT_VALUES: readonly ["auto", "minimal", "low", "medium", "high"];
184
- type BuiltinReasoningEffort = (typeof REASONING_EFFORT_VALUES)[number];
185
- type ExplicitReasoningEffort = Exclude<BuiltinReasoningEffort, `auto`>;
186
- type BuiltinAgentModelConfig = Pick<AgentConfig, `model` | `provider` | `onPayload` | `getApiKey`> & {
187
- reasoningEffort?: ExplicitReasoningEffort;
188
- };
189
- declare function builtinModelProviderLabel(provider: BuiltinModelProvider): string;
190
- declare function listBuiltinModelChoices(providers: ReadonlyArray<BuiltinModelProvider>): Array<BuiltinModelChoice>;
191
- declare function resolveBuiltinModelConfig(catalog: BuiltinModelCatalog, args: Readonly<Record<string, unknown>>): BuiltinAgentModelConfig;
192
-
193
231
  //#endregion
194
232
  //#region src/agents/horton.d.ts
195
233
  declare const HORTON_MODEL = "claude-sonnet-4-6";
196
234
  declare function generateTitle(userMessage: string, llmCall: (prompt: string) => Promise<string>, onFallback?: (reason: string) => void): Promise<string>;
235
+ interface ActiveGoalPromptInfo {
236
+ objective: string;
237
+ tokenBudget: number | null;
238
+ tokensUsed: number;
239
+ }
197
240
  declare function buildHortonSystemPrompt(workingDirectory: string, opts?: {
198
241
  hasDocsSupport?: boolean;
199
- hasEventSourceTools?: boolean;
242
+ hasWebhookSourceTools?: boolean;
200
243
  hasScheduleTools?: boolean;
201
244
  hasSkills?: boolean;
202
245
  docsUrl?: string;
203
246
  modelProvider?: string;
204
247
  modelId?: string;
248
+ activeGoal?: ActiveGoalPromptInfo;
205
249
  }): string;
206
250
  declare function createHortonTools(sandbox: Sandbox, ctx: HandlerContext, readSet: Set<string>, opts?: {
207
251
  docsSearchTool?: AgentTool$1;
@@ -254,4 +298,4 @@ declare function createHortonDocsSupport(workingDirectory: string, opts?: {
254
298
  }): HortonDocsSupport | null;
255
299
 
256
300
  //#endregion
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 };
301
+ export { AgentHandlerResult, BuiltinAgentHandlerOptions, BuiltinAgentModelConfig, BuiltinAgentsEntrypointOptions, BuiltinAgentsEntrypointServer, BuiltinAgentsServer, BuiltinAgentsServerOptions, BuiltinDockerSandboxMount, BuiltinDockerSandboxOptions, BuiltinElectricToolsFactory, BuiltinModelCatalog, 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, resolveBuiltinModelConfig, runBuiltinAgentsEntrypoint };