Skip to content

本能力家族涵盖三种让智能体在其主 turn 循环周围持续工作的方式:一个模型所有待办列表(todo)、可跨单个 turn 存活的后台任务(background jobs),以及稍后以普通对话 turn 形式返回的定时提醒(scheduled reminders)。它们的共同点是——持久化的、以会话/会话 id 为作用域的状态,模型通过小巧的工具表读取、修改——但分布在截然不同的包中,各自语义不同。

主题角色
packages/todo/tool-todoTodo面向模型的 todo_write 工具 + todos 投影
packages/jobs/jobsJobs服务定义(ctx.jobs)+ 共享类型
packages/jobs/jobs-localJobs进程本地提供方(LocalJobRegistry
packages/jobs/tool-jobsJobs面向模型的 job_* 工具 + 完成通知投递
packages/schedule/scheduleSchedule持久提醒 + 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 条目

jsonc
{ "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

JobRegistrypackages/jobs/jobs/src/index.ts)是一条抽象接缝。JobId 是形状为 <kind>-N 的 branded id——故意可预测,因为访问控制靠授权而非保密(以拥有者会话 id 作为围栏)。JobKind 派生自可扩展合并的映射;生产方通过声明合并扩展它:

ts
interface JobKindMap { bash: 'bash'; subagent: 'subagent' }
// 插件通过合并该接口继续追加 kind

JobStatus'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、拥有者 Agentowner),以及同步 run(): JobHooksJobHooks 暴露 cancel(reason?)、仅在生产方释放资源后才 resolve 的 done: Promise<JobOutcome>,以及可选的 readOutput()——它区分消耗型的流式任务与仅终态输出的任务。结算为先到先得(first-wins):一条终态记录、释放所有 waiters、一轮受控的监听器通知;完成于最后宣布,以便 reporter 可同步打开一轮模型 turn。

本地提供方——JobRegistryLocalJobRegistry

LocalJobRegistrypackages/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-requestedalready-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_createprompt 加上恰好一个after_secondsatevery_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 会话事件,操作有 createdeletedispatchScheduleChange)。创建存储完整记录;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_promptinvalid_selectorinvalid_ruleinvalid_time_zonenot_futuretime_out_of_rangefrequency_too_highcorrupt_schedule_loginternal_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 与固定频率追补