Skip to main content
Glama
README.md
# workflow-sm — 状态机驱动的 MCP 工作流引擎

一个**有状态**的工作流编排 MCP server。用工具调用驱动 agent 按确定的顺序执行流程,顺序是**硬约束**(跳步/乱序/重报都会被拒),状态**落盘持久化**,支持**断点恢复**。

**差异化**:它是唯一一个把执行顺序做成硬约束、可断点恢复、且能跑在**任意 MCP 客户端**里的轻量工作流引擎。不依赖某个特定 agent 框架(不像绑死 Claude Code 的元外壳),也不只是个无状态的配方图书馆(不像只给 YAML 不跟踪执行的方案)。详见 [DESIGN-RATIONALE.md](./docs/DESIGN-RATIONALE.md)。

---

## 它解决什么问题

你有一套流程(比如"调研 → 分析 → 起草 → 保存"或"讨论 → 规划 → 执行 → 审查"),想让 agent **严格按顺序**走,而不是它自己即兴编排、跳步、伪造结果。而且你需要:

- **可恢复** —— 流程跑到一半中断(关机、context 满、crash),能从断点接着跑,不用从头来
- **可约束** —— 顺序由 server 强制,agent 没法绕过(详见 [混沌测试](#测试))
- **可并行** —— 把独立的步骤标记成同组,同时激活,任意顺序完成
- **可重试** —— 给易失败的步骤设上限,失败自动重试,耗尽才放弃
- **可分支** —— agent 可以在 report 时自主决定跳到任意步(向前推进 / 向后回退 / 循环重做),无需任何声明式表达式引擎
- **不绑特定 agent** —— 标准 MCP 协议,Claude Code / Codex / Cline / 任意 MCP 客户端都能接

---

## 五个工具

| 工具 | 干什么 |
|---|---|
| `start_workflow(yaml)` | 开始一个工作流,返回 `run_id` + 第一步指令 |
| `report_step_result(run_id, step_id, output, status?, next_step_id?)` | 报告当前步结果,推进状态机(可选:报告失败触发重试 / 显式跳转到指定步) |
| `get_run_status(run_id)` | 查看某个 run 的当前进度(断点恢复时用) |
| `list_runs(only_incomplete?)` | 列出所有 run(含中断的),用于重启后找没跑完的 |
| `rewind(run_id, to_step_id)` | 把某个 run 倒带回退到指定步,从那里重新跑 |

**核心约束**:`report_step_result` 只能报告"当前活跃集"里的步骤(线性模式 = 当前步;并行模式 = 同组所有活跃步)——跳步、重报、乱序、报不存在步全部被拒。这是状态机的强制顺序保证,在 [混沌测试](#测试) 里实测覆盖。

---

## 三大能力

### 1. 并行组(`group`)
把连续的同 `group` 步骤标记为一组,server 会把它们**同时激活**为 running,你可以任意顺序 report 它们;组内全部完成后才推进到下一步。

```yaml
steps:
  - id: fetch-a
    group: collect      # ← 和 fetch-b 同组,同时开始
  - id: fetch-b
    group: collect
  - id: merge           # ← 组完成后才执行
```

### 2. 重试(`max_attempts`)
给易失败的步骤设上限。agent report `status: "failure"` 时,server 递增尝试次数,未耗尽就让该步重新 running 并提示"第 X/N 次重试";耗尽则该步 `failed`、整个 run `failed`。

```yaml
steps:
  - id: flaky-api
    max_attempts: 3     # ← 最多试 3 次(含首次)
```

### 3. agent 自主跳转(`next_step_id`)
agent 在 report 成功时,可以传 `next_step_id` 显式跳转到任意步骤——**这是条件分支、循环、回退的统一原语,完全由 agent 判断,server 不解释任何表达式**。

```
# report 时传 next_step_id 实现回退重做
report_step_result(run_id, step_id="verify", output="不满意", next_step_id="merge")
# → 跳回 merge 重跑(循环)
```

> 为什么不要表达式引擎?因为 **agent 本身就是最强的求值器**。让 agent 读 output 自己判断该跳哪,比让 server 解释 `when: output.contains("error")` 更灵活、更可靠。这是和 Temporal/Airflow 这类声明式 DAG 引擎**错位竞争**的根基。详见 [DESIGN-RATIONALE.md](./docs/DESIGN-RATIONALE.md)。

---

## 安装

### 1. 依赖
需要 Node.js ≥ 18。

```bash
git clone <repo-url> workflow-sm && cd workflow-sm
npm install
npm run build      # 编译 TypeScript → dist/
```

### 2. 注册到你的 MCP 客户端

**Claude Code**:
```bash
claude mcp add workflow-sm -s user -- node "/绝对路径/dist/server.js"
```

**其他 MCP 客户端**:在客户端的 MCP 配置里加一个 stdio server,command 为 `node`,args 为 `/绝对路径/dist/server.js`。

**Codex**(新版已有 `codex mcp add`,也可手编 `~/.codex/config.toml`):

```toml
[mcp_servers.workflow-sm]
command = "node"
args = ["C:/绝对路径/dist/server.js"]   # Windows 路径用正斜杠
```

> Codex(2026)已有与 Claude Code 同构的 skills(`~/.codex/skills/<name>/SKILL.md`,格式一致、`/skill` 调用)。工作流里绑定的 skill 在 Codex 下同样可用——把 skill 目录复制到 `~/.codex/skills/` 即可。

> ⚠️ **Codex 兼容性实测(2026-08,codex-cli 0.120.0 / 0.147.0)**:
> 1. **0.121+ 存在 MCP 工具注入回归**([issue #19649](https://github.com/openai/codex/issues/19649)):server 正常注册、拉起、握手,但工具不注入线程。**0.120.0 实测可用**(`npm i -g @openai/codex@0.120.0`)。
> 2. **exec 模式会自动取消 MCP 调用**(approval 无人批准即拒绝):加 `--dangerously-bypass-approvals-and-sandbox`;交互式 TUI 则在弹批准时确认即可。
> 3. 自定义 provider(OpenAI 兼容中转)需 `wire_api = "responses"`(0.147 起 chat 协议已移除)。
> 已实测:Codex 0.120 + 国产模型(GLM)驱动 workflow-sm 完整跑通 4 步工作流,看板实时刷新,断点落盘正常。

**Claude Desktop**(编辑 `claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "workflow-sm": {
      "command": "node",
      "args": ["/绝对路径/dist/server.js"]
    }
  }
}
```

> ⚠️ GUI 客户端(Claude Desktop / Cursor)不继承 shell 的 PATH:若用 nvm,`command` 需写 node 的**绝对路径**(如 `~/.nvm/versions/node/v20/bin/node`),否则会 spawn 失败。

> 验证(Claude Code):`claude mcp list` 应显示 `workflow-sm: ... ✔ Connected`。
> 注册后**重启客户端**,工具才会被发现(MCP 在会话开始时发现工具)。

### 3. 状态文件位置

状态持久化在 `~/.workflow-runs/<run_id>.json`(每 run 一份,人眼可读的 JSON)。跨项目共享,server 重启后自动 `loadAll` 恢复全部历史 run——这就是断点恢复。

### 4. 可视化看板(实时)

server 启动时会同时开一个本地 HTTP 看板(默认 `http://127.0.0.1:7777`,端口占用自动 +1)。**在 agent 跑工作流的同时,浏览器打开这个地址就能实时看到进度**——状态一变就自动刷新,无需手动刷新。

```
┌─ Claude Code ────┐  stdio(MCP)   ┌─ workflow-sm server(单进程)─┐  ┌─ 浏览器 ─────┐
│  agent 调工具     │ ──────────────►│  ├─ MCP stdio transport      │  │ 看板 HTML/JS │
└──────────────────┘                │  ├─ HTTP server(:7777)      │◄─│ (实时 SSE)  │
                                    │  └─ WorkflowEngine(内存真相)│◄─│ (可 rewind) │
                                    └──────────────────────────────┘  └─────────────┘
```

**看板能看到**:
- 左栏:所有 run 列表(状态 emoji / 进度 / 相对时间)
- 右栏:选中 run 的步骤详情——✓/▶/✗ 标记、**并行组同色高亮**、**重试计数**、耗时、output(点击展开)
- 当前活跃的步有**脉冲动画**边框,一眼看出"正在跑"

**看板能操作**:
- 🔄 刷新、📋 复制 run_id
- ⏪ rewind:选目标 step,倒带重跑(二次确认)
- 🗑️ 删除:清掉不要的/僵尸 run(内存+磁盘一起删,二次确认)

**状态语义**:侧栏分四组——● 运行中(10 分钟内有更新,带脉冲)/ ⏸️ **中断**(标记 running 但闲置超 10 分钟——没有进程在推进它,可续跑或删除)/ ✗ 失败 / ✓ 已完成。

**实时性**:基于 SSE(Server-Sent Events)。engine 每次状态变更(server 端)→ 推送给所有连着的浏览器,亚秒级延迟。

**找到端口**(server 被 Claude Code 启动时看不到 stderr):
- 看 `~/.workflow-runs/.web-port` 文件(记录最近启动实例的端口)
- 或直接试 `http://127.0.0.1:7777`(占用时会顺延到 7778...)

**多实例规则**(每个 MCP 客户端会话都会拉起自己的 server 实例):
| 实例 | 用途 |
|------|------|
| 会话实例(.web-port 指向的端口) | agent 正在跑的 run 的**实时**画面(SSE 亚秒推送) |
| 常驻查看器(`npm run viewer`) | **自动同步磁盘**的常驻看板(≤30 秒延迟),看所有实例的历史/新 run,会话间隙做 rewind/删除 |

> 查看器模式(`node dist/viewer.js`)每次读接口前重新加载磁盘状态,所以能看到其他实例新写入的 run;普通会话实例不开启此模式(保护自己的内存真相)。

> 看板是**查看 + rewind + 删除**,不能新建/编辑工作流(那是 YAML 文件的事)。它也**只显示 workflow-sm 启动的工作流**——agent 的普通任务(没调 `start_workflow`)不会出现在看板上。MCP stdio 协议完全不受影响——看板挂了也不会影响 agent 调工具。

---

## 使用

### 基本流程(在 agent 对话里)

```
请用 workflow-sm 跑 examples/sample-skill-workflow.yaml:
1. 读这个 YAML 的内容
2. 调用 start_workflow,把 YAML 内容作为 yaml 参数传入
3. 按返回的指令推进:调它说的 skill / 做它说的任务
   → 跑完调 report_step_result(run_id, step_id, output) 报告结果
   → 拿到下一步,重复直到完成
```

agent 会:
1. `start_workflow` → 拿到 run_id 和第一步指令
2. 第一步绑了 skill → 在对话里调用 `/skill-name`(交互式 skill 会问问题,正常答)
3. 跑完把产出 `report_step_result` 回去 → 推进到第二步
4. 重复直到 `🎉 工作流全部完成!`

### 断点恢复(核心特性)

```
跑到一半 → 直接关掉客户端 / 重启电脑(状态已自动落盘)
         ↓
重新打开客户端
         ↓
让 agent: list_runs(only_incomplete=true)
         → 看到 "⏸️ run_xxx | feature-dev-flow | 2/4步 | running"
         ↓
让 agent: get_run_status(run_id="run_xxx")
         → 确认该从第 3 步继续,前两步产出完整保留
         ↓
让 agent: report_step_result(run_id, step_id="第3步id", output="...")
         → 接着跑完
```

### 三个示例工作流

| 文件 | 演示 |
|---|---|
| `examples/sample-workflow.yaml` | 纯自然语言 4 步线性流程(最简单) |
| `examples/sample-skill-workflow.yaml` | 每步绑定真实 Claude Code skill(半自动) |
| `examples/sample-parallel-workflow.yaml` | **并行组 + 重试 + agent goto**(全部能力) |

---

## YAML 语法

```yaml
name: my-flow              # 必填:工作流名
description: 描述           # 可选

steps:                      # 必填:步骤数组
  - id: discuss             #   必填:步骤唯一 id
    skill: gsd-discuss-phase  # 可选:绑定的 skill(server 提示 agent 调用 /skill)
    args: "--analyze"         # 可选:传给 skill 的参数
    group: collect            # 可选:并行组名(连续同名的步同时激活)
    max_attempts: 3           # 可选:失败重试上限(默认 1 = 不重试)
    description: 收集上下文   # 可选
    instruction: 说明文字     # 可选:给 agent 的补充说明

  - id: plan                # 这步没绑 skill → 纯自然语言任务
    instruction: 基于上一步产出做规划
```

**所有新字段都是可选的**。一个不含任何新字段的 YAML,行为和纯线性流程完全一致(向后兼容)。

**两种步骤执行方式**:
- **绑了 `skill`** → server 提示 agent "调用 `/skill-name`",agent 在对话里跑(交互式 skill 会问问题)
- **没绑 skill** → server 给自然语言 `instruction`,agent 自己做

**report 时的高级参数**(全可选):
- `status: "failure"` → 触发该步的重试逻辑(受 `max_attempts` 限制)
- `next_step_id: "xxx"` → 成功后跳转到指定步(分支/循环/回退;仅 success 时生效)

---

## ⚠️ 重要说明

1. **断点恢复能恢复"状态",恢复不了"agent 的思考上下文"** —— 接手的 agent 是全新的,只能靠状态机喂给它的各步产出文本继续。线性、每步独立的流程没问题;需要 agent 跨步记住大量隐式上下文的复杂流程会打折。

2. **skill 是"半自动"的** —— server 管编排顺序,但 skill 的执行始终是 agent 在对话里调。交互式 skill 跑到一半会问你问题,这是 skill 本身的特性。

3. **状态不自动清理** —— `~/.workflow-runs/` 会累积 run 文件。手动删或写个清理脚本即可。

4. **明确不做的事**(见 [DESIGN-RATIONALE.md](./docs/DESIGN-RATIONALE.md)):声明式条件表达式引擎、自动循环语法、超时/定时器/cron、子工作流、数据库、Web UI、自动执行 skill、热加载定义。这些是刻意砍掉以守聚焦的——大多有现成替代(`next_step_id` 替代表达式,Temporal 替代定时器,等等)。

---

## 测试

```bash
npm run build
npm test                      # 一键跑全部 7 套

# 或单独跑:
node test/test-engine.mjs          # 状态机逻辑:17/17
node test/test-agent-chaos.mjs     # 混沌 agent(状态机管住乱来):7/7
node test/test-persistence.mjs     # 持久化 + 断点恢复 + skill 解析:23/23
node test/test-mcp-protocol.mjs    # MCP 协议全链路:12/12
node test/test-retry-goto.mjs      # 重试 + agent 跳转:27/27
node test/test-parallel.mjs        # 并行组:33/33
node test/test-web.mjs             # 可视化看板(HTTP+SSE+rewind):28/28
```

全部应显示 `0 failed`。合计 **147 个用例**。

---

## 项目结构

```
workflow-sm/
├── src/
│   ├── engine.ts              状态机逻辑(纯逻辑,可独立测试)
│   ├── server.ts              MCP server(5 个工具 + 持久化 + 启动看板)
│   └── web.ts                 HTTP+SSE 看板桥接层(零依赖,Node 内置 http)
├── web/
│   └── index.html             看板前端(纯原生 HTML/CSS/JS,暗色主题)
├── dist/                      编译产物(注册到 MCP 用的就是这个)
├── examples/                  三个示例工作流(YAML)
├── test/                      测试脚本
├── docs/                      设计、实验和验证文档
├── artifacts/                 演示产物、发布包与公众号素材
└── LICENSE                    MIT
```

## 如何卸载

```bash
claude mcp remove workflow-sm -s user          # 取消注册(Claude Code)
rm -rf ~/.workflow-runs                         # 删状态文件(可选)
```

## License

MIT — 见 [LICENSE](./LICENSE)。

Maintenance

ActivityMaintained
ResponsivenessNo issues