@electric-ax/agents 0.3.0 → 0.4.0
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 +227 -205
- package/dist/index.cjs +225 -201
- package/dist/index.d.cts +17 -11
- package/dist/index.d.ts +18 -12
- package/dist/index.js +226 -202
- package/package.json +4 -4
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: {
|
|
@@ -1900,7 +1903,7 @@ function truncate(str, max) {
|
|
|
1900
1903
|
//#region src/bootstrap.ts
|
|
1901
1904
|
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, 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`);
|
|
@@ -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,
|
|
@@ -1969,15 +1974,13 @@ const registerAgentTypes = registerBuiltinAgentTypes;
|
|
|
1969
1974
|
//#endregion
|
|
1970
1975
|
//#region src/server.ts
|
|
1971
1976
|
var BuiltinAgentsServer = class {
|
|
1972
|
-
server = null;
|
|
1973
1977
|
bootstrap = null;
|
|
1974
|
-
_url = null;
|
|
1975
|
-
publicBaseUrl = null;
|
|
1976
1978
|
_mcpRegistry = null;
|
|
1977
1979
|
mcpWatcherCloser = null;
|
|
1978
1980
|
mcpToolProviderName = null;
|
|
1979
1981
|
mcpApplyInFlight = new Set();
|
|
1980
1982
|
mcpStopping = false;
|
|
1983
|
+
pullWakeRunner = null;
|
|
1981
1984
|
options;
|
|
1982
1985
|
constructor(options) {
|
|
1983
1986
|
this.options = options;
|
|
@@ -1986,171 +1989,166 @@ var BuiltinAgentsServer = class {
|
|
|
1986
1989
|
get mcpRegistry() {
|
|
1987
1990
|
return this._mcpRegistry;
|
|
1988
1991
|
}
|
|
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
1992
|
async start() {
|
|
1998
|
-
if (this.
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
}
|
|
2007
|
-
});
|
|
1993
|
+
if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
|
|
1994
|
+
const pullWake = this.options.pullWake;
|
|
1995
|
+
if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
|
|
1996
|
+
try {
|
|
1997
|
+
const publicUrl = this.options.mcpOAuthRedirectBase ?? this.options.agentServerUrl;
|
|
1998
|
+
const mcpRegistry = createRegistry({
|
|
1999
|
+
publicUrl,
|
|
2000
|
+
openAuthorizeUrl: this.options.openAuthorizeUrl
|
|
2008
2001
|
});
|
|
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;
|
|
2002
|
+
this._mcpRegistry = mcpRegistry;
|
|
2003
|
+
const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
|
|
2004
|
+
const extras = this.options.extraMcpServers ?? [];
|
|
2005
|
+
const wirePersistence = async (cfg) => {
|
|
2006
|
+
const servers = [];
|
|
2007
|
+
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
2008
|
+
const persist = await keychainPersistence({ server: s.name });
|
|
2009
|
+
servers.push({
|
|
2010
|
+
...s,
|
|
2011
|
+
auth: {
|
|
2012
|
+
...s.auth,
|
|
2013
|
+
...persist
|
|
2128
2014
|
}
|
|
2129
2015
|
});
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2016
|
+
} else servers.push(s);
|
|
2017
|
+
return {
|
|
2018
|
+
...cfg,
|
|
2019
|
+
servers
|
|
2020
|
+
};
|
|
2021
|
+
};
|
|
2022
|
+
const merge = (jsonCfg) => {
|
|
2023
|
+
const jsonServers = jsonCfg?.servers ?? [];
|
|
2024
|
+
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
2025
|
+
const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
|
|
2026
|
+
return {
|
|
2027
|
+
servers: [...filteredExtras, ...jsonServers],
|
|
2028
|
+
raw: jsonCfg?.raw
|
|
2029
|
+
};
|
|
2030
|
+
};
|
|
2031
|
+
const onConfigError = this.options.onConfigError;
|
|
2032
|
+
const runApply = async (jsonCfg) => {
|
|
2033
|
+
if (this.mcpStopping) return;
|
|
2034
|
+
try {
|
|
2035
|
+
const wired = await wirePersistence(merge(jsonCfg));
|
|
2036
|
+
if (this.mcpStopping) return;
|
|
2037
|
+
await mcpRegistry.applyConfig(wired);
|
|
2038
|
+
} catch (e) {
|
|
2039
|
+
serverLog.error(`[mcp] applyConfig:`, e);
|
|
2040
|
+
try {
|
|
2041
|
+
onConfigError?.(e);
|
|
2042
|
+
} catch (cbErr) {
|
|
2043
|
+
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
const applyMerged = (jsonCfg) => {
|
|
2048
|
+
const p = runApply(jsonCfg);
|
|
2049
|
+
this.mcpApplyInFlight.add(p);
|
|
2050
|
+
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
2051
|
+
return p;
|
|
2052
|
+
};
|
|
2053
|
+
if (mcpConfigPath) {
|
|
2054
|
+
try {
|
|
2055
|
+
const cfg = await loadConfig(mcpConfigPath, process.env);
|
|
2056
|
+
applyMerged(cfg);
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
if (err.code !== `ENOENT`) throw err;
|
|
2059
|
+
if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
|
|
2060
|
+
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
|
|
2061
|
+
applyMerged(null);
|
|
2062
|
+
}
|
|
2063
|
+
try {
|
|
2064
|
+
this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
|
|
2065
|
+
onChange: (cfg) => void applyMerged(cfg),
|
|
2066
|
+
onError: (e) => serverLog.error(`[mcp] config error:`, e)
|
|
2138
2067
|
});
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2068
|
+
} catch (e) {
|
|
2069
|
+
serverLog.error(`[mcp] config watcher failed to start:`, e);
|
|
2070
|
+
}
|
|
2071
|
+
} else {
|
|
2072
|
+
if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
|
|
2073
|
+
applyMerged(null);
|
|
2074
|
+
}
|
|
2075
|
+
this.mcpToolProviderName = `mcp`;
|
|
2076
|
+
registerToolProvider({
|
|
2077
|
+
name: `mcp`,
|
|
2078
|
+
tools: () => {
|
|
2079
|
+
const tools = [];
|
|
2080
|
+
for (const entry of mcpRegistry.list()) {
|
|
2081
|
+
if (entry.status !== `ready`) continue;
|
|
2082
|
+
const live = mcpRegistry.get(entry.name);
|
|
2083
|
+
if (!live?.transport) continue;
|
|
2084
|
+
for (const t of entry.tools) tools.push(bridgeMcpTool({
|
|
2085
|
+
server: entry.name,
|
|
2086
|
+
tool: t,
|
|
2087
|
+
client: live.transport.client,
|
|
2088
|
+
timeoutMs: live.config.timeoutMs
|
|
2089
|
+
}));
|
|
2090
|
+
const caps = live.transport.client.getServerCapabilities?.();
|
|
2091
|
+
if (caps?.resources) tools.push(...buildResourceTools({
|
|
2092
|
+
server: entry.name,
|
|
2093
|
+
client: live.transport.client,
|
|
2094
|
+
timeoutMs: live.config.timeoutMs
|
|
2095
|
+
}));
|
|
2096
|
+
if (caps?.prompts) tools.push(...buildPromptTools({
|
|
2097
|
+
server: entry.name,
|
|
2098
|
+
client: live.transport.client,
|
|
2099
|
+
timeoutMs: live.config.timeoutMs
|
|
2100
|
+
}));
|
|
2101
|
+
}
|
|
2102
|
+
return tools;
|
|
2146
2103
|
}
|
|
2147
2104
|
});
|
|
2148
|
-
|
|
2105
|
+
this.bootstrap = await createBuiltinAgentHandler({
|
|
2106
|
+
agentServerUrl: this.options.agentServerUrl,
|
|
2107
|
+
workingDirectory: this.options.workingDirectory,
|
|
2108
|
+
streamFn: this.options.mockStreamFn,
|
|
2109
|
+
createElectricTools: this.options.createElectricTools,
|
|
2110
|
+
publicUrl,
|
|
2111
|
+
runtimeName: `builtin-agents`,
|
|
2112
|
+
serverHeaders: pullWake.headers
|
|
2113
|
+
});
|
|
2114
|
+
if (!this.bootstrap) throw new Error(`ANTHROPIC_API_KEY or OPENAI_API_KEY must be set before starting builtin agents`);
|
|
2115
|
+
await registerBuiltinAgentTypes(this.bootstrap);
|
|
2116
|
+
const registeredRunner = pullWake.registerRunner ? await this.registerPullWakeRunner(pullWake) : null;
|
|
2117
|
+
this.pullWakeRunner = createPullWakeRunner({
|
|
2118
|
+
baseUrl: this.options.agentServerUrl,
|
|
2119
|
+
runnerId: pullWake.runnerId,
|
|
2120
|
+
runtime: this.bootstrap.runtime,
|
|
2121
|
+
headers: pullWake.headers,
|
|
2122
|
+
claimHeaders: pullWake.claimHeaders,
|
|
2123
|
+
claimTokenHeader: pullWake.claimTokenHeader,
|
|
2124
|
+
heartbeatIntervalMs: pullWake.heartbeatIntervalMs,
|
|
2125
|
+
leaseMs: pullWake.leaseMs,
|
|
2126
|
+
offset: registeredRunner?.wake_stream_offset,
|
|
2127
|
+
onError: (error) => {
|
|
2128
|
+
serverLog.error(`[builtin-agents] pull-wake runner failed`, error);
|
|
2129
|
+
return true;
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
this.pullWakeRunner.start();
|
|
2133
|
+
serverLog.info(`[builtin-agents] pull-wake runner started: ${pullWake.runnerId}`);
|
|
2134
|
+
return `pull-wake:${pullWake.runnerId}`;
|
|
2135
|
+
} catch (error) {
|
|
2136
|
+
await this.stop().catch(() => {});
|
|
2137
|
+
throw error;
|
|
2138
|
+
}
|
|
2149
2139
|
}
|
|
2150
2140
|
async stop() {
|
|
2141
|
+
if (this.pullWakeRunner) {
|
|
2142
|
+
await this.pullWakeRunner.stop().catch((e) => {
|
|
2143
|
+
serverLog.error(`[builtin-agents] pull-wake runner stop failed`, e);
|
|
2144
|
+
});
|
|
2145
|
+
this.pullWakeRunner = null;
|
|
2146
|
+
}
|
|
2151
2147
|
if (this.bootstrap) {
|
|
2152
2148
|
this.bootstrap.runtime.abortWakes();
|
|
2153
|
-
await Promise.race([this.bootstrap.runtime.drainWakes().catch(() => {
|
|
2149
|
+
await Promise.race([this.bootstrap.runtime.drainWakes().catch((err) => {
|
|
2150
|
+
serverLog.error(`[builtin-agents] drainWakes failed during shutdown:`, err);
|
|
2151
|
+
}), new Promise((resolve) => setTimeout(resolve, 5e3))]);
|
|
2154
2152
|
this.bootstrap = null;
|
|
2155
2153
|
}
|
|
2156
2154
|
this.mcpStopping = true;
|
|
@@ -2173,39 +2171,29 @@ var BuiltinAgentsServer = class {
|
|
|
2173
2171
|
});
|
|
2174
2172
|
this._mcpRegistry = null;
|
|
2175
2173
|
}
|
|
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
2174
|
this.mcpStopping = false;
|
|
2184
|
-
this._url = null;
|
|
2185
|
-
this.publicBaseUrl = null;
|
|
2186
2175
|
}
|
|
2187
|
-
async
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
const
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2176
|
+
async registerPullWakeRunner(pullWake) {
|
|
2177
|
+
const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
|
|
2178
|
+
headers.set(`content-type`, `application/json`);
|
|
2179
|
+
const response = await fetch(appendPathToUrl(this.options.agentServerUrl, `/_electric/runners`), {
|
|
2180
|
+
method: `POST`,
|
|
2181
|
+
headers,
|
|
2182
|
+
body: JSON.stringify({
|
|
2183
|
+
id: pullWake.runnerId,
|
|
2184
|
+
owner_user_id: pullWake.ownerUserId,
|
|
2185
|
+
label: pullWake.label ?? `Built-in agents`,
|
|
2186
|
+
kind: `local`,
|
|
2187
|
+
admin_status: `enabled`
|
|
2188
|
+
})
|
|
2189
|
+
});
|
|
2190
|
+
if (!response.ok) throw new Error(`Failed to register pull-wake runner ${pullWake.runnerId}: ${response.status} ${await response.text()}`);
|
|
2191
|
+
return await response.json();
|
|
2202
2192
|
}
|
|
2203
2193
|
};
|
|
2204
2194
|
|
|
2205
2195
|
//#endregion
|
|
2206
2196
|
//#region src/entrypoint-lib.ts
|
|
2207
|
-
const DEFAULT_HOST = `127.0.0.1`;
|
|
2208
|
-
const DEFAULT_PORT = 4448;
|
|
2209
2197
|
function readEnv(env, names) {
|
|
2210
2198
|
for (const name of names) {
|
|
2211
2199
|
const value = env[name]?.trim();
|
|
@@ -2218,13 +2206,6 @@ function readRequiredEnv(env, names, description) {
|
|
|
2218
2206
|
if (value) return value;
|
|
2219
2207
|
throw new Error(`Missing ${description}. Set one of: ${names.map((name) => `"${name}"`).join(`, `)}`);
|
|
2220
2208
|
}
|
|
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
2209
|
function validateUrl(name, value) {
|
|
2229
2210
|
try {
|
|
2230
2211
|
new URL(value);
|
|
@@ -2233,20 +2214,63 @@ function validateUrl(name, value) {
|
|
|
2233
2214
|
throw new Error(`Invalid ${name}: "${value}"`);
|
|
2234
2215
|
}
|
|
2235
2216
|
}
|
|
2217
|
+
function buildAssertedAuthHeaders(env) {
|
|
2218
|
+
const headers = {};
|
|
2219
|
+
const email = readEnv(env, [`ELECTRIC_ASSERTED_AUTH_EMAIL`]);
|
|
2220
|
+
const name = readEnv(env, [`ELECTRIC_ASSERTED_AUTH_NAME`]);
|
|
2221
|
+
if (email) headers[`X-Electric-Asserted-Email`] = email;
|
|
2222
|
+
if (name) headers[`X-Electric-Asserted-Name`] = name;
|
|
2223
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
2224
|
+
}
|
|
2225
|
+
function parseAdditionalServerHeaders(env) {
|
|
2226
|
+
const raw = readEnv(env, [`ELECTRIC_AGENTS_SERVER_HEADERS`]);
|
|
2227
|
+
if (!raw) return void 0;
|
|
2228
|
+
let parsed;
|
|
2229
|
+
try {
|
|
2230
|
+
parsed = JSON.parse(raw);
|
|
2231
|
+
} catch {
|
|
2232
|
+
throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: expected JSON`);
|
|
2233
|
+
}
|
|
2234
|
+
if (!parsed || typeof parsed !== `object` || Array.isArray(parsed)) throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: expected a JSON object`);
|
|
2235
|
+
const headers = new Headers();
|
|
2236
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
2237
|
+
if (typeof value !== `string`) throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: header "${name}" must be a string`);
|
|
2238
|
+
headers.set(name, value);
|
|
2239
|
+
}
|
|
2240
|
+
const normalized = Object.fromEntries(headers.entries());
|
|
2241
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
2242
|
+
}
|
|
2243
|
+
function mergeHeaders(...sources) {
|
|
2244
|
+
const headers = new Headers();
|
|
2245
|
+
for (const source of sources) {
|
|
2246
|
+
if (!source) continue;
|
|
2247
|
+
new Headers(source).forEach((value, key) => headers.set(key, value));
|
|
2248
|
+
}
|
|
2249
|
+
const merged = Object.fromEntries(headers.entries());
|
|
2250
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
2251
|
+
}
|
|
2252
|
+
function hasHeader(headers, name) {
|
|
2253
|
+
return headers ? new Headers(headers).has(name) : false;
|
|
2254
|
+
}
|
|
2236
2255
|
function resolveBuiltinAgentsEntrypointOptions(env = process.env, cwd = process.cwd()) {
|
|
2237
2256
|
const agentServerUrl = validateUrl(`agent server URL`, readRequiredEnv(env, [`ELECTRIC_AGENTS_SERVER_URL`, `ELECTRIC_AGENTS_BASE_URL`], `agent server base URL`));
|
|
2238
|
-
const
|
|
2257
|
+
const runnerId = readRequiredEnv(env, [`ELECTRIC_AGENTS_PULL_WAKE_RUNNER_ID`, `PULL_WAKE_RUNNER_ID`], `pull-wake runner id`);
|
|
2258
|
+
const serverHeaders = mergeHeaders(buildAssertedAuthHeaders(env), parseAdditionalServerHeaders(env));
|
|
2239
2259
|
return {
|
|
2240
2260
|
agentServerUrl,
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2261
|
+
workingDirectory: readEnv(env, [`ELECTRIC_AGENTS_WORKING_DIRECTORY`, `WORKING_DIRECTORY`]) ?? cwd,
|
|
2262
|
+
pullWake: {
|
|
2263
|
+
runnerId,
|
|
2264
|
+
registerRunner: readEnv(env, [`ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER`]) === `true` || readEnv(env, [`ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER`]) === `1`,
|
|
2265
|
+
headers: serverHeaders,
|
|
2266
|
+
claimHeaders: serverHeaders,
|
|
2267
|
+
claimTokenHeader: hasHeader(serverHeaders, `authorization`) ? `electric-claim-token` : void 0
|
|
2268
|
+
}
|
|
2245
2269
|
};
|
|
2246
2270
|
}
|
|
2247
|
-
async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd(), createServer
|
|
2271
|
+
async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd(), createServer = (options$1) => new BuiltinAgentsServer(options$1) } = {}) {
|
|
2248
2272
|
const options = resolveBuiltinAgentsEntrypointOptions(env, cwd);
|
|
2249
|
-
const server = createServer
|
|
2273
|
+
const server = createServer(options);
|
|
2250
2274
|
const url = await server.start();
|
|
2251
2275
|
return {
|
|
2252
2276
|
options,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Built-in Electric Agents runtimes such as Horton and worker",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"./package.json": "./package.json"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@durable-streams/state": "
|
|
31
|
+
"@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@350",
|
|
32
32
|
"@mariozechner/pi-agent-core": "^0.70.2",
|
|
33
33
|
"@mariozechner/pi-ai": "^0.70.2",
|
|
34
34
|
"@sinclair/typebox": "^0.34.48",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"pino-pretty": "^13.0.0",
|
|
39
39
|
"sqlite-vec": "^0.1.9",
|
|
40
40
|
"zod": "^4.3.6",
|
|
41
|
-
"@electric-ax/agents-mcp": "0.2.
|
|
42
|
-
"@electric-ax/agents-runtime": "0.
|
|
41
|
+
"@electric-ax/agents-mcp": "0.2.1",
|
|
42
|
+
"@electric-ax/agents-runtime": "0.2.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/better-sqlite3": "^7.6.13",
|