DeepSeek Harness Superagent 执行模型:从工单到 PR,再到恢复

系列第 4 篇。本文跟着一张被接受的 Ticket,走完执行、交付和恢复。它建立在前面的领域模型与授权模型之上。


一、Ticket 的阶段:谁在等谁

每个 open Ticket 有一个 stage,由它正在等什么决定。表从上往下读,第一个匹配的行生效。

StageConditionPanel label
person / delivery向 Executor Session 投递 Input 失败;2 秒后重试,之后按倍增至最多 1 分钟;成功、新 Assignment 或 Ticket 结束会清除Input undelivered
person / answer它有一个 AttentionAwaiting answer
person / review最新 Report 是 readyAwaiting review
person / blocked, failed最新 Report 是 blocked 或 failed,且之后没有人的消息到达该 TicketBlocked · Failed
working / queued没有 Assignment,或 Assignment pendingQueued
idle最新 Report 是 waiting,或 Assignment endedWaiting for a Trigger
working其他情况Working

person 阶段表示没有人工输入就无法继续。一条新的人类消息会把 blocked 或 failed 的 Ticket 释放给 Executor,于是它进入下一个匹配阶段,通常是 working。

idle Ticket 已经完成本轮且没有未解决问题。订阅 Thread 上的 Trigger 可以继续它。

新 Assignment 等待执行槽

Executor 启动

ready Report + PR

Attention 等待回答

blocked / failed Report

waiting Report 或 Assignment 结束

人发消息

人回答问题

PR close 且工作完成

订阅 Thread 新 Trigger

Input 投递成功

queued

working

person_review

person_answer

person_blocked

idle

ended

person_delivery

二、Executor:每个 Ticket 一个 Session 和一个目录

每个 Ticket 得到一个名为 worker-<ticket id> 的 Executor Session,以及一个以 Ticket 短名命名的目录。目录创建在配置的 execution root 下,默认是 <stateDir>/work。

Executor Session 以操作者 OS 用户身份运行命令,无需逐条审批。它的 dsh-superagent-worker preset 包含工作所需工具,并在 5 秒后把前台 shell 命令移到后台任务。这样 worker 能在下一步接收新消息,而不是被长命令卡住。

主 Session 使用 dsh-superagent-router,同样有 5 秒 shell 与 job-wait 限制,但工具集更小,只用于路由。Session 保留创建时选择的 preset。Worker subagent 使用 DeepSeek Harness 的 standard preset。

后台工作与 worker slots

当 turn 结束时仍有 shell job 或 subagent 在运行,Ticket 会保留 worker slot 并保持 working。它不会报告 waiting,主 Session 也不收到 waiting report。完成的 job 或 subagent 会在同一 slot 中用结果唤醒 Executor。只有一轮结束时没有后台工作,Executor 才正常报告 waiting。

每轮结束后,后台工作最多有 30 分钟完成。到时限后,shell job 被 kill,Ticket 报告 waiting。报告包含 Executor 最后的话,以及哪些 job 被杀、哪些 subagent 仍活跃。Subagent 允许继续。Executor 在下一轮读取被杀 job 的结果。

把工作交给人会立即释放 slot,并保留 Report 生效。这适用于 ready Report 或问题之后,除非此后又有 Input 到达;也适用于任何 blocked 或 failed Report 之后。

Handoff 时 subagent 停止。为 review 提供的 server 等 shell job 可以继续到 Assignment 结束。它们不占 slot,也不受 30 分钟限制。它们的完成不会唤醒 Executor;Executor 在下一轮读取结果。

人也可以直接在 Executor Session 里写消息启动一轮。其后台工作遵循同样规则。

Assignment 结束时,其 shell job 被 kill,且不唤醒 Session。Subagent 被释放,它们醒来产生的 completion turn 被丢弃。替换 Assignment 和其等待 Input 在 shutdown 完成后启动。

在 DeepSeek Harness 进程内 stop 或 restart 插件走同样的 shutdown 路径。尚未进入 Session history 的 Input 会保留。Shutdown 等待每个 worker 后台工作最多 10 秒;进程 restart 会直接结束该工作。运行中被中断的 Assignment 以 recovery note 重启。

Worker subagent(包括嵌套的)使用 DeepSeek Harness 的 standard preset,并带 worker 权限。它们只保留 worker 允许的工作工具,Ticket 结束后失去这些工具。在 standard 下,shell 命令 60 秒后移到后台。Subagent 的 job 在其结束时被 kill。

准备 Ticket 目录

第一次 Assignment 前,如果 Ticket 目录不存在,prepare command 会在 execution root 中以 sh 运行一次,最多十分钟。没有 prepare command 时,worker 在空目录开始。

失败的命令使 Ticket 失败并记录输出。retry 或人在 Ticket Thread 上的消息会再次尝试准备。典型命令用 git worktree 创建 checkout。当分支最后一段与 Ticket 短名匹配时,该 PR 属于此 Ticket。

并发

Ticket 从 launch 开始(包括 prepare)持有 worker slot,或从唤醒 idle Executor 的 delivery 开始持有。它在整个 active turn 中也持有 slot。

配置的 concurrency limit 适用于这些 slots,另有一个额外 slot 给人的消息或 status request。这样即使正常 slots 满了,人仍能继续已有 Ticket。新工作不能阻止人继续已有工作。

其他 Input(例如 CI 结果)等待正常 slot。launch 在 working / queued 等待空 slot。launch 并发运行,所以慢 prepare command 只延迟自己的 Ticket。

执行变量

prepare command 和 Executor instructions 收到同样的执行变量。命令读环境变量;Executor 在 prompt 中读值和描述。它们都不含凭据。

VariableRegistered byValue
DSH_SUPERAGENT_DIR, DSH_SUPERAGENT_NAME, DSH_SUPERAGENT_TICKETExecutorTicket 的目录、短名和 id
DSH_SUPERAGENT_BLOCKED_COMMANDSExecutorExecutor 不能运行的命令
DSH_SUPERAGENT_GITHUB_REPOSITORYGitHubSource 监听的仓库,owner/name
DSH_SUPERAGENT_GITHUB_ACCOUNT, DSH_SUPERAGENT_GITHUB_COMMIT_EMAILGitHubAgent 账号和 commit email
DSH_SUPERAGENT_GITHUB_NUMBER, DSH_SUPERAGENT_GITHUB_PR_HEADGitHub启动 Ticket 的 Issue 或 PR,以及 PR head ref
DSH_SUPERAGENT_FEISHU_BOT, DSH_SUPERAGENT_FEISHU_CHATFeishuBot,以及启动 Ticket 的 chat

Ticket-specific 值来自目录和原始 Trigger。其他值来自当前部署设置。只有面板能修改执行环境设置;发到主 Session 的文本不能改变配置的 shell command。

Worker 通过 agent 账号的 gh 访问 GitHub,通过 bot 的 lark-cli 访问 Feishu。Blocked commands 按名字调用时失败。

这些 wrapper 和目录建立的是默认值,不是安全边界。worker 仍然可以通过绝对路径执行程序。

三、自动 merge 的边界

Auto-merge 由 Delivery requirements prompt 管理。Host 没有 merge-specific decision logic。

默认情况下,Executor 从不用 agent 账号启用 auto-merge:GitHub 会把 merge 归于启用它的人。Delivery requirements 必须指定另一个账号的 CLI 用于 merge。没有指定时,由人 merge PR。

默认指令只允许在以下条件下对 Ticket 打开的 PR 启用 auto-merge:

  • 它已 ready;
  • 所需的 approval 覆盖 head commit;
  • additions 加 deletions 少于 200 行。

推送新 revision 前,Executor 撤回 auto-merge。

没有正文的 review(例如 approval)也会作为 Trigger 到达,并可以唤醒正在等待 review 的 Executor。这些指令管理的是 Agent 行为,不是 GitHub merge 规则。要真正强制 approval,应在 branch protection 中要求,并 dismiss stale approvals。省略 merge CLI 或删除 auto-merge 段落,可以要求人工 merge。

四、Ticket 如何结束

  • Ready 但没有 PR: ready Report 将 Ticket 结束为 completed。
  • Nothing to do: Executor 发现没有人需要行动的工作,例如请求无法复现或已被替代,报告 dropped。Ticket 不发送消息就结束,除非匹配规则要求;人稍后在 Thread 上发消息可恢复它。

Pull Request 的结束规则

Ticket 从 Executor 报告开始跟随它启动的 PR。它也会跟随从其分支打开、并由 Executor 报告或 GitHub Source 发现的每个 PR。重新打开的 PR 会再次跟随。

当它打开的所有 PR 都不再 open 时,Ticket 完成。如果没有打开任何 PR,它跟随的所有 PR 都必须关闭。然后 Ticket 以最后一次相关 close 的结果结束,并保留 Subscriptions,以便后来的人评论能恢复它。

如果 resumed、retried 或 recovered 的工作尚未完成,close 不会结束 Ticket。如果 Executor 已确认或回答了它拥有的 follow-up,但之后还没有产生非 progress Report 或结束一轮,Ticket 也会等待。之后的 ready Report 在同样 PR 完成规则满足时结束 Ticket。

Chat Channels 收到一条 PR 结果最终通知。GitHub 已在 PR 本身上显示结果。移除 Ticket 对 PR 的 Subscription 也会停止跟随该 PR。

因此,“Awaiting review”意味着带 PR 的 ready Report。

Follow-up 与上下文 Thread

Executor 决定 Ticket 自己 Thread 上的人类 follow-up。它处理 scope 内请求,交回超出 scope 或属于其他 Ticket 的请求。一旦没有 Executor 拥有消息,它就变成主 Session 的候选。主 Session 对该 follow-up 只能在人的请求下停止工作。

Input 在 Session 把它带入一步并随后 report 或结束 turn 后算 read。Ticket 结束时,未 read、且未确认、回答、交回的消息返回 Eval。已 read 但没有行动的消息(例如 thanks)不会返回。

只用于上下文的 Thread 仍会投递消息,但不给 Ticket 人类 follow-up 的 ownership,也不恢复 ended Ticket。这些消息可以独立被评估。

Source 会在 live Ticket 订阅期间轮询订阅 Thread,并在最后一次 Trigger 与订阅 Ticket 结束中较晚时间后的两周内继续轮询。此后,消息只通过普通 intake 到达:GitHub notifications 或 Feishu live events。

五、失败与恢复

Executor blocked 或 failed

当 Executor Session 报告 blocked 或 failed,或启动失败、出错而记录 failed 时,Ticket 不再接收自动 Input。

模型 provider 或网络的 passing failure(HTTP 408、429、5xx,或连接被拒绝、重置、超时)会先重试三次,退避逐渐增长:launch 会重新运行;turn 失败的 worker 被告知继续;失败的 prepare command 不重试。

一旦 failed,CI 和 bot 消息会记录在它上面,但不投递、不唤醒 Session。

它的工具保持可用,因此人可以直接在 Session 中继续。

失败的 launch 不会自行重试。

人的消息、后来既非 blocked 也非 failed 的 Report,或 retry,会结束 hold。

这些状态从存储的 Reports 和 Inputs 恢复,所以 restart 后仍保留。

Retry

Retry 可用于处于以下状态的 open Ticket:

  • person / delivery;
  • person / blocked;
  • person / failed。

Retry 的 remedy 是新的 Assignment:重新启动 worker,并接收等待的 Input。

一旦人的消息到达 blocked 或 failed Ticket,Executor 就拥有消息,Ticket 显示为 working;失败的 launch 重新启动时显示为 queued,不再提供 retry,除非投递消息失败。

Retry 结束当前 Assignment,并在同一 Executor Session、目录、分支和 PR 中启动新 Assignment。

Executor 模型只在 Session 创建时应用;已存在 Session 保留其模型。

Restart

Running Assignment 以 “host restarted” 结束,新 Assignment 以 recovery note 开始,提示 Executor 在重复任何外部动作前检查目录、git history 和 GitHub,按 oldest Ticket 优先。

Waiting、ready、blocked、failed 的 Assignment 保持原样,除非它们在 Report 后收到 Input,这种情况下也会重新启动。

Session、目录、Subscriptions、待处理消息和下面的 stored work 在 restart 后保留。每个 Executor Session 把哪些消息带入 history 的记录也保留:被 restart 截断的 turn 所取的消息,只有在随后 Report 或 turn end 之后才算 read;被 cancel 或 error 在进入 history 前截断的消息,仍算 unread。

Retry 或 restart 会把替换 Assignment 所收到、但其 Executor Session 未取的消息再次交给新 Assignment。仍在 Session 中等待的副本,会被取出后清理,保证新投递是唯一副本。

Stored work:先存再做

dsh-superagent 在 Decision 后要做的事情,例如把 candidate、follow-up 或 report relay 给主 Session、启动 Assignment、投递 Input,都会先存储,再执行。

失败的 relay 或 delivery 会在 2 秒后重试,然后按倍增至最多 1 分钟;失败的 launch 使 Ticket 失败,除非是 passing failure。

每种工作在 overview 的 system problems 中报告 error,只在某一项连续失败三次后。失败的 Input delivery 会显示在 Ticket 上。

到主 Session 的 relay 保持顺序,notice 也一样;失败的一项会阻塞后面同类项。

Source failure

GitHub Source 分别读取每个 Issue、PR 和 Thread:一次读取失败会把 error 留在 Thread 上,下次 poll 再读,并计入 Source error;默认连续三次失败 poll 后报告。任意一次成功 poll 会重置计数。

GitHub 在 agent 失去 private 仓库访问权限时也会返回 404,因此 404 会重试。

读取持续失败的通知 Issue/PR,在首次失败一天后 dropped;GitHub 报告删除时立即 dropped。之后它不再使 poll 失败,其 Thread 保留 error,Source state 会提到它。

读取位置保持不动,因此后来的通知或 Thread poll 会读取失败期间遗漏的内容。

Feishu event consumer 报告每一次失败,订阅 topic 的 poll 把每次失败留在其 Thread 上。

GitHub list 或 Feishu page 中格式错误的 item 会被跳过,Source state 计数。

Channel failure

Outgoing messages 先存储,再作为 stored work 发送,从 5 秒开始退避,倍增至 300 秒。

Ticket 到同一会话的消息按入队顺序发送,所以 acknowledgement 先于后续 answer 或 result:如果同一 Ticket 到同一会话的更早消息尚未发出,当前消息会等待,直到更早消息连续失败三次并显示为 problem。

其他 Ticket 或无 Ticket 的消息不会等待它。

有失败尝试的消息在发出或撤回前是 system problem。

消息只会在 Source 不再监听会话,或 Source 回答该会话永远不能接收时 dropped;Source 关闭时则等待。

Source 会在 GitHub 上为 deleted Issue 或 locked conversation 返回这种情况,在 Feishu 上为 recalled message 或已找不到的消息返回这种情况。

对已不存在 review comment 的回复会改发到 PR conversation。

Dropped message 保留记录,包括何时、为何 dropped、为哪个 Ticket,因此它永远不会再次排队;dropped acknowledgement 读作 dropped,而不是 sent。

Pause processing

Pause 会停止 dispatch、新 Eval、Eval timeout、Input delivery 和 replies;已开始的 launch 等待处理恢复。

Source 继续记录 Trigger,运行中的 Executor turn 不被中断。

人发给 ended Ticket 的消息会等待:pause 后第一次 pass 会恢复 Ticket,只要当时 Mandate 仍按 Policy 有效。

Pause 期间进行中的 Eval 不过期;恢复后,deadline 向后移动 pause 的时长。

Invalid configuration

存储的 Source configuration 被其 kind 拒绝时,该 Source instance 被禁用。

Logo

更多推荐