返回文章

Build Notes

主动式仓库 Agent 的代码解剖:dsh-superagent 架构解析

以 15.6k 行源码为样本,拆解 dsh-superagent 的 Trigger→Eval→Mandate→Ticket→Assignment 链路、三层授权、持久 effect 队列与 worker 执行语义;给出可复用的设计与边界。

dsh-superagent 是 DeepSeek 团队公开的研究原型:一个只服务单个 GitHub 仓库的主动式编码 agent,以 DeepSeek Harness 的 bundle 形式发布。coding agent 本身不稀缺,它值得读的地方是把“谁可以让 agent 干活、能干什么、出了事怎么恢复”写成了代码。

这份报告基于公开提交 8c32a9d(2026-10-31)逐文件阅读。src 约 15,587 行,包含 60 个测试文件。文中判断都指向具体文件与函数,不引用尚未实现的论文设想。

先给结论:它的新意在治理和编排,包括 Gate + Mandate 的授权模型、follow-up 归属判定、observe 影子决策、事务性 effect 队列,以及 Ticket 不跟随 Session 的状态设计。模型能力、沙箱和多仓库协作不是它的强项。

阅读顺序:运行形态 → 领域模型 → 任务流转 → 授权 → 可靠性 → worker 执行 → Source → 模型接口 → 创新与边界。关键节点给出可复核的代码位置。

代码解析 · dsh-superagent · 提交 8c32a9d · 2026-11-04

主动式 agent 真正难的是授权、边界和恢复:谁可以让它写,写到哪,失败后怎么接着写。能不能写代码反而排在后面。这份报告按这四件事拆 dsh-superagent。

源码规模15,587src 行数
测试文件60约 800 个用例
持久 effect6事务后执行
工具面14 + 6主 Session + worker
01

形态:跑在 DSH 里的插件 bundle

dsh-superagent 没有自己的模型、会话或工具运行时。它把所有 @deepseek-ai/dsh-* 声明为 peerDependencies,自己只提供编排逻辑。

入口注册方式职责
主插件src/index.ts:10 的 apply()构造 Runtime,注册 Cordis 服务 orchestrator
GitHub Sourcesrc/sources/github.ts:112注册 kind,启动通知与仓库轮询
Feishu Sourcesrc/sources/feishu.ts:132注册 kind,启动事件、历史补收与话题轮询
面板DSH client bundle只走 /orchestrator/api/*,不直接 import host

cordis.patch.yml 还定义了两个 agent preset:worker 用 dsh-superagent-worker,主 Session 用 dsh-superagent-router。前者允许文件、搜索、shell、web、skill、todo 和 subagent;后者去掉写文件、web 和子代理,只保留路由所需的最小工具面。

这意味着它不能单独用 node index.js 跑起来。读它之前要先接受 DSH 的会话、工具、权限和工作区模型。

02

分层:三条依赖规则

目录分成 core、app、executor、sources、infra、client 六层,但真正重要的是三条规则,而不是目录本身。

层放什么依赖约束
core领域类型、Engine、五个 part、端口不 import 层外代码,不做 I/O
app组合根、Store、Settings、Prompts、Sessions、Effects、Outbox、HTTP、工具可以依赖 core 与 infra
executorDshExecutor、槽位、DSH 事件处理通过窄接口回调 Engine
sourcesSource 协议、GitHub、Feishu、通道与读取器不 import src/app
clientReact 面板、路由、文案只拿 host 侧类型与纯值
  • Trigger、Thread、Decision、Ticket、Assignment 只由 Engine 写;Source 只写自己的游标和 Thread meta。
  • Engine 只把 EngineForSources、EngineForMainSession、EngineForPeople、EngineForSettings、EngineForExecutor 这些 Pick 交给调用方,越权方法在类型层不存在。
  • core 无 I/O,sources 不反向依赖 app,所以领域规则可以用内存仓储完整测试(src/core/memory.ts)。

依赖规则写在 docs/007-architecture.md:255-274,角色接口在 src/core/engine.ts:261-270,端口在 src/core/ports.ts。

03

领域模型:五个聚合,一条主链

整个系统只有一条主链:Trigger 进入 Thread,Thread 上的批次进入 Eval,Eval 产出 Decision,Decision 的 accept 产出 Mandate 与 Ticket,Ticket 再绑定 Assignment 交给 Executor。

  1. 01Trigger不可变事件,幂等 id,threads[0] 是主 Thread
  2. 02Thread时间线 + 订阅集合;同一 Thread 同时最多一个 Eval
  3. 03Eval一个批次的判断;记录 strength、basis、deadline、primary
  4. 04Decisionaccept / ignore / stop / unavailable,附理由与审计
  5. 05Mandate不可变授权:strength、policyVersion、basis、actor、scope
  6. 06Ticket工作单元;持有 Mandate、Channels、Inputs、Assignments、follows
  7. 07AssignmentTicket 与一次 Executor 的绑定;pending → running → ended
  8. 08Report / Replyworker 的进度与结果;主 Session 按规则转成人可见消息
概念代码位置不变量
Triggersrc/core/model.ts:22-39同一条 Trigger 只能建一个 Ticket
Threadsrc/core/model.ts:157-182一个 Thread 同时最多一个 Eval
Evaluationsrc/core/model.ts:136-154候选卡送达后才开始计时
Mandatesrc/core/model.ts:126-133创建后不可变;只有 stop / 重校验失败能撤销
Ticketsrc/core/model.ts:277-308open → ended,可被人的消息 resume
Assignmentsrc/core/model.ts:218-233只有最新一个能是 pending/running

三个共享谓词把规则写在一处:followupOwners 决定一条人的消息归哪个 Ticket;waitsForPerson 决定是否暂停自动输入;ticketStage 决定面板上的阶段。它们让 Engine 的五个 part 不必各写一套判断。

04

任务流转:从一条消息到一次交付

任务在每个阶段换一次载体,也换一次决策者。后面所有模块都在实现这张表。

阶段载体谁决定产物
入站TriggerSource 的 scope()入库或 filtered
评估CandidateGate + 主 SessionDecision
授权DecisionEngine 按 Policy 复核Mandate + Ticket
执行AssignmentWorker SessionReport / PR
交付Report / Reply主 Session人可见消息
结束ticket.endedSource 事实 / worker / 人outcome
EXPLICIT

名单内的人 @ 了 agent

强度直接是 explicit;主 Session 用 orchestrator_route 在一步内 accept、attach、stop 或 ignore,不需要完整评估。

INFERRED

未被直接点名、但可能沾规则

先由 Gate 判为 inferred;主 Session 调 orchestrator_eval 载入有界线程窗口,必要时 eval_read,最后 eval_decide 提交方向、规则评估与证据。

  • Eval 的截止时间从候选卡进入主 Session 开始算;没送达的 Eval 永不过期。
  • 过期后最多重排两次,第三次记 unavailable;暂停处理会把截止时间整体后移。
  • 已经建过票的 Trigger 不会再次建票;重复的 accept 只返回原来的 Ticket。
  • Ticket 结束后,人再在它的 Thread 上说话,会用同一 Mandate 直接 resume,不重新评估。

这条链路分别在 src/core/intake.ts:19-62、src/core/evals.ts:71-156、src/core/tickets.ts:37-56,归属判定在 src/core/model.ts:75-88。

05

授权:模型提议,宿主复核

这是整个项目最重的部分。授权靠的是 Gate、准入表、Mandate 三层确定性代码,不是写在 prompt 里的“请遵守规则”。模型只能在这个边界内提议。

判定顺序结果
非人:有 actor 且在 Source 的 bot 名单最多 inferred
人:在 allowed 名单且 addressedexplicit
人:按接收 scope 查准入表 cellexplicit / inferred / ignore
落在已有 Ticket 的 Thread 上follow-up:名单内 explicit,名单外 inferred
准入 cell可选值默认
在名单、未 @inferred / ignoreinferred
不在名单、@ 了explicit / inferred / ignoreignore
不在名单、未 @inferred / ignoreignore

三种 accept mode 决定 inferred 候选的命运:off 不产生候选;observe 照常评估,但只记 would_accept 审计;auto 才真正建票。默认是 observe,也就是先跑影子决策。

Mandate 记录 basis:来源、接收 scope、row、sender、匹配规则、via(addressed / followup / admission / bot)。恢复已结束 Ticket 前必须 revalidate:Source 还要监听、sender 还在名单、admission cell 强度不降、inferred 还需要 auto 且规则仍启用。

一条底线:外部文本、引用和仓库文件都是不可信数据。它们可以影响“这条消息想干什么”,但不能改变“谁可以授权”和“Policy 是什么”。模型判断之后,宿主还要按同一份 Policy 再查一次。

授权代码在 src/policy.ts:189-240 的 gate 与 revalidate,src/policy.ts:40-61 的 assessmentOutcome,src/policy.ts:143-165 的 policyEffects。

06

运行时:2 秒时钟与持久副作用

Runtime 是组合根,不含领域规则。它按依赖顺序手工构造 Store、Setup、Prompts、Effects、SourceRegistry、Sessions、Settings、DshExecutor、MasterAssessor、Outbox、Engine 和 Passes;因为 Executor 和 Assessor 要回调 Engine,构造时用 getter 而不是直接传对象。

机制行为代码
Passes每 2 秒一次;同一时刻只跑一个 pass,期间的请求合并成下一次src/app/passes.ts:28-61
Effects事务内 enqueue,提交后 dispatch;同 kind/key 合并,保留队列位置src/app/effects.ts:61-66
Store单个 SQLite;generated column + partial index 让 pass 只扫活跃行src/app/store.ts:26-40
Outbox按 Ticket + 会话保序;前面一条连续失败三次才放行src/app/outbox.ts:39-51
effect处理器并发重试
assessmentMasterAssessor42s → 60s
launchDshExecutor空槽数5s → 300s
deliveryDshExecutor42s → 60s
relaySessions1,保序2s → 60s
noticeSessions1,保序2s → 60s
replyOutbox15s → 300s

Later 是这套队列的关键:它表示“现在不能跑”,比如暂停、槽满、Source 未安装。它不计失败,只按初始延迟重排,所以调度器不会把正常的背压当成故障。

副作用先落库、提交后执行,意味着一次崩溃最多留下“待执行”的行,而不是“已经做了但没记账”的状态。这是整个系统能恢复的基础。

07

执行:一个 Ticket,一个 Session,一个目录

Worker 的代码不在这个仓库里:它运行在 DSH 的 Session 中。宿主负责起 Session、投消息、收报告;调查、实现、测试和提 PR 由 worker 模型完成。

  1. 01accept 时算执行元数据短名、目录、assignment 文本;只建 pending Assignment,不建目录
  2. 02launch effect重新校验 Ticket/Assignment → 占槽 → 准备目录(10 分钟上限)
  3. 03建/恢复 Sessionworker-<ticketId>,worker preset,role model,prompt/skills/tools,danger-full-access
  4. 04首条 assignmentassignment ≤1500 字 + 原始 Trigger + 同批 Trigger;之后只发增量
  5. 05worker turn调查、实现、跑检查;长命令转后台;每条 Input 在下一个 step 可见
  6. 06reportprogress / waiting / ready / blocked / failed / dropped;问题单独走 attention
  7. 07结束或继续ready/PR 关闭/dropped/stop 结束;人消息可以 resume
资源规则
worker 槽maxWorkers 默认 3;人消息/状态请求可多占 1 个;唤醒优先于新 launch
后台工作turn 结束后占槽最多 30 分钟;超时杀 shell job,subagent 继续
handoffready / attention / blocked / failed 交给人时立刻释放槽
瞬时错误最多 3 次:launch 按 effect 退避,worker turn 15/30/60 秒续跑
ReportTicket 变化
progress只更新摘要,不打断工作
waiting进入 idle,等下一个 Trigger/Input
ready无 follows → completed;有 PR → 等关闭后按 merged/closed 结束
blocked / failed进入 person 阶段,自动输入停投
dropped结束;若跟随的 PR 已关闭,按关闭结果

执行代码在 src/executor/dsh.ts:97-111, 247-285, 424-470;槽位在 dsh.ts:165-199;后台与 handoff 在 dsh.ts:588-672。

08

Source:把外部世界翻译成 Trigger

Source 是可插拔的事件适配器。一个 kind 声明配置 schema、面板字段、监听 scope、通道、线程读取器、Policy 部分、执行变量和健康条目;Registry 负责实例、健康条目独占和 Policy part 合并。

Source入站去重与游标失败处理
GitHub通知、全仓 Issue/PR/评论流、默认分支 CI、Ticket PR 的 CI、已订阅 Thread游标回退 5 秒 + 确定性 id;游标不动时 ETag/304单条读取失败留在 Thread;连续 3 次轮询失败报 Source 错误;410 删除,404 重试
Feishu长连接事件、历史补收、群名刷新、已订阅话题轮询事件与历史共用一套 Trigger 规则;话题按游标消费者每次失败都报;话题轮询把错误留在 Thread

已订阅 Thread 在“有活 Ticket,或最后活动/结束在 14 天内”继续轮询(POLL_HORIZON_MS)。超过窗口后只靠普通通知或实时事件,不再主动补历史。

Source 决定推送;阅读仍由 worker 自己完成。worker 可以用 gh / lark-cli 读订阅之外的上下文;订阅只决定哪些消息会主动送到 Ticket。

协议在 src/sources/kind.ts,pollRound 在 src/sources/github.ts:280-296,14 天窗口在 src/sources/poll.ts:6-15。

09

模型接口:14 + 6 个工具,和一条注意力预算

主 Session 和 worker 各有一套工具和 prompt。宿主用工具面和系统提示词把“路由”和“实现”分开,避免主 Session 变成第二个工程师。

角色工具关键约束
主 Sessionorchestrator_read / route / eval / eval_read / eval_decide / reply / ticket_retry / request_status / note / trigger_requeue / thread_unsubscribe / settings_save / policy_sync / prompts_save不读代码、不查日志、不替 worker 解决 blocker;shell 等待被 noWait 限制
Workerorchestrator_read / ack / report / thread_unrelated / thread_follow / trigger_handback 加文件、搜索、shell/jobs、web、skill、todo、subagent不直接发 IM;Ticket 结束后工具 guard 拒绝调用

评估上下文是刻意有界的:Trigger 最多 4,000 字;窗口是首 5 条加 Trigger 两侧各 5 条,近邻 1,200 字、其余 400 字、未展开消息 50 字 stub;要看全文必须用 orchestrator_eval_read 按 id 展开。

Prompt 组装顺序固定:shared → 角色 → 团队三段(Communication / Ticket scope / Delivery requirements)→ subscriptions → replies → dispatch 或 work → admission → handoff → 执行环境 → Policy。Policy 永远在最后,团队只能覆盖内置段落,不能改变生成段。

工具在 src/app/tools/main.ts 与 executor.ts;评估上下文在 src/app/assessment.ts:36-37,183-220;prompt 顺序在 src/app/prompts.ts:102-118;漂移检查在 tests/model-surface.spec.ts。

10

创新点:哪些值得抄

它的零件大多来自成熟领域:能力授权、工作流引擎、事务性 outbox、事件溯源。真正少见的是组合方式,以及在 agent 场景里对边界的坚持。

  1. Gate + Mandate + 重校验:把“谁能让 agent 干活”从 prompt 里拿出来,变成可版本化、可审计、可撤销的能力凭证。模型判断之后宿主再复核。
  2. follow-up 归属判定:一条人消息落在已有 Ticket 的 Thread 上,归该 Ticket 的 worker;主 Session 只能 stop;worker 可以 handback 一次。这解决了多会话 agent 最容易出现的重复派活和抢消息。
  3. observe 影子决策:inferred 候选照常评估,但结论只记 would_accept,不建票。先用真实流量校准规则,再切 auto。这在 agent 产品里很少见,在运维里很常见。
  4. 事务性 effect 队列:评估、launch、投递、转发、通知、回复全部先写 SQLite 再执行,每类有独立并发、退避和保序。它让 agent 编排获得接近工作流引擎的可靠性,但实现只有一个数据库文件。
  5. Ticket 不跟随 Session:Ticket 可以换 Assignment、在 Session 退出后继续 idle、被人的消息用同一 Mandate 唤醒;执行实例和授权对象是分开的。这让恢复语义可以写清楚,而不是靠“会话还在不在”。

如果只抄三样:第一,Gate + Mandate + 重校验;第二,follow-up 归属与 handback;第三,observe + would_accept。其余部分值得学,但属于“照着做就能得到”的工程。

11

边界:它明确不解决什么

README 自己写明这是研究原型,不是生产软件;论文里有些设计并未在这个版本实现。把边界说清楚,比把它当成平台更重要。

边界表现影响
不是安全沙箱worker 以 operator 用户运行、danger-full-access、shim 可按绝对路径绕过隔离是默认路径护栏,不是权限边界
单机单库一个 SQLite 文件、目录锁、单进程没有水平扩展、多仓库、多团队隔离
Source 抽象未完成Outbox、links、resources、PR 通知、assessor 读仓库仍直连 GitHub/Feishu新增 Source 仍要改少量通用模块
数据无界增长trigger、decision、ended ticket、outbox、prompt 历史都不清理长期运行需要自己加归档或清理
判断仍靠模型方向是否已定、规则是否匹配,本质是语义判断schema 和证据字段只能降低方差,不能证明判断正确

还有一个常被忽略的代价:主 Session 被刻意限制成“不读代码”。这让路由更干净,但当评估需要判断技术可行性时,它只能依赖线程上下文和有限的仓库读取,误判会转移到 worker 的 handback 或 dropped。

12

代码索引:从哪读起

如果只有一小时,按下面顺序读;每个文件都先用 JSDoc 看职责,再跳到核心方法。

  • model.ts领域类型与不变量;先读 Trigger、Mandate、Ticket、Assignment。
  • engine.ts唯一写入口;看角色接口和每个命令的 JSDoc。
  • evals.ts批次、gate、超时、Decision;理解 accept 如何建票。
  • tickets.ts生命周期、订阅、Inputs、resume/stop/retry;最密的一块。
  • dsh.ts真正把 Ticket 变成 Session 的地方;launch、delivery、槽位、恢复。
  • effects.ts持久副作用队列;理解 Later 与背压。
  • policy.tsgate、准入表、Mandate 重校验、policyEffects。
  • sources/kind.tsSource 插件协议;先看字段和可选能力。
  • tools/main.ts主 Session 的 14 个工具;对照 prompts/dispatch.md 读。
  • prompts/模型实际读到的指令;先读 shared、orchestrator、dispatch、work、admission。

测试也是文档:tests/core-engine.spec.ts 覆盖领域规则,tests/runtime.spec.ts 在模拟 DSH 宿主上跑整套应用,tests/model-surface.spec.ts 检查模型可见面的漂移。

13

结论:把它当参考架构,不要当平台

dsh-superagent 最值得读的地方,是它把“主动”拆成了可审计的步骤:谁触发、谁授权、谁执行、谁决定结束。它让模型的判断被一层确定性代码包住,而不是去追求更聪明的模型。

它不是可以直接照搬的生产平台:没有真实沙箱,没有多租户,数据也不清理。但作为一份参考架构,它把主动式 agent 最难讲清楚的四件事(授权、归属、恢复、注意力预算)落到了具体文件和函数上。

可复用清单

  • 把授权做成 Mandate,而不是 prompt 里的一句话。
  • 给每条人消息定义唯一的归属和一次 handback。
  • 先跑 observe 影子决策,再放开 auto。
  • 所有外部动作先落库,再执行;恢复从表里读。
  • 让 Ticket 拥有状态,Session 只是执行实例。