Files
deepseek-harness/packages/tasks/tasks/README.zh.md
Yichen Jiang 59e759ce13 fix(tasks-local): layer control surfaces and listeners by registering scope
One host registry serves every composition in the process, so its two
service-wide collections answered per-owner questions process-wide. `start()`
asked only whether SOME surface was attached, so an agent whose own composition
loads no `tool-tasks` could start work it has no tool to collect or stop as soon
as any other preset attached one — and the answer changed depending on which
sessions happened to be open. `settle()` walked every registered listener, so a
task settling without a waiter injected one completion notice per mounted
preset into the same owner.

Both collections now sit in `ScopedLayers`, the layered-registry primitive
`tools` and `skills` already use: a registration files into its registering
context's scope, and a read unions the global layer with the owner's scope
chain. A surface or listener registered from an unscoped context lands in the
global layer and serves every owner, which is exactly the host-plane
composition's own controls, so the TUI path is unchanged without a special
case.

This supersedes the consumer-side filter in the previous commit. That filter
produced the right notices but sat in the wrong layer: it left the `start()`
gate process-wide, it could not be enforced against a producer that resolves
the registry directly, and it made a Consumer carry scope knowledge that the
other layered registries keep in the registry. `tool-tasks` is scope-agnostic
again and the `dsh-scope` edge moves to `tasks-local`.

`start()`'s refusal is now owner-relative, so its model-visible text names the
agent rather than the process. The shipped `minimal` preset keeps
`enableRunInBackground: false`, no longer as the safety boundary — the registry
owns that now — but so an agent that could never collect a task is not offered
the parameter at all.

Refs #2141
2026-08-10 16:12:41 +08:00

3.9 KiB
Raw Blame History

@deepseek-ai/dsh-tasks

English | 中文

后台任务注册表约定(ctx.tasks)。抽象的 TaskService 及其词汇类型在同一份约定下为长时间运行的生产方提供共享 id、owner 隔离、读取、取消、等待、通知和清理;进程局部注册表位于 dsh-tasks-local。生产方插件使用其不透明 id namespace 扩展 TaskKindMap

服务约定

  • start(spec): TaskId 验证控制表层、spec、确切且仍存活的 owner以及可选的 outputLimitBytes(如提供则须为正数),然后只调用生产方的 run() 一次。启动方抛出异常时不注册任何内容;成功返回会直接提交,不再执行其他可能失败的步骤。
  • get(id, caller?)list(caller?) 返回非消费式快照。列表只包含调用方拥有及无 owner 的任务。
  • read(id, caller?) 消费流任务的唯一游标;对于最终输出任务,则以幂等方式读取终止输出。
  • kill(id, caller?, reason?) 在更改状态前调用生产方取消。取消抛出异常时任务保持运行;成功则把状态改为 stopping,并将终止交付标记为已报告。
  • wait(id, timeoutMs, caller?, signal?) 返回终止快照,或在超时时返回存活快照。中止只会停止等待;一旦终止交付已向该等待方提交,终止结果优先。
  • onTaskDone(listener) 观察每条终止记录及其精确 owner。监听器抛出的异常和产生的拒绝都会被隔离系统不会等待监听器工作。
  • attachSurface(name) 在其 effect 生命周期内声明控制表层。当没有任何已附加的表层服务于 spec 的所有者时,start() 会在生产方执行前失败。

这两类注册都是相对于所有者的,因为一个注册表要服务进程内的每一套组合。从不带 scope 的上下文注册的表层或监听器服务于每个所有者;在某套 agent 组合的 scope 下注册的,则恰好服务于在该组合下组合出的 agent。因此未加载任何控制表层的组合无法借另一套组合的控制工具启动后台工作而一次结算也只会通知其所有者所属组合注册的监听器。

有 owner 的访问会比较任务的 SessionId 与调用方。bash-1 等 id 可预测,因此这道隔离是安全边界。无 owner 的任务向调用方开放,并持续到服务释放。

outputLimitBytes 是生产方拥有的模型呈现策略,会原样携带到快照中。控制表层在添加状态或通知元数据后应用它;注册表不会重写生产方输出,也不会为省略此字段的生产方虚构默认值。

实现还必须兑现约定的生命周期语义:注册的存续期长于生产方 fiber 与控制表层 fiberowner 释放和服务释放会取消仍在运行的工作并等待守约的生产方,结算遵循首次结果优先(一条终止记录、一轮异常受到隔离的监听器通知,然后释放等待方)。

参见任务类型目录运行时 Agent Noteseam Agent Note

模型体验

通过生产方插件和 dsh-tool-tasks 间接影响;它们会渲染 task id、输出、状态、取消和完成通知。

KV Cache 影响

不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。

已知限制与暂缓事项

  • 流输出只有一个消费游标:独立观察者需要游标或快照 API。
  • 前台工作无法转为后台:生产方在启动前选择前台或后台。
  • 约定是进程内的TaskStart.run() 传入回调和确切的 Agent 对象;持久化或跨进程后端必须先重塑身份、重启、所有权与观察语义,才能实现此 seam。