Electric Agents RuntimeHandler 深度指南:Webhook 唤醒路由、类型注册与部署配置
【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric
RuntimeHandler是@electric-ax/agents-runtime包面向宿主应用暴露的运行时入口工厂:它创建一个负责接收 Electric Agents runtime server 发来的 webhook 唤醒请求的路由器,并在启动时自动注册所有实体类型。本文结合该包的 API 参考文档与仓库源码,完整讲解RuntimeRouter/RuntimeHandler的接口契约、RuntimeRouterConfig全部配置项、webhook 签名校验、唤醒生命周期管理以及如何接入 Node HTTP 服务。
读完本文,你将掌握:如何用createRuntimeHandler搭建一个可被 Electric Agents 平台唤醒的实体运行时、如何理解并配置每个关键参数(serveEndpoint、webhookSignature、idleTimeout、sandboxProfiles等)、以及如何正确地进行优雅关闭与错误处理。
本文对应的官方 API 参考文档位于 website/docs/agents/reference/runtime-handler.md,源码实现位于 packages/agents-runtime/src/create-handler.ts。
一、RuntimeHandler 在 Electric Agents 架构中的角色
在 Electric Agents 的运行时模型中,每个实体(entity)拥有一条 append-only 事件流。当服务端决定唤醒一个实体时,它通过 webhook 将唤醒通知投递给实体所在的宿主进程;宿主进程中的RuntimeHandler就是这个 webhook 的接收与分发入口。
根据源码注释(create-handler.ts)的定位说明,create-handler.ts提供:
- Runtime router factory—— 创建一个 fetch 原生的请求路由器,负责 webhook 唤醒投递;
- 兼容的 Node HTTP 适配器—— 将 Node 的
IncomingMessage/ServerResponse桥接为 fetchRequest/Response。
工厂函数返回的对象承担两类职责:
- webhook 唤醒投递:接收 Electric Agents runtime server 发来的唤醒通知(
WakeNotification/WebhookNotification),解码、验签、查找到对应实体类型后,在后台异步执行该实体的 handler; - 启动时类型注册:将
defineEntity注册的所有实体类型以 upsert 语义批量注册到服务端(/_electric/entity-types),使平台知晓该宿主进程可承载哪些实体。
在@electric-ax/agents-runtime的 README 中,createRuntimeHandler()被描述为"Electric Agents 在实体被唤醒时调用的 webhook 入口"(README.md),足见其在运行时中的枢纽地位。
二、RuntimeRouter:核心接口契约
RuntimeRouter是运行时路由器的抽象接口,RuntimeHandler在其基础上扩展了 Node HTTP 适配能力。两者均由 create-handler.ts 定义并从包入口导出(index.ts)。
interface RuntimeRouter { handleRequest(request: Request): Promise<Response | null> handleWebhookRequest(request: Request): Promise<Response> dispatchWake( notification: WakeNotification, options?: Pick<ProcessWakeConfig, "claimHeaders" | "claimTokenHeader"> ): void dispatchWebhookWake(notification: WebhookNotification): void drainWakes(): Promise<void> waitForSettled(): Promise<void> abortWakes(): void debugState(): RuntimeDebugState readonly typeNames: string[] readonly sandboxProfileDescriptors: Array<{ name: string label: string description?: string remote?: boolean }> registerTypes(): Promise<void> }各方法的职责与调用链如下:
| 方法 | 返回类型 | 说明 |
|---|---|---|
handleRequest(request) | Promise<Response \| null> | 路由 fetchRequest。若请求路径与webhookPath不匹配则返回null,否则委托给handleWebhookRequest |
handleWebhookRequest(request) | Promise<Response> | 直接处理 webhook 请求,不做路径匹配 |
dispatchWake(notification, opts?) | void | 从任意传输通道分发一个已解析的唤醒通知 |
dispatchWebhookWake(notification) | void | 分发已解析的 webhook 通知,在后台运行唤醒 handler |
drainWakes() | Promise<void> | 等待所有在途唤醒 handler 收敛;若任一唤醒出错则抛出异常 |
waitForSettled() | Promise<void> | 等待所有在途唤醒 handler 收敛(drainWakes的友好别名) |
abortWakes() | void | 中止在途唤醒,使宿主进程可以快速关闭 |
debugState() | RuntimeDebugState | 返回运行时本地快照,用于测试与关闭诊断 |
typeNames | string[] | 所有已注册实体类型的名称(只读) |
sandboxProfileDescriptors | Array<{name, label, description?, remote?}> | 本运行时对外发布的 sandbox 沙箱配置档的线格式描述(只读) |
registerTypes() | Promise<void> | 将所有实体类型注册到 Electric Agents runtime server,采用 upsert 语义,每次启动均可安全调用 |
注意:源码中的
RuntimeRouter接口还包含一个文档未列出的isWakeActive(streamPath): boolean方法,用于判断某个流路径的唤醒是否已在途(create-handler.ts),可视为内部诊断能力。
2.1 请求处理流程(handleWebhookRequest 内部逻辑)
从 create-handler.ts 可以看到handleWebhookRequest的完整处理管线:
- 方法校验:非
POST请求直接返回405 Method not allowed; - 签名校验:若启用了
webhookSignature,读取请求头webhook-signature,用verifyWebhookSignature校验签名,失败时返回相应错误与状态码; - JSON 解析:将请求体解码为
WebhookNotification,解析失败返回400 Invalid JSON; - 实体类型查找:从通知中取
entity.type,通过注册表或模块级注册表查找实体类型;未找到返回503 Unknown entity type; - 分发执行:
dispatchWebhookWake(notification)在后台执行唤醒,立即返回200 { ok: true }。
2.2 路径路由(handleRequest)
const handleRequest = async (request: Request): Promise<Response | null> => { const pathname = new URL(request.url).pathname if (pathname !== webhookPath) return null return handleWebhookRequest(request) }handleRequest仅做 URL pathname 比对,命中webhookPath才继续处理,否则返回null交给上层框架继续路由(create-handler.ts)。
2.3 唤醒生命周期管理
dispatchWake的实现(create-handler.ts)揭示了唤醒后台执行的关键细节:
- 每次唤醒创建一个
AbortController作为关闭信号,调用processWake(notification, {...wakeConfig, shutdownSignal}); - 唤醒 Promise 被登记到
pendingWakes集合,同时以streamPath作为 label 记录,用于isWakeActive与诊断; - 唤醒失败时:优先调用
config.onWakeError?.(error)观察器,只有返回true(表示已处理)才不会把错误收集进wakeErrors数组;未处理错误会记录日志并在drainWakes时重新抛出; - 唤醒结束(无论成败)都会从三个登记结构中移除。
drainWakes(create-handler.ts)循环等待pendingWakes全部清空,之后若wakeErrors非空则抛出一个错误或AggregateError;abortWakes遍历所有AbortController触发中止;debugState返回RuntimeDebugState快照。
三、RuntimeHandler:Node HTTP 适配器
interface RuntimeHandler extends RuntimeRouter { onEnter(req: IncomingMessage, res: ServerResponse): Promise<void> }onEnter是 Node HTTP 兼容适配器,用于将传统 Nodehttp.createServer回调桥接到 fetch 世界:
| 方法 | 参数 | 说明 |
|---|---|---|
onEnter(req, res) | NodeIncomingMessage、ServerResponse | 将请求转换为 fetchRequest并委托给handleWebhookRequest |
从源码实现看(create-handler.ts),createRuntimeHandler内部先创建createRuntimeRouter(config),再包装出onEnter:
toFetchRequest(req)读取请求体、合并 headers、构造Request;若读取失败返回400 Request body read failed;- 调用
router.handleWebhookRequest(request); sendNodeResponse(res, response)将 fetchResponse的 status/headers/body 写回ServerResponse。
const onEnter = async (req, res) => { let request: Request try { request = await toFetchRequest(req) } catch (err) { await sendNodeResponse(res, json({ error: `Request body read failed`, details: ... }, 400)) return } const response = await router.handleWebhookRequest(request) await sendNodeResponse(res, response) }toFetchRequest将 Node 的多个同名 header 追加为 fetch Headers 的多个值,URL 基于req.headers.host与req.url构造,body 非空时以Buffer传入(create-handler.ts)。
3.1 实战接线:接入 Node HTTP 服务器
结合仓库示例 examples/agents-playground/server.ts 与 examples/agents-chat-starter/src/server/index.ts,一个典型的宿主接线如下:
import http from 'node:http' import { createRuntimeHandler } from '@electric-ax/agents-runtime' const runtime = createRuntimeHandler({ baseUrl: `http://localhost:4437`, // Electric Agents runtime server serveEndpoint: `http://localhost:3000/webhook`, // 对外可访问的回调地址 }) const server = http.createServer(async (req, res) => { if (req.url === `/webhook` && req.method === `POST`) { await runtime.onEnter(req, res) return } res.writeHead(404) res.end() }) server.listen(3000, async () => { await runtime.registerTypes() // 启动时注册所有实体类型 console.log(`App server ready on port 3000`) })onEnter适合传统 Node HTTP 服务;如果宿主框架本身基于 fetch(如 Bun、Deno、Hono 适配层),则应优先使用handleRequest(request)(源码注释明确建议新集成优先使用 fetch 原生入口,见 create-handler.ts)。
四、RuntimeDebugState:关闭与测试诊断快照
interface RuntimeDebugState { pendingWakeCount: number pendingWakeLabels: string[] wakeErrorCount: number typeNames: string[] }| 字段 | 类型 | 说明 |
|---|---|---|
pendingWakeCount | number | 在途唤醒 handler 数量 |
pendingWakeLabels | string[] | 标识每个待处理唤醒的标签(用于诊断) |
wakeErrorCount | number | 已出错的唤醒 handler 数量 |
typeNames | string[] | 所有已注册实体类型的名称 |
debugState()由源码中的内部状态直接映射而来:pendingWakeCount即pendingWakes.size,pendingWakeLabels即标签 Map 的值集合,wakeErrorCount即wakeErrors.length,typeNames来自getRegisteredTypes()(create-handler.ts)。该快照是判断"宿主是否可以安全退出"的重要依据。
五、工厂函数
function createRuntimeRouter(config: RuntimeRouterConfig): RuntimeRouter function createRuntimeHandler(config: RuntimeHandlerConfig): RuntimeHandlercreateRuntimeRouter:创建纯 fetch 路由器(含请求处理、唤醒分发、类型注册);createRuntimeHandler:在其之上叠加 Node HTTP 适配器onEnter;RuntimeHandlerConfig与RuntimeRouterConfig是同一类型(export type RuntimeHandlerConfig = RuntimeRouterConfig,见 create-handler.ts),两个工厂接受完全相同的配置对象。
六、RuntimeRouterConfig 全部配置项详解
配置接口完整定义见 create-handler.ts,与文档 API 参考一致。以下为全部字段的默认值与语义:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
baseUrl | string | -(必填) | Electric Agents runtime server 的基地址,如"http://localhost:4437" |
serveEndpoint | string | - | 你的应用对外暴露的完整 webhook 回调 URL,用于类型注册 |
webhookPath | string | serveEndpoint/handlerUrl的 pathname,兜底"/electric-agents" | handleRequest()匹配的路径 |
handlerUrl | string | - | serveEndpoint的向后兼容别名,新代码请优先用serveEndpoint |
registry | EntityRegistry | 模块级默认注册表 | 本 handler 使用的实体注册表 |
subscriptionPathForType | (typeName: string) => string | - | 覆盖每个实体类型注册时的 webhook 订阅路径 |
defaultDispatchPolicyForType | (typeName: string) => DispatchPolicy \| undefined | - | 覆盖每个实体类型注册的默认分发策略 |
serverHeaders | HeadersProvider | - | 发送给 agents server 控制面请求(类型注册、唤醒认领)的附加请求头 |
webhookSignature | false \| Partial<WebhookSignatureVerifierConfig> | 默认启用,JWKS 地址为${baseUrl}/__ds/jwks.json | webhook 签名校验配置;仅在可信进程内测试时设为false |
idleTimeout | number | 20000 | 关闭一次唤醒前的空闲超时(毫秒) |
heartbeatInterval | number | 10000 | 心跳间隔(毫秒) |
createElectricTools | (context) => AgentTool[] \| Promise<...> | - | 可选工具工厂,在每次唤醒执行 handler 前调用,为 agent 注入额外工具 |
onWakeError | (error: Error) => boolean \| void | - | 后台唤醒失败的观察器;返回true表示错误已处理,不会在 drain 时重新抛出 |
registrationConcurrency | number | 8 | 实体类型注册的最大并发数 |
sandboxProfiles | ReadonlyArray<SandboxProfile> | - | 本运行时发布的命名沙箱配置档;spawn 请求可按 profile 名称选择 |
publicUrl | string | - | 本运行时的公网 URL,转发给 agents server 用于GET /api/runtimes |
name | string | "default" | 运行时可读名称,用于运行时元数据去重 |
6.1 baseUrl 与 serveEndpoint:必填的两个端点
baseUrl指向Electric Agents runtime server(Durable Streams 服务端),控制面调用(类型注册/_electric/entity-types、JWKS 拉取、唤醒认领)都基于它拼接 URL,参见appendPathToUrl(baseUrl, "/_electric/entity-types")(create-handler.ts)。serveEndpoint是你自己的宿主应用对外可达的 webhook 回调地址。类型注册时它被写入注册体的serve_endpoint字段,并在未显式提供default_dispatch_policy时自动生成指向该地址的 webhook 分发策略(create-handler.ts)。
webhookPath的推导逻辑位于normalizeConfig(create-handler.ts):
const serveEndpoint = config.serveEndpoint ?? config.handlerUrl const webhookPath = config.webhookPath ?? getPathname(serveEndpoint) ?? `/electric-agents`即优先取显式webhookPath,其次取serveEndpoint(或handlerUrl)的 pathname,最后兜底/electric-agents。
6.2 webhookSignature:默认启用的签名校验
webhookSignature在normalizeConfig中被归一化(create-handler.ts):
const webhookSignature = config.webhookSignature === false ? false : { jwksUrl: config.webhookSignature?.jwksUrl ?? appendPathToUrl(config.baseUrl, `/__ds/jwks.json`), toleranceSeconds: config.webhookSignature?.toleranceSeconds, cacheTtlMs: config.webhookSignature?.cacheTtlMs, fetchClient: config.webhookSignature?.fetchClient, }要点:
- 默认从
${baseUrl}/__ds/jwks.json拉取 JWKS 公钥集,无需手动配置即可启用签名校验; - 校验发生在请求头
webhook-signature,格式为t=<timestamp>,kid=<kid>,ed25519=<signature>(可从测试用例 create-handler.test.ts 看到构造方式); - 可覆盖
jwksUrl、toleranceSeconds(时间容差秒)、cacheTtlMs(JWKS 缓存 TTL)、fetchClient; - 仅当运行可信的进程内测试时才设为
false,生产环境务必保持默认启用。
仓库测试 create-handler.test.ts 会先clearRegistry()再 mockprocessWake,并构造带 Ed25519 签名的 webhook 请求验证验签与分发链路,是理解该配置行为的可执行范例。
6.3 idleTimeout 与 heartbeatInterval:唤醒生命周期控制
idleTimeout默认20_000ms:在关闭一次唤醒(wake)前允许的空闲时间上限;heartbeatInterval默认10_000ms:向服务端发送心跳的间隔。
两者通过wakeConfig传入processWake(create-handler.ts),共同决定一次实体唤醒的执行时限与保活节奏。
6.4 createElectricTools:为每次唤醒注入额外工具
createElectricTools是一个可选工具工厂,签名接收每次唤醒的上下文,返回AgentTool[]:
createElectricTools?: (context: { entityUrl: string entityType: string args: Readonly<Record<string, unknown>> db: EntityStreamDBWithActions events: Array<ChangeEvent> upsertCronSchedule(opts: { id: string; expression: string; timezone?: string payload?: unknown; debounceMs?: number; timeoutMs?: number }): Promise<{ txid: string }> upsertFutureSendSchedule(opts: { id: string; payload: unknown; targetUrl?: string fireAt: string; messageType?: string }): Promise<{ txid: string }> deleteSchedule(opts: { id: string }): Promise<{ txid: string }> listWebhookSources(): Promise<Array<WebhookSourceContract>> subscribeToWebhookSource(opts: WebhookSourceSubscriptionInput) : Promise<{ txid: string; subscription: WebhookSourceSubscription }> unsubscribeFromWebhookSource(opts: { id: string }): Promise<{ txid: string }> }) => AgentTool[] | Promise<AgentTool[]>上下文提供了实体 URL/类型、spawn 参数、实体 StreamDB(含操作)、本次唤醒触发的事件列表,以及定时任务(cron/future send)和 webhook source 订阅管理能力。在 examples/agents-playground/server.ts 中可以看到通过createElectricTools注入自定义工具集合的用法。
6.5 onWakeError:后台唤醒错误观察器
onWakeError?: (error: Error) => boolean | void调用时机在dispatchWake的 catch 分支:若返回true表示该错误已被上层处理,不会进入wakeErrors收集列表,因此drainWakes()不会因它抛错;否则错误会记录日志并被drainWakes重新抛出。这为宿主提供了"吞掉已知可恢复错误 vs 让关闭流程感知错误"的精确控制(create-handler.ts)。
6.6 registrationConcurrency:类型注册并发控制
默认8。registerTypes()使用一个简单的 worker 池实现并发注册(forEachWithConcurrency,见 create-handler.ts),并发度取Math.max(1, Math.min(concurrency, items.length))。宿主进程拥有大量实体类型时,可通过此参数控制注册请求对服务端的瞬时压力。
6.7 sandboxProfiles、publicUrl 与 name:运行时元数据
sandboxProfiles:注册到本运行时的命名沙箱配置档。每个 profile 是(name, label, description?, factory)元组——factory 闭包只保存在运行时本地,仅有描述性字段通过sandboxProfileDescriptors广告给 agents server 并呈现在 UI 选择器中;spawn 负载通过sandbox.profile指定,服务端会按目标 runner 广告的集合校验。重复的 profile 名称会在createRuntimeRouter时直接抛错(fail-fast,见 create-handler.ts);publicUrl:运行时公网 URL,转发给 agents server 后可出现在GET /api/runtimes的公共运行时列表中;省略则该运行时被排除在公共列表之外;name:默认"default",作为/api/runtimes去重键(last-write-wins),同名多实例注册时后者覆盖前者。
七、registerTypes 的类型注册机制
registerTypes()是启动流程的关键一步。从源码(create-handler.ts)看:
- 通过注册表(
registry ?? 模块级默认注册表)枚举所有实体类型; - 以
registrationConcurrency并发向${baseUrl}/_electric/entity-types发送POST; - 每个注册体由
buildEntityTypeRegistrationBody构造(create-handler.ts),包含:name、description、可选的creation_schema/inbox_schemas/slash_commands、合并了DEFAULT_STATE_SCHEMAS的state_schemas、externally_writable_collections、permission_grants,以及serve_endpoint与default_dispatch_policy; serveEndpoint存在时写入serve_endpoint;defaultDispatchPolicyForType有返回则优先使用,否则自动生成指向serveEndpoint的 webhook 分发策略,subscription_id为webhook:<typeName>(特殊字符会被替换为_,见runtimeWebhookSubscriptionId,create-handler.ts);- 任一类型注册失败(网络错误或非 2xx),最终抛出汇总错误(
registered.length/total registered (n failed: ...)); - 由于采用 upsert 语义,每次启动重复调用是安全的——文档与 README 都强调这是推荐的启动惯例。
Schema 序列化方面,toJsonSchema(create-handler.ts)依次尝试 Standard Schema 的~standard.jsonSchema.input()、自定义toJSONSchema()、JSON Schema 关键字检测,最后兜底用zod-to-json-schema转换,确保 Zod 等 schema 库都能被转成注册所需的 JSON Schema。
八、部署配置与优雅关闭实践
综合以上配置项,一个面向生产的最小配置示例:
const runtime = createRuntimeHandler({ baseUrl: `http://electric-agents:4437`, serveEndpoint: `https://agent-host.example.com/webhook`, // webhookSignature: 默认启用,自动使用 ${baseUrl}/__ds/jwks.json idleTimeout: 20_000, heartbeatInterval: 10_000, registrationConcurrency: 8, serverHeaders: async () => ({ authorization: `Bearer ${process.env.AGENTS_SERVER_TOKEN}`, }), onWakeError: (err) => { // 记录并吞掉预期内的可恢复错误,避免 drainWakes 抛错 return true }, name: `web-1`, // 多实例部署时用于 /api/runtimes 去重 publicUrl: `https://agent-host.example.com`, }) await runtime.registerTypes()优雅关闭建议按以下顺序执行:
- 停止接收新请求(从负载均衡器摘除、
server.close()); await runtime.drainWakes()等待所有在途唤醒收敛——若某个唤醒失败且未被onWakeError标记为已处理,这里会抛出Error/AggregateError,应记录为关闭诊断信息;- 若需要在有限时间内强制退出,可调用
runtime.abortWakes()中止在途唤醒; - 通过
runtime.debugState()输出pendingWakeCount/wakeErrorCount等快照,便于排查"为何关不掉"。
注意区分三个"等待/中止"方法:drainWakes()会因错误抛异常,waitForSettled()是其友好别名(同样会抛错,源码实现即await drainWakes(),见 create-handler.ts),abortWakes()则主动触发 AbortController 中断在途唤醒以加速关闭。
九、测试验证与进一步探索
仓库为createRuntimeHandler/createRuntimeRouter提供了完整测试套件:
- packages/agents-runtime/test/create-handler.test.ts:覆盖 webhook 签名构造与验签、
onEnter请求桥接、错误请求(如连接重置导致的400)、注册与唤醒分发等场景; - packages/agents-runtime/test/runtime-dsl.ts:集成测试 DSL,可运行完整的运行时流程;
- packages/agents-runtime/test/sandbox-profiles.test.ts:验证 sandbox profile 的重复名检测与描述符发布。
可运行的完整宿主示例:
- examples/agents-playground/server.ts:
createEntityRegistry+createRuntimeHandler+createElectricTools的组合范例; - examples/agents-chat-starter/src/server/index.ts:聊天类实体宿主的完整接线;
- examples/agents-walkthrough/src/index.ts:从零到一的渐进式运行时搭建。
若需绕过 webhook 而使用拉取式唤醒(pull-based wake),可查看包内导出的 createPullWakeRunner;webhook 验签的底层实现位于packages/agents-runtime/src/webhook-signature.ts。两者与RuntimeHandler共同构成了@electric-ax/agents-runtime的完整唤醒接收体系。
【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考