minimal-agent 0.7.0 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimal-agent",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
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>",
@@ -7,14 +7,18 @@
7
7
  * - agent: 节点(stepExecutors/agent.ts)—— 作者显式声明的开放任务
8
8
  * 二者本质同构:受工作流目标/上下文指导、用工具随机应变、产出经验证后回灌 capture。
9
9
  * 为避免「同一个脑子写两份提示词、行为漂移」,这里收敛它们的**共享件**:
10
- * 1. 统一系统提示词纪律(DISCIPLINE)—— 诊断→动手→自我验证→诚实失败(不伪造)→输出纪律;
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
- /** 拼一个 workflow agent 的 system prompt:角色开场白 + 共享纪律。 */
33
- export function buildWorkflowAgentSystem(opening) {
34
- return `${opening}\n\n${DISCIPLINE}`;
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 判定同一套逻辑)。成功(没见到终结事件)则在末尾补一条
@@ -0,0 +1,89 @@
1
+ /**
2
+ * ============================================================
3
+ * plugins/workflow-runner/src/jsonPayload.ts —— 从 LLM 文本里提取纯 JSON 负载
4
+ * ------------------------------------------------------------
5
+ * 根因(为什么需要这个文件):
6
+ * "思考型"模型(MiniMax-M2/M3、DeepSeek-R1 系等)即使在 strict json_schema 下,
7
+ * 仍会把 <think>…</think> 思维链**直接写进 content 字段**(不是 reasoning_content)。
8
+ * src/llm/client.ts 故意原样保留这段思维链(主循环下一轮工具调用要靠它接上下文,
9
+ * 见 client.ts:200-205 注释),所以"剥离思维链"不能放进通用 transport——否则会
10
+ * 破坏主循环。剥离必须发生在「契约要求纯 JSON」的边界,也就是 workflow 的
11
+ * output_schema 解析处。本模块就是那个边界的唯一实现,被两个解析点共享:
12
+ * - stepExecutors/llm.ts(llm + output_schema 单轮)
13
+ * - miniReAct.ts(agent 节点 / llm+context_files / onError:recover 三处子循环)
14
+ * 放一处、两边 import,杜绝"只修一个解析点、另一个继续炸"的漂移。
15
+ *
16
+ * 处理顺序(保守、纯函数、可单测):
17
+ * 1. 去掉所有 <think>…</think> 块(多行、可多个、大小写不敏感)。
18
+ * **必须先做**:思维链里常出现 { } [ ](模型在"想" JSON 长什么样),
19
+ * 若不先剥,下面"找首个 JSON 起点"会错锚到思维链里的括号。
20
+ * 2. 兜底:只有起始 <think>、收尾 </think> 被 length 截断 → 砍掉从 <think> 起的全部。
21
+ * 3. 从剩余文本定位首个 { 或 [,配平括号截取到对应收尾。
22
+ * 这一步天然容忍 ```json 代码围栏与 JSON 前后的散文(只取配平的那一段),
23
+ * 所以不需要单独的"去围栏"正则(也就避免了误伤 JSON 字符串里的反引号)。
24
+ * 4. 找不到任何 JSON 起点 → 原样返回 trim 后文本,交调用方 JSON.parse 报
25
+ * "honest error"(保留原文前若干字符便于排查)。
26
+ * ============================================================
27
+ */
28
+ /**
29
+ * 从可能掺杂思维链 / 代码围栏 / 前后散文的文本里提取最可能的 JSON 负载字符串。
30
+ * 不做 JSON.parse(解析与错误信息留给调用方,便于各自定制报错文案)。
31
+ */
32
+ export function extractJsonPayload(raw) {
33
+ // 1. 去成对的思维链块
34
+ let s = raw.replace(/<think>[\s\S]*?<\/think>/gi, '');
35
+ // 2. 截断兜底:有起始无收尾(被 max tokens 截断)→ 丢弃从 <think> 起的尾巴
36
+ if (/<think>/i.test(s) && !/<\/think>/i.test(s)) {
37
+ s = s.slice(0, s.search(/<think>/i));
38
+ }
39
+ s = s.trim();
40
+ // 3. 定位首个 JSON 起点并配平截取
41
+ const start = firstJsonStart(s);
42
+ if (start === -1)
43
+ return s; // 没有 JSON:原样返回,交调用方报错
44
+ const end = matchBalanced(s, start);
45
+ return end === -1 ? s.slice(start) : s.slice(start, end + 1);
46
+ }
47
+ /** 首个 { 或 [ 的下标(取更靠前者);都没有返回 -1。 */
48
+ function firstJsonStart(s) {
49
+ const obj = s.indexOf('{');
50
+ const arr = s.indexOf('[');
51
+ if (obj === -1)
52
+ return arr;
53
+ if (arr === -1)
54
+ return obj;
55
+ return Math.min(obj, arr);
56
+ }
57
+ /**
58
+ * 从 start({ 或 [)配平到对应的 } 或 ],返回收尾下标;找不到返回 -1。
59
+ * 跳过字符串字面量内部的括号与转义,避免被 JSON 值里的 } / ] 误导。
60
+ */
61
+ function matchBalanced(s, start) {
62
+ const open = s[start];
63
+ const close = open === '{' ? '}' : ']';
64
+ let depth = 0;
65
+ let inStr = false;
66
+ let esc = false;
67
+ for (let i = start; i < s.length; i++) {
68
+ const c = s[i];
69
+ if (inStr) {
70
+ if (esc)
71
+ esc = false;
72
+ else if (c === '\\')
73
+ esc = true;
74
+ else if (c === '"')
75
+ inStr = false;
76
+ continue;
77
+ }
78
+ if (c === '"')
79
+ inStr = true;
80
+ else if (c === open)
81
+ depth++;
82
+ else if (c === close) {
83
+ depth--;
84
+ if (depth === 0)
85
+ return i;
86
+ }
87
+ }
88
+ return -1;
89
+ }
@@ -31,6 +31,7 @@
31
31
  * ============================================================
32
32
  */
33
33
  import { chat, executeTool, } from '../../../src/plugin-sdk.js';
34
+ import { extractJsonPayload } from './jsonPayload.js';
34
35
  /**
35
36
  * 执行一次 ReAct 子循环。**边跑边 yield UI 事件(stage_change / text / reasoning /
36
37
  * tool_start / tool_end),同时 return 最终 StepResult**——让 recover / agent 节点 /
@@ -49,6 +50,18 @@ export async function* runMiniReAct(args) {
49
50
  const maxTurns = args.maxTurns ?? 10;
50
51
  // 工作消息列表:拷贝一份,避免污染调用方
51
52
  const conv = [...messages];
53
+ // schema 模式:把 schema 写进 system(同 llm.ts)。保证 provider 不支持 response_format
54
+ // 而被 client.ts 退化掉时,模型仍知道确切字段结构、不至于瞎猜字段名。
55
+ if (output_schema) {
56
+ const hint = `\n\n最终只输出匹配以下 JSON Schema 的 JSON 对象本身(不要解释、不要 markdown 代码块):\n${JSON.stringify(output_schema)}`;
57
+ const sysIdx = conv.findIndex((m) => m.role === 'system');
58
+ if (sysIdx >= 0) {
59
+ conv[sysIdx] = { ...conv[sysIdx], content: `${conv[sysIdx].content ?? ''}${hint}` };
60
+ }
61
+ else {
62
+ conv.unshift({ role: 'system', content: hint.trimStart() });
63
+ }
64
+ }
52
65
  let finalText = '';
53
66
  /** 上一轮是否仍有 tool_calls(用于检测 "用尽轮数还在调工具" 这一退出态) */
54
67
  let lastTurnHadToolCalls = false;
@@ -162,9 +175,11 @@ export async function* runMiniReAct(args) {
162
175
  }
163
176
  // ---- 5. 输出处理 ----
164
177
  if (output_schema) {
178
+ // 同 llm.ts:先剥 <think> 思维链/围栏/前后散文再 parse(见 jsonPayload.ts 头注)。
179
+ const payload = extractJsonPayload(finalText);
165
180
  let parsed;
166
181
  try {
167
- parsed = JSON.parse(finalText);
182
+ parsed = JSON.parse(payload);
168
183
  }
169
184
  catch (e) {
170
185
  throw new Error(`miniReAct: output_schema 要求 JSON 但解析失败: ${e.message}\n收到: ${finalText.slice(0, 200)}`);
@@ -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: RECOVERY_SYSTEM },
84
+ { role: 'system', content: systemContent },
83
85
  { role: 'user', content: userMsg },
84
86
  ];
85
87
  try {
@@ -22,6 +22,7 @@
22
22
  * ============================================================
23
23
  */
24
24
  import { getWorkingDir } from '../../../src/plugin-sdk.js';
25
+ import { dlog } from '../../../src/utils/debugLog.js';
25
26
  import { evalExpr, interpolate } from './expressions.js';
26
27
  import { runRecovery } from './recovery.js';
27
28
  import { execAgentStep } from './stepExecutors/agent.js';
@@ -131,6 +132,7 @@ async function* runSteps(steps, vars, ctx, state) {
131
132
  }
132
133
  }
133
134
  const kind = stepKind(step);
135
+ dlog('wf', `step ${i + 1}/${steps.length} START ${step.id} (${kind})`);
134
136
  yield {
135
137
  type: 'workflow_step_start',
136
138
  id: step.id,
@@ -509,6 +511,7 @@ export async function* runWorkflow(def, inputs, ctx) {
509
511
  max: def.steps.length,
510
512
  message: `▶ workflow ${def.name}`,
511
513
  };
514
+ let succeeded = false;
512
515
  try {
513
516
  const halted = yield* runSteps(def.steps, vars, runCtx, state);
514
517
  if (halted)
@@ -523,9 +526,15 @@ export async function* runWorkflow(def, inputs, ctx) {
523
526
  message: `✓ workflow ${def.name} 完成`,
524
527
  };
525
528
  yield { type: 'workflow_done', name: def.name, vars: vars.snapshot() };
529
+ succeeded = true;
526
530
  }
527
531
  finally {
528
- await state.cleanup();
532
+ // 只在「干净跑完」时清现场;halt / 中断 / 抛错都**保留** .minimal-agent-workflow/
533
+ // (current.json / inputs.json / vars.json / progress.md,含失败那步的 ✗ 行)供事后排查
534
+ // ——这正是这些文件的设计目的(workflowState.ts 头注);失败时删掉等于自废武功。
535
+ // 残留不会永久堆积:下次同名 workflow 的 init() 会先 rm,/new 也按 .minimal-agent-* 前缀统一清。
536
+ if (succeeded)
537
+ await state.cleanup();
529
538
  }
530
539
  }
531
540
  /**
@@ -1,72 +1,50 @@
1
1
  /**
2
2
  * ============================================================
3
- * plugins/workflow-runner/src/stepExecutors/agent.ts —— agent step 执行
3
+ * plugins/workflow-runner/src/stepExecutors/agent.ts —— agent 步骤执行器
4
4
  * ------------------------------------------------------------
5
- * 「受工作流指导的 TAOR」一等公民。作者在 yaml 里 inline 描述一步开放任务
6
- * (agent: "..."),本执行器起一个**有界 TAOR agent**(miniReAct)随机应变完成:
7
- * 思考 → 用工具 → 观察 → 反思 → 再来,直到把这一步要的结果做出来。
5
+ * 执行 workflow 中 type: agent 的步骤:启动一个 miniReAct 子循环,
6
+ * 用工具随机应变地完成开放性任务。
8
7
  *
9
- * 与 skill: 节点的分工:
10
- * - skill: 跑一个**外部 SKILL.md 定义的、可复用的**完整 agent(runQuery,maxTurns 50,流式)
11
- * - agent: 跑一个**inline 描述的、一次性的**有界 agent(miniReAct,白名单工具 + output_schema
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
- import { interpolate } from '../expressions.js';
27
14
  import { runMiniReAct } from '../miniReAct.js';
28
- const AGENT_OPENING = `你是一名资深工程师。一个工作流把其中**一步开放任务**交给你自主完成。
29
- 你受工作流的总目标与已知上下文指导,但具体怎么做由你用工具随机应变决定——把这一步要的结果真正做出来。`;
30
- const AGENT_NODE_SYSTEM = buildWorkflowAgentSystem(AGENT_OPENING);
15
+ import { AGENT_OPENING, buildWorkflowAgentSystem, describeCaptureContract, interpretMiniReActResult, } from '../agentCommon.js';
16
+ /**
17
+ * 执行一个 agent 步骤。
18
+ *
19
+ * 关键变化(Phase 2 重构):
20
+ * buildWorkflowAgentSystem 现在是 async(因为需要扫描 skill/plugin 目录),
21
+ * agent 步骤的 system prompt 现在包含完整的:
22
+ * - 工具列表 + 使用规范
23
+ * - Skill 列表(含触发词)
24
+ * - Plugin 列表(含命令和描述)
25
+ * 这解决了之前 workflow 模式下模型不知道有 pptx/pdf 等 skill 的问题。
26
+ */
31
27
  export async function* execAgentStep(step, vars, ctx) {
32
- if (typeof step.agent !== 'string') {
33
- throw new Error(`step ${step.id}: 缺少 agent 字段`);
34
- }
35
- const task = interpolate(step.agent, vars);
36
- const goalLine = ctx.goal ? `工作流总目标:${ctx.goal}\n\n` : '';
37
- const captureLine = describeCaptureContract(step);
38
- // 可选 context_files:列出 path + hint,让 agent 用 Read 按需取(不 inline 内容)。
39
- let filesLine = '';
40
- if (step.context_files && step.context_files.length > 0) {
41
- const list = step.context_files
42
- .map((f) => `- ${interpolate(f.path, vars)}${f.hint ? ` — ${f.hint}` : ''}`)
43
- .join('\n');
44
- filesLine = `\n\n以下文件可用,按需用 Read 读取:\n${list}`;
45
- }
46
- const userMsg = `${goalLine}` +
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;
28
+ const tools = (ctx.tools ?? []).filter((t) => step.allowed_tools?.includes(t.name));
29
+ // ✅ Phase 2: 异步构建完整 system prompt(含 skill/plugin 列表)
30
+ const systemContent = await buildWorkflowAgentSystem(ctx.cwd, // 新增:传入 cwd 用于 skill/plugin 扫描
31
+ AGENT_OPENING, tools);
32
+ const userMsg = step.prompt +
33
+ describeCaptureContract(step) +
34
+ (step.input ? `\n\n上下文变量:${step.input}` : '');
35
+ const messages = [
36
+ { role: 'system', content: systemContent },
37
+ { role: 'user', content: userMsg },
38
+ ];
55
39
  const result = yield* runMiniReAct({
56
- messages: [
57
- { role: 'system', content: AGENT_NODE_SYSTEM },
58
- { role: 'user', content: userMsg },
59
- ],
40
+ messages,
60
41
  tools,
61
- output_schema: step.output_schema,
42
+ maxTurns: step.maxTurns ?? 12,
62
43
  provider: ctx.provider,
63
44
  signal: ctx.signal,
64
- maxTurns: step.maxTurns ?? 12,
65
45
  });
66
- const outcome = interpretMiniReActResult(result, step.output_schema !== undefined);
67
- if (!outcome.ok) {
68
- // 放弃 / 空产出 → 抛错,交给 dispatchActionStep 的失败路径 + step.onError 处置。
69
- throw new Error(`agent 未完成任务:${outcome.reason}`);
70
- }
46
+ const outcome = interpretMiniReActResult(result, !!step.output_schema);
47
+ if (!outcome.ok)
48
+ throw new Error(`agent 步骤放弃: ${outcome.reason}`);
71
49
  return outcome.value;
72
50
  }
@@ -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 }` 同义
@@ -22,7 +27,9 @@
22
27
  */
23
28
  import { ALL_TOOLS, chat, } from '../../../../src/plugin-sdk.js';
24
29
  import { interpolate } from '../expressions.js';
30
+ import { extractJsonPayload } from '../jsonPayload.js';
25
31
  import { runMiniReAct } from '../miniReAct.js';
32
+ import { assembleCapabilityHint } from '../../../../src/prompts/promptAssembly.js';
26
33
  const LLM_STEP_SYSTEM_PLAIN = '你正在被一个 workflow 调用。只输出本步骤要求的内容本身,不要寒暄、不要列要求复述、不要工具调用语法。';
27
34
  const LLM_STEP_SYSTEM_SCHEMA = `你正在被一个 workflow 调用。
28
35
  要求:
@@ -68,8 +75,21 @@ export async function* execLlmStep(step, vars, ctx) {
68
75
  }
69
76
  // ===== END NEW =====
70
77
  const useSchema = step.output_schema !== undefined;
78
+ // schema 模式:把 schema 直接写进 system prompt。这样无论 provider 是否支持
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)}`
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;
71
88
  const messages = [
72
- { role: 'system', content: useSchema ? LLM_STEP_SYSTEM_SCHEMA : LLM_STEP_SYSTEM_PLAIN },
89
+ {
90
+ role: 'system',
91
+ content: baseSystem + capabilityHint,
92
+ },
73
93
  { role: 'user', content: prompt },
74
94
  ];
75
95
  const responseFormat = useSchema
@@ -103,10 +123,13 @@ export async function* execLlmStep(step, vars, ctx) {
103
123
  }
104
124
  const trimmed = text.trim();
105
125
  if (useSchema) {
106
- // strict structured output:解析 JSON 后整个对象作为 raw
126
+ // strict structured output:解析 JSON 后整个对象作为 raw。
127
+ // 思考型模型即便在 strict 下仍会把 think…</think 写进 content(见 jsonPayload.ts
128
+ // 头注与 src/llm/client.ts:200),故先剥思维链/前后散文再 parse;报错仍带原文便于排查。
129
+ const payload = extractJsonPayload(text);
107
130
  let parsed;
108
131
  try {
109
- parsed = JSON.parse(trimmed);
132
+ parsed = JSON.parse(payload);
110
133
  }
111
134
  catch (e) {
112
135
  throw new Error(`step ${step.id}: output_schema 要求 JSON 输出但解析失败: ${e.message}\n收到: ${trimmed.slice(0, 200)}`);
@@ -96,6 +96,12 @@ export async function* execSkillStep(step, vars, ctx) {
96
96
  throw new Error('用户中断');
97
97
  if (ev.type === 'error')
98
98
  throw new Error(ev.error);
99
+ // 子 skill 内部那次 runQuery 的 turn_done 是它自己的终结标记,不是整个 workflow 的终结。
100
+ // 不外泄——否则把 turn_done 当终结的消费者(-p --output-format json / gateway broker)
101
+ // 会在 workflow 中途(如 critic 步之后、verify 步之前)误判结束、漏掉后续步骤。
102
+ // workflow 的对外收口由 index.ts::runWorkflowFromCommand 末尾统一补发。
103
+ if (ev.type === 'turn_done')
104
+ continue;
99
105
  yield ev;
100
106
  }
101
107
  const trimmed = text.trim();
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: commit
3
3
  description: 当用户说"提交"、"commit"、"git commit"、"保存改动"、"帮我提交代码"、"生成提交信息"时使用。自动分析 git diff,生成规范的 Conventional Commit message,支持 feat/fix/docs/refactor/chore/style/test 等类型前缀。
4
+ triggers: 提交代码、git commit、生成 commit message、版本管理、commit message
4
5
  ---
5
6
 
6
7
  # /commit —— Git 提交规范化
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: config
3
3
  description: 配置向导技能。当用户输入 "/config" 且没有任何其他内容时触发。用于重新配置 LLM provider(BASE_URL / API_KEY / MODEL)。
4
+ triggers: 配置、初始化、设置、重新配置、config
4
5
  ---
5
6
 
6
7
  # Config Skill
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: diff
3
3
  description: 当用户问"改了什么"、"有什么变化"、"show me changes"、"看看改动"、"diff"、"差异"时使用。显示工作区或暂存区的变更内容,帮助用户理解当前代码状态。
4
+ triggers: 代码差异、对比、变更审查、diff 分析、diff、代码对比
4
5
  ---
5
6
 
6
7
  # /diff —— 查看 Git 变更
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: docx
3
3
  description: "Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of 'Word doc', 'word document', '.docx', or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a 'report', 'memo', 'letter', 'template', or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation."
4
+ triggers: Word 文档、docx、文档编辑、报告撰写、合同、公文、批量文档处理、Word 文件
4
5
  license: Proprietary. LICENSE.txt has complete terms
5
6
  ---
6
7
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: image-gen-openrouter
3
3
  description: 当用户说"生成图片"、"画一张图"、"配图"、"OpenRouter 生图"、"nano banana"、"image-gen-openrouter",或在 workflow 里要按提示词生成图片时使用。调用 OpenRouter 的 Chat Completions 接口(OpenAI 兼容 + image modalities),按提示词生成任意比例图片并落盘。
4
+ triggers: 生成图片、画一张图、配图、OpenRouter 生图、image-gen-openrouter、文生图、AI画图
4
5
  ---
5
6
 
6
7
  # image-gen-openrouter —— OpenRouter 文生图
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: pdf
3
3
  description: Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill.
4
+ triggers: PDF 文件、PDF 转换、PDF 提取、PDF 生成、pdf 文件处理、PDF 操作
4
5
  license: Proprietary. LICENSE.txt has complete terms
5
6
  ---
6
7
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: pptx
3
3
  description: "Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill."
4
+ triggers: PPT、幻灯片、演示文稿、deck、slides、presentation、pptx 文件、做汇报材料、演讲稿
4
5
  license: Proprietary. LICENSE.txt has complete terms
5
6
  ---
6
7
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: simplify
3
3
  description: 当用户说"简化"、"审查"、"review"、"优化这段代码"、"太复杂了"、"能不能更简洁"、"代码质量"时使用。启动多维度代码审查,识别冗余、反模式、性能问题,给出具体修改建议。
4
+ triggers: 简化代码、重构、代码精简、降低复杂度、simplify、代码简化
4
5
  ---
5
6
 
6
7
  # /simplify —— 代码审查与简化
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: subtitle-srt
3
3
  description: 当用户说"生成字幕"、"做 SRT"、"字幕文件"、"subtitle-srt",或在 workflow 里要把分段文本 + 时长信息转成标准 SRT 字幕文件时使用。给定 cue 列表(每条带文本和起止时间或时长),生成符合 SRT 规范的字幕文件。
4
+ triggers: 生成字幕、做 SRT、字幕文件、subtitle-srt、字幕生成
4
5
  ---
5
6
 
6
7
  # subtitle-srt —— SRT 字幕生成
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: tts-zh
3
3
  description: 当用户说"中文配音"、"中文 TTS"、"语音合成"、"文字转语音"、"tts-zh",或在 workflow 里要把中文文本转成 mp3 时使用。优先用 MiniMax T2A API,失败时 fallback 到 edge-tts 离线方案,输出 mp3 到指定路径。
4
+ triggers: 中文配音、中文 TTS、语音合成、文字转语音、tts-zh、配音、朗读
4
5
  ---
5
6
 
6
7
  # tts-zh —— 中文 TTS(minimax 主 + edge-tts 兜底)
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: video-compose
3
3
  description: 当用户说"合成视频"、"做 mp4"、"图片转视频"、"ffmpeg 拼视频"、"video-compose",或在 workflow 里要把图片序列 + 可选音轨 + 可选字幕合成成 mp4 时使用。本 skill 是 ffmpeg 的 declarative 封装,所有参数(分辨率、字体、码率、转场)都可配置,不绑定特定视频格式。
4
+ triggers: 合成视频、做 mp4、图片转视频、ffmpeg 拼视频、video-compose、视频制作
4
5
  ---
5
6
 
6
7
  # video-compose —— 通用 ffmpeg 视频合成
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: web-artifacts-builder
3
3
  description: Suite of tools for creating elaborate, multi-component web artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts. Trigger when user asks to build a web app, React component, interactive UI, landing page, dashboard, or any frontend project.
4
+ triggers: Web 应用、React 组件、交互式 UI、落地页、仪表盘、前端项目、web-artifacts-builder、构建网页
4
5
  license: Complete terms in LICENSE.txt
5
6
  ---
6
7
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: webapp-testing
3
3
  description: Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
4
+ triggers: Web 测试、浏览器测试、Playwright、截图、UI 调试、webapp-testing、网页测试
4
5
  license: Complete terms in LICENSE.txt
5
6
  ---
6
7
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: xlsx
3
3
  description: "Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved."
4
+ triggers: Excel、表格、xlsx、数据处理、图表、批量数据、CSV 导入导出、电子表格
4
5
  license: Proprietary. LICENSE.txt has complete terms
5
6
  ---
6
7