@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/index.d.ts
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;
|
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, createSkillTools, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
4
|
+
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";
|
|
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,
|
|
@@ -1239,6 +1249,7 @@ function createAssistantHandler(options) {
|
|
|
1239
1249
|
return async function assistantHandler(ctx, wake) {
|
|
1240
1250
|
const readSet = new Set();
|
|
1241
1251
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
|
|
1252
|
+
const sourceBudget = resolveBuiltinModelSourceBudget(modelConfig);
|
|
1242
1253
|
const sandboxCwd = ctx.sandbox.workingDirectory;
|
|
1243
1254
|
const agentsMd = await readAgentsMd(ctx.sandbox);
|
|
1244
1255
|
const tools = [
|
|
@@ -1272,7 +1283,7 @@ function createAssistantHandler(options) {
|
|
|
1272
1283
|
}
|
|
1273
1284
|
})() : Promise.resolve();
|
|
1274
1285
|
if (docsSupport) ctx.useContext({
|
|
1275
|
-
sourceBudget
|
|
1286
|
+
sourceBudget,
|
|
1276
1287
|
sources: {
|
|
1277
1288
|
docs_toc: {
|
|
1278
1289
|
content: () => docsSupport.renderCompressedToc(),
|
|
@@ -1301,7 +1312,7 @@ function createAssistantHandler(options) {
|
|
|
1301
1312
|
}
|
|
1302
1313
|
});
|
|
1303
1314
|
else if (skillsRegistry && skillsRegistry.catalog.size > 0) ctx.useContext({
|
|
1304
|
-
sourceBudget
|
|
1315
|
+
sourceBudget,
|
|
1305
1316
|
sources: {
|
|
1306
1317
|
skills_catalog: {
|
|
1307
1318
|
content: () => skillsRegistry.renderCatalog(2e3),
|
|
@@ -1320,7 +1331,7 @@ function createAssistantHandler(options) {
|
|
|
1320
1331
|
}
|
|
1321
1332
|
});
|
|
1322
1333
|
else if (agentsMd) ctx.useContext({
|
|
1323
|
-
sourceBudget
|
|
1334
|
+
sourceBudget,
|
|
1324
1335
|
sources: {
|
|
1325
1336
|
conversation: {
|
|
1326
1337
|
content: () => ctx.timelineMessages(),
|
|
@@ -1377,6 +1388,15 @@ function registerHorton(registry, options) {
|
|
|
1377
1388
|
registry.define(`horton`, {
|
|
1378
1389
|
description: `Friendly capable assistant — chat, code, research, dispatch`,
|
|
1379
1390
|
creationSchema: hortonCreationSchema,
|
|
1391
|
+
permissionGrants: [{
|
|
1392
|
+
subject_kind: `principal_kind`,
|
|
1393
|
+
subject_value: `user`,
|
|
1394
|
+
permission: `spawn`
|
|
1395
|
+
}, {
|
|
1396
|
+
subject_kind: `principal_kind`,
|
|
1397
|
+
subject_value: `user`,
|
|
1398
|
+
permission: `manage`
|
|
1399
|
+
}],
|
|
1380
1400
|
handler: assistantHandler
|
|
1381
1401
|
});
|
|
1382
1402
|
return [`horton`];
|
|
@@ -1418,7 +1438,7 @@ function parseWorkerArgs(value) {
|
|
|
1418
1438
|
if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
|
|
1419
1439
|
return args;
|
|
1420
1440
|
}
|
|
1421
|
-
function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
1441
|
+
function buildToolsForWorker(tools, sandbox, ctx, readSet, opts) {
|
|
1422
1442
|
const out = [];
|
|
1423
1443
|
for (const name of tools) switch (name) {
|
|
1424
1444
|
case `bash`:
|
|
@@ -1437,7 +1457,10 @@ function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
|
1437
1457
|
out.push(braveSearchTool$1);
|
|
1438
1458
|
break;
|
|
1439
1459
|
case `fetch_url`:
|
|
1440
|
-
out.push(createFetchUrlTool(sandbox
|
|
1460
|
+
out.push(createFetchUrlTool(sandbox, {
|
|
1461
|
+
catalog: opts.modelCatalog,
|
|
1462
|
+
modelConfig: opts.modelConfig
|
|
1463
|
+
}));
|
|
1441
1464
|
break;
|
|
1442
1465
|
case `spawn_worker`:
|
|
1443
1466
|
out.push(createSpawnWorkerTool(ctx));
|
|
@@ -1548,11 +1571,23 @@ function registerWorker(registry, options) {
|
|
|
1548
1571
|
const { streamFn, modelCatalog } = options;
|
|
1549
1572
|
registry.define(`worker`, {
|
|
1550
1573
|
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
|
|
1574
|
+
permissionGrants: [{
|
|
1575
|
+
subject_kind: `principal_kind`,
|
|
1576
|
+
subject_value: `user`,
|
|
1577
|
+
permission: `spawn`
|
|
1578
|
+
}, {
|
|
1579
|
+
subject_kind: `principal_kind`,
|
|
1580
|
+
subject_value: `user`,
|
|
1581
|
+
permission: `manage`
|
|
1582
|
+
}],
|
|
1551
1583
|
async handler(ctx) {
|
|
1552
1584
|
const args = parseWorkerArgs(ctx.args);
|
|
1553
1585
|
const readSet = new Set();
|
|
1554
|
-
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
|
|
1555
1586
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
|
|
1587
|
+
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet, {
|
|
1588
|
+
modelCatalog,
|
|
1589
|
+
modelConfig
|
|
1590
|
+
});
|
|
1556
1591
|
const sharedStateTools = [];
|
|
1557
1592
|
if (args.sharedDb) {
|
|
1558
1593
|
const shared = await ctx.observe(db(args.sharedDb.id, args.sharedDb.schema));
|
|
@@ -1746,6 +1781,18 @@ function resolveCwd(args, fallback) {
|
|
|
1746
1781
|
return readWorkingDirectoryArg(args) ?? fallback;
|
|
1747
1782
|
}
|
|
1748
1783
|
|
|
1784
|
+
//#endregion
|
|
1785
|
+
//#region src/durable-streams-cache.ts
|
|
1786
|
+
const MEMORY_CACHE_SIZE_BYTES = 100 * 1024 * 1024;
|
|
1787
|
+
function installDurableStreamsFetchCache(options = {}) {
|
|
1788
|
+
if (options === false) return;
|
|
1789
|
+
const store = options.store === `sqlite` || options.sqliteLocation ? new cacheStores.SqliteCacheStore({
|
|
1790
|
+
location: options.sqliteLocation,
|
|
1791
|
+
maxCount: options.maxCount
|
|
1792
|
+
}) : new cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
|
|
1793
|
+
setGlobalDispatcher(new Agent().compose(interceptors.cache({ store })));
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1749
1796
|
//#endregion
|
|
1750
1797
|
//#region src/server.ts
|
|
1751
1798
|
var BuiltinAgentsServer = class {
|
|
@@ -1755,6 +1802,8 @@ var BuiltinAgentsServer = class {
|
|
|
1755
1802
|
mcpToolProviderName = null;
|
|
1756
1803
|
mcpApplyInFlight = new Set();
|
|
1757
1804
|
mcpStopping = false;
|
|
1805
|
+
mcpExtras = [];
|
|
1806
|
+
mcpLastJsonConfig = null;
|
|
1758
1807
|
pullWakeRunner = null;
|
|
1759
1808
|
options;
|
|
1760
1809
|
constructor(options) {
|
|
@@ -1764,8 +1813,70 @@ var BuiltinAgentsServer = class {
|
|
|
1764
1813
|
get mcpRegistry() {
|
|
1765
1814
|
return this._mcpRegistry;
|
|
1766
1815
|
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Replace the in-memory `extras` list and re-apply the merged config
|
|
1818
|
+
* against the last-known workspace `mcp.json` state. Workspace
|
|
1819
|
+
* `mcp.json` still wins on name collision. No-op once `stop()` has
|
|
1820
|
+
* latched `mcpStopping`.
|
|
1821
|
+
*/
|
|
1822
|
+
async setExtraMcpServers(extras) {
|
|
1823
|
+
if (!this._mcpRegistry || this.mcpStopping) return;
|
|
1824
|
+
this.mcpExtras = extras;
|
|
1825
|
+
await this.applyMerged(this.mcpLastJsonConfig);
|
|
1826
|
+
}
|
|
1827
|
+
async wirePersistence(cfg) {
|
|
1828
|
+
const servers = [];
|
|
1829
|
+
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
1830
|
+
const persist = await keychainPersistence({ server: s.name });
|
|
1831
|
+
servers.push({
|
|
1832
|
+
...s,
|
|
1833
|
+
auth: {
|
|
1834
|
+
...s.auth,
|
|
1835
|
+
...persist
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
} else servers.push(s);
|
|
1839
|
+
return {
|
|
1840
|
+
...cfg,
|
|
1841
|
+
servers
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
mergeMcp(jsonCfg) {
|
|
1845
|
+
const jsonServers = jsonCfg?.servers ?? [];
|
|
1846
|
+
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
1847
|
+
const filteredExtras = this.mcpExtras.filter((s) => !jsonNames.has(s.name));
|
|
1848
|
+
return {
|
|
1849
|
+
servers: [...filteredExtras, ...jsonServers],
|
|
1850
|
+
raw: jsonCfg?.raw
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
async runApply(jsonCfg) {
|
|
1854
|
+
if (this.mcpStopping) return;
|
|
1855
|
+
const registry = this._mcpRegistry;
|
|
1856
|
+
if (!registry) return;
|
|
1857
|
+
try {
|
|
1858
|
+
const wired = await this.wirePersistence(this.mergeMcp(jsonCfg));
|
|
1859
|
+
if (this.mcpStopping) return;
|
|
1860
|
+
await registry.applyConfig(wired);
|
|
1861
|
+
} catch (e) {
|
|
1862
|
+
serverLog.error(`[mcp] applyConfig:`, e);
|
|
1863
|
+
try {
|
|
1864
|
+
this.options.onConfigError?.(e);
|
|
1865
|
+
} catch (cbErr) {
|
|
1866
|
+
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
applyMerged(jsonCfg) {
|
|
1871
|
+
this.mcpLastJsonConfig = jsonCfg;
|
|
1872
|
+
const p = this.runApply(jsonCfg);
|
|
1873
|
+
this.mcpApplyInFlight.add(p);
|
|
1874
|
+
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
1875
|
+
return p;
|
|
1876
|
+
}
|
|
1767
1877
|
async start() {
|
|
1768
1878
|
if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
|
|
1879
|
+
installDurableStreamsFetchCache(this.options.durableStreamsFetchCache);
|
|
1769
1880
|
const pullWake = this.options.pullWake;
|
|
1770
1881
|
if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
|
|
1771
1882
|
try {
|
|
@@ -1776,76 +1887,28 @@ var BuiltinAgentsServer = class {
|
|
|
1776
1887
|
});
|
|
1777
1888
|
this._mcpRegistry = mcpRegistry;
|
|
1778
1889
|
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
|
-
};
|
|
1890
|
+
this.mcpExtras = this.options.extraMcpServers ?? [];
|
|
1828
1891
|
if (mcpConfigPath) {
|
|
1829
1892
|
try {
|
|
1830
1893
|
const cfg = await loadConfig(mcpConfigPath, process.env);
|
|
1831
|
-
applyMerged(cfg);
|
|
1894
|
+
this.applyMerged(cfg);
|
|
1832
1895
|
} catch (err) {
|
|
1833
1896
|
if (err.code !== `ENOENT`) throw err;
|
|
1834
|
-
if (
|
|
1835
|
-
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${
|
|
1836
|
-
applyMerged(null);
|
|
1897
|
+
if (this.mcpExtras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
|
|
1898
|
+
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${this.mcpExtras.length} server(s) from extras`);
|
|
1899
|
+
this.applyMerged(null);
|
|
1837
1900
|
}
|
|
1838
1901
|
try {
|
|
1839
1902
|
this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
|
|
1840
|
-
onChange: (cfg) => void applyMerged(cfg),
|
|
1903
|
+
onChange: (cfg) => void this.applyMerged(cfg),
|
|
1841
1904
|
onError: (e) => serverLog.error(`[mcp] config error:`, e)
|
|
1842
1905
|
});
|
|
1843
1906
|
} catch (e) {
|
|
1844
1907
|
serverLog.error(`[mcp] config watcher failed to start:`, e);
|
|
1845
1908
|
}
|
|
1846
1909
|
} else {
|
|
1847
|
-
if (
|
|
1848
|
-
applyMerged(null);
|
|
1910
|
+
if (this.mcpExtras.length > 0) serverLog.info(`[mcp] starting with ${this.mcpExtras.length} server(s) from extras`);
|
|
1911
|
+
this.applyMerged(null);
|
|
1849
1912
|
}
|
|
1850
1913
|
this.mcpToolProviderName = `mcp`;
|
|
1851
1914
|
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 },
|
package/docs/index.md
CHANGED
|
@@ -4,6 +4,9 @@ titleTemplate: "... - Electric Agents"
|
|
|
4
4
|
description: >-
|
|
5
5
|
The durable runtime for long-lived agents — entities, handlers, wakes, agent loops, and coordination, built on Electric Streams, TanStack DB, and pi.
|
|
6
6
|
outline: [2, 3]
|
|
7
|
+
next:
|
|
8
|
+
text: 'Quickstart'
|
|
9
|
+
link: '/docs/agents/quickstart'
|
|
7
10
|
---
|
|
8
11
|
|
|
9
12
|
<script setup>
|
|
@@ -12,7 +15,16 @@ import EntityOverviewDiagram from '../../src/components/agents-home/EntityOvervi
|
|
|
12
15
|
|
|
13
16
|
# Electric Agents
|
|
14
17
|
|
|
15
|
-
Electric Agents is **the durable runtime for long-lived agents**.
|
|
18
|
+
Electric Agents is **the durable runtime for long-lived agents**.
|
|
19
|
+
|
|
20
|
+
It's a runtime and communication fabric for spawning and scaling collaborative agents <span class="no-wrap-sm">[on serverless compute](/blog/2026/06/04/serverless-agents)</span> using your existing web systems.
|
|
21
|
+
|
|
22
|
+
> [!Warning] ✨ Start using Electric Agents now
|
|
23
|
+
> See the [Quickstart](/docs/agents/quickstart) to fire the system up and try the built-in agents.
|
|
24
|
+
>
|
|
25
|
+
> Dive into the [Walkthrough](/docs/agents/walkthrough) for a step-by-step guide to building a multi-agent system.
|
|
26
|
+
|
|
27
|
+
## System overview
|
|
16
28
|
|
|
17
29
|
Each agent is an **entity** — an addressable, schema-typed unit of state at `/{type}/{id}`. An entity's session and state live on a durable [Electric Stream](/streams/) of events.
|
|
18
30
|
|
|
@@ -22,7 +34,7 @@ Every step — runs, tool calls, text deltas, state changes — is appended to t
|
|
|
22
34
|
|
|
23
35
|
<EntityOverviewDiagram />
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
See the [Usage overview](/docs/agents/usage/overview) for a summary of the developer surface in a single page.
|
|
26
38
|
|
|
27
39
|
## How it works
|
|
28
40
|
|
|
@@ -38,7 +50,7 @@ The runtime SDK is a layer over three foundations:
|
|
|
38
50
|
|
|
39
51
|
**Outside the handler.** Any app or other entity can call [`createAgentsClient().observe(entity('/type/id'))`](/docs/agents/usage/clients-and-react) to load an entity's stream into a local DB and react to changes in real time, with the same schemas and types as the handler.
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
### Entities
|
|
42
54
|
|
|
43
55
|
Use entities to model anything long-lived and addressable — an agent session, a chat thread, a research job, a coordinator, a worker. You register a **type** with [`registry.define()`](/docs/agents/reference/entity-registry) and spawn **instances** at `/{type}/{id}`. Each instance has its own state, handler, and event stream. See [Defining entities](/docs/agents/usage/defining-entities).
|
|
44
56
|
|
|
@@ -53,7 +65,7 @@ registry.define("assistant", {
|
|
|
53
65
|
})
|
|
54
66
|
```
|
|
55
67
|
|
|
56
|
-
|
|
68
|
+
### Handlers
|
|
57
69
|
|
|
58
70
|
The function that runs when an entity wakes. Receives a [`HandlerContext`](/docs/agents/reference/handler-context) (`ctx`) and a [`WakeEvent`](/docs/agents/reference/wake-event) (`wake`). The handler decides how to respond: configure an agent, update state, spawn children, or any combination. See [Writing handlers](/docs/agents/usage/writing-handlers).
|
|
59
71
|
|
|
@@ -72,9 +84,9 @@ registry.define("support", {
|
|
|
72
84
|
})
|
|
73
85
|
```
|
|
74
86
|
|
|
75
|
-
|
|
87
|
+
### Waking and notifications
|
|
76
88
|
|
|
77
|
-
Events that trigger a handler invocation. Wake sources include incoming messages, child completion, state changes, and timers (scheduled sends, cron, timeouts). The [`WakeEvent`](/docs/agents/reference/wake-event) tells the handler why it was woken.
|
|
89
|
+
Events that trigger a handler invocation. Wake sources include incoming messages, child completion, state changes, and timers (scheduled sends, cron, timeouts). The [`WakeEvent`](/docs/agents/reference/wake-event) tells the handler why it was woken.
|
|
78
90
|
|
|
79
91
|
```ts
|
|
80
92
|
async handler(ctx, wake) {
|
|
@@ -89,35 +101,9 @@ async handler(ctx, wake) {
|
|
|
89
101
|
}
|
|
90
102
|
```
|
|
91
103
|
|
|
92
|
-
|
|
104
|
+
See [Waking entities](/docs/agents/usage/waking-entities) for more information.
|
|
93
105
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
registry.define("tracker", {
|
|
98
|
-
state: {
|
|
99
|
-
items: {
|
|
100
|
-
schema: z.object({
|
|
101
|
-
key: z.string(),
|
|
102
|
-
name: z.string(),
|
|
103
|
-
done: z.boolean(),
|
|
104
|
-
}),
|
|
105
|
-
primaryKey: "key",
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
async handler(ctx) {
|
|
109
|
-
// read
|
|
110
|
-
const item = ctx.db.collections.items.get("item-1")
|
|
111
|
-
|
|
112
|
-
// write
|
|
113
|
-
ctx.db.actions.items_insert({
|
|
114
|
-
row: { key: "item-2", name: "New", done: false },
|
|
115
|
-
})
|
|
116
|
-
},
|
|
117
|
-
})
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## Agent loop
|
|
106
|
+
### The agent loop
|
|
121
107
|
|
|
122
108
|
The core pattern is [`ctx.useAgent()`](/docs/agents/reference/agent-config) followed by `ctx.agent.run()`. This runs the LLM in a loop — it generates text, calls tools, and continues until it has nothing left to do. All activity is automatically persisted to the entity's stream. See [Configuring the agent](/docs/agents/usage/configuring-the-agent).
|
|
123
109
|
|
|
@@ -131,7 +117,7 @@ ctx.useAgent({
|
|
|
131
117
|
await ctx.agent.run()
|
|
132
118
|
```
|
|
133
119
|
|
|
134
|
-
|
|
120
|
+
### Tools
|
|
135
121
|
|
|
136
122
|
Functions the LLM can call during the agent loop. Each tool has a name, description, parameters (defined with [TypeBox](https://github.com/sinclairzx81/typebox) or any [Standard Schema](https://standardschema.dev) validator), and an execute function. Tools run in the handler's context and have access to the entity's state and coordination primitives. See [Defining tools](/docs/agents/usage/defining-tools) and the [`AgentTool` reference](/docs/agents/reference/agent-tool).
|
|
137
123
|
|
|
@@ -156,7 +142,7 @@ const searchKbTool: AgentTool = {
|
|
|
156
142
|
}
|
|
157
143
|
```
|
|
158
144
|
|
|
159
|
-
|
|
145
|
+
### Coordination
|
|
160
146
|
|
|
161
147
|
Entities interact through structured primitives. An entity can `spawn` children, `observe` other entities, `send` messages, and [share state](/docs/agents/usage/shared-state). These operations are all durable — they survive restarts and are tracked in the event stream. See [Spawning and coordinating](/docs/agents/usage/spawning-and-coordinating).
|
|
162
148
|
|
|
@@ -170,7 +156,7 @@ async handler(ctx) {
|
|
|
170
156
|
systemPrompt: "Analyse the report",
|
|
171
157
|
tools: ["read"],
|
|
172
158
|
},
|
|
173
|
-
{ initialMessage: "Find the top three issues", wake: "runFinished" }
|
|
159
|
+
{ initialMessage: "Find the top three issues", wake: { on: "runFinished", includeResponse: true } }
|
|
174
160
|
)
|
|
175
161
|
|
|
176
162
|
// send a message to another entity
|
|
@@ -183,7 +169,7 @@ async handler(ctx) {
|
|
|
183
169
|
}
|
|
184
170
|
```
|
|
185
171
|
|
|
186
|
-
|
|
172
|
+
### Built-in collections
|
|
187
173
|
|
|
188
174
|
Every entity automatically has collections for runs, steps, texts, tool calls, errors, inbox, and more. These are populated by the runtime as the agent operates and give you live observability into every step of the agent loop — useful for chat UIs, debugging tools, dashboards, and analytics. Query them from the handler or observe them externally. See the [Built-in collections reference](/docs/agents/reference/built-in-collections).
|
|
189
175
|
|
|
@@ -198,9 +184,45 @@ const db = await client.observe(entity("/support/ticket-42"))
|
|
|
198
184
|
console.log(db.collections.texts.toArray)
|
|
199
185
|
```
|
|
200
186
|
|
|
187
|
+
|
|
188
|
+
### Custom collections
|
|
189
|
+
|
|
190
|
+
Define custom persistent collections on the entity.
|
|
191
|
+
|
|
192
|
+
Defined as part of the [entity definition](/docs/agents/reference/entity-definition) and accessed through `ctx.db` alongside the [built-in collections](#built-in-collections).
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
registry.define("tracker", {
|
|
196
|
+
state: {
|
|
197
|
+
items: {
|
|
198
|
+
schema: z.object({
|
|
199
|
+
key: z.string(),
|
|
200
|
+
name: z.string(),
|
|
201
|
+
done: z.boolean(),
|
|
202
|
+
}),
|
|
203
|
+
primaryKey: "key",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
async handler(ctx) {
|
|
207
|
+
// read
|
|
208
|
+
const item = ctx.db.collections.items.get("item-1")
|
|
209
|
+
|
|
210
|
+
// write
|
|
211
|
+
ctx.db.actions.items_insert({
|
|
212
|
+
row: { key: "item-2", name: "New", done: false },
|
|
213
|
+
})
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
State is local to the entity, typed, and survives restarts. Use it for things that belong to the entity but aren't part of the agent's event stream — an order's items, a research job's findings, a chat session's TODOs.
|
|
219
|
+
|
|
220
|
+
See [Managing state](/docs/agents/usage/managing-state) for more information.
|
|
221
|
+
|
|
201
222
|
## Next steps
|
|
202
223
|
|
|
203
|
-
- [Quickstart](/docs/agents/quickstart) — run the built-in `horton` and `worker` entities and connect your own app
|
|
224
|
+
- [Quickstart](/docs/agents/quickstart) — run the built-in `horton` and `worker` entities and connect your own app
|
|
225
|
+
- [Walkthrough](./walkthrough) — go from a web or mobile app to a <span class="no-wrap">multi-agent</span> system
|
|
204
226
|
- [Usage overview](/docs/agents/usage/overview) — the full developer surface on one page.
|
|
205
227
|
- [Defining entities](/docs/agents/usage/defining-entities) — entity types, schemas, and configuration.
|
|
206
228
|
- [Writing handlers](/docs/agents/usage/writing-handlers) — handler lifecycle and the `ctx` API.
|