@electric-ax/agents 0.4.12 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
+ import { Agent, cacheStores, interceptors, setGlobalDispatcher } from "undici";
3
4
  import fs from "node:fs";
4
5
  import pino from "pino";
5
6
  import { fileURLToPath } from "node:url";
6
- import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillTools, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
7
+ import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillTools, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModel, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
7
8
  import { braveSearchTool, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
8
9
  import { chooseDefaultSandbox, isE2BAvailable, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
9
10
  import { z } from "zod";
@@ -16,6 +17,18 @@ import { nanoid } from "nanoid";
16
17
  import { getModels } from "@mariozechner/pi-ai";
17
18
  import { bridgeMcpTool, buildPromptTools, buildResourceTools, createRegistry, keychainPersistence, loadConfig, mcp, watchConfig } from "@electric-ax/agents-mcp";
18
19
 
20
+ //#region src/durable-streams-cache.ts
21
+ const MEMORY_CACHE_SIZE_BYTES = 100 * 1024 * 1024;
22
+ function installDurableStreamsFetchCache(options = {}) {
23
+ if (options === false) return;
24
+ const store = options.store === `sqlite` || options.sqliteLocation ? new cacheStores.SqliteCacheStore({
25
+ location: options.sqliteLocation,
26
+ maxCount: options.maxCount
27
+ }) : new cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
28
+ setGlobalDispatcher(new Agent().compose(interceptors.cache({ store })));
29
+ }
30
+
31
+ //#endregion
19
32
  //#region src/log.ts
20
33
  const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
21
34
  const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
@@ -865,6 +878,15 @@ async function fetchAvailableModelIds(provider) {
865
878
  function knownModelsForProvider(provider) {
866
879
  return provider === MOONSHOT_PROVIDER ? getMoonshotModels() : getModels(provider);
867
880
  }
881
+ function resolveBuiltinModelContextWindow(modelConfig) {
882
+ const modelId = String(modelConfig.model);
883
+ if (modelConfig.provider === MOONSHOT_PROVIDER) return getMoonshotModel(modelId)?.contextWindow ?? null;
884
+ if (!modelConfig.provider) return null;
885
+ return knownModelsForProvider(modelConfig.provider).find((model) => model.id === modelId)?.contextWindow ?? null;
886
+ }
887
+ function resolveBuiltinModelSourceBudget(modelConfig) {
888
+ return resolveBuiltinModelContextWindow(modelConfig) ?? 1e5;
889
+ }
868
890
  function choiceForKnownModel(provider, model) {
869
891
  return {
870
892
  provider,
@@ -1238,6 +1260,7 @@ function createAssistantHandler(options) {
1238
1260
  return async function assistantHandler(ctx, wake) {
1239
1261
  const readSet = new Set();
1240
1262
  const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
1263
+ const sourceBudget = resolveBuiltinModelSourceBudget(modelConfig);
1241
1264
  const sandboxCwd = ctx.sandbox.workingDirectory;
1242
1265
  const agentsMd = await readAgentsMd(ctx.sandbox);
1243
1266
  const tools = [
@@ -1271,7 +1294,7 @@ function createAssistantHandler(options) {
1271
1294
  }
1272
1295
  })() : Promise.resolve();
1273
1296
  if (docsSupport) ctx.useContext({
1274
- sourceBudget: 1e5,
1297
+ sourceBudget,
1275
1298
  sources: {
1276
1299
  docs_toc: {
1277
1300
  content: () => docsSupport.renderCompressedToc(),
@@ -1300,7 +1323,7 @@ function createAssistantHandler(options) {
1300
1323
  }
1301
1324
  });
1302
1325
  else if (skillsRegistry && skillsRegistry.catalog.size > 0) ctx.useContext({
1303
- sourceBudget: 1e5,
1326
+ sourceBudget,
1304
1327
  sources: {
1305
1328
  skills_catalog: {
1306
1329
  content: () => skillsRegistry.renderCatalog(2e3),
@@ -1319,7 +1342,7 @@ function createAssistantHandler(options) {
1319
1342
  }
1320
1343
  });
1321
1344
  else if (agentsMd) ctx.useContext({
1322
- sourceBudget: 1e5,
1345
+ sourceBudget,
1323
1346
  sources: {
1324
1347
  conversation: {
1325
1348
  content: () => ctx.timelineMessages(),
@@ -1376,6 +1399,15 @@ function registerHorton(registry, options) {
1376
1399
  registry.define(`horton`, {
1377
1400
  description: `Friendly capable assistant — chat, code, research, dispatch`,
1378
1401
  creationSchema: hortonCreationSchema,
1402
+ permissionGrants: [{
1403
+ subject_kind: `principal_kind`,
1404
+ subject_value: `user`,
1405
+ permission: `spawn`
1406
+ }, {
1407
+ subject_kind: `principal_kind`,
1408
+ subject_value: `user`,
1409
+ permission: `manage`
1410
+ }],
1379
1411
  handler: assistantHandler
1380
1412
  });
1381
1413
  return [`horton`];
@@ -1417,7 +1449,7 @@ function parseWorkerArgs(value) {
1417
1449
  if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
1418
1450
  return args;
1419
1451
  }
1420
- function buildToolsForWorker(tools, sandbox, ctx, readSet) {
1452
+ function buildToolsForWorker(tools, sandbox, ctx, readSet, opts) {
1421
1453
  const out = [];
1422
1454
  for (const name of tools) switch (name) {
1423
1455
  case `bash`:
@@ -1436,7 +1468,10 @@ function buildToolsForWorker(tools, sandbox, ctx, readSet) {
1436
1468
  out.push(braveSearchTool);
1437
1469
  break;
1438
1470
  case `fetch_url`:
1439
- out.push(createFetchUrlTool(sandbox));
1471
+ out.push(createFetchUrlTool(sandbox, {
1472
+ catalog: opts.modelCatalog,
1473
+ modelConfig: opts.modelConfig
1474
+ }));
1440
1475
  break;
1441
1476
  case `spawn_worker`:
1442
1477
  out.push(createSpawnWorkerTool(ctx));
@@ -1547,11 +1582,23 @@ function registerWorker(registry, options) {
1547
1582
  const { streamFn, modelCatalog } = options;
1548
1583
  registry.define(`worker`, {
1549
1584
  description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
1585
+ permissionGrants: [{
1586
+ subject_kind: `principal_kind`,
1587
+ subject_value: `user`,
1588
+ permission: `spawn`
1589
+ }, {
1590
+ subject_kind: `principal_kind`,
1591
+ subject_value: `user`,
1592
+ permission: `manage`
1593
+ }],
1550
1594
  async handler(ctx) {
1551
1595
  const args = parseWorkerArgs(ctx.args);
1552
1596
  const readSet = new Set();
1553
- const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
1554
1597
  const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
1598
+ const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet, {
1599
+ modelCatalog,
1600
+ modelConfig
1601
+ });
1555
1602
  const sharedStateTools = [];
1556
1603
  if (args.sharedDb) {
1557
1604
  const shared = await ctx.observe(db(args.sharedDb.id, args.sharedDb.schema));
@@ -1743,6 +1790,8 @@ var BuiltinAgentsServer = class {
1743
1790
  mcpToolProviderName = null;
1744
1791
  mcpApplyInFlight = new Set();
1745
1792
  mcpStopping = false;
1793
+ mcpExtras = [];
1794
+ mcpLastJsonConfig = null;
1746
1795
  pullWakeRunner = null;
1747
1796
  options;
1748
1797
  constructor(options) {
@@ -1752,8 +1801,70 @@ var BuiltinAgentsServer = class {
1752
1801
  get mcpRegistry() {
1753
1802
  return this._mcpRegistry;
1754
1803
  }
1804
+ /**
1805
+ * Replace the in-memory `extras` list and re-apply the merged config
1806
+ * against the last-known workspace `mcp.json` state. Workspace
1807
+ * `mcp.json` still wins on name collision. No-op once `stop()` has
1808
+ * latched `mcpStopping`.
1809
+ */
1810
+ async setExtraMcpServers(extras) {
1811
+ if (!this._mcpRegistry || this.mcpStopping) return;
1812
+ this.mcpExtras = extras;
1813
+ await this.applyMerged(this.mcpLastJsonConfig);
1814
+ }
1815
+ async wirePersistence(cfg) {
1816
+ const servers = [];
1817
+ for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
1818
+ const persist = await keychainPersistence({ server: s.name });
1819
+ servers.push({
1820
+ ...s,
1821
+ auth: {
1822
+ ...s.auth,
1823
+ ...persist
1824
+ }
1825
+ });
1826
+ } else servers.push(s);
1827
+ return {
1828
+ ...cfg,
1829
+ servers
1830
+ };
1831
+ }
1832
+ mergeMcp(jsonCfg) {
1833
+ const jsonServers = jsonCfg?.servers ?? [];
1834
+ const jsonNames = new Set(jsonServers.map((s) => s.name));
1835
+ const filteredExtras = this.mcpExtras.filter((s) => !jsonNames.has(s.name));
1836
+ return {
1837
+ servers: [...filteredExtras, ...jsonServers],
1838
+ raw: jsonCfg?.raw
1839
+ };
1840
+ }
1841
+ async runApply(jsonCfg) {
1842
+ if (this.mcpStopping) return;
1843
+ const registry = this._mcpRegistry;
1844
+ if (!registry) return;
1845
+ try {
1846
+ const wired = await this.wirePersistence(this.mergeMcp(jsonCfg));
1847
+ if (this.mcpStopping) return;
1848
+ await registry.applyConfig(wired);
1849
+ } catch (e) {
1850
+ serverLog.error(`[mcp] applyConfig:`, e);
1851
+ try {
1852
+ this.options.onConfigError?.(e);
1853
+ } catch (cbErr) {
1854
+ serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
1855
+ }
1856
+ }
1857
+ }
1858
+ applyMerged(jsonCfg) {
1859
+ this.mcpLastJsonConfig = jsonCfg;
1860
+ const p = this.runApply(jsonCfg);
1861
+ this.mcpApplyInFlight.add(p);
1862
+ p.finally(() => this.mcpApplyInFlight.delete(p));
1863
+ return p;
1864
+ }
1755
1865
  async start() {
1756
1866
  if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
1867
+ installDurableStreamsFetchCache(this.options.durableStreamsFetchCache);
1757
1868
  const pullWake = this.options.pullWake;
1758
1869
  if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
1759
1870
  try {
@@ -1764,76 +1875,28 @@ var BuiltinAgentsServer = class {
1764
1875
  });
1765
1876
  this._mcpRegistry = mcpRegistry;
1766
1877
  const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
1767
- const extras = this.options.extraMcpServers ?? [];
1768
- const wirePersistence = async (cfg) => {
1769
- const servers = [];
1770
- for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
1771
- const persist = await keychainPersistence({ server: s.name });
1772
- servers.push({
1773
- ...s,
1774
- auth: {
1775
- ...s.auth,
1776
- ...persist
1777
- }
1778
- });
1779
- } else servers.push(s);
1780
- return {
1781
- ...cfg,
1782
- servers
1783
- };
1784
- };
1785
- const merge = (jsonCfg) => {
1786
- const jsonServers = jsonCfg?.servers ?? [];
1787
- const jsonNames = new Set(jsonServers.map((s) => s.name));
1788
- const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
1789
- return {
1790
- servers: [...filteredExtras, ...jsonServers],
1791
- raw: jsonCfg?.raw
1792
- };
1793
- };
1794
- const onConfigError = this.options.onConfigError;
1795
- const runApply = async (jsonCfg) => {
1796
- if (this.mcpStopping) return;
1797
- try {
1798
- const wired = await wirePersistence(merge(jsonCfg));
1799
- if (this.mcpStopping) return;
1800
- await mcpRegistry.applyConfig(wired);
1801
- } catch (e) {
1802
- serverLog.error(`[mcp] applyConfig:`, e);
1803
- try {
1804
- onConfigError?.(e);
1805
- } catch (cbErr) {
1806
- serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
1807
- }
1808
- }
1809
- };
1810
- const applyMerged = (jsonCfg) => {
1811
- const p = runApply(jsonCfg);
1812
- this.mcpApplyInFlight.add(p);
1813
- p.finally(() => this.mcpApplyInFlight.delete(p));
1814
- return p;
1815
- };
1878
+ this.mcpExtras = this.options.extraMcpServers ?? [];
1816
1879
  if (mcpConfigPath) {
1817
1880
  try {
1818
1881
  const cfg = await loadConfig(mcpConfigPath, process.env);
1819
- applyMerged(cfg);
1882
+ this.applyMerged(cfg);
1820
1883
  } catch (err) {
1821
1884
  if (err.code !== `ENOENT`) throw err;
1822
- if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
1823
- else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
1824
- applyMerged(null);
1885
+ if (this.mcpExtras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
1886
+ else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${this.mcpExtras.length} server(s) from extras`);
1887
+ this.applyMerged(null);
1825
1888
  }
1826
1889
  try {
1827
1890
  this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
1828
- onChange: (cfg) => void applyMerged(cfg),
1891
+ onChange: (cfg) => void this.applyMerged(cfg),
1829
1892
  onError: (e) => serverLog.error(`[mcp] config error:`, e)
1830
1893
  });
1831
1894
  } catch (e) {
1832
1895
  serverLog.error(`[mcp] config watcher failed to start:`, e);
1833
1896
  }
1834
1897
  } else {
1835
- if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
1836
- applyMerged(null);
1898
+ if (this.mcpExtras.length > 0) serverLog.info(`[mcp] starting with ${this.mcpExtras.length} server(s) from extras`);
1899
+ this.applyMerged(null);
1837
1900
  }
1838
1901
  this.mcpToolProviderName = `mcp`;
1839
1902
  registerToolProvider({
package/dist/index.cjs CHANGED
@@ -39,6 +39,7 @@ const sqlite_vec = __toESM(require("sqlite-vec"));
39
39
  const nanoid = __toESM(require("nanoid"));
40
40
  const __mariozechner_pi_ai = __toESM(require("@mariozechner/pi-ai"));
41
41
  const __electric_ax_agents_mcp = __toESM(require("@electric-ax/agents-mcp"));
42
+ const undici = __toESM(require("undici"));
42
43
 
43
44
  //#region src/log.ts
44
45
  const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
@@ -889,6 +890,15 @@ async function fetchAvailableModelIds(provider) {
889
890
  function knownModelsForProvider(provider) {
890
891
  return provider === __electric_ax_agents_runtime.MOONSHOT_PROVIDER ? (0, __electric_ax_agents_runtime.getMoonshotModels)() : (0, __mariozechner_pi_ai.getModels)(provider);
891
892
  }
893
+ function resolveBuiltinModelContextWindow(modelConfig) {
894
+ const modelId = String(modelConfig.model);
895
+ if (modelConfig.provider === __electric_ax_agents_runtime.MOONSHOT_PROVIDER) return (0, __electric_ax_agents_runtime.getMoonshotModel)(modelId)?.contextWindow ?? null;
896
+ if (!modelConfig.provider) return null;
897
+ return knownModelsForProvider(modelConfig.provider).find((model) => model.id === modelId)?.contextWindow ?? null;
898
+ }
899
+ function resolveBuiltinModelSourceBudget(modelConfig) {
900
+ return resolveBuiltinModelContextWindow(modelConfig) ?? 1e5;
901
+ }
892
902
  function choiceForKnownModel(provider, model) {
893
903
  return {
894
904
  provider,
@@ -1263,6 +1273,7 @@ function createAssistantHandler(options) {
1263
1273
  return async function assistantHandler(ctx, wake) {
1264
1274
  const readSet = new Set();
1265
1275
  const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
1276
+ const sourceBudget = resolveBuiltinModelSourceBudget(modelConfig);
1266
1277
  const sandboxCwd = ctx.sandbox.workingDirectory;
1267
1278
  const agentsMd = await readAgentsMd(ctx.sandbox);
1268
1279
  const tools = [
@@ -1296,7 +1307,7 @@ function createAssistantHandler(options) {
1296
1307
  }
1297
1308
  })() : Promise.resolve();
1298
1309
  if (docsSupport) ctx.useContext({
1299
- sourceBudget: 1e5,
1310
+ sourceBudget,
1300
1311
  sources: {
1301
1312
  docs_toc: {
1302
1313
  content: () => docsSupport.renderCompressedToc(),
@@ -1325,7 +1336,7 @@ function createAssistantHandler(options) {
1325
1336
  }
1326
1337
  });
1327
1338
  else if (skillsRegistry && skillsRegistry.catalog.size > 0) ctx.useContext({
1328
- sourceBudget: 1e5,
1339
+ sourceBudget,
1329
1340
  sources: {
1330
1341
  skills_catalog: {
1331
1342
  content: () => skillsRegistry.renderCatalog(2e3),
@@ -1344,7 +1355,7 @@ function createAssistantHandler(options) {
1344
1355
  }
1345
1356
  });
1346
1357
  else if (agentsMd) ctx.useContext({
1347
- sourceBudget: 1e5,
1358
+ sourceBudget,
1348
1359
  sources: {
1349
1360
  conversation: {
1350
1361
  content: () => ctx.timelineMessages(),
@@ -1401,6 +1412,15 @@ function registerHorton(registry, options) {
1401
1412
  registry.define(`horton`, {
1402
1413
  description: `Friendly capable assistant — chat, code, research, dispatch`,
1403
1414
  creationSchema: hortonCreationSchema,
1415
+ permissionGrants: [{
1416
+ subject_kind: `principal_kind`,
1417
+ subject_value: `user`,
1418
+ permission: `spawn`
1419
+ }, {
1420
+ subject_kind: `principal_kind`,
1421
+ subject_value: `user`,
1422
+ permission: `manage`
1423
+ }],
1404
1424
  handler: assistantHandler
1405
1425
  });
1406
1426
  return [`horton`];
@@ -1442,7 +1462,7 @@ function parseWorkerArgs(value) {
1442
1462
  if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
1443
1463
  return args;
1444
1464
  }
1445
- function buildToolsForWorker(tools, sandbox, ctx, readSet) {
1465
+ function buildToolsForWorker(tools, sandbox, ctx, readSet, opts) {
1446
1466
  const out = [];
1447
1467
  for (const name of tools) switch (name) {
1448
1468
  case `bash`:
@@ -1461,7 +1481,10 @@ function buildToolsForWorker(tools, sandbox, ctx, readSet) {
1461
1481
  out.push(__electric_ax_agents_runtime_tools.braveSearchTool);
1462
1482
  break;
1463
1483
  case `fetch_url`:
1464
- out.push((0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox));
1484
+ out.push((0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox, {
1485
+ catalog: opts.modelCatalog,
1486
+ modelConfig: opts.modelConfig
1487
+ }));
1465
1488
  break;
1466
1489
  case `spawn_worker`:
1467
1490
  out.push(createSpawnWorkerTool(ctx));
@@ -1572,11 +1595,23 @@ function registerWorker(registry, options) {
1572
1595
  const { streamFn, modelCatalog } = options;
1573
1596
  registry.define(`worker`, {
1574
1597
  description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
1598
+ permissionGrants: [{
1599
+ subject_kind: `principal_kind`,
1600
+ subject_value: `user`,
1601
+ permission: `spawn`
1602
+ }, {
1603
+ subject_kind: `principal_kind`,
1604
+ subject_value: `user`,
1605
+ permission: `manage`
1606
+ }],
1575
1607
  async handler(ctx) {
1576
1608
  const args = parseWorkerArgs(ctx.args);
1577
1609
  const readSet = new Set();
1578
- const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
1579
1610
  const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
1611
+ const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet, {
1612
+ modelCatalog,
1613
+ modelConfig
1614
+ });
1580
1615
  const sharedStateTools = [];
1581
1616
  if (args.sharedDb) {
1582
1617
  const shared = await ctx.observe((0, __electric_ax_agents_runtime.db)(args.sharedDb.id, args.sharedDb.schema));
@@ -1770,6 +1805,18 @@ function resolveCwd(args, fallback) {
1770
1805
  return readWorkingDirectoryArg(args) ?? fallback;
1771
1806
  }
1772
1807
 
1808
+ //#endregion
1809
+ //#region src/durable-streams-cache.ts
1810
+ const MEMORY_CACHE_SIZE_BYTES = 100 * 1024 * 1024;
1811
+ function installDurableStreamsFetchCache(options = {}) {
1812
+ if (options === false) return;
1813
+ const store = options.store === `sqlite` || options.sqliteLocation ? new undici.cacheStores.SqliteCacheStore({
1814
+ location: options.sqliteLocation,
1815
+ maxCount: options.maxCount
1816
+ }) : new undici.cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
1817
+ (0, undici.setGlobalDispatcher)(new undici.Agent().compose(undici.interceptors.cache({ store })));
1818
+ }
1819
+
1773
1820
  //#endregion
1774
1821
  //#region src/server.ts
1775
1822
  var BuiltinAgentsServer = class {
@@ -1779,6 +1826,8 @@ var BuiltinAgentsServer = class {
1779
1826
  mcpToolProviderName = null;
1780
1827
  mcpApplyInFlight = new Set();
1781
1828
  mcpStopping = false;
1829
+ mcpExtras = [];
1830
+ mcpLastJsonConfig = null;
1782
1831
  pullWakeRunner = null;
1783
1832
  options;
1784
1833
  constructor(options) {
@@ -1788,8 +1837,70 @@ var BuiltinAgentsServer = class {
1788
1837
  get mcpRegistry() {
1789
1838
  return this._mcpRegistry;
1790
1839
  }
1840
+ /**
1841
+ * Replace the in-memory `extras` list and re-apply the merged config
1842
+ * against the last-known workspace `mcp.json` state. Workspace
1843
+ * `mcp.json` still wins on name collision. No-op once `stop()` has
1844
+ * latched `mcpStopping`.
1845
+ */
1846
+ async setExtraMcpServers(extras) {
1847
+ if (!this._mcpRegistry || this.mcpStopping) return;
1848
+ this.mcpExtras = extras;
1849
+ await this.applyMerged(this.mcpLastJsonConfig);
1850
+ }
1851
+ async wirePersistence(cfg) {
1852
+ const servers = [];
1853
+ for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
1854
+ const persist = await (0, __electric_ax_agents_mcp.keychainPersistence)({ server: s.name });
1855
+ servers.push({
1856
+ ...s,
1857
+ auth: {
1858
+ ...s.auth,
1859
+ ...persist
1860
+ }
1861
+ });
1862
+ } else servers.push(s);
1863
+ return {
1864
+ ...cfg,
1865
+ servers
1866
+ };
1867
+ }
1868
+ mergeMcp(jsonCfg) {
1869
+ const jsonServers = jsonCfg?.servers ?? [];
1870
+ const jsonNames = new Set(jsonServers.map((s) => s.name));
1871
+ const filteredExtras = this.mcpExtras.filter((s) => !jsonNames.has(s.name));
1872
+ return {
1873
+ servers: [...filteredExtras, ...jsonServers],
1874
+ raw: jsonCfg?.raw
1875
+ };
1876
+ }
1877
+ async runApply(jsonCfg) {
1878
+ if (this.mcpStopping) return;
1879
+ const registry = this._mcpRegistry;
1880
+ if (!registry) return;
1881
+ try {
1882
+ const wired = await this.wirePersistence(this.mergeMcp(jsonCfg));
1883
+ if (this.mcpStopping) return;
1884
+ await registry.applyConfig(wired);
1885
+ } catch (e) {
1886
+ serverLog.error(`[mcp] applyConfig:`, e);
1887
+ try {
1888
+ this.options.onConfigError?.(e);
1889
+ } catch (cbErr) {
1890
+ serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
1891
+ }
1892
+ }
1893
+ }
1894
+ applyMerged(jsonCfg) {
1895
+ this.mcpLastJsonConfig = jsonCfg;
1896
+ const p = this.runApply(jsonCfg);
1897
+ this.mcpApplyInFlight.add(p);
1898
+ p.finally(() => this.mcpApplyInFlight.delete(p));
1899
+ return p;
1900
+ }
1791
1901
  async start() {
1792
1902
  if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
1903
+ installDurableStreamsFetchCache(this.options.durableStreamsFetchCache);
1793
1904
  const pullWake = this.options.pullWake;
1794
1905
  if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
1795
1906
  try {
@@ -1800,76 +1911,28 @@ var BuiltinAgentsServer = class {
1800
1911
  });
1801
1912
  this._mcpRegistry = mcpRegistry;
1802
1913
  const mcpConfigPath = this.options.loadProjectMcpConfig ? node_path.default.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
1803
- const extras = this.options.extraMcpServers ?? [];
1804
- const wirePersistence = async (cfg) => {
1805
- const servers = [];
1806
- for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
1807
- const persist = await (0, __electric_ax_agents_mcp.keychainPersistence)({ server: s.name });
1808
- servers.push({
1809
- ...s,
1810
- auth: {
1811
- ...s.auth,
1812
- ...persist
1813
- }
1814
- });
1815
- } else servers.push(s);
1816
- return {
1817
- ...cfg,
1818
- servers
1819
- };
1820
- };
1821
- const merge = (jsonCfg) => {
1822
- const jsonServers = jsonCfg?.servers ?? [];
1823
- const jsonNames = new Set(jsonServers.map((s) => s.name));
1824
- const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
1825
- return {
1826
- servers: [...filteredExtras, ...jsonServers],
1827
- raw: jsonCfg?.raw
1828
- };
1829
- };
1830
- const onConfigError = this.options.onConfigError;
1831
- const runApply = async (jsonCfg) => {
1832
- if (this.mcpStopping) return;
1833
- try {
1834
- const wired = await wirePersistence(merge(jsonCfg));
1835
- if (this.mcpStopping) return;
1836
- await mcpRegistry.applyConfig(wired);
1837
- } catch (e) {
1838
- serverLog.error(`[mcp] applyConfig:`, e);
1839
- try {
1840
- onConfigError?.(e);
1841
- } catch (cbErr) {
1842
- serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
1843
- }
1844
- }
1845
- };
1846
- const applyMerged = (jsonCfg) => {
1847
- const p = runApply(jsonCfg);
1848
- this.mcpApplyInFlight.add(p);
1849
- p.finally(() => this.mcpApplyInFlight.delete(p));
1850
- return p;
1851
- };
1914
+ this.mcpExtras = this.options.extraMcpServers ?? [];
1852
1915
  if (mcpConfigPath) {
1853
1916
  try {
1854
1917
  const cfg = await (0, __electric_ax_agents_mcp.loadConfig)(mcpConfigPath, process.env);
1855
- applyMerged(cfg);
1918
+ this.applyMerged(cfg);
1856
1919
  } catch (err) {
1857
1920
  if (err.code !== `ENOENT`) throw err;
1858
- if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
1859
- else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
1860
- applyMerged(null);
1921
+ if (this.mcpExtras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
1922
+ else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${this.mcpExtras.length} server(s) from extras`);
1923
+ this.applyMerged(null);
1861
1924
  }
1862
1925
  try {
1863
1926
  this.mcpWatcherCloser = await (0, __electric_ax_agents_mcp.watchConfig)(mcpConfigPath, {
1864
- onChange: (cfg) => void applyMerged(cfg),
1927
+ onChange: (cfg) => void this.applyMerged(cfg),
1865
1928
  onError: (e) => serverLog.error(`[mcp] config error:`, e)
1866
1929
  });
1867
1930
  } catch (e) {
1868
1931
  serverLog.error(`[mcp] config watcher failed to start:`, e);
1869
1932
  }
1870
1933
  } else {
1871
- if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
1872
- applyMerged(null);
1934
+ if (this.mcpExtras.length > 0) serverLog.info(`[mcp] starting with ${this.mcpExtras.length} server(s) from extras`);
1935
+ this.applyMerged(null);
1873
1936
  }
1874
1937
  this.mcpToolProviderName = `mcp`;
1875
1938
  (0, __electric_ax_agents_runtime.registerToolProvider)({
package/dist/index.d.cts CHANGED
@@ -36,12 +36,26 @@ declare function createAgentHandler(agentServerUrl: string, workingDirectory?: s
36
36
  declare function registerBuiltinAgentTypes(bootstrap: AgentHandlerResult): Promise<void>;
37
37
  declare const registerAgentTypes: typeof registerBuiltinAgentTypes;
38
38
 
39
+ //#endregion
40
+ //#region src/durable-streams-cache.d.ts
41
+ type DurableStreamsFetchCacheOptions = false | {
42
+ store?: `memory` | `sqlite`;
43
+ sqliteLocation?: string;
44
+ maxCount?: number;
45
+ };
46
+
39
47
  //#endregion
40
48
  //#region src/server.d.ts
41
49
  interface BuiltinAgentsServerOptions {
42
50
  agentServerUrl: string;
43
51
  workingDirectory?: string;
44
52
  mockStreamFn?: StreamFn;
53
+ /**
54
+ * Configure the process-wide HTTP cache used by Undici-backed fetch calls.
55
+ * Defaults to a 100 MiB in-memory cache. Pass `false` to leave the global
56
+ * dispatcher unchanged.
57
+ */
58
+ durableStreamsFetchCache?: DurableStreamsFetchCacheOptions;
45
59
  /** Pull-wake runner configuration for built-in agents. */
46
60
  pullWake: {
47
61
  runnerId: string;
@@ -92,11 +106,24 @@ declare class BuiltinAgentsServer {
92
106
  private mcpToolProviderName;
93
107
  private mcpApplyInFlight;
94
108
  private mcpStopping;
109
+ private mcpExtras;
110
+ private mcpLastJsonConfig;
95
111
  private pullWakeRunner;
96
112
  readonly options: BuiltinAgentsServerOptions;
97
113
  constructor(options: BuiltinAgentsServerOptions);
98
114
  /** Embedded MCP registry. `null` until `start()` has run. */
99
115
  get mcpRegistry(): Registry | null;
116
+ /**
117
+ * Replace the in-memory `extras` list and re-apply the merged config
118
+ * against the last-known workspace `mcp.json` state. Workspace
119
+ * `mcp.json` still wins on name collision. No-op once `stop()` has
120
+ * latched `mcpStopping`.
121
+ */
122
+ setExtraMcpServers(extras: ReadonlyArray<McpServerConfig$1>): Promise<void>;
123
+ private wirePersistence;
124
+ private mergeMcp;
125
+ private runApply;
126
+ private applyMerged;
100
127
  start(): Promise<string>;
101
128
  stop(): Promise<void>;
102
129
  private registerPullWakeRunner;