@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/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mergeElectricPrincipalHeader } from "./server-headers-KD5yHFYT.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler,
|
|
4
|
+
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";
|
|
5
5
|
import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
|
|
6
6
|
import { chooseDefaultSandbox, isE2BAvailable, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
|
|
7
7
|
import fsSync from "node:fs";
|
|
@@ -15,6 +15,7 @@ import { load } from "sqlite-vec";
|
|
|
15
15
|
import { nanoid } from "nanoid";
|
|
16
16
|
import { getModels } from "@mariozechner/pi-ai";
|
|
17
17
|
import { bridgeMcpTool, buildPromptTools, buildResourceTools, createRegistry, keychainPersistence, loadConfig, mcp, watchConfig } from "@electric-ax/agents-mcp";
|
|
18
|
+
import { Agent, cacheStores, interceptors, setGlobalDispatcher } from "undici";
|
|
18
19
|
|
|
19
20
|
//#region src/log.ts
|
|
20
21
|
const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
|
|
@@ -865,6 +866,15 @@ async function fetchAvailableModelIds(provider) {
|
|
|
865
866
|
function knownModelsForProvider(provider) {
|
|
866
867
|
return provider === MOONSHOT_PROVIDER ? getMoonshotModels() : getModels(provider);
|
|
867
868
|
}
|
|
869
|
+
function resolveBuiltinModelContextWindow(modelConfig) {
|
|
870
|
+
const modelId = String(modelConfig.model);
|
|
871
|
+
if (modelConfig.provider === MOONSHOT_PROVIDER) return getMoonshotModel(modelId)?.contextWindow ?? null;
|
|
872
|
+
if (!modelConfig.provider) return null;
|
|
873
|
+
return knownModelsForProvider(modelConfig.provider).find((model) => model.id === modelId)?.contextWindow ?? null;
|
|
874
|
+
}
|
|
875
|
+
function resolveBuiltinModelSourceBudget(modelConfig) {
|
|
876
|
+
return resolveBuiltinModelContextWindow(modelConfig) ?? 1e5;
|
|
877
|
+
}
|
|
868
878
|
function choiceForKnownModel(provider, model) {
|
|
869
879
|
return {
|
|
870
880
|
provider,
|
|
@@ -968,6 +978,8 @@ function modelInputSchemaDefs(catalog) {
|
|
|
968
978
|
const HORTON_MODEL = `claude-sonnet-4-6`;
|
|
969
979
|
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.";
|
|
970
980
|
const TITLE_USER_PROMPT = (userMessage) => `User request:\n${userMessage}`;
|
|
981
|
+
const TITLE_GENERATION_TIMEOUT_MS = 8e3;
|
|
982
|
+
const HORTON_SKILLS_SLASH_COMMAND_OWNER = `horton:skills`;
|
|
971
983
|
const TITLE_STOP_WORDS = new Set([
|
|
972
984
|
`a`,
|
|
973
985
|
`an`,
|
|
@@ -1047,6 +1059,17 @@ function createConfiguredTitleCall(catalog, modelConfig, logPrefix) {
|
|
|
1047
1059
|
maxTokens: 64
|
|
1048
1060
|
});
|
|
1049
1061
|
}
|
|
1062
|
+
function withTimeout(promise, ms, description) {
|
|
1063
|
+
let timeout;
|
|
1064
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
1065
|
+
timeout = setTimeout(() => {
|
|
1066
|
+
reject(new Error(`${description} timed out after ${ms}ms`));
|
|
1067
|
+
}, ms);
|
|
1068
|
+
});
|
|
1069
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
1070
|
+
if (timeout) clearTimeout(timeout);
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1050
1073
|
async function generateTitle(userMessage, llmCall, onFallback) {
|
|
1051
1074
|
try {
|
|
1052
1075
|
const raw = await llmCall(TITLE_USER_PROMPT(userMessage));
|
|
@@ -1176,8 +1199,12 @@ function payloadToTitleText(payload) {
|
|
|
1176
1199
|
if (typeof payload === `string`) return payload;
|
|
1177
1200
|
if (payload == null) return ``;
|
|
1178
1201
|
if (typeof payload === `object`) {
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1202
|
+
const record = payload;
|
|
1203
|
+
const text = record.text;
|
|
1204
|
+
if (typeof text === `string`) return text;
|
|
1205
|
+
const source = record.source;
|
|
1206
|
+
if (typeof source === `string`) return source;
|
|
1207
|
+
return JSON.stringify(payload);
|
|
1181
1208
|
}
|
|
1182
1209
|
return String(payload);
|
|
1183
1210
|
}
|
|
@@ -1235,10 +1262,13 @@ async function readAgentsMd(sandbox) {
|
|
|
1235
1262
|
}
|
|
1236
1263
|
function createAssistantHandler(options) {
|
|
1237
1264
|
const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
|
|
1238
|
-
const
|
|
1265
|
+
const skillLoader = createContextSkillLoader(skillsRegistry, { slashCommandOwner: HORTON_SKILLS_SLASH_COMMAND_OWNER });
|
|
1266
|
+
const hasSkills = skillLoader.hasSkills;
|
|
1239
1267
|
return async function assistantHandler(ctx, wake) {
|
|
1268
|
+
const loadedSkills = await skillLoader.load(ctx);
|
|
1240
1269
|
const readSet = new Set();
|
|
1241
1270
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
|
|
1271
|
+
const sourceBudget = resolveBuiltinModelSourceBudget(modelConfig);
|
|
1242
1272
|
const sandboxCwd = ctx.sandbox.workingDirectory;
|
|
1243
1273
|
const agentsMd = await readAgentsMd(ctx.sandbox);
|
|
1244
1274
|
const tools = [
|
|
@@ -1249,7 +1279,7 @@ function createAssistantHandler(options) {
|
|
|
1249
1279
|
modelCatalog,
|
|
1250
1280
|
logPrefix: `[horton ${ctx.entityUrl}]`
|
|
1251
1281
|
}),
|
|
1252
|
-
...
|
|
1282
|
+
...loadedSkills.tools,
|
|
1253
1283
|
...mcp.tools()
|
|
1254
1284
|
];
|
|
1255
1285
|
const hasEventSourceTools = tools.some((tool) => getToolName(tool) === `list_event_sources`);
|
|
@@ -1258,21 +1288,22 @@ function createAssistantHandler(options) {
|
|
|
1258
1288
|
if (!firstUserMessage) return;
|
|
1259
1289
|
let title = null;
|
|
1260
1290
|
try {
|
|
1261
|
-
const result = await generateTitle(firstUserMessage, createConfiguredTitleCall(modelCatalog, modelConfig, `[horton ${ctx.entityUrl}]`), (reason) => {
|
|
1291
|
+
const result = await generateTitle(firstUserMessage, (prompt) => withTimeout(createConfiguredTitleCall(modelCatalog, modelConfig, `[horton ${ctx.entityUrl}]`)(prompt), TITLE_GENERATION_TIMEOUT_MS, `title generation`), (reason) => {
|
|
1262
1292
|
serverLog.warn(`[horton ${ctx.entityUrl}] title generation fell back to local title: ${reason}`);
|
|
1263
1293
|
});
|
|
1264
1294
|
if (result.length > 0) title = result;
|
|
1265
1295
|
} catch (err) {
|
|
1266
1296
|
serverLog.warn(`[horton ${ctx.entityUrl}] title generation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1297
|
+
title = buildFallbackTitle(firstUserMessage);
|
|
1267
1298
|
}
|
|
1268
1299
|
if (title !== null) try {
|
|
1269
|
-
await ctx.setTag(`title`, title);
|
|
1300
|
+
await withTimeout(ctx.setTag(`title`, title), TITLE_GENERATION_TIMEOUT_MS, `set title tag`);
|
|
1270
1301
|
} catch (err) {
|
|
1271
1302
|
serverLog.warn(`[horton ${ctx.entityUrl}] setTag failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1272
1303
|
}
|
|
1273
1304
|
})() : Promise.resolve();
|
|
1274
1305
|
if (docsSupport) ctx.useContext({
|
|
1275
|
-
sourceBudget
|
|
1306
|
+
sourceBudget,
|
|
1276
1307
|
sources: {
|
|
1277
1308
|
docs_toc: {
|
|
1278
1309
|
content: () => docsSupport.renderCompressedToc(),
|
|
@@ -1293,21 +1324,13 @@ function createAssistantHandler(options) {
|
|
|
1293
1324
|
max: 2e4,
|
|
1294
1325
|
cache: `stable`
|
|
1295
1326
|
} } : {},
|
|
1296
|
-
...skillsRegistry && skillsRegistry.catalog.size > 0 ?
|
|
1297
|
-
content: () => skillsRegistry.renderCatalog(2e3),
|
|
1298
|
-
max: 2e3,
|
|
1299
|
-
cache: `stable`
|
|
1300
|
-
} } : {}
|
|
1327
|
+
...skillsRegistry && skillsRegistry.catalog.size > 0 ? loadedSkills.sources : {}
|
|
1301
1328
|
}
|
|
1302
1329
|
});
|
|
1303
1330
|
else if (skillsRegistry && skillsRegistry.catalog.size > 0) ctx.useContext({
|
|
1304
|
-
sourceBudget
|
|
1331
|
+
sourceBudget,
|
|
1305
1332
|
sources: {
|
|
1306
|
-
|
|
1307
|
-
content: () => skillsRegistry.renderCatalog(2e3),
|
|
1308
|
-
max: 2e3,
|
|
1309
|
-
cache: `stable`
|
|
1310
|
-
},
|
|
1333
|
+
...loadedSkills.sources,
|
|
1311
1334
|
conversation: {
|
|
1312
1335
|
content: () => ctx.timelineMessages(),
|
|
1313
1336
|
cache: `volatile`
|
|
@@ -1320,7 +1343,7 @@ function createAssistantHandler(options) {
|
|
|
1320
1343
|
}
|
|
1321
1344
|
});
|
|
1322
1345
|
else if (agentsMd) ctx.useContext({
|
|
1323
|
-
sourceBudget
|
|
1346
|
+
sourceBudget,
|
|
1324
1347
|
sources: {
|
|
1325
1348
|
conversation: {
|
|
1326
1349
|
content: () => ctx.timelineMessages(),
|
|
@@ -1377,6 +1400,16 @@ function registerHorton(registry, options) {
|
|
|
1377
1400
|
registry.define(`horton`, {
|
|
1378
1401
|
description: `Friendly capable assistant — chat, code, research, dispatch`,
|
|
1379
1402
|
creationSchema: hortonCreationSchema,
|
|
1403
|
+
permissionGrants: [{
|
|
1404
|
+
subject_kind: `principal_kind`,
|
|
1405
|
+
subject_value: `user`,
|
|
1406
|
+
permission: `spawn`
|
|
1407
|
+
}, {
|
|
1408
|
+
subject_kind: `principal_kind`,
|
|
1409
|
+
subject_value: `user`,
|
|
1410
|
+
permission: `manage`
|
|
1411
|
+
}],
|
|
1412
|
+
slashCommands: buildSkillSlashCommands(skillsRegistry),
|
|
1380
1413
|
handler: assistantHandler
|
|
1381
1414
|
});
|
|
1382
1415
|
return [`horton`];
|
|
@@ -1418,7 +1451,7 @@ function parseWorkerArgs(value) {
|
|
|
1418
1451
|
if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
|
|
1419
1452
|
return args;
|
|
1420
1453
|
}
|
|
1421
|
-
function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
1454
|
+
function buildToolsForWorker(tools, sandbox, ctx, readSet, opts) {
|
|
1422
1455
|
const out = [];
|
|
1423
1456
|
for (const name of tools) switch (name) {
|
|
1424
1457
|
case `bash`:
|
|
@@ -1437,7 +1470,10 @@ function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
|
1437
1470
|
out.push(braveSearchTool$1);
|
|
1438
1471
|
break;
|
|
1439
1472
|
case `fetch_url`:
|
|
1440
|
-
out.push(createFetchUrlTool(sandbox
|
|
1473
|
+
out.push(createFetchUrlTool(sandbox, {
|
|
1474
|
+
catalog: opts.modelCatalog,
|
|
1475
|
+
modelConfig: opts.modelConfig
|
|
1476
|
+
}));
|
|
1441
1477
|
break;
|
|
1442
1478
|
case `spawn_worker`:
|
|
1443
1479
|
out.push(createSpawnWorkerTool(ctx));
|
|
@@ -1548,11 +1584,23 @@ function registerWorker(registry, options) {
|
|
|
1548
1584
|
const { streamFn, modelCatalog } = options;
|
|
1549
1585
|
registry.define(`worker`, {
|
|
1550
1586
|
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
|
|
1587
|
+
permissionGrants: [{
|
|
1588
|
+
subject_kind: `principal_kind`,
|
|
1589
|
+
subject_value: `user`,
|
|
1590
|
+
permission: `spawn`
|
|
1591
|
+
}, {
|
|
1592
|
+
subject_kind: `principal_kind`,
|
|
1593
|
+
subject_value: `user`,
|
|
1594
|
+
permission: `manage`
|
|
1595
|
+
}],
|
|
1551
1596
|
async handler(ctx) {
|
|
1552
1597
|
const args = parseWorkerArgs(ctx.args);
|
|
1553
1598
|
const readSet = new Set();
|
|
1554
|
-
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
|
|
1555
1599
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
|
|
1600
|
+
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet, {
|
|
1601
|
+
modelCatalog,
|
|
1602
|
+
modelConfig
|
|
1603
|
+
});
|
|
1556
1604
|
const sharedStateTools = [];
|
|
1557
1605
|
if (args.sharedDb) {
|
|
1558
1606
|
const shared = await ctx.observe(db(args.sharedDb.id, args.sharedDb.schema));
|
|
@@ -1746,6 +1794,18 @@ function resolveCwd(args, fallback) {
|
|
|
1746
1794
|
return readWorkingDirectoryArg(args) ?? fallback;
|
|
1747
1795
|
}
|
|
1748
1796
|
|
|
1797
|
+
//#endregion
|
|
1798
|
+
//#region src/durable-streams-cache.ts
|
|
1799
|
+
const MEMORY_CACHE_SIZE_BYTES = 100 * 1024 * 1024;
|
|
1800
|
+
function installDurableStreamsFetchCache(options = {}) {
|
|
1801
|
+
if (options === false) return;
|
|
1802
|
+
const store = options.store === `sqlite` || options.sqliteLocation ? new cacheStores.SqliteCacheStore({
|
|
1803
|
+
location: options.sqliteLocation,
|
|
1804
|
+
maxCount: options.maxCount
|
|
1805
|
+
}) : new cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
|
|
1806
|
+
setGlobalDispatcher(new Agent().compose(interceptors.cache({ store })));
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1749
1809
|
//#endregion
|
|
1750
1810
|
//#region src/server.ts
|
|
1751
1811
|
var BuiltinAgentsServer = class {
|
|
@@ -1755,6 +1815,8 @@ var BuiltinAgentsServer = class {
|
|
|
1755
1815
|
mcpToolProviderName = null;
|
|
1756
1816
|
mcpApplyInFlight = new Set();
|
|
1757
1817
|
mcpStopping = false;
|
|
1818
|
+
mcpExtras = [];
|
|
1819
|
+
mcpLastJsonConfig = null;
|
|
1758
1820
|
pullWakeRunner = null;
|
|
1759
1821
|
options;
|
|
1760
1822
|
constructor(options) {
|
|
@@ -1764,8 +1826,70 @@ var BuiltinAgentsServer = class {
|
|
|
1764
1826
|
get mcpRegistry() {
|
|
1765
1827
|
return this._mcpRegistry;
|
|
1766
1828
|
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Replace the in-memory `extras` list and re-apply the merged config
|
|
1831
|
+
* against the last-known workspace `mcp.json` state. Workspace
|
|
1832
|
+
* `mcp.json` still wins on name collision. No-op once `stop()` has
|
|
1833
|
+
* latched `mcpStopping`.
|
|
1834
|
+
*/
|
|
1835
|
+
async setExtraMcpServers(extras) {
|
|
1836
|
+
if (!this._mcpRegistry || this.mcpStopping) return;
|
|
1837
|
+
this.mcpExtras = extras;
|
|
1838
|
+
await this.applyMerged(this.mcpLastJsonConfig);
|
|
1839
|
+
}
|
|
1840
|
+
async wirePersistence(cfg) {
|
|
1841
|
+
const servers = [];
|
|
1842
|
+
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
1843
|
+
const persist = await keychainPersistence({ server: s.name });
|
|
1844
|
+
servers.push({
|
|
1845
|
+
...s,
|
|
1846
|
+
auth: {
|
|
1847
|
+
...s.auth,
|
|
1848
|
+
...persist
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
} else servers.push(s);
|
|
1852
|
+
return {
|
|
1853
|
+
...cfg,
|
|
1854
|
+
servers
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
mergeMcp(jsonCfg) {
|
|
1858
|
+
const jsonServers = jsonCfg?.servers ?? [];
|
|
1859
|
+
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
1860
|
+
const filteredExtras = this.mcpExtras.filter((s) => !jsonNames.has(s.name));
|
|
1861
|
+
return {
|
|
1862
|
+
servers: [...filteredExtras, ...jsonServers],
|
|
1863
|
+
raw: jsonCfg?.raw
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
async runApply(jsonCfg) {
|
|
1867
|
+
if (this.mcpStopping) return;
|
|
1868
|
+
const registry = this._mcpRegistry;
|
|
1869
|
+
if (!registry) return;
|
|
1870
|
+
try {
|
|
1871
|
+
const wired = await this.wirePersistence(this.mergeMcp(jsonCfg));
|
|
1872
|
+
if (this.mcpStopping) return;
|
|
1873
|
+
await registry.applyConfig(wired);
|
|
1874
|
+
} catch (e) {
|
|
1875
|
+
serverLog.error(`[mcp] applyConfig:`, e);
|
|
1876
|
+
try {
|
|
1877
|
+
this.options.onConfigError?.(e);
|
|
1878
|
+
} catch (cbErr) {
|
|
1879
|
+
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
applyMerged(jsonCfg) {
|
|
1884
|
+
this.mcpLastJsonConfig = jsonCfg;
|
|
1885
|
+
const p = this.runApply(jsonCfg);
|
|
1886
|
+
this.mcpApplyInFlight.add(p);
|
|
1887
|
+
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
1888
|
+
return p;
|
|
1889
|
+
}
|
|
1767
1890
|
async start() {
|
|
1768
1891
|
if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
|
|
1892
|
+
installDurableStreamsFetchCache(this.options.durableStreamsFetchCache);
|
|
1769
1893
|
const pullWake = this.options.pullWake;
|
|
1770
1894
|
if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
|
|
1771
1895
|
try {
|
|
@@ -1776,76 +1900,28 @@ var BuiltinAgentsServer = class {
|
|
|
1776
1900
|
});
|
|
1777
1901
|
this._mcpRegistry = mcpRegistry;
|
|
1778
1902
|
const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
|
|
1779
|
-
|
|
1780
|
-
const wirePersistence = async (cfg) => {
|
|
1781
|
-
const servers = [];
|
|
1782
|
-
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
1783
|
-
const persist = await keychainPersistence({ server: s.name });
|
|
1784
|
-
servers.push({
|
|
1785
|
-
...s,
|
|
1786
|
-
auth: {
|
|
1787
|
-
...s.auth,
|
|
1788
|
-
...persist
|
|
1789
|
-
}
|
|
1790
|
-
});
|
|
1791
|
-
} else servers.push(s);
|
|
1792
|
-
return {
|
|
1793
|
-
...cfg,
|
|
1794
|
-
servers
|
|
1795
|
-
};
|
|
1796
|
-
};
|
|
1797
|
-
const merge = (jsonCfg) => {
|
|
1798
|
-
const jsonServers = jsonCfg?.servers ?? [];
|
|
1799
|
-
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
1800
|
-
const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
|
|
1801
|
-
return {
|
|
1802
|
-
servers: [...filteredExtras, ...jsonServers],
|
|
1803
|
-
raw: jsonCfg?.raw
|
|
1804
|
-
};
|
|
1805
|
-
};
|
|
1806
|
-
const onConfigError = this.options.onConfigError;
|
|
1807
|
-
const runApply = async (jsonCfg) => {
|
|
1808
|
-
if (this.mcpStopping) return;
|
|
1809
|
-
try {
|
|
1810
|
-
const wired = await wirePersistence(merge(jsonCfg));
|
|
1811
|
-
if (this.mcpStopping) return;
|
|
1812
|
-
await mcpRegistry.applyConfig(wired);
|
|
1813
|
-
} catch (e) {
|
|
1814
|
-
serverLog.error(`[mcp] applyConfig:`, e);
|
|
1815
|
-
try {
|
|
1816
|
-
onConfigError?.(e);
|
|
1817
|
-
} catch (cbErr) {
|
|
1818
|
-
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
};
|
|
1822
|
-
const applyMerged = (jsonCfg) => {
|
|
1823
|
-
const p = runApply(jsonCfg);
|
|
1824
|
-
this.mcpApplyInFlight.add(p);
|
|
1825
|
-
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
1826
|
-
return p;
|
|
1827
|
-
};
|
|
1903
|
+
this.mcpExtras = this.options.extraMcpServers ?? [];
|
|
1828
1904
|
if (mcpConfigPath) {
|
|
1829
1905
|
try {
|
|
1830
1906
|
const cfg = await loadConfig(mcpConfigPath, process.env);
|
|
1831
|
-
applyMerged(cfg);
|
|
1907
|
+
this.applyMerged(cfg);
|
|
1832
1908
|
} catch (err) {
|
|
1833
1909
|
if (err.code !== `ENOENT`) throw err;
|
|
1834
|
-
if (
|
|
1835
|
-
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${
|
|
1836
|
-
applyMerged(null);
|
|
1910
|
+
if (this.mcpExtras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
|
|
1911
|
+
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${this.mcpExtras.length} server(s) from extras`);
|
|
1912
|
+
this.applyMerged(null);
|
|
1837
1913
|
}
|
|
1838
1914
|
try {
|
|
1839
1915
|
this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
|
|
1840
|
-
onChange: (cfg) => void applyMerged(cfg),
|
|
1916
|
+
onChange: (cfg) => void this.applyMerged(cfg),
|
|
1841
1917
|
onError: (e) => serverLog.error(`[mcp] config error:`, e)
|
|
1842
1918
|
});
|
|
1843
1919
|
} catch (e) {
|
|
1844
1920
|
serverLog.error(`[mcp] config watcher failed to start:`, e);
|
|
1845
1921
|
}
|
|
1846
1922
|
} else {
|
|
1847
|
-
if (
|
|
1848
|
-
applyMerged(null);
|
|
1923
|
+
if (this.mcpExtras.length > 0) serverLog.info(`[mcp] starting with ${this.mcpExtras.length} server(s) from extras`);
|
|
1924
|
+
this.applyMerged(null);
|
|
1849
1925
|
}
|
|
1850
1926
|
this.mcpToolProviderName = `mcp`;
|
|
1851
1927
|
registerToolProvider({
|
|
@@ -81,7 +81,7 @@ const proWorker = await ctx.spawn(
|
|
|
81
81
|
systemPrompt: PRO_WORKER_PROMPT,
|
|
82
82
|
sharedDb: { id: `debate-${ctx.entityUrl}`, schema: debateSchema },
|
|
83
83
|
},
|
|
84
|
-
{ initialMessage: proInitialMessage, wake: `runFinished
|
|
84
|
+
{ initialMessage: proInitialMessage, wake: { on: `runFinished`, includeResponse: true } }
|
|
85
85
|
)
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -47,7 +47,7 @@ The dispatcher exposes a `dispatch` tool. When the LLM classifies an incoming me
|
|
|
47
47
|
|
|
48
48
|
The tool then:
|
|
49
49
|
|
|
50
|
-
1. Spawns the requested entity type with `wake: 'runFinished'`.
|
|
50
|
+
1. Spawns the requested entity type with `wake: { on: 'runFinished', includeResponse: true }`.
|
|
51
51
|
2. Returns immediately with a status message. The dispatcher is re-invoked when the specialist finishes.
|
|
52
52
|
|
|
53
53
|
## Dispatch tool
|
|
@@ -59,7 +59,7 @@ await ctx.spawn(
|
|
|
59
59
|
type === `worker` ? { systemPrompt, tools: [`read`] } : { systemPrompt },
|
|
60
60
|
{
|
|
61
61
|
initialMessage: task,
|
|
62
|
-
wake: `runFinished`,
|
|
62
|
+
wake: { on: `runFinished`, includeResponse: true },
|
|
63
63
|
}
|
|
64
64
|
)
|
|
65
65
|
|
|
@@ -67,7 +67,7 @@ return {
|
|
|
67
67
|
content: [
|
|
68
68
|
{
|
|
69
69
|
type: `text` as const,
|
|
70
|
-
text: `Dispatched to "${type}" specialist (${id}).
|
|
70
|
+
text: `Dispatched to "${type}" specialist (${id}). The dispatcher will continue when it finishes.`,
|
|
71
71
|
},
|
|
72
72
|
],
|
|
73
73
|
details: { id, type },
|
|
@@ -42,9 +42,9 @@ The manager defines a handler-scoped tool called `analyze_with_perspectives`. Wh
|
|
|
42
42
|
|
|
43
43
|
1. Spawns 3 worker children -- optimist, pessimist, pragmatist -- each with a different system prompt.
|
|
44
44
|
2. Sends the same question to all three as `initialMessage`.
|
|
45
|
-
3. Uses `wake: 'runFinished'`
|
|
46
|
-
4. Collects results
|
|
47
|
-
5.
|
|
45
|
+
3. Uses `wake: { on: 'runFinished', includeResponse: true }` so the manager is re-invoked as each child completes.
|
|
46
|
+
4. Collects results from `runFinished` wake payloads or shared state after workers finish.
|
|
47
|
+
5. Runs a synthesis step after all child-completion wakes have been recorded.
|
|
48
48
|
|
|
49
49
|
On subsequent calls, the tool reuses existing children via `ctx.observe()` and `child.send()` instead of spawning new ones.
|
|
50
50
|
|
|
@@ -63,7 +63,7 @@ for (const perspective of PERSPECTIVES) {
|
|
|
63
63
|
`worker`,
|
|
64
64
|
childId,
|
|
65
65
|
{ systemPrompt: perspective.systemPrompt, tools: [`read`] },
|
|
66
|
-
{ initialMessage: question, wake: `runFinished
|
|
66
|
+
{ initialMessage: question, wake: { on: `runFinished`, includeResponse: true } }
|
|
67
67
|
)
|
|
68
68
|
children.insert({
|
|
69
69
|
key: perspective.id,
|
|
@@ -87,28 +87,16 @@ for (const perspective of PERSPECTIVES) {
|
|
|
87
87
|
|
|
88
88
|
## Collecting results
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
Do not wait for worker output inside the same wake. Spawn workers with `wake: { on: "runFinished", includeResponse: true }`, record each worker URL in manager state, and return. On each later child-completion wake, store `wake.payload.finished_child.response` (or read structured output from shared state). Once all workers have reported, run the reduce/synthesis step.
|
|
91
91
|
|
|
92
92
|
```ts
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const latest = runs[runs.length - 1]?.trim()
|
|
100
|
-
return latest || fallback
|
|
93
|
+
const finished = wake.payload?.finished_child
|
|
94
|
+
if (finished) {
|
|
95
|
+
ctx.state.workers.update(finished.url, (draft) => {
|
|
96
|
+
draft.status = finished.run_status
|
|
97
|
+
draft.output = finished.response ?? ""
|
|
98
|
+
})
|
|
101
99
|
}
|
|
102
|
-
|
|
103
|
-
const results = await Promise.all(
|
|
104
|
-
handles.map(async ({ id, handle }) => ({
|
|
105
|
-
id,
|
|
106
|
-
text: await readLatestCompletedText(
|
|
107
|
-
handle,
|
|
108
|
-
`(no completed output from ${id})`
|
|
109
|
-
),
|
|
110
|
-
}))
|
|
111
|
-
)
|
|
112
100
|
```
|
|
113
101
|
|
|
114
102
|
## State
|
|
@@ -39,7 +39,7 @@ export function registerPipeline(registry: EntityRegistry) {
|
|
|
39
39
|
The pipeline agent exposes a `run_stage` tool. The LLM drives the pipeline one stage at a time:
|
|
40
40
|
|
|
41
41
|
1. The LLM calls `run_stage` with an instruction and input for the current stage.
|
|
42
|
-
2. The tool spawns a worker with the instruction as its system prompt and the input as `initialMessage`, using `wake: 'runFinished'`.
|
|
42
|
+
2. The tool spawns a worker with the instruction as its system prompt and the input as `initialMessage`, using `wake: { on: 'runFinished', includeResponse: true }`.
|
|
43
43
|
3. The tool returns immediately. The pipeline entity is re-invoked when the worker finishes.
|
|
44
44
|
4. On each re-invocation, the wake event contains `finished_child.response` with the stage's output. The LLM then calls `run_stage` again with the next stage's instruction and the previous output as input.
|
|
45
45
|
5. This repeats until all stages are complete.
|
|
@@ -74,7 +74,7 @@ function createRunStageTool(ctx: HandlerContext): AgentTool {
|
|
|
74
74
|
`worker`,
|
|
75
75
|
id,
|
|
76
76
|
{ systemPrompt: instruction, tools: [`read`] },
|
|
77
|
-
{ initialMessage: input, wake: `runFinished
|
|
77
|
+
{ initialMessage: input, wake: { on: `runFinished`, includeResponse: true } }
|
|
78
78
|
)
|
|
79
79
|
ctx.db.actions.children_insert({
|
|
80
80
|
row: { key: id, url: child.entityUrl, stage: stageCount },
|
|
@@ -84,7 +84,7 @@ function createRunStageTool(ctx: HandlerContext): AgentTool {
|
|
|
84
84
|
content: [
|
|
85
85
|
{
|
|
86
86
|
type: `text` as const,
|
|
87
|
-
text: `Stage ${stageCount} spawned.
|
|
87
|
+
text: `Stage ${stageCount} spawned. The pipeline will continue when it finishes.`,
|
|
88
88
|
},
|
|
89
89
|
],
|
|
90
90
|
details: { stage: stageCount },
|