Files
deepseek-harness/.agents/notes/implemented/architecture/2026-07-26-task-registry-seam.zh.md
Tianyi Cui e20545c9ee fix(tasks): fail loud when the abstract seam is mounted directly
Review finding (Codex round 1): abstract erases at runtime and
@deepseek-ai/dsh-tasks used to be the mountable registry, so a stale
composition row would register a ctx.tasks with no method implementations
and fail far from the misconfiguration. The seam constructor now rejects
direct mounts with a load-time pointer at dsh-tasks-local; the seam suite
pins the fence, the Agent Note cost paragraph records the actual behavior,
and the stale tool-pty README requirement line names the implementation
package.
2026-07-26 07:20:45 +08:00

5.8 KiB
Raw Blame History

Agent Note: 任务注册表是一个能力 seamdsh-tasks / dsh-tasks-local

Status: implemented

English | 中文

问题

后台任务运行时交付时把 TaskService 做成了单个具体包package@deepseek-ai/dsh-tasks 既拥有每个生产方和控制接口面向编程的 ctx.tasks 契约,也拥有进程内实现(内存存储、结算簿记、所有者清理 effect、拆除。这种捆绑重新耦合了仓库能力 seam 规则本要分离的两种变化速率:一旦替换注册表的存储或生命周期后端,被搅动的就是同一个包,而生产方(dsh-tool-bashdsh-tool-ptydsh-tool-subagent)、控制接口(dsh-tool-tasks)和 TaskKindMap 扩展方正是从这个包导入类型与 ctx.tasks 接口。harness 中其余每项可替换能力——bash、pty、fs、skill技能、subagent、web、会话持久化——都已具备接口实现消费方三分任务注册表曾是仅剩的 core 模式例外,仅由一条 TODO(task-service-backend) 注释把守。

决策

tasks/ 如今是一个 bash 三件套形态的三包能力家族:

  • @deepseek-ai/dsh-tasks(接口)——抽象的 TaskService extends Service,拥有 ctx.tasks、八个方法的契约(startlistgetreadkillwaitonTaskDoneattachSurface)、全部词汇类型(TaskIdTaskKindMapTaskStartTaskHooksTaskOutcomeTaskSnapshotTaskReadTaskDoneListener),以及快照不变式配套插件。类级 JSDoc 陈述了每个实现都必须兑现的语义:注册的存续期长于生产方与控制接口的 fiber有所有者的访问以会话为界结算遵循首次结果优先且监听器错误被隔离并且在没有附加任何控制接口时 start 拒绝启动工作。
  • @deepseek-ai/dsh-tasks-local(实现)——LocalTaskService,即原样迁移的进程内注册表:内存存储、按 kind 划分的计数器、等待方簿记、TASK_WAIT_TIMEOUT deadline 代码、所有者清理 effect以及强制失败的拆除。dsh-timeout 依赖随之迁入此包seam 包不含任何实现依赖。
  • @deepseek-ai/dsh-tool-tasks(消费方)——保持不变;它注入 'tasks',从不导入实现类型。

各组合在原先加载 dsh-tasks 的位置改为加载 dsh-tasks-localCLI命令行界面的 cordis.yml 配置项、agent-spine-demo、各测试 harness以及工具目录生成器的启动流程。生产方的配置错误诊断信息「background tasks unavailable: load …」)点名 dsh-tasks-local,因为部署方修复该问题的办法是加载实现包,而非接口包。生产方、TaskKindMap 声明合并和控制接口仍然只导入 @deepseek-ai/dsh-tasks

该 seam 保持进程内契约语义不变:TaskStart.run() 仍然传入回调和确切的 Agent 对象,因此持久化或跨进程后端在能实现此接口之前仍有设计工作要做(身份、重启、所有权、观察)。这次拆分把该项未来工作移出了每个消费方的依赖图;它并不预先设计后端。

曾考虑的替代方案

**在第二个后端出现之前保持具体服务(维持现状)。**这正是运行时 Agent Note 当初的立场:在第二种实现出现前抽取接口,可能固化错误的边界。该方案落选,因为这条边界已不再是臆测:八个服务方法及其语义自引入以来在每一次生产方集成中都保持稳定,它们正是 dsh-tool-tasks 与各生产方已经面向编程的那套接口,而且仓库约定默认将可替换能力拆成三个包。剩余风险(持久化后端可能需要变更契约)不因这次拆分而改变:无论拆分与否,这类变更都会落在 seam 包里;而若维持现状,它们今天还会连带搅动每个消费方的实现依赖。

**在单个包内仅抽取接口(在具体类旁导出一个抽象类)。**否决,因为它在运作层面并未分离任何东西:消费方依然依赖携带实现及其依赖项的那个包,而替换后端若不把本地实现纳入自身依赖图,就仍然无法发布。在这里,包边界才是独立演进的单位。

**拆出 types.ts 但让服务保持具体。**基于同样的理由否决:类型并不是 seamctx.tasks 及其方法契约才是。生产方需要的是服务键和语义,而不只是类型形状。

后果

换来的是:任务注册表如今与全仓库通行的 seam 形态一致;持久化、远程或带插桩的注册表将是一个实现八个抽象方法的兄弟包,这样的注册表落地时,任何生产方、控制接口或 TaskKindMap 扩展方都无需改动。seam 包的 README 陈述契约;生命周期簿记方面的事实归实现包的 README 所有。注册表行为测试套件(所有者清理、结算、等待、拆除)随 dsh-tasks-local 存放seam 包保留一个桩子类stub subclass测试固定 ctx.tasks 下的注册行为与单一服务的重复注册行为,外加基于探针的不变式测试套件。

代价是:多出一个包,即多一份 manifest元数据清单、tsconfig、README 与不变式配套插件;同时各组合必须点名实现包。abstract 在运行时会被擦除,而这个包名过去正是可挂载的具体注册表,因此 seam 的构造函数在被直接挂载时会响亮失败——一条过期的组合配置行会在加载时得到「load an implementation such as @deepseek-ai/dsh-tasks-local」而不是一个方法残缺的 ctx.tasks 在远离错误配置处才失败。若日后另一个后端成为推荐的默认选择,点名 dsh-tasks-local 的配置错误诊断信息将随之陈旧;这一点已被接受。