Claude Code Dynamic Workflow:一个可恢复的多 Agent Harness Runtime
从模型可见接口、受限 JS VM、scheduler、resume journal、progress projection 到 failure semantics,拆解 Claude Code Dynamic Workflow 的整体设计。
这篇文章面向 agent / harness 工程师,重点放在 Dynamic Workflow 的执行模型、状态边界和可借鉴的 runtime 设计。以下分析基于本机安装的 Claude Code 2.1.193 行为观察。
Executive Summary
- Dynamic Workflow 可以理解成 Claude Code 内置的 deterministic multi-agent harness runtime。
- 主 Claude 负责生成、选择或修改 workflow script;runtime 负责执行脚本、调度 subagents、记录进度和恢复状态。
- 模型可见入口是
Workflow/RunWorkflow,输入包括script、name、args、scriptPath、resumeFromRunId。 - workflow script 运行在受限 JavaScript VM 中,runtime 注入
agent()、parallel()、pipeline()、workflow()、budget、phase()、log()、args。 scriptPath把一次性 tool call 变成可编辑 artifact;Claude 可以直接 Edit / Write 这个脚本,再用同一路径重跑。resumeFromRunId依赖 journal 和 agent-call chain key。缓存 key 大致是sha256(prevKey + prompt + canonical(optsSubset)),控制流变化会自然影响后续复用。pipeline()是默认推荐 primitive,语义是 item-level streaming;parallel()是 barrier,只有需要全量上游结果时才适合。- progress 来自 runtime 对
workflow_phase、workflow_agent、workflow_log的 task projection,不能解读成模型内部“思考百分比”。 - 安全边界来自 concurrency cap、agent lifetime cap、array boundary cap、token budget hard ceiling、stall retry、structured output retry、abort / skip / retry 控制。
- Agent Team 更像交互式协作 surface;Dynamic Workflow 更像可保存、可恢复、可观察的 orchestration program。
Mental Model
Dynamic Workflow 至少有三层:
- Model-visible layer:主 Claude 能看到
Workflow工具,以及 workflow 的输入输出 contract。 - Execution layer:受限 JS VM 执行 workflow script,并通过 host hooks 调用 subagent runtime。
- Projection layer:runtime 把 phase、agent、log 投影到 local workflow task,UI 再展示
/workflows、后台任务、进度条和控制按钮。
这个三层模型很重要。workflow script 承载 deterministic control plane;文件系统、网络、进程、MCP 等实际能力来自 agent() 背后的 subagent main loop、工具系统、权限系统、MCP 连接和 task registry。
Workflow Tool Contract
模型能调用的是一个 workflow tool。用户侧通常看到 Workflow,内部别名里也有 RunWorkflow。核心输入如下:
| 字段 | 含义 |
|---|---|
script | 自包含 workflow script。必须以 export const meta = ... 开头。 |
name | 运行一个已注册 workflow,可以来自内置、项目 .claude/workflows/ 或 plugin。 |
args | 原样暴露给脚本的全局变量。应该传 JSON 值,避免把数组/对象先 stringify。 |
scriptPath | 从磁盘读取 workflow script。优先级高于 script 和 name。 |
resumeFromRunId | 从同一 session 内的既有 workflow run 恢复。 |
输出里通常包含:
| 字段 | 含义 |
|---|---|
status | async_launched 或 remote_launched。 |
taskId | 本地 task registry 中的 workflow task id。 |
taskType | 本地 workflow 是 local_workflow,远端场景可表现为 remote_agent。 |
runId | 本地 workflow run id,形如 wf_...,用于 resume。 |
scriptPath | 本次持久化后的脚本路径。后续迭代直接改这个文件。 |
transcriptDir | subagent transcript 输出目录。 |
sessionUrl | 远端 cloud session URL,仅远端场景有。 |
warning | 非阻断提醒,例如远端会基于已 push 分支运行。 |
error | 语法检查或输入校验失败时的错误。 |
scriptPath 的存在改变了 workflow 的生命周期。普通 tool call 的输入会淹没在对话上下文里;workflow script 会落盘,成为可编辑、可审查、可再次执行的 artifact。
Script Contract And Registry
每个 workflow script 都要先声明 meta:
export const meta = {
name: "review-changes",
description: "Review changed files across multiple independent dimensions.",
whenToUse: "Use for high-recall review of a change set.",
phases: [
{ title: "Scope", detail: "Pin diff and review surface" },
{ title: "Find", detail: "Run independent finder angles" },
{ title: "Verify", detail: "Adversarially verify candidates" },
{ title: "Synthesize", detail: "Merge, rank, and cap findings" },
],
}
// Script body starts here.
meta 的约束比普通配置更严格:
export const meta = ...必须是脚本第一条语句。meta必须是 pure literal。- 不能引用变量、函数调用、spread、computed key、template interpolation。
meta.name和meta.description是必填非空字符串。phases用于 UI 预注册阶段;脚本里phase("...")或agent(..., { phase })会投影到对应 group。
workflow registry 有几类来源:
- Built-in workflows:当前版本能看到
code-review和deep-research这类内置 workflow。 - Project workflows:项目内
.claude/workflows/*.js。 - User workflows:用户级 workflow 目录。
- Plugin workflows:plugin manifest 可声明
workflowsPath或workflowsPaths。
脚本有大小上限,当前观察到的 cap 是 512 KiB。这避免把 workflow 变成无限制代码包,也让 tool input、落盘、parse、approval dialog 的成本保持可控。
Restricted JavaScript VM
workflow script 使用 JavaScript 语法。TypeScript 类型注解、interface、generic 都会 parse 失败。
VM 内可用的关键全局对象:
| 名称 | 作用 |
|---|---|
agent(prompt, opts) | 启动 subagent,并把 subagent final response 作为返回值。 |
parallel(thunks) | 并发执行一组 thunk,等待全部 settled 后返回数组。 |
pipeline(items, ...stages) | 让每个 item 独立流过多个 stage,没有 stage 间全局 barrier。 |
workflow(nameOrRef, args) | 在当前 workflow 内调用一个子 workflow。 |
phase(title) | 设置当前阶段,影响后续 agent 的 UI 分组。 |
log(message) | 写入 workflow log。 |
budget | 暴露 token target、已花费、剩余量。 |
args | tool input 传入的参数值。 |
console | 输出被接到 workflow log。 |
VM 的限制来自可恢复性和 host boundary 安全:
- 无 Node.js
fs、child_process、import()这类直接宿主能力。 Date.now()、Math.random()、无参new Date()不可用。codeGeneration.strings和codeGeneration.wasm关闭。- 跨 VM 边界传值时会 sanitize:函数被丢弃,
__proto__被跳过,循环引用被处理。 - 单个数组穿越边界有 cap,当前观察是
4096。 - workflow 返回值不能是 function,最终会被 clone / JSON 化为可序列化结果。
这些约束让脚本更接近 deterministic control plane。时间戳、随机性、外部状态应该通过 args 或 subagent 工具调用显式进入,避免让脚本本身隐式漂移。
Host Hook Architecture
runtime 会先 parse meta 和 script body,再创建 VM context。VM 内的函数由 host hooks 包装,调用时会跨回宿主 runtime。
agent() 的返回值会回到脚本。子 Agent 被提示:它的 final text 会作为函数返回值,而非给人看的聊天回复。这个提示很关键,否则 subagent 容易输出 “Done” 或一段面向用户的解释,导致 workflow 难以 parse。
有 schema 时,runtime 会给 subagent 加 StructuredOutput 工具要求。subagent 必须通过工具调用返回结构化对象,validation 失败会触发重试。
Orchestration Primitives
agent(prompt, opts)
agent() 是 workflow 的基本执行单元。
const result = await agent("Review this diff for correctness bugs.", {
label: "correctness",
phase: "Find",
schema: FINDINGS_SCHEMA,
effort: "high",
})
常见 opts:
| 选项 | 影响 |
|---|---|
label | UI 展示用 label,不进入 resume key。 |
phase | UI 分组用 phase,不进入 resume key。 |
schema | 要求 StructuredOutput,进入 resume key。 |
model | 覆盖本次 subagent model,进入 resume key。 |
effort | 覆盖 reasoning effort,进入 resume key。 |
isolation | 例如 worktree,进入 resume key。 |
agentType | 使用自定义 agent definition,进入 resume key。 |
stallMs | stall detection 超时,不进入 resume key。 |
无 schema 时,agent() 返回 final text string。有 schema 时,返回 validated object。用户 skip、某些终局 API error 或 pipeline/parallel 分支失败时,结果可能是 null,脚本应显式过滤。
parallel(thunks)
parallel() 接收 thunk 数组,直接传 promise 数组会失败:
const results = await parallel([
() => agent("Find correctness bugs.", { label: "correctness", schema: FINDINGS }),
() => agent("Find test gaps.", { label: "tests", schema: FINDINGS }),
() => agent("Find compatibility risks.", { label: "compat", schema: FINDINGS }),
])
const findings = results.filter(Boolean).flatMap((r) => r.findings)
它是 barrier。所有 thunk settle 后,调用才返回。某个 slot throw 时,通常会被记录成 workflow_log,对应结果为 null。这种语义适合“下一步必须看到完整上游集合”的场景,例如 dedupe、全量排序、全局 early exit。
pipeline(items, ...stages)
pipeline() 是 Dynamic Workflow 里更值得注意的 primitive。
const verified = await pipeline(
DIMENSIONS,
(dimension) =>
agent(dimension.prompt, {
label: `find:${dimension.key}`,
phase: "Find",
schema: FINDINGS_SCHEMA,
}),
(review, originalDimension, index) =>
parallel(
review.findings.map((finding) => () =>
agent(verifyPrompt(finding), {
label: `verify:${index}:${finding.file}`,
phase: "Verify",
schema: VERDICT_SCHEMA,
})
)
)
)
pipeline() 没有阶段间全局 barrier。item A 的 stage 1 完成后可以立即进入 stage 2;item B 可能仍在 stage 1。对 code review、research、audit 来说,这比“先 parallel 全部 finder,再 parallel 全部 verifier”的 wall-clock 更好。
workflow(nameOrRef, args)
脚本里可以调用另一个 workflow:
const research = await workflow("deep-research", {
question: "How does this API migration affect SDK users?",
})
子 workflow 共享父 workflow 的 concurrency cap、agent counter、abort signal 和 token budget。子 workflow 的 agents 会在 /workflows 中显示为 child group。嵌套限制为一层,child workflow 里再调用 workflow() 会失败。
Scheduler And Backpressure
Dynamic Workflow 可以让主 Claude 写循环和 fan-out,但 runtime 会限制吞吐:
| 边界 | 当前观察到的行为 |
|---|---|
| 本地 agent 并发 | 大致 min(16, cpu cores - 2),下限 2。 |
| 远端 agent 并发 | 有独立 cap,当前观察到常量为 50。 |
| 单 workflow lifetime agent cap | 1000。 |
| 单个 array boundary cap | 4096。 |
| token budget | hard ceiling,达到后不再启动新 agent。 |
| in-flight agents | budget hit 后,已在飞的结果会尽量保留。 |
budget 暴露给脚本:
while (budget.total && budget.remaining() > 50_000) {
const round = await agent("Find more distinct bugs.", { schema: BUGS_SCHEMA })
log(`${round.bugs.length} candidates, ${Math.round(budget.remaining() / 1000)}k tokens left`)
}
budget.remaining() 在没有 token target 时是 Infinity。所以 budget-driven loop 必须 guard budget.total。runtime 也有 1000 agent cap 作为 runaway-loop backstop。
Subagent Execution Model
默认 subagent type 是内置 workflow-subagent。它拥有 session 工具能力,但会被禁止使用一些会破坏返回值协议或导致递归编排的工具,例如直接向用户发消息、再启动某些 agent/workflow 入口等。
subagent prompt 的关键语义:
- final text 是
agent()的返回值。 - 如果要求 JSON,应返回 raw JSON,不要 code fence 和解释。
- 不要用面向用户的确认语,例如 “Done”。
- 有 schema 时,必须调用 StructuredOutput 工具。
agentType 可以切换到已有 custom agent:
const plan = await agent("Design the migration plan.", {
agentType: "software-architect",
schema: PLAN_SCHEMA,
})
schema 会和 custom agent prompt 组合。换句话说,Dynamic Workflow 可以复用 agent registry,同时把返回值边界收紧成 structured output。
isolation: "worktree" 会给 subagent 创建独立 git worktree。适合并行修改文件的场景,成本包括 worktree 创建、磁盘占用、最终 cleanup 或保留 review。远端 isolation 有单独路径和 UI 文案;当前本机 build 中,本地 agent call 对 isolation: "remote" 可能直接拒绝,因此写 workflow 时要按环境能力降级。
Structured Output As Harness Boundary
对 harness 来说,schema 是 data plane。
无 schema:
const text = await agent("Summarize the migration risk.")
有 schema:
const VERDICT_SCHEMA = {
type: "object",
required: ["isReal", "confidence", "reason"],
properties: {
isReal: { type: "boolean" },
confidence: { enum: ["high", "medium", "low"] },
reason: { type: "string" },
},
}
const verdict = await agent("Try to refute this finding.", {
schema: VERDICT_SCHEMA,
})
schema 让 downstream 代码可以做:
- dedupe by file / line / claim id
- filter by confidence
- majority vote
- rank by severity
- join source metadata
- cap final report length
- collect caveats and dropped coverage
这是 multi-agent harness 的常见分界线:LLM 负责非结构化探索和判断,workflow code 负责集合操作、控制流和可审计的筛选逻辑。
Resume Journal And Cache Key
每个本地 workflow run 都会关联一个 journal。当前观察到 journal 是 journal.jsonl,主要记录两类事件:
{"type":"started","key":"v2:...","agentId":"..."}
{"type":"result","key":"v2:...","agentId":"...","result":{...}}
resume 时,runtime 会 load journal,构建:
results: Map<key, resultEvent>started: Map<key, startedEvents[]>
agent call 的 resume key 比 prompt hash 更细。它更接近下面这个结构:
keyN = "v2:" + sha256(
previousKey + "\0" +
prompt + "\0" +
canonicalize({
schema,
model,
effort,
isolation,
agentType
})
)
进入 key 的 opts subset:
| opts 字段 | 进入 resume key |
|---|---|
schema | yes |
model | yes |
effort | yes |
isolation | yes |
agentType | yes |
label | no |
phase | no |
stallMs | no |
chain key 的效果很实际:如果你在前面插入一个新的 agent(),后面的 previousKey 会变化,后续 agent 的 key 会自然失效。这样可以避免脚本控制流变化后误复用旧结果。
这个设计支持“最长未变化前缀复用”。同一个 script、同一个 args、同样 agent call 顺序时,resume 可以直接命中已完成结果。修改中间控制流后,从第一个受影响调用开始重跑。
Progress Projection
进度条和 /workflows 视图来自 runtime 的状态投影。runtime 会产生三类进度实体:
| 类型 | 典型字段 |
|---|---|
workflow_phase | index, title, kind |
workflow_agent | index, label, phaseIndex, state, tokens, toolCalls, durationMs, error |
workflow_log | message |
task registry 对 workflow_phase 和 workflow_agent 做 upsert,key 类似 type:index。log 则追加,并有截断策略。task 会派生:
agentCounttotalTokenstotalToolCallsprogressVersion- per-agent state
- phase grouping
所以进度百分比更接近“已完成 agent / 已知 agent group / phase 状态”的执行进度。它不能代表模型内部认知百分比。模型本身无法可靠报告“思考完成 37%”;runtime 可以可靠知道哪些 agent 已启动、哪些完成、哪些失败、当前 token 和 tool call 计数是多少。
Failure Semantics
Dynamic Workflow 的工程价值很大一部分来自失败语义。常见情况如下:
| 情况 | 行为 |
|---|---|
单个 agent() terminal API error | 记录 workflow log,通常返回 null 给分支。 |
parallel() slot throw | 对应 slot 变 null,整体 parallel() 返回数组。 |
pipeline() item throw | 该 item 变 null,剩余 stages 跳过。 |
| user skip agent | 当前 agent abort,结果为 null,progress 标记 skipped。 |
| user retry agent | 当前 attempt abort,进入 retry attempt。 |
| stall | 长时间无进展后标记 stalled,并重试到 cap。 |
| structured output validation failure | 继续要求 StructuredOutput,直到 retry cap。 |
| degraded / throttle-like response | backoff 后重试一次。 |
| budget exceeded | 不再启动新的 agent,已在飞 agent 的结果尽量保留。 |
| workflow abort | 根据原因进入 paused、killed 或 failed 状态。 |
这和普通对话里的“让模型继续试试”不同。workflow runtime 把失败变成可记录、可投影、可恢复的状态。脚本作者可以把 null 当作数据处理,例如 .filter(Boolean)、记录 dropped coverage、进入下一轮补扫。
Built-in Workflow Patterns
当前版本能看到两个很能说明设计意图的内置 workflow。
code-review
大致模式:
它把 code review 拆成 recall-oriented finder、candidate verification、gap sweep、final synthesis。高 effort 下增加角度数量、候选数量和 sweep,避免只依赖单个 agent “更努力地想”。
deep-research
大致模式:
这个 workflow 更像 research harness:多角度搜索、URL 去重、抽取 falsifiable claims、对 claim 做 adversarial verification、最后生成带 caveats 的报告。
这些内置 workflow 暗示了 Dynamic Workflow 的目标场景:高 recall、可验证、多阶段、可结构化汇总。
Harness Patterns
以下 pattern 都能直接映射到 Dynamic Workflow 的 primitives。
| Pattern | 结构 |
|---|---|
| Exhaustive review | finder pool -> dedupe -> verifier panel -> synthesize |
| Deep research | search angles -> fetch/extract -> claim verification -> cited report |
| Incident diagnosis | logs/diff/config/history 并行收集 -> root cause voting -> fix plan |
| Migration review | module inventory -> compatibility check -> rollout plan -> risk table |
| Large repo tracing | config/protocol/handler/UI/test 多线调查 -> data-flow synthesis |
| Loop until dry | finder rounds until N consecutive rounds produce no fresh candidates |
| Budget-scaled audit | 根据 budget.total 自动决定 finder 数量、round 数和 verifier 票数 |
| Judge panel | 多方案生成 -> independent scoring -> winner synthesis |
| Perspective-diverse verify | correctness/security/perf/repro 不同 lens 投票 |
一个 compact 的 loop-until-dry skeleton:
const seen = new Set()
const confirmed = []
let dryRounds = 0
while (dryRounds < 2 && (!budget.total || budget.remaining() > 50_000)) {
const found = await parallel(
FINDERS.map((finder) => () =>
agent(finder.prompt, { phase: "Find", schema: FINDINGS_SCHEMA })
)
)
const fresh = found
.filter(Boolean)
.flatMap((r) => r.findings)
.filter((f) => !seen.has(keyOf(f)))
if (fresh.length === 0) {
dryRounds++
log(`dry round ${dryRounds}/2`)
continue
}
dryRounds = 0
fresh.forEach((f) => seen.add(keyOf(f)))
const judged = await parallel(
fresh.map((finding) => () =>
parallel(
["correctness", "security", "repro"].map((lens) => () =>
agent(verifyPrompt(finding, lens), {
phase: "Verify",
schema: VERDICT_SCHEMA,
})
)
).then((votes) => ({ finding, votes: votes.filter(Boolean) }))
)
)
confirmed.push(...judged.filter(Boolean).filter(isMajorityReal))
}
return { confirmed, seen: seen.size }
这个例子展示了三件事:
- LLM 负责找候选和判断。
- JS 负责 loop、dedupe、vote、budget guard。
- runtime 负责并发、进度、重试、恢复和上限。
Agent Team Comparison
Agent Team 和 Dynamic Workflow 都能多 agent。区别主要在 coordination surface。
| 维度 | Agent Team | Dynamic Workflow |
|---|---|---|
| 协调者 | 主 Claude / 用户 | workflow script |
| 控制流 | 对话和自然语言 | if、for、parallel()、pipeline() |
| 返回值 | message / transcript / summary | string / structured object / JSON-like result |
| 状态 | agent/task 状态 | phase、agent、log、journal、snapshot |
| 恢复 | 依赖继续对话和 transcript 理解 | scriptPath + resumeFromRunId |
| 复用 | 协作现场 | workflow file、project workflow、plugin workflow |
| 适合 | 模糊探索、人工插手频繁 | 流程明确、可重复、需结构化和恢复 |
实际使用上可以组合:先用 Agent Team 探索任务边界,等流程稳定后沉淀成 workflow。也可以让 Dynamic Workflow 在某个 stage 使用 custom agentType,把已有 agent 专长纳入脚本化 harness。
Design Lessons For Harness Engineers
Dynamic Workflow 里最值得借鉴的设计点:
-
Deterministic control plane 让编排逻辑运行在受限、可检查、可重放的语言子集里。
-
Structured data plane 用 schema 把 agent output 变成程序变量,避免只留下自然语言 transcript。
-
Artifact-based iteration
scriptPath让 workflow 可以被 review、edit、rerun、resume。 -
Journaled resume
started和result事件足以支持 agent-call 级别复用。chain key 让控制流变化自动失效。 -
Observable projection runtime 发 phase/agent/log,UI 只做 task projection。这样 CLI、后台任务、notification、SDK event 都能共享同一状态源。
-
Backpressure first concurrency cap、agent cap、array cap、budget hard ceiling 都属于 harness 核心边界。
-
Failure as data branch failure 返回
null、log 记录原因、progress 标记状态。脚本可以继续处理剩余集合。 -
Composable but bounded nesting
workflow()支持子流程复用,同时限制一层嵌套,避免 orchestration recursion 失控。 -
Human controls stay in the loop pause、kill、skip、retry 直接作用于 runtime task / agent controller,无需让模型“理解用户想停哪个”。
-
Budget-aware autonomy
budget暴露给脚本后,workflow 可以实现“按预算扩展深度”,支持动态调整 fan-out。
Closing
Dynamic Workflow 把 Claude Code 的多 agent 能力组织成一个可执行 runtime:主 Claude 写控制流,JS VM 执行控制流,subagents 执行具体认知任务,journal 支持恢复,task projection 支持观察和控制。
对 agent / harness 工程师来说,重点不在“能同时启动几个 agent”。重点在这一组边界:deterministic script、structured output、bounded scheduler、journaled resume、observable task state、budget-aware loops。
这些边界合在一起,才让多 agent 协作从一次性的对话现场,进入可调试、可恢复、可复用的工程系统。