ByteNoteByteNote
DeepSeek Harness 后台任务运行时设计解析
字

字节笔记本

2026年10月6日 · 约 6 分钟读完

DeepSeek Harness 后台任务运行时设计解析

API中转
¥120

在 AI Agent 的日常运行里,不少工作不适合堵在对话主线上:跑一条耗时的构建命令、让子代理去整理一个目录、执行随手丢下的脚本。这些长任务需要一个统一的运行时来管。开源项目 DeepSeek Harness 把这件事收敛成一个服务接缝 ctx.jobs:插件作为生产方把长任务交给注册表,注册表统一负责身份、访问控制和生命周期。本文基于它的子系统文档,拆解这套后台任务运行时的类型契约与关键设计。

DeepSeek Harness 后台任务运行时的角色与数据流

可预测的 ID 与五值状态机

每个任务由注册表签发形如 <kind>-N 的品牌化 ID。kind 来自一个可通过声明合并扩展的映射,内置 bash 与 subagent 两类,插件可以继续追加自己的类别,注册表把每种 kind 当作不透明的 ID 命名空间。值得注意的是,ID 是可预测的,文档明确写道:访问控制依赖拥有者授权,而不是 ID 保密。

状态机只有五个值:running、stopping、completed、killed、failed。生产方特有的细节,比如退出码或截断原因,不进状态机,统一放进快照的 detail 字段。

生产方契约:预检之后不再失败

启动任务时,生产方向 JobRegistry.start 传入一个 JobStart 声明,包含四类信息:

  • kind:任务类别,同时是 ID 前缀;
  • label:给模型看的一行说明,比如命令本身或委派描述;
  • outputLimitBytes:可选,为每条面向模型的完成通知和输出读取设 UTF-8 字节上限;
  • owner:可选,拥有任务的活跃 Agent,访问按其会话 ID 圈定,Agent 销毁时会取消并等待它的任务;不传则创建无主任务,任何调用者都能访问,直到服务销毁。

责任划分写得很清楚:生产方拥有执行资源,运行时拥有身份、访问权限和生命周期状态。run() 在预检完成之后才被调用,只调用一次,同步返回任务钩子;如果它抛异常,注册表里什么都不会留下,半启动的资源由生产方自己清理。反过来,run() 正常返回之后,注册提交就不会再失败。这个预检前置、提交不可失败的次序,是整套契约里最值得借鉴的一笔。

三个钩子与一种结局

运行时通过 JobHooks 控制生产方。cancel 必须同步、幂等,并最终让 done 落定,原因字符串原样转发。done 是一个 Promise,关键在它的时机:它要等生产方释放完资源才 resolve,而不是工作刚结束就 resolve,且不允许 reject,运行时会把拒绝转成 failed 状态。可选的 readOutput 用来区分两类任务,有它的是流式任务,读取自上次以来的增量,没有它的是只交最终输出的任务,每个任务只有一条消费游标。

生产方通过 done 上报 JobOutcome:status 说明任务是怎么结束的,detail 放 exit code: 3、max-tokens 这类类别相关的细节,output 只提供给没有 readOutput 的任务,流式任务留空。

任务从启动到结算的生命周期:准入、状态机与 first-wins 结算

消费视图:快照与 reported 抑制

模型和工具看到的永远是只读投影:每次调用返回一个全新的快照对象,绝不暴露注册表的内部状态。快照里有身份、状态、时间戳,还有一个容易被忽略的 reported 布尔值:当 kill、read、wait 或销毁流程中的取消已经上报、或承诺上报终态之后,它就被置位,完成通知器据此抑制重复播报。

销毁路径为什么也要置位?文档给的理由很实在:拥有者或服务正在销毁,这条记录已经没有读者了,如果每层销毁都开一轮对话去播报终态,每层都要花一次模型请求。读输出同样有讲究:流式任务读到的是增量,终态任务在存活期间读到空,落定之后则幂等地返回最终输出,永远不会被消费掉。

服务行为:准入、结算与通知

抽象 JobRegistry 定义了 start、list、get、read、kill、wait、onJobDone、onJobsChanged 和 attachController 九个能力,LocalJobRegistry 是进程内的默认实现,同一上下文只能挂一个实现,重复加载会直接抛错。几个行为值得一提。

准入有上限。配置项 maxConcurrentJobsPerOwner 默认 10,按精确拥有者统计 running 加 stopping 的记录数,无主任务共享一个桶,终态结算释放额度。

结算只此一次。采用 first-wins 语义:只留一条终态记录,释放所有等待者,监听器只通知一轮,就算生产方晚到的结局也一样。完成通知特意排在最后,等记录提交、所有其他观察者都看过之后才发,因为通知器可能同步打开一轮模型对话。

启动有门槛。如果没有任何已挂载的任务控制器服务于某个拥有者,start 会直接拒绝,避免生产方启动一个拥有者既收不了也停不了的任务。这个判断是相对拥有者的:无作用域上下文注册的服务于所有拥有者,挂在某个 Agent 组合作用域下的只服务于组合内的 Agent。

变更通知不带语义。onJobsChanged 在每次改变可见集合的提交后触发,包括注册、每一次进入 stopping 的迁移、结算、拥有者销毁移除和服务销毁清空;它不承载已通知的含义,也不改动 reported,观察者应该重新读取而不是累积增量。

小结

这套设计的取舍可以概括成三句话:身份可预测,安全靠授权;预检前置,注册不可失败;结算只此一次,通知抑制到刚好够用。对任何想给 Agent 加后台能力的框架来说,这份类型契约都值得当作参考实现来读,尤其是 done 的时机、reported 的抑制和准入上限这三处细节,直接决定了长任务在真实对话里的成本。

相关文章

分享: