@electric-ax/agents 0.4.11 → 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 +235 -89
- package/dist/index.cjs +233 -87
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +243 -97
- 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/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ const node_path = __toESM(require("node:path"));
|
|
|
27
27
|
const node_url = __toESM(require("node:url"));
|
|
28
28
|
const __electric_ax_agents_runtime = __toESM(require("@electric-ax/agents-runtime"));
|
|
29
29
|
const __electric_ax_agents_runtime_tools = __toESM(require("@electric-ax/agents-runtime/tools"));
|
|
30
|
+
const __electric_ax_agents_runtime_sandbox = __toESM(require("@electric-ax/agents-runtime/sandbox"));
|
|
30
31
|
const node_fs = __toESM(require("node:fs"));
|
|
31
32
|
const pino = __toESM(require("pino"));
|
|
32
33
|
const zod = __toESM(require("zod"));
|
|
@@ -38,6 +39,7 @@ const sqlite_vec = __toESM(require("sqlite-vec"));
|
|
|
38
39
|
const nanoid = __toESM(require("nanoid"));
|
|
39
40
|
const __mariozechner_pi_ai = __toESM(require("@mariozechner/pi-ai"));
|
|
40
41
|
const __electric_ax_agents_mcp = __toESM(require("@electric-ax/agents-mcp"));
|
|
42
|
+
const undici = __toESM(require("undici"));
|
|
41
43
|
|
|
42
44
|
//#region src/log.ts
|
|
43
45
|
const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
|
|
@@ -788,7 +790,8 @@ function createSpawnWorkerTool(ctx, modelConfig) {
|
|
|
788
790
|
wake: {
|
|
789
791
|
on: `runFinished`,
|
|
790
792
|
includeResponse: true
|
|
791
|
-
}
|
|
793
|
+
},
|
|
794
|
+
sandbox: `inherit`
|
|
792
795
|
});
|
|
793
796
|
const workerUrl = handle.entityUrl;
|
|
794
797
|
return {
|
|
@@ -887,6 +890,15 @@ async function fetchAvailableModelIds(provider) {
|
|
|
887
890
|
function knownModelsForProvider(provider) {
|
|
888
891
|
return provider === __electric_ax_agents_runtime.MOONSHOT_PROVIDER ? (0, __electric_ax_agents_runtime.getMoonshotModels)() : (0, __mariozechner_pi_ai.getModels)(provider);
|
|
889
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
|
+
}
|
|
890
902
|
function choiceForKnownModel(provider, model) {
|
|
891
903
|
return {
|
|
892
904
|
provider,
|
|
@@ -1176,19 +1188,19 @@ function getToolName(tool) {
|
|
|
1176
1188
|
const name = tool.name;
|
|
1177
1189
|
return typeof name === `string` ? name : null;
|
|
1178
1190
|
}
|
|
1179
|
-
function createHortonTools(
|
|
1191
|
+
function createHortonTools(sandbox, ctx, readSet, opts = {}) {
|
|
1180
1192
|
return [
|
|
1181
|
-
(0, __electric_ax_agents_runtime_tools.createBashTool)(
|
|
1182
|
-
(0, __electric_ax_agents_runtime_tools.createReadFileTool)(
|
|
1183
|
-
(0, __electric_ax_agents_runtime_tools.createWriteTool)(
|
|
1184
|
-
(0, __electric_ax_agents_runtime_tools.createEditTool)(
|
|
1193
|
+
(0, __electric_ax_agents_runtime_tools.createBashTool)(sandbox),
|
|
1194
|
+
(0, __electric_ax_agents_runtime_tools.createReadFileTool)(sandbox, readSet),
|
|
1195
|
+
(0, __electric_ax_agents_runtime_tools.createWriteTool)(sandbox, readSet),
|
|
1196
|
+
(0, __electric_ax_agents_runtime_tools.createEditTool)(sandbox, readSet),
|
|
1185
1197
|
__electric_ax_agents_runtime_tools.braveSearchTool,
|
|
1186
|
-
...opts.modelCatalog && opts.modelConfig ? [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)({
|
|
1198
|
+
...opts.modelCatalog && opts.modelConfig ? [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox, {
|
|
1187
1199
|
catalog: opts.modelCatalog,
|
|
1188
1200
|
modelConfig: opts.modelConfig,
|
|
1189
1201
|
log: (message) => serverLog.info(message),
|
|
1190
1202
|
logPrefix: opts.logPrefix ?? `[horton]`
|
|
1191
|
-
})] : [__electric_ax_agents_runtime_tools.
|
|
1203
|
+
})] : [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox)],
|
|
1192
1204
|
createSpawnWorkerTool(ctx, opts.modelConfig),
|
|
1193
1205
|
(0, __electric_ax_agents_runtime_tools.createSendTool)(ctx.send, { selfEntityUrl: ctx.entityUrl }),
|
|
1194
1206
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
@@ -1242,11 +1254,10 @@ async function extractFirstUserMessage(ctx) {
|
|
|
1242
1254
|
function messageSeq(message) {
|
|
1243
1255
|
return typeof message._seq === `number` ? message._seq : -1;
|
|
1244
1256
|
}
|
|
1245
|
-
function readAgentsMd(
|
|
1246
|
-
const agentsMdPath = node_path.default.join(workingDirectory, `AGENTS.md`);
|
|
1257
|
+
async function readAgentsMd(sandbox) {
|
|
1258
|
+
const agentsMdPath = node_path.default.posix.join(sandbox.workingDirectory, `AGENTS.md`);
|
|
1247
1259
|
try {
|
|
1248
|
-
|
|
1249
|
-
const content = node_fs.default.readFileSync(agentsMdPath, `utf8`);
|
|
1260
|
+
const content = (await sandbox.readFile(agentsMdPath)).toString(`utf8`);
|
|
1250
1261
|
return [
|
|
1251
1262
|
`<context_file kind="instructions" path="${agentsMdPath}">`,
|
|
1252
1263
|
content,
|
|
@@ -1257,16 +1268,17 @@ function readAgentsMd(workingDirectory) {
|
|
|
1257
1268
|
}
|
|
1258
1269
|
}
|
|
1259
1270
|
function createAssistantHandler(options) {
|
|
1260
|
-
const {
|
|
1271
|
+
const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
|
|
1261
1272
|
const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
|
|
1262
1273
|
return async function assistantHandler(ctx, wake) {
|
|
1263
1274
|
const readSet = new Set();
|
|
1264
|
-
const effectiveCwd = typeof ctx.args.workingDirectory === `string` && ctx.args.workingDirectory.trim().length > 0 ? ctx.args.workingDirectory : workingDirectory;
|
|
1265
1275
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
|
|
1266
|
-
const
|
|
1276
|
+
const sourceBudget = resolveBuiltinModelSourceBudget(modelConfig);
|
|
1277
|
+
const sandboxCwd = ctx.sandbox.workingDirectory;
|
|
1278
|
+
const agentsMd = await readAgentsMd(ctx.sandbox);
|
|
1267
1279
|
const tools = [
|
|
1268
1280
|
...ctx.electricTools,
|
|
1269
|
-
...createHortonTools(
|
|
1281
|
+
...createHortonTools(ctx.sandbox, ctx, readSet, {
|
|
1270
1282
|
docsSearchTool,
|
|
1271
1283
|
modelConfig,
|
|
1272
1284
|
modelCatalog,
|
|
@@ -1295,7 +1307,7 @@ function createAssistantHandler(options) {
|
|
|
1295
1307
|
}
|
|
1296
1308
|
})() : Promise.resolve();
|
|
1297
1309
|
if (docsSupport) ctx.useContext({
|
|
1298
|
-
sourceBudget
|
|
1310
|
+
sourceBudget,
|
|
1299
1311
|
sources: {
|
|
1300
1312
|
docs_toc: {
|
|
1301
1313
|
content: () => docsSupport.renderCompressedToc(),
|
|
@@ -1324,7 +1336,7 @@ function createAssistantHandler(options) {
|
|
|
1324
1336
|
}
|
|
1325
1337
|
});
|
|
1326
1338
|
else if (skillsRegistry && skillsRegistry.catalog.size > 0) ctx.useContext({
|
|
1327
|
-
sourceBudget
|
|
1339
|
+
sourceBudget,
|
|
1328
1340
|
sources: {
|
|
1329
1341
|
skills_catalog: {
|
|
1330
1342
|
content: () => skillsRegistry.renderCatalog(2e3),
|
|
@@ -1343,7 +1355,7 @@ function createAssistantHandler(options) {
|
|
|
1343
1355
|
}
|
|
1344
1356
|
});
|
|
1345
1357
|
else if (agentsMd) ctx.useContext({
|
|
1346
|
-
sourceBudget
|
|
1358
|
+
sourceBudget,
|
|
1347
1359
|
sources: {
|
|
1348
1360
|
conversation: {
|
|
1349
1361
|
content: () => ctx.timelineMessages(),
|
|
@@ -1357,7 +1369,7 @@ function createAssistantHandler(options) {
|
|
|
1357
1369
|
}
|
|
1358
1370
|
});
|
|
1359
1371
|
ctx.useAgent({
|
|
1360
|
-
systemPrompt: buildHortonSystemPrompt(
|
|
1372
|
+
systemPrompt: buildHortonSystemPrompt(sandboxCwd, {
|
|
1361
1373
|
hasDocsSupport: Boolean(docsSupport),
|
|
1362
1374
|
hasSkills,
|
|
1363
1375
|
docsUrl,
|
|
@@ -1385,7 +1397,6 @@ function registerHorton(registry, options) {
|
|
|
1385
1397
|
serverLog.warn(`[horton-docs] warmup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1386
1398
|
});
|
|
1387
1399
|
const assistantHandler = createAssistantHandler({
|
|
1388
|
-
workingDirectory,
|
|
1389
1400
|
streamFn,
|
|
1390
1401
|
docsSupport,
|
|
1391
1402
|
docsSearchTool,
|
|
@@ -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,26 +1462,29 @@ 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,
|
|
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`:
|
|
1449
|
-
out.push((0, __electric_ax_agents_runtime_tools.createBashTool)(
|
|
1469
|
+
out.push((0, __electric_ax_agents_runtime_tools.createBashTool)(sandbox));
|
|
1450
1470
|
break;
|
|
1451
1471
|
case `read`:
|
|
1452
|
-
out.push((0, __electric_ax_agents_runtime_tools.createReadFileTool)(
|
|
1472
|
+
out.push((0, __electric_ax_agents_runtime_tools.createReadFileTool)(sandbox, readSet));
|
|
1453
1473
|
break;
|
|
1454
1474
|
case `write`:
|
|
1455
|
-
out.push((0, __electric_ax_agents_runtime_tools.createWriteTool)(
|
|
1475
|
+
out.push((0, __electric_ax_agents_runtime_tools.createWriteTool)(sandbox, readSet));
|
|
1456
1476
|
break;
|
|
1457
1477
|
case `edit`:
|
|
1458
|
-
out.push((0, __electric_ax_agents_runtime_tools.createEditTool)(
|
|
1478
|
+
out.push((0, __electric_ax_agents_runtime_tools.createEditTool)(sandbox, readSet));
|
|
1459
1479
|
break;
|
|
1460
1480
|
case `web_search`:
|
|
1461
1481
|
out.push(__electric_ax_agents_runtime_tools.braveSearchTool);
|
|
1462
1482
|
break;
|
|
1463
1483
|
case `fetch_url`:
|
|
1464
|
-
out.push(__electric_ax_agents_runtime_tools.
|
|
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));
|
|
@@ -1569,14 +1592,26 @@ function buildSharedStateTools(shared, schema, mode) {
|
|
|
1569
1592
|
return tools;
|
|
1570
1593
|
}
|
|
1571
1594
|
function registerWorker(registry, options) {
|
|
1572
|
-
const {
|
|
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, workingDirectory, 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));
|
|
@@ -1654,6 +1689,7 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1654
1689
|
modelCatalog
|
|
1655
1690
|
});
|
|
1656
1691
|
typeNames.push(`worker`);
|
|
1692
|
+
const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
|
|
1657
1693
|
const runtime = (0, __electric_ax_agents_runtime.createRuntimeHandler)({
|
|
1658
1694
|
baseUrl: agentServerUrl,
|
|
1659
1695
|
serveEndpoint,
|
|
@@ -1664,7 +1700,8 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1664
1700
|
idleTimeout: 5 * 6e4,
|
|
1665
1701
|
createElectricTools: createBuiltinElectricTools(createElectricTools),
|
|
1666
1702
|
publicUrl,
|
|
1667
|
-
name: runtimeName ?? `builtin-agents
|
|
1703
|
+
name: runtimeName ?? `builtin-agents`,
|
|
1704
|
+
sandboxProfiles
|
|
1668
1705
|
});
|
|
1669
1706
|
return {
|
|
1670
1707
|
handler: runtime.onEnter,
|
|
@@ -1688,6 +1725,97 @@ async function registerBuiltinAgentTypes(bootstrap) {
|
|
|
1688
1725
|
serverLog.info(`[builtin-agents] ${bootstrap.typeNames.length} built-in agent types ready: ${bootstrap.typeNames.join(`, `)}`);
|
|
1689
1726
|
}
|
|
1690
1727
|
const registerAgentTypes = registerBuiltinAgentTypes;
|
|
1728
|
+
/**
|
|
1729
|
+
* Guard so repeated `buildBuiltinSandboxProfiles` calls in one process don't
|
|
1730
|
+
* re-run the boot sweep.
|
|
1731
|
+
*/
|
|
1732
|
+
let dockerSweptOnBoot = false;
|
|
1733
|
+
function sweepOrphanedDockerSandboxesOnce(sweep) {
|
|
1734
|
+
if (dockerSweptOnBoot) return;
|
|
1735
|
+
dockerSweptOnBoot = true;
|
|
1736
|
+
sweep().then((removed) => {
|
|
1737
|
+
if (removed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep removed ${removed.length} leftover container(s)`);
|
|
1738
|
+
}).catch((err) => serverLog.warn(`[builtin-agents] docker sandbox boot sweep error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Built-in sandbox profiles. `local` is always available. `docker` is
|
|
1742
|
+
* gated on Docker being reachable so a user without Docker installed
|
|
1743
|
+
* sees only what works — the UI never offers a non-functional choice.
|
|
1744
|
+
*/
|
|
1745
|
+
async function buildBuiltinSandboxProfiles(workingDirectory) {
|
|
1746
|
+
const profiles = [{
|
|
1747
|
+
name: `local`,
|
|
1748
|
+
label: `Local`,
|
|
1749
|
+
description: `Runs on the host without isolation. Full filesystem access.`,
|
|
1750
|
+
factory: ({ args }) => (0, __electric_ax_agents_runtime_sandbox.chooseDefaultSandbox)(resolveCwd(args, workingDirectory))
|
|
1751
|
+
}];
|
|
1752
|
+
try {
|
|
1753
|
+
const { isDockerAvailable } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
|
|
1754
|
+
if (await isDockerAvailable()) {
|
|
1755
|
+
const { dockerSandbox, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
|
|
1756
|
+
sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
|
|
1757
|
+
profiles.push({
|
|
1758
|
+
name: `docker`,
|
|
1759
|
+
label: `Docker`,
|
|
1760
|
+
description: `Runs in a hardened Docker container: dropped capabilities, no privilege escalation, and CPU/memory/process limits. The chosen working directory is mounted read-write and, by default, network egress is unrestricted (allow-all).`,
|
|
1761
|
+
factory: ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
|
|
1762
|
+
const cwd = readWorkingDirectoryArg(args);
|
|
1763
|
+
return dockerSandbox({
|
|
1764
|
+
initialNetworkPolicy: { mode: `allow-all` },
|
|
1765
|
+
extraMounts: cwd ? [{
|
|
1766
|
+
hostPath: cwd,
|
|
1767
|
+
containerPath: `/work`,
|
|
1768
|
+
readOnly: false
|
|
1769
|
+
}] : void 0,
|
|
1770
|
+
sandboxKey,
|
|
1771
|
+
persistent,
|
|
1772
|
+
owner,
|
|
1773
|
+
entityType,
|
|
1774
|
+
entityUrl
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
} else serverLog.info(`[builtin-agents] docker daemon not reachable — docker sandbox profile not registered`);
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
serverLog.warn(`[builtin-agents] failed to probe docker availability: ${err instanceof Error ? err.message : String(err)}`);
|
|
1781
|
+
}
|
|
1782
|
+
if (process.env.E2B_API_KEY) if (await (0, __electric_ax_agents_runtime_sandbox.isE2BAvailable)()) profiles.push({
|
|
1783
|
+
name: `e2b`,
|
|
1784
|
+
label: `E2B`,
|
|
1785
|
+
description: `Runs in a remote E2B microVM. Persistent sandboxes survive across wakes and are reachable from any runner.`,
|
|
1786
|
+
remote: true,
|
|
1787
|
+
factory: ({ sandboxKey, persistent, owner }) => (0, __electric_ax_agents_runtime_sandbox.remoteSandbox)({
|
|
1788
|
+
provider: `e2b`,
|
|
1789
|
+
apiKey: process.env.E2B_API_KEY,
|
|
1790
|
+
sandboxKey,
|
|
1791
|
+
persistent,
|
|
1792
|
+
owner,
|
|
1793
|
+
initialNetworkPolicy: { mode: `allow-all` }
|
|
1794
|
+
})
|
|
1795
|
+
});
|
|
1796
|
+
else serverLog.info(`[builtin-agents] E2B_API_KEY set but the "e2b" package is not installed — e2b sandbox profile not registered`);
|
|
1797
|
+
console.log(`[builtin-agents] sandbox profiles advertised: ${profiles.map((p) => p.name).join(`, `)}`);
|
|
1798
|
+
return profiles;
|
|
1799
|
+
}
|
|
1800
|
+
function readWorkingDirectoryArg(args) {
|
|
1801
|
+
const v = args.workingDirectory;
|
|
1802
|
+
return typeof v === `string` && v.trim().length > 0 ? v : null;
|
|
1803
|
+
}
|
|
1804
|
+
function resolveCwd(args, fallback) {
|
|
1805
|
+
return readWorkingDirectoryArg(args) ?? fallback;
|
|
1806
|
+
}
|
|
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
|
+
}
|
|
1691
1819
|
|
|
1692
1820
|
//#endregion
|
|
1693
1821
|
//#region src/server.ts
|
|
@@ -1698,6 +1826,8 @@ var BuiltinAgentsServer = class {
|
|
|
1698
1826
|
mcpToolProviderName = null;
|
|
1699
1827
|
mcpApplyInFlight = new Set();
|
|
1700
1828
|
mcpStopping = false;
|
|
1829
|
+
mcpExtras = [];
|
|
1830
|
+
mcpLastJsonConfig = null;
|
|
1701
1831
|
pullWakeRunner = null;
|
|
1702
1832
|
options;
|
|
1703
1833
|
constructor(options) {
|
|
@@ -1707,8 +1837,70 @@ var BuiltinAgentsServer = class {
|
|
|
1707
1837
|
get mcpRegistry() {
|
|
1708
1838
|
return this._mcpRegistry;
|
|
1709
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
|
+
}
|
|
1710
1901
|
async start() {
|
|
1711
1902
|
if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
|
|
1903
|
+
installDurableStreamsFetchCache(this.options.durableStreamsFetchCache);
|
|
1712
1904
|
const pullWake = this.options.pullWake;
|
|
1713
1905
|
if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
|
|
1714
1906
|
try {
|
|
@@ -1719,76 +1911,28 @@ var BuiltinAgentsServer = class {
|
|
|
1719
1911
|
});
|
|
1720
1912
|
this._mcpRegistry = mcpRegistry;
|
|
1721
1913
|
const mcpConfigPath = this.options.loadProjectMcpConfig ? node_path.default.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
|
|
1722
|
-
|
|
1723
|
-
const wirePersistence = async (cfg) => {
|
|
1724
|
-
const servers = [];
|
|
1725
|
-
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
1726
|
-
const persist = await (0, __electric_ax_agents_mcp.keychainPersistence)({ server: s.name });
|
|
1727
|
-
servers.push({
|
|
1728
|
-
...s,
|
|
1729
|
-
auth: {
|
|
1730
|
-
...s.auth,
|
|
1731
|
-
...persist
|
|
1732
|
-
}
|
|
1733
|
-
});
|
|
1734
|
-
} else servers.push(s);
|
|
1735
|
-
return {
|
|
1736
|
-
...cfg,
|
|
1737
|
-
servers
|
|
1738
|
-
};
|
|
1739
|
-
};
|
|
1740
|
-
const merge = (jsonCfg) => {
|
|
1741
|
-
const jsonServers = jsonCfg?.servers ?? [];
|
|
1742
|
-
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
1743
|
-
const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
|
|
1744
|
-
return {
|
|
1745
|
-
servers: [...filteredExtras, ...jsonServers],
|
|
1746
|
-
raw: jsonCfg?.raw
|
|
1747
|
-
};
|
|
1748
|
-
};
|
|
1749
|
-
const onConfigError = this.options.onConfigError;
|
|
1750
|
-
const runApply = async (jsonCfg) => {
|
|
1751
|
-
if (this.mcpStopping) return;
|
|
1752
|
-
try {
|
|
1753
|
-
const wired = await wirePersistence(merge(jsonCfg));
|
|
1754
|
-
if (this.mcpStopping) return;
|
|
1755
|
-
await mcpRegistry.applyConfig(wired);
|
|
1756
|
-
} catch (e) {
|
|
1757
|
-
serverLog.error(`[mcp] applyConfig:`, e);
|
|
1758
|
-
try {
|
|
1759
|
-
onConfigError?.(e);
|
|
1760
|
-
} catch (cbErr) {
|
|
1761
|
-
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
};
|
|
1765
|
-
const applyMerged = (jsonCfg) => {
|
|
1766
|
-
const p = runApply(jsonCfg);
|
|
1767
|
-
this.mcpApplyInFlight.add(p);
|
|
1768
|
-
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
1769
|
-
return p;
|
|
1770
|
-
};
|
|
1914
|
+
this.mcpExtras = this.options.extraMcpServers ?? [];
|
|
1771
1915
|
if (mcpConfigPath) {
|
|
1772
1916
|
try {
|
|
1773
1917
|
const cfg = await (0, __electric_ax_agents_mcp.loadConfig)(mcpConfigPath, process.env);
|
|
1774
|
-
applyMerged(cfg);
|
|
1918
|
+
this.applyMerged(cfg);
|
|
1775
1919
|
} catch (err) {
|
|
1776
1920
|
if (err.code !== `ENOENT`) throw err;
|
|
1777
|
-
if (
|
|
1778
|
-
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${
|
|
1779
|
-
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);
|
|
1780
1924
|
}
|
|
1781
1925
|
try {
|
|
1782
1926
|
this.mcpWatcherCloser = await (0, __electric_ax_agents_mcp.watchConfig)(mcpConfigPath, {
|
|
1783
|
-
onChange: (cfg) => void applyMerged(cfg),
|
|
1927
|
+
onChange: (cfg) => void this.applyMerged(cfg),
|
|
1784
1928
|
onError: (e) => serverLog.error(`[mcp] config error:`, e)
|
|
1785
1929
|
});
|
|
1786
1930
|
} catch (e) {
|
|
1787
1931
|
serverLog.error(`[mcp] config watcher failed to start:`, e);
|
|
1788
1932
|
}
|
|
1789
1933
|
} else {
|
|
1790
|
-
if (
|
|
1791
|
-
applyMerged(null);
|
|
1934
|
+
if (this.mcpExtras.length > 0) serverLog.info(`[mcp] starting with ${this.mcpExtras.length} server(s) from extras`);
|
|
1935
|
+
this.applyMerged(null);
|
|
1792
1936
|
}
|
|
1793
1937
|
this.mcpToolProviderName = `mcp`;
|
|
1794
1938
|
(0, __electric_ax_agents_runtime.registerToolProvider)({
|
|
@@ -1896,6 +2040,7 @@ var BuiltinAgentsServer = class {
|
|
|
1896
2040
|
async registerPullWakeRunner(pullWake) {
|
|
1897
2041
|
const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
|
|
1898
2042
|
headers.set(`content-type`, `application/json`);
|
|
2043
|
+
const profiles = this.bootstrap?.runtime.sandboxProfileDescriptors ?? [];
|
|
1899
2044
|
const response = await fetch((0, __electric_ax_agents_runtime.appendPathToUrl)(this.options.agentServerUrl, `/_electric/runners`), {
|
|
1900
2045
|
method: `POST`,
|
|
1901
2046
|
headers,
|
|
@@ -1904,7 +2049,8 @@ var BuiltinAgentsServer = class {
|
|
|
1904
2049
|
owner_principal: pullWake.ownerPrincipal,
|
|
1905
2050
|
label: pullWake.label ?? `Built-in agents`,
|
|
1906
2051
|
kind: `local`,
|
|
1907
|
-
admin_status: `enabled
|
|
2052
|
+
admin_status: `enabled`,
|
|
2053
|
+
sandbox_profiles: profiles
|
|
1908
2054
|
})
|
|
1909
2055
|
});
|
|
1910
2056
|
if (!response.ok) throw new Error(`Failed to register pull-wake runner ${pullWake.runnerId}: ${response.status} ${await response.text()}`);
|
package/dist/index.d.cts
CHANGED
|
@@ -2,6 +2,7 @@ import { AgentConfig, AgentTool, AvailableProvider, DispatchPolicy, EntityRegist
|
|
|
2
2
|
import { AgentTool as AgentTool$1, StreamFn } from "@mariozechner/pi-agent-core";
|
|
3
3
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
4
|
import { ListedEntry as McpListedEntry, McpConfig, McpServerConfig, McpServerConfig as McpServerConfig$1, Registry, Registry as McpRegistry, RegistrySnapshot, RegistrySubscriber } from "@electric-ax/agents-mcp";
|
|
5
|
+
import { Sandbox } from "@electric-ax/agents-runtime/sandbox";
|
|
5
6
|
import { ChangeEvent } from "@durable-streams/state";
|
|
6
7
|
import { braveSearchTool } from "@electric-ax/agents-runtime/tools";
|
|
7
8
|
|
|
@@ -35,12 +36,26 @@ declare function createAgentHandler(agentServerUrl: string, workingDirectory?: s
|
|
|
35
36
|
declare function registerBuiltinAgentTypes(bootstrap: AgentHandlerResult): Promise<void>;
|
|
36
37
|
declare const registerAgentTypes: typeof registerBuiltinAgentTypes;
|
|
37
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
|
+
|
|
38
47
|
//#endregion
|
|
39
48
|
//#region src/server.d.ts
|
|
40
49
|
interface BuiltinAgentsServerOptions {
|
|
41
50
|
agentServerUrl: string;
|
|
42
51
|
workingDirectory?: string;
|
|
43
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;
|
|
44
59
|
/** Pull-wake runner configuration for built-in agents. */
|
|
45
60
|
pullWake: {
|
|
46
61
|
runnerId: string;
|
|
@@ -91,11 +106,24 @@ declare class BuiltinAgentsServer {
|
|
|
91
106
|
private mcpToolProviderName;
|
|
92
107
|
private mcpApplyInFlight;
|
|
93
108
|
private mcpStopping;
|
|
109
|
+
private mcpExtras;
|
|
110
|
+
private mcpLastJsonConfig;
|
|
94
111
|
private pullWakeRunner;
|
|
95
112
|
readonly options: BuiltinAgentsServerOptions;
|
|
96
113
|
constructor(options: BuiltinAgentsServerOptions);
|
|
97
114
|
/** Embedded MCP registry. `null` until `start()` has run. */
|
|
98
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;
|
|
99
127
|
start(): Promise<string>;
|
|
100
128
|
stop(): Promise<void>;
|
|
101
129
|
private registerPullWakeRunner;
|
|
@@ -167,7 +195,7 @@ declare function buildHortonSystemPrompt(workingDirectory: string, opts?: {
|
|
|
167
195
|
modelProvider?: string;
|
|
168
196
|
modelId?: string;
|
|
169
197
|
}): string;
|
|
170
|
-
declare function createHortonTools(
|
|
198
|
+
declare function createHortonTools(sandbox: Sandbox, ctx: HandlerContext, readSet: Set<string>, opts?: {
|
|
171
199
|
docsSearchTool?: AgentTool$1;
|
|
172
200
|
modelConfig?: ReturnType<typeof resolveBuiltinModelConfig>;
|
|
173
201
|
modelCatalog?: BuiltinModelCatalog;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AgentConfig, AgentTool, AvailableProvider, DispatchPolicy, EntityRegistry, HandlerContext, HeadersProvider, ProcessWakeConfig, PullWakeRunnerConfig, RuntimeHandler, SkillsRegistry, WakeEvent } from "@electric-ax/agents-runtime";
|
|
2
2
|
import { braveSearchTool } from "@electric-ax/agents-runtime/tools";
|
|
3
|
+
import { Sandbox } from "@electric-ax/agents-runtime/sandbox";
|
|
3
4
|
import { ListedEntry as McpListedEntry, McpConfig, McpServerConfig, McpServerConfig as McpServerConfig$1, Registry, Registry as McpRegistry, RegistrySnapshot, RegistrySubscriber } from "@electric-ax/agents-mcp";
|
|
4
5
|
import { AgentTool as AgentTool$1, StreamFn } from "@mariozechner/pi-agent-core";
|
|
5
6
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
@@ -35,12 +36,26 @@ declare function createAgentHandler(agentServerUrl: string, workingDirectory?: s
|
|
|
35
36
|
declare function registerBuiltinAgentTypes(bootstrap: AgentHandlerResult): Promise<void>;
|
|
36
37
|
declare const registerAgentTypes: typeof registerBuiltinAgentTypes;
|
|
37
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
|
+
|
|
38
47
|
//#endregion
|
|
39
48
|
//#region src/server.d.ts
|
|
40
49
|
interface BuiltinAgentsServerOptions {
|
|
41
50
|
agentServerUrl: string;
|
|
42
51
|
workingDirectory?: string;
|
|
43
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;
|
|
44
59
|
/** Pull-wake runner configuration for built-in agents. */
|
|
45
60
|
pullWake: {
|
|
46
61
|
runnerId: string;
|
|
@@ -91,11 +106,24 @@ declare class BuiltinAgentsServer {
|
|
|
91
106
|
private mcpToolProviderName;
|
|
92
107
|
private mcpApplyInFlight;
|
|
93
108
|
private mcpStopping;
|
|
109
|
+
private mcpExtras;
|
|
110
|
+
private mcpLastJsonConfig;
|
|
94
111
|
private pullWakeRunner;
|
|
95
112
|
readonly options: BuiltinAgentsServerOptions;
|
|
96
113
|
constructor(options: BuiltinAgentsServerOptions);
|
|
97
114
|
/** Embedded MCP registry. `null` until `start()` has run. */
|
|
98
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;
|
|
99
127
|
start(): Promise<string>;
|
|
100
128
|
stop(): Promise<void>;
|
|
101
129
|
private registerPullWakeRunner;
|
|
@@ -167,7 +195,7 @@ declare function buildHortonSystemPrompt(workingDirectory: string, opts?: {
|
|
|
167
195
|
modelProvider?: string;
|
|
168
196
|
modelId?: string;
|
|
169
197
|
}): string;
|
|
170
|
-
declare function createHortonTools(
|
|
198
|
+
declare function createHortonTools(sandbox: Sandbox, ctx: HandlerContext, readSet: Set<string>, opts?: {
|
|
171
199
|
docsSearchTool?: AgentTool$1;
|
|
172
200
|
modelConfig?: ReturnType<typeof resolveBuiltinModelConfig>;
|
|
173
201
|
modelCatalog?: BuiltinModelCatalog;
|