← 返回主页
dsh-superagent 文档

003 执行

003 Execution
跟随一个被接受的 Ticket 走完执行、交付与恢复。 GitHub 原文 ↗ 社区中文翻译 · 以原文为准

本文档跟随一个已被接受的 Ticket(工单)走完执行、交付与恢复。它建立在 001 领域模型与 002 授权之上。

Ticket 阶段

每个开放的 Ticket 同一时刻只有一个阶段,由它正在等待什么决定。下表自上而下读,第一条命中的行生效。

阶段条件面板标签
person / delivery向 Executor Session 送达一个 Input(输入)失败;2 秒后重试,随后按倍增延迟重试(上限一分钟);送达成功、出现新的 Assignment(派工)或 Ticket 结束会清除该状态Input undelivered
person / answer它有一个 Attention(待人工决定)Awaiting answer
person / review最新的 Report(报告)为 readyAwaiting review
person / blocked、failed最新的 Report 为 blocked 或 failed,且此后没有人的消息到达该 TicketBlocked · Failed
working / queued没有 Assignment,或只有 pending 的 AssignmentQueued
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 Session 以操作者的 OS 用户身份、无需审批地运行命令。它们的 dsh-superagent-worker 预置包含工作所需的工具,并在 5 秒后把前台 shell 命令转为后台任务。这使 worker 能在下一步接收新消息,而不必等待一条长命令跑完。

主 Session 使用 dsh-superagent-router,有同样的 5 秒 shell 与任务等待上限,但用于路由的工具集更小。Session 保留创建时选定的预置。Worker 子 agent 使用 DeepSeek Harness 的 standard 预置。

后台工作与 worker 槽位

当一个回合结束时仍有 shell 任务或子 agent 在运行,Ticket 保留其 worker 槽位并保持 working。它不会被报告为等待,主 Session 也收不到等待报告。完成的任务或子 agent 会在同一槽位用其结果唤醒 Executor。一旦某个回合结束时没有后台工作,Executor 照常报告等待。

每个回合结束后,后台工作至多有 30 分钟完成。到时限后,shell 任务被杀死,Ticket 被报告为等待。报告包含 Executor 的最后输出、哪些任务被杀死、哪些子 agent 仍在活动。子 agent 被允许继续。Executor 在下一个回合读取被杀任务的结果。

向人移交会立即释放槽位,并保留当前生效的 Report。这适用于 ready Report 或问题之后(除非此后又到达了新的 Input),以及任何 blocked 或 failed Report 之后。

移交时子 agent 停止。shell 任务——例如一个供人评审的服务器——可以继续运行直到 Assignment 结束。它们不占用槽位,也不受 30 分钟时限约束,后续回合结束后同样如此。它们的完成不唤醒 Executor;Executor 在下一个回合读取结果。

人也可以通过直接在 Executor Session 里写消息来发起一个回合。其中的后台工作遵循同样的规则。

Assignment 结束时,它的 shell 任务被杀死且不唤醒 Session。它的子 agent 被释放,它们唤醒的任何完成回合都被丢弃。替换的 Assignment 及其等待中的 Input 在该关闭流程结束后才开始。

在 DeepSeek Harness 进程内停止或重启插件走同一条关闭路径。尚未被记入 Session 历史的 Input 会被保留。关闭时最多等待每个 worker 的后台工作 10 秒;进程重启则直接终结这些工作。忙碌中被中断的 Assignment 会带着一条恢复备注重启。

Worker 子 agent(含嵌套的)使用 DeepSeek Harness 的 standard 预置并继承 worker 的权限。它们只保留 worker 允许的工作工具,Ticket 结束时失去这些工具。在 standard 下,shell 命令 60 秒后转入后台。子 agent 结束时它的任务被杀死。

准备 Ticket 目录

第一次 Assignment 之前,如果 Ticket 目录不存在,prepare 命令会在执行根目录下用 sh 运行一次。它有十分钟完成。没有 prepare 命令时,worker 在空目录中启动。

命令失败则 Ticket 失败,并记录其输出。重试、或人在 Ticket 的某个 Thread 上的消息会再次尝试准备。典型命令用 git worktree 创建一个检出。当分支路径的最后一段与 Ticket 短名一致时,该 pull request 归属此 Ticket。

并发

Ticket 从一次启动(包括准备阶段)开始、或从唤醒其空闲 Executor 的那次送达开始持有 worker 槽位。在活跃回合期间它也持有槽位。

配置的并发上限作用于这些槽位,并为人的消息或状态请求额外保留一个槽位。这种唤醒可以在普通槽位满时使用额外槽位,并在新启动之前优先取得下一个空闲槽位。因此新工作不会阻止人继续一个既有 Ticket。

其他 Input(例如 CI 结果)等待普通槽位。一次启动在 working / queued 中等待直到槽位空闲。多个启动并发运行,因此慢的 prepare 命令只拖慢它自己的 Ticket。

执行变量

prepare 命令与 Executor 指示接收同一组执行变量。命令读取环境变量;Executor 在其 prompt 中读取它们的值与描述。任何变量都不包含凭据。

变量注册者值
DSH_SUPERAGENT_DIR、DSH_SUPERAGENT_NAME、DSH_SUPERAGENT_TICKETExecutorTicket 的目录、短名与 id
DSH_SUPERAGENT_BLOCKED_COMMANDSExecutorExecutor 不得运行的命令
DSH_SUPERAGENT_GITHUB_REPOSITORYGitHub该 Source 监听的仓库,owner/name
DSH_SUPERAGENT_GITHUB_ACCOUNT、DSH_SUPERAGENT_GITHUB_COMMIT_EMAILGitHubagent 账号及其 commit 邮箱
DSH_SUPERAGENT_GITHUB_NUMBER、DSH_SUPERAGENT_GITHUB_PR_HEADGitHub发起该 Ticket 的 Issue 或 pull request;当它是 pull request 时,还有其 head ref
DSH_SUPERAGENT_FEISHU_BOT、DSH_SUPERAGENT_FEISHU_CHAT飞书bot,以及发起该 Ticket 的会话

特定于 Ticket 的值来自其目录与发起 Trigger。其他值来自当前部署设置。只有面板能更改执行环境设置;发给主 Session 的文本不能更改配置的 shell 命令。

Worker 通过 agent 账号的 gh 访问 GitHub,通过 bot 的 lark-cli 访问飞书。被封禁的命令按名称调用时会失败。

这些包装器与目录建立的是默认约束,不是安全边界。Worker 仍然可以通过绝对路径执行程序。

合并

自动合并由交付要求(Delivery requirements)prompt 治理。宿主没有合并专用的决策逻辑。

默认情况下,Executor 从不以 agent 账号开启自动合并:GitHub 会把合并归因于开启它的那个账号。交付要求必须指定另一个账号的 CLI 用于合并。没有指定时,由人来合并 pull request。

默认指示只允许对为该 Ticket 开启的 pull request 自动合并,条件是:它已 ready、必需的批准覆盖 head commit、且增删行数合计少于 200 行。推送另一个修订版本之前,Executor 会先撤回自动合并。

没有正文的 review(例如批准)也会作为 Trigger 到达,可以唤醒等待 review 的 Executor。这些指示约束的是 agent,而不是 GitHub 的合并规则。要强制要求批准,请在分支保护中要求批准并消除过期批准。省略合并 CLI、或删除自动合并段落,即可要求手动合并。

Ticket 如何结束

  • ready 而没有 pull request:ready Report 将 Ticket 结束为已完成。
  • 无事可做:Executor 发现没有需要人处理的工作时(例如请求无法复现、或已被取代),报告 dropped。Ticket 无消息结束(除非匹配的规则要求一条消息),人之后在其 Thread 上的消息可以恢复它。

Pull request

Executor 报告过的、Ticket 起始的那个 pull request 会被 Ticket 一直跟随。从其分支开启的每个 pull request,一经 Executor 报告或被 GitHub Source 发现,也会被跟随。被重新打开的 pull request 会再次被跟随。

当它开启的 pull request 全部不再开放时,Ticket 完成。如果它一个也没开启,则它跟随的所有 pull request 都关闭后完成。此时它以最后一次相关关闭的结果结束,并保留其 Subscription(订阅),允许之后的人的评论恢复它。

当恢复、重试或恢复中的工作尚未完成时,一次关闭不会结束 Ticket。如果 Executor 确认或回答了它拥有的某条 follow-up(后续消息)、但此后还没有在读到该消息后做出非 progress 的 Report 或结束一个回合,Ticket 也会等待。之后的 ready Report 会在满足同一 pull request 完成规则后结束 Ticket。

聊天 Channel(渠道)会收到一条关于 pull request 结果的最终通知。GitHub 本身已在 pull request 上显示结果。删除 Ticket 对某个 pull request 的 Subscription 也会停止跟随它。

因此「Awaiting review」的含义是:带 pull request 的 ready Report。

后续消息与上下文 Thread

由 Executor 决定其 Ticket 自己 Thread 上的人的后续消息。它处理范围内的请求,把超出范围或属于其他 Ticket 的请求交还。一旦不再有 Executor 拥有该消息,它就成为主 Session 的候选。除此之外,主 Session 对 follow-up 只能应人的要求停止工作。

当一个 Input 被 Session 记入某个步骤、并随后做出 Report 或结束回合时,视为已读。Ticket 结束时,未被确认、回答或交还的未读消息按 002 授权所述回到 Eval;已读但无需行动的消息(例如感谢)不会。

仅为上下文而跟随的 Thread 仍会送达消息,但不赋予 Ticket 对人后续消息的所有权,也不恢复已结束的 Ticket。这些消息可以被独立研判。

只要还有活跃 Ticket 在订阅,Source 就会轮询已订阅的 Thread,并持续到最后一个 Trigger 与各订阅 Ticket 结束两者中较晚者之后的两个星期。此后,消息只经普通 intake 到达:GitHub 通知或飞书实时事件。

失败与恢复

Executor 受阻或失败

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

模型供应商或网络的临时性故障——HTTP 408、429、5xx,或连接被拒绝、重置、超时——会先重试三次,退避逐渐增长:启动重新运行;回合失败的 worker 会被告知继续;失败的 prepare 命令不再重试。

失败之后,CI 与 bot 的消息会被记录在它上面,但不会送达,也不会唤醒 Session。

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

失败的启动不会自行重试。

人的消息、之后一条既非 blocked 也非 failed 的 Report、或一次重试,会解除该保持状态。

该状态由已存储的 Report 与 Input 推导得出,因此重启会保留它。

重试

当开放的 Ticket 处于 person / delivery、person / blocked 或 person / failed 时可用:一个新的 Assignment——它会重新启动 worker 并取走等待中的 Input——是这三种状态的共同解法。

人的消息一旦到达 blocked 或 failed 的 Ticket,Executor 就拥有了该消息,于是 Ticket 显示为 working(或失败启动重新开始时显示为 queued),并不再提供重试,除非该消息的送达失败了。

重试会结束当前 Assignment,并在同一 Executor Session、目录、分支与 pull request 中启动一个新的 Assignment。

Executor 模型只在创建 Session 时生效;既有 Session 保留其模型。

重启

运行中的 Assignment 以「host restarted」结束,新的 Assignment 带着一条恢复备注启动,该备注要求 Executor 在重复任何对外动作之前,先检查其目录、git 历史与 GitHub;最早的 Ticket 优先。

处于等待、ready、blocked 或 failed 的 Assignment 保持原状,除非在该 Report 之后有 Input 到达——那种情况下它们也会重新启动。

Session、目录、Subscription、待发消息与下文所述的已存储工作在重启后存活;每个 Executor Session 把哪些消息记入了历史的记录也存活——一个步骤在认领消息之后、调用模型之前完成记录:被重启截断的回合所认领的消息,在随后有 Report 或回合结束时算作已读;被取消或出错截断在记入历史之前的步骤所认领的消息,算作未读。

重试或重启会把被替换 Assignment 已送达、但其 Executor Session 未取走的消息再次交给新 Assignment——取消或被截断的步骤可能丢弃了它们;仍留在 Session 中等待的副本(由重启保留、或跨崩溃持久化)会在新 Assignment 的第一条消息之前被取出,因此新的送达是唯一副本。

已存储工作

dsh-superagent 在做出决定之后所做的事——把候选、后续消息或报告转发给主 Session、启动 Assignment、送达 Input——都先存储、后执行。

失败的转发或送达(例如其 Session 无法到达)在 2 秒后重试,随后按倍增延迟重试(上限一分钟);失败的启动会使其 Ticket 失败,除非该失败是上述临时性失败。

每一类工作只有在同一条目连续失败三次后,才在总览的系统问题中报告错误;失败的 Input 送达显示在其 Ticket 上。

发往主 Session 的转发保持顺序,通知亦然;一条失败会挡住它同类的后续条目。

Source 失败

GitHub Source 独立读取每个 Issue、pull request 与 Thread:一次失败的读取把错误留在其 Thread 上,下次轮询时再读,并计入该 Source 的错误——默认连续三次失败轮询后报告;任何一次成功轮询都会清零计数。

当 agent 失去对私有仓库的访问时,GitHub 也会回答 404,因此 404 会被重试。

持续读取失败的被通知 Issue 或 pull request,在其第一次失败读取一天后被丢弃;GitHub 报告它已删除时立即丢弃。此后它不再使轮询失败,其 Thread 保留错误,Source 的状态会点名它。

它的读取位置保持不动,因此之后的通知或对其 Thread 的轮询会读到失败期间错过的内容。

飞书事件消费者会报告每一次失败;订阅话题的轮询把每次失败保留在其 Thread 上。

GitHub 列表或飞书分页中的畸形条目会被跳过,并计入 Source 的状态。

Channel 失败

外发消息作为已存储工作保存和发送,从 5 秒起按倍增退避重试,上限 300 秒。

一个 Ticket 发往同一会话的消息按排队顺序发出,因此它的确认消息先于其后的回答或结果:同一 Ticket 发往同一会话的较早消息尚未发出时,较晚消息等待,直到较早消息连续失败三次并显示为问题。

其他 Ticket 的消息、以及不属于任何 Ticket 的消息,不会为它等待。

有失败尝试的消息在发出或撤回之前算作系统问题。

只有当其 Source 不再监听该会话、或 Source 回答该会话永远无法接收它时,消息才被丢弃;其 Source 关闭期间,它等待。

对 GitHub 上已删除的 Issue 或已锁定的会话、对飞书上被撤回的消息或飞书已找不到的消息,Source 会给出这样的回答。

对已不存在的 review 评论的回复,会改发到该 pull request 的会话。

被丢弃的消息保留其记录——何时、为何被丢弃、属于哪个 Ticket——因此不会再排队;被丢弃的确认显示为 dropped,而非已发送。

暂停处理

暂停会停止:派发、新的 Eval、Eval 超时、Input 送达与回复;已在进行中的启动等待处理恢复。

Source 继续记录 Trigger;运行中的 Executor 回合不被打断。

人发给已结束 Ticket 的消息会等待:暂停后的第一遍处理会恢复该 Ticket——前提是它的 Mandate(授权)在当时 的 Policy 下仍然成立。

进行中的 Eval 在暂停期间不过期;其截止时刻顺延暂停的时长。

无效配置

被其种类拒绝的已存储 Source 配置会禁用该 Source 实例。