dsh-agent-graph
Verifieddsh-agent-graph · v0.2.1 · MIT · Web UI
Lightweight graph orchestration for DeepSeek Harness: scoped agent nodes, handoff contracts, bounded rework, and a layered global ledger.
Install
dsh plugin add dsh-agent-graph Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-agent-graph
DeepSeek Harness 的图编排插件:职责单一的 agent 节点、结构化交接、有界返工,以及分层的全局账本。
English · 简体中文
一个「交付这个需求」的任务,可以拆成各管一段的节点:调研、spec、实现、测试计划、写测试。dsh-agent-graph 负责运行这张图。每个节点都是一次性的 DSH 子 agent;上游把交付以结构化契约的形式交给下游;下游发现上游交付不足时,带着证据把问题退回直接上游;所有节点的进度、todo、关键决策与踩坑都记录在同一个分层账本里,人和 agent 都能从 README 分层下钻查看。
dsh plugin --profile web add dsh-agent-graph
目录
它是什么
| 关注点 | dsh-agent-graph 的做法 |
|---|---|
| 任务拆解 | 用 needs 边声明依赖的 YAML 图;运行前校验环、未知引用、重复 id |
| 节点执行 | 每次激活 = 一次 ctx.subagents.start('spawn', …);注入 scope prompt、上游交接、账本指针与结构化输出 schema |
| 注意力聚焦 | 节点之间不共享会话;一次激活只看到自己的 prompt、上游交付与账本 |
| 可视化编排 | 在 DSH Web 的 编排 会话页签中,以拖拽画布、节点属性、依赖勾选和即时校验创建 DAG |
| 节点间交付 | 结构化交接版本(summary / artifacts / openIssues)注入每一个直接下游激活 |
| 上游交付不足 | 有界返工请求退回直接上游,附证据与验收标准;可 provide / decline / 逐跳 forward |
| 共享记忆 | 每次运行一个账本(README → index → 节点分区 → 明细),节点读写,引擎确定性再生 |
| 失败行为 | 停图(fail-fast);/graph resume 重试失败与中断的节点 |
| 可观察 | /graph status + 账本;每次激活、交接、返工轨迹与索引都落盘 |
图以人类斜杠命令(/graph)的形式在 DSH 会话里执行,不是暴露给模型的工具。但节点本身是普通子 agent,可以使用宿主授予它们的工具。
为什么需要它
DSH 已经很擅长一次性的委派。长链路多步任务会碰到三个反复出现的问题:
- 上下文串味:一个会话把所有关注点堆在一起;被要求写测试的节点开始操心架构。历史越长,职责边界越模糊。
- 沉默补偿:上游结果不够好时,下游倾向于猜、绕过去、或悄悄扩大自己的范围,而不是把精确的请求退回去。
- 推理蒸发:节点被重新拉起(重试、resume、追加)时,对话没了,当时的关键决策和踩坑也一并消失。
dsh-agent-graph 用机制而不是提示词来对应这三件事:用声明的图管边界,用带证据与上限的返工协议管升级,用持久账本管记忆。agent 保持一次性;图和账本持久。
与其他方式对比
这些能力解决的是相邻而非相同的问题,可以组合使用;按任务形态选表面即可。
| 需求 | dsh-agent-graph |
内置 subagent |
内置 workflow |
内置 ralph |
|---|---|---|---|---|
| 工作单元 | 依赖图中的一个声明节点 | 一次委派调用 | 模型写的 JS 编排脚本 | 一个固定目标的迭代 |
| 依赖关系 | 显式 needs 边 + 拓扑调度 |
在对话里人工决定 | 脚本里命令式安排 | 不适用 |
| agent 间数据 | 结构化交接契约 + 产物 | 最终回答文本 | 脚本变量 | 有界结构化报告 |
| 上游交付不足时 | 有界返工请求退回直接上游,附证据与验收标准 | 调用方重试或重新提示 | 脚本自行决定(无协议) | 工作区 + 下一轮 |
| 重启后保留什么 | 图、交接版本、账本、运行状态(/graph resume) |
无 | 运行快照 / effect cache(随插件) | 工作区 |
| 人类审查面 | /graph status + 分层账本 |
聊天 | 运行 UI | 聊天 |
任务有真实依赖结构、参与者超过两三个、且链路长到「谁在什么时候决定了什么」必须活过重新拉起时,用 dsh-agent-graph。只有一两次委派时,内置 subagent 是更短的路。
使用示例
仓库自带五节点示例(examples/feature-delivery.yaml)与无模型干跑脚本(examples/fake-script.json):
npm install
npm run demo
真实运行用同一张图,节点换成真实子 agent:
/graph run examples/feature-delivery.yaml
Started run feature-delivery-20260918-024342 (graph "feature-delivery", 5 nodes, concurrency 2).
Ledger: /path/to/workspace/.agent-graph/runs/feature-delivery-20260918-024342
Track with /graph status; nodes are DSH subagents, so this may take a while.
追踪与查看都在会话里完成:
/graph status
Run feature-delivery-20260918-024342 — feature-delivery · status completed
activations 7/24 · started 2026-09-17T18:43:42.782Z · finished 2026-09-17T18:43:42.807Z
node state try handoff
implement ok 2 nodes/implement/handoff/v1.md
research ok 1 nodes/research/handoff/v1.md
spec ok 1 nodes/spec/handoff/v2.md
test-plan self_handled 2 nodes/test-plan/handoff/v1.md
tests ok 1 nodes/tests/handoff/v1.md
/graph show test-plan # 打印该节点的账本分区
干跑脚本演示了两种返工结局:implement 把缺失细节退回 spec,spec 提供补充件(provided);test-plan 向 implement 索要接口边界,implement 以「不属于我的职责」拒绝 —— test-plan 随后自行明确缺口(declined → self_handled)。两次交换都记录在 requests/ 下。
可视化 DAG 编排器(V2)
安装到 DSH 的 Web profile 后,会话顶部与内置视图并列的位置会出现新的 编排 页签。它编辑的仍是同一份图定义:拖动卡片调整画布布局;在右侧属性面板修改节点 id、职责范围、提示词与产物;勾选直接上游即可绘制依赖边。草稿和布局按浏览器会话保存;点击保存会把规范化后的 YAML 写入 .agent-graph/graphs/<name>.yaml。这个路径相对于 DSH 主进程的工作目录(即启动 dsh web 时所在的目录),可用插件配置里的 workspace 覆盖;因此图定义与运行账本都落在该工作区,而不是各个会话自己的 cwd。
节点会话的创建边界是刻意严格的:
- 编辑或保存图:只校验并写 YAML,绝不调用
ctx.subagents.start(),因此不会创建任何 agent 会话。 - 保存并运行:先保存同一份 YAML,再启动一次图运行。
- 调度器激活就绪节点:只有根节点会立即启动;下游必须等所有直接上游结算。真正轮到某个节点时,调度器才为这一次激活调用 DSH 的
ctx.subagents.start()。 - DSH 侧边栏展示子会话:这些激活是普通 DSH 子 agent 会话(标签为
agent-graph:<graph>:<node>),会出现在父会话的侧边栏中。仍在 pending 或被依赖阻塞的节点没有子会话,自然也不会占用侧边栏。
因此画布可以先作为低成本的规划面:先搭建、移动并校验复杂 DAG,不产生任何模型工作;真正运行后,再通过 DSH 的子会话和持久账本观察执行过程。
安装
装进 DSH
从 npm 安装(推荐):
dsh plugin --profile web add dsh-agent-graph
从 GitHub 安装(可加 #<sha> 固定版本):
dsh plugin --profile web add github:wrc093/dsh-agent-graph
从本地目录(开发):
cd /path/to/dsh-agent-graph
npm install && npm run build
dsh plugin --profile web add "$(pwd)"
安装或修改后重启 dsh web。确认插件行已进入合成树:
dsh --profile web --dump-config | grep -i agent-graph
要求:Node.js >=22;DSH profile 具备 commands 与 subagents 服务;subagent provider 必须支持结构化输出(spawn 支持;acp、claude-code、codex 不支持)。
更新 / 卸载
dsh plugin --profile web add dsh-agent-graph # 从 npm 更新
dsh plugin --profile web add github:wrc093/dsh-agent-graph#<sha> # 或固定 commit
dsh plugin --profile web remove dsh-agent-graph # 卸载
图定义
name: feature-delivery
description: 调研 → spec → 实现 → 测试计划 → 测试
concurrency: 2
budget:
maxNodeRuns: 24 # 整个 run 的最大激活次数(含返工重跑)
reworkPerEdge: 2 # 每条依赖边允许的返工次数
nodes:
- id: research
scope: 调研事实、约束与风险;不产出方案、不写代码
prompt: |
围绕本次需求完成调研:相关模块、约束、风险……
outputs: [facts, constraints, risks]
- id: spec
needs: [research] # 直接上游;同时也是唯一允许返工的对象
scope: 把调研固化为可执行的 spec;不写代码
prompt: |
基于上游交接产出 spec……
| 字段 | 含义 |
|---|---|
name、description |
图的身份,记录进账本 |
concurrency |
最大并行激活数,默认 2 |
budget.maxNodeRuns |
整个 run 的激活上限,默认 40 |
budget.reworkPerEdge |
每条依赖边的返工上限,默认 2 |
nodes[].id |
^[a-z][a-z0-9_-]*$,图内唯一 |
nodes[].scope |
职责边界;是节点(和上游)判断返工请求是否成立的依据 |
nodes[].prompt |
每次激活注入的任务说明 |
nodes[].needs |
直接上游 id;决定调度顺序,也是唯一可返工对象 |
nodes[].outputs |
可选,仅文档用途(展示在节点账本分区) |
编译器会拒绝环、未知/重复引用、自环、非正数预算与非法字段。拓扑序按 id 排序的 frontier 计算,同一份图永远得到同一顺序。
命令
| 命令 | 行为 |
|---|---|
/graph run <file.yaml> |
校验图、建立账本、后台启动运行并立即返回 |
/graph status [runId] |
展示运行:状态表、未决返工请求、失败、最近动态(默认最近一次) |
/graph resume [runId] |
从 run.json 重建引擎继续跑;失败与中断节点重试 |
/graph show [runId] <node> |
打印某个节点的账本分区(nodes/<id>/index.md) |
/graph runs |
列出本工作区的运行,最新在前 |
/graph editor save <base64url> |
编排 页签使用的内部桥接命令:校验并保存规范 YAML,绝不启动 run 或子 agent |
节点是完整子 agent,所以运行在后台执行;账本是权威进度面,/graph status 在运行活跃时读内存状态,否则读 run.json。
运行机制
- 校验与编译:解析 YAML、检查图、计算上下游映射与规范拓扑序。
- 调度:所有
needs已结算(ok/self_handled)的节点进入就绪队列,最多并行concurrency;未决返工请求的 holder 激活占用同一并发额度。 - 激活:以一次性子 agent 启动。prompt 由引擎确定性拼装:
scope+prompt、上游最新交接、账本路径(运行README、本节点分区、decisions/pitfalls 索引)、记录义务,以及适用时的返工请求或重试结论。 - 交接:成功后写出
nodes/<id>/handoff/vN.md;其summary/artifacts/openIssues注入每一个直接下游激活。 - 记录:每次激活写入
nodes/<id>/attempts/NNN.md;账本的 README、索引、时间线与节点分区在每次状态变化后确定性再生。 - 结算或失败:所有节点
ok/self_handled即完成;第一个节点失败即停图(在途激活被中止),resume把失败与中断节点放回pending。
所有持久化写入经过单一 persist 链串行化,并发节点完成不会在同一批文件上竞争。
返工协议
无法完成职责的节点以 status: "needs_rework" 结束,并给出:
| 字段 | 含义 |
|---|---|
target |
直接上游节点 id(非直接上游会降级为自理) |
problem |
缺什么 / 哪里不对 |
evidence |
账本/交接引用,例如 nodes/spec/handoff/v1.md#interface |
acceptance |
补到什么程度算够 |
上游被重新拉起作为 holder,必须给出恰好一种结局:
| 结局 | 效果 |
|---|---|
provided |
补充件写为该 holder 的新交接版本并交回发起方;发起方带着新材料与出处(providedBy)重新激活 |
declined |
发起方重新激活并被要求自己补齐;最终状态为 self_handled |
| forward | holder 以 needs_rework 把请求交给自己的直接上游;请求逐跳传递并保留完整轨迹 |
有界规则保证循环必收敛:
- 每个请求对每个节点最多 forward 一次;非法/缺失 target 降级为
declined。 - 同一问题(
problem归一化后按 origin 哈希)只允许请求一次。重复请求给发起方一次自理机会;再犯则节点显式失败(unbounded rework)。 budget.reworkPerEdge限制每条边的请求数;超限的请求降级为自理。- holder 激活失败与普通节点失败一致:停图。
每个请求都写入 requests/R-XXXX.md 并带完整轨迹,升级路径事后可审。
全局账本
.agent-graph/runs/<runId>/
├── README.md # 运行总览、节点表、最近动态
├── index.md # 分区索引
├── progress.md # 完整活动时间线
├── run.json # 机器可读的持久化状态
├── nodes/<id>/
│ ├── index.md # 职责、状态、上下游、交接、尝试
│ ├── handoff/vN.md # 交付版本(含返工补充件)
│ └── attempts/NNN.md # 引擎写的每次激活记录
├── decisions/index.md # 节点写的 `D-*.md` 汇总
├── pitfalls/index.md # 节点写的 `P-*.md` 汇总
└── requests/R-XXXX.md # 返工请求与完整轨迹
节点 agent 被要求用普通文件工具维护自己的工作记录(attempts/、decisions/D-<HHMMSS>-<slug>.md、pitfalls/P-<HHMMSS>-<slug>.md、todo.md)。布局与索引归引擎所有,节点从不编辑共享文件。阅读是分层的:README → index → 节点分区 → 按需下钻。
.agent-graph/ 默认被仓库 .gitignore 忽略;是否提交或导出某个账本,按工作区自行决定。
节点输出契约
每次激活必须以结构化输出结束,通过 DSH outputSchema 强制(子 agent 调用 structured-output 工具):
{
"status": "ok | needs_rework | failed",
"summary": "给下游的交接摘要",
"artifacts": ["产物路径"],
"openIssues": ["下游需要知道的事"],
"rework": {
"target": "直接上游 id",
"problem": "缺什么",
"evidence": ["nodes/spec/handoff/v1.md#section"],
"acceptance": "补到什么程度算够"
},
"reworkOutcome": "provided | declined",
"addendum": "交回发起方的补充内容",
"declineReason": "拒绝理由"
}
status 为 needs_rework 时必须给出 rework;节点作为返工 holder 应答时必须给出 reworkOutcome 与 addendum/declineReason。违反契约的结果会让节点带明确原因失败;引擎对真实与脚本结果一视同仁地校验。
CLI
CLI 负责校验、干跑与检查。真实执行发生在 DSH 内,因为节点是宿主子 agent。
dsh-agent-graph validate <file.yaml> # 解析 + 校验,打印拓扑序
dsh-agent-graph run <file.yaml> --fake <script.json> # 用脚本化响应干跑(无模型、无网络)
dsh-agent-graph status [--run <runId>] [--workspace <dir>] # 查看运行(状态表、请求、失败)
dsh-agent-graph resume [--run <runId>] --fake <script.json> # 继续已停止/失败运行
--fake 脚本格式:{ "nodes": { "<nodeId>": [ {result}, … ] } },示例见 examples/fake-script.json。退出码:0 成功、1 用法/校验错误、2 运行失败结束。
数据处理与限制
- 账本是
<workspace>/.agent-graph/下的本地纯文本,包含节点摘要、产物路径、决策与踩坑;不包含模型凭据、其他会话的 prompt 或工具调用参数。节点写的摘要可能引用项目内容,分享账本前请自行审查。 - 节点以 DSH 子 agent 身份运行,权限跟随宿主授予;本插件不会放宽沙箱或审批设置。
- 每次激活都是一次真实子 agent 运行并消耗 token。预算用于约束花费:
budget.maxNodeRuns限制总激活数,budget.reworkPerEdge限制返工,nodeTimeoutMs(插件配置,默认 30 分钟)限制单次激活。
| 限制 | 默认 | 含义 |
|---|---|---|
concurrency |
2 |
最大并行激活数 |
budget.maxNodeRuns |
40 |
整个 run 的激活总数(含重试与返工) |
budget.reworkPerEdge |
2 |
每条依赖边的返工请求数 |
nodeTimeoutMs |
1800000 |
单次激活硬超时;0 表示关闭 |
超出 maxNodeRuns 以 budget exhausted 结束运行;超时使该节点失败(停图),原因写入账本。
排查
| 现象 | 检查 |
|---|---|
/graph 变成普通聊天 |
插件是否装在当前 profile、commands 服务是否存在、安装后是否重启 |
subagent provider … does not support … capability |
换支持 outputSchema 的 provider(spawn/fork);acp、claude-code、codex 不支持 |
finished without structured output |
子 agent 未调用 structured-output 工具;检查 provider 支持情况并重试该节点 |
运行立刻 failed 且 budget exhausted |
调大 budget.maxNodeRuns 或减少返工;返工循环可在 requests/ 里看到 |
unbounded rework 失败 |
节点重复请求了一个已被自理的问题;收紧节点 prompt 或上游交付 |
no runnable nodes: the graph is stuck |
某个依赖从未结算;在 /graph status 看节点状态与未决请求 |
时间线里出现 invalid rework target |
节点指向了非上游节点,已降级自理;修节点 prompt 或 needs 边 |
崩溃后状态里有 pending 节点 |
执行 /graph resume <runId>;已完成节点不会重跑 |
| 启动时报图校验错误 | 未知/重复 id、环、自环或非正数预算;错误信息会列出全部问题 |
开发
npm install
npm run typecheck
npm test
npm run demo # 完整干跑自带示例(不需要模型)
npm run build # dist/(插件入口 + CLI)
| 文件 | 职责 |
|---|---|
src/core/graph.ts |
YAML 解析、校验、规范拓扑编译 |
src/core/engine.ts |
调度、交接、返工状态机、持久化 |
src/core/store.ts |
账本布局与索引/时间线的确定性再生 |
src/core/runner.ts |
节点结果契约、JSON schema、脚本化测试替身 |
src/core/prompt.ts |
确定性激活 prompt 组装(scope、交接、账本、规则) |
src/core/types.ts |
共享词汇(图、运行状态、请求、结果) |
src/dsh/contracts.ts |
ctx.commands 与 ctx.subagents 的结构契约 |
src/dsh/subagent-runner.ts |
一次性 spawn runner(结构化输出) |
src/plugin.ts、src/cli.ts |
斜杠命令面与 CLI |
test/、examples/ |
单元/集成测试与自带示例 |
测试是确定性的:脚本化 runner 替代子 agent,调度、返工与账本语义无需模型或网络即可覆盖;插件命令面针对 fake host 验证。V1 设计记录见 mydocs/specs/。
本地 DSH 开发:构建 checkout、把路径装进开发 profile,改完重启 DSH。不要把 .agent-graph/ 与凭据提交进仓库。
贡献
带最小图、脚本化 trace(或账本片段)与期望的调度/返工行为,开一个 issue。语义改动请补一个聚焦的回归测试并跑上面的检查。文档需要区分「协议保证的行为」与「依赖节点 prompt 与模型行为的部分」。
许可
本项目基于 MIT License 开源。