minimal-agent 0.8.1 → 0.9.0
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/README.md +4 -5
- package/package.json +1 -1
- package/plugins/workflow-runner/plugin.js +1 -0
- package/plugins/workflow-runner/src/agentCommon.js +82 -47
- package/plugins/workflow-runner/src/expressions.js +48 -2
- package/plugins/workflow-runner/src/index.js +1 -0
- package/plugins/workflow-runner/src/jsonPayload.js +1 -1
- package/plugins/workflow-runner/src/loader.js +70 -8
- package/plugins/workflow-runner/src/recovery.js +43 -35
- package/plugins/workflow-runner/src/runner.js +143 -34
- package/plugins/workflow-runner/src/stepExecutors/agent.js +64 -35
- package/plugins/workflow-runner/src/stepExecutors/assert.js +14 -2
- package/plugins/workflow-runner/src/stepExecutors/llm.js +48 -107
- package/plugins/workflow-runner/src/stepExecutors/skill.js +50 -39
- package/plugins/workflow-runner/src/stepExecutors/tool.js +20 -7
- package/plugins/workflow-runner/src/stepExecutors/verify.js +11 -3
- package/plugins/workflow-runner/src/taskIdentity.js +141 -0
- package/plugins/workflow-runner/src/types.js +1 -2
- package/plugins/workflow-runner/src/workflowState.js +211 -28
- package/skills/adversarial-review/SKILL.md +0 -1
- package/skills/pptx/scripts/office/__pycache__/soffice.cpython-314.pyc +0 -0
- package/src/askBridge.js +10 -0
- package/src/context/archive.js +4 -0
- package/src/infra/verification.js +25 -13
- package/src/llm/client.js +160 -12
- package/src/loop.js +15 -12
- package/src/plugins/pluginLoader.js +2 -1
- package/src/plugins/pluginRunner.js +9 -21
- package/src/prompts/pluginList.js +5 -5
- package/src/prompts/promptAssembly.js +3 -38
- package/src/prompts/skillList.js +1 -1
- package/src/prompts/system.js +0 -1
- package/src/ui/AskQuestion.js +20 -3
- package/src/ui/InputBox.js +70 -3
- package/src/ui/hooks/useChat.js +13 -0
- package/workflows/adaptive-research.yaml +5 -3
- package/workflows/book-review-short.yaml +5 -3
- package/workflows/e2e-write-greet.yaml +3 -2
- package/workflows/plan.yaml +219 -167
- package/workflows/ralph-loop.yaml +169 -0
- package/workflows/youtube-shorts.yaml +8 -5
- package/plugins/HOW-TO-WRITE-A-PLUGIN.md +0 -186
- package/plugins/ralph-wiggum/.claude-plugin/plugin.json +0 -9
- package/plugins/ralph-wiggum/README.md +0 -197
- package/plugins/ralph-wiggum/commands/cancel-ralph.md +0 -18
- package/plugins/ralph-wiggum/commands/help.md +0 -137
- package/plugins/ralph-wiggum/commands/ralph-loop.md +0 -85
- package/plugins/ralph-wiggum/hooks/hooks.json +0 -15
- package/plugins/ralph-wiggum/hooks/stop-hook.sh +0 -191
- package/plugins/ralph-wiggum/plugin.js +0 -280
- package/plugins/ralph-wiggum/src/goalState.js +0 -258
- package/plugins/ralph-wiggum/src/sentinels.js +0 -76
- package/plugins/ralph-wiggum/src/stopHookRunner.js +0 -104
- package/plugins/ralph-wiggum/src/verificationGate.js +0 -202
- package/plugins/workflow-runner/src/miniReAct.js +0 -223
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* 支持的 step kind(loader 已校验):
|
|
13
13
|
* - tool / llm / assert —— 原子动作(execXxxStep 返回 StepResult)
|
|
14
|
-
* - agent / skill —— 有界 TAOR:
|
|
14
|
+
* - agent / skill —— 有界 TAOR:runQuery(agent) / runQuery(skill);
|
|
15
15
|
* 透传子事件(stage_change/text/reasoning/tool_*)
|
|
16
16
|
* - branch / loop / pause —— 控制流,直接在 runSteps 内处理(不走 executor)
|
|
17
17
|
* - parallel / vote —— 并发 / 投票门,也在 runSteps 内处理
|
|
@@ -41,7 +41,49 @@ import { WorkflowState } from './workflowState.js';
|
|
|
41
41
|
* (-p 非交互模式另由 src/cli/print.ts 把通用 warning 事件落 stderr。)
|
|
42
42
|
*/
|
|
43
43
|
const WF_PLUGIN_ID = 'workflow-runner';
|
|
44
|
+
/**
|
|
45
|
+
* 从工作流 inputs 定义 + 实际值中提取用户的原始目标,
|
|
46
|
+
* 构建紧凑的目标锚定字符串。
|
|
47
|
+
*
|
|
48
|
+
* 提取策略:
|
|
49
|
+
* 1. 优先取名为 "goal" 的输入(plan.yaml / ralph-loop.yaml 等通用约定)
|
|
50
|
+
* 2. 其次取第一个 required: true 的 string 类型输入
|
|
51
|
+
* 3. 都没有 → 返回空字符串(executor 自动跳过)
|
|
52
|
+
*
|
|
53
|
+
* 锚定字符串格式紧凑(~60-150 字符),注入每步 prompt 前置,
|
|
54
|
+
* 确保 LLM 在任何步骤都能"看到"用户原始目标,避免长上下文稀释。
|
|
55
|
+
*/
|
|
56
|
+
function buildGoalAnchor(inputDefs, inputValues) {
|
|
57
|
+
let goalValue;
|
|
58
|
+
// 策略 1:优先 "goal"
|
|
59
|
+
const goalInput = inputDefs.find((d) => d.name === 'goal' && d.type === 'string');
|
|
60
|
+
if (goalInput) {
|
|
61
|
+
const v = inputValues[goalInput.name];
|
|
62
|
+
if (typeof v === 'string' && v.trim()) {
|
|
63
|
+
goalValue = v.trim();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 策略 2:fallback 第一个 required string
|
|
67
|
+
if (!goalValue) {
|
|
68
|
+
const firstRequired = inputDefs.find((d) => d.required && d.type === 'string');
|
|
69
|
+
if (firstRequired) {
|
|
70
|
+
const v = inputValues[firstRequired.name];
|
|
71
|
+
if (typeof v === 'string' && v.trim()) {
|
|
72
|
+
goalValue = v.trim();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!goalValue)
|
|
77
|
+
return '';
|
|
78
|
+
// 截断过长目标(>200 字符时保留首尾,中间用 … 连接)
|
|
79
|
+
const display = goalValue.length > 200
|
|
80
|
+
? `${goalValue.slice(0, 120)}…${goalValue.slice(-60)}`
|
|
81
|
+
: goalValue;
|
|
82
|
+
return `> 🎯 用户原始目标:${display}\n> ⚠️ 所有操作必须直接回应此目标,不可偏离或遗忘。`;
|
|
83
|
+
}
|
|
44
84
|
function stepKind(step) {
|
|
85
|
+
if (step.type === 'break')
|
|
86
|
+
return 'break';
|
|
45
87
|
if (step.type)
|
|
46
88
|
return step.type;
|
|
47
89
|
if (step.tool)
|
|
@@ -78,7 +120,7 @@ function bindCapture(captureMap, result, vars) {
|
|
|
78
120
|
*/
|
|
79
121
|
async function* dispatchActionStep(step, vars, ctx) {
|
|
80
122
|
if (step.type === 'assert')
|
|
81
|
-
return execAssertStep(step, vars);
|
|
123
|
+
return execAssertStep(step, vars, ctx);
|
|
82
124
|
if (step.type === 'verify')
|
|
83
125
|
return await execVerifyStep(step, vars);
|
|
84
126
|
if (step.tool)
|
|
@@ -182,8 +224,63 @@ async function* runSteps(steps, vars, ctx, state) {
|
|
|
182
224
|
};
|
|
183
225
|
continue;
|
|
184
226
|
}
|
|
185
|
-
// ---------- 控制流:loop ----------
|
|
227
|
+
// ---------- 控制流:loop(for-each 或 while) ----------
|
|
186
228
|
if (step.type === 'loop') {
|
|
229
|
+
const isWhileMode = step.over === undefined;
|
|
230
|
+
const maxIter = step.max_iterations ?? 200; // while 模式安全上限
|
|
231
|
+
const useFreshContext = step.fresh_context === true;
|
|
232
|
+
// fresh context:循环开始时 snapshot 当前 history
|
|
233
|
+
let baseHistorySnapshot = null;
|
|
234
|
+
if (useFreshContext) {
|
|
235
|
+
baseHistorySnapshot = ctx.history.slice();
|
|
236
|
+
}
|
|
237
|
+
if (isWhileMode) {
|
|
238
|
+
// ===== WHILE 循环模式 =====
|
|
239
|
+
const asName = step.as ?? '_iter';
|
|
240
|
+
let iterCount = 0;
|
|
241
|
+
let shouldBreak = false;
|
|
242
|
+
while (iterCount < maxIter) {
|
|
243
|
+
if (ctx.signal?.aborted) {
|
|
244
|
+
yield { type: 'interrupted' };
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
// 注入迭代变量
|
|
248
|
+
vars.push();
|
|
249
|
+
vars.set(asName, iterCount);
|
|
250
|
+
vars.set(`${asName}_idx`, iterCount);
|
|
251
|
+
// fresh context:每轮重置 history 到循环入口快照
|
|
252
|
+
if (useFreshContext && baseHistorySnapshot) {
|
|
253
|
+
ctx.history.length = 0;
|
|
254
|
+
ctx.history.push(...baseHistorySnapshot);
|
|
255
|
+
}
|
|
256
|
+
yield {
|
|
257
|
+
type: 'plugin_progress',
|
|
258
|
+
pluginId: WF_PLUGIN_ID,
|
|
259
|
+
current: iterCount + 1,
|
|
260
|
+
max: maxIter,
|
|
261
|
+
message: `while-loop ${step.id} 迭代 ${iterCount + 1}/${maxIter}`,
|
|
262
|
+
};
|
|
263
|
+
const childHalted = yield* runSteps(step.do ?? [], vars, ctx, state);
|
|
264
|
+
vars.pop();
|
|
265
|
+
if (childHalted) {
|
|
266
|
+
// 区分 break 退出和 error/halt 退出
|
|
267
|
+
// break 由 runSteps 内部处理,返回 true 但不 yield error
|
|
268
|
+
// 这里统一返回 true(跳出外层循环)
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
iterCount++;
|
|
272
|
+
}
|
|
273
|
+
await state.appendProgress(`✓ ${step.id} (while ×${iterCount}, 达到上限 ${maxIter})`);
|
|
274
|
+
await state.writeVars(vars.snapshot());
|
|
275
|
+
yield {
|
|
276
|
+
type: 'workflow_step_end',
|
|
277
|
+
id: step.id,
|
|
278
|
+
ok: true,
|
|
279
|
+
output: `while loop ${iterCount} iterations (max=${maxIter})`,
|
|
280
|
+
};
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
// ===== FOR-EACH 循环模式(原有逻辑) =====
|
|
187
284
|
let arr;
|
|
188
285
|
try {
|
|
189
286
|
arr = evalExpr(step.over ?? '[]', vars);
|
|
@@ -212,6 +309,11 @@ async function* runSteps(steps, vars, ctx, state) {
|
|
|
212
309
|
vars.push();
|
|
213
310
|
vars.set(asName, arr[idx]);
|
|
214
311
|
vars.set(`${asName}_idx`, idx);
|
|
312
|
+
// fresh context 也支持 for-each 模式
|
|
313
|
+
if (useFreshContext && baseHistorySnapshot) {
|
|
314
|
+
ctx.history.length = 0;
|
|
315
|
+
ctx.history.push(...baseHistorySnapshot);
|
|
316
|
+
}
|
|
215
317
|
const childHalted = yield* runSteps(step.do ?? [], vars, ctx, state);
|
|
216
318
|
vars.pop();
|
|
217
319
|
if (childHalted)
|
|
@@ -392,6 +494,35 @@ async function* runSteps(steps, vars, ctx, state) {
|
|
|
392
494
|
};
|
|
393
495
|
continue;
|
|
394
496
|
}
|
|
497
|
+
// ---------- 控制流:break(循环退出信号) ----------
|
|
498
|
+
//
|
|
499
|
+
// 仅在 loop 的 do[] 内有效。命中时跳出最近一层循环。
|
|
500
|
+
// 通常配合 when 条件使用(如检测到 DONE 哨兵时跳出);
|
|
501
|
+
// 无条件 break 极少用(等价于只跑一轮的循环)。
|
|
502
|
+
if (step.type === 'break') {
|
|
503
|
+
let shouldBreak = true;
|
|
504
|
+
if (step.when) {
|
|
505
|
+
try {
|
|
506
|
+
shouldBreak = Boolean(evalExpr(step.when, vars));
|
|
507
|
+
}
|
|
508
|
+
catch (e) {
|
|
509
|
+
// when 表达式错误 → 不跳出,当 skipped 处理
|
|
510
|
+
shouldBreak = false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (shouldBreak) {
|
|
514
|
+
await state.appendProgress(`↵ ${step.id}: break(跳出循环)`);
|
|
515
|
+
yield { type: 'workflow_step_end', id: step.id, ok: true, output: 'break' };
|
|
516
|
+
return true; // ← 关键:返回 true 让外层 runSteps / loop 终止
|
|
517
|
+
}
|
|
518
|
+
// 条件不满足 → 当 skip 处理,继续下一步
|
|
519
|
+
yield {
|
|
520
|
+
type: 'workflow_step_skipped',
|
|
521
|
+
id: step.id,
|
|
522
|
+
reason: 'break 条件不满足',
|
|
523
|
+
};
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
395
526
|
// ---------- 普通动作 step:tool / llm / skill / assert ----------
|
|
396
527
|
try {
|
|
397
528
|
const result = yield* dispatchActionStep(step, vars, ctx);
|
|
@@ -499,8 +630,12 @@ export async function* runWorkflow(def, inputs, ctx) {
|
|
|
499
630
|
// onError:'recover' 用:把工作流总目标(description)透传给恢复 agent,
|
|
500
631
|
// 让它知道「这条工作流到底想干什么」。已有 goal 则不覆盖。
|
|
501
632
|
const runCtx = { ...ctx, goal: ctx.goal ?? def.description };
|
|
633
|
+
// 通用目标锚定:从 inputs 中提取用户的原始目标,
|
|
634
|
+
// 注入 RunContext.goalAnchor,让 agent/llm/skill 步自动前置到每步 prompt。
|
|
635
|
+
// 提取策略:优先 "goal" 命名的输入 → 其次第一个 required string 输入。
|
|
636
|
+
runCtx.goalAnchor = buildGoalAnchor(def.inputs ?? [], inputs);
|
|
502
637
|
const state = new WorkflowState(getWorkingDir());
|
|
503
|
-
await state.init(def.name, inputs);
|
|
638
|
+
await state.init(def.name, inputs, def.steps);
|
|
504
639
|
yield {
|
|
505
640
|
type: 'workflow_start',
|
|
506
641
|
name: def.name,
|
|
@@ -528,38 +663,12 @@ export async function* runWorkflow(def, inputs, ctx) {
|
|
|
528
663
|
max: def.steps.length,
|
|
529
664
|
message: `✓ workflow ${def.name} 完成`,
|
|
530
665
|
};
|
|
666
|
+
// v0.9 重构:不再需要 assistant_message 摘要注入。
|
|
667
|
+
// agent/llm/skill 步已通过 runQuery(TAOR) 直接写入共享 ctx.history,
|
|
668
|
+
// 每步的完整对话上下文(user→assistant→tool→...)都自然积累在 history 中。
|
|
669
|
+
// 后续对话和用户调整都能看到完整的工作流执行过程。
|
|
531
670
|
yield { type: 'workflow_done', name: def.name, vars: vars.snapshot() };
|
|
532
671
|
succeeded = true;
|
|
533
|
-
// ---- 层1:写入摘要到对话历史(让后续对话记住这次 workflow 做了什么)----
|
|
534
|
-
const snapshot = vars.snapshot();
|
|
535
|
-
const summaryText = typeof snapshot.summary_text === 'string'
|
|
536
|
-
? snapshot.summary_text
|
|
537
|
-
: '';
|
|
538
|
-
// 收集关键产出物(从 vars 中提取文件路径)
|
|
539
|
-
const artifacts = [];
|
|
540
|
-
if (typeof snapshot.plan_text === 'string')
|
|
541
|
-
artifacts.push('./PLAN.md');
|
|
542
|
-
// 检查是否有执行结果(execute_plan 步的产出)
|
|
543
|
-
if (typeof snapshot.execution_result === 'string') {
|
|
544
|
-
// 尝试从执行结果中提取文件路径(常见模式:产出物路径)
|
|
545
|
-
const fileMatches = snapshot.execution_result.match(/`[^`]*\.(pptx|docx|pdf|xlsx|md|ts|js|py)[^`]*`/g);
|
|
546
|
-
if (fileMatches)
|
|
547
|
-
artifacts.push(...fileMatches.map((f) => f.replace(/`/g, '')));
|
|
548
|
-
}
|
|
549
|
-
const summaryLines = [
|
|
550
|
-
`**工作流「${def.name}」执行完成**`,
|
|
551
|
-
``,
|
|
552
|
-
`- 步骤: ${def.steps.length}/${def.steps.length} 全部完成`,
|
|
553
|
-
...(artifacts.length > 0 ? [`- 产出物: ${artifacts.join(', ')}`] : []),
|
|
554
|
-
...(summaryText ? [`- 摘要: ${summaryText}`] : []),
|
|
555
|
-
];
|
|
556
|
-
yield {
|
|
557
|
-
type: 'assistant_message',
|
|
558
|
-
message: {
|
|
559
|
-
role: 'assistant',
|
|
560
|
-
content: summaryLines.join('\n'),
|
|
561
|
-
},
|
|
562
|
-
};
|
|
563
672
|
}
|
|
564
673
|
finally {
|
|
565
674
|
// 成功时归档(保留 vars.json/progress.md 供后续回溯);失败/中断时保留原状供排查
|
|
@@ -2,53 +2,82 @@
|
|
|
2
2
|
* ============================================================
|
|
3
3
|
* plugins/workflow-runner/src/stepExecutors/agent.ts —— agent 步骤执行器
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* v0.9 重构:不再使用独立 miniReAct 子循环,而是委托主 TAOR 引擎
|
|
6
|
+
* (runQuery / src/loop.ts)执行。这样做的好处:
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* 1. 上下文自然积累:每步的完整对话(user→assistant→tool→...)
|
|
9
|
+
* 都写入共享 history,后续步骤和用户对话都能看到
|
|
10
|
+
* 2. 工具/skills/插件信息来自系统提示词(history[0]),无需重复组装
|
|
11
|
+
* 3. 压缩自动生效:history 增长时 autoCompact / reactiveCompact 自动触发
|
|
12
|
+
* 4. Token 消耗透明:TUI status 行实时显示真实 token 数
|
|
13
|
+
* 5. 中断后上下文完整:已完成的步骤全在 history 里,可继续/调整
|
|
14
|
+
*
|
|
15
|
+
* 与 llm 步的区别:
|
|
16
|
+
* - agent 有 maxTurns 上限(默认 12),允许多轮工具调用
|
|
17
|
+
* - agent 的产出经过验证闸门(interpretMiniReActResult)
|
|
18
|
+
* - llm 只跑单轮 chat(maxTurns=1)
|
|
12
19
|
* ============================================================
|
|
13
20
|
*/
|
|
21
|
+
import { runQuery } from '../../../../src/loop.js';
|
|
14
22
|
import { interpolate } from '../expressions.js';
|
|
15
|
-
import {
|
|
16
|
-
import { AGENT_OPENING, buildWorkflowAgentSystem, describeCaptureContract, interpretMiniReActResult, } from '../agentCommon.js';
|
|
23
|
+
import { describeCaptureContract, interpretMiniReActResult, } from '../agentCommon.js';
|
|
17
24
|
/**
|
|
18
|
-
* 执行一个 agent
|
|
25
|
+
* 执行一个 agent 步骤:构造 step prompt → 委托 runQuery(TAOR) → 提取结果。
|
|
19
26
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* - Plugin 列表(含命令和描述)
|
|
26
|
-
* 这解决了之前 workflow 模式下模型不知道有 pptx/pdf 等 skill 的问题。
|
|
27
|
+
* runQuery 会:
|
|
28
|
+
* 1. 把 prompt 作为 user message push 进 ctx.history
|
|
29
|
+
* 2. 跑 TAOR 循环(调 LLM → 执行工具 → 喂回 → 直到模型不调工具)
|
|
30
|
+
* 3. 把所有 assistant / tool 消息 push 进 ctx.history
|
|
31
|
+
* 4. yield UI 事件(text_delta / tool_start / tool_end / ...)
|
|
27
32
|
*/
|
|
28
33
|
export async function* execAgentStep(step, vars, ctx) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
? allTools.filter((t) => step.allowed_tools.includes(t.name))
|
|
33
|
-
: allTools;
|
|
34
|
-
// ✅ Phase 2: 异步构建完整 system prompt(含 skill/plugin 列表)
|
|
35
|
-
const systemContent = await buildWorkflowAgentSystem(ctx.cwd, // 新增:传入 cwd 用于 skill/plugin 扫描
|
|
36
|
-
AGENT_OPENING, tools);
|
|
37
|
-
const userMsg = interpolate(step.agent ?? '', vars) +
|
|
34
|
+
const maxTurns = step.maxTurns ?? 12;
|
|
35
|
+
// 构造用户提示:包含步骤指令 + 变量 + capture 约束 + 内部标记防护
|
|
36
|
+
const userPrompt = interpolate(step.agent ?? '', vars) +
|
|
38
37
|
describeCaptureContract(step) +
|
|
39
|
-
(step.input ? `\n\n上下文变量:${interpolate(step.input, vars)}` : '')
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
(step.input ? `\n\n上下文变量:${interpolate(step.input, vars)}` : '') +
|
|
39
|
+
'\n\n⚠️ 输出规范:直接输出你的工作成果即可,不要在输出中包含任何类似「放弃」、' +
|
|
40
|
+
'「GIVEUP」、' +
|
|
41
|
+
'「__AGENT__」或' +
|
|
42
|
+
'「[wf-internal」的内部标记文本。';
|
|
43
|
+
// 如果有 output_schema,把 schema 约束嵌入 prompt(runQuery 不原生支持 responseFormat)
|
|
44
|
+
const schemaHint = step.output_schema
|
|
45
|
+
? `\n\n【结构化输出约束】请严格按照以下 JSON Schema 输出结果(不要包裹在 markdown 代码块中):\n${JSON.stringify(step.output_schema, null, 2)}`
|
|
46
|
+
: '';
|
|
47
|
+
const fullPrompt = userPrompt + schemaHint;
|
|
48
|
+
// 通用目标锚定:如果 runner 提供了 goalAnchor(从 inputs 自动提取),
|
|
49
|
+
// 前置注入到 prompt。确保每步 LLM 都能看到用户原始目标,
|
|
50
|
+
// 避免长上下文 / fresh_context 导致目标被稀释或遗忘。
|
|
51
|
+
const effectivePrompt = ctx.goalAnchor
|
|
52
|
+
? `${ctx.goalAnchor}\n\n${fullPrompt}`
|
|
53
|
+
: fullPrompt;
|
|
54
|
+
// 记录 runQuery 开始前的 history 长度,用于之后提取本步的 assistant 输出
|
|
55
|
+
const historyLenBefore = ctx.history.length;
|
|
56
|
+
// 委托 TAOR 引擎执行(携带 sessionState 以启用上下文压缩)
|
|
57
|
+
yield* runQuery(effectivePrompt, {
|
|
48
58
|
provider: ctx.provider,
|
|
59
|
+
history: ctx.history,
|
|
49
60
|
signal: ctx.signal,
|
|
61
|
+
maxTurns,
|
|
62
|
+
chat: ctx.chat,
|
|
63
|
+
executeTool: ctx.executeTool,
|
|
64
|
+
sessionState: ctx.sessionState,
|
|
50
65
|
});
|
|
51
|
-
|
|
66
|
+
// 从 history 中提取本步产生的最后一条 assistant message 内容(用于 capture 绑定)
|
|
67
|
+
let rawText = '';
|
|
68
|
+
for (let i = ctx.history.length - 1; i >= historyLenBefore; i--) {
|
|
69
|
+
const msg = ctx.history[i];
|
|
70
|
+
if (msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.trim()) {
|
|
71
|
+
rawText = msg.content;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// 验证闸门:判断结果是"真交付"还是"放弃"
|
|
76
|
+
const stepResult = {
|
|
77
|
+
raw: { text: rawText, result: rawText },
|
|
78
|
+
preview: rawText.length > 200 ? `${rawText.slice(0, 200)}...` : rawText,
|
|
79
|
+
};
|
|
80
|
+
const outcome = interpretMiniReActResult(stepResult, !!step.output_schema);
|
|
52
81
|
if (!outcome.ok)
|
|
53
82
|
throw new Error(`agent 步骤放弃: ${outcome.reason}`);
|
|
54
83
|
return outcome.value;
|
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
* src/workflows/stepExecutors/assert.ts —— assert step 执行
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
5
|
* evalExpr(condition) → 真值通过;假值抛错(带 onFail 自定义信息)。
|
|
6
|
+
*
|
|
7
|
+
* v0.9.3(上下文统一修复):assert 步骤通过后也写入 ctx.history,
|
|
8
|
+
* 让后续 LLM 步骤能看到验证通过的信息(如"文件已存在""变量值符合预期"等)。
|
|
6
9
|
* ============================================================
|
|
7
10
|
*/
|
|
8
11
|
import { evalExpr, interpolate } from '../expressions.js';
|
|
9
|
-
export function execAssertStep(step, vars) {
|
|
12
|
+
export function execAssertStep(step, vars, ctx) {
|
|
10
13
|
if (typeof step.condition !== 'string') {
|
|
11
14
|
throw new Error(`step ${step.id}: assert 缺少 condition`);
|
|
12
15
|
}
|
|
13
16
|
let ok;
|
|
14
17
|
try {
|
|
15
|
-
// P2:表达式上下文直接 evalExpr(${} 由引擎原生处理),不再"先插值再 eval"。
|
|
16
18
|
ok = Boolean(evalExpr(step.condition, vars));
|
|
17
19
|
}
|
|
18
20
|
catch (e) {
|
|
@@ -24,5 +26,15 @@ export function execAssertStep(step, vars) {
|
|
|
24
26
|
: `assertion failed: ${step.condition}`;
|
|
25
27
|
throw new Error(msg);
|
|
26
28
|
}
|
|
29
|
+
// ════ v0.9.3:上下文统一 —— assert 通过也留痕 ════
|
|
30
|
+
// 让后续 LLM 步骤知道某个前置条件/不变量已确认满足。
|
|
31
|
+
if (ctx?.history) {
|
|
32
|
+
ctx.history.push({
|
|
33
|
+
role: 'assistant',
|
|
34
|
+
content: `[workflow:${step.id}] ✅ 断言通过:${step.condition}`,
|
|
35
|
+
timestamp: Date.now(),
|
|
36
|
+
id: crypto.randomUUID(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
27
39
|
return { raw: { ok: true }, preview: 'assert passed' };
|
|
28
40
|
}
|
|
@@ -1,132 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ============================================================
|
|
3
|
-
* plugins/workflow-runner/src/stepExecutors/llm.ts —— llm
|
|
3
|
+
* plugins/workflow-runner/src/stepExecutors/llm.ts —— llm 步骤执行器
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - 自带独立 system + user 两条消息,不污染主 history
|
|
8
|
-
* - AsyncGenerator:非 schema 模式把 text_delta 流式 yield 给 UI(答案是主角,
|
|
9
|
-
* 消除 TUI"无声"),最终仍 return StepResult;schema 模式静默累积 JSON 不回显。
|
|
10
|
-
* reasoning 字段在这里忽略;只关心最终文本
|
|
5
|
+
* v0.9 重构:不再使用独立 miniReAct 子循环或裸 chat(),而是委托主 TAOR 引擎
|
|
6
|
+
* (runQuery / src/loop.ts)执行。
|
|
11
7
|
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
8
|
+
* 与 agent 步的区别:
|
|
9
|
+
* - llm 默认单轮(maxTurns=1),不期望工具调用(但 runQuery 允许模型自主决定)
|
|
10
|
+
* - 有 context_files 时允许少量轮次读取文件后输出
|
|
11
|
+
* - output_schema 嵌入 prompt 作为结构化约束
|
|
12
|
+
* - 不经过 interpretMiniReActResult 验证闸门(直接信任 LLM 输出)
|
|
16
13
|
*
|
|
17
14
|
* capture:
|
|
18
|
-
* - `{ text: var_name }`
|
|
19
|
-
* - `{
|
|
20
|
-
* - `{ <field>: var_name }` 当 output_schema 命中时按 raw 上的字段名取
|
|
21
|
-
*
|
|
22
|
-
* ADR-05: 当 step.output_schema 存在 → 走 provider-native structured output:
|
|
23
|
-
* - 通过 chat() 的 responseFormat 参数下发 JSON Schema
|
|
24
|
-
* - 收到的文本必须解析为 JSON 对象,整对象作为 raw 暴露给 capture
|
|
25
|
-
* - system prompt 切换为严格 JSON 模式,禁止 markdown 代码块包裹
|
|
15
|
+
* - `{ text: var_name }` 绑生成文本
|
|
16
|
+
* - `{ <field>: var_name }` 当 output_schema 存在时按 JSON 字段取值
|
|
26
17
|
* ============================================================
|
|
27
18
|
*/
|
|
28
|
-
import {
|
|
29
|
-
import { interpolate } from '../expressions.js';
|
|
19
|
+
import { runQuery } from '../../../../src/loop.js';
|
|
30
20
|
import { extractJsonPayload } from '../jsonPayload.js';
|
|
31
|
-
import {
|
|
32
|
-
import { assembleCapabilityHint } from '../../../../src/prompts/promptAssembly.js';
|
|
33
|
-
const LLM_STEP_SYSTEM_PLAIN = '你正在被一个 workflow 调用。只输出本步骤要求的内容本身,不要寒暄、不要列要求复述、不要工具调用语法。';
|
|
34
|
-
const LLM_STEP_SYSTEM_SCHEMA = `你正在被一个 workflow 调用。
|
|
35
|
-
要求:
|
|
36
|
-
1. 严格按指定 JSON Schema 输出有效 JSON,**不要 markdown 代码块包裹**(不要 \`\`\`json 也不要 \`\`\`)
|
|
37
|
-
2. 不要寒暄、不要解释、不要额外字段
|
|
38
|
-
3. 字段缺失或类型不符会导致 workflow 失败
|
|
39
|
-
4. 字符串内特殊字符必须正确转义(双引号 \\", 反斜杠 \\\\, 换行 \\n)`;
|
|
40
|
-
const LLM_STEP_SYSTEM_WITH_TOOLS = `你正在被一个 workflow 调用。你被授予了有限的工具(通常是 Read)以查询提供的上下文文件。
|
|
41
|
-
|
|
42
|
-
工作流程:
|
|
43
|
-
1. 先用 Read 工具读取你需要的文件(按需读取,**不要全部读完再思考**——大文件分段读、按 hint 选读)
|
|
44
|
-
2. 基于读到的内容生成最终回答
|
|
45
|
-
3. 不要寒暄、不要复述要求、不要解释你的推理过程
|
|
46
|
-
4. 完成后**不要**再调用工具,直接输出最终回答
|
|
47
|
-
|
|
48
|
-
如果 workflow 要求结构化输出,请在最终回答里严格遵循 JSON Schema(无 markdown 代码块包裹)。`;
|
|
21
|
+
import { interpolate } from '../expressions.js';
|
|
49
22
|
export async function* execLlmStep(step, vars, ctx) {
|
|
50
23
|
if (typeof step.llm !== 'string')
|
|
51
24
|
throw new Error(`step ${step.id}: 缺少 llm 字段`);
|
|
52
25
|
const prompt = interpolate(step.llm, vars);
|
|
53
|
-
//
|
|
26
|
+
// 有 context_files → 在 prompt 中附上文件列表,runQuery 的 TAOR 循环中
|
|
27
|
+
// 模型可自主用 Read 工具读取(ALL_TOOLS 全量可用)
|
|
28
|
+
let fullPrompt = prompt;
|
|
54
29
|
if (step.context_files && step.context_files.length > 0) {
|
|
55
30
|
const fileList = step.context_files
|
|
56
31
|
.map((f) => `- ${interpolate(f.path, vars)}${f.hint ? ` — ${f.hint}` : ''}`)
|
|
57
32
|
.join('\n');
|
|
58
|
-
|
|
59
|
-
const allowedNames = step.allowed_tools ?? ['Read'];
|
|
60
|
-
const allowedTools = ALL_TOOLS.filter((t) => allowedNames.includes(t.name));
|
|
61
|
-
if (allowedTools.length === 0) {
|
|
62
|
-
throw new Error(`step ${step.id}: context_files 启用但 allowed_tools=${JSON.stringify(allowedNames)} 没有匹配的可用工具`);
|
|
63
|
-
}
|
|
64
|
-
return yield* runMiniReAct({
|
|
65
|
-
messages: [
|
|
66
|
-
{ role: 'system', content: LLM_STEP_SYSTEM_WITH_TOOLS },
|
|
67
|
-
{ role: 'user', content: augmented },
|
|
68
|
-
],
|
|
69
|
-
tools: allowedTools,
|
|
70
|
-
output_schema: step.output_schema,
|
|
71
|
-
provider: ctx.provider,
|
|
72
|
-
signal: ctx.signal,
|
|
73
|
-
maxTurns: step.maxTurns ?? 10,
|
|
74
|
-
});
|
|
33
|
+
fullPrompt = `${prompt}\n\n**以下上下文文件可用,请用 Read 工具按需读取**:\n${fileList}`;
|
|
75
34
|
}
|
|
76
|
-
//
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
// response_format.json_schema(火山 GLM 等不支持、会被 client.ts 退化掉 response_format),
|
|
80
|
-
// 模型都知道确切字段名与结构 —— 退化路径也能产出正确形状的 JSON,而非瞎猜字段。
|
|
81
|
-
const schemaHint = useSchema
|
|
82
|
-
? `\n\n最终只输出匹配以下 JSON Schema 的 JSON 对象本身(不要解释、不要 markdown 代码块):\n${JSON.stringify(step.output_schema)}`
|
|
35
|
+
// output_schema 约束嵌入 prompt
|
|
36
|
+
const schemaHint = step.output_schema
|
|
37
|
+
? `\n\n【结构化输出约束】请严格按照以下 JSON Schema 输出有效 JSON 对象本身(不要 markdown 代码块包裹):\n${JSON.stringify(step.output_schema, null, 2)}`
|
|
83
38
|
: '';
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const responseFormat = useSchema
|
|
96
|
-
? {
|
|
97
|
-
type: 'json_schema',
|
|
98
|
-
json_schema: {
|
|
99
|
-
name: `${step.id}_output`,
|
|
100
|
-
schema: step.output_schema,
|
|
101
|
-
strict: true,
|
|
102
|
-
},
|
|
103
|
-
}
|
|
104
|
-
: undefined;
|
|
105
|
-
let text = '';
|
|
106
|
-
for await (const ev of chat({
|
|
39
|
+
fullPrompt += schemaHint;
|
|
40
|
+
// 通用目标锚定:同 agent.ts,确保每步 LLM 不偏离原始目标
|
|
41
|
+
const effectivePrompt = ctx.goalAnchor
|
|
42
|
+
? `${ctx.goalAnchor}\n\n${fullPrompt}`
|
|
43
|
+
: fullPrompt;
|
|
44
|
+
// llm 步默认单轮;有 context_files 时允许多轮(读文件→输出)
|
|
45
|
+
const maxTurns = step.maxTurns ?? (step.context_files?.length ? 10 : 1);
|
|
46
|
+
// 记录 history 长度用于提取本步输出
|
|
47
|
+
const historyLenBefore = ctx.history.length;
|
|
48
|
+
// 委托 TAOR 引擎执行(携带 sessionState 以启用上下文压缩)
|
|
49
|
+
yield* runQuery(effectivePrompt, {
|
|
107
50
|
provider: ctx.provider,
|
|
108
|
-
|
|
109
|
-
tools: [],
|
|
110
|
-
responseFormat,
|
|
51
|
+
history: ctx.history,
|
|
111
52
|
signal: ctx.signal,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
53
|
+
maxTurns,
|
|
54
|
+
chat: ctx.chat,
|
|
55
|
+
executeTool: ctx.executeTool,
|
|
56
|
+
sessionState: ctx.sessionState,
|
|
57
|
+
});
|
|
58
|
+
// 从 history 提取本步最后一条 assistant 消息
|
|
59
|
+
let rawText = '';
|
|
60
|
+
for (let i = ctx.history.length - 1; i >= historyLenBefore; i--) {
|
|
61
|
+
const msg = ctx.history[i];
|
|
62
|
+
if (msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.trim()) {
|
|
63
|
+
rawText = msg.content;
|
|
64
|
+
break;
|
|
122
65
|
}
|
|
123
66
|
}
|
|
124
|
-
const trimmed =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// 头注与 src/llm/client.ts:200),故先剥思维链/前后散文再 parse;报错仍带原文便于排查。
|
|
129
|
-
const payload = extractJsonPayload(text);
|
|
67
|
+
const trimmed = rawText.trim();
|
|
68
|
+
// 结构化输出模式:解析 JSON
|
|
69
|
+
if (step.output_schema) {
|
|
70
|
+
const payload = extractJsonPayload(trimmed);
|
|
130
71
|
let parsed;
|
|
131
72
|
try {
|
|
132
73
|
parsed = JSON.parse(payload);
|
|
@@ -142,7 +83,7 @@ export async function* execLlmStep(step, vars, ctx) {
|
|
|
142
83
|
preview: `[json] ${trimmed.slice(0, 180)}${trimmed.length > 180 ? '...' : ''}`,
|
|
143
84
|
};
|
|
144
85
|
}
|
|
145
|
-
//
|
|
86
|
+
// 裸文本模式
|
|
146
87
|
return {
|
|
147
88
|
raw: { text: trimmed, result: trimmed },
|
|
148
89
|
preview: trimmed.length > 200 ? `${trimmed.slice(0, 200)}...` : trimmed,
|