将 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>
17 KiB
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_SCRIPTSfeature flag 接好注册位;constants/tools.ts的CORE_TOOLS在 flag 开启时含workflow。
2. 关键决策(brainstorming 结论)
- 范围:完整忠实引擎——全部钩子 + schema 结构化输出 + 并发上限(16/1000/4096)+ journaling/resume + token budget + worktree 隔离 + named-workflow 加载 + 进度流到
/workflows。 - 包边界:严格端口适配(依赖倒置)。
packages/workflow-engine/零src/*/builtin-tools运行时导入;只声明端口接口;核心侧提供一个 adapter 模块实现这些接口;tools.ts装配时注入。 - 文件模型:
.claude/workflows/<name>.ts|.js|.mjs脚本文件 → 命名 workflow(Workflow工具name参数解析到它)+ 生成/<name>斜杠命令;/workflows变为实时进度查看器。删除 现有.md/.yaml清单逻辑。 - 执行路径: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.ts(WORKFLOW_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 输入 schema(script/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 的返回,包内明确定义为联合类型:
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)
- 提取
export const meta = { … }——必须是纯字面量(无变量/插值/展开),解析为对象;缺失或非字面量 → 抛错。 - 剥离
export const meta语句。 - 剩余 body(含顶层
return)包进async function(agent, parallel, pipeline, phase, log, workflow, args, budget, Date, Math){ <body> }。 - 以抛异常的 shim 传入
Date(now()/无参new Date()抛)、Math(random()抛)——靠函数参数 shadow 全局,使裸Date.now()命中 shim。这是确定性保障,非密码学级沙箱(与真实引擎意图一致:阻断 resume 破坏性的非确定性)。 - 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 上限 | 抛错 |
| kill(abort) | signal 传播;agent() 检查 signal;workflow 停;task killed;通知 partial |
工具调用层(call)脚本非法 |
直接返回错误给模型(不进后台) |
6. 测试策略
包内全量单测,无需真实 LLM(mock 端口——解耦的核心收益):
engine.test.ts:mockAgentRunner(按 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+usage;schema→解析校验返回对象;处理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 将细化)
- 新建包
packages/workflow-engine/(package.json/tsconfig/类型/端口/常量)。 - 引擎核心:script 包装、concurrency、journal、budget、structuredOutput、namedWorkflows。
- 钩子实现 + runWorkflow 编排 + 进度事件。
- 自包含工具描述符(schema/desc/prompt/result 映射)。
- 包内全量单测。
- 核心侧 adapter + wiring + 句柄构造。
- 迁移现有文件、改
/workflows为进度查看器、改 named-workflow 命令。 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 阶段补一个最小版本。