@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/entrypoint.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { createServer } from "node:http";
|
|
4
3
|
import fs from "node:fs";
|
|
5
4
|
import pino from "pino";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { completeWithLowCostModel, createEntityRegistry, createRuntimeHandler, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
6
|
+
import { appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
8
7
|
import { eq, not, queryOnce } from "@durable-streams/state";
|
|
9
8
|
import { z } from "zod";
|
|
10
9
|
import { createHash } from "node:crypto";
|
|
@@ -22,8 +21,12 @@ const LOG_DIR = process.env.ELECTRIC_AGENTS_LOG_DIR ?? path.resolve(process.cwd(
|
|
|
22
21
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
23
22
|
const LOG_FILE = path.join(LOG_DIR, `builtin-agents-${Date.now()}.jsonl`);
|
|
24
23
|
const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
|
|
25
|
-
const
|
|
26
|
-
const
|
|
24
|
+
const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
|
|
25
|
+
const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST && !IS_ELECTRON_MAIN;
|
|
26
|
+
const streams = [{ stream: pino.destination({
|
|
27
|
+
dest: LOG_FILE,
|
|
28
|
+
sync: IS_ELECTRON_MAIN
|
|
29
|
+
}) }];
|
|
27
30
|
if (USE_PRETTY_LOGS) streams.push({ stream: pino.transport({
|
|
28
31
|
target: `pino-pretty`,
|
|
29
32
|
options: {
|
|
@@ -622,8 +625,8 @@ function createHortonDocsSupport(workingDirectory, opts = {}) {
|
|
|
622
625
|
logPrefix: `[horton-docs]`
|
|
623
626
|
});
|
|
624
627
|
function resolveCurrentQuestion(wake, events, inbox) {
|
|
625
|
-
if (wake.type === `
|
|
626
|
-
const eventQuestion = findLatestQuestion(events.filter((event) => event.type === `
|
|
628
|
+
if (wake.type === `inbox`) {
|
|
629
|
+
const eventQuestion = findLatestQuestion(events.filter((event) => event.type === `inbox`).map((event) => event.value));
|
|
627
630
|
if (eventQuestion) return eventQuestion;
|
|
628
631
|
}
|
|
629
632
|
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) => {
|
|
@@ -1898,9 +1902,8 @@ function truncate(str, max) {
|
|
|
1898
1902
|
|
|
1899
1903
|
//#endregion
|
|
1900
1904
|
//#region src/bootstrap.ts
|
|
1901
|
-
const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = `/_electric/builtin-agent-handler`;
|
|
1902
1905
|
async function createBuiltinAgentHandler(options) {
|
|
1903
|
-
const { agentServerUrl, serveEndpoint
|
|
1906
|
+
const { agentServerUrl, serveEndpoint, workingDirectory, streamFn, createElectricTools, publicUrl, runtimeName, baseSkillsDir: baseSkillsDirOverride, serverHeaders, defaultDispatchPolicyForType } = options;
|
|
1904
1907
|
const modelCatalog = await createBuiltinModelCatalog({ allowMockFallback: Boolean(streamFn) });
|
|
1905
1908
|
if (!modelCatalog) {
|
|
1906
1909
|
serverLog.warn(`[builtin-agents] no supported model provider API key found — set ANTHROPIC_API_KEY or OPENAI_API_KEY`);
|
|
@@ -1908,7 +1911,7 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1908
1911
|
}
|
|
1909
1912
|
const cwd = workingDirectory ?? process.cwd();
|
|
1910
1913
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1911
|
-
const baseSkillsDir = path.resolve(here, `../skills`);
|
|
1914
|
+
const baseSkillsDir = baseSkillsDirOverride ?? path.resolve(here, `../skills`);
|
|
1912
1915
|
let skillsRegistry = null;
|
|
1913
1916
|
try {
|
|
1914
1917
|
skillsRegistry = await createSkillsRegistry({
|
|
@@ -1938,6 +1941,8 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1938
1941
|
serveEndpoint,
|
|
1939
1942
|
registry,
|
|
1940
1943
|
subscriptionPathForType: (name) => `/${name}/*/main`,
|
|
1944
|
+
defaultDispatchPolicyForType,
|
|
1945
|
+
serverHeaders,
|
|
1941
1946
|
idleTimeout: 5e3,
|
|
1942
1947
|
createElectricTools,
|
|
1943
1948
|
publicUrl,
|
|
@@ -1959,15 +1964,13 @@ async function registerBuiltinAgentTypes(bootstrap) {
|
|
|
1959
1964
|
//#endregion
|
|
1960
1965
|
//#region src/server.ts
|
|
1961
1966
|
var BuiltinAgentsServer = class {
|
|
1962
|
-
server = null;
|
|
1963
1967
|
bootstrap = null;
|
|
1964
|
-
_url = null;
|
|
1965
|
-
publicBaseUrl = null;
|
|
1966
1968
|
_mcpRegistry = null;
|
|
1967
1969
|
mcpWatcherCloser = null;
|
|
1968
1970
|
mcpToolProviderName = null;
|
|
1969
1971
|
mcpApplyInFlight = new Set();
|
|
1970
1972
|
mcpStopping = false;
|
|
1973
|
+
pullWakeRunner = null;
|
|
1971
1974
|
options;
|
|
1972
1975
|
constructor(options) {
|
|
1973
1976
|
this.options = options;
|
|
@@ -1976,171 +1979,167 @@ var BuiltinAgentsServer = class {
|
|
|
1976
1979
|
get mcpRegistry() {
|
|
1977
1980
|
return this._mcpRegistry;
|
|
1978
1981
|
}
|
|
1979
|
-
get url() {
|
|
1980
|
-
if (!this._url) throw new Error(`Builtin agents server not started`);
|
|
1981
|
-
return this._url;
|
|
1982
|
-
}
|
|
1983
|
-
get registeredBaseUrl() {
|
|
1984
|
-
if (!this.publicBaseUrl) throw new Error(`Builtin agents server not started`);
|
|
1985
|
-
return this.publicBaseUrl;
|
|
1986
|
-
}
|
|
1987
1982
|
async start() {
|
|
1988
|
-
if (this.
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
}
|
|
1997
|
-
});
|
|
1983
|
+
if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
|
|
1984
|
+
const pullWake = this.options.pullWake;
|
|
1985
|
+
if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
|
|
1986
|
+
try {
|
|
1987
|
+
const publicUrl = this.options.mcpOAuthRedirectBase ?? this.options.agentServerUrl;
|
|
1988
|
+
const mcpRegistry = createRegistry({
|
|
1989
|
+
publicUrl,
|
|
1990
|
+
openAuthorizeUrl: this.options.openAuthorizeUrl
|
|
1998
1991
|
});
|
|
1999
|
-
this.
|
|
2000
|
-
const
|
|
2001
|
-
this.
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
const serveEndpoint = new URL(webhookPath, this.publicBaseUrl.endsWith(`/`) ? this.publicBaseUrl : `${this.publicBaseUrl}/`).toString();
|
|
2012
|
-
const publicUrl = this.options.mcpOAuthRedirectBase ?? this.publicBaseUrl;
|
|
2013
|
-
const mcpRegistry = createRegistry({
|
|
2014
|
-
publicUrl,
|
|
2015
|
-
openAuthorizeUrl: this.options.openAuthorizeUrl
|
|
2016
|
-
});
|
|
2017
|
-
this._mcpRegistry = mcpRegistry;
|
|
2018
|
-
const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
|
|
2019
|
-
const extras = this.options.extraMcpServers ?? [];
|
|
2020
|
-
const wirePersistence = async (cfg) => {
|
|
2021
|
-
const servers = [];
|
|
2022
|
-
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
2023
|
-
const persist = await keychainPersistence({ server: s.name });
|
|
2024
|
-
servers.push({
|
|
2025
|
-
...s,
|
|
2026
|
-
auth: {
|
|
2027
|
-
...s.auth,
|
|
2028
|
-
...persist
|
|
2029
|
-
}
|
|
2030
|
-
});
|
|
2031
|
-
} else servers.push(s);
|
|
2032
|
-
return {
|
|
2033
|
-
...cfg,
|
|
2034
|
-
servers
|
|
2035
|
-
};
|
|
2036
|
-
};
|
|
2037
|
-
const merge = (jsonCfg) => {
|
|
2038
|
-
const jsonServers = jsonCfg?.servers ?? [];
|
|
2039
|
-
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
2040
|
-
const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
|
|
2041
|
-
return {
|
|
2042
|
-
servers: [...filteredExtras, ...jsonServers],
|
|
2043
|
-
raw: jsonCfg?.raw
|
|
2044
|
-
};
|
|
2045
|
-
};
|
|
2046
|
-
const onConfigError = this.options.onConfigError;
|
|
2047
|
-
const runApply = async (jsonCfg) => {
|
|
2048
|
-
if (this.mcpStopping) return;
|
|
2049
|
-
try {
|
|
2050
|
-
const wired = await wirePersistence(merge(jsonCfg));
|
|
2051
|
-
if (this.mcpStopping) return;
|
|
2052
|
-
await mcpRegistry.applyConfig(wired);
|
|
2053
|
-
} catch (e) {
|
|
2054
|
-
serverLog.error(`[mcp] applyConfig:`, e);
|
|
2055
|
-
try {
|
|
2056
|
-
onConfigError?.(e);
|
|
2057
|
-
} catch (cbErr) {
|
|
2058
|
-
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
};
|
|
2062
|
-
const applyMerged = (jsonCfg) => {
|
|
2063
|
-
const p = runApply(jsonCfg);
|
|
2064
|
-
this.mcpApplyInFlight.add(p);
|
|
2065
|
-
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
2066
|
-
return p;
|
|
2067
|
-
};
|
|
2068
|
-
if (mcpConfigPath) {
|
|
2069
|
-
try {
|
|
2070
|
-
const cfg = await loadConfig(mcpConfigPath, process.env);
|
|
2071
|
-
applyMerged(cfg);
|
|
2072
|
-
} catch (err) {
|
|
2073
|
-
if (err.code !== `ENOENT`) throw err;
|
|
2074
|
-
if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
|
|
2075
|
-
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
|
|
2076
|
-
applyMerged(null);
|
|
2077
|
-
}
|
|
2078
|
-
try {
|
|
2079
|
-
this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
|
|
2080
|
-
onChange: (cfg) => void applyMerged(cfg),
|
|
2081
|
-
onError: (e) => serverLog.error(`[mcp] config error:`, e)
|
|
2082
|
-
});
|
|
2083
|
-
} catch (e) {
|
|
2084
|
-
serverLog.error(`[mcp] config watcher failed to start:`, e);
|
|
2085
|
-
}
|
|
2086
|
-
} else {
|
|
2087
|
-
if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
|
|
2088
|
-
applyMerged(null);
|
|
2089
|
-
}
|
|
2090
|
-
this.mcpToolProviderName = `mcp`;
|
|
2091
|
-
registerToolProvider({
|
|
2092
|
-
name: `mcp`,
|
|
2093
|
-
tools: () => {
|
|
2094
|
-
const tools = [];
|
|
2095
|
-
for (const entry of mcpRegistry.list()) {
|
|
2096
|
-
if (entry.status !== `ready`) continue;
|
|
2097
|
-
const live = mcpRegistry.get(entry.name);
|
|
2098
|
-
if (!live?.transport) continue;
|
|
2099
|
-
for (const t of entry.tools) tools.push(bridgeMcpTool({
|
|
2100
|
-
server: entry.name,
|
|
2101
|
-
tool: t,
|
|
2102
|
-
client: live.transport.client,
|
|
2103
|
-
timeoutMs: live.config.timeoutMs
|
|
2104
|
-
}));
|
|
2105
|
-
const caps = live.transport.client.getServerCapabilities?.();
|
|
2106
|
-
if (caps?.resources) tools.push(...buildResourceTools({
|
|
2107
|
-
server: entry.name,
|
|
2108
|
-
client: live.transport.client,
|
|
2109
|
-
timeoutMs: live.config.timeoutMs
|
|
2110
|
-
}));
|
|
2111
|
-
if (caps?.prompts) tools.push(...buildPromptTools({
|
|
2112
|
-
server: entry.name,
|
|
2113
|
-
client: live.transport.client,
|
|
2114
|
-
timeoutMs: live.config.timeoutMs
|
|
2115
|
-
}));
|
|
2116
|
-
}
|
|
2117
|
-
return tools;
|
|
1992
|
+
this._mcpRegistry = mcpRegistry;
|
|
1993
|
+
const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
|
|
1994
|
+
const extras = this.options.extraMcpServers ?? [];
|
|
1995
|
+
const wirePersistence = async (cfg) => {
|
|
1996
|
+
const servers = [];
|
|
1997
|
+
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
1998
|
+
const persist = await keychainPersistence({ server: s.name });
|
|
1999
|
+
servers.push({
|
|
2000
|
+
...s,
|
|
2001
|
+
auth: {
|
|
2002
|
+
...s.auth,
|
|
2003
|
+
...persist
|
|
2118
2004
|
}
|
|
2119
2005
|
});
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2006
|
+
} else servers.push(s);
|
|
2007
|
+
return {
|
|
2008
|
+
...cfg,
|
|
2009
|
+
servers
|
|
2010
|
+
};
|
|
2011
|
+
};
|
|
2012
|
+
const merge = (jsonCfg) => {
|
|
2013
|
+
const jsonServers = jsonCfg?.servers ?? [];
|
|
2014
|
+
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
2015
|
+
const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
|
|
2016
|
+
return {
|
|
2017
|
+
servers: [...filteredExtras, ...jsonServers],
|
|
2018
|
+
raw: jsonCfg?.raw
|
|
2019
|
+
};
|
|
2020
|
+
};
|
|
2021
|
+
const onConfigError = this.options.onConfigError;
|
|
2022
|
+
const runApply = async (jsonCfg) => {
|
|
2023
|
+
if (this.mcpStopping) return;
|
|
2024
|
+
try {
|
|
2025
|
+
const wired = await wirePersistence(merge(jsonCfg));
|
|
2026
|
+
if (this.mcpStopping) return;
|
|
2027
|
+
await mcpRegistry.applyConfig(wired);
|
|
2028
|
+
} catch (e) {
|
|
2029
|
+
serverLog.error(`[mcp] applyConfig:`, e);
|
|
2030
|
+
try {
|
|
2031
|
+
onConfigError?.(e);
|
|
2032
|
+
} catch (cbErr) {
|
|
2033
|
+
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
const applyMerged = (jsonCfg) => {
|
|
2038
|
+
const p = runApply(jsonCfg);
|
|
2039
|
+
this.mcpApplyInFlight.add(p);
|
|
2040
|
+
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
2041
|
+
return p;
|
|
2042
|
+
};
|
|
2043
|
+
if (mcpConfigPath) {
|
|
2044
|
+
try {
|
|
2045
|
+
const cfg = await loadConfig(mcpConfigPath, process.env);
|
|
2046
|
+
applyMerged(cfg);
|
|
2047
|
+
} catch (err) {
|
|
2048
|
+
if (err.code !== `ENOENT`) throw err;
|
|
2049
|
+
if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
|
|
2050
|
+
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
|
|
2051
|
+
applyMerged(null);
|
|
2052
|
+
}
|
|
2053
|
+
try {
|
|
2054
|
+
this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
|
|
2055
|
+
onChange: (cfg) => void applyMerged(cfg),
|
|
2056
|
+
onError: (e) => serverLog.error(`[mcp] config error:`, e)
|
|
2128
2057
|
});
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2058
|
+
} catch (e) {
|
|
2059
|
+
serverLog.error(`[mcp] config watcher failed to start:`, e);
|
|
2060
|
+
}
|
|
2061
|
+
} else {
|
|
2062
|
+
if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
|
|
2063
|
+
applyMerged(null);
|
|
2064
|
+
}
|
|
2065
|
+
this.mcpToolProviderName = `mcp`;
|
|
2066
|
+
registerToolProvider({
|
|
2067
|
+
name: `mcp`,
|
|
2068
|
+
tools: () => {
|
|
2069
|
+
const tools = [];
|
|
2070
|
+
for (const entry of mcpRegistry.list()) {
|
|
2071
|
+
if (entry.status !== `ready`) continue;
|
|
2072
|
+
const live = mcpRegistry.get(entry.name);
|
|
2073
|
+
if (!live?.transport) continue;
|
|
2074
|
+
for (const t of entry.tools) tools.push(bridgeMcpTool({
|
|
2075
|
+
server: entry.name,
|
|
2076
|
+
tool: t,
|
|
2077
|
+
client: live.transport.client,
|
|
2078
|
+
timeoutMs: live.config.timeoutMs
|
|
2079
|
+
}));
|
|
2080
|
+
const caps = live.transport.client.getServerCapabilities?.();
|
|
2081
|
+
if (caps?.resources) tools.push(...buildResourceTools({
|
|
2082
|
+
server: entry.name,
|
|
2083
|
+
client: live.transport.client,
|
|
2084
|
+
timeoutMs: live.config.timeoutMs
|
|
2085
|
+
}));
|
|
2086
|
+
if (caps?.prompts) tools.push(...buildPromptTools({
|
|
2087
|
+
server: entry.name,
|
|
2088
|
+
client: live.transport.client,
|
|
2089
|
+
timeoutMs: live.config.timeoutMs
|
|
2090
|
+
}));
|
|
2091
|
+
}
|
|
2092
|
+
return tools;
|
|
2136
2093
|
}
|
|
2137
2094
|
});
|
|
2138
|
-
|
|
2095
|
+
this.bootstrap = await createBuiltinAgentHandler({
|
|
2096
|
+
agentServerUrl: this.options.agentServerUrl,
|
|
2097
|
+
workingDirectory: this.options.workingDirectory,
|
|
2098
|
+
streamFn: this.options.mockStreamFn,
|
|
2099
|
+
createElectricTools: this.options.createElectricTools,
|
|
2100
|
+
publicUrl,
|
|
2101
|
+
runtimeName: `builtin-agents`,
|
|
2102
|
+
baseSkillsDir: this.options.baseSkillsDir,
|
|
2103
|
+
serverHeaders: pullWake.headers
|
|
2104
|
+
});
|
|
2105
|
+
if (!this.bootstrap) throw new Error(`ANTHROPIC_API_KEY or OPENAI_API_KEY must be set before starting builtin agents`);
|
|
2106
|
+
await registerBuiltinAgentTypes(this.bootstrap);
|
|
2107
|
+
const registeredRunner = pullWake.registerRunner ? await this.registerPullWakeRunner(pullWake) : null;
|
|
2108
|
+
this.pullWakeRunner = createPullWakeRunner({
|
|
2109
|
+
baseUrl: this.options.agentServerUrl,
|
|
2110
|
+
runnerId: pullWake.runnerId,
|
|
2111
|
+
runtime: this.bootstrap.runtime,
|
|
2112
|
+
headers: pullWake.headers,
|
|
2113
|
+
claimHeaders: pullWake.claimHeaders,
|
|
2114
|
+
claimTokenHeader: pullWake.claimTokenHeader,
|
|
2115
|
+
heartbeatIntervalMs: pullWake.heartbeatIntervalMs,
|
|
2116
|
+
leaseMs: pullWake.leaseMs,
|
|
2117
|
+
offset: registeredRunner?.wake_stream_offset,
|
|
2118
|
+
onError: (error) => {
|
|
2119
|
+
serverLog.error(`[builtin-agents] pull-wake runner failed`, error);
|
|
2120
|
+
return true;
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
this.pullWakeRunner.start();
|
|
2124
|
+
serverLog.info(`[builtin-agents] pull-wake runner started: ${pullWake.runnerId}`);
|
|
2125
|
+
return `pull-wake:${pullWake.runnerId}`;
|
|
2126
|
+
} catch (error) {
|
|
2127
|
+
await this.stop().catch(() => {});
|
|
2128
|
+
throw error;
|
|
2129
|
+
}
|
|
2139
2130
|
}
|
|
2140
2131
|
async stop() {
|
|
2132
|
+
if (this.pullWakeRunner) {
|
|
2133
|
+
await this.pullWakeRunner.stop().catch((e) => {
|
|
2134
|
+
serverLog.error(`[builtin-agents] pull-wake runner stop failed`, e);
|
|
2135
|
+
});
|
|
2136
|
+
this.pullWakeRunner = null;
|
|
2137
|
+
}
|
|
2141
2138
|
if (this.bootstrap) {
|
|
2142
2139
|
this.bootstrap.runtime.abortWakes();
|
|
2143
|
-
await Promise.race([this.bootstrap.runtime.drainWakes().catch(() => {
|
|
2140
|
+
await Promise.race([this.bootstrap.runtime.drainWakes().catch((err) => {
|
|
2141
|
+
serverLog.error(`[builtin-agents] drainWakes failed during shutdown:`, err);
|
|
2142
|
+
}), new Promise((resolve) => setTimeout(resolve, 5e3))]);
|
|
2144
2143
|
this.bootstrap = null;
|
|
2145
2144
|
}
|
|
2146
2145
|
this.mcpStopping = true;
|
|
@@ -2163,39 +2162,29 @@ var BuiltinAgentsServer = class {
|
|
|
2163
2162
|
});
|
|
2164
2163
|
this._mcpRegistry = null;
|
|
2165
2164
|
}
|
|
2166
|
-
if (this.server) {
|
|
2167
|
-
const server = this.server;
|
|
2168
|
-
await new Promise((resolve) => {
|
|
2169
|
-
server.close(() => resolve());
|
|
2170
|
-
});
|
|
2171
|
-
this.server = null;
|
|
2172
|
-
}
|
|
2173
2165
|
this.mcpStopping = false;
|
|
2174
|
-
this._url = null;
|
|
2175
|
-
this.publicBaseUrl = null;
|
|
2176
2166
|
}
|
|
2177
|
-
async
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2167
|
+
async registerPullWakeRunner(pullWake) {
|
|
2168
|
+
const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
|
|
2169
|
+
headers.set(`content-type`, `application/json`);
|
|
2170
|
+
const response = await fetch(appendPathToUrl(this.options.agentServerUrl, `/_electric/runners`), {
|
|
2171
|
+
method: `POST`,
|
|
2172
|
+
headers,
|
|
2173
|
+
body: JSON.stringify({
|
|
2174
|
+
id: pullWake.runnerId,
|
|
2175
|
+
owner_user_id: pullWake.ownerUserId,
|
|
2176
|
+
label: pullWake.label ?? `Built-in agents`,
|
|
2177
|
+
kind: `local`,
|
|
2178
|
+
admin_status: `enabled`
|
|
2179
|
+
})
|
|
2180
|
+
});
|
|
2181
|
+
if (!response.ok) throw new Error(`Failed to register pull-wake runner ${pullWake.runnerId}: ${response.status} ${await response.text()}`);
|
|
2182
|
+
return await response.json();
|
|
2192
2183
|
}
|
|
2193
2184
|
};
|
|
2194
2185
|
|
|
2195
2186
|
//#endregion
|
|
2196
2187
|
//#region src/entrypoint-lib.ts
|
|
2197
|
-
const DEFAULT_HOST = `127.0.0.1`;
|
|
2198
|
-
const DEFAULT_PORT = 4448;
|
|
2199
2188
|
function readEnv(env, names) {
|
|
2200
2189
|
for (const name of names) {
|
|
2201
2190
|
const value = env[name]?.trim();
|
|
@@ -2208,13 +2197,6 @@ function readRequiredEnv(env, names, description) {
|
|
|
2208
2197
|
if (value) return value;
|
|
2209
2198
|
throw new Error(`Missing ${description}. Set one of: ${names.map((name) => `"${name}"`).join(`, `)}`);
|
|
2210
2199
|
}
|
|
2211
|
-
function readPort(env) {
|
|
2212
|
-
const raw = readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_PORT`, `PORT`]);
|
|
2213
|
-
if (!raw) return DEFAULT_PORT;
|
|
2214
|
-
const port = Number(raw);
|
|
2215
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid builtin agents port "${raw}". Expected an integer between 1 and 65535.`);
|
|
2216
|
-
return port;
|
|
2217
|
-
}
|
|
2218
2200
|
function validateUrl(name, value) {
|
|
2219
2201
|
try {
|
|
2220
2202
|
new URL(value);
|
|
@@ -2223,20 +2205,55 @@ function validateUrl(name, value) {
|
|
|
2223
2205
|
throw new Error(`Invalid ${name}: "${value}"`);
|
|
2224
2206
|
}
|
|
2225
2207
|
}
|
|
2208
|
+
function parseAdditionalServerHeaders(env) {
|
|
2209
|
+
const raw = readEnv(env, [`ELECTRIC_AGENTS_SERVER_HEADERS`]);
|
|
2210
|
+
if (!raw) return void 0;
|
|
2211
|
+
let parsed;
|
|
2212
|
+
try {
|
|
2213
|
+
parsed = JSON.parse(raw);
|
|
2214
|
+
} catch {
|
|
2215
|
+
throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: expected JSON`);
|
|
2216
|
+
}
|
|
2217
|
+
if (!parsed || typeof parsed !== `object` || Array.isArray(parsed)) throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: expected a JSON object`);
|
|
2218
|
+
const headers = new Headers();
|
|
2219
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
2220
|
+
if (typeof value !== `string`) throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: header "${name}" must be a string`);
|
|
2221
|
+
headers.set(name, value);
|
|
2222
|
+
}
|
|
2223
|
+
const normalized = Object.fromEntries(headers.entries());
|
|
2224
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
2225
|
+
}
|
|
2226
|
+
function mergeHeaders(...sources) {
|
|
2227
|
+
const headers = new Headers();
|
|
2228
|
+
for (const source of sources) {
|
|
2229
|
+
if (!source) continue;
|
|
2230
|
+
new Headers(source).forEach((value, key) => headers.set(key, value));
|
|
2231
|
+
}
|
|
2232
|
+
const merged = Object.fromEntries(headers.entries());
|
|
2233
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
2234
|
+
}
|
|
2235
|
+
function hasHeader(headers, name) {
|
|
2236
|
+
return headers ? new Headers(headers).has(name) : false;
|
|
2237
|
+
}
|
|
2226
2238
|
function resolveBuiltinAgentsEntrypointOptions(env = process.env, cwd = process.cwd()) {
|
|
2227
2239
|
const agentServerUrl = validateUrl(`agent server URL`, readRequiredEnv(env, [`ELECTRIC_AGENTS_SERVER_URL`, `ELECTRIC_AGENTS_BASE_URL`], `agent server base URL`));
|
|
2228
|
-
const
|
|
2240
|
+
const runnerId = readRequiredEnv(env, [`ELECTRIC_AGENTS_PULL_WAKE_RUNNER_ID`, `PULL_WAKE_RUNNER_ID`], `pull-wake runner id`);
|
|
2241
|
+
const serverHeaders = mergeHeaders(parseAdditionalServerHeaders(env));
|
|
2229
2242
|
return {
|
|
2230
2243
|
agentServerUrl,
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2244
|
+
workingDirectory: readEnv(env, [`ELECTRIC_AGENTS_WORKING_DIRECTORY`, `WORKING_DIRECTORY`]) ?? cwd,
|
|
2245
|
+
pullWake: {
|
|
2246
|
+
runnerId,
|
|
2247
|
+
registerRunner: readEnv(env, [`ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER`]) === `true` || readEnv(env, [`ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER`]) === `1`,
|
|
2248
|
+
headers: serverHeaders,
|
|
2249
|
+
claimHeaders: serverHeaders,
|
|
2250
|
+
claimTokenHeader: hasHeader(serverHeaders, `authorization`) ? `electric-claim-token` : void 0
|
|
2251
|
+
}
|
|
2235
2252
|
};
|
|
2236
2253
|
}
|
|
2237
|
-
async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd(), createServer
|
|
2254
|
+
async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd(), createServer = (options$1) => new BuiltinAgentsServer(options$1) } = {}) {
|
|
2238
2255
|
const options = resolveBuiltinAgentsEntrypointOptions(env, cwd);
|
|
2239
|
-
const server = createServer
|
|
2256
|
+
const server = createServer(options);
|
|
2240
2257
|
const url = await server.start();
|
|
2241
2258
|
return {
|
|
2242
2259
|
options,
|
|
@@ -2260,10 +2277,9 @@ async function main() {
|
|
|
2260
2277
|
try {
|
|
2261
2278
|
const started = await runBuiltinAgentsEntrypoint();
|
|
2262
2279
|
server = started.server;
|
|
2263
|
-
console.log(`Builtin agents
|
|
2280
|
+
console.log(`Builtin agents pull-wake runner started at ${started.url}`);
|
|
2264
2281
|
console.log(`Registering against: ${started.options.agentServerUrl}`);
|
|
2265
2282
|
console.log(`Working directory: ${started.options.workingDirectory}`);
|
|
2266
|
-
if (started.options.baseUrl) console.log(`Public webhook base URL: ${started.options.baseUrl}`);
|
|
2267
2283
|
process.on(`SIGINT`, () => {
|
|
2268
2284
|
stop(0);
|
|
2269
2285
|
});
|