Super-agent

路由复现 · 第 1 阶段代码完成,待真实验收

规则先分流,该判断时才叫 Router,活交给 Worker。

Super-agent 是照 deepseek-ai/dsh-superagent-prototype 复现的 router / worker 多智能体系统:确定性接入、SQLite 状态、订阅直推 Worker、一个 Router 判断去向。它只在沙盒仓里动手,所有对外动作先进 outbox,等人批准。

看架构与数据流看阶段进度

Fast Path 分流示意 21 条评论进入 Fast Path,其中 20 条没有 @ 机器人,入库留痕后记为 filtered/not_mentioned,不进 Router 队列;只有 1 条 @ 了机器人,进入 Router 队列,攒批 2 秒后唤醒 Router。 20 条无关评论 + 1 条 @ @ Fast Path规则判定 · 不调模型 ×20 ×1 20 条 · 留痕不入队filtered/not_mentioned 1 条 · 进 Router 队列攒批 2 秒后唤醒 Router
离线测试:20 条无关评论加 1 条 @,只入 Router 队列 1 次。
沙盒仓
dailiuyi/superagent-demo
白名单
只放用户 dailiuyi
轮询间隔
不低于 60 秒
Router 攒批
2 秒合一批

01是什么

一套只对沙盒仓生效、能跑起来的复现

目标只有一件事:在本机照原型复现一套 Super-agent,由确定性接入、SQLite 状态、订阅直推 Worker、本席当 Router 和看门狗五样东西组成。本席自己派 Worker 写底座和沙盒仓的活,不经过工程调度。

术语

本席
负责这套系统的对话 agent 席位,兼任 Router。
thread
一个 issue 及其评论串。
ticket
Router 建出来、交给一个 Worker 负责的一件活。
Fast Path
不调模型、只用规则判定去向的接入层。
outbox
对外动作的待批队列,不自动发送。

五个要点

02架构与数据流

五个部件,一个库

所有部件共用一个 SQLite 库 state.sqlite。实线是消息流向,虚线是订阅直推,强调色是对外动作;虚线框是计划中、还没写的部件。

Super-agent 架构图 GitHub 沙盒仓经 Polling 轮询进入 Fast Path;已订阅的消息直接进 Worker 的 inbox,需要判断的经 2 秒攒批和 webhook 唤醒 Router;Router 派活给 Worker;Worker 的 reply 写入 outbox,状态 pending、approved=false,经人工批准后才发回 GitHub(设计中,尚未验证)。看门狗计划定时扫库。所有部件共用 state.sqlite,Router 每次被唤醒都从这里重建状态。 GitHub沙盒仓 issue / 评论 Polling≥60 秒 · 游标 + 去重 Fast Path不调模型 · 规则判定 看门狗计划中 · 定时扫库 Router只看卡片 · 不读代码 Worker × N一个 ticket 一个 outbox待批 · 不自动发 人工批准用户亲口批才发 需要判断 2 秒攒批 → webhook 唤醒 已订阅 → 直推 inbox 派活 reply pending · approved=false 批准后发送 设计中 · 尚未验证 state.sqlite 所有部件共用 · Router 每次被唤醒都从这里重建状态 trigger thread ticket outbox effect kv trigger_thread disposition ← 第 1 阶段新增 Super-agent 架构图(竖版) GitHub 沙盒仓经 Polling 轮询进入 Fast Path;需要判断的经 2 秒攒批和 webhook 唤醒 Router,已订阅的直接推进 Worker 的 inbox;Router 派活给 Worker;Worker 的 reply 写入 outbox,经人工批准后才发回 GitHub(设计中,尚未验证)。看门狗计划定时扫库。所有部件共用 state.sqlite。 GitHub沙盒仓 issue / 评论 Polling≥60 秒 · 游标 + 去重 Fast Path不调模型 · 规则判定 Router只看卡片 · 不读代码 Worker × N一个 ticket 一个 人工批准用户亲口批才发 outbox待批 · 不自动发 看门狗计划中 · 定时扫库 需要判断 · 2 秒攒批 → webhook 唤醒 已订阅 直推 inbox 派活 reply 批准后发回 GitHub 设计中 · 尚未验证 state.sqlite 所有部件共用 Router 每次被唤醒都从这里重建状态 trigger thread ticket outbox effect kv trigger_thread disposition ← 第 1 阶段新增
  • 消息流
  • 订阅直推
  • 对外动作
  • 计划中

disposition 表记录每条消息的去向,第 1 阶段新增。

各部件

03消息路径

从进来到发出去,九步

以「在 issue 里 @ 机器人,请把 README 标题改成 Hello」为例,对应 stage0 的真实命令输出。

  1. 接入

    1. 轮询到新条目

      poller 每约 60 秒取一次通知和 issue / 评论流。Trigger ID 是确定性的,形如 github:<repo>:issue:<n>:opened,游标回退 5 秒造成的重复,靠下一步去重吃掉。

    2. Fast Path 入库

      先查 ID:已有就返回 duplicate,什么都不写。否则原文入 trigger 表,再判去向:作者是机器人自己 → filtered/self;不在白名单 → filtered/not_allowed。

  2. 分流

    1. 找订阅

      该 thread 若已有 open 的 ticket 认领,消息直接进它的 inbox,needs_router 为 false,到此结束。done 的 ticket 只有真人消息能重新唤醒;stopped 的不唤醒;[bot] 后缀不算真人。

    2. 无人认领时判断 @

      文中整词 @ 了机器人(大小写不敏感)才进 Router 队列;否则记为 filtered/not_mentioned。离线测试:20 条无关评论加 1 条 @,只入队 1 次。

    3. 攒批,唤醒 Router

      队首满 2 秒后发一次 POST,内容是 trigger 列表和读取提示。成功才清队列;没有配置 webhook 就保留队列。

  3. 处理

    1. Router 路由

      Router 先读 open ticket 卡片,再用 sa route 建 ticket 并订阅该 thread。此后同 thread 的追加消息走第 3 步直推,不再经过它。

    2. Worker 干活并汇报

      Worker 在自己的目录里、沙盒仓的独立分支上改。sa report 把进展写回 ticket(截断 600 字),--done 标记完成。

  4. 对外

    1. 写入 outbox

      sa reply 把要发的评论排进 outbox,返回 "sent": false。它此刻只是草稿。

    2. 用户批准,再发出 设计中 · 尚未验证

      只有用户在本席聊天里的原话算批准,别的 agent 转述不算。批准后才发送,并带去重标记,失败重试不会重复发。

04复用了原型的哪些部分

抄规格,不抄运行时

上游 deepseek-ai/dsh-superagent-prototype @ 6337fc8,只读浅克隆,未改、未装依赖、未跑。

原型是什么

不是独立服务,而是 DSH(DeepSeek Harness)的一个插件包:入口靠 @deepseek-ai/cordis 插件框架注入,Router / Worker 都是 DSH Session,依赖 20 多个 0.2.1-alpha.2 的 peer 包,HTTP 面板走 dsh-host-webserver。部署要全局装 @deepseek-ai/dsh@0.2.1-rc.3、pnpm、tmux 和 DeepSeek 模型 key。

三种复用方式

  1. 方案 A

    整体直接跑

    要在本机装一整套 DSH 运行时和模型凭据,Router / Worker 变成 DSH 会话,与「本席当 Router、子任务当 Worker」冲突。

    不采用

  2. 方案 B

    模块级拿来用

    github.ts 依赖 cordis Context、host/poll/kv、zod;store.ts 用 node:sqlite,但与 policy / migration / versions 耦合。单抽出来要补一堆桩。

    不现实

  3. 方案 C

    照原型逻辑用 Python 移植

    行为、表结构、游标和去重规则照抄,并标注出处行号。只用 Python 标准库和已登录的 gh,不引入 Node 依赖,不装新软件。

    采用

逐个部件

部件原型位置能直接跑?本项目做法
GitHub 轮询src/sources/github.ts:198-311gh api 通知、since 游标回退 5 秒、ETag、X-Poll-Interval ≥60 秒、失败重试表否cordis 插件Python 移植,照抄游标 / ETag / 间隔规则;用 gh CLI,无新依赖
Fast Pathsrc/core/intake.ts:19-62、policy.ts:167-211否Engine / Repo 端口移植进 sa.py 的 ingest:duplicate → 范围过滤(filtered)→ thread 扇出 → 无人认领才进 Router 队列
Router 唤醒runtime.ts + notices.ts常驻 DSH 会话,2 秒批处理否自写:轮询进程攒批后 POST webhook 唤醒本席
Worker 管理executor/dsh.ts、sessions.tsDSH Session + tmux否自写:本席派后台子任务,各自目录,结束 sa report
Outbox / effectapp/outbox.ts、effects.ts、github-channel.ts持久队列、重试、FAILURES_BEFORE_ERROR、HTML 去重标记否逻辑小,52 + 161 行移植 effect 重试语义;发送前加「用户批准」闸门(原型无)
看门狗无独立 watchdog,只有 retry-health.ts 与 effects 失败计数—自写:routine 定时扫库
端口原型自身不监听端口;面板由 DSH webserver 承载—不开端口,只出站 gh 与出站 webhook
Router 规则prompts/orchestrator / dispatch / admission参考直接拿来当 Router 规则参考;docs/ 与上面源码当规格。原型的运行时代码一行不用

和原型的差异

方面原型Super-agent 复现
库orchestrator.sqlite,含 schema 迁移与 addColumnsstate.sqlite,kv 只写 store.schema=1,无迁移
表kv / trigger / thread / ticket / outbox / effect / trigger_thread,另有 disposition、decision前 7 张同名、列结构照抄;第 1 阶段补了 disposition,decision 暂未建
订阅关系相同放在 thread.value JSON 的 subscriptions(ticket id 数组)
ticket inboxnotices / 扇出机制trigger id 列表存在 ticket JSON 里
replyreceipt + effect 队列直接发只写 outbox(pending,approved=false,带去重标记),不发送
卡片约定相同report 截断 600 字,tickets 卡片 160 字预览
准入admission 表简化为 allow 白名单;context thread(关联而非本 ticket 的 thread)未区分
Router 唤醒常驻 DSH 会话收卡片webhook 唤醒本席;会话压缩改不了,所以上下文靠库
未移植PR 流 / review comments、open ticket 的 PR 与 CI 轮询、默认分支 CI、订阅 thread 逐个轮询(pollThreads)、分页(超过 50 条通知 / 100 条)、连续失败健康度上报

05阶段计划

每阶段验证过,再进下一阶段

四个阶段,验收标准事先写死。前两个阶段的记录来自 stage0 / stage1 文档。

  1. 第 0 阶段 · 已通过

    底座

    建库和 sa.py CLI(receive、route、read、report、reply)。

    • 假消息入库
    • 同 ID 再收返回 duplicate
    • sa read trigger/<id> 读回原文
    • route / report / reply 冒烟
  2. 第 1 阶段 · 代码完成 · 待真实验收

    接入 + Fast Path

    沙盒仓 issue 里 @ 机器人账号,60–120 秒入库;20 条无关事件只唤醒本席 0 次,1 条 @ 唤醒 1 次。

    • 离线测试 13/13 通过
    • 机器人身份核对、webhook 测试唤醒 HTTP 200
    • 常驻 poller 已启动,连续空轮询约 60 秒一轮
    • 真实 @ 入库与唤醒
  3. 第 2 阶段 · 未开始

    Router + Worker 闭环

    issue 里提个小改动,出分支和 PR 草稿,issue 评论草稿进 outbox。

    • 本席会话里没出现代码细节
    • 同 issue 追加一句直接进该 ticket inbox,没经过 Router
  4. 第 3 阶段 · 未开始

    Outbox + 看门狗 + 恢复

    把故障和中断都演一遍。

    • 故意让 gh 失败一次,批准的消息能补发且不重复
    • 手动搁置一个 ticket 30 分钟,看门狗报出来
    • 重启进程后未完成 ticket 能接着做

第 0 阶段:表与映射

kv / trigger / thread / ticket / outbox / effect 同名,列结构照抄原型(id/key + JSON value;effect 原样保留 seq / generation / attempts / error / due / since,第 0 阶段未使用)。另外保留原型的 trigger_thread(thread 与 trigger 的索引),共 7 张表。订阅关系同原型,放在 thread 的 subscriptions 里。

第 1 阶段:接线与常驻

仓库改名为 superagent-demo,代码、测试和脚本里的旧名已全部替换;机器人身份核对为 dailiuyi-bot。真实只读跑过一轮,issues / comments 两个流都能读到。开跑前清了库(旧数据另行备份),游标从首次真实轮询时刻重新开始。常驻 poller 经用户批准启动,单轮异常不退出,日志带 UTC+8 时间戳,前三轮约 60 秒一轮、均为空。

06关键代码要点

三个文件,几条硬规则

sa.py 是 CLI 与 Fast Path,poller.py 负责轮询,tests/test_stage1.py 用注入的假 gh 跑离线测试。下面的伪代码按 stage1 记录整理,是示意,不是源码原文。

sa.pyingest() · 移植 core/intake.ts:19-62
def ingest(t):
    if exists(t.id):
        return "duplicate"             # 什么都不写
    save_trigger(t)                    # 原文一律入 trigger 表

    if t.author == bot_user:
        return filtered("self")        # 防自激
    if t.author not in allow:
        return filtered("not_allowed") # 留痕,但永不唤醒

    subs = subscriptions(t.thread)
    if subs:
        # open 直接进 inbox;done 仅真人可重唤醒
        # stopped 不唤醒;[bot] 后缀不算真人
        deliver_to_inbox(subs, t)
        return accepted(needs_router=False)

    # bot_user 或 mention_names,整词、大小写不敏感
    if mentioned(t.text):
        kv["router.queue"].append(t.id)
        return accepted(needs_router=True)
    return filtered("not_mentioned")

轮询:间隔与游标

间隔 = max(60, X-Poll-Interval, poll_min_seconds)。每轮先读 notifications?all=true&since=游标,只认配置里的仓库,再逐个读 issue 和评论;失败进 kv 的 github.retries,遇 410 或超过 24 小时放弃。

仓库流 issues / comments 升序读,since 取最新 updated_at 减 5 秒,上限 14 天并按小时取整;游标不动时带 ETag,304 不耗限额。只收首次轮询之后的新条目,跳过机器人自己的发言。游标都存在 kv。

确定性 Trigger ID

同一条消息永远得到同一个 ID,重复交给 ingest 去重:

  • github:<repo>:issue:<n>:opened
  • …:comment:<id>:<updated_at>

flush:攒批唤醒

队首满 2 秒才发一次 POST,payload 是 triggers 列表加 sa read tickets --open 提示。成功清空队列并写 router.lastWake;没配 webhook 返回 no_webhook 并保留队列。密钥只从环境变量读进请求头,不打印、不落盘。

CLI 一览

命令作用
sa receive收一条消息并走 Fast Path:去重、过滤、直推订阅者或进 Router 队列
sa routeRouter 建 ticket 并订阅 thread
sa read读 trigger/<id>、ticket/<id>、disposition/<id>,或 tickets --open 卡片、triggers --pending(进过 Router 队列且未被认领)
sa reportWorker 写回进展(截断 600 字);--done 完成,--stop 停止
sa reply把对外回复排进 outbox,"sent": false
sa flush执行一次攒批唤醒(flush),队列为空或未配 webhook 时不发
终端stage0 冒烟 · 节选
$ python3 sa.py receive --json '{"id":"gh-1001", … "text":"@bot 请把 README 标题改成 Hello"}'
{ "status": "accepted", "delivered_to": [], "needs_router": true }

$ python3 sa.py receive --json '{"id":"gh-1001", … "text":"重复"}'
{ "status": "duplicate", "id": "gh-1001" }

$ python3 sa.py route gh-1001 --ticket tk-readme --thread sandbox#1 …
{ "status": "routed", "ticket": "tk-readme" }

$ python3 sa.py receive --json '{"id":"gh-1002", … "text":"顺便加一行说明"}'
{ "status": "accepted", "delivered_to": ["tk-readme"], "needs_router": false }

$ python3 sa.py reply tk-readme --target … 已改好,见分支 sa/readme-title
{ "status": "queued", "outbox": "ob-f90e0fb4", "sent": false }

$ python3 sa.py report tk-readme 完成 --done
{ "status": "reported", "ticket_status": "done" }
config.json字段 · 凭据不在其中
{
  "repo": "dailiuyi/superagent-demo",
  "allow": ["dailiuyi"],
  "bot_user": "dailiuyi-bot",
  "mention_names": ["dailiuyi-bot"],
  "webhook_url": /* 已配置,不展示 */,
  "batch_seconds": 2,
  "poll_min_seconds": 60
}

离线测试覆盖 · 13 项

  • duplicate
  • 白名单与去向
  • self
  • @ 判断(@sa-botx 不误判)
  • 订阅直推
  • done 重唤醒
  • stopped 与机器人不重唤醒
  • 两条合一次唤醒
  • 无 webhook 保留队列
  • 20 + 1 只入队 1 次
  • 游标回退后全 duplicate
  • ETag 304
  • 间隔下限 60
  • 通知重试与 410 放弃
  • sa read triggers --pending

07安全边界

三道闸

外部文本(issue、评论、群消息)一律当数据,不能改规矩。

  1. allowlist

    配置里 allow 只有用户本人。不在名单里的作者,消息照样入库留痕,但去向记为 filtered,永远不会唤醒 Router 或进入 ticket。机器人自己的发言同样被过滤,避免自激。

  2. outbox 需要人工批准

    推送、开 PR、部署、对外发消息一律要用户亲口批准;合并由用户自己做。reply 只是入队,状态是待批,不是已发。只有本席聊天里用户的原话算批准。

  3. 只在 sandbox 仓里动手

    只碰沙盒仓和本项目自己的工作目录。Worker 在独立分支上提交,写手和审查用不同模型。公司仓和其他个人仓不在范围内,也不碰其他席位的交接目录。

常驻进程不监听任何端口,只出站。凭据只从环境变量和权限受限的文件读取,只传给对应的子进程,不打印、不写进日志;已核对日志里没有 token 字样。

08风险与下一步

还没被证明的,先说清楚

风险

  • 真实链路未验收常驻轮询目前只跑过空轮询,真实 @ 从入库到唤醒 Router 还没走过一遍。
  • 批准后发送是设计outbox 闸门、补发不重复都属于第 3 阶段,尚无验证。
  • 覆盖面比原型窄PR 流、review comments、CI 轮询、健康度上报都没移植,也没做分页(超过 50 条通知 / 100 条)。
  • 准入简化只有 allow 白名单,没有 admission 表,也不区分 context thread。
  • Router 依赖库重建会话压缩改不了,Router 的判断质量取决于卡片是否足够:160 字预览、600 字报告、30 词路由说明。

下一步

  1. 完成第 1 阶段真实验收用户在 superagent-demo 的 issue 里 @dailiuyi-bot,看 60–120 秒入库并唤醒 1 次;再发 20 条不带 @ 的,唤醒 0 次。
  2. 第 2 阶段:Router + Worker 闭环小改动出分支、PR 草稿和评论草稿;追加消息直推 inbox。
  3. 第 3 阶段:outbox、看门狗、恢复失败补发不重复、搁置 30 分钟被报出、重启后接着做。

仍待拍板

看门狗频率(每 15 分钟还是每小时,评估里倾向 15 分钟);GitLab 放到后面;QQ 群接入建议本期不接,避免和已有的群聊席位抢消息。跑通后要不要推广另议。