Files
deepseek-harness/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.zh.md
Yichen Jiang d261a9ca3b sync rfc
2026-07-18 08:56:46 +08:00

8.5 KiB
Raw Blame History

RFC: 基于 AsyncLocalStorage 的 Agent 执行上下文

Status: implemented

English | 中文

问题

Harness 中存在两种有用但不同的上下文概念。Cordis Context 负责选择服务、注册归属和生命周期;agent.ctx 是一个存活 Agent 所拥有的扁平注册作用域。Agent 与会话身份描述的则是异步操作主体。若把根 ctx.agent 改成「当前正在运行的 Agent」,就会混淆这两种含义,并在单进程并发驱动多个 Agent 时失效。

进程内深层基础设施仍需要可信的发起 Agent。能力传输层、追踪辅助函数、日志器和网关客户端可能位于显式 loop、工具及请求参数的下层。在每个私有辅助函数中传递 agent 会增加管道代码,而进程级可变槽会在 await 之间发生并发错误。模型可见参数同样不合适,因为模型不能选择可信的会话或路由请求头。它是必载控制基础设施,而不是模型可见的可选上下文增强。

决策

@deepseek-ai/dsh-agent-execution 使用 Node AsyncLocalStorage 提供必载的 ctx.agentExecution 服务。该帧只包含准确的存活 Agent:

export interface AgentExecution {
  readonly agent: Agent
}

export interface AgentExecutionService {
  current(): AgentExecution | undefined
  require(): AgentExecution
  run<T>(execution: AgentExecution | undefined, operation: () => T): T
}

current() 执行可选读取,require() 抛出 no agent execution context is active,run() 保留操作返回的准确同步值或 Promise。run(undefined, operation) 会建立真实的清空边界,供不得继承 Agent 的工作使用。一个同类实现曾观察到未清空的隐式值穿过已调度工作泄漏进后续轮次;显式 undefined 边界可以防止这类泄漏。会话仍通过 execution.agent.session 推导;轮次、步骤、工具调用、signal、模型、cwd、沙箱和授权继续由现有归属方管理。

AgentLoop 注入该服务,并用 agentExecution.run({ agent }, ...) 包裹每个具体驱动的完整 runLoop 生命周期。因此,并发驱动使用彼此独立的存储,子驱动会遮蔽父驱动,子边界结束后父存储得到恢复。创建、持久化加载和尚未发布的 setup(agentCtx) 位于子边界之外:由父 Agent 发起的创建使用父身份,而 agentCtx.agent 显式标识子 Agent。

隐式身份不会取代显式契约。ToolExecution.agent、AssembleContext.agent、GenerateOptions.sessionId、任务归属、父子请求、ctx.agent、agentCtx.agent、审批与 hook 主体、cwd 选择、取消、worker/进程消息、持久化记录及协议身份都保持显式传递。远程边界会把所需身份写入类型化请求,因为 ALS 只在进程内有效。

提供方使用有序复合 effect。teardown 会先拒绝新边界,再移除服务并等待 AgentLoop 等注入方排空,随后等待活动的返回 Promise 边界,最后调用 AsyncLocalStorage.disable()。排空期间,进行中代码可通过保留的服务引用继续调用 current() 和 require();dispose 后,保留引用会抛出 agent execution service is disposed。根 Context dispose 可能并发启动同级 fiber 的 teardown,因此除 Cordis 依赖顺序外还必须统计活动边界。

在 run() 内创建的异步资源会继承其存储,即使返回的操作没有等待它们。Agent 所拥有的前台工作可以继承 { agent },但仍使用其执行 seam 的显式取消和 dispose 契约。无关的定时器、队列和部署基础设施在 run(undefined, operation) 下启动,并拥有显式停止操作。队列、worker、进程和协议边界必须序列化身份,不能期待 ALS 传播。

宿主感知的传输层可以从 ctx.agentExecution.require().agent.session.id 推导由部署方拥有的 X-Harness-Session-Id 等请求头;模型可见 schema 和参数中不包含该请求头。本决策不让现有生产 MCP 或 Web 传输层采用此请求头。测试替身传输层用于证明可信边界,而不会把宿主路由策略分配给现有的提供方无关 seam。

本决策扩展 Agent 注册作用域契约及其运行时设计,不会改变其中 agent.ctx 的静态含义。

参考模型

Claude Code Harness 中的设计
AppState store Cordis 部署服务及其拥有的实时状态
QueryEngine Agent 驱动及 loop 所拥有的运行时状态
ToolUseContext 能力边界上的显式 Agent、工具和请求参数
AgentContext ALS 窄粒度 AgentExecution 载体
Transcript 事件溯源 Session 与持久化后端

验证

服务测试锁定可选与必需读取、同步与跨 await 传播、并发与嵌套边界、显式清空、throw 或 rejection 后的恢复、准确返回值身份、排空顺序及已 dispose 引用错误。AgentLoop 集成测试覆盖重叠的真实驱动、嵌套父子创建、无 Agent 的直接工具执行、提供方或根 Context teardown 期间的取消、服务重启,以及 Agent dispose 后保留的引用。针对脱离主调用链的异步工作的测试会防止上下文泄漏,同时不改变现有显式取消契约。

测试替身能力传输层在内部推导 X-Harness-Session-Id,并断言工具 schema 与记录的参数都不包含身份字段。组合测试和生成目录确保默认 bundle、SDK 主干、Python 运行时闭包及直接 AgentLoop harness 都装载提供方;缺少提供方时 AgentLoop 保持未激活。文档检查确保 AGENTS.md 中的仓库布局、packages/core/README.md 中的包表,以及 packages/README.md 中的分组说明都与该必载提供方保持一致。

考虑过的替代方案

在每个函数中传递 Agent。 公开、worker、进程、持久化和协议边界继续显式传递,但要求每个进程内私有辅助函数都携带 Agent 只会增加管道代码,不会提高可信度。ALS 仅限于这些显式边界内部的异步调用链。

让 ctx.agent 变成动态值。 ctx.agent 已经表示与 Agent 作用域 Cordis 上下文静态关联的 Agent。改变根上下文的含义会混合注册作用域与执行作用域,并让并发行为变得意外。

保存完整的可变运行时帧。 Agent、会话、inbox、取消、轮次、步骤、工具执行和持久化已经有各自的真源。重复保存会产生陈旧快照和另一套生命周期。包装对象为另行论证的陈旧安全标签保留扩展空间,而不会把存储简化成裸 Agent。

包含步骤级 AbortSignal、cwd、沙箱或授权。 它们的生命周期与权限不匹配驱动边界,而且现有 seam 已经显式传递这些值。新增控制能力需要独立决策和嵌套生命周期契约。

使用进程级 currentAgent。 并发 Agent 和 subagent 会在异步 continuation 间相互覆盖,因此可变全局值只在 Harness 不具备的串行保证下才正确。

从模型可见参数推导身份。 不能信任模型或用户输入来选择会话、租户或沙箱路由。

给每个能力 seam 增加路由身份。 这会把宿主关注点扩散到提供方无关 API。宿主感知实现拥有其传输请求头,而公开边界继续显式传递身份。

后果

深层基础设施可以获得一个可信的进程内发起 Agent,而无需加宽现有工具和能力请求。并发及嵌套驱动会自动隔离,缺少提供方时 AgentLoop 保持未激活,HMR 或根 Context dispose 会在禁用 ALS 前达到静止状态。

该依赖不会出现在函数签名中,并且携带一个存活能力对象。消费方必须将其限制在横切基础设施中,把隐式存在视为既不证明存活、也不授予权限,并保留显式取消和归属检查。ALS 还有常驻传播成本,也无法跨越 worker、进程、HTTP 或持久化队列边界。

该 teardown 设计有意接受 Node Stability 1(实验性) 的 AsyncLocalStorage.disable() 依赖。Node 要求在 ALS 实例可被垃圾回收前调用 disable(),这对 HMR 替换提供方所拥有的实例尤为重要;服务状态守卫会阻止 dispose 后通过后续 run() 重新进入该实例。

该帧有意省略轮次、步骤、signal、cwd、沙箱和授权。若真实消费方无法使用现有显式字段,必须另行论证扩展;陈旧字段最多只能误标遥测数据,绝不能授予控制权。