Files
claude-code/docs/superpowers/specs/2026-06-12-workflow-engine-design.md
claude-code-best d236880bc3 feat(workflow): add workflow engine, /workflows panel, /ultracode skill
将 feat/sdk-backend 分支中 workflow 相关的 20 个 commit 压缩为单 commit:

- 工作流引擎核心:phase / agent / parallel / pipeline 编排原语(packages/workflow-engine/)
- /workflows 面板:三区焦点布局(顶部 run tabs + 左侧 phase 侧栏 + 右侧 agent 列表)
- /ultracode skill:多 agent workflow 编排入口
- 进度存储 / journal / notification 系统
- WorkflowService 生命周期管理 + SentryErrorBoundary
- 脚本沙箱:禁用 dynamic import()、JSON args 防御性归一化
- journal 与 named-workflow 路径统一在 projectRoot
- 错误处理:parallel/pipeline hooks 错误日志、failure routing、semaphore abort
- workflow 工具升级为 core 工具 + PascalCase 命名

Co-Authored-By: glm-5.1 <zai-org@claude-code-best.win>
2026-06-13 20:07:18 +08:00

232 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Workflow Engine — 重建设计
- 日期2026-06-12
- 状态:已通过 brainstorming待 writing-plans
- 范围:把被掏空的「清单推进」版 WorkflowTool 重建为**完整忠实的确定性 JS 脚本编排引擎**,并**独立成包**,解除与核心层的深度依赖。
## 1. 背景与现状
当前 `packages/builtin-tools/src/tools/WorkflowTool/WorkflowTool.ts` 是个被阉割的版本:把 `.claude/workflows/` 里的 `.md`/`.yaml` 解析成清单,靠模型手动调用 `advance` 推进,**没有任何子 agent 编排能力**。
真正的 Workflow 能力是一个**确定性 JS 脚本编排引擎**:后台执行脚本,提供 `agent()`/`parallel()`/`pipeline()`/`phase()`/`log()` 钩子,真正 spawn 子 agent支持 schema 校验、并发上限、journaling/resume、token budget、进度流。
### 可复用的现有基础设施
- `src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts`完整的后台任务生命周期register/complete/fail/kill/skip/retry/orphan 清理)。**完好,复用**。
- `packages/builtin-tools/src/tools/AgentTool/runAgent.ts`:子 agent 执行核心async generator接收 `agentDefinition`+`promptMessages`+`toolUseContext`+`canUseTool`,运行完整 query 循环)。**作为 `agent()` 钩子后端**。
- `assembleToolPool``src/tools.ts`):构建子 agent 工具池。
- `finalizeAgentTool` / `extractTextContent``agentToolUtils.ts`):抽取 agent 最终消息 + usage。
- `WorkflowPermissionRequest.tsx`:权限 UI核心侧 React复用
- `tools.ts` 已用 `WORKFLOW_SCRIPTS` feature flag 接好注册位;`constants/tools.ts``CORE_TOOLS` 在 flag 开启时含 `workflow`
## 2. 关键决策brainstorming 结论)
1. **范围**:完整忠实引擎——全部钩子 + schema 结构化输出 + 并发上限16/1000/4096+ journaling/resume + token budget + worktree 隔离 + named-workflow 加载 + 进度流到 `/workflows`
2. **包边界****严格端口适配(依赖倒置)**。`packages/workflow-engine/``src/*` / `builtin-tools` 运行时导入;只声明端口接口;核心侧提供一个 adapter 模块实现这些接口;`tools.ts` 装配时注入。
3. **文件模型**`.claude/workflows/<name>.ts|.js|.mjs` 脚本文件 → 命名 workflow`Workflow` 工具 `name` 参数解析到它)+ 生成 `/<name>` 斜杠命令;`/workflows` 变为实时进度查看器。**删除** 现有 `.md`/`.yaml` 清单逻辑。
4. **执行路径****async 函数包装 + 信号量 + 注入端口**(方案 A。进程内 async 模型,与 `runAgent` 的 async generator 天然契合,端口可 mock 测试。不用 `vm` 沙箱或 worker 进程。
## 3. 架构与依赖方向
```
┌─────────────────────────────────────────────────────────────┐
│ packages/workflow-engine/ ← 新包,零 src/* 运行时导入 │
│ 声明端口(接口),持有引擎/钩子/并发/journal/budget/schema │
│ + 自包含的 WorkflowTool 描述符zod schema/desc/prompt
└──────────────▲──────────────────────────▲───────────────────┘
│ 实现implements │ 注入DI
┌──────────────┴──────────────────────────┴───────────────────┐
│ src/workflow/ ← 核心侧薄层 │
│ adapter.ts: 用 runAgent/assembleToolPool/LocalWorkflowTask │
│ /AppState 实现端口 │
│ wiring.ts: createWorkflowTool(adapter) → 适配为 Tool │
│ 注册到 tools.tsWORKFLOW_SCRIPTS flag 之后) │
└─────────────────────────────────────────────────────────────┘
```
包**不认识** `buildTool` / `toolUseContext` / `runAgent` / `Message` 类型。仅通过端口接口与不透明 host 句柄对话。
### 端口契约(包内 `ports.ts`
| 端口 | 职责 | 核心侧 adapter 实现 |
|---|---|---|
| `AgentRunner` | `agent()` 后端:`runAgentToResult(params, hostHandle) → AgentRunResult` | 委托 `runAgent` + `assembleToolPool`schema 时注入 StructuredOutput 工具;`finalizeAgentTool` 抽取最终消息 + usage |
| `ProgressEmitter` | `emit(event)` 推进度事件 | 写 `LocalWorkflowTaskState.progress` + `rootSetAppState` |
| `TaskRegistrar` | 后台任务生命周期 + 读 `pendingAgentAction` | 复用 `LocalWorkflowTask` API |
| `JournalStore` | journal 读写(按 runId | 文件 fs`.claude/workflow-runs/<runId>/journal.jsonl`),走端口便于 mock |
| `PermissionGate` | `agent()` 前置权限/取消检查 | abort signal + `pendingAgentAction` |
| `Logger` | 调试日志 + 遥测 | `logForDebugging` / `logEvent` |
**不透明 host 句柄**`HostHandle = { readonly __workflowHost: unique symbol }`。核心侧每次工具调用构造一个句柄(内含 `toolUseContext`/`canUseTool`/`agentId` 等),包内绝不检视,只透传给 `AgentRunner`adapter 把它 cast 回核心上下文。包对核心类型零依赖的唯一缝隙,且是不透明的。
### 包结构
```
packages/workflow-engine/
package.json @claude-code-best/workflow-engine (workspace:*)
tsconfig.json
src/
index.ts 公共导出
ports.ts 端口接口 + HostHandle
types.ts 纯类型WorkflowInput/Run/JournalEntry/ProgressEvent/AgentRunParams…
tool/
WorkflowTool.ts createWorkflowTool(ports) → 自包含描述符
schema.ts 输入 schemascript/name/scriptPath/args/resumeFromRunId/desc/title
constants.ts WORKFLOW_TOOL_NAME 等
engine/
runWorkflow.ts 引擎入口:校验/包装/执行/journal/resume
context.ts 执行上下文(端口/信号量/budget/journal/计数器/host
hooks.ts agent/parallel/pipeline/phase/log/workflow 实现
script.ts meta 字面量提取 + async 包装 + 沙箱 shim
concurrency.ts Semaphore + 上限16 / 1000 总 / 4096 每次调用)
journal.ts hash + 读/写 journal
budget.ts budget 累加器total/spent/remaining
structuredOutput.ts JSON Schema → 结果校验(纯函数)
namedWorkflows.ts name → .claude/workflows/<name>.ts|js|mjs 解析(仅 fs
constants.ts 目录/上限常量
progress/events.ts ProgressEvent 类型 + emit 委托
__tests__/ …
```
核心侧薄层:`src/workflow/adapter.ts` + `src/workflow/wiring.ts``packages/builtin-tools` 从新包 re-export 描述符。
## 4. 引擎内部
### 4.1 钩子语义
| 钩子 | 语义 | 失败行为 |
|---|---|---|
| `agent(prompt, opts?)` | 取信号量 → 查 journal命中即返回缓存→ 调 `AgentRunner` → 写 journal → 返回 | 终态 API 错耗尽重试 → `null`(不抛) |
| `parallel(thunks)` | **屏障**`Promise.all` 所有 thunk每个内部各自过信号量wall-clock = 最慢项 | 单项抛错/agent 错 → 该项 `null`;调用本身永不 reject |
| `pipeline(items, …stages)` | **无屏障**:每项跑 `stage1→stage2→…` 异步链多链并发stage 回调收 `(prevResult, originalItem, index)` | 某 stage 抛错 → 该项 `null`、跳过后续 stage |
| `phase(title)` | 开启新阶段,后续 agent/log 归入该组直到下次 `phase()` | — |
| `log(message)` | 向用户发一行旁白进度 | — |
| `workflow(nameOrRef, args?)` | 内联跑子 workflow返回其返回值共享并发/计数/budget`/workflows` 显示为 `▸ name` 组 | 子 workflow 内再嵌套 → 抛错(仅一层) |
`agent``opts``label``phase`(显式分组)、`schema`JSON Schema`model``isolation:'worktree'``agentType`(自定义子 agent 类型)、`allowedTools`
- 无 schema 返回 `string`;有 schema 返回校验对象;用户 skip / agent 终态死亡 → 返回 `null`
### 4.2 并发与上限(`concurrency.ts`
- `Semaphore` 许可数 = `min(16, cpuCores - 2)``agent()` 取 1。
- 单个 workflow 生命周期**总 agent 数 ≤ 1000** → 超出抛错。
- 单次 `parallel`/`pipeline` 调用 **items ≤ 4096** → 超出抛错(显式错误,不静默截断)。
### 4.3 Journal / Resume`journal.ts`
- journal = 按**执行顺序**的 `{ key, result }` 列表,存 `.claude/workflow-runs/<runId>/journal.jsonl`
- `key` = `hash(prompt + canonical(opts 去掉 label/phase 等纯展示字段))`
- 命中:`agent()` 先算 key与 journal 下一项 key 比对 → **匹配则返回缓存并前进**,不匹配则丢弃后续 journal、现场重跑。
- 因 JS 去掉 `Date.now`/`random` 后确定,执行顺序确定 → 自然得到「最长未变前缀命中、首个发散点之后全重跑」。
- `resumeFromRunId`:载入该 run 的 journal 重放。脚本源码 hash 一致 → 100% 命中;脚本改动 → 全重跑。脚本 hash 存入 run 记录。
### 4.4 Budget`budget.ts`
- `budget.total`:来自用户 `+500k` 式 turn 级 token 指令,由 **host/turn 上下文注入**adapter 从 turn 的 token 指令读取,经 `HostHandle` 传入),**不是** 工具 input 参数。无指令则 `null`
- `budget.spent()`:本 turn 所有 agent 输出 token 之和(`AgentRunResult.usage`adapter 从 subagent usage 填)。
- `budget.remaining()``max(0, total - spent)`,无 total 则 `Infinity`
- **硬上限**`spent()``total` 后,`agent()` 抛错。预算是主循环与 workflow 共享池。
### 4.7 AgentRunResult 类型(`types.ts`
`AgentRunner.runAgentToResult` 的返回,包内明确定义为联合类型:
```ts
type AgentRunResult =
| { kind: 'ok'; output: string | object; usage: { outputTokens: number } }
| { kind: 'skipped' } // 用户 skip → agent() 返回 null
| { kind: 'dead' } // 终态 API 错耗尽重试 → agent() 返回 null
```
`output``string`(无 schema或已校验对象有 schema`agent()` 据此映射:`ok`→返回 output`skipped`/`dead`→返回 `null`
### 4.5 脚本包装与沙箱(`script.ts`
1. 提取 `export const meta = { … }`——**必须是纯字面量**(无变量/插值/展开),解析为对象;缺失或非字面量 → 抛错。
2. 剥离 `export const meta` 语句。
3. 剩余 body含顶层 `return`)包进 `async function(agent, parallel, pipeline, phase, log, workflow, args, budget, Date, Math){ <body> }`
4. 以**抛异常的 shim** 传入 `Date``now()`/无参 `new Date()` 抛)、`Math``random()` 抛)——靠函数参数 shadow 全局,使裸 `Date.now()` 命中 shim。这是确定性保障非密码学级沙箱与真实引擎意图一致阻断 resume 破坏性的非确定性)。
5. meta 的 `phases` 可用于进度预声明(可选)。
### 4.6 进度事件(`progress/events.ts`
`ProgressEmitter.emit(event)` 类型:`run_started``phase_started/done``agent_started/done{label,phase,result摘要}``log``run_done{returnValue/status}`。adapter 写入 task 进度结构 + AppState`/workflows` 视图消费。
## 5. 错误处理
| 场景 | 行为 |
|---|---|
| 脚本无 `meta` / `meta` 非字面量 / 语法错 | 引擎抛错 → task `failed` → 通知带错误信息 |
| `Date.now`/`Math.random`/`new Date()` | shim 抛 → 冒泡为脚本错误 → task failed |
| `agent()` 终态 API 错(重试耗尽) | 返回 `null`**不杀** workflow |
| `parallel`/`pipeline` 单项抛错 | 该项 `null`workflow 继续 |
| budget 耗尽 | `agent()` 抛错(脚本可 try/catch |
| 并发/1000/4096 上限 | 抛错 |
| killabort | signal 传播;`agent()` 检查 signalworkflow 停task `killed`;通知 partial |
| 工具调用层(`call`)脚本非法 | 直接返回错误给模型(不进后台) |
## 6. 测试策略
包内全量单测,**无需真实 LLM**mock 端口——解耦的核心收益):
- `engine.test.ts`mock `AgentRunner`(按 prompt 返回预设结果)端到端跑脚本,断言返回值 + 进度事件序列。
- `hooks.test.ts`parallel 单项错→null、pipeline 无屏障顺序、agent schema 校验、skip/dead→null。
- `concurrency.test.ts`信号量限并发、1000/4096 上限抛错。
- `journal.test.ts`hash 稳定、resume 命中前缀、脚本变更全重跑、中途发散重跑尾部。
- `budget.test.ts`spent 累加、触顶抛错。
- `script.test.ts`meta 字面量提取、非字面量/语法错、shim 抛。
- `structuredOutput.test.ts``namedWorkflows.test.ts`
核心侧最小冒烟adapter 用 `runAgent` 真接线的重 mock 测试wiring 注册测试。重量级逻辑都在包内。可选:`tests/integration/` 加一个 workflow tool-chain 集成测试feature-gated
## 7. 核心侧实现
### 7.1 adapter`src/workflow/adapter.ts`
`createWorkflowAdapter()` 返回端口实现:
- **AgentRunner.runAgentToResult(params, hostHandle)**cast 句柄→`{toolUseContext, canUseTool, assistantMessage}`;按 `params.agentType` 从 registry 解析 agentDefinition缺省=通用 workflow 子 agent`assembleToolPool`;有 schema→注入 StructuredOutput 工具+系统指令;调 `runAgent` 收消息→`finalizeAgentTool` 抽 text+usageschema→解析校验返回对象处理 `pendingAgentAction`(skip)→`null`、终态死亡→`null`;返回 `{kind:'ok', text/object, usage}`
- **ProgressEmitter**:写 `LocalWorkflowTaskState.progress` + `rootSetAppState`
- **TaskRegistrar**:复用现有 `registerLocalWorkflowTask/complete/fail/kill` + 读 `pendingAgentAction`
- **JournalStore / Logger / PermissionGate**fs / `logForDebugging`+`logEvent` / abort+pendingAction。
### 7.2 wiring`src/workflow/wiring.ts`
- `createWorkflowTool()`:建 adapter → 调包的 `createWorkflowTool(adapter)` 得描述符 → 包成 `buildTool` 兼容 `Tool` 返回。
- `tools.ts``const WorkflowTool = feature('WORKFLOW_SCRIPTS') ? require('./workflow/wiring.js').createWorkflowTool() : null`(替换现有清单版)。
`call` 流程校验脚本inline/file/named 解析)→ meta 校验失败直接返错给模型 → 持久化脚本 + 算 hash → resume 则载入 run+journal → 注册后台 task → **立即返回 `{runId, scriptPath}`** → 脱离执行引擎、流进度 → 完成时 complete + 通知(返回值/错误)。
## 8. 现有文件迁移
| 文件 | 处理 |
|---|---|
| `builtin-tools/.../WorkflowTool/WorkflowTool.ts`(清单版) | 删除,逻辑移入新包 |
| `constants.ts`WORKFLOW_TOOL_NAME | 移入包 `tool/constants.ts`core 侧 re-export |
| `WorkflowPermissionRequest.tsx`React UI | 移到 `src/workflow/`(依赖 src 权限组件,属核心侧) |
| `createWorkflowCommand.ts`.md/.yaml 扫描) | 改为扫 `.ts/.js/.mjs` → 生成 `/<name>` 命令,调用时以脚本启动引擎 |
| `bundled/index.ts`no-op | 保留为包的 bundled-workflow 扩展点 |
| `src/utils/workflowRuns.ts`(清单记录) | 重写为 run+journal 模型(或并入包 JournalStore |
| `src/commands/workflows/index.ts` | 改为**实时进度查看器**,复用 `WorkflowDetailDialog.tsx` |
| `src/tasks.ts` LocalWorkflowTask 门控 | 保持不变 |
| `constants/tools.ts` CORE_TOOLS 含 `workflow` | 保持 |
## 9. 工作分解writing-plans 将细化)
1. 新建包 `packages/workflow-engine/`package.json/tsconfig/类型/端口/常量)。
2. 引擎核心script 包装、concurrency、journal、budget、structuredOutput、namedWorkflows。
3. 钩子实现 + runWorkflow 编排 + 进度事件。
4. 自包含工具描述符schema/desc/prompt/result 映射)。
5. 包内全量单测。
6. 核心侧 adapter + wiring + 句柄构造。
7. 迁移现有文件、改 `/workflows` 为进度查看器、改 named-workflow 命令。
8. `bun run precheck` 零错误;手动 dev 冒烟。
## 10. 非目标 / 风险
- **非密码学沙箱**:函数参数 shadow 全局 `Date`/`Math``globalThis.Date` 仍可达。可接受——目标是阻断 resume 破坏性的非确定性,不是隔离恶意代码。若未来需强隔离再上 `vm`/worker方案 B/C
- **resume 正确性依赖确定性执行**:用户脚本若绕过 shim 用 `globalThis.Date` 制造非确定性resume 可能命中错缓存。属可接受的边界,文档提示。
- **预算共享语义**`budget.spent()` 与主循环的 token 计数共享,需 adapter 正确上报 subagent usage若 provider 不报 usage 则 budget 降级为 `Infinity`
- **StructuredOutput 工具**:核心侧需存在/实现一个按 JSON Schema 强制结构化输出的子 agent 工具(注入 + 解析。若当前无现成实现wiring 阶段补一个最小版本。