minimal-agent 0.7.1 → 0.8.1
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/package.json +1 -1
- package/plugins/ralph-wiggum/plugin.js +1 -1
- package/plugins/workflow-runner/plugin.js +3 -0
- package/plugins/workflow-runner/src/agentCommon.js +22 -5
- package/plugins/workflow-runner/src/index.js +2 -0
- package/plugins/workflow-runner/src/loader.js +12 -0
- package/plugins/workflow-runner/src/recovery.js +4 -2
- package/plugins/workflow-runner/src/runner.js +36 -6
- package/plugins/workflow-runner/src/stepExecutors/agent.js +39 -56
- package/plugins/workflow-runner/src/stepExecutors/llm.js +12 -2
- package/plugins/workflow-runner/src/stepExecutors/verify.js +41 -0
- package/plugins/workflow-runner/src/workflowState.js +30 -2
- package/src/infra/verification.js +228 -0
- package/src/plugins/pluginRunner.js +19 -2
- package/src/prompts/pluginList.js +54 -0
- package/src/prompts/promptAssembly.js +243 -0
- package/src/prompts/skillList.js +15 -20
- package/src/prompts/system.js +12 -167
- package/src/ui/App.js +1 -1
- package/workflows/plan.yaml +59 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimal-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "最小化 Agent 系统 —— 10 工具 + 插件系统 + workflow DSL + 自动压缩 + OpenAI 兼容 + Ink TUI;NodeNext + tsc 原地编译,dev 用 Bun .ts、install 用 Node .js(学习/教学用)",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Bill Wang <leiwang0359@gmail.com>",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
import { dirname } from 'node:path';
|
|
24
24
|
import { runQuery, getWorkingDir, } from '../../src/plugin-sdk.js';
|
|
25
|
+
import { parseVerifyArgs, runVerification } from '../../src/infra/verification.js';
|
|
25
26
|
import { GoalState, Phase } from './src/goalState.js';
|
|
26
|
-
import { parseVerifyArgs, runVerification } from './src/verificationGate.js';
|
|
27
27
|
import { makeCompletionMatcher, hasNeedReplanSentinel, hasNeedGoalRevisionSentinel, extractLearnings, } from './src/sentinels.js';
|
|
28
28
|
import { executeStopHook } from './src/stopHookRunner.js';
|
|
29
29
|
const PLUGIN_NAME = 'ralph-wiggum';
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* 缓存这次 import,所以 cold start 后 dynamic import 只跑一次。
|
|
13
13
|
* ============================================================
|
|
14
14
|
*/
|
|
15
|
+
import { ALL_TOOLS, getWorkingDir } from '../../src/plugin-sdk.js';
|
|
15
16
|
import { runWorkflowFromCommand, runWorkflowsList } from './src/index.js';
|
|
16
17
|
const api = {
|
|
17
18
|
async *runCommand(commandName, args, ctx) {
|
|
@@ -24,6 +25,8 @@ const api = {
|
|
|
24
25
|
provider: ctx.provider,
|
|
25
26
|
history: ctx.history,
|
|
26
27
|
signal: ctx.signal,
|
|
28
|
+
cwd: getWorkingDir(),
|
|
29
|
+
tools: ALL_TOOLS,
|
|
27
30
|
});
|
|
28
31
|
return;
|
|
29
32
|
}
|
|
@@ -7,14 +7,18 @@
|
|
|
7
7
|
* - agent: 节点(stepExecutors/agent.ts)—— 作者显式声明的开放任务
|
|
8
8
|
* 二者本质同构:受工作流目标/上下文指导、用工具随机应变、产出经验证后回灌 capture。
|
|
9
9
|
* 为避免「同一个脑子写两份提示词、行为漂移」,这里收敛它们的**共享件**:
|
|
10
|
-
* 1.
|
|
11
|
-
* 两处只在前面各加一句**角色开场白**(recover=救火 / agent
|
|
10
|
+
* 1. 统一系统提示词基础内核(来自 promptAssembly.ts)—— 含工具/skill/plugin 完整列表;
|
|
11
|
+
* 两处只在前面各加一句**角色开场白**(recover=救火 / agent=自主完成一步),
|
|
12
|
+
* 后面追加共享纪律块。
|
|
12
13
|
* 2. 统一验证闸门(interpretMiniReActResult)—— miniReAct 跑完后,判定「真交付」还是
|
|
13
14
|
* 「放弃/不可信」,**永不把没验证过的产出当成功**。这是 Phase 2 验证闸门的提取。
|
|
14
15
|
* ============================================================
|
|
15
16
|
*/
|
|
17
|
+
import { assembleBasePrompt } from '../../../src/prompts/promptAssembly.js';
|
|
16
18
|
/** agent 显式放弃的唯一标记;与 DISCIPLINE 收尾约束严格对应。recover 与 agent 节点共用。 */
|
|
17
19
|
export const GIVEUP_MARKER = '__AGENT_GIVEUP__';
|
|
20
|
+
/** agent 步骤的角色开场白(与 recovery 的 RECOVER_OPENING 对称) */
|
|
21
|
+
export const AGENT_OPENING = '你是一名资深工程师,被一个确定性工作流临时调来「自主完成一步开放任务」。';
|
|
18
22
|
/** 共享纪律块:所有 workflow 内 agent 的通用行为约束(角色开场白由各调用方拼在前面)。 */
|
|
19
23
|
const DISCIPLINE = `按这个纪律干活:
|
|
20
24
|
1. 先想清楚「这一步要产出什么」,必要时用只读工具(Read / Grep / Glob)做最小探查,别凭空猜、别凭空编。
|
|
@@ -29,9 +33,22 @@ const DISCIPLINE = `按这个纪律干活:
|
|
|
29
33
|
收尾(二选一,硬约束):
|
|
30
34
|
- 真的达成了 → 最终消息**只输出这一步要的结果本身**,不要寒暄、不要复述要求、不要解释推理过程。若要求结构化输出,严格按 JSON Schema、**不要**用 markdown 代码块包裹。
|
|
31
35
|
- 确实做不成 → 最终消息**只输出一行**:\`${GIVEUP_MARKER}: <一句话说明为什么>\`,让工作流诚实地停下。绝不要为了「交差」而编一个假结果。`;
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
/**
|
|
37
|
+
* 拼一个 workflow agent 的完整 system prompt:
|
|
38
|
+
*
|
|
39
|
+
* 角色开场白 + 基础内核(含 skill/plugin/工具完整列表) + 共享纪律
|
|
40
|
+
*
|
|
41
|
+
* 基础内核来自 assembleBasePrompt(),确保 workflow agent 与 TUI 普通对话
|
|
42
|
+
* 看到完全相同的 skill 列表和插件信息。
|
|
43
|
+
*/
|
|
44
|
+
export async function buildWorkflowAgentSystem(cwd, opening, tools) {
|
|
45
|
+
const baseKernel = await assembleBasePrompt({
|
|
46
|
+
cwd,
|
|
47
|
+
tools,
|
|
48
|
+
includeProjectInstructions: false, // workflow 子步骤不需要项目指令
|
|
49
|
+
includePlugins: true, // 插件列表对 agent 也有参考价值
|
|
50
|
+
});
|
|
51
|
+
return `${opening}\n\n${baseKernel}\n\n${DISCIPLINE}`;
|
|
35
52
|
}
|
|
36
53
|
/** 把 step.capture 翻成「下游会把你的结果存成哪些变量」,让 agent 产出对得上形状。 */
|
|
37
54
|
export function describeCaptureContract(step) {
|
|
@@ -188,6 +188,8 @@ export async function* runWorkflowFromCommand(rawArgs, opts) {
|
|
|
188
188
|
provider: opts.provider,
|
|
189
189
|
signal: opts.signal,
|
|
190
190
|
history: opts.history,
|
|
191
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
192
|
+
tools: opts.tools ?? [],
|
|
191
193
|
};
|
|
192
194
|
// 透传 runWorkflow 的事件,并观察是否出现终结事件(error / interrupted = 失败或中断,
|
|
193
195
|
// 与 gateway broker 的 TERMINAL 判定同一套逻辑)。成功(没见到终结事件)则在末尾补一条
|
|
@@ -130,6 +130,7 @@ function validate(obj, file) {
|
|
|
130
130
|
// editor 因 D2 红线禁止 import 后端、手抄了同一份;后端在此演进时该测试会红,提示同步 editor。
|
|
131
131
|
export const CONTROL_FLOW_TYPES = new Set([
|
|
132
132
|
'assert',
|
|
133
|
+
'verify',
|
|
133
134
|
'branch',
|
|
134
135
|
'loop',
|
|
135
136
|
'pause',
|
|
@@ -138,6 +139,7 @@ export const CONTROL_FLOW_TYPES = new Set([
|
|
|
138
139
|
]);
|
|
139
140
|
export const VALID_STEP_TYPES = new Set([
|
|
140
141
|
'assert',
|
|
142
|
+
'verify',
|
|
141
143
|
'branch',
|
|
142
144
|
'loop',
|
|
143
145
|
'pause',
|
|
@@ -207,6 +209,16 @@ function validateSteps(steps, file, pathPrefix) {
|
|
|
207
209
|
if (s.type === 'assert' && s.condition === undefined) {
|
|
208
210
|
throw new WorkflowLoadError(`${p} type=assert 必须带 'condition'`, file);
|
|
209
211
|
}
|
|
212
|
+
if (s.type === 'verify') {
|
|
213
|
+
if (!Array.isArray(s.checks) || s.checks.length === 0) {
|
|
214
|
+
throw new WorkflowLoadError(`${p} type=verify 必须带非空 'checks' 数组`, file);
|
|
215
|
+
}
|
|
216
|
+
for (const [ci, c] of s.checks.entries()) {
|
|
217
|
+
if (typeof c !== 'string' || !c.includes(':')) {
|
|
218
|
+
throw new WorkflowLoadError(`${p}.checks[${ci}] 格式错误,应为 "type:value"(如 "file_exists:./x")`, file);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
210
222
|
if (s.type === 'branch') {
|
|
211
223
|
if (s.then !== undefined && !Array.isArray(s.then)) {
|
|
212
224
|
throw new WorkflowLoadError(`${p}.then 必须是数组`, file);
|
|
@@ -29,9 +29,9 @@ import { ALL_TOOLS } from '../../../src/plugin-sdk.js';
|
|
|
29
29
|
import { buildWorkflowAgentSystem, describeCaptureContract, interpretMiniReActResult, truncate, } from './agentCommon.js';
|
|
30
30
|
import { interpolate, interpolateDeep } from './expressions.js';
|
|
31
31
|
import { runMiniReAct } from './miniReAct.js';
|
|
32
|
+
// RECOVERY_SYSTEM 现在是 async(需要扫描 skill/plugin),在 runRecovery 内部构建
|
|
32
33
|
const RECOVER_OPENING = `你是一名资深的自动化工程师 / SRE,被一个确定性工作流临时调来「救火」。
|
|
33
34
|
工作流里一个原本写死的步骤刚刚执行失败了,现在交给你随机应变——先看错误信息诊断根因,再想办法真正达成这个步骤**本来要的结果**(关注它要的结果,别拘泥它原先怎么写)。`;
|
|
34
|
-
const RECOVERY_SYSTEM = buildWorkflowAgentSystem(RECOVER_OPENING);
|
|
35
35
|
/** 描述失败节点「本来要干什么」,喂给恢复 agent 作为意图说明。 */
|
|
36
36
|
function describeStepIntent(step, vars) {
|
|
37
37
|
if (step.tool) {
|
|
@@ -78,8 +78,10 @@ export async function* runRecovery(step, vars, ctx, errorMsg) {
|
|
|
78
78
|
const tools = allowed && allowed.length > 0
|
|
79
79
|
? ALL_TOOLS.filter((t) => allowed.includes(t.name))
|
|
80
80
|
: ALL_TOOLS;
|
|
81
|
+
// ✅ Phase 2: 异步构建完整 system prompt(含 skill/plugin 列表)
|
|
82
|
+
const systemContent = await buildWorkflowAgentSystem(ctx.cwd, RECOVER_OPENING, tools);
|
|
81
83
|
const messages = [
|
|
82
|
-
{ role: 'system', content:
|
|
84
|
+
{ role: 'system', content: systemContent },
|
|
83
85
|
{ role: 'user', content: userMsg },
|
|
84
86
|
];
|
|
85
87
|
try {
|
|
@@ -27,6 +27,7 @@ import { evalExpr, interpolate } from './expressions.js';
|
|
|
27
27
|
import { runRecovery } from './recovery.js';
|
|
28
28
|
import { execAgentStep } from './stepExecutors/agent.js';
|
|
29
29
|
import { execAssertStep } from './stepExecutors/assert.js';
|
|
30
|
+
import { execVerifyStep } from './stepExecutors/verify.js';
|
|
30
31
|
import { execLlmStep } from './stepExecutors/llm.js';
|
|
31
32
|
import { execSkillStep } from './stepExecutors/skill.js';
|
|
32
33
|
import { execToolStep } from './stepExecutors/tool.js';
|
|
@@ -78,6 +79,8 @@ function bindCapture(captureMap, result, vars) {
|
|
|
78
79
|
async function* dispatchActionStep(step, vars, ctx) {
|
|
79
80
|
if (step.type === 'assert')
|
|
80
81
|
return execAssertStep(step, vars);
|
|
82
|
+
if (step.type === 'verify')
|
|
83
|
+
return await execVerifyStep(step, vars);
|
|
81
84
|
if (step.tool)
|
|
82
85
|
return yield* execToolStep(step, vars, ctx);
|
|
83
86
|
if (step.agent)
|
|
@@ -86,7 +89,7 @@ async function* dispatchActionStep(step, vars, ctx) {
|
|
|
86
89
|
return yield* execLlmStep(step, vars, ctx);
|
|
87
90
|
if (step.skill)
|
|
88
91
|
return yield* execSkillStep(step, vars, ctx);
|
|
89
|
-
throw new Error(`step ${step.id}: 无可执行字段(tool/agent/llm/skill/assert)`);
|
|
92
|
+
throw new Error(`step ${step.id}: 无可执行字段(tool/agent/llm/skill/assert/verify)`);
|
|
90
93
|
}
|
|
91
94
|
/**
|
|
92
95
|
* 递归执行 steps[]。返回 true 表示该 block 已 halt(上游应中止)。
|
|
@@ -527,14 +530,41 @@ export async function* runWorkflow(def, inputs, ctx) {
|
|
|
527
530
|
};
|
|
528
531
|
yield { type: 'workflow_done', name: def.name, vars: vars.snapshot() };
|
|
529
532
|
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
|
+
};
|
|
530
563
|
}
|
|
531
564
|
finally {
|
|
532
|
-
//
|
|
533
|
-
// (current.json / inputs.json / vars.json / progress.md,含失败那步的 ✗ 行)供事后排查
|
|
534
|
-
// ——这正是这些文件的设计目的(workflowState.ts 头注);失败时删掉等于自废武功。
|
|
535
|
-
// 残留不会永久堆积:下次同名 workflow 的 init() 会先 rm,/new 也按 .minimal-agent-* 前缀统一清。
|
|
565
|
+
// 成功时归档(保留 vars.json/progress.md 供后续回溯);失败/中断时保留原状供排查
|
|
536
566
|
if (succeeded)
|
|
537
|
-
await state.
|
|
567
|
+
await state.archive();
|
|
538
568
|
}
|
|
539
569
|
}
|
|
540
570
|
/**
|
|
@@ -1,72 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ============================================================
|
|
3
|
-
* plugins/workflow-runner/src/stepExecutors/agent.ts —— agent
|
|
3
|
+
* plugins/workflow-runner/src/stepExecutors/agent.ts —— agent 步骤执行器
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 思考 → 用工具 → 观察 → 反思 → 再来,直到把这一步要的结果做出来。
|
|
5
|
+
* 执行 workflow 中 type: agent 的步骤:启动一个 miniReAct 子循环,
|
|
6
|
+
* 用工具随机应变地完成开放性任务。
|
|
8
7
|
*
|
|
9
|
-
* 与
|
|
10
|
-
* -
|
|
11
|
-
* - agent
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 与 onError:'recover' 共享 agentCommon 的系统提示词纪律与验证闸门——同一个脑子。
|
|
15
|
-
*
|
|
16
|
-
* 失败语义:agent 放弃 / 空产出 → interpretMiniReActResult 判 ok:false → 本执行器 throw,
|
|
17
|
-
* 走 dispatchActionStep 的正常失败路径,由 step.onError 接管(halt/continue/recover)。
|
|
18
|
-
* —— 与其它动作 step 完全一致:agent 节点自己也能声明 onError:'recover' 被再救一次。
|
|
19
|
-
*
|
|
20
|
-
* 流式:execAgentStep 是 AsyncGenerator,`yield* runMiniReAct(...)` 把子 agent 的
|
|
21
|
-
* text / reasoning / tool_start / tool_end 事件透传到 runWorkflow → UI,内层思考实时可见。
|
|
8
|
+
* 与 llm 步骤的区别:
|
|
9
|
+
* - agent 有工具白名单(allowed_tools 过滤),可以 Read/Write/Grep/...
|
|
10
|
+
* - agent 跑多轮 miniReAct(maxTurns 上限),llm 只跑单轮 chat
|
|
11
|
+
* - agent 的产出经过验证闸门(interpretMiniReActResult),llm 直接透传
|
|
22
12
|
* ============================================================
|
|
23
13
|
*/
|
|
24
|
-
import { ALL_TOOLS } from '../../../../src/plugin-sdk.js';
|
|
25
|
-
import { buildWorkflowAgentSystem, describeCaptureContract, interpretMiniReActResult, } from '../agentCommon.js';
|
|
26
14
|
import { interpolate } from '../expressions.js';
|
|
27
15
|
import { runMiniReAct } from '../miniReAct.js';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
import { AGENT_OPENING, buildWorkflowAgentSystem, describeCaptureContract, interpretMiniReActResult, } from '../agentCommon.js';
|
|
17
|
+
/**
|
|
18
|
+
* 执行一个 agent 步骤。
|
|
19
|
+
*
|
|
20
|
+
* 关键变化(Phase 2 重构):
|
|
21
|
+
* buildWorkflowAgentSystem 现在是 async(因为需要扫描 skill/plugin 目录),
|
|
22
|
+
* agent 步骤的 system prompt 现在包含完整的:
|
|
23
|
+
* - 工具列表 + 使用规范
|
|
24
|
+
* - Skill 列表(含触发词)
|
|
25
|
+
* - Plugin 列表(含命令和描述)
|
|
26
|
+
* 这解决了之前 workflow 模式下模型不知道有 pptx/pdf 等 skill 的问题。
|
|
27
|
+
*/
|
|
31
28
|
export async function* execAgentStep(step, vars, ctx) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
`你这一步要完成的任务:\n${task}` +
|
|
48
|
-
`${filesLine}${captureLine}\n\n` +
|
|
49
|
-
`请用工具把它做出来;确实做不成就按约定输出放弃标记。`;
|
|
50
|
-
// 工具集:默认全量 ALL_TOOLS(真 agency);step.allowed_tools 收窄。
|
|
51
|
-
const allowed = step.allowed_tools;
|
|
52
|
-
const tools = allowed && allowed.length > 0
|
|
53
|
-
? ALL_TOOLS.filter((t) => allowed.includes(t.name))
|
|
54
|
-
: ALL_TOOLS;
|
|
29
|
+
// 工具白名单:allowed_tools 声明时按白名单过滤;未声明时默认全量(不限制)
|
|
30
|
+
const allTools = ctx.tools ?? [];
|
|
31
|
+
const tools = step.allowed_tools
|
|
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) +
|
|
38
|
+
describeCaptureContract(step) +
|
|
39
|
+
(step.input ? `\n\n上下文变量:${interpolate(step.input, vars)}` : '');
|
|
40
|
+
const messages = [
|
|
41
|
+
{ role: 'system', content: systemContent },
|
|
42
|
+
{ role: 'user', content: userMsg },
|
|
43
|
+
];
|
|
55
44
|
const result = yield* runMiniReAct({
|
|
56
|
-
messages
|
|
57
|
-
{ role: 'system', content: AGENT_NODE_SYSTEM },
|
|
58
|
-
{ role: 'user', content: userMsg },
|
|
59
|
-
],
|
|
45
|
+
messages,
|
|
60
46
|
tools,
|
|
61
|
-
|
|
47
|
+
maxTurns: step.maxTurns ?? 12,
|
|
62
48
|
provider: ctx.provider,
|
|
63
49
|
signal: ctx.signal,
|
|
64
|
-
maxTurns: step.maxTurns ?? 12,
|
|
65
50
|
});
|
|
66
|
-
const outcome = interpretMiniReActResult(result, step.output_schema
|
|
67
|
-
if (!outcome.ok)
|
|
68
|
-
|
|
69
|
-
throw new Error(`agent 未完成任务:${outcome.reason}`);
|
|
70
|
-
}
|
|
51
|
+
const outcome = interpretMiniReActResult(result, !!step.output_schema);
|
|
52
|
+
if (!outcome.ok)
|
|
53
|
+
throw new Error(`agent 步骤放弃: ${outcome.reason}`);
|
|
71
54
|
return outcome.value;
|
|
72
55
|
}
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* 消除 TUI"无声"),最终仍 return StepResult;schema 模式静默累积 JSON 不回显。
|
|
10
10
|
* reasoning 字段在这里忽略;只关心最终文本
|
|
11
11
|
*
|
|
12
|
+
* Phase 2 重构:
|
|
13
|
+
* - system prompt 现在追加 CapabilityHint(skill + plugin 轻量列表)
|
|
14
|
+
* - 即使 tools=[] 的 llm 步骤也知道有哪些 skill/plugin 可用,
|
|
15
|
+
* 可以在输出文本中引用或建议用户后续使用(如 draft_plan 提到"建议用 /pptx 执行")
|
|
16
|
+
*
|
|
12
17
|
* capture:
|
|
13
18
|
* - `{ text: var_name }` 绑生成文本(无 output_schema 时)
|
|
14
19
|
* - `{ result: var_name }` 同义
|
|
@@ -24,6 +29,7 @@ import { ALL_TOOLS, chat, } from '../../../../src/plugin-sdk.js';
|
|
|
24
29
|
import { interpolate } from '../expressions.js';
|
|
25
30
|
import { extractJsonPayload } from '../jsonPayload.js';
|
|
26
31
|
import { runMiniReAct } from '../miniReAct.js';
|
|
32
|
+
import { assembleCapabilityHint } from '../../../../src/prompts/promptAssembly.js';
|
|
27
33
|
const LLM_STEP_SYSTEM_PLAIN = '你正在被一个 workflow 调用。只输出本步骤要求的内容本身,不要寒暄、不要列要求复述、不要工具调用语法。';
|
|
28
34
|
const LLM_STEP_SYSTEM_SCHEMA = `你正在被一个 workflow 调用。
|
|
29
35
|
要求:
|
|
@@ -75,10 +81,14 @@ export async function* execLlmStep(step, vars, ctx) {
|
|
|
75
81
|
const schemaHint = useSchema
|
|
76
82
|
? `\n\n最终只输出匹配以下 JSON Schema 的 JSON 对象本身(不要解释、不要 markdown 代码块):\n${JSON.stringify(step.output_schema)}`
|
|
77
83
|
: '';
|
|
84
|
+
// ✅ Phase 2: 追加 CapabilityHint —— 让 llm 步骤知道有哪些 skill/plugin 可用
|
|
85
|
+
// 即使 tools=[] 不能执行工具,模型也能在输出中引用或建议用户后续使用
|
|
86
|
+
const capabilityHint = await assembleCapabilityHint(ctx.cwd);
|
|
87
|
+
const baseSystem = (useSchema ? LLM_STEP_SYSTEM_SCHEMA : LLM_STEP_SYSTEM_PLAIN) + schemaHint;
|
|
78
88
|
const messages = [
|
|
79
89
|
{
|
|
80
90
|
role: 'system',
|
|
81
|
-
content:
|
|
91
|
+
content: baseSystem + capabilityHint,
|
|
82
92
|
},
|
|
83
93
|
{ role: 'user', content: prompt },
|
|
84
94
|
];
|
|
@@ -114,7 +124,7 @@ export async function* execLlmStep(step, vars, ctx) {
|
|
|
114
124
|
const trimmed = text.trim();
|
|
115
125
|
if (useSchema) {
|
|
116
126
|
// strict structured output:解析 JSON 后整个对象作为 raw。
|
|
117
|
-
// 思考型模型即便在 strict 下仍会把
|
|
127
|
+
// 思考型模型即便在 strict 下仍会把 think…</think 写进 content(见 jsonPayload.ts
|
|
118
128
|
// 头注与 src/llm/client.ts:200),故先剥思维链/前后散文再 parse;报错仍带原文便于排查。
|
|
119
129
|
const payload = extractJsonPayload(text);
|
|
120
130
|
let parsed;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* stepExecutors/verify.ts —— verify step 执行
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 复用 src/infra/verification.ts 共享验证基础设施,
|
|
6
|
+
* 支持 shell / file_exists / file_contains / test_count 四种检查。
|
|
7
|
+
*
|
|
8
|
+
* 与 assert 的区别:
|
|
9
|
+
* - assert: evalExpr 表达式(轻量,适合简单文件存在判断)
|
|
10
|
+
* - verify: 完整检查器(重量,支持命令执行/正则匹配/测试计数)
|
|
11
|
+
* ============================================================
|
|
12
|
+
*/
|
|
13
|
+
import { parseCheckString, runVerification } from '../../../../src/infra/verification.js';
|
|
14
|
+
import { interpolate } from '../expressions.js';
|
|
15
|
+
export async function execVerifyStep(step, vars) {
|
|
16
|
+
if (!Array.isArray(step.checks) || step.checks.length === 0) {
|
|
17
|
+
throw new Error(`step ${step.id}: verify 缺少 checks 数组`);
|
|
18
|
+
}
|
|
19
|
+
// 解析并插值每条 check 字符串
|
|
20
|
+
const checks = [];
|
|
21
|
+
for (const raw of step.checks) {
|
|
22
|
+
const interpolated = interpolate(raw, vars);
|
|
23
|
+
const check = parseCheckString(interpolated);
|
|
24
|
+
if (!check) {
|
|
25
|
+
throw new Error(`step ${step.id}: 无法解析 check "${interpolated}",格式应为 "type:value"`);
|
|
26
|
+
}
|
|
27
|
+
checks.push(check);
|
|
28
|
+
}
|
|
29
|
+
// 执行所有检查
|
|
30
|
+
const result = await runVerification(checks);
|
|
31
|
+
if (!result.passed) {
|
|
32
|
+
const msg = step.onFail
|
|
33
|
+
? interpolate(step.onFail, vars)
|
|
34
|
+
: `verify 失败: ${result.summary}`;
|
|
35
|
+
throw new Error(msg);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
raw: { passed: true, summary: result.summary, details: result.details },
|
|
39
|
+
preview: result.summary,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -18,16 +18,19 @@
|
|
|
18
18
|
* 因为执行流程是确定性的,没有 LLM 自规划过程。
|
|
19
19
|
* ============================================================
|
|
20
20
|
*/
|
|
21
|
-
import { mkdir, writeFile, appendFile, rm } from 'node:fs/promises';
|
|
21
|
+
import { mkdir, writeFile, appendFile, rm, rename } from 'node:fs/promises';
|
|
22
22
|
import { join } from 'node:path';
|
|
23
23
|
const STATE_DIR = '.minimal-agent-workflow';
|
|
24
|
+
const DONE_SUFFIX = '.done';
|
|
24
25
|
export class WorkflowState {
|
|
25
26
|
dir;
|
|
26
27
|
constructor(workingDir) {
|
|
27
28
|
this.dir = join(workingDir, STATE_DIR);
|
|
28
29
|
}
|
|
29
30
|
async init(name, inputs) {
|
|
30
|
-
|
|
31
|
+
// 启动新 workflow 时清理旧状态(含之前的归档)
|
|
32
|
+
await rm(this.dir, { recursive: true, force: true }).catch(() => { });
|
|
33
|
+
await rm(this.dir + DONE_SUFFIX, { recursive: true, force: true }).catch(() => { });
|
|
31
34
|
await mkdir(this.dir, { recursive: true });
|
|
32
35
|
await writeFile(join(this.dir, 'current.json'), JSON.stringify({ name, startedAt: new Date().toISOString() }, null, 2), 'utf8');
|
|
33
36
|
await writeFile(join(this.dir, 'inputs.json'), JSON.stringify(inputs, null, 2), 'utf8');
|
|
@@ -40,7 +43,32 @@ export class WorkflowState {
|
|
|
40
43
|
async writeVars(snapshot) {
|
|
41
44
|
await writeFile(join(this.dir, 'vars.json'), JSON.stringify(snapshot, null, 2), 'utf8').catch(() => { });
|
|
42
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* 归档(成功完成时调用):重命名状态目录为 .done 后缀,保留所有中间产物。
|
|
48
|
+
* vars.json / progress.md / inputs.json 全部保留——供后续对话 Read 回溯。
|
|
49
|
+
*/
|
|
50
|
+
async archive() {
|
|
51
|
+
const doneDir = this.dir + DONE_SUFFIX;
|
|
52
|
+
try {
|
|
53
|
+
// 先清理旧的归档(如果存在)
|
|
54
|
+
await rm(doneDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
await rename(this.dir, doneDir);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// rename 失败时静默(某些文件系统/权限问题),保留原目录
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 强制清理(/new 或新 workflow init 时调用):删除状态目录(含 .done 归档)。
|
|
68
|
+
*/
|
|
43
69
|
async cleanup() {
|
|
44
70
|
await rm(this.dir, { recursive: true, force: true }).catch(() => { });
|
|
71
|
+
// 同时清理可能的归档目录
|
|
72
|
+
await rm(this.dir + DONE_SUFFIX, { recursive: true, force: true }).catch(() => { });
|
|
45
73
|
}
|
|
46
74
|
}
|