minimal-agent 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MINIMAL-TRACE.md +77 -0
- package/package.json +3 -1
- package/plugins/ralph-wiggum/README.md +21 -3
- package/plugins/ralph-wiggum/commands/help.md +15 -4
- package/plugins/ralph-wiggum/commands/ralph-loop.md +30 -4
- package/plugins/ralph-wiggum/plugin.js +83 -8
- package/plugins/ralph-wiggum/src/goalState.js +39 -41
- package/plugins/ralph-wiggum/src/sentinels.js +62 -7
- package/plugins/workflow-runner/commands/workflow.md +81 -1
- package/plugins/workflow-runner/commands/workflows.md +2 -0
- package/plugins/workflow-runner/src/agentCommon.js +100 -0
- package/plugins/workflow-runner/src/index.js +15 -2
- package/plugins/workflow-runner/src/loader.js +19 -3
- package/plugins/workflow-runner/src/miniReAct.js +60 -7
- package/plugins/workflow-runner/src/recovery.js +107 -0
- package/plugins/workflow-runner/src/runner.js +73 -9
- package/plugins/workflow-runner/src/stepExecutors/agent.js +72 -0
- package/plugins/workflow-runner/src/stepExecutors/llm.js +2 -2
- package/workflows/adaptive-research.yaml +41 -0
|
@@ -6,7 +6,7 @@ argument-hint: <name> [val ...] [--input key=value ...]
|
|
|
6
6
|
# /workflow
|
|
7
7
|
|
|
8
8
|
本命令由 workflow-runner 插件接管(plugin.ts.runCommand),不会把 prompt 喂给 LLM。
|
|
9
|
-
直接调用插件内置的 YAML DAG 执行器,按 steps 顺序运行 tool / llm / assert / branch / loop 等节点。
|
|
9
|
+
直接调用插件内置的 YAML DAG 执行器,按 steps 顺序运行 tool / llm / skill / agent / assert / branch / loop 等节点。
|
|
10
10
|
|
|
11
11
|
用法示例:
|
|
12
12
|
|
|
@@ -22,4 +22,84 @@ argument-hint: <name> [val ...] [--input key=value ...]
|
|
|
22
22
|
- 多余的位置参数(超过 inputs 声明数量)被丢弃并 stderr 打 warning,不影响 workflow 执行
|
|
23
23
|
- 值含空格时用引号:`/workflow xxx "AI 教程 进阶"`
|
|
24
24
|
|
|
25
|
+
## agent 节点:把一步交给会随机应变的子 agent
|
|
26
|
+
|
|
27
|
+
大多数 step 是确定性的(`tool` 调一个原子工具、`llm` 单轮生成)。但有些步骤本质是开放式的——
|
|
28
|
+
「把 ./data 下最新的 csv 整理成 summary.md」这种,怎么做得看现场。这时用 `agent:` 节点:inline 写一句
|
|
29
|
+
任务描述,工作流就地起一个**有界 TAOR 子 agent**(思考 → 用工具 → 观察 → 反思 → 再来),让它随机应变
|
|
30
|
+
把这一步要的结果做出来,产出经验证后 `capture` 给下游。这就是「受工作流指导的 TAOR」——happy path 仍是
|
|
31
|
+
确定性骨架,只在你显式声明的节点上注入完整 agency。
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
steps:
|
|
35
|
+
- id: summarize
|
|
36
|
+
agent: "把 ./data 下最新的 csv 读出来,整理成一份 markdown 摘要写到 summary.md"
|
|
37
|
+
capture: { content: summary } # agent 的最终产出存进变量 summary
|
|
38
|
+
- id: gate
|
|
39
|
+
type: assert
|
|
40
|
+
condition: "fileExists('summary.md')"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`agent:` 的配套字段(全部复用现有字段,都可选):
|
|
44
|
+
|
|
45
|
+
- `allowed_tools: [Read, Write]` —— 收窄它能用的工具;**缺省给全量工具**(要真 agency)。
|
|
46
|
+
- `output_schema: {...}` —— 要求结构化产出(provider-native JSON Schema),按字段 `capture`。
|
|
47
|
+
- `context_files: [{ path, hint }]` —— 列出可读文件让它按需 Read(不 inline 内容)。
|
|
48
|
+
- `maxTurns: <n>` —— 子循环最大轮数,默认 12。
|
|
49
|
+
- `capture` / `when` / `onError` —— 与其它动作 step 一致(agent 节点也能 `onError: recover` 被再救一次)。
|
|
50
|
+
|
|
51
|
+
`agent` vs `skill`:`agent` 是 **inline、一次性**的有界子循环(miniReAct,轻、收敛快、产出受验证闸门把关);
|
|
52
|
+
`skill` 是跑一个**外部 SKILL.md 定义、可复用**的完整 agent(runQuery)。需要复用同一套能力用 `skill`,
|
|
53
|
+
临时让某步「自己想办法」用 `agent`。
|
|
54
|
+
|
|
55
|
+
**信任边界 / 不伪造**:agent 节点与 `onError: recover` 共用同一套系统提示词纪律——内置安全护栏(最小动作、
|
|
56
|
+
优先可逆、不碰工作目录外、不删无关数据)、收尾前自我验证、**严禁伪造产出**;做不成就诚实放弃(抛错走 `onError`)。
|
|
57
|
+
默认握全量工具,信任边界同 `skill:`——「运行一条 workflow 即信任它」。
|
|
58
|
+
|
|
59
|
+
## 节点失败怎么办:onError(指导 ↔ 自主 旋钮)
|
|
60
|
+
|
|
61
|
+
传统工作流的痛点:某个节点一失败、或撞上预料之外的情况,整条流程就崩、停摆。
|
|
62
|
+
每个**动作 step**(tool / llm / skill / agent / assert)可声明 `onError`,决定失败时的行为,
|
|
63
|
+
让一条工作流在「刚性确定」和「随机应变」之间按节点滑动:
|
|
64
|
+
|
|
65
|
+
- `halt`(**默认**):节点失败 → 整条工作流停、报错。最刚性、最可审计。
|
|
66
|
+
- `continue`:跳过该节点、继续往下。⚠️ 下游若用 `capture` 依赖它会拿到空值,慎用。
|
|
67
|
+
- `recover`(**自适应恢复**):失败时**不立刻停**,而是临时起一个**有界 agent**,带着
|
|
68
|
+
「工作流目标 + 这个节点本来要做什么 + 错在哪 + 当前已知变量 + 下游 capture 契约」去**随机应变**——
|
|
69
|
+
它会先诊断根因,再换路径 / 换命令 / 先读后写 / 补齐前置条件,**并在收尾前自我验证**产出确实满足意图,
|
|
70
|
+
想办法把这一步要的**结果**做出来。
|
|
71
|
+
**救回** → 用同一个 `capture` 绑定结果、继续走正常流程;**救不回** → 它会**诚实放弃**(绝不伪造假结果),
|
|
72
|
+
工作流带原因报错停止(错误信息形如「自适应恢复未成功:<原因>」)。这是「确定性工作流 × 会随机应变的 agent」的结合点。
|
|
73
|
+
|
|
74
|
+
`recover` 的配套字段(都可选):
|
|
75
|
+
|
|
76
|
+
- `recover_hint: "<提示>"` —— 给恢复 agent 的提示(支持 `${var}` 插值),如 `"端口被占就换 3001 重试"`。
|
|
77
|
+
- `allowed_tools: [Read, Bash]` —— 收窄恢复 agent 能用的工具;**缺省给全量工具**。
|
|
78
|
+
- `maxTurns: <n>` —— 恢复子循环最大轮数,默认 12(够「诊断 + 探查 + 动手 + 验证」一圈)。
|
|
79
|
+
|
|
80
|
+
示例:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
steps:
|
|
84
|
+
- id: start_server
|
|
85
|
+
tool: Bash
|
|
86
|
+
args: { command: "node server.js" }
|
|
87
|
+
onError: recover
|
|
88
|
+
recover_hint: "端口被占就换一个端口重试"
|
|
89
|
+
capture: { content: server_log }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
注意事项:
|
|
93
|
+
|
|
94
|
+
- `onError` **只作用于动作 step**。控制流节点(branch / loop / vote / parallel 的条件或结构错误、
|
|
95
|
+
以及 `when` 表达式错误)**始终 halt** —— 那类是「作者写错了」,不是「运行期意外」,不走恢复。
|
|
96
|
+
- `continue` vs `recover`:前者「放弃这步、盲目往下」,后者「想办法把这步做成、保住下游」。
|
|
97
|
+
要扛住「预料之外」的情况,用 `recover`。
|
|
98
|
+
- **信任边界**:恢复 agent 默认握有全量工具,与 `skill:` 节点一致——「运行一条 workflow 即信任它」。
|
|
99
|
+
不信任来源的 yaml 本就不该跑。
|
|
100
|
+
- **不伪造 / 诚实失败**:恢复 agent 的系统提示词内置安全护栏(只做最小必要动作、优先可逆、不碰工作目录外、
|
|
101
|
+
不删无关数据)并**严禁编造假结果**——它要么交付经自我验证的真产出,要么显式放弃让工作流诚实 halt。
|
|
102
|
+
框架侧另有验证闸门:空产出 / 显式放弃一律**不绑 `capture`、不放行下游**,杜绝把脏值喂给后续步骤。
|
|
103
|
+
- `recover` 是 opt-in 的:确定性 / 可审计正是工作流的价值,不默认抹掉;由作者按节点决定授予多少「自主权」。
|
|
104
|
+
|
|
25
105
|
输入 /workflows 查看可用工作流列表。
|
|
@@ -6,3 +6,5 @@ description: 列出当前 cwd 与插件包内可用的 YAML 工作流
|
|
|
6
6
|
|
|
7
7
|
由 workflow-runner 插件接管(plugin.ts.runCommand),扫描 cwd/workflows/ 与
|
|
8
8
|
plugin 自带 examples/ 下的 *.yaml,把名称 + 一行描述列出来。
|
|
9
|
+
|
|
10
|
+
提示:用 `agent:` 节点可把开放式的一步交给有界 TAOR 子 agent 随机应变完成;用 `onError: recover` 可让失败节点交给恢复 agent 自适应救回(而非整条崩停)。二者共用同一套「自我验证 / 不伪造」纪律——详见 `/workflow` 命令说明。
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* plugins/workflow-runner/src/agentCommon.ts —— workflow 内「有界 agent」的共享大脑
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 工作流里有两处会临时起一个 agent(都跑 miniReAct):
|
|
6
|
+
* - onError:'recover'(recovery.ts)—— 节点失败后救火
|
|
7
|
+
* - agent: 节点(stepExecutors/agent.ts)—— 作者显式声明的开放任务
|
|
8
|
+
* 二者本质同构:受工作流目标/上下文指导、用工具随机应变、产出经验证后回灌 capture。
|
|
9
|
+
* 为避免「同一个脑子写两份提示词、行为漂移」,这里收敛它们的**共享件**:
|
|
10
|
+
* 1. 统一系统提示词纪律(DISCIPLINE)—— 诊断→动手→自我验证→诚实失败(不伪造)→输出纪律;
|
|
11
|
+
* 两处只在前面各加一句**角色开场白**(recover=救火 / agent=自主完成一步)。
|
|
12
|
+
* 2. 统一验证闸门(interpretMiniReActResult)—— miniReAct 跑完后,判定「真交付」还是
|
|
13
|
+
* 「放弃/不可信」,**永不把没验证过的产出当成功**。这是 Phase 2 验证闸门的提取。
|
|
14
|
+
* ============================================================
|
|
15
|
+
*/
|
|
16
|
+
/** agent 显式放弃的唯一标记;与 DISCIPLINE 收尾约束严格对应。recover 与 agent 节点共用。 */
|
|
17
|
+
export const GIVEUP_MARKER = '__AGENT_GIVEUP__';
|
|
18
|
+
/** 共享纪律块:所有 workflow 内 agent 的通用行为约束(角色开场白由各调用方拼在前面)。 */
|
|
19
|
+
const DISCIPLINE = `按这个纪律干活:
|
|
20
|
+
1. 先想清楚「这一步要产出什么」,必要时用只读工具(Read / Grep / Glob)做最小探查,别凭空猜、别凭空编。
|
|
21
|
+
2. 用工具**实际去做**(读文件、执行命令、写文件…),不要只描述计划。
|
|
22
|
+
3. **收尾前必须自我验证**:用工具确认产出确实存在且满足意图(写了文件就 Read 回来核对、跑了命令就检查退出码与输出)。没验证过不算完成。
|
|
23
|
+
|
|
24
|
+
安全护栏(你握有包括 Bash 在内的全部工具,务必克制):
|
|
25
|
+
- 只做达成意图所必需的最小动作,优先可逆操作。
|
|
26
|
+
- 严禁破坏性行为:不 rm -rf、不删除/覆盖与本步无关的用户数据、不碰工作目录以外的东西。
|
|
27
|
+
- **绝不靠伪造、占位、或编造一段「看起来对」的文本来假装成功**——你的产出会被工作流绑给后续步骤,假结果比诚实失败更糟。
|
|
28
|
+
|
|
29
|
+
收尾(二选一,硬约束):
|
|
30
|
+
- 真的达成了 → 最终消息**只输出这一步要的结果本身**,不要寒暄、不要复述要求、不要解释推理过程。若要求结构化输出,严格按 JSON Schema、**不要**用 markdown 代码块包裹。
|
|
31
|
+
- 确实做不成 → 最终消息**只输出一行**:\`${GIVEUP_MARKER}: <一句话说明为什么>\`,让工作流诚实地停下。绝不要为了「交差」而编一个假结果。`;
|
|
32
|
+
/** 拼一个 workflow agent 的 system prompt:角色开场白 + 共享纪律。 */
|
|
33
|
+
export function buildWorkflowAgentSystem(opening) {
|
|
34
|
+
return `${opening}\n\n${DISCIPLINE}`;
|
|
35
|
+
}
|
|
36
|
+
/** 把 step.capture 翻成「下游会把你的结果存成哪些变量」,让 agent 产出对得上形状。 */
|
|
37
|
+
export function describeCaptureContract(step) {
|
|
38
|
+
if (!step.capture)
|
|
39
|
+
return '';
|
|
40
|
+
const names = Object.values(step.capture);
|
|
41
|
+
if (names.length === 0)
|
|
42
|
+
return '';
|
|
43
|
+
return `\n\n下游契约:工作流会把你的结果保存为变量【${names.join('、')}】供后续步骤引用,请确保你的产出能填好这些变量。`;
|
|
44
|
+
}
|
|
45
|
+
export function truncate(s, n) {
|
|
46
|
+
return s.length > n ? `${s.slice(0, n)}…` : s;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 解析非 schema 模式下 agent 最终文本里的放弃标记。
|
|
50
|
+
* - 命中 `__AGENT_GIVEUP__: <原因>` → 返回原因字符串(可能为空串)
|
|
51
|
+
* - 未命中 → null(视为给出了真实结果)
|
|
52
|
+
*/
|
|
53
|
+
export function parseGiveup(text) {
|
|
54
|
+
const idx = text.indexOf(GIVEUP_MARKER);
|
|
55
|
+
if (idx === -1)
|
|
56
|
+
return null;
|
|
57
|
+
return text
|
|
58
|
+
.slice(idx + GIVEUP_MARKER.length)
|
|
59
|
+
.replace(/^[::\s]+/, '')
|
|
60
|
+
.trim();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 验证闸门:把 miniReAct 的 StepResult 判定为「真交付」或「放弃/不可信」。
|
|
64
|
+
* - schema 模式:miniReAct 已 strict 解析(解析失败会抛、落不到这里),能拿到结果即 ok;
|
|
65
|
+
* 补 content 别名不覆盖具名字段。
|
|
66
|
+
* - 非 schema 模式:空产出 / 命中 GIVEUP 标记 → ok:false;否则 ok:true,raw 补 text/result/content
|
|
67
|
+
* 三键别名,兼容 capture:{content:x}/{result:x}/{text:x}。
|
|
68
|
+
* 注意:preview 这里给「干净版」(不加任何前缀),需要前缀的调用方(如 recover 的 "recovered: ")
|
|
69
|
+
* 自行包装。
|
|
70
|
+
*/
|
|
71
|
+
export function interpretMiniReActResult(result, hasSchema) {
|
|
72
|
+
if (hasSchema) {
|
|
73
|
+
const raw = { ...result.raw };
|
|
74
|
+
if (typeof raw.result === 'string' && raw.content === undefined) {
|
|
75
|
+
raw.content = raw.result;
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, value: { raw, preview: result.preview } };
|
|
78
|
+
}
|
|
79
|
+
const text = typeof result.raw.text === 'string' ? result.raw.text : '';
|
|
80
|
+
const trimmed = text.trim();
|
|
81
|
+
if (trimmed.length === 0) {
|
|
82
|
+
return { ok: false, reason: '没有产出任何结果' };
|
|
83
|
+
}
|
|
84
|
+
const giveup = parseGiveup(trimmed);
|
|
85
|
+
if (giveup !== null) {
|
|
86
|
+
return { ok: false, reason: giveup || '显式放弃(未给原因)' };
|
|
87
|
+
}
|
|
88
|
+
const raw = {
|
|
89
|
+
text: trimmed,
|
|
90
|
+
result: trimmed,
|
|
91
|
+
content: trimmed,
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
value: {
|
|
96
|
+
raw,
|
|
97
|
+
preview: trimmed.length > 200 ? `${trimmed.slice(0, 200)}…` : trimmed,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ============================================================
|
|
3
|
-
* src/
|
|
3
|
+
* plugins/workflow-runner/src/index.ts —— /workflow 与 /workflows 命令的胶水层
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
5
|
* 作为 pluginRunner 与 runner 之间的桥:
|
|
6
6
|
* - runWorkflowFromCommand:解析 /workflow <name> --input k=v args 调度
|
|
@@ -189,7 +189,20 @@ export async function* runWorkflowFromCommand(rawArgs, opts) {
|
|
|
189
189
|
signal: opts.signal,
|
|
190
190
|
history: opts.history,
|
|
191
191
|
};
|
|
192
|
-
|
|
192
|
+
// 透传 runWorkflow 的事件,并观察是否出现终结事件(error / interrupted = 失败或中断,
|
|
193
|
+
// 与 gateway broker 的 TERMINAL 判定同一套逻辑)。成功(没见到终结事件)则在末尾补一条
|
|
194
|
+
// 通用 `turn_done`,让对外流式协议以通用终结事件收口——否则成功 workflow 只发私有
|
|
195
|
+
// workflow_done:gateway TERMINAL 不认它 → 子进程关闭时误补 interrupted;
|
|
196
|
+
// `-p --output-format json` 因无 turn_done → stop_reason 兜底 error → ok:false(成功却报失败)。
|
|
197
|
+
// 与 runWorkflowsList 末尾的 turn_done 是同一约定;失败/中断不补(error/interrupted 已是终结)。
|
|
198
|
+
let sawTerminal = false;
|
|
199
|
+
for await (const ev of runWorkflow(def, coerced, ctx)) {
|
|
200
|
+
if (ev.type === 'error' || ev.type === 'interrupted')
|
|
201
|
+
sawTerminal = true;
|
|
202
|
+
yield ev;
|
|
203
|
+
}
|
|
204
|
+
if (!sawTerminal)
|
|
205
|
+
yield { type: 'turn_done' };
|
|
193
206
|
}
|
|
194
207
|
/** /workflows —— 列出可用 workflow。yield 一条 text 事件让 UI 显示。 */
|
|
195
208
|
export async function* runWorkflowsList() {
|
|
@@ -144,6 +144,12 @@ export const VALID_STEP_TYPES = new Set([
|
|
|
144
144
|
'parallel',
|
|
145
145
|
'vote',
|
|
146
146
|
]);
|
|
147
|
+
// 动作字段里「自带 kind 含义」的那几个(不含 type —— type 的 kind 由其值决定)。
|
|
148
|
+
// 单一来源:validateSteps 的 actionFields + test/editorSchemaSync.test.ts 都从这里取,
|
|
149
|
+
// 新增动作 kind 只改这一处(并同步 editor 手抄镜像 + 该测试快照)。
|
|
150
|
+
export const ACTION_KINDS = ['tool', 'skill', 'agent', 'llm'];
|
|
151
|
+
// onError 合法值(单一来源:onError 校验 + editor schema 同步测试)。
|
|
152
|
+
export const ON_ERROR_VALUES = ['halt', 'continue', 'recover'];
|
|
147
153
|
function validateSteps(steps, file, pathPrefix) {
|
|
148
154
|
const seenIds = new Set();
|
|
149
155
|
for (const [i, item] of steps.entries()) {
|
|
@@ -164,15 +170,25 @@ function validateSteps(steps, file, pathPrefix) {
|
|
|
164
170
|
if (s.type !== undefined && (typeof s.type !== 'string' || !VALID_STEP_TYPES.has(s.type))) {
|
|
165
171
|
throw new WorkflowLoadError(`${p}.type "${String(s.type)}" 非法;合法值: ${[...VALID_STEP_TYPES].join(' / ')}`, file);
|
|
166
172
|
}
|
|
167
|
-
//
|
|
168
|
-
const actionFields = [
|
|
173
|
+
// 动作字段恰好一个(ACTION_KINDS 单一来源 + 控制流 type)
|
|
174
|
+
const actionFields = [...ACTION_KINDS, 'type'];
|
|
169
175
|
const present = actionFields.filter((k) => s[k] !== undefined);
|
|
170
176
|
if (present.length === 0) {
|
|
171
|
-
throw new WorkflowLoadError(`${p} 必须有以下字段之一:
|
|
177
|
+
throw new WorkflowLoadError(`${p} 必须有以下字段之一: ${actionFields.join(' / ')}`, file);
|
|
172
178
|
}
|
|
173
179
|
if (present.length > 1) {
|
|
174
180
|
throw new WorkflowLoadError(`${p} 同时设置了 ${present.join(', ')};只允许其中一个`, file);
|
|
175
181
|
}
|
|
182
|
+
// onError 枚举校验:runner 只认 halt/continue/recover,typo 会被静默当 halt,
|
|
183
|
+
// 故在加载期拦掉,给作者明确错误。
|
|
184
|
+
if (s.onError !== undefined &&
|
|
185
|
+
!ON_ERROR_VALUES.includes(s.onError)) {
|
|
186
|
+
throw new WorkflowLoadError(`${p}.onError "${String(s.onError)}" 非法;合法值: ${ON_ERROR_VALUES.join(' / ')}`, file);
|
|
187
|
+
}
|
|
188
|
+
// recover_hint 必须是字符串(仅 onError:'recover' 时有意义,但不强制配对)
|
|
189
|
+
if (s.recover_hint !== undefined && typeof s.recover_hint !== 'string') {
|
|
190
|
+
throw new WorkflowLoadError(`${p}.recover_hint 必须是 string`, file);
|
|
191
|
+
}
|
|
176
192
|
// 控制流字段一致性
|
|
177
193
|
if (s.type === 'branch' && s.condition === undefined) {
|
|
178
194
|
throw new WorkflowLoadError(`${p} type=branch 必须带 'condition'`, file);
|
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
* ============================================================
|
|
3
3
|
* plugins/workflow-runner/src/miniReAct.ts —— 简化版 ReAct 子循环
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 本循环只做最纯净的 ReAct:调 LLM → 执行 tool_call → 喂回 → 再调 → 直到模型
|
|
6
|
+
* 不再调工具或达到 maxTurns。**有三个调用方**(都要「有界、白名单、不污染主
|
|
7
|
+
* history、直接拿 StepResult」):
|
|
8
|
+
* - execLlmStep 的 context_files 分支(llm.ts,maxTurns 默认 10)
|
|
9
|
+
* - agent: 节点(stepExecutors/agent.ts,默认 12)
|
|
10
|
+
* - onError:'recover' 恢复 agent(recovery.ts,默认 12)
|
|
11
|
+
*
|
|
12
|
+
* **流式**:本循环边跑边 yield UI 事件——text(答案)/ reasoning(思维链)/
|
|
13
|
+
* tool_start/tool_end(工具)/ **stage_change(thinking→streaming→tool_executing,
|
|
14
|
+
* 对齐 src/loop.ts::runQuery)**。stage_change 是 TUI LiveArea 显示的开关(idle 时
|
|
15
|
+
* LiveArea 整块隐藏),没有它则上述内层事件在 TUI 上发了也看不见。output_schema
|
|
16
|
+
* 模式下**仍切 stage、但不回显 text**(避免把 JSON 原文当答案刷屏,对齐 llm.ts)。
|
|
8
17
|
*
|
|
9
18
|
* 为什么不复用 src/loop.ts::runQuery?
|
|
10
19
|
* - runQuery 依赖太重:history 持久化、自动压缩、反应式压缩自救、
|
|
@@ -23,7 +32,11 @@
|
|
|
23
32
|
*/
|
|
24
33
|
import { chat, executeTool, } from '../../../src/plugin-sdk.js';
|
|
25
34
|
/**
|
|
26
|
-
* 执行一次 ReAct
|
|
35
|
+
* 执行一次 ReAct 子循环。**边跑边 yield UI 事件(stage_change / text / reasoning /
|
|
36
|
+
* tool_start / tool_end),同时 return 最终 StepResult**——让 recover / agent 节点 /
|
|
37
|
+
* llm+context_files 三处子 agent 的内层思考在 TUI / webchat 实时可见(之前是纯 async
|
|
38
|
+
* 不 yield,"哑"的;且漏发 stage_change 时 TUI LiveArea 整块隐藏,等于没显示)。
|
|
39
|
+
* 调用方用 `const result = yield* runMiniReAct(...)` 既透传事件、又拿到 StepResult。
|
|
27
40
|
*
|
|
28
41
|
* 抛错场景:
|
|
29
42
|
* - 用户中断(signal.aborted)
|
|
@@ -31,7 +44,7 @@ import { chat, executeTool, } from '../../../src/plugin-sdk.js';
|
|
|
31
44
|
* - 用尽 maxTurns 仍未收敛
|
|
32
45
|
* - output_schema 解析失败 / 非对象
|
|
33
46
|
*/
|
|
34
|
-
export async function runMiniReAct(args) {
|
|
47
|
+
export async function* runMiniReAct(args) {
|
|
35
48
|
const { messages, tools, output_schema, provider, signal } = args;
|
|
36
49
|
const maxTurns = args.maxTurns ?? 10;
|
|
37
50
|
// 工作消息列表:拷贝一份,避免污染调用方
|
|
@@ -57,6 +70,11 @@ export async function runMiniReAct(args) {
|
|
|
57
70
|
// ---- 1. 调 LLM 并累积流式响应 ----
|
|
58
71
|
let textAccum = '';
|
|
59
72
|
const toolCallsAccum = [];
|
|
73
|
+
// 进 LLM 流前本质是「等首 token」= thinking;首个 text_delta 时切 streaming(一次性)。
|
|
74
|
+
// 对齐 src/loop.ts::runQuery —— 没有这两个 stage_change,TUI LiveArea 会一直 idle 隐藏,
|
|
75
|
+
// 下面 yield 的 text/reasoning/tool 事件在 TUI 上等于白发。
|
|
76
|
+
let stageStreamingYielded = false;
|
|
77
|
+
yield { type: 'stage_change', stage: 'thinking' };
|
|
60
78
|
for await (const ev of chat({
|
|
61
79
|
provider,
|
|
62
80
|
messages: conv,
|
|
@@ -66,14 +84,26 @@ export async function runMiniReAct(args) {
|
|
|
66
84
|
})) {
|
|
67
85
|
if (ev.type === 'text_delta') {
|
|
68
86
|
textAccum += ev.delta;
|
|
87
|
+
if (!stageStreamingYielded) {
|
|
88
|
+
stageStreamingYielded = true;
|
|
89
|
+
yield { type: 'stage_change', stage: 'streaming' };
|
|
90
|
+
}
|
|
91
|
+
// output_schema 模式:切了 streaming 阶段但**不回显 JSON 原文**(不把结构化产出
|
|
92
|
+
// 当答案刷屏,对齐 llm.ts:121 的克制);非 schema 模式才把答案流式 yield 给 UI。
|
|
93
|
+
if (!output_schema)
|
|
94
|
+
yield { type: 'text', delta: ev.delta };
|
|
69
95
|
}
|
|
70
96
|
else if (ev.type === 'tool_call_delta') {
|
|
71
97
|
mergeToolCallDelta(toolCallsAccum, ev);
|
|
72
98
|
}
|
|
99
|
+
else if (ev.type === 'reasoning_delta') {
|
|
100
|
+
// 思维链字符串增量透传给 UI(reasoning_details 是结构化数组、无 delta,跳过)
|
|
101
|
+
if (ev.delta)
|
|
102
|
+
yield { type: 'reasoning', delta: ev.delta };
|
|
103
|
+
}
|
|
73
104
|
else if (ev.type === 'done' && ev.stopReason === 'aborted') {
|
|
74
105
|
throw new Error('用户中断');
|
|
75
106
|
}
|
|
76
|
-
// reasoning_delta / 其它事件忽略
|
|
77
107
|
}
|
|
78
108
|
// ---- 2. 把 assistant 消息推入 conv ----
|
|
79
109
|
conv.push({
|
|
@@ -89,6 +119,8 @@ export async function runMiniReAct(args) {
|
|
|
89
119
|
}
|
|
90
120
|
lastTurnHadToolCalls = true;
|
|
91
121
|
// ---- 4. 执行所有 tool_calls,结果回填为 tool 消息 ----
|
|
122
|
+
// 进工具执行 = tool_executing 阶段(驱动 TUI LiveArea 显示运行中工具,对齐 loop.ts:340)。
|
|
123
|
+
yield { type: 'stage_change', stage: 'tool_executing' };
|
|
92
124
|
for (const tc of toolCallsAccum) {
|
|
93
125
|
// 重复检测
|
|
94
126
|
const sig = `${tc.function.name}:${tc.function.arguments}`;
|
|
@@ -99,11 +131,27 @@ export async function runMiniReAct(args) {
|
|
|
99
131
|
}
|
|
100
132
|
if (signal?.aborted)
|
|
101
133
|
throw new Error('用户中断');
|
|
134
|
+
// 执行前后 yield tool_start/tool_end,让 UI 看到子 agent 在用工具(事件形状对齐
|
|
135
|
+
// src/loop.ts;miniReAct 串行执行,顺序 yield 即可,不需要 loop 的并行事件队列)。
|
|
136
|
+
yield {
|
|
137
|
+
type: 'tool_start',
|
|
138
|
+
toolName: tc.function.name,
|
|
139
|
+
toolCallId: tc.id,
|
|
140
|
+
argsPreview: previewArgs(tc.function.arguments),
|
|
141
|
+
};
|
|
102
142
|
const result = await executeTool(tc.function.name, tc.function.arguments, signal);
|
|
143
|
+
const toolContent = result.ok ? result.content : `工具错误: ${result.error}`;
|
|
144
|
+
yield {
|
|
145
|
+
type: 'tool_end',
|
|
146
|
+
toolName: tc.function.name,
|
|
147
|
+
toolCallId: tc.id,
|
|
148
|
+
ok: result.ok,
|
|
149
|
+
content: toolContent,
|
|
150
|
+
};
|
|
103
151
|
conv.push({
|
|
104
152
|
role: 'tool',
|
|
105
153
|
tool_call_id: tc.id,
|
|
106
|
-
content:
|
|
154
|
+
content: toolContent,
|
|
107
155
|
});
|
|
108
156
|
}
|
|
109
157
|
// 继续下一轮
|
|
@@ -153,3 +201,8 @@ function mergeToolCallDelta(acc, ev) {
|
|
|
153
201
|
if (ev.argumentsDelta)
|
|
154
202
|
slot.function.arguments += ev.argumentsDelta;
|
|
155
203
|
}
|
|
204
|
+
/** 给 tool_start 事件用的参数预览(压成一行、截断到 60 字符),与 loop.ts previewArgs 同义。 */
|
|
205
|
+
function previewArgs(rawJson) {
|
|
206
|
+
const oneLine = rawJson.replace(/\s+/g, ' ').trim();
|
|
207
|
+
return oneLine.length <= 60 ? oneLine : `${oneLine.slice(0, 60)}...`;
|
|
208
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* plugins/workflow-runner/src/recovery.ts —— 节点失败的自适应恢复
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 「确定性工作流 × 随机应变 agent」的结合点(onError:'recover')。
|
|
6
|
+
*
|
|
7
|
+
* 传统 DAG 的脆点:某节点抛错 → 整条 halt(或 onError:'continue' 盲跳,下游
|
|
8
|
+
* capture 拿 undefined 静默级联错)。当节点声明 onError:'recover' 时,runner
|
|
9
|
+
* 不立刻 halt,而是调本模块起一个**有界恢复 agent**(复用 miniReAct),喂给它
|
|
10
|
+
* 「工作流总目标 + 失败节点的意图 + 错误信息 + 当前已知变量 + 下游 capture 契约」,
|
|
11
|
+
* 让它**随机应变去达成该节点本来要的结果**(换路径 / 换命令 / 先读后写…)。
|
|
12
|
+
*
|
|
13
|
+
* —— 共享大脑 ——
|
|
14
|
+
* 恢复 agent 与 agent: 节点(stepExecutors/agent.ts)本质同构,故系统提示词纪律与
|
|
15
|
+
* 验证闸门都收敛在 agentCommon.ts:本模块只贴一句「救火」角色开场白(RECOVER_OPENING),
|
|
16
|
+
* 跑完后交给 interpretMiniReActResult 判定「真救回 / 放弃」——绝不把没验证过的产出当成功
|
|
17
|
+
* 绑给下游。返回 AgentOutcome:
|
|
18
|
+
* - { ok:true, value } —— runner 用同一个 step.capture 绑定后继续 happy path
|
|
19
|
+
* - { ok:false, reason } —— runner 带原因 honest halt
|
|
20
|
+
*
|
|
21
|
+
* 设计取舍:复用 miniReAct 而非 runQuery —— 恢复要的正是「有界、白名单、不污染主
|
|
22
|
+
* history、直接返回 StepResult」;默认全量 ALL_TOOLS(与 skill/agent 节点同等授权、
|
|
23
|
+
* 同一信任边界),step.allowed_tools 可收窄。本函数是 AsyncGenerator,yield*
|
|
24
|
+
* runMiniReAct 把恢复 agent 的内层思考透传给 UI;warning/plugin_progress/halt 仍由
|
|
25
|
+
* runner 在调用前后发(事件卫生集中在 runner)。
|
|
26
|
+
* ============================================================
|
|
27
|
+
*/
|
|
28
|
+
import { ALL_TOOLS } from '../../../src/plugin-sdk.js';
|
|
29
|
+
import { buildWorkflowAgentSystem, describeCaptureContract, interpretMiniReActResult, truncate, } from './agentCommon.js';
|
|
30
|
+
import { interpolate, interpolateDeep } from './expressions.js';
|
|
31
|
+
import { runMiniReAct } from './miniReAct.js';
|
|
32
|
+
const RECOVER_OPENING = `你是一名资深的自动化工程师 / SRE,被一个确定性工作流临时调来「救火」。
|
|
33
|
+
工作流里一个原本写死的步骤刚刚执行失败了,现在交给你随机应变——先看错误信息诊断根因,再想办法真正达成这个步骤**本来要的结果**(关注它要的结果,别拘泥它原先怎么写)。`;
|
|
34
|
+
const RECOVERY_SYSTEM = buildWorkflowAgentSystem(RECOVER_OPENING);
|
|
35
|
+
/** 描述失败节点「本来要干什么」,喂给恢复 agent 作为意图说明。 */
|
|
36
|
+
function describeStepIntent(step, vars) {
|
|
37
|
+
if (step.tool) {
|
|
38
|
+
const args = interpolateDeep(step.args ?? {}, vars);
|
|
39
|
+
return `调用工具 ${step.tool},参数:${JSON.stringify(args)}`;
|
|
40
|
+
}
|
|
41
|
+
if (step.agent) {
|
|
42
|
+
return `自主完成这个开放任务:${interpolate(step.agent, vars)}`;
|
|
43
|
+
}
|
|
44
|
+
if (step.llm) {
|
|
45
|
+
return `用 LLM 生成内容,提示词:${interpolate(step.llm, vars)}`;
|
|
46
|
+
}
|
|
47
|
+
if (step.skill) {
|
|
48
|
+
const input = step.input ? interpolate(step.input, vars) : '(无显式输入)';
|
|
49
|
+
return `执行 skill「${interpolate(step.skill, vars)}」,输入:${input}`;
|
|
50
|
+
}
|
|
51
|
+
if (step.type === 'assert') {
|
|
52
|
+
return `让这个断言成立:${step.condition}`;
|
|
53
|
+
}
|
|
54
|
+
return `完成步骤 ${step.id}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 尝试自适应恢复一个失败的动作 step。
|
|
58
|
+
* - 救回 → { ok:true, value }(value.raw 已 normalize,可被原 step.capture 绑定)
|
|
59
|
+
* - 放弃 / 不可信 / 子循环抛错(含用户中断)→ { ok:false, reason }
|
|
60
|
+
* (中断的最终语义由调用方在拿到结果后另行检测 ctx.signal 决定)
|
|
61
|
+
*/
|
|
62
|
+
export async function* runRecovery(step, vars, ctx, errorMsg) {
|
|
63
|
+
const intent = describeStepIntent(step, vars);
|
|
64
|
+
const knownVars = truncate(JSON.stringify(vars.snapshot()), 1500);
|
|
65
|
+
const goalLine = ctx.goal ? `工作流总目标:${ctx.goal}\n\n` : '';
|
|
66
|
+
const captureLine = describeCaptureContract(step);
|
|
67
|
+
const hintLine = step.recover_hint
|
|
68
|
+
? `\n\n作者给恢复的提示:${interpolate(step.recover_hint, vars)}`
|
|
69
|
+
: '';
|
|
70
|
+
const userMsg = `${goalLine}` +
|
|
71
|
+
`失败的步骤(id=${step.id})本来要做的事:\n${intent}\n\n` +
|
|
72
|
+
`它失败时的错误:\n${truncate(errorMsg, 800)}\n\n` +
|
|
73
|
+
`当前已知的变量(JSON):\n${knownVars}` +
|
|
74
|
+
`${captureLine}${hintLine}\n\n` +
|
|
75
|
+
`请随机应变,把这一步要的结果做出来;做不成就按约定输出放弃标记。`;
|
|
76
|
+
// 工具集:默认全量(与 skill/agent 节点同等 agency);step.allowed_tools 收窄。
|
|
77
|
+
const allowed = step.allowed_tools;
|
|
78
|
+
const tools = allowed && allowed.length > 0
|
|
79
|
+
? ALL_TOOLS.filter((t) => allowed.includes(t.name))
|
|
80
|
+
: ALL_TOOLS;
|
|
81
|
+
const messages = [
|
|
82
|
+
{ role: 'system', content: RECOVERY_SYSTEM },
|
|
83
|
+
{ role: 'user', content: userMsg },
|
|
84
|
+
];
|
|
85
|
+
try {
|
|
86
|
+
const result = yield* runMiniReAct({
|
|
87
|
+
messages,
|
|
88
|
+
tools,
|
|
89
|
+
output_schema: step.output_schema,
|
|
90
|
+
provider: ctx.provider,
|
|
91
|
+
signal: ctx.signal,
|
|
92
|
+
maxTurns: step.maxTurns ?? 12,
|
|
93
|
+
});
|
|
94
|
+
const outcome = interpretMiniReActResult(result, step.output_schema !== undefined);
|
|
95
|
+
if (!outcome.ok)
|
|
96
|
+
return outcome;
|
|
97
|
+
// 救回:preview 加 recovered: 前缀(runner 的 step_end output 用),raw 原样。
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
value: { raw: outcome.value.raw, preview: `recovered: ${outcome.value.preview}` },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
// miniReAct 抛错(用尽轮数 / 重复工具调用 / schema 解析失败 / 中断)→ 恢复失败。
|
|
105
|
+
return { ok: false, reason: e.message ?? String(e) };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* - 信号 abort → yield interrupted 后 return
|
|
11
11
|
*
|
|
12
12
|
* 支持的 step kind(loader 已校验):
|
|
13
|
-
* - tool / llm / assert
|
|
14
|
-
* - skill ——
|
|
15
|
-
*
|
|
13
|
+
* - tool / llm / assert —— 原子动作(execXxxStep 返回 StepResult)
|
|
14
|
+
* - agent / skill —— 有界 TAOR:miniReAct(agent) / 外部 SKILL.md(skill);
|
|
15
|
+
* 透传子事件(stage_change/text/reasoning/tool_*)
|
|
16
|
+
* - branch / loop / pause —— 控制流,直接在 runSteps 内处理(不走 executor)
|
|
17
|
+
* - parallel / vote —— 并发 / 投票门,也在 runSteps 内处理
|
|
16
18
|
*
|
|
17
19
|
* 控制流嵌套通过 runSteps 递归实现:branch.then/else / loop.do 子数组
|
|
18
20
|
* 是 StepDef[],调用方语义与顶层 def.steps 一致;变量栈用 VarStack.push/pop
|
|
@@ -21,6 +23,8 @@
|
|
|
21
23
|
*/
|
|
22
24
|
import { getWorkingDir } from '../../../src/plugin-sdk.js';
|
|
23
25
|
import { evalExpr, interpolate } from './expressions.js';
|
|
26
|
+
import { runRecovery } from './recovery.js';
|
|
27
|
+
import { execAgentStep } from './stepExecutors/agent.js';
|
|
24
28
|
import { execAssertStep } from './stepExecutors/assert.js';
|
|
25
29
|
import { execLlmStep } from './stepExecutors/llm.js';
|
|
26
30
|
import { execSkillStep } from './stepExecutors/skill.js';
|
|
@@ -42,6 +46,8 @@ function stepKind(step) {
|
|
|
42
46
|
return 'tool';
|
|
43
47
|
if (step.skill)
|
|
44
48
|
return 'skill';
|
|
49
|
+
if (step.agent)
|
|
50
|
+
return 'agent';
|
|
45
51
|
if (step.llm)
|
|
46
52
|
return 'llm';
|
|
47
53
|
return 'assert'; // 不会到这里(loader 已校验),兜底
|
|
@@ -65,19 +71,21 @@ function bindCapture(captureMap, result, vars) {
|
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
/**
|
|
68
|
-
* 分发"动作"型 step(tool / llm / skill / assert)到对应 executor。
|
|
69
|
-
* 控制流 step(branch / loop / pause)由 runSteps 直接处理,不走这里。
|
|
74
|
+
* 分发"动作"型 step(tool / agent / llm / skill / assert)到对应 executor。
|
|
75
|
+
* 控制流 step(branch / loop / pause / parallel / vote)由 runSteps 直接处理,不走这里。
|
|
70
76
|
*/
|
|
71
77
|
async function* dispatchActionStep(step, vars, ctx) {
|
|
72
78
|
if (step.type === 'assert')
|
|
73
79
|
return execAssertStep(step, vars);
|
|
74
80
|
if (step.tool)
|
|
75
81
|
return yield* execToolStep(step, vars, ctx);
|
|
82
|
+
if (step.agent)
|
|
83
|
+
return yield* execAgentStep(step, vars, ctx);
|
|
76
84
|
if (step.llm)
|
|
77
85
|
return yield* execLlmStep(step, vars, ctx);
|
|
78
86
|
if (step.skill)
|
|
79
87
|
return yield* execSkillStep(step, vars, ctx);
|
|
80
|
-
throw new Error(`step ${step.id}: 无可执行字段(tool/llm/skill/assert)`);
|
|
88
|
+
throw new Error(`step ${step.id}: 无可执行字段(tool/agent/llm/skill/assert)`);
|
|
81
89
|
}
|
|
82
90
|
/**
|
|
83
91
|
* 递归执行 steps[]。返回 true 表示该 block 已 halt(上游应中止)。
|
|
@@ -396,6 +404,54 @@ async function* runSteps(steps, vars, ctx, state) {
|
|
|
396
404
|
catch (e) {
|
|
397
405
|
const errMsg = e.message ?? String(e);
|
|
398
406
|
await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
|
|
407
|
+
// onError:'recover' —— 不立刻 halt,交给有界恢复 agent 随机应变把这一步救回来。
|
|
408
|
+
// 「确定性工作流 × TAOR」的结合点:happy path 走刚性 DAG,撞上预料之外的就地
|
|
409
|
+
// 降级为 agent 自适应;救回则继续 happy path,救不回才 honest halt。
|
|
410
|
+
if (step.onError === 'recover') {
|
|
411
|
+
yield {
|
|
412
|
+
type: 'warning',
|
|
413
|
+
message: `step "${step.id}" 失败:${errMsg};尝试自适应恢复…`,
|
|
414
|
+
};
|
|
415
|
+
yield {
|
|
416
|
+
type: 'plugin_progress',
|
|
417
|
+
pluginId: WF_PLUGIN_ID,
|
|
418
|
+
current: i + 1,
|
|
419
|
+
max: steps.length,
|
|
420
|
+
message: `↻ 恢复 ${step.id}…`,
|
|
421
|
+
};
|
|
422
|
+
const rec = yield* runRecovery(step, vars, ctx, errMsg);
|
|
423
|
+
if (ctx.signal?.aborted) {
|
|
424
|
+
yield { type: 'interrupted' };
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
// 验证闸门:只有恢复 agent 真正交付(rec.ok)才绑 capture 走下去;
|
|
428
|
+
// 显式放弃 / 产出不可信(rec.ok=false)则带原因 honest halt,绝不盲绑脏值。
|
|
429
|
+
if (rec.ok) {
|
|
430
|
+
if (step.capture)
|
|
431
|
+
bindCapture(step.capture, rec.value, vars);
|
|
432
|
+
await state.appendProgress(`⚠→✓ ${step.id} recovered`);
|
|
433
|
+
await state.writeVars(vars.snapshot());
|
|
434
|
+
yield {
|
|
435
|
+
type: 'workflow_step_end',
|
|
436
|
+
id: step.id,
|
|
437
|
+
ok: true,
|
|
438
|
+
output: rec.value.preview,
|
|
439
|
+
};
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const combined = `${errMsg}(自适应恢复未成功:${rec.reason})`;
|
|
443
|
+
yield {
|
|
444
|
+
type: 'workflow_step_end',
|
|
445
|
+
id: step.id,
|
|
446
|
+
ok: false,
|
|
447
|
+
error: combined,
|
|
448
|
+
};
|
|
449
|
+
yield {
|
|
450
|
+
type: 'error',
|
|
451
|
+
error: `Workflow halted at step "${step.id}": ${combined}`,
|
|
452
|
+
};
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
399
455
|
if (step.onError === 'continue') {
|
|
400
456
|
yield {
|
|
401
457
|
type: 'workflow_step_end',
|
|
@@ -424,12 +480,20 @@ async function* runSteps(steps, vars, ctx, state) {
|
|
|
424
480
|
* 运行一个 workflow。
|
|
425
481
|
*
|
|
426
482
|
* yields 混合事件:本地 WorkflowEvent(workflow_* 系列,UI 静默忽略不渲染)
|
|
427
|
-
* + 通用 LoopEvent(error / interrupted 等,
|
|
428
|
-
*
|
|
483
|
+
* + 通用 LoopEvent(error / interrupted / 子 step 透传的 text/tool/stage_change 等,
|
|
484
|
+
* UI 识别并渲染)。经 plugin.ts -> pluginRunner 透传给 UI;workflow_* 由 PluginEvent
|
|
485
|
+
* 开放契约承载。
|
|
486
|
+
*
|
|
487
|
+
* 成功跑完以 `workflow_done` 收尾(携带 vars 快照);中途失败/中断只 yield error/interrupted。
|
|
488
|
+
* **对外流式协议的通用终结事件 `turn_done` 由命令层 index.ts 在成功后补发**(见那里)——
|
|
489
|
+
* 本函数自身事件序列保持不变,直测它的 runner.test.ts / 单元测试仍以 workflow_done 为最后一条。
|
|
429
490
|
*/
|
|
430
491
|
export async function* runWorkflow(def, inputs, ctx) {
|
|
431
492
|
const vars = new VarStack();
|
|
432
493
|
vars.setGlobal('inputs', inputs);
|
|
494
|
+
// onError:'recover' 用:把工作流总目标(description)透传给恢复 agent,
|
|
495
|
+
// 让它知道「这条工作流到底想干什么」。已有 goal 则不覆盖。
|
|
496
|
+
const runCtx = { ...ctx, goal: ctx.goal ?? def.description };
|
|
433
497
|
const state = new WorkflowState(getWorkingDir());
|
|
434
498
|
await state.init(def.name, inputs);
|
|
435
499
|
yield {
|
|
@@ -446,7 +510,7 @@ export async function* runWorkflow(def, inputs, ctx) {
|
|
|
446
510
|
message: `▶ workflow ${def.name}`,
|
|
447
511
|
};
|
|
448
512
|
try {
|
|
449
|
-
const halted = yield* runSteps(def.steps, vars,
|
|
513
|
+
const halted = yield* runSteps(def.steps, vars, runCtx, state);
|
|
450
514
|
if (halted)
|
|
451
515
|
return;
|
|
452
516
|
// 完成态也给一条 plugin_progress(放在 workflow_done 之前,保证
|