@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/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 USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST;
25
- const streams = [{ stream: pino.destination(LOG_FILE) }];
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 = `${agentServerUrl}${DEFAULT_BUILTIN_AGENT_HANDLER_PATH}`, workingDirectory, streamFn, createElectricTools, publicUrl, runtimeName } = options;
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.server) throw new Error(`Builtin agents server already started`);
1999
- return new Promise((resolve, reject) => {
2000
- this.server = createServer((req, res) => {
2001
- this.handleRequest(req, res).catch((error) => {
2002
- serverLog.error(`[builtin-agents] unhandled request error`, error);
2003
- if (!res.headersSent) {
2004
- res.writeHead(500, { "content-type": `application/json` });
2005
- res.end(JSON.stringify({ error: `Internal server error` }));
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.server.on(`error`, reject);
2010
- const host = this.options.host ?? `127.0.0.1`;
2011
- this.server.listen(this.options.port, host, async () => {
2012
- try {
2013
- const addr = this.server.address();
2014
- if (typeof addr === `string`) this._url = addr;
2015
- else if (addr) {
2016
- const resolvedHost = host === `0.0.0.0` ? `127.0.0.1` : host;
2017
- this._url = `http://${resolvedHost}:${addr.port}`;
2018
- } else throw new Error(`Could not determine builtin agents server address`);
2019
- this.publicBaseUrl = this.options.baseUrl ?? this._url;
2020
- const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
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
- this.bootstrap = await createBuiltinAgentHandler({
2131
- agentServerUrl: this.options.agentServerUrl,
2132
- serveEndpoint,
2133
- workingDirectory: this.options.workingDirectory,
2134
- streamFn: this.options.mockStreamFn,
2135
- createElectricTools: this.options.createElectricTools,
2136
- publicUrl,
2137
- runtimeName: `builtin-agents`
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
- if (!this.bootstrap) throw new Error(`ANTHROPIC_API_KEY or OPENAI_API_KEY must be set before starting builtin agents`);
2140
- await registerBuiltinAgentTypes(this.bootstrap);
2141
- serverLog.info(`[builtin-agents] webhook handler listening at ${serveEndpoint}`);
2142
- resolve(this._url);
2143
- } catch (error) {
2144
- await this.stop().catch(() => {});
2145
- reject(error);
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(() => {}), new Promise((resolve) => setTimeout(resolve, 5e3))]);
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 handleRequest(req, res) {
2188
- const method = req.method?.toUpperCase();
2189
- const pathname = new URL(req.url ?? `/`, `http://localhost`).pathname;
2190
- const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
2191
- if (pathname === `/_electric/health` && method === `GET`) {
2192
- res.writeHead(200, { "content-type": `application/json` });
2193
- res.end(JSON.stringify({ status: `ok` }));
2194
- return;
2195
- }
2196
- if (pathname === webhookPath && method === `POST` && this.bootstrap) {
2197
- await this.bootstrap.handler(req, res);
2198
- return;
2199
- }
2200
- res.writeHead(404, { "content-type": `application/json` });
2201
- res.end(JSON.stringify({ error: `Not found` }));
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 baseUrl = readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_BASE_URL`, `BUILTIN_AGENTS_BASE_URL`]);
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
- baseUrl: baseUrl ? validateUrl(`builtin agents base URL`, baseUrl) : void 0,
2242
- host: readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_HOST`, `HOST`]) ?? DEFAULT_HOST,
2243
- port: readPort(env),
2244
- workingDirectory: readEnv(env, [`ELECTRIC_AGENTS_WORKING_DIRECTORY`, `WORKING_DIRECTORY`]) ?? cwd
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: createServer$1 = (options$1) => new BuiltinAgentsServer(options$1) } = {}) {
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$1(options);
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.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": "npm:@electric-ax/durable-streams-state-beta@^0.3.1",
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.0",
42
- "@electric-ax/agents-runtime": "0.1.3"
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",