Embedded built-ins
The CLI commands electric agents start-builtin and electric agents quickstart run the built-in Horton and worker runtime for you. If you need to host those built-ins inside your own process, use the exported APIs from @electric-ax/agents.
BuiltinAgentsServer
BuiltinAgentsServer registers horton and worker, advertises the runtime's sandbox profiles, and starts a pull-wake runner that claims wakes from the Electric Agents server. This is the same model used by the CLI and desktop app.
import { BuiltinAgentsServer } from "@electric-ax/agents"
const server = new BuiltinAgentsServer({
agentServerUrl: "http://localhost:4437",
workingDirectory: process.cwd(),
loadProjectMcpConfig: true,
pullWake: {
runnerId: "builtin-agents",
ownerPrincipal: "/principal/system%3Abuiltin-agents",
registerRunner: true,
},
})
const runtimeUrl = await server.start()
console.log(runtimeUrl) // "pull-wake:builtin-agents"
// Later, during shutdown:
await server.stop()Options
import type { RuntimeRouterConfig } from "@electric-ax/agents-runtime"
type CreateElectricTools = RuntimeRouterConfig["createElectricTools"]
interface BuiltinAgentsServerOptions {
agentServerUrl: string
workingDirectory?: string
mockStreamFn?: StreamFn
durableStreamsFetchCache?: DurableStreamsFetchCacheOptions | false
pullWake: {
runnerId: string
ownerPrincipal?: string
label?: string
registerRunner?: boolean
headers?: HeadersProvider
claimHeaders?: HeadersProvider
claimTokenHeader?: ClaimTokenHeader
heartbeatIntervalMs?: number
eventHeartbeatThrottleMs?: number
leaseMs?: number
}
enabledModelValues?: readonly string[] | null
baseSkillsDir?: string
createElectricTools?: CreateElectricTools
// MCP integration
extraMcpServers?: ReadonlyArray<McpServerConfig>
loadProjectMcpConfig?: boolean
mcpOAuthRedirectBase?: string
openAuthorizeUrl?: (url: string, server: string) => void
onConfigError?: (error: unknown) => void
}| Field | Description |
|---|---|
agentServerUrl | Electric Agents coordinator server URL. |
workingDirectory | Directory used by Horton and worker file tools. Defaults to process.cwd(). |
mockStreamFn | Optional test stream function. Lets you run without a real model provider. |
durableStreamsFetchCache | Optional process-wide HTTP cache tuning for Undici-backed fetch calls. Pass false to leave the global dispatcher unchanged. |
pullWake | Pull-wake runner configuration. runnerId identifies this runtime to the server. Set registerRunner: true when this process should create/update the runner record. |
enabledModelValues | Optional allowlist of model values exposed by built-in agent creation schemas. Values use the model catalog's provider:model form. |
baseSkillsDir | Override for the bundled skills directory, useful when an embedder packages @electric-ax/agents. |
createElectricTools | Optional factory for tools injected into built-in agent handlers. The default built-in factory includes webhook-source and schedule tools; override or wrap it when adding host-specific tools. |
extraMcpServers | MCP servers contributed by the embedder. On name conflict with mcp.json, mcp.json wins. authorizationCode servers are auto-wired with keychainPersistence. |
loadProjectMcpConfig | Load <workingDirectory>/mcp.json (and watch it). Off by default because stdio MCP servers can spawn local commands, so embedders must opt in. The Electron desktop and electric-ax CLI opt in. |
mcpOAuthRedirectBase | Base for OAuth redirect URIs (full URI is <base>/oauth/callback/<server-name>). Must be stable across restarts so DCR client info stays valid. The runtime never listens at this URI; the embedder intercepts the redirect. |
openAuthorizeUrl | Hook invoked when an authorizationCode MCP server first needs user consent. Receives the SDK-generated authorize URL. The desktop opens it in a sandboxed BrowserWindow; headless embedders can read the URL from the authenticating envelope of addServer and surface it themselves. |
onConfigError | Invoked when applying an MCP config (initial boot or watcher reload) fails. Errors are always logged; this hook is for surfacing them programmatically. |
Without mockStreamFn, at least one supported provider must be configured before the built-in handler starts: ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, MOONSHOT_API_KEY, or a valid OpenAI Codex CLI auth file for the openai-codex provider.
Pull-wake headers and principals
When the server enforces principals or auth, pass the same headers to runner registration and wake claims:
const serverHeaders = {
"electric-principal": "service:local-runtime",
authorization: `Bearer ${process.env.ELECTRIC_AGENTS_TOKEN}`,
}
const server = new BuiltinAgentsServer({
agentServerUrl: "http://localhost:4437",
pullWake: {
runnerId: "local-runtime",
ownerPrincipal: "/principal/service%3Alocal-runtime",
registerRunner: true,
headers: serverHeaders,
claimHeaders: serverHeaders,
claimTokenHeader: "electric-claim-token",
},
})Use claimTokenHeader: "electric-claim-token" when your authorization header is reserved for server auth. Otherwise the default claim token transport is the standard Authorization: Bearer <claim-token> header.
createBuiltinAgentHandler
Use createBuiltinAgentHandler() when you need the lower-level registry/runtime objects. If you pass serveEndpoint, registerTypes() registers webhook dispatch for the built-in types. If you are using pull-wake, prefer BuiltinAgentsServer, which wires runner registration, MCP, sandbox profiles, and wake claiming for you.
import {
createBuiltinAgentHandler,
registerBuiltinAgentTypes,
} from "@electric-ax/agents"
const bootstrap = await createBuiltinAgentHandler({
agentServerUrl: "http://localhost:4437",
serveEndpoint: "https://example.com/_electric/builtin-agent-handler",
workingDirectory: process.cwd(),
})
if (!bootstrap) {
throw new Error("No supported model provider is configured")
}
await registerBuiltinAgentTypes(bootstrap)
// In your HTTP server:
await bootstrap.handler(req, res)Result
interface AgentHandlerResult {
handler(req: IncomingMessage, res: ServerResponse): Promise<void>
runtime: RuntimeHandler
registry: EntityRegistry
typeNames: string[]
skillsRegistry: SkillsRegistry | null
shutdownSandboxes: (() => Promise<void>) | null
modelCatalog: BuiltinModelCatalog
}Call shutdownSandboxes() during process shutdown when it is present. modelCatalog is the catalog used by the built-in Horton and Worker definitions; pass it along if you register sibling built-in-style types directly with registerHorton() or registerWorker(). Most embedders should use registerBuiltinAgentTypes() instead.
Extra Electric Tools
Both BuiltinAgentsServer and createBuiltinAgentHandler() accept createElectricTools. The factory receives the same context shape as RuntimeRouterConfig.createElectricTools and can add host-specific tools to Horton.
If you do not provide a custom factory, the built-in runtime injects webhook-source tools and schedule tools (list_schedules, upsert_cron_schedule, upsert_future_send, and delete_schedule). If you replace the factory entirely, include those tools yourself when Horton should keep that behavior.
import { BuiltinAgentsServer } from "@electric-ax/agents"
import { Type } from "@sinclair/typebox"
const server = new BuiltinAgentsServer({
agentServerUrl: "http://localhost:4437",
pullWake: {
runnerId: "builtin-agents",
registerRunner: true,
},
createElectricTools: ({ entityUrl, upsertCronSchedule }) => [
{
name: "schedule_daily_summary",
label: "Schedule daily summary",
description: "Schedule a daily summary wake for this entity.",
parameters: Type.Object({
hour: Type.Number(),
}),
execute: async (_id, params) => {
const { hour } = params as { hour: number }
await upsertCronSchedule({
id: "daily-summary",
expression: `0 ${hour} * * *`,
payload: `Run daily summary for ${entityUrl}`,
})
return { content: [{ type: "text", text: "Scheduled." }], details: {} }
},
},
],
})Entrypoint Helpers
runBuiltinAgentsEntrypoint() reads environment variables, creates a BuiltinAgentsServer, and starts it. This is what the electric-agents package binary uses.
import {
resolveBuiltinAgentsEntrypointOptions,
runBuiltinAgentsEntrypoint,
} from "@electric-ax/agents"
const options = resolveBuiltinAgentsEntrypointOptions(process.env)
const { server, url } = await runBuiltinAgentsEntrypoint()
console.log(options.agentServerUrl, url)
await server.stop()Environment variables:
| Variable | Description |
|---|---|
ELECTRIC_AGENTS_SERVER_URL | Required coordinator server URL. |
ELECTRIC_AGENTS_BASE_URL | Legacy alias for ELECTRIC_AGENTS_SERVER_URL. |
ELECTRIC_AGENTS_PULL_WAKE_RUNNER_ID | Required pull-wake runner id. |
PULL_WAKE_RUNNER_ID | Legacy alias for ELECTRIC_AGENTS_PULL_WAKE_RUNNER_ID. |
ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER | Set to true or 1 to register/update the runner record before claiming. |
ELECTRIC_AGENTS_PRINCIPAL | Optional principal key sent as Electric-Principal. |
ELECTRIC_AGENTS_SERVER_HEADERS | Optional JSON object of additional server headers. |
ELECTRIC_AGENTS_WORKING_DIRECTORY | Working directory for file tools. |
WORKING_DIRECTORY | Legacy alias for ELECTRIC_AGENTS_WORKING_DIRECTORY. |
Built-in Agent APIs
The built-in agent exports are also available if you want to compose your own runtime:
| Export | Purpose |
|---|---|
registerHorton() | Register the horton type on an EntityRegistry. |
registerWorker() | Register the worker type on an EntityRegistry. |
HORTON_MODEL | Default model id used by Horton and worker. |
buildHortonSystemPrompt() | Build Horton's system prompt for a working directory. |
createHortonTools() | Create Horton's base shell/file/search/worker tools. |
createSpawnWorkerTool() | Create the spawn_worker tool for another agent. |
WORKER_TOOL_NAMES | Valid primitive tool names for workers. |
createHortonDocsSupport() | Create Horton's docs knowledge-base support. |
For the behavior of horton and worker, see Horton and Worker.