@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/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
- webhookPath?: string;
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 handleRequest;
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 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: {
@@ -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 === `message_received`) {
625
- const eventQuestion = findLatestQuestion(events.filter((event) => event.type === `message_received`).map((event) => event.value));
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 (uses your ANTHROPIC_API_KEY)`);
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 = `${agentServerUrl}${DEFAULT_BUILTIN_AGENT_HANDLER_PATH}`, workingDirectory, streamFn, createElectricTools, publicUrl, runtimeName } = options;
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.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
- });
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.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;
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
- 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`
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
- 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);
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(() => {}), new Promise((resolve) => setTimeout(resolve, 5e3))]);
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 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` }));
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 baseUrl = readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_BASE_URL`, `BUILTIN_AGENTS_BASE_URL`]);
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
- 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
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: createServer$1 = (options$1) => new BuiltinAgentsServer(options$1) } = {}) {
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$1(options);
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 === "message_received") {
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 — "message_received", "wake", etc.
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 === "message_received") {
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` | `message_received` | `MessageReceived` | Inbound messages |
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 |