dsh-superagent 是 DeepSeek 团队开源的一个主动编码 Agent 原型(MIT,TypeScript,约 1.6 万 stars),打包成 DeepSeek Harness 的一个 bundle。它只干一件事:替团队盯住一个 GitHub 仓库(可选接飞书),有人 @ 它提需求时,它开一个工作项、在自己的会话和目录里把活干完,再回到原对话里回复,比如提交一个 PR。README 开篇就强调这是「研究原型、不是官方产品」,但它的架构——Router 与 Worker 两个会话分工、Eval 两级过滤、存算分离、以及一堆自愈机制——是这套系统里最值得拆的部分。

关键判断

这套架构的核心判断只有一句:让 Agent 尽量少占用人的注意力,同时尽量多产出价值。一个能替全队干活的 Agent,同样能浪费全队的注意力,所以它只在被要求、或团队规则允许时才行动,只在必须有人介入时才开口。这个原则落成两个技术决定:一是把「判断接不接活」和「具体干活」拆成两个会话,各管各的;二是把「谁有权发起」用独立的授权层管起来,不让提示词越权。后者尤其重要——授权是代码强制执行的,不是靠提示词祈祷。

领域模型

先看它怎么给工作建模。一条工作从 Source 产生 Trigger 开始:Source 是事件流(GitHub 或飞书),Trigger 是不可变事件,Thread 是时间序的 Trigger,Eval 对一批 Trigger 做判断,通过后创建带 Mandate 的 Ticket,Assignment 把 Ticket 绑给一个 Executor,结果经 Channel 写回 Source。可以简化成下面这张关系:

type SourceKind = "github" | "feishu";

interface Trigger {
  id: string;
  actor: string;      // 谁发起的
  threadIds: string[];
}

interface Mandate {
  policyVersion: string;
  scope: string[];
  strength: "explicit" | "inferred";
}

interface Ticket {
  id: string;
  mandate: Mandate;   // 授权边界,不可变
  channels: string[];
  stage: "queued" | "working" | "review" | "idle";
}

这串概念里 Mandate 最值得注意:它是 Eval 通过时授予的授权边界,不可变,包含策略版本、范围、强度、命中的规则。Ticket 结束不代表 Mandate 结束,只有人的 stop、或策略不再覆盖时才撤销。也就是说,Agent 能走多远,不是它自己说了算,而是签发它的授权说了算。

Eval 两级过滤

接不接活的判断在 Eval 里做,分两层。第一层 gate 看「谁发起的」:允许名单里的人显式请求、或机器人列表里的 bot 产生的推断候选,才能进下一步,其他人直接挡掉;第二层 assessment 评估这个事件值不值得开新活。两层各自独立,gate 管准入、assessment 管价值。用代码表达大致是这样:

type Decision = "accept" | "stop" | "ignore" | "unavailable";

function evalTrigger(trigger: Trigger, policy: Policy): Decision {
  // 第一层 gate:先看谁发起的、能不能发起
  if (!policy.allowedActors.includes(trigger.actor)) {
    return "ignore";
  }
  // 第二层 assessment:再评估这个事件值不值得开新活
  const matched = policy.rules.filter((r) => r.matches(trigger));
  return matched.length > 0 ? "accept" : "ignore";
}

Router 与 Worker

过了 Eval,干活的部分又拆成两种会话。主会话用 dsh-superagent-router preset,工具集更小,只做路由和总控;每个 Ticket 拿一个 Executor 会话,命名 worker-,跑在独立目录里,工具集更大、能直接执行命令。Worker 的命令无需审批就以操作者系统用户身份执行,5 秒后把前台命令挪到后台,这样它能继续收新消息而不是干等一条长命令。这套「Router 只管调度、Worker 只干一件活」的分层,是它能并行处理多张 Ticket 又不会互相串味的原因。

存算分离

实现上它做了一层存算分离。Engine 是所有领域对象的唯一写者,Trigger、Thread、Decision、Ticket、Assignment 都由它在一个事务里落库;Assessor 和 Executor 通过 ports 接口回传结果;真正发消息、回 PR 这类外部副作用,走 Outbox,在事务提交之后才执行。也就是说,领域状态先落库、副作用后置,这样进程在哪一步崩了,状态都能从库里恢复,不会出现「消息发了但状态没记」这种不一致。这正是它敢让 Agent 长时间自主运行、而不用人一路盯着的底气。

自愈

自愈散落在好几处。输入投递失败,2 秒后重试、间隔翻倍直到 1 分钟;一轮结束仍有后台任务时,最多再给 30 分钟,到点杀掉 shell 任务、把 Ticket 报成等待;进程重启会恢复被中断的 Assignment,中断时没进会话历史的输入会被保留。最典型的重试逻辑可以写成:

function nextRetryDelay(attempt: number): number {
  // 2 秒起步,逐次翻倍,封顶 1 分钟
  return Math.min(2000 * 2 ** attempt, 60_000);
}

这些机制合起来,让这个原型在「人不在场」的场景下也能自己爬起来,而不是一崩就断在那里等救。

踩坑

上手有几个坑。一是别把它当多仓库平台,它明确只服务一个仓库,README 第一句就划了这条线。二是别指望它替你决策,最终改不改、合不合,决定权在人,auto-merge 默认也是关的。三是部署前置依赖不少,Node 22.19+、pnpm、git 2.32+、tmux、Python 3.8+、gh 2.48+ 一个都不能少,SETUP 里用一条探测命令先探环境:

node -v; pnpm -v; git --version; tmux -V; python3 --version
gh --version | head -1

适用边界

最后说边界。它适合已经用 DeepSeek Harness、想让一个 Agent 长期盯仓库接活的团队,架构里「授权与执行分离」「副作用后置」「自愈恢复」这几个设计,单独拎出来也值得抄。但如果你的场景是多仓库协作、或者想要开箱即用的成品,它不是答案——README 明确这是研究代码、可能有 bug、工程风格不一致,拿来之前要自己审一遍改一遍。方向是「更自主的 Agent 系统」的一次探索,而不是一个能直接上生产的交付物。