---
url: /wave-agent/specs/multi-agent/workflow.md
description: 确定性多子代理编排：pipeline/parallel 控制流、受控脚本沙箱、预算硬上限与失败上报
---

# 功能规格说明：Workflow — 确定性多子 Agent 编排

**创建日期**：2026-06-07

## 用户场景与测试 *（必填）*

### 用户故事：运行工作流探索代码库（优先级：P1）

作为用户，我希望要求 agent"使用工作流"探索代码库，以便它并行编排多个子 agent（扫描、逐文件分析、综合），而不是在一个上下文中顺序执行。

**为什么是这个优先级**：这是核心用例——单个对话轮次无法协调的大规模并行多 agent 工作。

**独立测试**：创建示例项目，发送消息要求 agent"use a workflow to explore this project"，验证 Workflow 工具被调用，脚本包含 `agent()`、`pipeline()` 和 `phase()`，最终结果是综合的概览。

**验收场景**：

1. **假设**有包含源文件的项目目录，**当**用户说"use a workflow to explore this project"时，**则** agent 调用 Workflow 工具，使用包含 `agent()`、`pipeline()` 和 `phase()` 的 JS 脚本。
2. **假设**有运行中的工作流，**当**工作流完成时，**则** `<task-notification>` 被注入对话中（meta 消息，UI 消息流隐藏、仅模型可见，见 `core/background-task-notification`），agent 报告结果。
3. **假设**有包含多个阶段的工作流，**当**用户运行 `/workflows` 时，**则**系统列出运行记录，包含名称、状态、agent 计数和 token 使用量。

***

### 用户故事：选择加入强制执行（优先级：P1）

作为用户，我希望 Workflow 工具仅在我显式请求多 agent 编排时才被调用，以便 agent 不会为简单任务默默生成数十个 agent。

**为什么是这个优先级**：没有选择加入，工作流可能在用户不知情的情况下消耗大量 token。

**独立测试**：发送不请求工作流的简单问题，验证 agent 使用 Agent 工具或直接回答——不使用 Workflow 工具。

**验收场景**：

1. **假设**用户说"find all TODO comments"，**当** agent 处理请求时，**则** agent 不调用 Workflow 工具（使用 Agent 工具或直接回答）。
2. **假设**用户说"use a workflow to find all TODO comments"，**当** agent 处理请求时，**则** agent 调用 Workflow 工具。
3. **假设**用户调用一个内嵌工作流脚本的技能（`/技能名 <参数>`），**当**斜杠命令执行时，**则** Workflow 工具被调用并附带该技能的脚本。

***

### 用户故事：受控的脚本执行环境与确定性（优先级：P1）

作为用户，我希望工作流脚本只能触达编排 API，既不读写文件、不访问网络、不读进程信息，也不引入外部模块，以便我在批量编排时不必担心脚本越权，也不会因为脚本里的说明文字被误判而反复重写。

**为什么是这个优先级**：工作流会在后台跑几十个 agent 并写仓库；脚本一旦能触达文件系统或网络，风险面远超一次普通对话。

**独立测试**：提交一份正文含外部模块引入与宿主全局访问的脚本，验证被拒绝且错误指向具体位置；再提交一份只在传给 `agent()` 的提示词里出现同类字样的脚本，验证正常启动。

**验收场景**：

1. **假设**脚本正文出现真实的外部模块引入或宿主全局访问（动态引入、`require()`、进程对象、网络请求、宿主全局），**当**校验并执行脚本时，**则**脚本被拒绝并指出违规位置，且不产生任何副作用。
2. **假设**同类字样只出现在传给 `agent()` 的提示词字符串或注释里，**当**校验脚本时，**则**校验通过——字符串与注释不参与禁令判定。
3. **假设**脚本调用当前时间或随机数接口，**当**执行到该调用时，**则**抛出确定性错误（"会破坏恢复"语义），不静默返回时间或随机值。
4. **假设**脚本含 TypeScript 语法、括号或引号不配对，**当**校验脚本时，**则**给出含出错行与列（或代码片段）的诊断，脚本不执行。
5. **假设** `meta` 不是纯字面量（含变量引用、函数调用、展开、模板插值、计算键），**当**校验脚本时，**则**被拒绝并指明违规类别（例如模板插值、计算键）。
6. **假设**脚本试图经注入的函数或对象间接取到宿主 realm 的能力（例如对注入的 API 取 `constructor` 再构造代码、对宿主调用返回的对象或注入的数据走原型链），**当**执行到该处时，**则**沙箱拒绝从字符串生成代码、脚本拿不到宿主全局——注入的可调用对象与跨边界的数据都换成沙箱 realm 内的副本。
7. **假设** `meta` 不是脚本的第一条语句，**当**校验脚本时，**则**被拒绝并提示 `export const meta = { name, description, phases }` 必须是第一条语句。

***

### 用户故事：预算与规模硬上限（优先级：P1）

作为用户，我希望用自然语言给出本轮的 token 上限（例如"用 500k token 做这件事"），并且工作流到顶就停、agent 总数也有上限，以便批量任务不会在我不注意时烧掉无法预估的成本。

**为什么是这个优先级**：单个子 agent 的实测消耗在百万 token 量级，没有上限时唯一的刹车是人肉盯着屏幕。

**独立测试**：发一条带 token 目标的消息让 agent 跑工作流，验证脚本读到该目标、`spent()` 随子代理消耗增长、达到上限后新增 agent 调用被拒且已完成结果保留；再看启动回执是否带上限；最后让一个运行越过规模预警阈值，验证只在 `/workflows` 的运行行与详情里出现「运行过大」标记，而模型上下文里没有任何对应通知。

**验收场景**：

1. **假设**用户消息里给出了 token 目标（`+500k`、`use 500k tokens` 等形式），**当**本轮工作流运行时，**则**脚本里 `budget.total` 为该目标、`budget.spent()` 为本轮已消耗的输出 token 数、`budget.remaining()` 为 `max(0, total - spent)`，且语义为硬上限而非建议值。
2. **假设**用户没有给出 token 目标，**当**脚本读取 `budget` 时，**则** `total` 为 `null`、`remaining()` 为 `Infinity`；工具描述给出的"循环到预算用尽"写法必须自带硬迭代上限。
3. **假设**消耗已达上限，**当**脚本再次发起 `agent()` 时，**则**抛出预算超限错误（文案含已用/上限，并说明在飞 agent 会跑完且其结果保留），此后不再新增 agent。
4. **假设**脚本累计发起第 1001 个 `agent()`，**当**该调用发生时，**则**抛出 agent 数上限错误，文案提示"通常意味着未设预算的循环没有终止"。
5. **假设**同一轮内主循环与多个工作流并发消耗，**当**脚本读取 `spent()` 时，**则**读到的是本轮共享的用量，而不是单个工作流自己的用量。
6. **假设**脚本需要自查规模（自己限流，或如实说明砍掉了多少），**当**脚本调用 `getAgentCount()` 与 `getStats()` 时，**则**分别读到本运行已发起的 agent 数与本运行的计数明细（完成 / 失败 / 跳过 / 空结果 / 工具调用数）；两者与完成通知的 `<usage>` 同源。
7. **假设**运行中的累计 token 或 agent 数越过规模预警阈值（25 个 agent / 1.5M tokens），**当**越过时，**则**只在 `/workflows` 的运行行与详情里标出「运行过大」（给出当前规模、阈值与该界面既有的停止方式），**且不向模型上下文注入任何通知**——是否停止由人决定；每个运行最多标记一次，不重复刷屏。**2026-10-16 口径变更**：原先此场景要求注入一条 `<task-notification>` 预警通知，文案还带「Stop it if this is not what the user asked for: TaskStop …」。理由是「模型看不到运行列表，需要被提醒」，实际后果是**每一次完整的多阶段调研都必然被打断**——完整一轮 ≈ 1 scope + 5 search + 15 fetch + 12 claims × 3 votes + 1 synth ≈ 58 个 agent，必然越过 25-agents 阈值，于是模型每次都会停下来向用户请示「要不要继续烧」，用户叫停后 Verify/Synthesize 两阶段根本没跑（2026-10-16 实测：6.3M tokens / 6 agents 触发，停在 Fetch 之后，24.3M 子代理 token 只换回 5 份未综合的中间结果；当时的驱动案例是内置 deep-research，它已随本次改动移出仓库——等 wave 有内建 web search 再上）。对齐 Claude Code：同阈值在 CC 中只作为工作流列表标记呈现（footer「Large workflow · /workflows to stop」+ 遥测），模型始终不知情，成本靠用户给的 token 目标与脚本自身上界控制，不靠"模型自觉停"。
8. **假设**工作流启动，**当**模型读启动回执时，**则**回执里给出本轮的约束信息——agent 数上限与 token 目标（用户未给目标时明说未设）——使模型在启动时就能向用户交代约束。

***

### 用户故事：写脚本前能拿到完整 API 参考（优先级：P1）

作为用户，我希望 agent 在动笔前能拿到脚本 API 的完整参考（钩子签名、`meta` 形状、质量模式与示例），而不是靠猜签名，以便一次把脚本写对、不浪费探针轮次。

**为什么是这个优先级**：签名猜错的表现是"返回值莫名是 undefined/空值"，排查成本极高（实测两次探针才定位到是回调参数位置搞错）。

**独立测试**：在不加载参考内容的前提下让 agent 写一个多阶段脚本，验证工具描述里已含 `meta` 形状、`agent()`/`pipeline()`/`parallel()` 的回调与返回值约定以及一个可运行示例；再验证描述给出了加载完整参考的入口。

**验收场景**：

1. **假设**完整参考可按需加载，**当** Workflow 工具描述发给模型时（精简与完整两种模式均适用），**则**描述至少包含 `meta` 必填字段与 `phases` 元素形状（`{title, detail?}`）、`pipeline` 阶段回调各参数的含义、`parallel` 是屏障语义、`agent()` 的返回值约定，并附"先加载参考取完整内容"的指针。
2. **假设**完整参考不可按需加载，**当**工具描述生成时，**则**完整参考（钩子清单、恢复语义、质量模式与示例）内联进描述，信息不缺失。
3. **假设** `meta.phases` 里存在形状非法的元素（如字符串数组 `["改写","校验"]`），**当**解析 `meta` 时，**则**非法元素被丢弃；全部非法时 `phases` 视为不存在，启动回执与进度面板不出现空的 `Phases:` 行。

***

### 用户故事：从日志恢复工作流（优先级：P2）

作为用户，我希望从中断处恢复停止的工作流，以便已完成的 agent 不重新运行且不浪费 token。

**为什么是这个优先级**：恢复为被中断或需要在运行中编辑的长工作流节省大量 token。

**独立测试**：运行工作流，在执行中停止，使用 `resumeFromRunId` 调用 Workflow，验证缓存的 agent 结果立即返回，只有新/更改的 agent 实时运行。

**验收场景**：

1. **假设**工作流运行在停止前完成了 10 个 agent 中的 5 个，**当**用户使用 `resumeFromRunId` 恢复时，**则** agent 0-4 立即返回缓存结果，agent 5-9 实时运行。
2. **假设**有恢复的工作流，**当**脚本相同且参数相同时，**则** 100% 的 agent 调用返回缓存结果（完全缓存命中）。
3. **假设**同一台机器上先后或并行存在多个会话——包括同一工作目录下的多个会话（`/clear` 之后、桌面端同一项目的两个面板、两个 CLI 会话），**当**在某个会话里列出运行（`/workflows`）或用 `resumeFromRunId` 恢复时，**则**只看到并能命中本会话自己的运行；其他工作目录的运行、以及同一工作目录下其他会话的运行既不出现在列表里，也不能被恢复（运行目录按「工作目录 + 会话」两级隔离：既不落在所有项目共享的全局目录，也不落在同一工作目录的其他会话里）。恢复会话（`wave -r <id>`）时会话 id 不变，因此该会话此前的运行仍然可见、可续——与 Claude Code 一致（`resumeFromRunId` 的语义是 `Same-session only`）。

***

### 用户故事：把脚本存进库并按名字复用（优先级：P2）

作为用户，我希望把跑通的工作流脚本按名字存进库、以后在新会话里直接按名字调用，以便不必让模型重写一遍、也不必去翻 run 目录里的绝对路径。

**为什么是这个优先级**：在此之前的对齐里，脚本活不过它自己的那一次运行——脚本确实被持久化到 run 目录的 `script.js`，但那条路径按会话再按 runId 分叉，模型无从发现、人也要翻磁盘。脚本库解决的是「脚本从哪来」，与运行隔离是两件事。

**独立测试**：跑通一个脚本 → 在 `/workflows` 详情里存为 `code-review`（project scope）→ 同一工作目录新开一个会话 → 调用 `Workflow({name:"code-review"})` → 按库里的副本运行；再用 user scope 存同名脚本 → 在另一个工作目录的会话里按名可用。

**验收场景**：

1. **假设**要解析名字，**当**按名调用时，**则**按「项目级 `<workdir>/.wave/workflows/<name>.js` > 用户级 `~/.wave/workflows/<name>.js`」的顺序查找，同名时项目副本胜出；两处都没有时报 `Workflow "<name>" not found` 并列出当前可用的名字。库目录不做向上搜索（与技能的目录发现一致），也不读 `.claude/workflows`。
2. **假设**一个运行已结束，**当**用户在 CLI 的 `/workflows` 运行详情里按 `s`，**则**打开保存对话框：输入名字、用 `Tab` 在 `project` ⇄ `user` 之间切换 scope、确认覆盖；保存内容是该 run 目录里 `script.js` 的**原文**（逐字节复制，不重新序列化、不注入或改写 `meta`）。IDE 三端（VS Code / JetBrains / 桌面端）的 `/workflows` 详情本轮不提供保存动作，按名调用两端一致（见边界情况「保存入口本轮只在 CLI」）。
3. **假设**用户输入的名字含空格或大写，**当**保存执行时，**则**文件名按 `toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")` 规范化，为空时回退 `workflow`，写成 `<name>.js`；对话里回报的是规范化后的名字与落盘绝对路径。
4. **假设**目标文件已存在且未确认覆盖，**当**保存执行时，**则**失败并原样给出 `Dynamic workflow "<name>" already exists at <path>. Use a different name or overwrite.`（对话框从这条消息反解路径展示）；确认覆盖后写入成功。
5. **假设**写入落盘，**当**保存 project scope 时用 `wx`（已存在即失败），**当**保存 user scope 时用原子写 + 写前存在检查——两档都不允许「先删后写」留下的中间态。
6. **假设**按名调用成功，**当**脚本开始执行时，**则**照旧把它复制进本会话的运行目录（`<…>/workflows/<会话>/<runId>/script.js`）再执行；`/workflows` 列表与 `resumeFromRunId` 仍然只看本会话的运行——库改变的是脚本来源，不改变运行的会话级隔离。
7. **假设**同时给出 `script` / `scriptPath` / `name` 中的两个或更多，**当**工具被调用时，**则**报错说明三者只能给一个（不做静默择一）。
8. **假设**名字文件不可读、或脚本 `meta` 非法，**当**按名调用时，**则**报出具体原因（路径与校验失败原因），不回退到空脚本或默认脚本继续跑。

***

### 用户故事：调用插件携带的工作流（优先级：P2）

作为插件作者，我希望把工作流脚本随插件一起分发、让用户按 `<plugin>:<name>` 调用，以便插件能提供「一次装好就能跑」的编排，而不是在技能正文里教模型拼 `WAVE_PLUGIN_ROOT` 的绝对路径。

**为什么是这个优先级**：插件的其它六类内容都是按名字寻址的（`<pluginName>:<name>`），只有工作流要模型自己拼路径——路径式虽然能跑，但模型发现不了、也不是插件作者该关心的事。

**独立测试**：做一个带 `.wave-plugin/plugin.json` 与 `workflows/scan.js` 的插件 → 安装启用 → 调用 `Workflow({name:"my-plugin:scan"})` 能跑起来；卸载插件后同一调用立刻报 `not found`。

**验收场景**：

1. **假设**插件根目录下有 `workflows/*.js`，**当**插件加载时，**则**逐个解析为插件工作流（不认 `.ts` / `.mjs`），每个文件都要通过与内联脚本相同的 `meta` 校验（acorn 解析 + 纯字面量）；单个文件非法只跳过它自己，不影响该插件的其它工作流与其它六类内容，也不使插件加载失败。
2. **假设**文件 `workflows/scan.js`，**当**注册名字时，**则**键为 `<pluginName>:scan`——名字取文件名（去掉 `.js`），不取 `meta.name`；`meta.name` 只用于展示（`/workflows` 列表行与紧凑行）。
3. **假设**插件被卸载、禁用或就地重载，**当**换装完成，**则**该插件贡献的工作流名字同步增删（与六类能力的增删同批），不留指向旧版本目录的残留。
4. **假设**插件升级后版本目录变了，**当**按名字调用时，**则**解析到当前已启用版本里的那份脚本；已经跑起来的旧运行不受影响——它的脚本副本在自己的 run 目录里，`resumeFromRunId` 续的也是那份副本。
5. **假设**用户/项目脚本库里也有同名脚本，**当**按名调用时，**则**两者不冲突：插件来源必须写成 `<plugin>:<name>`，库来源写裸名字。
6. **假设**技能的 `allowed-tools` 里写 `Workflow(my-plugin:scan)`，**当**模型以这个名字调用时，**则**规则命中并直接放行（括号规则支持非 Bash、非路径工具，且工具名/规则名允许 `:`）；同一处修复让 `Agent(<子代理>)` 这类带参规则也生效。括号里用**逗号分隔的多个名字**同样成立（`Agent(a, b, c)` 命中其中任意一个，未列出的名字不命中）——Claude Code 生态的插件就是这样一条规则授权整组子代理的，照搬过来必须能跑。
7. **假设**名字不存在（插件已禁用、或插件里没这个文件），**当**调用时，**则**报 `Workflow "<plugin>:<name>" not found` 并列出可用名字，不退化成整工具失败。

***

### 用户故事：来自 agent 的结构化输出（优先级：P2）

作为用户，我希望工作流 agent 返回匹配 schema 的结构化 JSON，以便下游阶段可以可靠地处理结果而无需脆弱的文本解析。

**为什么是这个优先级**：多阶段管道需要阶段之间的机器可读数据流。

**独立测试**：调用 `agent('List all files', {schema: {type: 'object', properties: {files: {type: 'array'}}, required: ['files']}})`，验证结果是经过验证的对象而非字符串。

**验收场景**：

1. **假设** agent 调用带有 `opts.schema`，**当**子 agent 完成时，**则**结果是匹配 schema 的经过验证的对象；该约束通过"注入结构化输出工具 + 提示词约定 + 对话内催办"实现，**不通过强制 `tool_choice`**，因此开启 thinking 的模型也不会因此报错。
2. **假设**子 agent 本轮没有调用结构化输出工具，**当**该轮结束时，**则**系统在子 agent 对话内追加一条催办消息并重试，重试上限为 5 次。
3. **假设**重试次数用尽仍拿不到通过校验的输出，**当** `agent()` 返回时，**则**抛出错误并在文案里带上最后一次校验失败的摘要，而不是返回 `null` 或降级为文本里抠 JSON。
4. **假设**传入的 schema 本身非法或不可满足（例如根不是 object、存在不可能取值的子结构），**当** `agent()` 被调用时，**则**在启动子 agent 之前就报错，不消耗 token。
5. **假设** agent 调用不带 `opts.schema`，**当**子 agent 完成时，**则**结果是 agent 的最终文本字符串。
6. **假设**请求层需要强制 `tool_choice` 而当前模型开启了 thinking，**当**请求构建时，**则**强制 `tool_choice` 降级为 `auto` 并留一条日志，请求不因该冲突失败。

***

### 用户故事：停止运行并如实上报失败（优先级：P2）

作为用户，我希望中止一个正在跑的工作流时能真的停下来，并且完成通知如实区分成功、失败与跳过，以便我不会把"跑完了"当成"跑成了"。

**为什么是这个优先级**：没有停止通道时唯一的刹车是让用户去 TUI 里手点；而把失败当成功会直接污染下游结论。

**独立测试**：让工作流跑起来后请求停止，验证运行终止、在飞 agent 被取消、后台任务状态为 killed、且不再启动新的 agent；再制造一个失败 agent，验证完成通知里的失败计数与失败明细；再让脚本 `return` 一个对象，验证完成通知里带上该返回值、超长时截断并给出状态文件路径；最后在运行中途结束进程，重开会话验证该运行不再显示为"运行中"。

**验收场景**：

1. **假设**工作流在后台运行，**当**模型用启动回执或完成通知里给出的后台任务标识调用停止工具时，**则**该运行被终止、在飞 agent 全部取消、任务状态变为 killed。
2. **假设**某个 `agent()` 子代理以终止性错误结束（例如 API 错误），**当**该调用返回时，**则**脚本收到可捕获的异常（可自行 try/catch 兜底），而不是与"用户跳过"混同的 `null`。
3. **假设**用户跳过了某个 agent，**当**该调用返回时，**则**返回 `null`——这是唯一返回 `null` 的情形。
4. **假设**工作流结束，**当**注入完成通知时，**则**标题只说明结局（`completed` / `failed` / `aborted`，失败时带错误文案）而**不含**任何计数，计数以独立的 `<usage>` 块给出：`agent_count`（发起过的 agent 数）、`agents_done`、`agents_error`、`agents_skipped`、`agents_empty_result`、`subagent_tokens`、`tool_uses`、`duration_ms`；此外带失败项明细与恢复指引。
5. **假设**运行被终止，**当**终止生效后，**则**在飞子代理的中止信号被触发、不再启动新的子代理，运行收尾时不再新增结果文件。
6. **假设**工作流以失败结束，**当**模型读取通知时，**则**通知里包含逐 agent 结果文件的位置，便于模型先查再下结论，而不是假设缓存结果非空。
7. **假设**脚本自己处理了某个失败（例如在 `catch` 里放弃该 item），**当**运行结束时，**则**脚本可以用 `recordFailure(消息)` 把该失败写进本运行的失败明细，`getFailures()` 读到的列表里包含它，且该记录不额外产生进度日志；这条明细与 `pipeline()`/`parallel()` 记录的失败出现在同一处。
8. **假设**进程退出时（关闭宿主、`Ctrl-C`、崩溃）有工作流在跑，**当**本会话再次加载运行列表时，**则**这些运行不再显示为"运行中"，而是就地标为已中断、说明是由进程退出打断，并给出用 `resumeFromRunId` 接着跑的指引；该提示对本会话每次中断只出现一次。判定依据：运行只存在于启动它的进程里——持久化状态为"运行中"、却不在当前进程内存中的运行，必定已被打断（本规格不要求跨进程接管正在跑的运行）。
9. **假设**脚本以 `return` 给出了返回值，**当**注入完成通知时，**则**该返回值随通知一起交给模型（`<result>` 块：字符串原样、对象按 JSON 序列化），使模型不必先读文件就能直接向用户交代结论（对齐 Claude Code：它的 Workflow 调用把脚本返回值当工具结果返回）；返回值超过上限（`MAX_RESULT_CHARS_IN_SUMMARY = 8000` 字符）时截断并在截断处标注运行状态文件（`run-state.json`）的完整路径；脚本没有返回值、或运行以失败/中止结束时，通知里不出现 `<result>` 块。返回值属交付与展示，不参与预算判定与规模统计。返回值是脚本自由输出的文本（报告对象、甚至它抓来的网页正文），而通知是用 `标签正则` 读回来的，因此其中每一个标签起始符都转义为实体（`&lt;`）——否则一个含 `</result>` 的返回值会提前闭合该块，一个含 `<status>failed</status>` 的返回值会让成功的运行被读成失败。

***

### 用户故事：工作流管理对话框（IDE 与 CLI）（优先级：P2）

作为 IDE 或 CLI 用户，我希望通过 `/workflows` 斜杠命令打开对话框查看工作流运行详情（名称、状态、阶段、agent 数、token、经过时间），并能停止运行中的工作流，以便在工作流运行时监控多 agent 编排。

**为什么是这个优先级**：两个宿主都有 `/workflows`，但此前各写各的——CLI 的列表行给的是 runId 前缀与阶段箭头行、详情里没有失败明细与脚本自己 `log()` 的行，IDE 侧的字段又是另一套。同一个运行在两个宿主里看到不同的东西，会让人怀疑哪一边是准的，因此对齐的对象是**信息模型**（字段与顺序、零值省略规则）而不是某一个渲染器。信息密度对齐 Claude Code 的 `Dynamic workflows` 对话框：列表行给状态、名称与时长/agent/token，详情给出阶段列表与脚本日志行。

**独立测试**：在 IDE 中输入 `/workflows` 验证弹出对话框列出运行、进入详情看到阶段与脚本日志行、点停止终止运行；在 CLI 中输入 `/workflows` 验证同一批字段出现在列表行与详情里、`k` 停止、`Esc` 返回；再只保留一条运行时重开 IDE 对话框，验证跳过列表直进该运行详情；最后让一个运行的 agent 数或 token 越过规模预警阈值，验证两个宿主的列表行与详情都出现「运行过大」标记，且模型上下文里没有对应通知。

**验收场景**：

1. **假设** 存在工作流运行，**当** 用户在 IDE 或 CLI 输入 `/workflows`，**则** 打开工作流管理对话框，按开始时间倒序列出每个运行；每行给出状态（IDE 为带无障碍名的状态色圆点，CLI 为状态色文字）、工作流名称（取 `meta.name`，缺失或为空时回退 `meta.description`）以及 meta（经过时间、agent 数、token 数——值为 0 的项不占位），行的 label 不再带 runId 前缀，也不再显示阶段箭头行；IDE 标题栏同时给出 `N 运行中` / `M 已完成` 副标题（计数为 0 的一项不显示）。
2. **假设** 当前会话只有一个工作流运行，**当** 用户在 IDE 打开对话框，**则** 直接进入该运行的详情，不再先显示只有一行的列表；该对话框从详情返回时若总共只有这一条运行，直接关闭对话框。CLI 不跳过列表——终端里 Enter 只是一次按键，而列表本身就是"还有没有别的运行"的上下文——选中行仍按 Enter 进详情。
3. **假设** 对话框已打开且工作流有阶段，**当** 用户选中某个运行，**则** 详情视图显示 runId、描述、状态、起止时间、运行时长、agent 数、token、脚本路径、错误（若有）、失败明细（若有）、阶段列表（每阶段标题/agent 数/token/经过时间），以及脚本 `log()` 写出的日志行（若有）；没有内容的区块不出现（不留空的阶段/日志区块）；CLI 的日志区块只取最后 10 行，与它的输出区块同一约定（Ink 面板不滚动）。
4. **假设** 选中运行中的工作流，**当** 用户点击停止（IDE）或按 `k`（CLI），**则** 通过 `stopWorkflowRun` 请求终止该运行，列表随后反映新状态。
5. **假设** 工作流后台任务状态变化，**当** 客户端收到 `backgroundTasksChange` 通知，**则** 自动调用 `getWorkflowRuns` 刷新运行列表并推送 `updateWorkflowRuns` 给 webview / 刷新 CLI 的运行状态。
6. **假设** 某运行的累计 agent 数或 token 越过规模预警阈值，**当** 用户打开 `/workflows`，**则** 该运行在列表行与详情里带「运行过大」标记（IDE 为带无障碍名的警示标记，CLI 为警示文字），并给出该界面既有的停止方式（IDE 停止按钮 / CLI `k`）；标记只表示规模、不改 `running` 状态语义，也不注入模型上下文（见「预算与规模硬上限」场景 7）；阈值判定与标记只依赖运行快照里已有的 agent 数与 token 数，webview 不直连 SDK 的工作流内部模块。

***

### 用户故事：在后台任务面板里看到工作流进度（优先级：P2）

作为用户，我希望在 `/tasks` 面板里选中一个工作流任务时能看到它的阶段与消耗进度而不是空输出，以便不切到 `/workflows` 就能确认它在推进。

**为什么是这个优先级**：工作流以"后台任务"身份出现在 `/tasks`（与 Bash 后台命令、后台子代理同一列表），但它的进度只走 `/workflows`——同一个任务在一个面板里是活的、在另一个面板里是空的。用户按 `/tasks` 读到空输出会把正常推进误判成卡住（实测：一次多阶段调研运行 18 分钟里，IDE 面板的 OUTPUT 恒为「(无输出)」，因为它只调 `phase()`、从没调 `log()`；CLI 面板更彻底——工作流任务的 stdout 为空，连 OUTPUT 区块都不渲染，详情区只剩任务头部字段）。

**独立测试**：启动一个多阶段工作流（脚本不必调用 `log()`），在运行中与结束后分别在 IDE 与 CLI 打开 `/tasks` 并选中该工作流任务，验证详情区显示阶段列表（每阶段标题/agent 数/token/经过时间）与运行级 agent 数、token、起止与时长；再打开 `/workflows` 验证同一运行的字段与之一致。

**验收场景**：

1. **假设** 有运行中的工作流，**当** 用户在 `/tasks` 里选中该工作流任务，**则** 详情区展示该运行的阶段列表（标题、agent 数、token、经过时间）与运行级 agent 数、token、起止与时长、脚本路径，并随进度事件刷新；这一块不依赖脚本是否调用 `log()`，字段与顺序和同一运行的 `/workflows` 详情一致（IDE 两处由同一个进度块组件渲染，CLI 两处按同一批字段渲染）。
2. **假设** 工作流已结束（completed/failed/aborted），**当** 用户在 `/tasks` 里选中它，**则** 仍能看到上述进度与最终状态、失败时的错误文案与失败明细，而不是空输出或只剩任务头部字段。
3. **假设** 该运行已不在当前会话的运行列表里（例如会话已被 `/clear` 清走），**当** 用户选中对应的后台任务，**则** 详情区退化为该面板原本的内容（IDE 为文本输出区、CLI 为任务头部字段），不报错、不空白。

***

### 用户故事：工具调用的紧凑显示（优先级：P2）

作为用户，我希望对话流里 Workflow 调用的折叠行显示工作流的描述，以便不展开就能知道这次编排要做什么。

**为什么是这个优先级**：折叠行是工作流调用在消息流中的唯一常显信息；显示脚本内部的标识符（`meta.name` 这类 slug）对人读不友好。

**独立测试**：让 agent 调用 Workflow 并传入带 `meta.description` 的内联脚本，验证对话流中该次调用的折叠行显示该 description。

**验收场景**：

1. **假设** `script` 参数是含合法 `meta` 的脚本，**当** 生成该次调用的紧凑文本时，**则** 取 `meta.description`（剔除控制字符后原样显示，不截断），而不是 `meta.name`。
2. **假设** 脚本解析不出合法 `meta`，**当** 生成紧凑文本时，**则** 回退为脚本首个非空行：超过 80 个字符时截断并追加 `…`，脚本还有后续行时再追加 `… +N lines`。
3. **假设** 只传 `scriptPath`，**当** 生成紧凑文本时，**则** 取脚本文件名（basename），而不是完整路径。
4. **假设** 取不到任何脚本内容（既无 `script`/`scriptPath`，或脚本为空、纯空白），**当** 生成紧凑文本时，**则** 紧凑文本为空串——只显示工具名，不追加参数文本，也不出现 `undefined`/`null`。

***

### 边界情况

* **脚本运行环境是白名单**：脚本在独立沙箱上下文里执行，只能看到编排 API（`agent`、`parallel`、`pipeline`、`phase`、`log`、`args`、`budget`、`getFailures`、`recordFailure`、`getAgentCount`、`getStats`）与显式注入的基础设施（控制台、计时器）；模块引入、进程对象、网络请求、宿主全局一概不可达。注入的可调用对象与跨边界的数据（含 `args`、`budget` 视图、`getFailures()` / `getStats()` 的返回值、宿主调用的返回值与错误）都在沙箱 realm 内重建，脚本不能经原型链借到宿主 realm 的构造器。
* **禁令判定只看代码结构**：字符串字面量、模板字符串与注释里的任何字样都不参与判定——提示词里写"不要用 `fs.readFileSync`"这类说明文字不会导致脚本被拒。
* **确定性接口在运行时拒绝**：当前时间与随机数接口由运行时替身抛错（文案说明会破坏恢复），而不是靠文本匹配拦截。
* **`meta` 必须是纯字面量且是第一条语句**：变量引用、函数调用、展开、模板插值、计算键都会被拒，并给出具体违规类别。
* **`meta.phases` 形状非法即丢弃**：只接受带字符串 `title` 的元素（可带 `detail`/`model`）；全非法时视为未提供，渲染不回退为空行。
* **`pipeline` 首阶段的首参就是该 item**：每个阶段回调收到 `(prevResult, originalItem, index)`，首阶段的 `prevResult` 即该 item（不是 `undefined`）；阶段抛错只把该 item 降为 `null` 并跳过其后续阶段。
* **超出 agent 限制**：如果脚本生成超过 1000 个 agent，第 1001 个 agent() 调用抛出错误。
* **超出预算**：token 目标取自用户消息里给出的目标（如 `+500k`）；消耗达到上限后，进一步的 agent() 调用抛出，在飞 agent 跑完且其结果保留。预算不再是 `args` 里的魔术键。
* **记账户径**：单个 agent 的消耗以 usage 里的总量字段计（该字段已含缓存命中），不再叠加缓存字段；该数字同时用于预算判定与完成通知。
* **运行中中止**：如果用户或模型停止工作流，所有飞行中的 agent 被取消，运行状态设置为"aborted"/"killed"，此后不再写工作区。
* **残留的"运行中"必须收敛**：运行只活在启动它的进程里，所以进程退出（宿主关闭、`Ctrl-C`、崩溃）后在磁盘上留着的"运行中"是过时状态——加载运行列表时须就地改为已中断（带进程退出说明），不再向用户或模型谎报还在跑。
* **子 agent 中的 Workflow 工具**：Workflow 工具在子 agent 中被禁止（防止无限递归）。
* **脚本与运行目录按「工作目录 + 会话」隔离**：每次 Workflow 调用都把脚本持久化到本会话在本工作目录下的运行目录，即使执行失败；`/workflows` 列表与 `resumeFromRunId` 都只看这一个目录，因此同一工作目录下另开一个会话既看不到、也续不上本会话的运行（见「从日志恢复工作流」场景 3）。运行状态（`run-state.json`）与日志（`journal.jsonl`）和脚本同处这个运行目录（`<…>/workflows/<会话>/<runId>/`）：列表加载与恢复续跑都按这个位置找它们，多嵌套一层会让已结束的运行对下一个进程不可见、让续跑读不到日志而把全部 agent 重跑一遍。
* **紧凑文本是纯展示层的**：它不进模型上下文、不影响脚本校验与执行；脚本非法时工具照常报错，紧凑文本只是降级为原始脚本文本。
* **紧凑文本生成对解析失败必须安全降级**：脚本为空、缺 `meta`、`meta` 字段缺失或类型不对、整段只有注释等情况下都不得抛错，一律走首行回退。
* **`/tasks` 与 `/workflows` 读同一份运行数据**：工作流后台任务的 id 就是 runId，两个面板按这个 id 关联同一个运行，面板只做渲染、不持有第二份进度状态；因此也不给工作流任务引入 `outputPath` 文本日志文件——它的进度本来就以结构化状态存在（对齐 Claude Code：其工作流任务同样在构造时创建 `.output` 文件却在运行中不写入，进度走任务对象上的结构化字段，仅完成时向该文件落一份终态快照）。
* **脚本 `log()` 行进运行状态，不进任务输出文件**：`log()` 的行随运行状态一起带给两个面板（同样对齐 Claude Code 把脚本日志挂在任务对象上、而非灌进输出文件的做法），因此它们要进 `run-state.json` 才能在下一个进程里仍可见；日志行按上限截断（沿用 Claude Code 的量级：累计超过 1000 行时丢弃最老的、保留最近 500 行），避免长跑工作流把运行状态撑到不可读。
* **两个渲染器共用一套信息模型，不共用组件**：IDE（webview，VS Code / JetBrains / 桌面端共用）与 CLI（Ink）各有一套渲染代码——一个渲染 DOM、一个渲染终端文本，无法共用组件——因此对齐的对象是信息模型：`/workflows` 列表行的字段与顺序、详情字段集合与顺序、`/tasks` 里同一运行的进度字段，以及零值省略规则；两端的测试按同一批字段断言，避免"改了一边忘了另一边"。渲染差异只允许来自载体能力：CLI 的日志区块只取最后 10 行（面板不滚动）、状态用状态色文字而非色点、保留可见列表并以 Enter 进详情（不跳过单条运行）。
* **脚本的最终返回值随完成通知交给模型**：脚本 `return` 的对象写进 `run-state.json` 的 `result` 字段（`/workflows` 的运行快照里也带它），同时随完成通知以 `<result>` 块发给模型——Claude Code 的 Workflow 调用把返回值当工具结果返回，wave 是"启动即返回"的后台形态，于是用完成通知补上这一步；超过 8000 字符时截断并给出状态文件路径，避免一次研究把上下文灌满。
* **三个脚本入口互斥、汇入同一个解析步**：`script`（内联）/ `scriptPath`（绝对路径）/ `name`（按名）最多给一个，多给即报错，不做静默择一；`name` 里带 `:` 视为插件来源、不带即库来源。解析出脚本文本后的校验、运行目录落盘、执行与上报对三个入口完全一致——不因为脚本来自库里或插件里就多一条特权路径。
* **库只改变脚本来源，不改变运行的会话级隔离**：「脚本与运行目录按工作目录 + 会话隔离」这一条仍然成立——库让下一个会话能拿到同一段脚本，但拿不到上一个会话的运行；`resumeFromRunId` 依旧只在会话内可命中。
* **插件工作流的名字取文件名**：注册名是 `<pluginName>:<文件名去 .js>`，`meta.name` 只作展示，两者不一致时不报错、以文件名寻址（插件作者改名靠改文件名，与 CC 一致）。
* **保存入口本轮只在 CLI**：脚本库的落盘语义（目录、命名、冲突、scope、原文复制）是 SDK 层的单一实现，CLI 的 `/workflows` 详情直接调用它；IDE 三端的 `/workflows` 详情本轮只保留查看与停止，不提供保存动作——因此三端能按名调用同一个脚本，但只有 CLI 能把它存进库。补齐 IDE 保存入口属于后续（要扩 `saveWorkflowScript` 的宿主命令路由与 webview 表单），不影响库本身。
