@electric-ax/agents 0.4.18 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/entrypoint.js +88 -14
  2. package/dist/index.cjs +87 -13
  3. package/dist/index.d.cts +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +88 -14
  6. package/docs/entities/agents/horton.md +22 -17
  7. package/docs/entities/agents/worker.md +13 -6
  8. package/docs/entities/patterns/blackboard.md +1 -1
  9. package/docs/entities/patterns/dispatcher.md +1 -1
  10. package/docs/entities/patterns/manager-worker.md +10 -5
  11. package/docs/entities/patterns/map-reduce.md +1 -1
  12. package/docs/entities/patterns/pipeline.md +1 -1
  13. package/docs/entities/patterns/reactive-observers.md +1 -1
  14. package/docs/index.md +6 -4
  15. package/docs/quickstart.md +2 -2
  16. package/docs/reference/agent-config.md +13 -3
  17. package/docs/reference/built-in-collections.md +128 -9
  18. package/docs/reference/cli.md +34 -4
  19. package/docs/reference/entity-definition.md +39 -7
  20. package/docs/reference/entity-handle.md +19 -1
  21. package/docs/reference/handler-context.md +130 -5
  22. package/docs/reference/runtime-handler.md +42 -14
  23. package/docs/reference/wake-event.md +29 -1
  24. package/docs/usage/app-setup.md +38 -7
  25. package/docs/usage/attachments.md +129 -0
  26. package/docs/usage/clients-and-react.md +23 -2
  27. package/docs/usage/configuring-the-agent.md +15 -5
  28. package/docs/usage/context-composition.md +2 -1
  29. package/docs/usage/defining-entities.md +9 -5
  30. package/docs/usage/defining-tools.md +1 -1
  31. package/docs/usage/embedded-builtins.md +82 -31
  32. package/docs/usage/managing-state.md +5 -0
  33. package/docs/usage/mcp-servers.md +16 -8
  34. package/docs/usage/overview.md +39 -14
  35. package/docs/usage/permissions-and-principals.md +160 -0
  36. package/docs/usage/programmatic-runtime-client.md +158 -16
  37. package/docs/usage/sandboxing.md +162 -0
  38. package/docs/usage/signals.md +138 -0
  39. package/docs/usage/spawning-and-coordinating.md +30 -11
  40. package/docs/usage/testing.md +1 -1
  41. package/docs/usage/waking-entities.md +34 -6
  42. package/docs/usage/webhook-sources.md +171 -0
  43. package/docs/usage/writing-handlers.md +13 -55
  44. package/docs/walkthrough.md +13 -5
  45. package/package.json +3 -3
@@ -5,7 +5,7 @@ import fs from "node:fs";
5
5
  import pino from "pino";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { GOAL_SLASH_COMMAND, MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, buildSkillSlashCommands, commentsCollection, completeWithLowCostModel, createContextSkillLoader, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillsRegistry, db, detectAvailableProviders, dispatchGoalCommand, formatTokenCount, getMoonshotApiKey, getMoonshotModel, getMoonshotModels, isGoalCommandText, parseGoalCommand, pgSync, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
8
- import { braveSearchTool, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createMarkGoalCompleteTool, createReadFileTool, createScheduleTools, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
8
+ import { braveSearchTool, createBashTool, createEditTool, createFetchUrlTool, createMarkGoalCompleteTool, createReadFileTool, createScheduleTools, createSendTool, createWebhookSourceTools, createWriteTool } from "@electric-ax/agents-runtime/tools";
9
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";
@@ -814,7 +814,7 @@ function createSpawnWorkerTool(ctx, modelConfig) {
814
814
 
815
815
  //#endregion
816
816
  //#region src/tools/observe-pg-sync.ts
817
- function asToolResult(value) {
817
+ function asToolResult$1(value) {
818
818
  return {
819
819
  content: [{
820
820
  type: `text`,
@@ -832,9 +832,9 @@ function createObservePgSyncTool(ctx) {
832
832
  return {
833
833
  name: `observe_pg_sync`,
834
834
  label: `Observe Postgres Sync`,
835
- description: `Observe an Electric Postgres shape stream and wake this agent when matching row changes arrive.`,
835
+ 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.`,
836
836
  parameters: Type.Object({
837
- url: Type.Optional(Type.String({ description: `Optional Electric shape endpoint URL. Defaults to the server-configured pg-sync URL.` })),
837
+ url: 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.` }),
838
838
  table: Type.String({
839
839
  minLength: 1,
840
840
  pattern: `\\S`,
@@ -851,6 +851,7 @@ function createObservePgSyncTool(ctx) {
851
851
  }),
852
852
  execute: async (_toolCallId, params) => {
853
853
  const args = params;
854
+ if (typeof args.url !== `string` || args.url.trim().length === 0) throw new Error(`url is required`);
854
855
  if (typeof args.table !== `string` || args.table.trim().length === 0) throw new Error(`table is required`);
855
856
  const source = pgSync({
856
857
  url: args.url,
@@ -865,16 +866,79 @@ function createObservePgSyncTool(ctx) {
865
866
  ...args.wake?.ops ? { ops: args.wake.ops } : {},
866
867
  ...args.wake?.debounceMs !== void 0 ? { debounceMs: args.wake.debounceMs } : {}
867
868
  };
868
- await ctx.observe(source, { wake });
869
- return asToolResult({
870
- sourceRef: source.sourceRef,
871
- streamUrl: source.streamUrl,
869
+ const handle = await ctx.observe(source, { wake });
870
+ if (!handle.streamUrl) throw new Error(`pg-sync observation did not return a stream URL for ${handle.sourceRef}`);
871
+ return asToolResult$1({
872
+ sourceRef: handle.sourceRef,
873
+ streamUrl: handle.streamUrl,
872
874
  wake
873
875
  });
874
876
  }
875
877
  };
876
878
  }
877
879
 
880
+ //#endregion
881
+ //#region src/tools/unobserve-pg-sync.ts
882
+ function asToolResult(value) {
883
+ return {
884
+ content: [{
885
+ type: `text`,
886
+ text: typeof value === `string` ? value : JSON.stringify(value, null, 2)
887
+ }],
888
+ details: {}
889
+ };
890
+ }
891
+ function isRecord$1(value) {
892
+ return typeof value === `object` && value !== null && !Array.isArray(value);
893
+ }
894
+ function listPgSyncObservations(ctx) {
895
+ const manifests = ctx.db.collections.manifests?.toArray;
896
+ if (!Array.isArray(manifests)) return [];
897
+ const observations = [];
898
+ for (const entry of manifests) {
899
+ if (!isRecord$1(entry) || entry.kind !== `source` || entry.sourceType !== `pgSync` || typeof entry.sourceRef !== `string`) continue;
900
+ const config = isRecord$1(entry.config) ? entry.config : {};
901
+ observations.push({
902
+ sourceRef: entry.sourceRef,
903
+ ...typeof config.table === `string` ? { table: config.table } : {},
904
+ ...typeof config.url === `string` ? { url: config.url } : {},
905
+ ...typeof entry.streamUrl === `string` ? { streamUrl: entry.streamUrl } : {}
906
+ });
907
+ }
908
+ return observations.sort((left, right) => left.sourceRef.localeCompare(right.sourceRef));
909
+ }
910
+ function createUnobservePgSyncTool(ctx) {
911
+ return {
912
+ name: `unobserve_pg_sync`,
913
+ label: `Stop Observing Postgres Sync`,
914
+ 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.`,
915
+ parameters: Type.Object({
916
+ sourceRef: Type.Optional(Type.String({ description: `The sourceRef returned by observe_pg_sync. Preferred — unambiguous.` })),
917
+ table: Type.Optional(Type.String({ description: `The observed table name. Used only when sourceRef is not given; fails if more than one observation matches.` }))
918
+ }),
919
+ execute: async (_toolCallId, params) => {
920
+ const args = params;
921
+ const observations = listPgSyncObservations(ctx);
922
+ if (!args.sourceRef && !args.table) return asToolResult(observations.length > 0 ? { observations } : `You have no active pg-sync observations.`);
923
+ let sourceRef = args.sourceRef;
924
+ if (!sourceRef) {
925
+ const matches = observations.filter((o) => o.table === args.table);
926
+ if (matches.length === 0) return asToolResult(`No active pg-sync observation found for table "${args.table}".`);
927
+ if (matches.length > 1) return asToolResult({
928
+ error: `Multiple pg-sync observations match table "${args.table}"; pass a sourceRef instead.`,
929
+ matches
930
+ });
931
+ sourceRef = matches[0].sourceRef;
932
+ } else if (!observations.some((o) => o.sourceRef === sourceRef)) return asToolResult(`No active pg-sync observation found for sourceRef "${sourceRef}".`);
933
+ await ctx.unobserve(sourceRef);
934
+ return asToolResult({
935
+ unobserved: true,
936
+ sourceRef
937
+ });
938
+ }
939
+ };
940
+ }
941
+
878
942
  //#endregion
879
943
  //#region src/tools/fork.ts
880
944
  function createForkTool(ctx) {
@@ -1306,7 +1370,7 @@ async function generateTitle(userMessage, llmCall, onFallback) {
1306
1370
  }
1307
1371
  function buildHortonSystemPrompt(workingDirectory, opts = {}) {
1308
1372
  const docsTools = opts.hasDocsSupport ? `\n- search_electric_agents_docs: hybrid search over the built-in Electric Agents docs index` : ``;
1309
- 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` : ``;
1373
+ 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` : ``;
1310
1374
  const titleTool = `\n- set_title: set or rename this chat session's UI title`;
1311
1375
  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` : ``;
1312
1376
  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` : ``;
@@ -1363,9 +1427,10 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
1363
1427
  - fetch_url: fetch and convert a URL to markdown
1364
1428
  - spawn_worker: dispatch a subagent for an isolated task
1365
1429
  - 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.
1366
- - observe_pg_sync: observe an Electric Postgres sync stream and wake on matching changes
1430
+ - observe_pg_sync: observe an Electric Postgres sync stream and wake on matching changes (see "Observing Postgres tables")
1431
+ - unobserve_pg_sync: stop being woken by a pg-sync stream you previously observed (see "Observing Postgres tables")
1367
1432
  - send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs.
1368
- ${eventSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1433
+ ${webhookSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1369
1434
 
1370
1435
  # Working with files
1371
1436
  - Prefer edit over write when modifying existing files.
@@ -1373,6 +1438,14 @@ ${eventSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1373
1438
  - Use absolute paths or paths relative to the current working directory.
1374
1439
  ${modelGuidance}${docsGuidance}${skillsGuidance}${onboardingGuidance}${docsUrlGuidance}
1375
1440
 
1441
+ # Observing Postgres tables
1442
+ observe_pg_sync subscribes you to row changes in a Postgres table via an Electric shape stream:
1443
+ - 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.
1444
+ - 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.
1445
+ - 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.
1446
+ - The observation persists across wakes — register it once, don't re-register on every wake.
1447
+ - 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.
1448
+
1376
1449
  # Risky actions
1377
1450
  Pause and confirm with the user before:
1378
1451
  - Destructive operations (deleting files, rm -rf, dropping data, force-pushing)
@@ -1444,6 +1517,7 @@ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1444
1517
  createSpawnWorkerTool(ctx, opts.modelConfig),
1445
1518
  createForkTool(ctx),
1446
1519
  createObservePgSyncTool(ctx),
1520
+ createUnobservePgSyncTool(ctx),
1447
1521
  createSetTitleTool(ctx),
1448
1522
  createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
1449
1523
  ...ctx.getGoal()?.status === `active` ? [createMarkGoalCompleteTool(ctx)] : [],
@@ -1584,7 +1658,7 @@ function createAssistantHandler(options) {
1584
1658
  ...loadedSkills.tools,
1585
1659
  ...mcp.tools()
1586
1660
  ];
1587
- const hasEventSourceTools = tools.some((tool) => getToolName(tool) === `list_event_sources`);
1661
+ const hasWebhookSourceTools = tools.some((tool) => getToolName(tool) === `list_webhook_sources`);
1588
1662
  const hasScheduleTools = tools.some((tool) => getToolName(tool) === `upsert_cron_schedule`);
1589
1663
  const titlePromise = !ctx.tags.title ? (async () => {
1590
1664
  const firstUserMessage = await extractFirstUserMessage(ctx);
@@ -1686,7 +1760,7 @@ function createAssistantHandler(options) {
1686
1760
  docsUrl,
1687
1761
  modelProvider: modelConfig.provider,
1688
1762
  modelId: String(modelConfig.model),
1689
- hasEventSourceTools,
1763
+ hasWebhookSourceTools,
1690
1764
  hasScheduleTools,
1691
1765
  ...activeGoalPromptInfo && { activeGoal: activeGoalPromptInfo }
1692
1766
  }),
@@ -1974,7 +2048,7 @@ function dedupeToolsByName(tools) {
1974
2048
  }
1975
2049
  function createBuiltinElectricTools(custom) {
1976
2050
  return async (context) => {
1977
- const builtinTools = [...createEventSourceTools(context), ...createScheduleTools({
2051
+ const builtinTools = [...createWebhookSourceTools(context), ...createScheduleTools({
1978
2052
  ...context,
1979
2053
  db: context.db
1980
2054
  })];
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) {
@@ -1313,7 +1377,7 @@ async function generateTitle(userMessage, llmCall, onFallback) {
1313
1377
  }
1314
1378
  function buildHortonSystemPrompt(workingDirectory, opts = {}) {
1315
1379
  const docsTools = opts.hasDocsSupport ? `\n- search_electric_agents_docs: hybrid search over the built-in Electric Agents docs index` : ``;
1316
- 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` : ``;
1317
1381
  const titleTool = `\n- set_title: set or rename this chat session's UI title`;
1318
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` : ``;
1319
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` : ``;
@@ -1370,9 +1434,10 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
1370
1434
  - fetch_url: fetch and convert a URL to markdown
1371
1435
  - spawn_worker: dispatch a subagent for an isolated task
1372
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.
1373
- - 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")
1374
1439
  - send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs.
1375
- ${eventSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1440
+ ${webhookSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1376
1441
 
1377
1442
  # Working with files
1378
1443
  - Prefer edit over write when modifying existing files.
@@ -1380,6 +1445,14 @@ ${eventSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1380
1445
  - Use absolute paths or paths relative to the current working directory.
1381
1446
  ${modelGuidance}${docsGuidance}${skillsGuidance}${onboardingGuidance}${docsUrlGuidance}
1382
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
+
1383
1456
  # Risky actions
1384
1457
  Pause and confirm with the user before:
1385
1458
  - Destructive operations (deleting files, rm -rf, dropping data, force-pushing)
@@ -1451,6 +1524,7 @@ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1451
1524
  createSpawnWorkerTool(ctx, opts.modelConfig),
1452
1525
  createForkTool(ctx),
1453
1526
  createObservePgSyncTool(ctx),
1527
+ createUnobservePgSyncTool(ctx),
1454
1528
  createSetTitleTool(ctx),
1455
1529
  (0, __electric_ax_agents_runtime_tools.createSendTool)(ctx.send, { selfEntityUrl: ctx.entityUrl }),
1456
1530
  ...ctx.getGoal()?.status === `active` ? [(0, __electric_ax_agents_runtime_tools.createMarkGoalCompleteTool)(ctx)] : [],
@@ -1591,7 +1665,7 @@ function createAssistantHandler(options) {
1591
1665
  ...loadedSkills.tools,
1592
1666
  ...__electric_ax_agents_mcp.mcp.tools()
1593
1667
  ];
1594
- const hasEventSourceTools = tools.some((tool) => getToolName(tool) === `list_event_sources`);
1668
+ const hasWebhookSourceTools = tools.some((tool) => getToolName(tool) === `list_webhook_sources`);
1595
1669
  const hasScheduleTools = tools.some((tool) => getToolName(tool) === `upsert_cron_schedule`);
1596
1670
  const titlePromise = !ctx.tags.title ? (async () => {
1597
1671
  const firstUserMessage = await extractFirstUserMessage(ctx);
@@ -1693,7 +1767,7 @@ function createAssistantHandler(options) {
1693
1767
  docsUrl,
1694
1768
  modelProvider: modelConfig.provider,
1695
1769
  modelId: String(modelConfig.model),
1696
- hasEventSourceTools,
1770
+ hasWebhookSourceTools,
1697
1771
  hasScheduleTools,
1698
1772
  ...activeGoalPromptInfo && { activeGoal: activeGoalPromptInfo }
1699
1773
  }),
@@ -1982,7 +2056,7 @@ function dedupeToolsByName(tools) {
1982
2056
  }
1983
2057
  function createBuiltinElectricTools(custom) {
1984
2058
  return async (context) => {
1985
- 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)({
1986
2060
  ...context,
1987
2061
  db: context.db
1988
2062
  })];
package/dist/index.d.cts CHANGED
@@ -239,7 +239,7 @@ interface ActiveGoalPromptInfo {
239
239
  }
240
240
  declare function buildHortonSystemPrompt(workingDirectory: string, opts?: {
241
241
  hasDocsSupport?: boolean;
242
- hasEventSourceTools?: boolean;
242
+ hasWebhookSourceTools?: boolean;
243
243
  hasScheduleTools?: boolean;
244
244
  hasSkills?: boolean;
245
245
  docsUrl?: string;
package/dist/index.d.ts CHANGED
@@ -239,7 +239,7 @@ interface ActiveGoalPromptInfo {
239
239
  }
240
240
  declare function buildHortonSystemPrompt(workingDirectory: string, opts?: {
241
241
  hasDocsSupport?: boolean;
242
- hasEventSourceTools?: boolean;
242
+ hasWebhookSourceTools?: boolean;
243
243
  hasScheduleTools?: boolean;
244
244
  hasSkills?: boolean;
245
245
  docsUrl?: string;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { mergeElectricPrincipalHeader } from "./server-headers-KD5yHFYT.js";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { GOAL_SLASH_COMMAND, MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, buildSkillSlashCommands, commentsCollection, completeWithLowCostModel, createContextSkillLoader, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillsRegistry, db, detectAvailableProviders, dispatchGoalCommand, formatTokenCount, getMoonshotApiKey, getMoonshotModel, getMoonshotModels, isGoalCommandText, parseGoalCommand, pgSync, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
5
- import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createMarkGoalCompleteTool, createReadFileTool, createScheduleTools, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
5
+ import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createFetchUrlTool, createMarkGoalCompleteTool, createReadFileTool, createScheduleTools, createSendTool, createWebhookSourceTools, createWriteTool } from "@electric-ax/agents-runtime/tools";
6
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";
@@ -796,7 +796,7 @@ function createSpawnWorkerTool(ctx, modelConfig) {
796
796
 
797
797
  //#endregion
798
798
  //#region src/tools/observe-pg-sync.ts
799
- function asToolResult(value) {
799
+ function asToolResult$1(value) {
800
800
  return {
801
801
  content: [{
802
802
  type: `text`,
@@ -814,9 +814,9 @@ function createObservePgSyncTool(ctx) {
814
814
  return {
815
815
  name: `observe_pg_sync`,
816
816
  label: `Observe Postgres Sync`,
817
- description: `Observe an Electric Postgres shape stream and wake this agent when matching row changes arrive.`,
817
+ 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.`,
818
818
  parameters: Type.Object({
819
- url: Type.Optional(Type.String({ description: `Optional Electric shape endpoint URL. Defaults to the server-configured pg-sync URL.` })),
819
+ url: 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.` }),
820
820
  table: Type.String({
821
821
  minLength: 1,
822
822
  pattern: `\\S`,
@@ -833,6 +833,7 @@ function createObservePgSyncTool(ctx) {
833
833
  }),
834
834
  execute: async (_toolCallId, params) => {
835
835
  const args = params;
836
+ if (typeof args.url !== `string` || args.url.trim().length === 0) throw new Error(`url is required`);
836
837
  if (typeof args.table !== `string` || args.table.trim().length === 0) throw new Error(`table is required`);
837
838
  const source = pgSync({
838
839
  url: args.url,
@@ -847,16 +848,79 @@ function createObservePgSyncTool(ctx) {
847
848
  ...args.wake?.ops ? { ops: args.wake.ops } : {},
848
849
  ...args.wake?.debounceMs !== void 0 ? { debounceMs: args.wake.debounceMs } : {}
849
850
  };
850
- await ctx.observe(source, { wake });
851
- return asToolResult({
852
- sourceRef: source.sourceRef,
853
- streamUrl: source.streamUrl,
851
+ const handle = await ctx.observe(source, { wake });
852
+ if (!handle.streamUrl) throw new Error(`pg-sync observation did not return a stream URL for ${handle.sourceRef}`);
853
+ return asToolResult$1({
854
+ sourceRef: handle.sourceRef,
855
+ streamUrl: handle.streamUrl,
854
856
  wake
855
857
  });
856
858
  }
857
859
  };
858
860
  }
859
861
 
862
+ //#endregion
863
+ //#region src/tools/unobserve-pg-sync.ts
864
+ function asToolResult(value) {
865
+ return {
866
+ content: [{
867
+ type: `text`,
868
+ text: typeof value === `string` ? value : JSON.stringify(value, null, 2)
869
+ }],
870
+ details: {}
871
+ };
872
+ }
873
+ function isRecord$1(value) {
874
+ return typeof value === `object` && value !== null && !Array.isArray(value);
875
+ }
876
+ function listPgSyncObservations(ctx) {
877
+ const manifests = ctx.db.collections.manifests?.toArray;
878
+ if (!Array.isArray(manifests)) return [];
879
+ const observations = [];
880
+ for (const entry of manifests) {
881
+ if (!isRecord$1(entry) || entry.kind !== `source` || entry.sourceType !== `pgSync` || typeof entry.sourceRef !== `string`) continue;
882
+ const config = isRecord$1(entry.config) ? entry.config : {};
883
+ observations.push({
884
+ sourceRef: entry.sourceRef,
885
+ ...typeof config.table === `string` ? { table: config.table } : {},
886
+ ...typeof config.url === `string` ? { url: config.url } : {},
887
+ ...typeof entry.streamUrl === `string` ? { streamUrl: entry.streamUrl } : {}
888
+ });
889
+ }
890
+ return observations.sort((left, right) => left.sourceRef.localeCompare(right.sourceRef));
891
+ }
892
+ function createUnobservePgSyncTool(ctx) {
893
+ return {
894
+ name: `unobserve_pg_sync`,
895
+ label: `Stop Observing Postgres Sync`,
896
+ 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.`,
897
+ parameters: Type.Object({
898
+ sourceRef: Type.Optional(Type.String({ description: `The sourceRef returned by observe_pg_sync. Preferred — unambiguous.` })),
899
+ table: Type.Optional(Type.String({ description: `The observed table name. Used only when sourceRef is not given; fails if more than one observation matches.` }))
900
+ }),
901
+ execute: async (_toolCallId, params) => {
902
+ const args = params;
903
+ const observations = listPgSyncObservations(ctx);
904
+ if (!args.sourceRef && !args.table) return asToolResult(observations.length > 0 ? { observations } : `You have no active pg-sync observations.`);
905
+ let sourceRef = args.sourceRef;
906
+ if (!sourceRef) {
907
+ const matches = observations.filter((o) => o.table === args.table);
908
+ if (matches.length === 0) return asToolResult(`No active pg-sync observation found for table "${args.table}".`);
909
+ if (matches.length > 1) return asToolResult({
910
+ error: `Multiple pg-sync observations match table "${args.table}"; pass a sourceRef instead.`,
911
+ matches
912
+ });
913
+ sourceRef = matches[0].sourceRef;
914
+ } else if (!observations.some((o) => o.sourceRef === sourceRef)) return asToolResult(`No active pg-sync observation found for sourceRef "${sourceRef}".`);
915
+ await ctx.unobserve(sourceRef);
916
+ return asToolResult({
917
+ unobserved: true,
918
+ sourceRef
919
+ });
920
+ }
921
+ };
922
+ }
923
+
860
924
  //#endregion
861
925
  //#region src/tools/fork.ts
862
926
  function createForkTool(ctx) {
@@ -1289,7 +1353,7 @@ async function generateTitle(userMessage, llmCall, onFallback) {
1289
1353
  }
1290
1354
  function buildHortonSystemPrompt(workingDirectory, opts = {}) {
1291
1355
  const docsTools = opts.hasDocsSupport ? `\n- search_electric_agents_docs: hybrid search over the built-in Electric Agents docs index` : ``;
1292
- 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` : ``;
1356
+ 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` : ``;
1293
1357
  const titleTool = `\n- set_title: set or rename this chat session's UI title`;
1294
1358
  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` : ``;
1295
1359
  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` : ``;
@@ -1346,9 +1410,10 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
1346
1410
  - fetch_url: fetch and convert a URL to markdown
1347
1411
  - spawn_worker: dispatch a subagent for an isolated task
1348
1412
  - 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.
1349
- - observe_pg_sync: observe an Electric Postgres sync stream and wake on matching changes
1413
+ - observe_pg_sync: observe an Electric Postgres sync stream and wake on matching changes (see "Observing Postgres tables")
1414
+ - unobserve_pg_sync: stop being woken by a pg-sync stream you previously observed (see "Observing Postgres tables")
1350
1415
  - send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs.
1351
- ${eventSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1416
+ ${webhookSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1352
1417
 
1353
1418
  # Working with files
1354
1419
  - Prefer edit over write when modifying existing files.
@@ -1356,6 +1421,14 @@ ${eventSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
1356
1421
  - Use absolute paths or paths relative to the current working directory.
1357
1422
  ${modelGuidance}${docsGuidance}${skillsGuidance}${onboardingGuidance}${docsUrlGuidance}
1358
1423
 
1424
+ # Observing Postgres tables
1425
+ observe_pg_sync subscribes you to row changes in a Postgres table via an Electric shape stream:
1426
+ - 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.
1427
+ - 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.
1428
+ - 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.
1429
+ - The observation persists across wakes — register it once, don't re-register on every wake.
1430
+ - 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.
1431
+
1359
1432
  # Risky actions
1360
1433
  Pause and confirm with the user before:
1361
1434
  - Destructive operations (deleting files, rm -rf, dropping data, force-pushing)
@@ -1427,6 +1500,7 @@ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1427
1500
  createSpawnWorkerTool(ctx, opts.modelConfig),
1428
1501
  createForkTool(ctx),
1429
1502
  createObservePgSyncTool(ctx),
1503
+ createUnobservePgSyncTool(ctx),
1430
1504
  createSetTitleTool(ctx),
1431
1505
  createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
1432
1506
  ...ctx.getGoal()?.status === `active` ? [createMarkGoalCompleteTool(ctx)] : [],
@@ -1567,7 +1641,7 @@ function createAssistantHandler(options) {
1567
1641
  ...loadedSkills.tools,
1568
1642
  ...mcp.tools()
1569
1643
  ];
1570
- const hasEventSourceTools = tools.some((tool) => getToolName(tool) === `list_event_sources`);
1644
+ const hasWebhookSourceTools = tools.some((tool) => getToolName(tool) === `list_webhook_sources`);
1571
1645
  const hasScheduleTools = tools.some((tool) => getToolName(tool) === `upsert_cron_schedule`);
1572
1646
  const titlePromise = !ctx.tags.title ? (async () => {
1573
1647
  const firstUserMessage = await extractFirstUserMessage(ctx);
@@ -1669,7 +1743,7 @@ function createAssistantHandler(options) {
1669
1743
  docsUrl,
1670
1744
  modelProvider: modelConfig.provider,
1671
1745
  modelId: String(modelConfig.model),
1672
- hasEventSourceTools,
1746
+ hasWebhookSourceTools,
1673
1747
  hasScheduleTools,
1674
1748
  ...activeGoalPromptInfo && { activeGoal: activeGoalPromptInfo }
1675
1749
  }),
@@ -1958,7 +2032,7 @@ function dedupeToolsByName(tools) {
1958
2032
  }
1959
2033
  function createBuiltinElectricTools(custom) {
1960
2034
  return async (context) => {
1961
- const builtinTools = [...createEventSourceTools(context), ...createScheduleTools({
2035
+ const builtinTools = [...createWebhookSourceTools(context), ...createScheduleTools({
1962
2036
  ...context,
1963
2037
  db: context.db
1964
2038
  })];