本能力家族涵盖三种让智能体在其主 turn 循环周围持续工作的方式:一个模型所有的待办列表(todo)、可跨单个 turn 存活的后台任务(background jobs),以及稍后以普通对话 turn 形式返回的定时提醒(scheduled reminders)。它们的共同点是——持久化的、以会话/会话 id 为作用域的状态,模型通过小巧的工具表读取、修改——但分布在截然不同的包中,各自语义不同。
| 包 | 主题 | 角色 |
|---|---|---|
packages/todo/tool-todo | Todo | 面向模型的 todo_write 工具 + todos 投影 |
packages/jobs/jobs | Jobs | 服务定义(ctx.jobs)+ 共享类型 |
packages/jobs/jobs-local | Jobs | 进程本地提供方(LocalJobRegistry) |
packages/jobs/tool-jobs | Jobs | 面向模型的 job_* 工具 + 完成通知投递 |
packages/schedule/schedule | Schedule | 持久提醒 + schedule_* 工具 |
Todo:packages/todo/tool-todo
待办状态是会话所有、整表替换的状态。唯一的工具 todo_write 接收一个完整的 todos 数组并替换之前的列表——没有单条编辑,也没有 add/update/complete 操作。每个条目是 { content, status },其中 status ∈ { pending, in_progress, completed }。
execute 向拥有该响应的智能体会话追加一条 todo/write 事件(exec.agent.session.append('todo/write', { todos }));重放为最后写入胜出(last-write-wins),UI 从这些会话事件渲染。非智能体调用方(没有拥有会话)会被拒绝。工具会校验 schema 无法表达的约束:去空白后非空、唯一的 content,以及除非设置了 allowParallelInProgress,否则最多一个 in_progress 条目。
{ "todos": [ { "content": "重构智能体循环", "status": "in_progress" } ] }配置键 allowParallelInProgress 为必填,无默认值——部署必须显式选择是否允许同时有多个激活条目。它只改写工具的 description:并行模式要求模型标记每个正被积极处理的任务;单柄模式要求恰好一个,并拒绝标记多个的调用。
当组合了会话投影接缝时,插件还会注册一个 todos 投影(键 'todos',值 TodoItem[] | null):
- 在
todo/write时 → 新列表; - 在
turn/start时 →null(清空;turn/end保留已完成清单可见); - 否则 → 返回同一状态引用。
后台任务运行时:packages/jobs/*
任务是跨工具调用存活的长时间后台工作——后台 bash、PTY terminal_send、子智能体都注册到通用 ctx.jobs 运行时,并通过同一组三个工具读取、列出、终止。
服务定义——ctx.jobs
JobRegistry(packages/jobs/jobs/src/index.ts)是一条抽象接缝。JobId 是形状为 <kind>-N 的 branded id——故意可预测,因为访问控制靠授权而非保密(以拥有者会话 id 作为围栏)。JobKind 派生自可扩展合并的映射;生产方通过声明合并扩展它:
interface JobKindMap { bash: 'bash'; subagent: 'subagent' }
// 插件通过合并该接口继续追加 kindJobStatus 为 'running' | 'stopping' | 'completed' | 'killed' | 'failed';生产方特定的细节归属 JobSnapshot.detail。抽象表面:
start(spec) -> JobId list(caller?) -> JobSnapshot[]
get(id, caller?) -> JobSnapshot read(id, caller?) -> JobRead
kill(id, caller?, reason?) wait(id, timeoutMs, caller?, signal?)
onJobDone(listener) onJobsChanged(listener)
attachController(name)JobStart 声明 kind、一行 label、可选 outputLimitBytes、拥有者 Agent(owner),以及同步 run(): JobHooks。JobHooks 暴露 cancel(reason?)、仅在生产方释放资源后才 resolve 的 done: Promise<JobOutcome>,以及可选的 readOutput()——它区分消耗型的流式任务与仅终态输出的任务。结算为先到先得(first-wins):一条终态记录、释放所有 waiters、一轮受控的监听器通知;完成于最后宣布,以便 reporter 可同步打开一轮模型 turn。
本地提供方——JobRegistry → LocalJobRegistry
LocalJobRegistry(packages/jobs/jobs-local/src/index.ts)是进程本地提供方。配置 maxConcurrentJobsPerOwner(默认 10)是一个正整数安全整数,统计每个确切拥有者的 running + stopping 记录,未拥有任务共用一个桶;撞到上限的生产方会被告知先 job_kill 一个多余任务再重试。start 在没有 controller 为拥有者服务时拒绝工作(注册表强制要求:生产方不能启动其拥有者无法收集或终止的工作)。
模型工具——tool-jobs
dsh-tool-jobs 命名三个工具并挂载一个 controller:
| 工具 | 用途 |
|---|---|
job_output | 读取流式增量(或幂等的终态输出);wait: true + timeout_ms 阻塞并夹取上限 |
job_list | 列出调用者所有/未拥有的任务,含 id、kind、status |
job_kill | 请求取消;返回 cancellation-requested 或 already-finished |
配置掌控等待上限与完成通知投递:waitTimeoutMs(默认 30 秒)、maxWaitTimeoutMs(默认 10 分钟——更大的模型提供值会被夹低)、completionDelivery('wakeup' 为闲置拥有者打开一轮 turn,'quiet' 保持挂起),以及 maxConsecutiveWakes(默认 3,约束"唤醒的 turn 又启动其完成会再次唤醒它的任务"这一自激链)。
完成投递是微妙之处。结算时,插件构建一条有界的、经 retainer 截断的完成通知,并:
- 唤醒闲置拥有者(
owner.followup(message)),直到唤醒预算耗尽,或 - 注入忙碌拥有者的下一步(
owner.inject(message))——通知在下一步 inbox 中等待,因此同时结算的任务只花一步。
交互式消费者:job_output 最多等满上限,且每个响应都以一行 [status: ...] 结尾。加载 tool-jobs 还会追加横切系统提示指引:track every background job id you start、不要忙轮询、用 job_output 收集、对不再相关的任务用 job_kill。
调度:packages/schedule/schedule
调度拥有以普通后续对话 turn 形式返回原存活会话的持久提醒。它刻意不是通用调度器:没有外部通知通道,也没有冷会话调度器——投递模式固定为 session-local,因此提醒只在会话存活时准点运行,否则在会话恢复前变为 overdue。这种会话本地衰减也体现在模型工具的 state 字段上(scheduled | overdue)。
工具
| 工具 | 行为 |
|---|---|
schedule_create | prompt 加上恰好一个:after_seconds、at、every_seconds |
schedule_list | 按创建顺序列出所有活跃提醒(id、UTC 目标、state、投递模式) |
schedule_delete | 按精确 id 删除;未知/已完结 id 返回 deleted: false |
after_seconds 是正整数安全整数的延迟;at 要么是携带时区偏移的严格 RFC 3339 字符串,要么是带显式 IANA 时区的本地 { date, time, time_zone } 对象;every_seconds 是固定频率间隔,至少 MIN_EVERY_INTERVAL_SECONDS = 300(五分钟)——没有 Cron 表达式、循环时区或共享冷却。固定频率提醒以创建时刻为锚、跳过错过的实例:若会话在多个目标期间一直冷或忙,每条 every 记录在一次批量中只贡献其最新到期实例。每次创建都把首个目标规范化为四位年份的 RFC 3339 UTC scheduledAt,因此重放绝不依赖环境时区状态。
持久状态与生命周期
唯一持久权威是版本 1 的 schedule/change 会话事件,操作有 create、delete、dispatch(ScheduleChange)。创建存储完整记录;delete 与一次性 dispatch 是终态且仅 id 的转移;every dispatch 携带 acceptedAt(拍板时的墙钟时间)并通常推进记录而非终止它。严格解码器与 fold 会拒绝未知版本、多余字段、复用 id、形状不匹配以及针对非活跃记录的转移。fork 只 fold SessionHeader.seedLength 之后的事件,因此保留历史却不继承父会话的活跃提醒。
到期工作会等待智能体完全空闲、抢占维护阶段、排队一次 followup()(一轮普通的后继 turn)、追加 dispatch 变更。投递是尽力而为的至少一次:它只通过普通转录呈现(无独立 Web 回执),且"已受理但尚未持久 dispatch"的窄崩溃区间可能在恢复后重复提醒内容。
配置与工具注册
调度工具只注册在启用了 opt-in 的 Schedule 插件之后创建的活跃 root Agent scope 内(registerScheduleTools)。稳定的工具级错误码:invalid_prompt、invalid_selector、invalid_rule、invalid_time_zone、not_future、time_out_of_range、frequency_too_high、corrupt_schedule_log、internal_error,以及 persistence_uncertain(在无法确知 eager 写入是否提交时返回,而非猜测)。管理调用会串行通过共享的 Session-persistence 屏障。
包
| 包 |
|---|
@deepseek-ai/dsh-tool-todo |
@deepseek-ai/dsh-jobs |
@deepseek-ai/dsh-jobs-local |
@deepseek-ai/dsh-tool-jobs |
@deepseek-ai/dsh-schedule |
延伸阅读
- 交互(Interactions)——
followup()与 inbox-claimed turn 投递 - 上下文来源(Context sources)——会话事件 fold 与投影
- Shell——
bash与 terminal 工具如何注册run_in_background任务 - 子智能体——
subagent任务 kind 及其生命周期 docs/subsystems/jobs.md——后台任务运行时参考docs/subsystems/schedule.md——持久调度记录、dispatch 与固定频率追补