@electric-ax/agents 0.4.12 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entrypoint.js +126 -63
- package/dist/index.cjs +125 -62
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +126 -63
- package/docs/entities/patterns/blackboard.md +1 -1
- package/docs/entities/patterns/dispatcher.md +3 -3
- package/docs/entities/patterns/manager-worker.md +11 -23
- package/docs/entities/patterns/map-reduce.md +1 -1
- package/docs/entities/patterns/pipeline.md +3 -3
- package/docs/index.md +61 -39
- package/docs/quickstart.md +26 -22
- package/docs/reference/entity-handle.md +51 -25
- package/docs/reference/handler-context.md +1 -1
- package/docs/reference/wake-event.md +1 -1
- package/docs/usage/defining-tools.md +4 -5
- package/docs/usage/overview.md +10 -6
- package/docs/usage/shared-state.md +3 -3
- package/docs/usage/spawning-and-coordinating.md +34 -18
- package/docs/usage/writing-handlers.md +1 -1
- package/docs/walkthrough.md +1156 -0
- package/package.json +4 -3
- package/skills/quickstart/scaffold/package.json +1 -1
- package/skills/quickstart.md +16 -10
package/dist/entrypoint.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
1823
|
-
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
1859
|
-
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${
|
|
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 (
|
|
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;
|