DeepSeek Harness 源码解析 · 第 25 讲
DeepSeek Harness 源码解析系列 第 25 讲:Jobs 作业与 Schedule 定时调度 基于 DeepSeek Harness 源码 · 2026-09-08 💡 本讲一句话: Agent 经常要"把一件慢活丢到后台,然后继续干别的"——这就是 Jobs (起了能杀、能等、能读输出);它还要"过 N 秒 / 某个时刻再提醒我一次"——这就是 Schedule 。本讲拆开这两套机制:作业靠一个进程内注册表守住 first-wins 结算与属主隔离,提醒则把状态 写进会话事件日志 、用纯函数 fold 出来,跨重启不丢。 一、两个"异步回来找我"的能力,两套完全不同的地基 前面几讲我们拆过 subagent(第 18 讲)和 workflow(第 24 讲)——它们都是"同步等结果"的编排。但真实任务里还有两类 异步 需求:① 起一个后台作业(跑测试、爬数据、委托子代理),模型不想干等着,要能继续推进独立步骤,回头再收结果;② 设一个提醒("5 分钟后检查构建""明早 9 点汇报进度"),到点了系统主动把内容递回给会话。 这两件事在仓库里是 两组独立的包 ,地基完全不同——这是本讲最重要的认知: 包 角色 状态存哪 dsh-jobs Jobs 能力缝(Service Definition):抽象注册表 + 生命周期契约 src/index.ts 、 types.ts dsh-jobs-local 进程内 Provider:内存注册表 + first-wins 结算 + 属主隔离 src/index.ts (内存 Map) dsh-tool-jobs 模型可见层: job_output/list/kill + 完成通知投递 src/index.ts dsh-schedule 定时提醒:纯函数域 + 运行时投影 + schedule_create/list/delete 会话事件日志 ( schedule/change ) 关键差异: Jobs 的状态是 易失的进程内内存 (作业活着才有意义,进程没了作业也没了);Schedule 的状态是 持久的会话日志事件 (提醒必须跨重启存活)。这个"易失 vs 持久"的分野,决定了后面两套代码里反复出现的两条纪律——Jobs 守 first-wins 结算 + 属主隔离 ,Schedule 守 纯函数 fold + 持久化屏障 。 二、能力缝:JobRegistry 只声明契约,不碰存储 dsh-jobs/src/index.ts 定义了一个抽象类 JobRegistry extends Service ,它把"后台作业"这件事的 全部语义 写进 JSDoc:id 铸造、会话级访问围栏、first-wins 结算、属主清理、controller 准入。实现类( dsh-jobs-local )只需继承并实现 start/list/get/read/kill/wait/onJobDone/onJobsChanged/attachController 九个方法。 构造函数里藏着一个很"工程"的细节—— 防止有人把抽象缝本身当实现加载 : 📄 packages/jobs/jobs/src/index.ts (第 62-71 行) export abstract class JobRegistry extends Service { // 抽象能力缝:只声明契约,不碰任何存储实现 constructor(ctx: Context) { // 构造时先做"防误用"检查,再交给 Cordis 注册为 ctx.jobs if (new.target === JobRegistry) { // new.target===抽象类本身 = 有人直接实例化了缝(cordis.yml 配置写错) throw new Error('@deepseek-ai/dsh-jobs is the abstract job registry seam; load an implementation such as @deepseek-ai/dsh-jobs-local instead') // fail loud:加载期就炸,别等运行到一半才报"方法不存在" } // 检查通过才继续 super(ctx, 'jobs…