9.2 KiB
Agent Note: 基于 AsyncLocalStorage 的发起 Agent 作用域
Status: implemented
English | 中文
问题
harness 中存在两种有用但不同的上下文概念。Cordis Context 负责选择服务、注册归属和生命周期;agent.ctx 是一个存活 Agent 所拥有的扁平注册作用域。Agent 与会话身份描述的则是异步操作主体。若把根 ctx.agent 改成「当前正在运行的 Agent」,就会混淆这两种含义,并在单进程并发驱动多个 Agent 时失效。
进程内深层基础设施有时需要在显式传递的循环、工具及请求参数之下获取可信的发起 Agent,例如宿主感知传输层、追踪辅助函数、日志器或网关客户端。要求每个私有辅助函数都转发 agent 会造成重复,而进程级可变槽会在跨 await 时发生并发错误。模型可见参数也不适用,因为模型不得选择可信的会话或路由请求头。该载体归 Agent 服务所有,而非模型可见的可选上下文。
决策
必需的 ctx.agents 服务使用 Node AsyncLocalStorage 携带发起 Agent。它直接存储同一个 Agent,不引入只有一个字段的帧;另一个私有运行标记只记录嵌套边界的谱系,供 teardown 记账使用,不携带身份。核心数据目录标明了所携带的类型。
currentInitiator() 用于可选读取,requireInitiator() 抛出 no initiating agent is active,withInitiator(agent, operation) 保留操作返回的同步值或 Promise 本身。withoutInitiator(operation) 会建立清空边界,供不得继承 Agent 的工作使用。会话仍通过 agent.session 推导;轮次、步骤、工具调用、signal、模型、cwd、沙箱和授权继续由现有归属方管理。
AgentLoop 已经注入 ctx.agents,并用 agents.withInitiator(agent, ...) 包裹每个具体驱动的完整 runLoop 生命周期。循环、轮次、步骤和工具调用的包内私有入口从 ctx.agents 恢复同一个 Agent,一次推导 agent.session,再由操作内辅助函数捕获该值,避免在浅层接口中转发具体驱动或 Session。若 Session 本身就是底层辅助函数的实际接口,该函数会保留狭窄的 Session 参数,而不会只为隐式查找而接收更宽泛的 Context。
因此,并发驱动使用彼此独立的存储。子驱动的异步延续携带子 Agent;withInitiator() 返回后,调用方立即恢复之前的存储,而活动运行计数仍持续跟踪返回的 Promise,直到其结束。创建、持久化加载和尚未发布的 setup(agentCtx) 位于子驱动边界之外:由父 Agent 发起的创建使用父身份,而 agentCtx.agent 显式标识子 Agent。
隐式身份不会取代显式契约。ToolExecution.agent、AssembleContext.agent、GenerateOptions.sessionId、任务归属、父子请求、ctx.agent、agentCtx.agent、审批与 hook 主体、cwd 选择、取消、worker 和进程消息、持久化记录及协议身份都保持显式传递。远程边界会把所需身份写入类型化请求,因为 ALS 只在进程内有效。
AgentRegistry 管理一个有序的发起方生命周期。teardown 会先拒绝新边界;移除 ctx.agents 后,AgentLoop 等注入方开始排空,注册表随后等待活动的返回 Promise 边界,最后调用 AsyncLocalStorage.disable()。如果某个边界继承的异步调用链启动所属 Cordis fiber 的卸载,私有运行标记谱系会从排空范围中释放该嵌套边界链,从而避免 teardown 等待自身完成,同时继续排空无关边界。在普通排空期间,进行中代码可通过保留的服务引用继续调用 currentInitiator() 和 requireInitiator();dispose(资源释放)后,发起方方法会抛出 agent initiator scope is disposed。根 Context dispose 可能并发启动同级 fiber 的 teardown,因此除 Cordis 依赖顺序外仍必须统计活动边界。
发起方作用域不负责管理脱离返回链的工作:注册表排空只跟踪 withInitiator() 或 withoutInitiator() 返回的 Promise。边界内创建的异步资源会继承其存储,直到自身结束或 ALS 被禁用;所属 seam 必须显式停止未纳入返回 Promise 的工作。Agent 所属的前台工作会把完整生命周期纳入返回值,并保留显式取消契约。无关的定时器、队列和部署基础设施在 withoutInitiator(operation) 下启动;队列、worker、进程和协议边界必须序列化身份,不能期待 ALS 传播。
宿主感知的传输层可以从 ctx.agents.requireInitiator().session.id 推导由部署方拥有的 X-Harness-Session-Id 等请求头;模型可见 schema 和参数中不包含该请求头。本决策不让现有生产 MCP 或 Web 传输层采用此请求头。测试替身传输层用于证明可信边界,而不会把宿主路由策略分配给现有的提供方无关 seam。
本决策扩展 Agent 注册作用域契约及其运行时设计,不会改变其中 agent.ctx 的静态含义。
验证
Agent 服务测试锁定可选与必需读取、同步值及跨 realm Promise 的精确身份、内建 Promise 结束状态观察、并发、嵌套及清空边界、同步抛错或 Promise 拒绝后的恢复、普通与重入排空顺序及保留引用的错误。AgentLoop 集成测试锁定并发与嵌套驱动、无 Agent 调用、AgentRegistry 重启、根 Context 销毁,以及包内私有的循环和工具调度通过隐式查找完成。组合、模块图、构建及运行时闭包检查确保默认组合包、SDK 主干、Python 运行时闭包及直接 AgentLoop harness 通过 ctx.agents 完成接线,无需其他提供方。
测试替身形式的宿主感知传输层在内部推导 X-Harness-Session-Id,并验证工具 schema 与日志中记录的参数都不包含身份字段。服务有意不排空边界操作所返回 Promise 之外的异步工作;这类工作仍由所属方的显式停止契约管理。
考虑过的替代方案
在每个函数中传递 Agent。 公开、worker、进程、持久化和协议边界继续显式传递,但要求每个进程内私有辅助函数都携带 Agent 只会造成重复转发,不会提高可信度。ALS 仅限于这些显式边界内部的异步调用链。
让 ctx.agent 变成动态值。 ctx.agent 已经表示与 Agent 作用域 Cordis 上下文静态关联的 Agent。改变根上下文的含义会混合注册作用域与执行作用域,并让并发行为变得意外。
新增独立的 ctx.agentExecution 服务。 该载体没有独立后端、配置或身份类型:它存储的是 ctx.agents 已经管理的同一个 Agent,而 AgentLoop 本就依赖该服务。第二个必需提供方会增加包、组合、生命周期、生成目录及测试 harness 接线,却没有拆出真实能力。
保存命名帧或完整运行时帧。 只有一个字段的 { agent } 帧只是包装该值,而 Agent、会话、inbox、取消、轮次、步骤、工具执行和持久化已经有各自的真源。增加更多字段会产生陈旧快照和另一套生命周期;直接携带 Agent,由方法名标识边界,无需重复保存状态。
包含步骤级 AbortSignal、cwd、沙箱或授权。 它们的生命周期及权限范围与驱动边界不一致,而且现有 seam 已经显式传递这些值。新增控制能力需要独立决策和嵌套生命周期契约。
使用进程级 currentAgent。 并发 Agent 和 subagent 会在异步延续执行之间相互覆盖,因此可变全局值只在 harness 不具备的串行保证下才正确。
从模型可见参数推导身份。 不能信任模型或用户输入来选择会话、租户或沙箱路由。
给每个能力 seam 增加路由身份。 这会把宿主关注点扩散到提供方无关 API。宿主感知实现拥有其传输请求头,而公开边界继续显式传递身份。
后果
深层基础设施可以获得一个可信的进程内发起 Agent,而无需加宽现有工具和能力请求。并发及嵌套驱动会自动隔离,AgentLoop 不增加新的必需服务,HMR(热模块替换)或根 Context dispose 会在禁用 ALS 前达到完全停稳。
该依赖不会出现在函数签名中,并且携带一个具有控制能力的 Agent 对象。消费方必须将其限制在横切基础设施中,把隐式存在视为既不证明存活、也不授予权限,并保留显式取消和归属检查。ALS 还有常驻传播成本,也无法跨越 worker、进程、HTTP 或持久化队列边界。
该销毁设计有意依赖 Node 的 Stability 1(实验性) API AsyncLocalStorage.disable()。Node 要求在 ALS 实例可被垃圾回收前调用 disable(),这对 HMR 替换 AgentRegistry 所拥有的实例尤为重要;服务状态守卫会阻止 dispose 后通过后续边界重新进入该实例。
该作用域有意只携带 Agent,省略轮次、步骤、signal、cwd、沙箱和授权。若真实消费方无法使用现有显式字段,必须另行论证扩展;陈旧字段最多只能误标遥测数据,绝不能授予控制权。