Skip to content

3.6 后台任务 Jobs

本章概览:Jobs 能力缝给长运行工具一个"所有者隔离"的后台任务协议:观察、取消、等待与完成通知。本章覆盖 ctx.jobs 契约、进程内实现 jobs-local、模型侧 job_* 工具,以及 kill/wait/观察的语义。

概述 / 定位

长运行工作(后台 bash、PTY 发送、子代理委派)都需要统一的"注册后台工作 → 读取输出 → 取消/等待 → 完成通知"协议。Jobs 把这份协议抽成一个能力缝:Service Definition 是 dsh-jobsctx.jobs),Service Provider 是 dsh-jobs-local(进程内注册表),模型侧 Consumer 是 dsh-tool-jobsjob_output / job_list / job_kill 控制器)。Producer 插件通过声明合并扩展 JobKindMap不透明 id 命名空间(目前含 bashsubagent)。

出处:packages/jobs/README.mddeepseek-harness-src/packages/jobs/README.md)、docs/capability-seams.mddeepseek-harness-src/docs/capability-seams.md

ctx.jobs 契约

成员语义
start(spec)预检(已附控制器、spec、精确活 owner、可选 outputLimitBytes、提供方准入)后调用 producer 的 run() 一次;预检拒绝或 starter 抛错不留任何 job id,成功返回即提交
get(id, caller?) / list(caller?)非消耗快照;list 只含调用方自有与无主任务
read(id, caller?)流任务消耗单游标读取增量;终态任务幂等读终态输出
kill(id, caller?, reason?)先调用 producer 取消再改状态:取消抛错则任务保持运行;成功则转 stopping 并标记终态已报告
wait(id, timeoutMs, caller?, signal?)返回终态快照或超时时的活快照;abort 只停止等待,结算一旦向该等待者提交终态投递则获胜
onJobDone(listener)观察每个终态记录(带精确 owner);监听器故障被包含、不等待
onJobsChanged(listener)观察可见集合变化——注册、每次 stopping 转换(含拆除前那次)、结算、所有者处置移除、服务处置清空
attachController(name)为其 effect 生命周期声明一个任务控制器;无控制器服务该 owner 时 start 在执行前失败

注册与监听都是 owner 相对的:一个注册表服务进程内所有组合,但一个组合加载的控制器/监听器只服务其作用域覆盖的 agent——某组合没加载控制器,就不能借着另一组合的控制器启动后台工作。

出处:packages/jobs/jobs/README.mddeepseek-harness-src/packages/jobs/jobs/README.md

所有者隔离

JobId<kind>-N 形式的品牌 id(branded id)。id 可预测,因此访问控制靠授权围栏而非保密:拥有访问把任务的 SessionId 与调用方比对;无主任务对任何调用方开放、存续到服务处置。owner 处置会取消该对象的任务、等待 producer 静止并移除快照;被复用的 agent/session id 不能重定向旧清理。

输出、kill 与 wait 语义

outputLimitBytes 是 producer 自有的模型呈现策略,原样进入快照;控制器在附加状态或通知元数据后应用它,注册表不重写 producer 输出、也不为省略它的 producer 发明默认值。超限的完成通知保留稳定 id 前缀与 job_output 收集指令,使其在 PTY 支持的 64 字节最小宽度下仍可操作。

  • kill 先取消后改状态:状态转换不抢先于实际取消;成功转 stopping 并标记 reported。
  • wait 以终态为准:结算一旦提交终态投递,即使等待者中途 abort 也仍收到终态快照。
  • 结算 first-wins:最早的终态结果记录一次、释放等待者、做一轮包含式监听器通知;完成通知在记录提交、可见集合变化发布之后才宣布——reporter 可能同步打开一个模型轮次。

观察契约与完成通知

onJobDone 是"终态记录 + 精确 owner"的交付通道;onJobsChanged 是可见集合的 owner 粒度变化信号(移除这种变化是 per-job 记录无法表达的),携带变化集合移动的那个 owner,或 undefined 表示无主任务变化。两者都不是超集关系:前者耦合控制器对通知投递的依赖(会标记 reported),后者不含投递含义、不标记任何东西。

未报告的完成向精确 owner 投递 background job <id> (<kind>: <label>) finished [status: ...]. Read its output with job_output.。车道取决于 owner 状态:忙 owner 注入(并入下一个 step 边界,多个任务同时结算只花一步);闲 owner 用 follow-up 唤醒(否则没人认领的通知等于模型永远不会知道的完成)。唤醒有预算:maxConsecutiveWakes(默认 3)限制一个 owner 由此打开的轮次数,超限后降级为注入;用户消息可恢复预算——因为该链自激发:被唤醒的轮次可能又启动一个后台任务。

jobs-local 生命周期

LocalJobRegistry 把每条记录放在内存、按 kind 发放 <kind>-N id、每次发放全新快照。注册表比 producer 与 controller 的 fiber 都长寿:任务属于 owner 与后端,producer/控制器重载不停任务。服务处置关闭监听器、取消全部活任务、等待记录、从幸存 owner 作用域解除 effect;若拆除取消抛错,注册表强制失败记录并警告工作可能被孤立,而不是死锁。准入:maxConcurrentJobsPerOwner 默认 10,计数精确 owner 的 running + stopping 记录;无主任务共享一个独立服务桶;终态历史不占容量,只有 producer done 结算才释放 stopping 任务的槽位。饱和时 start 在执行前失败并指导模型 job_kill、等待停止、重试——不排队、不抢占、不维护第二个可变计数器。

出处:packages/jobs/jobs-local/README.mddeepseek-harness-src/packages/jobs/jobs-local/README.md

关键设计决策

  • 通用注册表而非各工具私有后台:统一 id、读取、取消、通知词汇,producer 只需实现 JobHookscancel/done/readOutput?)。
  • 授权围栏而非 id 保密:id 可预测,SessionId 比对才是边界。
  • kill 先取消后改状态:状态转换不抢先于实际取消。
  • 完成通知唤醒有预算:自激发唤醒链被 maxConsecutiveWakes 限制。
  • 注册表长寿:producer 工具 fiber 消亡不影响已登记任务;结算通知在记录提交后才宣布。
  • 流输出单游标:独立观察者需要游标或快照 API(当前未实现)。

扩展点

  • 新 producer:扩展 JobKindMap、实现 JobHooks,调 ctx.jobs.start()tool-bash 的后台模式与 tool-terminalterminal_send(run_in_background: true) 是现成范例。
  • 新后端:实现抽象 JobRegistry;跨进程/持久后端需重塑身份、重启、所有权与观察语义。
  • 观察者onJobDone / onJobsChanged,或模型侧 job_output/job_list/job_kill

参考资料

本文档为学习用途的原创讲解,基于 MIT 许可的开源项目 deepseek-ai/deepseek-harness 编写;所有引用均注明出处。