# dsh-agent-graph

<p align="center"><strong>DeepSeek Harness 的图编排插件：职责单一的 agent 节点、结构化交接、有界返工，以及分层的全局账本。</strong></p>

<p align="center">
  <a href="https://www.npmjs.com/package/dsh-agent-graph"><img src="https://img.shields.io/npm/v/dsh-agent-graph?color=cb3837&label=npm" alt="npm version"></a>
  <a href="https://www.npmjs.com/package/dsh-agent-graph"><img src="https://img.shields.io/npm/dt/dsh-agent-graph?style=flat-square&color=cb3837" alt="npm downloads"></a>
  <img src="https://img.shields.io/badge/Node.js-%3E%3D22-22c55e" alt="Node.js >=22">
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-22c55e" alt="MIT license"></a>
</p>

<p align="center"><a href="README.md">English</a> · <strong>简体中文</strong></p>

<p align="center">
  <img src="assets/readme-dag-light.png" alt="dsh-agent-graph 的 DAG 工作流：职责清晰的 agent 结构化交接、向直接上游的有界返工，以及共享账本记录" width="100%">
</p>

一个「交付这个需求」的任务，可以拆成各管一段的节点：调研、spec、实现、测试计划、写测试。`dsh-agent-graph` 负责运行这张图。每个节点都是一次性的 DSH 子 agent；上游把交付以结构化契约的形式交给下游；下游发现上游交付不足时，带着证据把问题退回**直接上游**；所有节点的进度、todo、关键决策与踩坑都记录在同一个分层账本里，人和 agent 都能从 `README` 分层下钻查看。

```bash
dsh plugin --profile web add dsh-agent-graph
```

## 目录

- [它是什么](#它是什么)
- [为什么需要它](#为什么需要它)
- [与其他方式对比](#与其他方式对比)
- [使用示例](#使用示例)
- [可视化 DAG 编排器（V2）](#可视化-dag-编排器v2)
- [安装](#安装)
- [图定义](#图定义)
- [命令](#命令)
- [运行机制](#运行机制)
- [返工协议](#返工协议)
- [全局账本](#全局账本)
- [节点输出契约](#节点输出契约)
- [CLI](#cli)
- [数据处理与限制](#数据处理与限制)
- [排查](#排查)
- [开发](#开发)
- [贡献](#贡献)
- [许可](#许可)

## 它是什么

| 关注点 | `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`）：

```bash
npm install
npm run demo
```

真实运行用同一张图，节点换成真实子 agent：

```text
/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.
```

追踪与查看都在会话里完成：

```text
/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`。

节点会话的创建边界是刻意严格的：

1. **编辑或保存图**：只校验并写 YAML，绝不调用 `ctx.subagents.start()`，因此**不会**创建任何 agent 会话。
2. **保存并运行**：先保存同一份 YAML，再启动一次图运行。
3. **调度器激活就绪节点**：只有根节点会立即启动；下游必须等所有直接上游结算。真正轮到某个节点时，调度器才为这一次激活调用 DSH 的 `ctx.subagents.start()`。
4. **DSH 侧边栏展示子会话**：这些激活是普通 DSH 子 agent 会话（标签为 `agent-graph:<graph>:<node>`），会出现在父会话的侧边栏中。仍在 pending 或被依赖阻塞的节点没有子会话，自然也不会占用侧边栏。

因此画布可以先作为低成本的规划面：先搭建、移动并校验复杂 DAG，不产生任何模型工作；真正运行后，再通过 DSH 的子会话和持久账本观察执行过程。

## 安装

### 装进 DSH

从 npm 安装（推荐）：

```bash
dsh plugin --profile web add dsh-agent-graph
```

从 GitHub 安装（可加 `#<sha>` 固定版本）：

```bash
dsh plugin --profile web add github:wrc093/dsh-agent-graph
```

从本地目录（开发）：

```bash
cd /path/to/dsh-agent-graph
npm install && npm run build
dsh plugin --profile web add "$(pwd)"
```

安装或修改后重启 `dsh web`。确认插件行已进入合成树：

```bash
dsh --profile web --dump-config | grep -i agent-graph
```

要求：Node.js `>=22`；DSH profile 具备 `commands` 与 `subagents` 服务；subagent provider 必须支持结构化输出（`spawn` 支持；`acp`、`claude-code`、`codex` 不支持）。

### 更新 / 卸载

```bash
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                    # 卸载
```

## 图定义

```yaml
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`。

## 运行机制

1. **校验与编译**：解析 YAML、检查图、计算上下游映射与规范拓扑序。
2. **调度**：所有 `needs` 已结算（`ok` / `self_handled`）的节点进入就绪队列，最多并行 `concurrency`；未决返工请求的 holder 激活占用同一并发额度。
3. **激活**：以一次性子 agent 启动。prompt 由引擎确定性拼装：`scope` + `prompt`、上游最新交接、账本路径（运行 `README`、本节点分区、decisions/pitfalls 索引）、记录义务，以及适用时的返工请求或重试结论。
4. **交接**：成功后写出 `nodes/<id>/handoff/vN.md`；其 `summary` / `artifacts` / `openIssues` 注入每一个直接下游激活。
5. **记录**：每次激活写入 `nodes/<id>/attempts/NNN.md`；账本的 README、索引、时间线与节点分区在每次状态变化后确定性再生。
6. **结算或失败**：所有节点 `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` 并带完整轨迹，升级路径事后可审。

## 全局账本

```text
.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 工具）：

```json
{
  "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。

```bash
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、环、自环或非正数预算；错误信息会列出全部问题 |

## 开发

```bash
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](https://github.com/wrc093/dsh-agent-graph/issues)。语义改动请补一个聚焦的回归测试并跑上面的检查。文档需要区分「协议保证的行为」与「依赖节点 prompt 与模型行为的部分」。

## 许可

本项目基于 [MIT License](LICENSE) 开源。
