@electric-ax/agents 0.3.0 → 0.4.1
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 +225 -209
- package/dist/index.cjs +223 -205
- package/dist/index.d.cts +21 -11
- package/dist/index.d.ts +22 -12
- package/dist/index.js +224 -206
- package/docs/index.md +3 -3
- package/docs/reference/built-in-collections.md +1 -1
- package/docs/reference/wake-event.md +4 -4
- package/docs/usage/spawning-and-coordinating.md +1 -1
- package/docs/usage/waking-entities.md +5 -5
- package/docs/usage/writing-handlers.md +1 -1
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { AgentConfig, AgentTool, AvailableProvider, EntityRegistry, EntityStreamDBWithActions, HandlerContext, RuntimeHandler, WakeEvent } from "@electric-ax/agents-runtime";
|
|
1
|
+
import { AgentConfig, AgentTool, AvailableProvider, DispatchPolicy, EntityRegistry, EntityStreamDBWithActions, HandlerContext, HeadersProvider, PullWakeRunnerConfig, RuntimeHandler, WakeEvent } from "@electric-ax/agents-runtime";
|
|
2
2
|
import { ChangeEvent } from "@durable-streams/state";
|
|
3
3
|
import { braveSearchTool } from "@electric-ax/agents-runtime/tools";
|
|
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 { IncomingMessage, ServerResponse } from "node:http";
|
|
6
5
|
import { AgentTool as AgentTool$1, StreamFn } from "@mariozechner/pi-agent-core";
|
|
6
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
7
7
|
|
|
8
8
|
//#region src/skills/types.d.ts
|
|
9
9
|
interface SkillMeta {
|
|
@@ -43,6 +43,10 @@ interface BuiltinAgentHandlerOptions {
|
|
|
43
43
|
streamFn?: StreamFn;
|
|
44
44
|
publicUrl?: string;
|
|
45
45
|
runtimeName?: string;
|
|
46
|
+
/** Override for the built-in skills directory; required when embedders bundle this package. */
|
|
47
|
+
baseSkillsDir?: string;
|
|
48
|
+
serverHeaders?: HeadersProvider;
|
|
49
|
+
defaultDispatchPolicyForType?: (typeName: string) => DispatchPolicy | undefined;
|
|
46
50
|
createElectricTools?: (context: {
|
|
47
51
|
entityUrl: string;
|
|
48
52
|
entityType: string;
|
|
@@ -85,12 +89,20 @@ declare const registerAgentTypes: typeof registerBuiltinAgentTypes;
|
|
|
85
89
|
//#region src/server.d.ts
|
|
86
90
|
interface BuiltinAgentsServerOptions {
|
|
87
91
|
agentServerUrl: string;
|
|
88
|
-
baseUrl?: string;
|
|
89
|
-
port: number;
|
|
90
|
-
host?: string;
|
|
91
92
|
workingDirectory?: string;
|
|
92
93
|
mockStreamFn?: StreamFn;
|
|
93
|
-
|
|
94
|
+
/** Pull-wake runner configuration for built-in agents. */
|
|
95
|
+
pullWake: {
|
|
96
|
+
runnerId: string;
|
|
97
|
+
ownerUserId?: string;
|
|
98
|
+
label?: string;
|
|
99
|
+
registerRunner?: boolean;
|
|
100
|
+
headers?: PullWakeRunnerConfig[`headers`];
|
|
101
|
+
claimHeaders?: PullWakeRunnerConfig[`claimHeaders`];
|
|
102
|
+
claimTokenHeader?: PullWakeRunnerConfig[`claimTokenHeader`];
|
|
103
|
+
heartbeatIntervalMs?: PullWakeRunnerConfig[`heartbeatIntervalMs`];
|
|
104
|
+
leaseMs?: PullWakeRunnerConfig[`leaseMs`];
|
|
105
|
+
};
|
|
94
106
|
/** Invoked when an `authorizationCode` server needs user consent. */
|
|
95
107
|
openAuthorizeUrl?: (url: string, server: string) => void;
|
|
96
108
|
/**
|
|
@@ -115,6 +127,8 @@ interface BuiltinAgentsServerOptions {
|
|
|
115
127
|
* so the embedder must opt in.
|
|
116
128
|
*/
|
|
117
129
|
loadProjectMcpConfig?: boolean;
|
|
130
|
+
/** Override for the built-in skills directory; required when embedders bundle this package. */
|
|
131
|
+
baseSkillsDir?: string;
|
|
118
132
|
createElectricTools?: (context: {
|
|
119
133
|
entityUrl: string;
|
|
120
134
|
entityType: string;
|
|
@@ -149,24 +163,20 @@ interface BuiltinAgentsServerOptions {
|
|
|
149
163
|
}) => Array<AgentTool> | Promise<Array<AgentTool>>;
|
|
150
164
|
}
|
|
151
165
|
declare class BuiltinAgentsServer {
|
|
152
|
-
private server;
|
|
153
166
|
private bootstrap;
|
|
154
|
-
private _url;
|
|
155
|
-
private publicBaseUrl;
|
|
156
167
|
private _mcpRegistry;
|
|
157
168
|
private mcpWatcherCloser;
|
|
158
169
|
private mcpToolProviderName;
|
|
159
170
|
private mcpApplyInFlight;
|
|
160
171
|
private mcpStopping;
|
|
172
|
+
private pullWakeRunner;
|
|
161
173
|
readonly options: BuiltinAgentsServerOptions;
|
|
162
174
|
constructor(options: BuiltinAgentsServerOptions);
|
|
163
175
|
/** Embedded MCP registry. `null` until `start()` has run. */
|
|
164
176
|
get mcpRegistry(): Registry | null;
|
|
165
|
-
get url(): string;
|
|
166
|
-
get registeredBaseUrl(): string;
|
|
167
177
|
start(): Promise<string>;
|
|
168
178
|
stop(): Promise<void>;
|
|
169
|
-
private
|
|
179
|
+
private registerPullWakeRunner;
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { completeWithLowCostModel, createEntityRegistry, createRuntimeHandler, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
3
|
+
import { appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import pino from "pino";
|
|
6
6
|
import { eq, not, queryOnce } from "@durable-streams/state";
|
|
@@ -14,15 +14,18 @@ import { nanoid } from "nanoid";
|
|
|
14
14
|
import { getModels } from "@mariozechner/pi-ai";
|
|
15
15
|
import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createFetchUrlTool, createReadFileTool, createWriteTool, fetchUrlTool } from "@electric-ax/agents-runtime/tools";
|
|
16
16
|
import { bridgeMcpTool, buildPromptTools, buildResourceTools, createRegistry, keychainPersistence, loadConfig, mcp, watchConfig } from "@electric-ax/agents-mcp";
|
|
17
|
-
import { createServer } from "node:http";
|
|
18
17
|
|
|
19
18
|
//#region src/log.ts
|
|
20
19
|
const LOG_DIR = process.env.ELECTRIC_AGENTS_LOG_DIR ?? path.resolve(process.cwd(), `logs`);
|
|
21
20
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
22
21
|
const LOG_FILE = path.join(LOG_DIR, `builtin-agents-${Date.now()}.jsonl`);
|
|
23
22
|
const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
|
|
24
|
-
const
|
|
25
|
-
const
|
|
23
|
+
const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
|
|
24
|
+
const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST && !IS_ELECTRON_MAIN;
|
|
25
|
+
const streams = [{ stream: pino.destination({
|
|
26
|
+
dest: LOG_FILE,
|
|
27
|
+
sync: IS_ELECTRON_MAIN
|
|
28
|
+
}) }];
|
|
26
29
|
if (USE_PRETTY_LOGS) streams.push({ stream: pino.transport({
|
|
27
30
|
target: `pino-pretty`,
|
|
28
31
|
options: {
|
|
@@ -621,8 +624,8 @@ function createHortonDocsSupport(workingDirectory, opts = {}) {
|
|
|
621
624
|
logPrefix: `[horton-docs]`
|
|
622
625
|
});
|
|
623
626
|
function resolveCurrentQuestion(wake, events, inbox) {
|
|
624
|
-
if (wake.type === `
|
|
625
|
-
const eventQuestion = findLatestQuestion(events.filter((event) => event.type === `
|
|
627
|
+
if (wake.type === `inbox`) {
|
|
628
|
+
const eventQuestion = findLatestQuestion(events.filter((event) => event.type === `inbox`).map((event) => event.value));
|
|
626
629
|
if (eventQuestion) return eventQuestion;
|
|
627
630
|
}
|
|
628
631
|
const wakeQuestion = payloadToText(wake.payload).trim();
|
|
@@ -1423,7 +1426,8 @@ function registerHorton(registry, options) {
|
|
|
1423
1426
|
const { workingDirectory, streamFn, skillsRegistry = null, modelCatalog } = options;
|
|
1424
1427
|
const docsUrl = options.docsUrl ?? process.env.HORTON_DOCS_URL;
|
|
1425
1428
|
if (process.env.BRAVE_SEARCH_API_KEY) serverLog.info(`[horton] Web search: using Brave Search API`);
|
|
1426
|
-
else serverLog.warn(`[horton] BRAVE_SEARCH_API_KEY not set — web search will fall back to Anthropic built-in search
|
|
1429
|
+
else if (process.env.ANTHROPIC_API_KEY) serverLog.warn(`[horton] BRAVE_SEARCH_API_KEY not set — web search will fall back to Anthropic built-in search`);
|
|
1430
|
+
else serverLog.warn(`[horton] BRAVE_SEARCH_API_KEY and ANTHROPIC_API_KEY not set — web search tool will be unavailable`);
|
|
1427
1431
|
const docsSupport = createHortonDocsSupport(workingDirectory);
|
|
1428
1432
|
const docsSearchTool = docsSupport?.createSearchTool();
|
|
1429
1433
|
docsSupport?.ensureReady().catch((error) => {
|
|
@@ -1900,7 +1904,7 @@ function truncate(str, max) {
|
|
|
1900
1904
|
//#region src/bootstrap.ts
|
|
1901
1905
|
const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = `/_electric/builtin-agent-handler`;
|
|
1902
1906
|
async function createBuiltinAgentHandler(options) {
|
|
1903
|
-
const { agentServerUrl, serveEndpoint
|
|
1907
|
+
const { agentServerUrl, serveEndpoint, workingDirectory, streamFn, createElectricTools, publicUrl, runtimeName, baseSkillsDir: baseSkillsDirOverride, serverHeaders, defaultDispatchPolicyForType } = options;
|
|
1904
1908
|
const modelCatalog = await createBuiltinModelCatalog({ allowMockFallback: Boolean(streamFn) });
|
|
1905
1909
|
if (!modelCatalog) {
|
|
1906
1910
|
serverLog.warn(`[builtin-agents] no supported model provider API key found — set ANTHROPIC_API_KEY or OPENAI_API_KEY`);
|
|
@@ -1908,7 +1912,7 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1908
1912
|
}
|
|
1909
1913
|
const cwd = workingDirectory ?? process.cwd();
|
|
1910
1914
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1911
|
-
const baseSkillsDir = path.resolve(here, `../skills`);
|
|
1915
|
+
const baseSkillsDir = baseSkillsDirOverride ?? path.resolve(here, `../skills`);
|
|
1912
1916
|
let skillsRegistry = null;
|
|
1913
1917
|
try {
|
|
1914
1918
|
skillsRegistry = await createSkillsRegistry({
|
|
@@ -1938,6 +1942,8 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1938
1942
|
serveEndpoint,
|
|
1939
1943
|
registry,
|
|
1940
1944
|
subscriptionPathForType: (name) => `/${name}/*/main`,
|
|
1945
|
+
defaultDispatchPolicyForType,
|
|
1946
|
+
serverHeaders,
|
|
1941
1947
|
idleTimeout: 5e3,
|
|
1942
1948
|
createElectricTools,
|
|
1943
1949
|
publicUrl,
|
|
@@ -1969,15 +1975,13 @@ const registerAgentTypes = registerBuiltinAgentTypes;
|
|
|
1969
1975
|
//#endregion
|
|
1970
1976
|
//#region src/server.ts
|
|
1971
1977
|
var BuiltinAgentsServer = class {
|
|
1972
|
-
server = null;
|
|
1973
1978
|
bootstrap = null;
|
|
1974
|
-
_url = null;
|
|
1975
|
-
publicBaseUrl = null;
|
|
1976
1979
|
_mcpRegistry = null;
|
|
1977
1980
|
mcpWatcherCloser = null;
|
|
1978
1981
|
mcpToolProviderName = null;
|
|
1979
1982
|
mcpApplyInFlight = new Set();
|
|
1980
1983
|
mcpStopping = false;
|
|
1984
|
+
pullWakeRunner = null;
|
|
1981
1985
|
options;
|
|
1982
1986
|
constructor(options) {
|
|
1983
1987
|
this.options = options;
|
|
@@ -1986,171 +1990,167 @@ var BuiltinAgentsServer = class {
|
|
|
1986
1990
|
get mcpRegistry() {
|
|
1987
1991
|
return this._mcpRegistry;
|
|
1988
1992
|
}
|
|
1989
|
-
get url() {
|
|
1990
|
-
if (!this._url) throw new Error(`Builtin agents server not started`);
|
|
1991
|
-
return this._url;
|
|
1992
|
-
}
|
|
1993
|
-
get registeredBaseUrl() {
|
|
1994
|
-
if (!this.publicBaseUrl) throw new Error(`Builtin agents server not started`);
|
|
1995
|
-
return this.publicBaseUrl;
|
|
1996
|
-
}
|
|
1997
1993
|
async start() {
|
|
1998
|
-
if (this.
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
}
|
|
2007
|
-
});
|
|
1994
|
+
if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
|
|
1995
|
+
const pullWake = this.options.pullWake;
|
|
1996
|
+
if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
|
|
1997
|
+
try {
|
|
1998
|
+
const publicUrl = this.options.mcpOAuthRedirectBase ?? this.options.agentServerUrl;
|
|
1999
|
+
const mcpRegistry = createRegistry({
|
|
2000
|
+
publicUrl,
|
|
2001
|
+
openAuthorizeUrl: this.options.openAuthorizeUrl
|
|
2008
2002
|
});
|
|
2009
|
-
this.
|
|
2010
|
-
const
|
|
2011
|
-
this.
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
const serveEndpoint = new URL(webhookPath, this.publicBaseUrl.endsWith(`/`) ? this.publicBaseUrl : `${this.publicBaseUrl}/`).toString();
|
|
2022
|
-
const publicUrl = this.options.mcpOAuthRedirectBase ?? this.publicBaseUrl;
|
|
2023
|
-
const mcpRegistry = createRegistry({
|
|
2024
|
-
publicUrl,
|
|
2025
|
-
openAuthorizeUrl: this.options.openAuthorizeUrl
|
|
2026
|
-
});
|
|
2027
|
-
this._mcpRegistry = mcpRegistry;
|
|
2028
|
-
const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
|
|
2029
|
-
const extras = this.options.extraMcpServers ?? [];
|
|
2030
|
-
const wirePersistence = async (cfg) => {
|
|
2031
|
-
const servers = [];
|
|
2032
|
-
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
2033
|
-
const persist = await keychainPersistence({ server: s.name });
|
|
2034
|
-
servers.push({
|
|
2035
|
-
...s,
|
|
2036
|
-
auth: {
|
|
2037
|
-
...s.auth,
|
|
2038
|
-
...persist
|
|
2039
|
-
}
|
|
2040
|
-
});
|
|
2041
|
-
} else servers.push(s);
|
|
2042
|
-
return {
|
|
2043
|
-
...cfg,
|
|
2044
|
-
servers
|
|
2045
|
-
};
|
|
2046
|
-
};
|
|
2047
|
-
const merge = (jsonCfg) => {
|
|
2048
|
-
const jsonServers = jsonCfg?.servers ?? [];
|
|
2049
|
-
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
2050
|
-
const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
|
|
2051
|
-
return {
|
|
2052
|
-
servers: [...filteredExtras, ...jsonServers],
|
|
2053
|
-
raw: jsonCfg?.raw
|
|
2054
|
-
};
|
|
2055
|
-
};
|
|
2056
|
-
const onConfigError = this.options.onConfigError;
|
|
2057
|
-
const runApply = async (jsonCfg) => {
|
|
2058
|
-
if (this.mcpStopping) return;
|
|
2059
|
-
try {
|
|
2060
|
-
const wired = await wirePersistence(merge(jsonCfg));
|
|
2061
|
-
if (this.mcpStopping) return;
|
|
2062
|
-
await mcpRegistry.applyConfig(wired);
|
|
2063
|
-
} catch (e) {
|
|
2064
|
-
serverLog.error(`[mcp] applyConfig:`, e);
|
|
2065
|
-
try {
|
|
2066
|
-
onConfigError?.(e);
|
|
2067
|
-
} catch (cbErr) {
|
|
2068
|
-
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
};
|
|
2072
|
-
const applyMerged = (jsonCfg) => {
|
|
2073
|
-
const p = runApply(jsonCfg);
|
|
2074
|
-
this.mcpApplyInFlight.add(p);
|
|
2075
|
-
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
2076
|
-
return p;
|
|
2077
|
-
};
|
|
2078
|
-
if (mcpConfigPath) {
|
|
2079
|
-
try {
|
|
2080
|
-
const cfg = await loadConfig(mcpConfigPath, process.env);
|
|
2081
|
-
applyMerged(cfg);
|
|
2082
|
-
} catch (err) {
|
|
2083
|
-
if (err.code !== `ENOENT`) throw err;
|
|
2084
|
-
if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
|
|
2085
|
-
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
|
|
2086
|
-
applyMerged(null);
|
|
2087
|
-
}
|
|
2088
|
-
try {
|
|
2089
|
-
this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
|
|
2090
|
-
onChange: (cfg) => void applyMerged(cfg),
|
|
2091
|
-
onError: (e) => serverLog.error(`[mcp] config error:`, e)
|
|
2092
|
-
});
|
|
2093
|
-
} catch (e) {
|
|
2094
|
-
serverLog.error(`[mcp] config watcher failed to start:`, e);
|
|
2095
|
-
}
|
|
2096
|
-
} else {
|
|
2097
|
-
if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
|
|
2098
|
-
applyMerged(null);
|
|
2099
|
-
}
|
|
2100
|
-
this.mcpToolProviderName = `mcp`;
|
|
2101
|
-
registerToolProvider({
|
|
2102
|
-
name: `mcp`,
|
|
2103
|
-
tools: () => {
|
|
2104
|
-
const tools = [];
|
|
2105
|
-
for (const entry of mcpRegistry.list()) {
|
|
2106
|
-
if (entry.status !== `ready`) continue;
|
|
2107
|
-
const live = mcpRegistry.get(entry.name);
|
|
2108
|
-
if (!live?.transport) continue;
|
|
2109
|
-
for (const t of entry.tools) tools.push(bridgeMcpTool({
|
|
2110
|
-
server: entry.name,
|
|
2111
|
-
tool: t,
|
|
2112
|
-
client: live.transport.client,
|
|
2113
|
-
timeoutMs: live.config.timeoutMs
|
|
2114
|
-
}));
|
|
2115
|
-
const caps = live.transport.client.getServerCapabilities?.();
|
|
2116
|
-
if (caps?.resources) tools.push(...buildResourceTools({
|
|
2117
|
-
server: entry.name,
|
|
2118
|
-
client: live.transport.client,
|
|
2119
|
-
timeoutMs: live.config.timeoutMs
|
|
2120
|
-
}));
|
|
2121
|
-
if (caps?.prompts) tools.push(...buildPromptTools({
|
|
2122
|
-
server: entry.name,
|
|
2123
|
-
client: live.transport.client,
|
|
2124
|
-
timeoutMs: live.config.timeoutMs
|
|
2125
|
-
}));
|
|
2126
|
-
}
|
|
2127
|
-
return tools;
|
|
2003
|
+
this._mcpRegistry = mcpRegistry;
|
|
2004
|
+
const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
|
|
2005
|
+
const extras = this.options.extraMcpServers ?? [];
|
|
2006
|
+
const wirePersistence = async (cfg) => {
|
|
2007
|
+
const servers = [];
|
|
2008
|
+
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
2009
|
+
const persist = await keychainPersistence({ server: s.name });
|
|
2010
|
+
servers.push({
|
|
2011
|
+
...s,
|
|
2012
|
+
auth: {
|
|
2013
|
+
...s.auth,
|
|
2014
|
+
...persist
|
|
2128
2015
|
}
|
|
2129
2016
|
});
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2017
|
+
} else servers.push(s);
|
|
2018
|
+
return {
|
|
2019
|
+
...cfg,
|
|
2020
|
+
servers
|
|
2021
|
+
};
|
|
2022
|
+
};
|
|
2023
|
+
const merge = (jsonCfg) => {
|
|
2024
|
+
const jsonServers = jsonCfg?.servers ?? [];
|
|
2025
|
+
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
2026
|
+
const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
|
|
2027
|
+
return {
|
|
2028
|
+
servers: [...filteredExtras, ...jsonServers],
|
|
2029
|
+
raw: jsonCfg?.raw
|
|
2030
|
+
};
|
|
2031
|
+
};
|
|
2032
|
+
const onConfigError = this.options.onConfigError;
|
|
2033
|
+
const runApply = async (jsonCfg) => {
|
|
2034
|
+
if (this.mcpStopping) return;
|
|
2035
|
+
try {
|
|
2036
|
+
const wired = await wirePersistence(merge(jsonCfg));
|
|
2037
|
+
if (this.mcpStopping) return;
|
|
2038
|
+
await mcpRegistry.applyConfig(wired);
|
|
2039
|
+
} catch (e) {
|
|
2040
|
+
serverLog.error(`[mcp] applyConfig:`, e);
|
|
2041
|
+
try {
|
|
2042
|
+
onConfigError?.(e);
|
|
2043
|
+
} catch (cbErr) {
|
|
2044
|
+
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
const applyMerged = (jsonCfg) => {
|
|
2049
|
+
const p = runApply(jsonCfg);
|
|
2050
|
+
this.mcpApplyInFlight.add(p);
|
|
2051
|
+
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
2052
|
+
return p;
|
|
2053
|
+
};
|
|
2054
|
+
if (mcpConfigPath) {
|
|
2055
|
+
try {
|
|
2056
|
+
const cfg = await loadConfig(mcpConfigPath, process.env);
|
|
2057
|
+
applyMerged(cfg);
|
|
2058
|
+
} catch (err) {
|
|
2059
|
+
if (err.code !== `ENOENT`) throw err;
|
|
2060
|
+
if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
|
|
2061
|
+
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
|
|
2062
|
+
applyMerged(null);
|
|
2063
|
+
}
|
|
2064
|
+
try {
|
|
2065
|
+
this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
|
|
2066
|
+
onChange: (cfg) => void applyMerged(cfg),
|
|
2067
|
+
onError: (e) => serverLog.error(`[mcp] config error:`, e)
|
|
2138
2068
|
});
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2069
|
+
} catch (e) {
|
|
2070
|
+
serverLog.error(`[mcp] config watcher failed to start:`, e);
|
|
2071
|
+
}
|
|
2072
|
+
} else {
|
|
2073
|
+
if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
|
|
2074
|
+
applyMerged(null);
|
|
2075
|
+
}
|
|
2076
|
+
this.mcpToolProviderName = `mcp`;
|
|
2077
|
+
registerToolProvider({
|
|
2078
|
+
name: `mcp`,
|
|
2079
|
+
tools: () => {
|
|
2080
|
+
const tools = [];
|
|
2081
|
+
for (const entry of mcpRegistry.list()) {
|
|
2082
|
+
if (entry.status !== `ready`) continue;
|
|
2083
|
+
const live = mcpRegistry.get(entry.name);
|
|
2084
|
+
if (!live?.transport) continue;
|
|
2085
|
+
for (const t of entry.tools) tools.push(bridgeMcpTool({
|
|
2086
|
+
server: entry.name,
|
|
2087
|
+
tool: t,
|
|
2088
|
+
client: live.transport.client,
|
|
2089
|
+
timeoutMs: live.config.timeoutMs
|
|
2090
|
+
}));
|
|
2091
|
+
const caps = live.transport.client.getServerCapabilities?.();
|
|
2092
|
+
if (caps?.resources) tools.push(...buildResourceTools({
|
|
2093
|
+
server: entry.name,
|
|
2094
|
+
client: live.transport.client,
|
|
2095
|
+
timeoutMs: live.config.timeoutMs
|
|
2096
|
+
}));
|
|
2097
|
+
if (caps?.prompts) tools.push(...buildPromptTools({
|
|
2098
|
+
server: entry.name,
|
|
2099
|
+
client: live.transport.client,
|
|
2100
|
+
timeoutMs: live.config.timeoutMs
|
|
2101
|
+
}));
|
|
2102
|
+
}
|
|
2103
|
+
return tools;
|
|
2146
2104
|
}
|
|
2147
2105
|
});
|
|
2148
|
-
|
|
2106
|
+
this.bootstrap = await createBuiltinAgentHandler({
|
|
2107
|
+
agentServerUrl: this.options.agentServerUrl,
|
|
2108
|
+
workingDirectory: this.options.workingDirectory,
|
|
2109
|
+
streamFn: this.options.mockStreamFn,
|
|
2110
|
+
createElectricTools: this.options.createElectricTools,
|
|
2111
|
+
publicUrl,
|
|
2112
|
+
runtimeName: `builtin-agents`,
|
|
2113
|
+
baseSkillsDir: this.options.baseSkillsDir,
|
|
2114
|
+
serverHeaders: pullWake.headers
|
|
2115
|
+
});
|
|
2116
|
+
if (!this.bootstrap) throw new Error(`ANTHROPIC_API_KEY or OPENAI_API_KEY must be set before starting builtin agents`);
|
|
2117
|
+
await registerBuiltinAgentTypes(this.bootstrap);
|
|
2118
|
+
const registeredRunner = pullWake.registerRunner ? await this.registerPullWakeRunner(pullWake) : null;
|
|
2119
|
+
this.pullWakeRunner = createPullWakeRunner({
|
|
2120
|
+
baseUrl: this.options.agentServerUrl,
|
|
2121
|
+
runnerId: pullWake.runnerId,
|
|
2122
|
+
runtime: this.bootstrap.runtime,
|
|
2123
|
+
headers: pullWake.headers,
|
|
2124
|
+
claimHeaders: pullWake.claimHeaders,
|
|
2125
|
+
claimTokenHeader: pullWake.claimTokenHeader,
|
|
2126
|
+
heartbeatIntervalMs: pullWake.heartbeatIntervalMs,
|
|
2127
|
+
leaseMs: pullWake.leaseMs,
|
|
2128
|
+
offset: registeredRunner?.wake_stream_offset,
|
|
2129
|
+
onError: (error) => {
|
|
2130
|
+
serverLog.error(`[builtin-agents] pull-wake runner failed`, error);
|
|
2131
|
+
return true;
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
this.pullWakeRunner.start();
|
|
2135
|
+
serverLog.info(`[builtin-agents] pull-wake runner started: ${pullWake.runnerId}`);
|
|
2136
|
+
return `pull-wake:${pullWake.runnerId}`;
|
|
2137
|
+
} catch (error) {
|
|
2138
|
+
await this.stop().catch(() => {});
|
|
2139
|
+
throw error;
|
|
2140
|
+
}
|
|
2149
2141
|
}
|
|
2150
2142
|
async stop() {
|
|
2143
|
+
if (this.pullWakeRunner) {
|
|
2144
|
+
await this.pullWakeRunner.stop().catch((e) => {
|
|
2145
|
+
serverLog.error(`[builtin-agents] pull-wake runner stop failed`, e);
|
|
2146
|
+
});
|
|
2147
|
+
this.pullWakeRunner = null;
|
|
2148
|
+
}
|
|
2151
2149
|
if (this.bootstrap) {
|
|
2152
2150
|
this.bootstrap.runtime.abortWakes();
|
|
2153
|
-
await Promise.race([this.bootstrap.runtime.drainWakes().catch(() => {
|
|
2151
|
+
await Promise.race([this.bootstrap.runtime.drainWakes().catch((err) => {
|
|
2152
|
+
serverLog.error(`[builtin-agents] drainWakes failed during shutdown:`, err);
|
|
2153
|
+
}), new Promise((resolve) => setTimeout(resolve, 5e3))]);
|
|
2154
2154
|
this.bootstrap = null;
|
|
2155
2155
|
}
|
|
2156
2156
|
this.mcpStopping = true;
|
|
@@ -2173,39 +2173,29 @@ var BuiltinAgentsServer = class {
|
|
|
2173
2173
|
});
|
|
2174
2174
|
this._mcpRegistry = null;
|
|
2175
2175
|
}
|
|
2176
|
-
if (this.server) {
|
|
2177
|
-
const server = this.server;
|
|
2178
|
-
await new Promise((resolve) => {
|
|
2179
|
-
server.close(() => resolve());
|
|
2180
|
-
});
|
|
2181
|
-
this.server = null;
|
|
2182
|
-
}
|
|
2183
2176
|
this.mcpStopping = false;
|
|
2184
|
-
this._url = null;
|
|
2185
|
-
this.publicBaseUrl = null;
|
|
2186
2177
|
}
|
|
2187
|
-
async
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
const
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2178
|
+
async registerPullWakeRunner(pullWake) {
|
|
2179
|
+
const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
|
|
2180
|
+
headers.set(`content-type`, `application/json`);
|
|
2181
|
+
const response = await fetch(appendPathToUrl(this.options.agentServerUrl, `/_electric/runners`), {
|
|
2182
|
+
method: `POST`,
|
|
2183
|
+
headers,
|
|
2184
|
+
body: JSON.stringify({
|
|
2185
|
+
id: pullWake.runnerId,
|
|
2186
|
+
owner_user_id: pullWake.ownerUserId,
|
|
2187
|
+
label: pullWake.label ?? `Built-in agents`,
|
|
2188
|
+
kind: `local`,
|
|
2189
|
+
admin_status: `enabled`
|
|
2190
|
+
})
|
|
2191
|
+
});
|
|
2192
|
+
if (!response.ok) throw new Error(`Failed to register pull-wake runner ${pullWake.runnerId}: ${response.status} ${await response.text()}`);
|
|
2193
|
+
return await response.json();
|
|
2202
2194
|
}
|
|
2203
2195
|
};
|
|
2204
2196
|
|
|
2205
2197
|
//#endregion
|
|
2206
2198
|
//#region src/entrypoint-lib.ts
|
|
2207
|
-
const DEFAULT_HOST = `127.0.0.1`;
|
|
2208
|
-
const DEFAULT_PORT = 4448;
|
|
2209
2199
|
function readEnv(env, names) {
|
|
2210
2200
|
for (const name of names) {
|
|
2211
2201
|
const value = env[name]?.trim();
|
|
@@ -2218,13 +2208,6 @@ function readRequiredEnv(env, names, description) {
|
|
|
2218
2208
|
if (value) return value;
|
|
2219
2209
|
throw new Error(`Missing ${description}. Set one of: ${names.map((name) => `"${name}"`).join(`, `)}`);
|
|
2220
2210
|
}
|
|
2221
|
-
function readPort(env) {
|
|
2222
|
-
const raw = readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_PORT`, `PORT`]);
|
|
2223
|
-
if (!raw) return DEFAULT_PORT;
|
|
2224
|
-
const port = Number(raw);
|
|
2225
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid builtin agents port "${raw}". Expected an integer between 1 and 65535.`);
|
|
2226
|
-
return port;
|
|
2227
|
-
}
|
|
2228
2211
|
function validateUrl(name, value) {
|
|
2229
2212
|
try {
|
|
2230
2213
|
new URL(value);
|
|
@@ -2233,20 +2216,55 @@ function validateUrl(name, value) {
|
|
|
2233
2216
|
throw new Error(`Invalid ${name}: "${value}"`);
|
|
2234
2217
|
}
|
|
2235
2218
|
}
|
|
2219
|
+
function parseAdditionalServerHeaders(env) {
|
|
2220
|
+
const raw = readEnv(env, [`ELECTRIC_AGENTS_SERVER_HEADERS`]);
|
|
2221
|
+
if (!raw) return void 0;
|
|
2222
|
+
let parsed;
|
|
2223
|
+
try {
|
|
2224
|
+
parsed = JSON.parse(raw);
|
|
2225
|
+
} catch {
|
|
2226
|
+
throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: expected JSON`);
|
|
2227
|
+
}
|
|
2228
|
+
if (!parsed || typeof parsed !== `object` || Array.isArray(parsed)) throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: expected a JSON object`);
|
|
2229
|
+
const headers = new Headers();
|
|
2230
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
2231
|
+
if (typeof value !== `string`) throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: header "${name}" must be a string`);
|
|
2232
|
+
headers.set(name, value);
|
|
2233
|
+
}
|
|
2234
|
+
const normalized = Object.fromEntries(headers.entries());
|
|
2235
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
2236
|
+
}
|
|
2237
|
+
function mergeHeaders(...sources) {
|
|
2238
|
+
const headers = new Headers();
|
|
2239
|
+
for (const source of sources) {
|
|
2240
|
+
if (!source) continue;
|
|
2241
|
+
new Headers(source).forEach((value, key) => headers.set(key, value));
|
|
2242
|
+
}
|
|
2243
|
+
const merged = Object.fromEntries(headers.entries());
|
|
2244
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
2245
|
+
}
|
|
2246
|
+
function hasHeader(headers, name) {
|
|
2247
|
+
return headers ? new Headers(headers).has(name) : false;
|
|
2248
|
+
}
|
|
2236
2249
|
function resolveBuiltinAgentsEntrypointOptions(env = process.env, cwd = process.cwd()) {
|
|
2237
2250
|
const agentServerUrl = validateUrl(`agent server URL`, readRequiredEnv(env, [`ELECTRIC_AGENTS_SERVER_URL`, `ELECTRIC_AGENTS_BASE_URL`], `agent server base URL`));
|
|
2238
|
-
const
|
|
2251
|
+
const runnerId = readRequiredEnv(env, [`ELECTRIC_AGENTS_PULL_WAKE_RUNNER_ID`, `PULL_WAKE_RUNNER_ID`], `pull-wake runner id`);
|
|
2252
|
+
const serverHeaders = mergeHeaders(parseAdditionalServerHeaders(env));
|
|
2239
2253
|
return {
|
|
2240
2254
|
agentServerUrl,
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2255
|
+
workingDirectory: readEnv(env, [`ELECTRIC_AGENTS_WORKING_DIRECTORY`, `WORKING_DIRECTORY`]) ?? cwd,
|
|
2256
|
+
pullWake: {
|
|
2257
|
+
runnerId,
|
|
2258
|
+
registerRunner: readEnv(env, [`ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER`]) === `true` || readEnv(env, [`ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER`]) === `1`,
|
|
2259
|
+
headers: serverHeaders,
|
|
2260
|
+
claimHeaders: serverHeaders,
|
|
2261
|
+
claimTokenHeader: hasHeader(serverHeaders, `authorization`) ? `electric-claim-token` : void 0
|
|
2262
|
+
}
|
|
2245
2263
|
};
|
|
2246
2264
|
}
|
|
2247
|
-
async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd(), createServer
|
|
2265
|
+
async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd(), createServer = (options$1) => new BuiltinAgentsServer(options$1) } = {}) {
|
|
2248
2266
|
const options = resolveBuiltinAgentsEntrypointOptions(env, cwd);
|
|
2249
|
-
const server = createServer
|
|
2267
|
+
const server = createServer(options);
|
|
2250
2268
|
const url = await server.start();
|
|
2251
2269
|
return {
|
|
2252
2270
|
options,
|
package/docs/index.md
CHANGED
|
@@ -60,7 +60,7 @@ The function that runs when an entity wakes. Receives a [`HandlerContext`](/docs
|
|
|
60
60
|
```ts
|
|
61
61
|
registry.define("support", {
|
|
62
62
|
async handler(ctx, wake) {
|
|
63
|
-
if (wake.type === "
|
|
63
|
+
if (wake.type === "inbox") {
|
|
64
64
|
ctx.useAgent({
|
|
65
65
|
systemPrompt: "You are a support agent.",
|
|
66
66
|
model: "claude-sonnet-4-5-20250929",
|
|
@@ -78,11 +78,11 @@ Events that trigger a handler invocation. Wake sources include incoming messages
|
|
|
78
78
|
|
|
79
79
|
```ts
|
|
80
80
|
async handler(ctx, wake) {
|
|
81
|
-
// wake.type — "
|
|
81
|
+
// wake.type — "inbox", "wake", etc.
|
|
82
82
|
// wake.source — who triggered the wake
|
|
83
83
|
// wake.payload — message content or wake data
|
|
84
84
|
|
|
85
|
-
if (wake.type === "
|
|
85
|
+
if (wake.type === "inbox") {
|
|
86
86
|
const userMessage = wake.payload
|
|
87
87
|
// handle incoming message
|
|
88
88
|
}
|
|
@@ -23,7 +23,7 @@ Every entity automatically has these 17 collections, populated by the runtime as
|
|
|
23
23
|
| `toolCalls` | `tool_call` | `ToolCall` | Tool call lifecycle |
|
|
24
24
|
| `reasoning` | `reasoning` | `Reasoning` | Reasoning block lifecycle |
|
|
25
25
|
| `errors` | `error` | `ErrorEvent` | Diagnostic errors |
|
|
26
|
-
| `inbox` | `
|
|
26
|
+
| `inbox` | `inbox` | `MessageReceived` | Inbound messages |
|
|
27
27
|
| `wakes` | `wake` | `WakeEntry` | Wake delivery records |
|
|
28
28
|
| `entityCreated` | `entity_created` | `EntityCreated` | Entity bootstrap metadata |
|
|
29
29
|
| `entityStopped` | `entity_stopped` | `EntityStopped` | Entity shutdown signal |
|