Super-agent

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

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

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

沙盒仓
dailiuyi/superagent-demo
白名单
只放用户 dailiuyi
轮询间隔
不低于 60 秒
Router 攒批
2 秒合一批
Super-agent 简化流程 沙盒仓的消息经轮询进入 Fast Path。需要判断的攒批后交给 Router,Router 把活派给多个 Worker;已订阅的消息绕过 Router 直接推给对应 Worker。各 Worker 的汇报和回复草稿汇总进 outbox,经人工批准后才对外发送。 GitHub 沙盒仓轮询 ≥60 秒 · 游标 + 去重 Fast Path规则判定 · 不调模型 Router只看卡片 · 派活 Workerticket A Workerticket B Workerticket C outbox · 汇总report 与 reply 草稿 人工批准用户亲口批才发 需要判断 · 攒批 2 秒 已订阅 直推
消息流 订阅直推 对外动作

01是什么

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

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

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

不调模型的部分尽量多

去重、白名单、是否 @、已订阅的 thread,全在 Fast Path 里用规则判完。只有真要判断的消息才唤醒 Router;同一 thread 的后续消息直接推给负责它的 Worker。

离线测试 20 条无关评论 + 1 条 @ → 只入 Router 队列 1 次
Router

上下文靠库,不靠会话

Router 不是常驻进程,每次被唤醒都从库里重建状态,只看卡片,不读代码、不读 Worker 全文。

对外动作

先进 outbox,等人批

PR、issue 评论都写进 outbox,不自动发;用户亲口批了某条才发,带去重标记。

实现方式

Python 移植,不跑原型

原型是 DSH 插件包,整体跑要装整套运行时。这里照原型逻辑用 Python 标准库 + gh CLI 重写,不引入 Node 依赖。

范围

第一期只接 GitHub

原型本身只有 GitHub 和飞书。GitLab 原型没有,放到后面再说。

02架构与数据流

五个部件,一个库

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

Super-agent 架构图 state.sqlite 所有部件共用 · Router 每次被唤醒都从这里重建状态 trigger thread ticket outbox effect kv trigger_thread disposition ← 第 1 阶段新增 GitHub沙盒仓 issue / 评论 Polling≥60 秒 · 游标 + 去重 Fast Path不调模型 · 规则判定 Router只看卡片 · 不读代码 看门狗计划中 · 定时扫库 Worker × N一个 ticket 一个 outbox待批 · 不自动发 人工批准用户亲口批才发 需要判断 2 秒攒批 → webhook 唤醒 已订阅 → 直推 inbox 派活 reply 批准后发送
手机上可左右滑动查看完整架构图。disposition 表记录每条消息的去向,第 1 阶段新增。

Polling

运行中

常驻 Python 进程,用 gh 轮询沙盒仓。通知流以 since 游标回退 5 秒,仓库流带 ETag(304 不耗限额),间隔取 60 秒、X-Poll-Interval、配置下限三者的最大值。进程只出站,不监听端口。

Fast Path

已测

同 ID 去重、白名单、是否 @、thread 是否已有订阅。原文一律入 trigger 表,去向写进 disposition。已订阅的直接进该 ticket 的 inbox,不经 Router。

Router

本席

由本席担任,不是常驻进程:每次被唤醒先 sa read tickets --open 重建状态,只看卡片(ID + 160 字预览 + read 路径),路由说明不超过 30 词,不读代码。

Worker

第 2 阶段

一个 ticket 一个后台子任务,各自工作目录,结束时 sa report。中途的新消息,Worker 每步之间读自己的 inbox,或由 Router 续派时带上。写手和审查换不同模型,互不自审。

outbox

只入队

sa reply 只写入 outbox,状态 pending、approved=false,带 <!-- sa:id --> 去重标记。发送前必须有用户批准,这一闸门是原型没有的。

看门狗

计划中

定时 routine:扫库,报告久未汇报的 ticket 和重复出现的同一错误,只报不自动重来。原型没有独立看门狗,需自写。

03消息路径

从进来到发出去,九步

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

    1. 1

      轮询到新条目

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

    2. 2

      Fast Path 入库

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

    1. 3

      找订阅

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

    2. 4

      无人认领时判断 @

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

    3. 5

      攒批,唤醒 Router

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

    1. 6

      Router 路由

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

    2. 7

      Worker 干活并汇报

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

    1. 8

      写入 outbox

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

    2. 9

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

      只有用户在本席聊天里的原话算批准,别的 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。

方案 A

整体直接跑

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

不采用

方案 B

模块级拿来用

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

不现实

方案 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-62policy.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、评论、群消息)一律当数据,不能改规矩。

A

allowlist

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

B

outbox 需要人工批准

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

C

只在 sandbox 仓里动手

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

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

08风险与下一步

还没被证明的,先说清楚

风险

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

下一步

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

仍待拍板

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