@electric-ax/agents 0.4.12 → 0.4.14
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 +155 -79
- package/dist/index.cjs +154 -78
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +155 -79
- 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,
|
|
7
|
+
import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, buildSkillSlashCommands, completeWithLowCostModel, createContextSkillLoader, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, 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,
|
|
@@ -967,6 +989,8 @@ function modelInputSchemaDefs(catalog) {
|
|
|
967
989
|
//#region src/agents/horton.ts
|
|
968
990
|
const TITLE_SYSTEM_PROMPT = "You generate concise chat session titles in 3-5 words. Respond with only the title, no quotes, no punctuation, no preamble.";
|
|
969
991
|
const TITLE_USER_PROMPT = (userMessage) => `User request:\n${userMessage}`;
|
|
992
|
+
const TITLE_GENERATION_TIMEOUT_MS = 8e3;
|
|
993
|
+
const HORTON_SKILLS_SLASH_COMMAND_OWNER = `horton:skills`;
|
|
970
994
|
const TITLE_STOP_WORDS = new Set([
|
|
971
995
|
`a`,
|
|
972
996
|
`an`,
|
|
@@ -1046,6 +1070,17 @@ function createConfiguredTitleCall(catalog, modelConfig, logPrefix) {
|
|
|
1046
1070
|
maxTokens: 64
|
|
1047
1071
|
});
|
|
1048
1072
|
}
|
|
1073
|
+
function withTimeout(promise, ms, description) {
|
|
1074
|
+
let timeout;
|
|
1075
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
1076
|
+
timeout = setTimeout(() => {
|
|
1077
|
+
reject(new Error(`${description} timed out after ${ms}ms`));
|
|
1078
|
+
}, ms);
|
|
1079
|
+
});
|
|
1080
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
1081
|
+
if (timeout) clearTimeout(timeout);
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1049
1084
|
async function generateTitle(userMessage, llmCall, onFallback) {
|
|
1050
1085
|
try {
|
|
1051
1086
|
const raw = await llmCall(TITLE_USER_PROMPT(userMessage));
|
|
@@ -1175,8 +1210,12 @@ function payloadToTitleText(payload) {
|
|
|
1175
1210
|
if (typeof payload === `string`) return payload;
|
|
1176
1211
|
if (payload == null) return ``;
|
|
1177
1212
|
if (typeof payload === `object`) {
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1213
|
+
const record = payload;
|
|
1214
|
+
const text = record.text;
|
|
1215
|
+
if (typeof text === `string`) return text;
|
|
1216
|
+
const source = record.source;
|
|
1217
|
+
if (typeof source === `string`) return source;
|
|
1218
|
+
return JSON.stringify(payload);
|
|
1180
1219
|
}
|
|
1181
1220
|
return String(payload);
|
|
1182
1221
|
}
|
|
@@ -1234,10 +1273,13 @@ async function readAgentsMd(sandbox) {
|
|
|
1234
1273
|
}
|
|
1235
1274
|
function createAssistantHandler(options) {
|
|
1236
1275
|
const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
|
|
1237
|
-
const
|
|
1276
|
+
const skillLoader = createContextSkillLoader(skillsRegistry, { slashCommandOwner: HORTON_SKILLS_SLASH_COMMAND_OWNER });
|
|
1277
|
+
const hasSkills = skillLoader.hasSkills;
|
|
1238
1278
|
return async function assistantHandler(ctx, wake) {
|
|
1279
|
+
const loadedSkills = await skillLoader.load(ctx);
|
|
1239
1280
|
const readSet = new Set();
|
|
1240
1281
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
|
|
1282
|
+
const sourceBudget = resolveBuiltinModelSourceBudget(modelConfig);
|
|
1241
1283
|
const sandboxCwd = ctx.sandbox.workingDirectory;
|
|
1242
1284
|
const agentsMd = await readAgentsMd(ctx.sandbox);
|
|
1243
1285
|
const tools = [
|
|
@@ -1248,7 +1290,7 @@ function createAssistantHandler(options) {
|
|
|
1248
1290
|
modelCatalog,
|
|
1249
1291
|
logPrefix: `[horton ${ctx.entityUrl}]`
|
|
1250
1292
|
}),
|
|
1251
|
-
...
|
|
1293
|
+
...loadedSkills.tools,
|
|
1252
1294
|
...mcp.tools()
|
|
1253
1295
|
];
|
|
1254
1296
|
const hasEventSourceTools = tools.some((tool) => getToolName(tool) === `list_event_sources`);
|
|
@@ -1257,21 +1299,22 @@ function createAssistantHandler(options) {
|
|
|
1257
1299
|
if (!firstUserMessage) return;
|
|
1258
1300
|
let title = null;
|
|
1259
1301
|
try {
|
|
1260
|
-
const result = await generateTitle(firstUserMessage, createConfiguredTitleCall(modelCatalog, modelConfig, `[horton ${ctx.entityUrl}]`), (reason) => {
|
|
1302
|
+
const result = await generateTitle(firstUserMessage, (prompt) => withTimeout(createConfiguredTitleCall(modelCatalog, modelConfig, `[horton ${ctx.entityUrl}]`)(prompt), TITLE_GENERATION_TIMEOUT_MS, `title generation`), (reason) => {
|
|
1261
1303
|
serverLog.warn(`[horton ${ctx.entityUrl}] title generation fell back to local title: ${reason}`);
|
|
1262
1304
|
});
|
|
1263
1305
|
if (result.length > 0) title = result;
|
|
1264
1306
|
} catch (err) {
|
|
1265
1307
|
serverLog.warn(`[horton ${ctx.entityUrl}] title generation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1308
|
+
title = buildFallbackTitle(firstUserMessage);
|
|
1266
1309
|
}
|
|
1267
1310
|
if (title !== null) try {
|
|
1268
|
-
await ctx.setTag(`title`, title);
|
|
1311
|
+
await withTimeout(ctx.setTag(`title`, title), TITLE_GENERATION_TIMEOUT_MS, `set title tag`);
|
|
1269
1312
|
} catch (err) {
|
|
1270
1313
|
serverLog.warn(`[horton ${ctx.entityUrl}] setTag failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1271
1314
|
}
|
|
1272
1315
|
})() : Promise.resolve();
|
|
1273
1316
|
if (docsSupport) ctx.useContext({
|
|
1274
|
-
sourceBudget
|
|
1317
|
+
sourceBudget,
|
|
1275
1318
|
sources: {
|
|
1276
1319
|
docs_toc: {
|
|
1277
1320
|
content: () => docsSupport.renderCompressedToc(),
|
|
@@ -1292,21 +1335,13 @@ function createAssistantHandler(options) {
|
|
|
1292
1335
|
max: 2e4,
|
|
1293
1336
|
cache: `stable`
|
|
1294
1337
|
} } : {},
|
|
1295
|
-
...skillsRegistry && skillsRegistry.catalog.size > 0 ?
|
|
1296
|
-
content: () => skillsRegistry.renderCatalog(2e3),
|
|
1297
|
-
max: 2e3,
|
|
1298
|
-
cache: `stable`
|
|
1299
|
-
} } : {}
|
|
1338
|
+
...skillsRegistry && skillsRegistry.catalog.size > 0 ? loadedSkills.sources : {}
|
|
1300
1339
|
}
|
|
1301
1340
|
});
|
|
1302
1341
|
else if (skillsRegistry && skillsRegistry.catalog.size > 0) ctx.useContext({
|
|
1303
|
-
sourceBudget
|
|
1342
|
+
sourceBudget,
|
|
1304
1343
|
sources: {
|
|
1305
|
-
|
|
1306
|
-
content: () => skillsRegistry.renderCatalog(2e3),
|
|
1307
|
-
max: 2e3,
|
|
1308
|
-
cache: `stable`
|
|
1309
|
-
},
|
|
1344
|
+
...loadedSkills.sources,
|
|
1310
1345
|
conversation: {
|
|
1311
1346
|
content: () => ctx.timelineMessages(),
|
|
1312
1347
|
cache: `volatile`
|
|
@@ -1319,7 +1354,7 @@ function createAssistantHandler(options) {
|
|
|
1319
1354
|
}
|
|
1320
1355
|
});
|
|
1321
1356
|
else if (agentsMd) ctx.useContext({
|
|
1322
|
-
sourceBudget
|
|
1357
|
+
sourceBudget,
|
|
1323
1358
|
sources: {
|
|
1324
1359
|
conversation: {
|
|
1325
1360
|
content: () => ctx.timelineMessages(),
|
|
@@ -1376,6 +1411,16 @@ function registerHorton(registry, options) {
|
|
|
1376
1411
|
registry.define(`horton`, {
|
|
1377
1412
|
description: `Friendly capable assistant — chat, code, research, dispatch`,
|
|
1378
1413
|
creationSchema: hortonCreationSchema,
|
|
1414
|
+
permissionGrants: [{
|
|
1415
|
+
subject_kind: `principal_kind`,
|
|
1416
|
+
subject_value: `user`,
|
|
1417
|
+
permission: `spawn`
|
|
1418
|
+
}, {
|
|
1419
|
+
subject_kind: `principal_kind`,
|
|
1420
|
+
subject_value: `user`,
|
|
1421
|
+
permission: `manage`
|
|
1422
|
+
}],
|
|
1423
|
+
slashCommands: buildSkillSlashCommands(skillsRegistry),
|
|
1379
1424
|
handler: assistantHandler
|
|
1380
1425
|
});
|
|
1381
1426
|
return [`horton`];
|
|
@@ -1417,7 +1462,7 @@ function parseWorkerArgs(value) {
|
|
|
1417
1462
|
if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
|
|
1418
1463
|
return args;
|
|
1419
1464
|
}
|
|
1420
|
-
function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
1465
|
+
function buildToolsForWorker(tools, sandbox, ctx, readSet, opts) {
|
|
1421
1466
|
const out = [];
|
|
1422
1467
|
for (const name of tools) switch (name) {
|
|
1423
1468
|
case `bash`:
|
|
@@ -1436,7 +1481,10 @@ function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
|
1436
1481
|
out.push(braveSearchTool);
|
|
1437
1482
|
break;
|
|
1438
1483
|
case `fetch_url`:
|
|
1439
|
-
out.push(createFetchUrlTool(sandbox
|
|
1484
|
+
out.push(createFetchUrlTool(sandbox, {
|
|
1485
|
+
catalog: opts.modelCatalog,
|
|
1486
|
+
modelConfig: opts.modelConfig
|
|
1487
|
+
}));
|
|
1440
1488
|
break;
|
|
1441
1489
|
case `spawn_worker`:
|
|
1442
1490
|
out.push(createSpawnWorkerTool(ctx));
|
|
@@ -1547,11 +1595,23 @@ function registerWorker(registry, options) {
|
|
|
1547
1595
|
const { streamFn, modelCatalog } = options;
|
|
1548
1596
|
registry.define(`worker`, {
|
|
1549
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
|
+
}],
|
|
1550
1607
|
async handler(ctx) {
|
|
1551
1608
|
const args = parseWorkerArgs(ctx.args);
|
|
1552
1609
|
const readSet = new Set();
|
|
1553
|
-
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
|
|
1554
1610
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
|
|
1611
|
+
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet, {
|
|
1612
|
+
modelCatalog,
|
|
1613
|
+
modelConfig
|
|
1614
|
+
});
|
|
1555
1615
|
const sharedStateTools = [];
|
|
1556
1616
|
if (args.sharedDb) {
|
|
1557
1617
|
const shared = await ctx.observe(db(args.sharedDb.id, args.sharedDb.schema));
|
|
@@ -1743,6 +1803,8 @@ var BuiltinAgentsServer = class {
|
|
|
1743
1803
|
mcpToolProviderName = null;
|
|
1744
1804
|
mcpApplyInFlight = new Set();
|
|
1745
1805
|
mcpStopping = false;
|
|
1806
|
+
mcpExtras = [];
|
|
1807
|
+
mcpLastJsonConfig = null;
|
|
1746
1808
|
pullWakeRunner = null;
|
|
1747
1809
|
options;
|
|
1748
1810
|
constructor(options) {
|
|
@@ -1752,8 +1814,70 @@ var BuiltinAgentsServer = class {
|
|
|
1752
1814
|
get mcpRegistry() {
|
|
1753
1815
|
return this._mcpRegistry;
|
|
1754
1816
|
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Replace the in-memory `extras` list and re-apply the merged config
|
|
1819
|
+
* against the last-known workspace `mcp.json` state. Workspace
|
|
1820
|
+
* `mcp.json` still wins on name collision. No-op once `stop()` has
|
|
1821
|
+
* latched `mcpStopping`.
|
|
1822
|
+
*/
|
|
1823
|
+
async setExtraMcpServers(extras) {
|
|
1824
|
+
if (!this._mcpRegistry || this.mcpStopping) return;
|
|
1825
|
+
this.mcpExtras = extras;
|
|
1826
|
+
await this.applyMerged(this.mcpLastJsonConfig);
|
|
1827
|
+
}
|
|
1828
|
+
async wirePersistence(cfg) {
|
|
1829
|
+
const servers = [];
|
|
1830
|
+
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
1831
|
+
const persist = await keychainPersistence({ server: s.name });
|
|
1832
|
+
servers.push({
|
|
1833
|
+
...s,
|
|
1834
|
+
auth: {
|
|
1835
|
+
...s.auth,
|
|
1836
|
+
...persist
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
} else servers.push(s);
|
|
1840
|
+
return {
|
|
1841
|
+
...cfg,
|
|
1842
|
+
servers
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
mergeMcp(jsonCfg) {
|
|
1846
|
+
const jsonServers = jsonCfg?.servers ?? [];
|
|
1847
|
+
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
1848
|
+
const filteredExtras = this.mcpExtras.filter((s) => !jsonNames.has(s.name));
|
|
1849
|
+
return {
|
|
1850
|
+
servers: [...filteredExtras, ...jsonServers],
|
|
1851
|
+
raw: jsonCfg?.raw
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
async runApply(jsonCfg) {
|
|
1855
|
+
if (this.mcpStopping) return;
|
|
1856
|
+
const registry = this._mcpRegistry;
|
|
1857
|
+
if (!registry) return;
|
|
1858
|
+
try {
|
|
1859
|
+
const wired = await this.wirePersistence(this.mergeMcp(jsonCfg));
|
|
1860
|
+
if (this.mcpStopping) return;
|
|
1861
|
+
await registry.applyConfig(wired);
|
|
1862
|
+
} catch (e) {
|
|
1863
|
+
serverLog.error(`[mcp] applyConfig:`, e);
|
|
1864
|
+
try {
|
|
1865
|
+
this.options.onConfigError?.(e);
|
|
1866
|
+
} catch (cbErr) {
|
|
1867
|
+
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
applyMerged(jsonCfg) {
|
|
1872
|
+
this.mcpLastJsonConfig = jsonCfg;
|
|
1873
|
+
const p = this.runApply(jsonCfg);
|
|
1874
|
+
this.mcpApplyInFlight.add(p);
|
|
1875
|
+
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
1876
|
+
return p;
|
|
1877
|
+
}
|
|
1755
1878
|
async start() {
|
|
1756
1879
|
if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
|
|
1880
|
+
installDurableStreamsFetchCache(this.options.durableStreamsFetchCache);
|
|
1757
1881
|
const pullWake = this.options.pullWake;
|
|
1758
1882
|
if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
|
|
1759
1883
|
try {
|
|
@@ -1764,76 +1888,28 @@ var BuiltinAgentsServer = class {
|
|
|
1764
1888
|
});
|
|
1765
1889
|
this._mcpRegistry = mcpRegistry;
|
|
1766
1890
|
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
|
-
};
|
|
1891
|
+
this.mcpExtras = this.options.extraMcpServers ?? [];
|
|
1816
1892
|
if (mcpConfigPath) {
|
|
1817
1893
|
try {
|
|
1818
1894
|
const cfg = await loadConfig(mcpConfigPath, process.env);
|
|
1819
|
-
applyMerged(cfg);
|
|
1895
|
+
this.applyMerged(cfg);
|
|
1820
1896
|
} catch (err) {
|
|
1821
1897
|
if (err.code !== `ENOENT`) throw err;
|
|
1822
|
-
if (
|
|
1823
|
-
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${
|
|
1824
|
-
applyMerged(null);
|
|
1898
|
+
if (this.mcpExtras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
|
|
1899
|
+
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${this.mcpExtras.length} server(s) from extras`);
|
|
1900
|
+
this.applyMerged(null);
|
|
1825
1901
|
}
|
|
1826
1902
|
try {
|
|
1827
1903
|
this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
|
|
1828
|
-
onChange: (cfg) => void applyMerged(cfg),
|
|
1904
|
+
onChange: (cfg) => void this.applyMerged(cfg),
|
|
1829
1905
|
onError: (e) => serverLog.error(`[mcp] config error:`, e)
|
|
1830
1906
|
});
|
|
1831
1907
|
} catch (e) {
|
|
1832
1908
|
serverLog.error(`[mcp] config watcher failed to start:`, e);
|
|
1833
1909
|
}
|
|
1834
1910
|
} else {
|
|
1835
|
-
if (
|
|
1836
|
-
applyMerged(null);
|
|
1911
|
+
if (this.mcpExtras.length > 0) serverLog.info(`[mcp] starting with ${this.mcpExtras.length} server(s) from extras`);
|
|
1912
|
+
this.applyMerged(null);
|
|
1837
1913
|
}
|
|
1838
1914
|
this.mcpToolProviderName = `mcp`;
|
|
1839
1915
|
registerToolProvider({
|