204 lines
17 KiB
Markdown
204 lines
17 KiB
Markdown
# 003 执行
|
||
|
||
本文带着一份已接单的 Ticket 走完执行、交付与恢复。它建立在 [001 领域模型](001-领域模型.md) 与 [002 授权](002-授权.md) 之上。
|
||
|
||
## Ticket 阶段
|
||
|
||
每个 open Ticket 恰好处于一个阶段,由它在等什么决定。表从上往下读,第一条匹配的行生效。
|
||
|
||
| 阶段 | 条件 | 面板标签 |
|
||
| --- | --- | --- |
|
||
| person / delivery | 向 Executor Session 投递一个 Input 失败;2 秒后重试,之后延迟逐次翻倍、封顶一分钟;一次成功、一个新的 Assignment、或 Ticket 的结束都会清掉它 | Input undelivered |
|
||
| person / answer | 它有一个 Attention | Awaiting answer |
|
||
| person / review | 最近的 Report 是 `ready` | Awaiting review |
|
||
| person / blocked, failed | 最近的 Report 是 `blocked` 或 `failed`,且此后没有人的一条消息到达过该 Ticket | Blocked · Failed |
|
||
| working / queued | 没有 Assignment,或只有一个 pending 的 | Queued |
|
||
| idle | 最近的 Report 是 `waiting`,或 Assignment 已结束 | Waiting for a Trigger |
|
||
| working | 其余一切 | Working |
|
||
|
||
`person` 阶段意味着这个 Ticket 没有人的输入就走不下去。一条新的人类消息会把 `blocked` 或 `failed` 的 Ticket 释放给它的 Executor,于是它进入下一条匹配的阶段,通常是 `working`。
|
||
|
||
一个 `idle` Ticket 结束了它的回合,没有悬而未决的问题。被订阅 Thread 上的一个 Trigger 可以让它继续。
|
||
|
||
## Executor
|
||
|
||
每个 Ticket 得到一个名为 `worker-<ticket id>` 的 Executor Session,和一个以 Ticket 短名命名的目录。目录创建在配置的执行根之下,默认为 `<stateDir>/work`。
|
||
|
||
Executor Sessions 以操作者的 OS 用户身份免审批地运行命令。它们的 `dsh-superagent-worker` 预设包含干活所需的工具,并把前台 shell 命令在 5 秒后移入后台任务。这让一个 worker 能在下一个步骤接收新消息,而不是干等一条长命令跑完。
|
||
|
||
main Session 使用 `dsh-superagent-router`,带同样的 5 秒 shell 与 job 等待上限,以及一个更适合路由的更小工具集。Sessions 保留创建时选定的预设。Worker subagents 使用 DeepSeek Harness 的 `standard` 预设。
|
||
|
||
### 后台工作与 worker 槽位
|
||
|
||
当一个回合结束、还有 shell 任务或 subagents 在运行时,Ticket 保留它的 worker 槽位、维持 working。它不会被上报为等待,main Session 也不会收到 waiting 报告。一个完成的 job 或 subagent 带着它的结果、在同一个槽位里唤醒 Executor。一旦某个回合在没有后台工作的情况下结束,Executor 才照常上报 waiting。
|
||
|
||
每个回合之后,后台工作至多还有 30 分钟完成。到点时,shell 任务被杀掉,Ticket 被上报为 waiting。报告会带上 Executor 最后说的话、以及哪些 job 被杀、哪些 subagents 仍在活动。Subagents 允许继续运行。Executor 在它的下一个回合读取被杀 job 的结果。
|
||
|
||
向人的移交会立刻释放槽位,并让 Report 保持有效。这适用于一次 `ready` Report 或一个问题之后——除非此后又有一个 Input 到达——以及任何 `blocked` 或 `failed` Report 之后。
|
||
|
||
移交时,subagents 停止。Shell 任务——比如一个为评审而开着的服务器——可以继续到 Assignment 结束为止。它们不占槽位、也不受 30 分钟上限约束,包括在之后的回合里。它们的完成不唤醒 Executor;Executor 在下一个回合读它们的结果。
|
||
|
||
人也可以直接在 Executor Session 里写一条消息来发起一个回合。它的后台工作遵循同样的规则。
|
||
|
||
一个 Assignment 结束时,它的 shell 任务被杀掉且不唤醒 Session。它的 subagents 被释放,它们唤醒的任何完成回合都被丢弃。接替的 Assignment 及其等待中的 Inputs 在那次关闭完成之后才开始。
|
||
|
||
在 DeepSeek Harness 进程内停止或重启插件走同一条关闭路径。尚未进入 Session 历史的 Inputs 会被保留。关闭会为每个 worker 的后台工作至多等待 10 秒;一次进程重启会直接终结这些工作。一个在忙碌中被中断的 Assignment 会带着恢复备注重启。
|
||
|
||
Worker subagents(包括嵌套的)使用 DeepSeek Harness 的 `standard` 预设,并带着 worker 的权限。它们只保留 worker 允许的工作工具,并在 Ticket 结束时失去这些工具。`standard` 之下,一条 shell 命令在 60 秒后转入后台。一个 subagent 的 jobs 在它结束时被杀掉。
|
||
|
||
### 准备 Ticket 目录
|
||
|
||
在第一个 Assignment 之前,若 Ticket 目录不存在,prepare command 会在执行根用 `sh` 运行一次。它有十分钟完成。没有 prepare command 时,worker 在一个空目录里启动。
|
||
|
||
命令失败会让 Ticket 失败并记录它的输出。一次重试、或 Ticket 某个 Threads 上的一条人类消息,会再次尝试准备。一条典型的命令用 `git worktree` 创建一个 checkout。当一个分支的最后一段路径与 Ticket 的短名一致时,那个 pull request 就属于这个 Ticket。
|
||
|
||
### 并发
|
||
|
||
一个 Ticket 从一次启动(包括准备)开始、或从唤醒其空闲 Executor 的那次投递开始,持有 worker 槽位。在一个活跃回合的整个过程里,它也持有该槽位。
|
||
|
||
配置的并发上限作用于这些槽位,并为人类消息或状态请求多留一个额外槽位。这样的唤醒在普通槽位占满时可以使用额外槽位,并在新启动之前拿走下一个空槽。因此,新的工作永远不会妨碍一个人继续一个已有的 Ticket。
|
||
|
||
其他 Inputs——例如 CI 结果——等待普通槽位。一次启动在 `working / queued` 里等到有槽位空出。启动们并发运行,所以一条慢的 prepare command 只拖慢它自己的 Ticket。
|
||
|
||
### 执行变量
|
||
|
||
prepare command 与 Executor 指令收到同样的执行变量。命令读环境变量;Executor 在它的提示词里读这些变量的值与说明。其中没有凭据。
|
||
|
||
| 变量 | 注册方 | 值 |
|
||
| --- | --- | --- |
|
||
| `DSH_SUPERAGENT_DIR`、`DSH_SUPERAGENT_NAME`、`DSH_SUPERAGENT_TICKET` | Executor | Ticket 的目录、短名与 id |
|
||
| `DSH_SUPERAGENT_BLOCKED_COMMANDS` | Executor | Executor 不得运行的命令 |
|
||
| `DSH_SUPERAGENT_GITHUB_REPOSITORY` | GitHub | 该 Source 盯守的仓库,`owner/name` |
|
||
| `DSH_SUPERAGENT_GITHUB_ACCOUNT`、`DSH_SUPERAGENT_GITHUB_COMMIT_EMAIL` | GitHub | agent 账号及其 commit email |
|
||
| `DSH_SUPERAGENT_GITHUB_NUMBER`、`DSH_SUPERAGENT_GITHUB_PR_HEAD` | GitHub | 发起该 Ticket 的 Issue 或 pull request,以及它是 pull request 时的 head ref |
|
||
| `DSH_SUPERAGENT_FEISHU_BOT`、`DSH_SUPERAGENT_FEISHU_CHAT` | Feishu | 发起该 Ticket 的 bot 与 chat |
|
||
|
||
Ticket 专属的值来自它的目录与发起它的 Trigger。其他值来自当前的部署设置。执行环境设置只有面板能改;发给 main Session 的文本改不了配置好的 shell 命令。
|
||
|
||
Workers 通过 agent 账号的 `gh` 访问 GitHub,通过 bot 的 `lark-cli` 访问 Feishu。被屏蔽的命令按名字调用时会失败。
|
||
|
||
这些包装器与目录立的是默认值,不是安全边界。worker 仍可以用绝对路径执行一个程序。
|
||
|
||
## 合并
|
||
|
||
Auto-merge 由 **Delivery requirements** 提示词管辖。host 没有合并专属的决策逻辑。
|
||
|
||
默认情况下,Executor 从不以 agent 账号开启 auto-merge:GitHub 会把合并记在开启它的人头上。Delivery requirements 必须点名另一个账号的 CLI 来合并。没有指定时,由人来合并 pull request。
|
||
|
||
默认指令只在这个 pull request 为该 Ticket 所开、已经就绪、所需批准覆盖了 head commit、且新增加删除合计少于 200 行时,允许 auto-merge。在推送下一个 revision 之前,Executor 先撤回 auto-merge。
|
||
|
||
没有正文的评审——例如批准——也作为 Triggers 到达,并能唤醒一个在等评审的 Executor。这些指令管的是 agent,不是 GitHub 的合并规则。要强制评审,就在分支保护里要求评审并让过期批准失效。想强制人工合并,就不配合并 CLI、或删掉 auto-merge 那一段。
|
||
|
||
## Ticket 如何结束
|
||
|
||
- **没有 pull request 的就绪:** 一次 `ready` Report 把 Ticket 以 completed 结束。
|
||
- **无事可做:** 一个发现没有可供人行动的工作的 Executor——例如无法复现或已被取代的请求——上报 `dropped`。Ticket 静默结束,除非命中的规则要求说一声;此后其 Threads 上的人类消息会让它恢复。
|
||
### Pull requests
|
||
|
||
一个 Ticket 一旦它的 Executor 上报了它由此开始的那个 pull request,就开始跟踪它。它同样跟踪每个从其分支开出、并被 Executor 上报或被 GitHub Source 发现的 pull request。重新打开的 pull request 会被重新跟踪。
|
||
|
||
当它开出的 pull requests 没有一个还开着时,这个 Ticket 就完成了。若它一个都没开,则它跟踪的所有 pull requests 都必须已关闭。于是它带着最后一次相关关闭的结果结束,并保留它的 Subscriptions,让后来的评论能让它恢复。
|
||
|
||
在有被恢复、被重试或被找回的工作尚未完成时,一次关闭不会结束 Ticket。若 Executor 认领或答复过一条它认领的跟进、但读完那条消息之后既没有再发过一次非 progress 的 Report、也没有结束过一次回合,同样要等。之后的 `ready` Report 一旦满足同样的 pull-request 完成规则,就结束 Ticket。
|
||
|
||
Chat Channels 会收到一条关于 pull-request 结果的最终通知。GitHub 本来就会在 pull request 上显示结果。移除 Ticket 对某个 pull request 的 Subscription,也同时让它不再跟踪那个 pull request。
|
||
|
||
因此"Awaiting review"意味着一份带着 pull request 的 `ready` Report。
|
||
|
||
### 跟进与上下文 Threads
|
||
|
||
Executor 决定其 Ticket 自己 Threads 上的人类跟进。它处理 scope 内的请求,把超出 scope 或属于另一个 Ticket 的请求交还。一旦没有 Executor 认领这条消息,它成为 main Session 的候选。否则 main Session 对跟进只能应人的要求去停止工作。
|
||
|
||
一个 Input 在 Session 把它纳入一个步骤、并且该步骤随后上报或结束回合之后,算作已读。Ticket 结束时,那些未读、未被认领、未答复、未交还的消息回到 Eval,如 [002 授权](002-授权.md) 所述。已读而未处理的,例如感谢,不会。
|
||
|
||
仅为上下文关注的 Threads 仍会投递消息,但不让 Ticket 拥有人类跟进的所有权,也不恢复 ended Tickets。那些消息可以被独立评估。
|
||
|
||
Sources 在一个活着的 Ticket 订阅期间轮询被订阅的 Thread,并在最后一个 Trigger 与订阅的 Tickets 结束两者中较晚者之后继续轮询两周。此后消息只经普通准入到达:GitHub notifications 或 Feishu 实时事件。
|
||
|
||
## 失败与恢复
|
||
|
||
### Executor 受阻或失败
|
||
|
||
当一个 Executor Session 上报 `blocked` 或 `failed`,或启动失败、出错(后者记录为 `failed`)时,Ticket 停止接受自动的 Input。
|
||
|
||
一次会过去的模型 provider 或网络失败——HTTP 408、429、5xx,或被拒、被重置、超时的连接——先重试:共三次、退避递增;一次启动会重新运行,一个回合失败的 worker 会被告知继续;一条失败的 prepare command 不再重试。
|
||
|
||
一旦失败,CI 与 bot 消息会记录在它身上,但不投递、也不唤醒 Session。
|
||
|
||
它的工具保持可用,所以人可以直接在 Session 里继续。
|
||
|
||
一次失败的启动不会自行重试。
|
||
|
||
一条人类消息、一次此后既非 `blocked` 也非 `failed` 的 Report、或一次重试,会解除这个扣留。
|
||
|
||
它是从存储的 Reports 与 Inputs 推算出来的,所以重启后仍在。
|
||
### 重试
|
||
|
||
当 open Ticket 处于 `person / delivery`、`person / blocked` 或 `person / failed` 时可用:一个新的 Assignment——重启它的 worker、拿走那些等待中的 Inputs——分别是它们的补救。
|
||
|
||
一旦一条人类消息到达过 blocked 或 failed 的 Ticket,消息就已在 Executor 手里,于是 Ticket 显示为 working——failed 启动重新进行时显示为 queued——并不再提供重试,除非那条消息的投递失败了。
|
||
|
||
一次重试结束当前的 Assignment,并在同一个 Executor Session、目录、分支与 pull request 里开始一个新的。
|
||
|
||
Executor 模型只在创建 Session 时生效;已有的 Session 保持它的模型。
|
||
### 重启
|
||
|
||
运行中的 Assignments 以 "host restarted" 结束,新的带着一条恢复备注启动,要求 Executor 在重复任何对外动作之前先检查它的目录、git 历史与 GitHub,最老的 Ticket 优先。
|
||
|
||
处于 waiting、ready、blocked 或 failed 的 Assignments 保持原样——除非那份 Report 之后有 Input 到达过它们,那种情况下它们也会重新启动。
|
||
|
||
Sessions、目录、Subscriptions、待处理的消息、以及下文的 stored work 都挺过重启;"每条消息被哪个 Executor Session 纳入了历史"的记录同样挺过重启——一个步骤在它认领消息之后、发起模型调用之前写这条记录:重启打断的那个回合纳入的消息,在随后出现一次 Report 或回合结束时算作已读;被一个步骤认领、但在写入历史之前被取消或错误打断的,算作未读。
|
||
|
||
一次重试或重启,会把曾投递给被替换的那个 Assignment、而其 Executor Session 未纳入的消息——取消或被中断的步骤可能丢掉的那些——重新交给新的 Assignment;一份仍在 Session 队列里等待、被重启保留、或跨崩溃持久化的副本,会在新 Assignment 的第一条消息之前被取走,让新的投递是唯一的一份。
|
||
### Stored work
|
||
|
||
dsh-superagent 在一个决定之后做的事——把一个候选、一条跟进或一份报告转交给 main Session,启动一个 Assignment,投递一个 Input——都先存储、后执行。
|
||
|
||
一次失败的转交或投递——例如它的 Session 联系不上——在 2 秒后重试,之后延迟逐次翻倍、封顶一分钟;一次失败的启动会失败它的 Ticket,除非那是一次会过去的失败,如上文。
|
||
|
||
每一种 work 只有在同一条目连续失败三次之后,才在 overview 的系统问题里报一个错;失败的 Input 投递改为显示在它的 Ticket 上。
|
||
|
||
向 main Session 的转交保持顺序,通知同样;一条失败会压住它后面同类的条目。
|
||
### Source 失败
|
||
|
||
GitHub Source 逐个读取每个 Issue、pull request 与 Thread:一次失败的读取把它的错误留在它的 Thread 上,下一次轮询重读,并计入 Source 的错误——默认连续三次轮询失败后才上报;任何一次成功的轮询会重置计数。
|
||
|
||
GitHub 在 agent 失去对一个私有仓库的访问权时也回答 404,所以 404 会被重试。
|
||
|
||
一个被通知的 Issue 或 pull request,若读取持续失败,会在首次失败读取的一天后被放弃;GitHub 报告它已删除时则立刻放弃。此后它不再让轮询失败,它的 Thread 保留着错误,Source 的状态会点名它。
|
||
|
||
它的读取位置停在原地,所以后来的通知或对它 Thread 的轮询会补上失败读取错过的内容。
|
||
|
||
Feishu 事件消费者报告每一次失败;对订阅话题的轮询把每次失败留在它的 Thread 上。
|
||
|
||
GitHub 列表或 Feishu 分页里的一个畸形条目会被跳过,Source 的状态为它计数。
|
||
### Channel 失败
|
||
|
||
对外消息作为 stored work 存储并发送,从 5 秒开始退避重试、逐次翻倍、封顶 300 秒。
|
||
|
||
一个 Ticket 发往同一个对话的消息按排队的先后发出,所以它的认领回执先于它后来的答复或结果:只要同一 Ticket 发往同一对话的更前一条还没发出,当前这条就等着——直到那一条连续失败三次、显示为一个问题。
|
||
|
||
其他 Tickets 的消息、以及不属于任何 Ticket 的消息,不为它等待。
|
||
|
||
一条有失败尝试的消息,在发出或被撤回之前,是一个系统问题。
|
||
|
||
一条消息只有在它的 Source 不再监听那个对话时、或 Source 回答那个对话永远收不到它时才被丢弃;它的 Source 关闭期间,它等着。
|
||
|
||
GitHub 对已删除的 Issue 或被锁定的对话给出这样的回答;Feishu 对被撤回或已不存在的消息给出这样的回答。
|
||
|
||
对一条已不存在的 review comment 的答复,改发到那个 pull request 的对话里。
|
||
|
||
一条被丢弃的消息保留它的记录——丢弃的时间、原因、以及它本属于哪个 Ticket——因此它绝不会被重新排队;一次被丢弃的认领回执读作 dropped,而非 sent。
|
||
### 暂停处理
|
||
|
||
停止派发、新的 Evals、Eval 超时、Input 投递与答复;已在进行中的启动会等到处理恢复。
|
||
|
||
Sources 继续记录 Triggers,正在运行的 Executor 回合不被打断。
|
||
|
||
发往 ended Ticket 的人类消息会等待:暂停解除后的第一个回合若该 Ticket 的 Mandate 按当时的 Policy 仍然成立,就恢复它。
|
||
|
||
一次进行中的 Eval 在暂停期间不过期;它的截止时间按暂停的时长顺延。
|
||
### 无效配置
|
||
|
||
一份被其 kind 拒绝的、已存储的 Source 配置,会禁用那个 Source 实例。
|