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

字节笔记本

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

DeepSeek Harness 后台任务运行时解析

API中转
¥120

DeepSeek Harness 后台任务运行时架构:生产方、注册表与消费方

DeepSeek Harness(简称 dsh)是 DeepSeek 开源的智能体框架,主打一切皆插件,底层由 Cordis 框架驱动,目前处于开发者预览阶段。真实任务里的 agent 经常要发起一些没法当场等完的活:一条要跑几分钟的命令,一个派去查资料的分身。如果每个插件各自造轮子管理生命周期,模型就拿不到统一的任务列表,也谈不上统一收尾。DeepSeek Harness 把这件事收敛成一个注册表服务,也就是插件眼中的 ctx.jobs,内置 bash 与 subagent 两类任务,插件还能通过声明合并注册自己的任务种类,运行时把每种 kind 当作不透明的 id 命名空间。本文依据仓库 packages/jobs 模块的类型文档,拆解这套后台任务运行时的设计。

可预测的 id,五个状态

任务 id 形如 bash-1、subagent-2,由注册表按种类前缀加序号生成。文档特别强调一点:id 是可预测的,所以访问控制不依赖 id 保密,而是依赖拥有者授权,比对的是拥有者会话。生命周期状态只有五个值:

ts
type JobStatus = 'running' | 'stopping' | 'completed' | 'killed' | 'failed'

生产方特有的细节(比如退出码)不进状态机,而是放进快照的 detail 字段,避免枚举无限膨胀。

生产方契约:先预检,再原子注册

启动一个任务要提交一份 JobStart 声明:kind 是身份前缀,label 是给模型看的一行说明,outputLimitBytes 给每条面向模型的完成通知和每次输出读取设 UTF-8 字节上限,owner 指明拥有这个任务的 agent。省略 owner 会创建无主任务,服务销毁前任何调用者都能访问。

这里的顺序约定很讲究:运行时先做预检(访问与清理检查),预检通过才调用 run(),随后原子提交注册。run() 一旦正常返回,注册就不可能再失败;run() 中途抛出则什么都不会留下,半启动的资源由生产方自己清理。所有权分界一句话就能说清:执行资源归生产方,身份、访问权限和生命周期状态归运行时。

生产方还要交回一组 JobHooks 钩子。cancel 必须同步、幂等,并最终让 done 落定;done 是一个 Promise,关键在于它在生产方释放资源之后才 resolve,而不是工作做完就 resolve,运行时靠这个时间点确认收尾干净。约定 done 不允许 reject,一旦拒绝,运行时把它转成 failed。可选的 readOutput 用来区分两类任务:实现了它的是流式任务,每次调用返回自上次以来的增量;不实现的就是只有最终输出的任务,每个任务只有一条消费游标。结束时生产方通过 done 给出 JobOutcome,detail 放一句人话,文档里的例子是 exit code: 3 和 max-tokens,没用 readOutput 的任务在这里交出最终 output。

任务状态流转与结算语义

消费方视图:每次都是新快照

外界拿到的从来不是注册表本体,而是 JobSnapshot 只读投影,每次调用新建一个对象,绝不暴露活的注册表状态。快照里有 id、kind、label、status、ownerSession、起止时间等字段,ownerSession 承担授权与关联,完成监听器则会另外拿到确切的拥有者对象。有个容易被忽略的字段是 reported:终止状态一旦被上报或已承诺上报,它就置为真,完成通知方据此抑制重复播报。文档解释了销毁场景为何直接把记录标成 reported:拥有者或服务正在销毁,记录已经没有读者,如果通知方还为此新开一轮对话,等于每层销毁流程都白花一次模型请求。

注册表的四条硬规矩

JobRegistry 是抽象的服务定义,LocalJobRegistry 是进程内的默认实现,一个上下文只允许加载一个实现,重复加载会按 Cordis 的标准行为直接抛错。它的语义里有几条很硬的规矩。

第一,结算只赢一次。第一个终态记录生效,等待者全部放行,监听器只通知一轮,哪怕生产方很晚才交出结果,也改不了已定的终局。

第二,完成通知最后发。先把记录落定,等其他所有观察者都看过结算,才通知监听器,因为监听器可能同步新开一轮模型请求,顺序反了就会先花钱后对账。

第三,作用域按拥有者算。一个进程里的所有组合共用一个注册表,但从无作用域上下文注册的任务对所有拥有者可见,在某个 agent 组合作用域里注册的就只服务该组合下的 agent。attachController 挂上的控制器同理:没有控制器服务的拥有者,start 会直接拒绝,保证生产方开不出拥有者既收不了又停不掉的活。

第四,并发有准入闸。本地实现的 maxConcurrentJobsPerOwner 必须是正整数,默认 10,按确切拥有者统计 running 与 stopping 的记录数,无主任务共享一个服务级配额桶,容量在生产方结算之后释放。

接口清单本身很克制:start、list、get、read、kill、wait、onJobDone、onJobsChanged、attachController。kill 请求取消并把任务标记为 stopping 且已上报,返回 requested 或 already-finished;wait 是有界等待,不负责取消,等待方可以在任务存活期间自行中止,任务已结算时终态快照优先送达;onJobsChanged 观察可见集合的变化,要求观察方重读而不是累积增量,它不携带送达语义,也不标记 reported,与 onJobDone 不是包含关系。

给插件作者的结论

要发起后台工作,就声明 JobStart 并交回规范的钩子,其余的身份、鉴权、并发与收尾都交给 ctx.jobs;要观察任务,就消费快照和监听器,不要试图持有活的注册表引用。服务定义约定、本地实现与准入策略、面向模型的消费入口分别放在 dsh-jobs、dsh-jobs-local 与 dsh-tool-jobs 三个包里。DeepSeek Harness 还在开发者预览阶段,接口可能随版本调整,动手前建议以仓库 docs/subsystems 下的最新文档为准。

相关文章

分享: