跳到主要内容
路径文档

目标、作业与待办

DSH 把"要做的事"分成三个层次,各自服务于不同粒度的推进:

层次服务面向状态存哪
目标ctx.goals跨多轮/多 step 的长期目标会话日志(goal/change 事件)
作业ctx.jobs长时运行的后台操作(共享 id、owner 隔离)进程内注册表
待办tool-todotodo_write一次会话内的行动清单事件溯源会话日志

1. 目标(goal)

当前完成目标,保存在 agent 的既有会话里,同一时刻至多一个。

const ref = await ctx.goals.create({
objective: '审计整个仓库的文档一致性',
maxGoalRounds, // 可覆盖部署默认 defaultMaxGoalRounds: 256
})
// 状态机:edit / pause / resume / complete / block / clear
await ctx.goals.edit(ref, {...}) // 所有变更都带 GoalRef{id,revision} 的 compare-and-set 栅栏
await ctx.goals.complete(ref)

关键点(源码 README):

  • create 产生修订号 1 的 active 目标并 armed;每个变更追加一个持久的 goal/change 事件(带完整后置快照),clear 用 revisioned tombstone
  • block 是统一阻塞态:provider 限制、配置预算、执行错误、请求人类输入都用这一个 durable phase,记录 policy-owned 的 kebab-code + 归一化解释
  • resume 只在轮次上限还有剩余时接受(且清掉旧的 blocker reason)
  • disarm() 是生命周期例外:移除进程内继续权限但不写 revision/不发事件

目标不是简单字符串:它承载完成条件、轮次上限、阻塞状态,是"驱动一个长任务持续向前"的机制。

/goal 人类命令

command-goal 注册全局 /goal 命令,人可直接控制目标(不走模型轮次):

输入结果
/goal显示当前目标、durable phase、轮次计数/上限、有效后续命令(无目标时显示用法)
/goal <目标>创建并 arm 一个新目标;未完成的目标不会被替换,除非先 clear
/goal edit <目标>改目标文字,不改 phase/激活态
/goal pause / /goal resume暂停并 disarm / 恢复并 rearm(受剩余轮次上限约束)

goal-round-driver:同会话续推进

goal-round-driver同会话续推驱动:把 active + armed 的目标转成连续的 goal round,经公共 Agent 与会话服务推进:

- id: goal
name: '@deepseek-ai/dsh-goal'
- id: tool-goal
name: '@deepseek-ai/dsh-tool-goal'
- id: goal-round-driver
name: '@deepseek-ai/dsh-goal-round-driver'

目标推进 → goal round → 模型工作 → 下个 round,直到 complete / block / 轮次上限。

2. 作业(jobs)

后台作业注册表:给长生命周期生产者的共享 id、owner 隔离、读取、取消、等待、通知、清理,统一在一个 ctx.jobs 契约下。

const id = await ctx.jobs.start({ owner, controller, spec })
await ctx.jobs.get(id) // 非消费快照
await ctx.jobs.read(id) // 流式作业消费游标;终态幂等读
await ctx.jobs.wait(id, 30_000) // 等终态,超时返回当前快照
await ctx.jobs.kill(id, caller, '不再需要')
ctx.jobs.onJobDone(...) // 观察每个终态记录(只含精确 owner)
ctx.jobs.onJobsChanged(...) // 观察可见集合变化(owner 粒度)
  • owner 是边界:bash-1 这类 id 可预测,get/list/read/kill/wait 都对比 caller 的 SessionId
  • unowned 作业对任意 caller 开放,存活到服务卸载
  • 生产者为 dsh-jobs-local,通过 TaskKindMap 扩展不透明 id 命名空间
  • 模型面向 tool-jobs:job_output / job_list / job_kill

完成通知与自动唤醒

tool-jobs 会 via ctx.jobs.onJobDone(...) 监听每个作业的终态,替未报告的完成投递一条通知,即使模型从未读过输出也不会漏(源码 packages/jobs/tool-jobs/src/index.ts):

background job <id> (<kind>: <label>) finished [status: ...]. Read its output with job_output.

按 owner 忙闲分流:

owner 状态投递方式说明
busy(正在跑 step)注入 owner.inject(message)通知进 owner 的 next-step inbox,本轮内就能读到;同刻结清的多个作业只花一步
idle(空闲)completionDelivery: 'wakeup'owner.followup(message)给空闲 owner 开一个新回合(自动 followup);quiet 时则留在 pending,等别的东西唤醒
# tool-jobs 配置(组合示例)
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
# completionDelivery: 'wakeup' 默认;改 'quiet' 则空闲 owner 不再被开回合
# maxConsecutiveWakes: 3 默认,限制"被唤醒的回合又启动会再唤醒它的作业"这条自激链
  • maxConsecutiveWakes 默认 3:限制一个 owner 在再次消费用户输入前,被完成唤醒打开的回合数——封顶"被唤醒的回合又启动了会再唤醒它的作业"这条自激链。每次 agent/inbox/claimed 且消息来自用户源(message.source.kind === 'user')时重置预算;quiet 投递不花预算(因为没有回合被打开)。
  • completionDelivery: 'quiet':空闲 owner 不被开回合,通知留在 pending,直到别的输入唤醒它才送达。同一 owner 同步被替代(会话替换)会拿到满预算。
  • 该系统提示引导模型(源码 packages/jobs/tool-jobs/src/index.tsctx.systemPrompt.section):

Track every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.

  • job_output(wait:true) 是阻塞读,但归 tool-jobs:waitTimeoutMs 默认 30s(单次默认等待),maxWaitTimeoutMs 默认 600_000(10 分钟)是任何单次等待的硬上限——模型给的 timeout_ms 会被钳到它。超时返回的是活的作业快照([status: running])而不是 TOOL_TIMEOUT 错误,作业保持存活,之后还能再读。onJobDone 也会把仍有等待者的终态标记为 reported(源码 packages/jobs/jobs-local/src/index.tssettle)。

并发上限

dsh-jobs-localexact owner 限活跃作业数(源码 packages/jobs/jobs-local/src/index.ts):

  • maxConcurrentJobsPerOwner 默认 10;计入 running + stopping 两种状态(已终态的 completed/killed/failed 不占位)。
  • unowned 作业是独立桶:activeTaskCountowner === undefined 单算,不受任何 owner 的额度约束。
  • 到顶时 start() 抛错并给出模型的正确动作:"use job_kill to stop an unneeded job, wait for it to finish, then retry"——即 job_kill 停掉不需要的、等它走完停稳、再重试启动。
- id: jobs-local
name: '@deepseek-ai/dsh-jobs-local'
maxConcurrentJobsPerOwner: 10 # 默认;running+stopping 计容,unowned 独立
// packages/jobs/jobs-local/src/index.ts
if (active >= this.maxConcurrentJobsPerOwner) {
throw new Error(
`background job limit reached for this owner (limit: ${this.maxConcurrentJobsPerOwner}); use job_kill to stop an unneeded job, wait for it to finish, then retry`,
)
}

3. 待办(todo)

tool-todo 提供模型可调的 todo_write:把行动清单写进事件溯源会话日志。它适合"这一轮我要按顺序做什么"的短期清单,状态随会话持久化。

todo_write 部署策略

tool-todoConfig.allowParallelInProgress必配项(z.boolean().required(),源码 packages/todo/tool-todo/src/index.ts),部署必须显式声明"能否多条并进":

allowParallelInProgress模型指引适用
true允许多条 in_progress,要求标记每一个正在推进的任务fan-out:并发的子 agent、后台命令、workflow 扇出
false至多一条 in_progress;调用把它标成多条会被拒绝(抛错)单活动纪律的一次一步推进
- id: tool-todo
name: '@deepseek-ai/dsh-tool-todo'
allowParallelInProgress: true # 必配;fan-out 用 true,单活动纪律用 false
// packages/todo/tool-todo/src/index.ts
if (!allowParallel && active > 1) {
throw new Error(`invalid todos: at most one task may be in_progress (got ${active})`)
}
  • 单 owner 边界:todo_write 需要 owning agent session,executeexec.agent 不存在(非 agent 调用方)直接抛错,不做 no-op。
  • 整表替换是唯一操作:send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits);每次调用 append 一条 todo/write 快照,replay 是 last-write-wins,无读回。投影(todos key)持有最新整表,turn/start 清零到 null

三者怎么配合

一个典型长任务的推进:

创建一个 goal「完成 X」 → 长期方向
用 todo_write 拆本轮待办 → 短期步骤
较慢/可后台的操作进 jobs → 不阻塞主循环,随时回来收结果
goal 完成时 complete() → 收尾

三者的共同点:都以事件溯源/注册表持有状态,谁也不会丢进度。

验证

zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E '"goal/|"todo/' | head
# jobs 是进程内的,用 dsh web --dump-config | grep jobs 看是否装载

下一步