Appearance
3.6 后台任务 Jobs
本章概览:Jobs 能力缝给长运行工具一个"所有者隔离"的后台任务协议:观察、取消、等待与完成通知。本章覆盖
ctx.jobs契约、进程内实现 jobs-local、模型侧job_*工具,以及 kill/wait/观察的语义。
概述 / 定位
长运行工作(后台 bash、PTY 发送、子代理委派)都需要统一的"注册后台工作 → 读取输出 → 取消/等待 → 完成通知"协议。Jobs 把这份协议抽成一个能力缝:Service Definition 是 dsh-jobs(ctx.jobs),Service Provider 是 dsh-jobs-local(进程内注册表),模型侧 Consumer 是 dsh-tool-jobs(job_output / job_list / job_kill 控制器)。Producer 插件通过声明合并扩展 JobKindMap 的不透明 id 命名空间(目前含 bash 与 subagent)。
出处:
packages/jobs/README.md(deepseek-harness-src/packages/jobs/README.md)、docs/capability-seams.md(deepseek-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.md(deepseek-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.md(deepseek-harness-src/packages/jobs/jobs-local/README.md)
关键设计决策
- 通用注册表而非各工具私有后台:统一 id、读取、取消、通知词汇,producer 只需实现
JobHooks(cancel/done/readOutput?)。 - 授权围栏而非 id 保密:id 可预测,
SessionId比对才是边界。 - kill 先取消后改状态:状态转换不抢先于实际取消。
- 完成通知唤醒有预算:自激发唤醒链被
maxConsecutiveWakes限制。 - 注册表长寿:producer 工具 fiber 消亡不影响已登记任务;结算通知在记录提交后才宣布。
- 流输出单游标:独立观察者需要游标或快照 API(当前未实现)。
扩展点
- 新 producer:扩展
JobKindMap、实现JobHooks,调ctx.jobs.start();tool-bash的后台模式与tool-terminal的terminal_send(run_in_background: true)是现成范例。 - 新后端:实现抽象
JobRegistry;跨进程/持久后端需重塑身份、重启、所有权与观察语义。 - 观察者:
onJobDone/onJobsChanged,或模型侧job_output/job_list/job_kill。
参考资料
- packages/jobs/README.md — 能力族总览(
deepseek-harness-src/packages/jobs/README.md) - packages/jobs/jobs/README.md — Service Definition 契约(
deepseek-harness-src/packages/jobs/jobs/README.md) - packages/jobs/jobs-local/README.md — 进程内注册表(
deepseek-harness-src/packages/jobs/jobs-local/README.md) - packages/jobs/tool-jobs/README.md — 模型侧控制器(
deepseek-harness-src/packages/jobs/tool-jobs/README.md) - docs/subsystems/jobs.md — 后台任务运行时参考(
deepseek-harness-src/docs/subsystems/jobs.md) - 设计笔记:generic long-running tool runtime、job registry seam