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.
@@ -26,6 +26,7 @@ import { ALL_TOOLS } from '../../tools/index.js';
26
26
  import { setAskHandler, } from '../../askBridge.js';
27
27
  import { clearFileState } from '../../tools/shared/fileState.js';
28
28
  import { runWithPlugins } from '../../plugins/pluginRunner.js';
29
+ import { dlog } from '../../utils/debugLog.js';
29
30
  /**
30
31
  * reasoningPreview 环形 buffer 容量(字符)。
31
32
  * LiveArea 现在多行显示思维链尾部(最近 N 行),需要喂饱足够文本;
@@ -201,6 +202,10 @@ export function useChat(args) {
201
202
  return new Promise((resolve, reject) => {
202
203
  askResolverRef.current?.reject(new Error('被新的提问取代'));
203
204
  askResolverRef.current = { resolve, reject };
205
+ dlog('ui', 'ask handler INVOKED (等待用户答题)', {
206
+ n: questions.length,
207
+ headers: questions.map((q) => q.header),
208
+ });
204
209
  setPendingAsk(questions);
205
210
  const onAbort = () => {
206
211
  if (!askResolverRef.current)
@@ -223,6 +228,7 @@ export function useChat(args) {
223
228
  const r = askResolverRef.current;
224
229
  askResolverRef.current = null;
225
230
  setPendingAsk(null);
231
+ dlog('ui', 'ask ANSWERED (继续执行)', { selected: answers.map((a) => a.selected) });
226
232
  r?.resolve(answers);
227
233
  }, []);
228
234
  // mount 时异步读 active session(默认 'default',注册表里有就用注册值)。
@@ -267,6 +273,7 @@ export function useChat(args) {
267
273
  setRunningToolsMap(new Map());
268
274
  const ac = new AbortController();
269
275
  abortRef.current = ac;
276
+ dlog('ui', 'turn START', { input: trimmed.slice(0, 80) });
270
277
  try {
271
278
  for await (const ev of runWithPlugins(trimmed, {
272
279
  provider: args.provider,
@@ -293,13 +300,17 @@ export function useChat(args) {
293
300
  scheduleToolBump: toolBumpFlush.schedule,
294
301
  cancelToolBump: toolBumpFlush.cancel,
295
302
  bump,
303
+ // 工作流进度逐步可见(持久暗色行)
304
+ emitProgressLine: ephemeralLine,
296
305
  });
297
306
  }
298
307
  }
299
308
  catch (e) {
300
309
  setError(`未捕获异常:${e.message}`);
310
+ dlog('ui', 'turn THREW', { err: e.message });
301
311
  }
302
312
  finally {
313
+ dlog('ui', 'turn END (finally)');
303
314
  setIsLoading(false);
304
315
  cancelAllThrottles();
305
316
  resetTurnState();
@@ -514,6 +525,27 @@ export function useChat(args) {
514
525
  };
515
526
  }
516
527
  export function handleEvent(ev, setters) {
528
+ // ---- 工作流进度事件(WorkflowEvent,不在 LoopEvent union 里)----
529
+ // 渲染成持久的暗色行落进 scrollback:跑到哪一步、成功/失败/跳过都看得见,
530
+ // 且出错后仍停留在屏幕上 —— 解决"工作流执行过程不可见、不知道停在哪"。
531
+ const wf = ev;
532
+ switch (wf.type) {
533
+ case 'workflow_start':
534
+ setters.emitProgressLine?.(`▶ 工作流 ${wf.name} 开始(共 ${wf.totalSteps} 步)`);
535
+ return;
536
+ case 'workflow_step_start':
537
+ setters.emitProgressLine?.(` ▶ ${(wf.index ?? 0) + 1}/${wf.total ?? '?'} ${wf.id} (${wf.kind})`);
538
+ return;
539
+ case 'workflow_step_end':
540
+ setters.emitProgressLine?.(` ${wf.ok ? '✓' : '✗'} ${wf.id}${wf.error ? `:${wf.error}` : ''}`);
541
+ return;
542
+ case 'workflow_step_skipped':
543
+ setters.emitProgressLine?.(` ⊘ ${wf.id}:${wf.reason ?? ''}`);
544
+ return;
545
+ case 'workflow_done':
546
+ setters.emitProgressLine?.(`✓ 工作流 ${wf.name} 完成`);
547
+ return;
548
+ }
517
549
  // 收口到 LoopEvent:未识别的 PluginEvent type 走 default no-op
518
550
  const e = ev;
519
551
  switch (e.type) {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ============================================================
3
+ * src/utils/debugLog.ts —— 文件调试日志(环境变量开关,默认全程 no-op)
4
+ * ------------------------------------------------------------
5
+ * 为什么需要:TUI(Ink)独占 stdout/stderr,console.log 在 TUI 里看不见、还会搅乱画面;
6
+ * 而"卡死/不响应"这类问题恰恰无法靠正常退出来事后检查。把关键生命周期点**同步追加**到
7
+ * 一个磁盘文件,进程即便随后挂死,日志也已落盘——文件的最后一行就指向卡住的位置。
8
+ *
9
+ * 用法:
10
+ * MINIMAL_AGENT_DEBUG_LOG=D:\tmp\ma-debug.log bun run start
11
+ * 然后复现问题,把该文件最后几十行发来即可定位。
12
+ *
13
+ * 设计:
14
+ * - 未设环境变量 → dlog() 直接 return,零开销、零行为改变(不影响 npm/dev/绿色版任何路径)。
15
+ * - 用 appendFileSync(同步):宁可慢一点也要保证"挂死前那一行确实写进了磁盘"。
16
+ * - 任何写日志的异常都吞掉,绝不让"记日志"反过来影响主流程。
17
+ * ============================================================
18
+ */
19
+ import { appendFileSync } from 'node:fs';
20
+ const LOG_PATH = process.env.MINIMAL_AGENT_DEBUG_LOG;
21
+ /** 调试日志是否开启(供调用方决定要不要算开销较大的 data)。 */
22
+ export function isDebugLogOn() {
23
+ return !!LOG_PATH;
24
+ }
25
+ /**
26
+ * 追加一行调试日志:`<ISO 时间> [tag] msg {data}`。
27
+ * 未开启(无 MINIMAL_AGENT_DEBUG_LOG)时立即返回。
28
+ */
29
+ export function dlog(tag, msg, data) {
30
+ if (!LOG_PATH)
31
+ return;
32
+ try {
33
+ const ts = new Date().toISOString();
34
+ const line = data !== undefined
35
+ ? `${ts} [${tag}] ${msg} ${safeJson(data)}\n`
36
+ : `${ts} [${tag}] ${msg}\n`;
37
+ appendFileSync(LOG_PATH, line);
38
+ }
39
+ catch {
40
+ // 记日志失败绝不影响主流程
41
+ }
42
+ }
43
+ /** 安全 JSON:循环引用 / 超长都不抛、不撑爆日志。 */
44
+ function safeJson(v) {
45
+ try {
46
+ const s = JSON.stringify(v);
47
+ return s.length > 600 ? `${s.slice(0, 600)}…` : s;
48
+ }
49
+ catch {
50
+ return String(v);
51
+ }
52
+ }
@@ -1,26 +1,40 @@
1
1
  # ============================================================
2
- # workflows/plan.yaml —— 复刻 Claude Code「plan」流程(Analyze→Verify→Synthesize→Critic→Gate)
2
+ # workflows/plan.yaml —— 复刻 Claude Code「plan」流程(Clarify→Analyze→Verify→Synthesize→Critic→Gate)
3
3
  # ------------------------------------------------------------
4
- # 把 Ultracode 动态工作流的多阶段编进确定性 DAG,全用现有 StepKind(不改引擎):
5
- # Analyze : analyze(agent 只读探索) → extract_findings(llm 固化成可验证 claims) → clarify_depth(AskUserQuestion 人机交互)
6
- # Verify : verify_findings(type:vote 红队对抗——对每条 finding 反向提问证伪,存活的才进下游)
7
- # Synthesize: draft_plan(agent 综合"通过验证的结论"+用户选择,写 ./PLAN.md)
8
- # Critic : critic_plan(skill:adversarial-review 反向提问审稿,把审稿意见追加进 ./PLAN.md)
9
- # Gate : verify(assert 守门,确认 ./PLAN.md 确实生成)
10
- # 非交互宿主(-p/gateway)下 AskUserQuestion 自动选推荐项、vote 走 LLM,整条流程照常跑完。
4
+ # 设计目标:① 跨基座模型稳(弱模型也能跑完,不依赖强 agent 多轮工具循环);
5
+ # ② 提问只在「开头一次性」发生,之后全自主无打断,直到产出可提交的 PLAN.md。
6
+ # 全用现有 StepKind(不改引擎):
7
+ # Clarify : clarify_depth(AskUserQuestion 开头一次性问深度——全流程唯一的人机交互点)
8
+ # Analyze : analyze(agent 只读探索) → extract_findings(llm+schema 固化成可验证 claims)
9
+ # Verify : verify_findings(type:vote 红队对每条 finding 反向提问证伪,存活的才进下游)
10
+ # Synthesize: draft_plan(llm 纯文本出计划正文) → read_existing_plan + write_plan(tool 确定性落盘)
11
+ # Critic : critic_plan(skill:adversarial-review 反向提问审稿,追加进 PLAN.md)
12
+ # Gate : verify(assert 守门确认 PLAN.md 生成)
11
13
  # ------------------------------------------------------------
12
- # 注:critic 阶段用 skill: 节点 dogfood 仓库自带的 adversarial-review skill —— 演示
13
- # 「同一套对抗审查方法既能 standalone(/adversarial-review)用、也能在 workflow 里被复用」。
14
- # skill 双源查找(cwd 优先、packageRoot fallback);万一异地 cwd 缺该 skill,
14
+ # 模型兼容性要点(为什么这样切分——上一版在火山 GLM-5.1 上两处翻车的根治):
15
+ # 1. 写盘用 "llm 出正文 + tool:Write 落盘" 而非 agent。原 agent+Write 写法在弱模型上
16
+ # 会反复调 Write 触发 miniReAct 重复检测 halt;真因还叠加 Write 的"覆盖前须先 Read"
17
+ # 守卫——PLAN.md 已存在时首个 Write 被拒、模型重试同参 Write→3 次→halt。
18
+ # 故 write 前置一个 tool:Read(onError:continue,首跑文件不存在则略过)满足守卫。
19
+ # 2. 结构化输出(extract_findings / vote)由 client.ts 自动退化:provider 不支持
20
+ # response_format.json_schema(火山 GLM)时去掉它、靠 prompt 内嵌 schema + 容错解析。
21
+ # 3. 仍是 agent 的只剩 analyze(只读探索)与 critic(审稿,onError:continue 兜底),
22
+ # 它们在弱模型上最坏只是 halt/跳过,不破坏已产出的计划。
23
+ # ------------------------------------------------------------
24
+ # 提问节奏:唯一提问点 clarify_depth 放最前——问完之后全程自主无打断。任务专属、
25
+ # 本工作流没问的点(受众/时长/范围等)由 draft 自做合理默认假设并在正文开头列出,
26
+ # 绝不中途打断(契合"明确有优劣自动判断、只有真取舍才问、开头问完即自主")。
27
+ # ------------------------------------------------------------
28
+ # 注:critic 用 skill: 节点 dogfood 仓库自带的 adversarial-review skill。
29
+ # skill 双源查找(cwd 优先、packageRoot fallback);异地 cwd 缺该 skill 时
15
30
  # critic 的 onError:continue 兜底——审稿出岔不应推翻已写好的计划。
16
31
  # ============================================================
17
32
  name: plan
18
33
  description: |
19
- 受工作流指导 + 红队对抗验证 + 反向提问审稿 + 人机交互的动态计划流程。
20
- Analyze(探索+固化+问深度) → Verify(vote 红队证伪候选结论) → Synthesize(综合通过验证的结论写 PLAN.md)
21
- → Critic(反向提问审稿,把意见追加进 PLAN.md) → Gate(assert 守门确认文件生成)。
22
- minimal-agent 的旗舰演示。
23
- version: "0.2"
34
+ 受工作流指导 + 红队对抗验证 + 反向提问审稿的动态计划流程(跨模型稳、开头一次性提问后全自主)。
35
+ Clarify(开头问深度) → Analyze(探索+固化) → Verify(vote 红队证伪) → Synthesize(出正文+落盘)
36
+ → Critic(反向提问审稿追加) → Gate(assert 守门)。minimal-agent 的旗舰演示。
37
+ version: "0.3"
24
38
 
25
39
  inputs:
26
40
  - name: goal
@@ -29,6 +43,26 @@ inputs:
29
43
  description: "要做的事 / 目标"
30
44
 
31
45
  steps:
46
+ # ===================== Clarify(开头唯一提问点)=====================
47
+ # [Clarify] 一次性问"计划深度"(这是真有取舍、无统一最优的点;第一项=推荐,非交互自动选它)。
48
+ # 放最前:问完之后全程自主。其它任务专属细节(受众/时长/范围)由 draft 自做合理默认并列出,
49
+ # 不再中途打断——契合"明确有优劣自动判断、只有真取舍才问"。
50
+ - id: clarify_depth
51
+ tool: AskUserQuestion
52
+ args:
53
+ questions:
54
+ - header: "计划深度"
55
+ question: "希望这份实现计划做到多详细?"
56
+ options:
57
+ - label: "详细分步 (推荐)"
58
+ description: "拆成可执行的有序步骤,每步说清动作、涉及文件与验收点"
59
+ - label: "快速概览"
60
+ description: "只给高层方向与关键决策,不展开到每一步"
61
+ - label: "带风险/回滚评估"
62
+ description: "在详细分步之外,额外标注每步风险与回滚方案"
63
+ capture:
64
+ content: depth_answer
65
+
32
66
  # ===================== Analyze =====================
33
67
  # [Analyze-1] 只读探索:agent 自主用 Read/Grep/Glob 摸清现状(不写盘、不副作用)
34
68
  - id: analyze
@@ -40,7 +74,7 @@ steps:
40
74
  capture:
41
75
  content: analysis
42
76
 
43
- # [Analyze-2] 固化成可验证 claims:llm 单轮 + output_schema,把笔记抽成数组(供 vote 逐条验证)
77
+ # [Analyze-2] 固化成可验证 claims:llm 单轮 + output_schema(provider 不支持时由 client.ts 自动退化)
44
78
  - id: extract_findings
45
79
  llm: |
46
80
  从下面的调研笔记中抽取「候选关键事实」与「未决问题」,原样保留信息、不要新增臆测。
@@ -63,27 +97,9 @@ steps:
63
97
  findings: findings
64
98
  open_questions: open_questions
65
99
 
66
- # [Analyze-3] 人机交互:直接用 AskUserQuestion 问「计划深度」(第一项=推荐,非交互自动选它)
67
- - id: clarify_depth
68
- tool: AskUserQuestion
69
- args:
70
- questions:
71
- - header: "计划深度"
72
- question: "希望这份实现计划做到多详细?"
73
- options:
74
- - label: "详细分步 (推荐)"
75
- description: "拆成可执行的有序步骤,每步说清动作、涉及文件与验收点"
76
- - label: "快速概览"
77
- description: "只给高层方向与关键决策,不展开到每一步"
78
- - label: "带风险/回滚评估"
79
- description: "在详细分步之外,额外标注每步风险与回滚方案"
80
- capture:
81
- content: depth_answer
82
-
83
100
  # ===================== Verify =====================
84
- # [Verify] 红队对抗:vote 节点对每条 finding 反向提问证伪(voter 默认倾向 refuted=true)
85
- # refuted 票数 >= threshold 的 finding 被驳回,只有存活的进下游 —— 杜绝未证实结论污染计划。
86
- # 注:voter 偏向证伪,存活集可能偏少甚至为空;下游 draft_plan 对此做了 graceful degrade(见其提示)。
101
+ # [Verify] 红队对抗:vote 对每条 finding 反向提问证伪(voter 默认倾向 refuted=true);
102
+ # 存活集可能偏少甚至为空,draft 对此 graceful degrade(见其提示)。
87
103
  - id: verify_findings
88
104
  type: vote
89
105
  input: "findings"
@@ -99,11 +115,12 @@ steps:
99
115
  total: total_count
100
116
 
101
117
  # ===================== Synthesize =====================
102
- # [Synthesize] 综合产出:agent 用"通过验证的结论"+用户选择落盘 ./PLAN.md,遇取舍可再问用户
118
+ # [Synthesize-1] 生成计划正文:llm 纯文本(不开工具、不提问)——确定性、跨模型稳,
119
+ # 彻底避开"agent 反复调 Write"的弱模型死循环。
103
120
  - id: draft_plan
104
- agent: |
105
- 综合下列输入,为目标「${inputs.goal}」写一份中文实现计划,用 Write 落到 ./PLAN.md
106
- (目录不存在先创建)。
121
+ llm: |
122
+ 综合下列输入,为目标「${inputs.goal}」写一份**中文实现计划的完整 markdown 正文**
123
+ (直接输出正文本身:不要寒暄、不要复述要求、不要用代码围栏包裹整篇)。
107
124
 
108
125
  —— 通过红队对抗验证的结论(${kept_count}/${total_count} 条存活)——
109
126
  ${verified_findings}
@@ -115,21 +132,32 @@ steps:
115
132
  ${depth_answer}
116
133
 
117
134
  要求:
118
- - 计划详细程度遵循用户上面的选择;
119
- - 关于上面"存活结论"为空的两种情形请分别处理:若原本就没抽出 finding,按目标从头调研;
120
- 若是被红队全部驳回(存活 0 但 total>0),别盲信被否的结论,用 Read/Grep/Glob 重新取证后再写;
121
- - 若遇方案取舍或关键信息模糊、继续做风险大,调 AskUserQuestion 让用户拍板
122
- (每题第一个选项放推荐并在 label 末尾标「(推荐)」),据回答继续;
123
- - 写完用 Read 自检 ./PLAN.md 内容确实落好了再收尾。
124
- allowed_tools: [Read, Grep, Glob, Write, AskUserQuestion]
135
+ - 详细程度遵循用户选择的深度;
136
+ - "存活结论"为空时不要硬编:按目标常识给出合理的计划骨架;
137
+ - 凡需按需求拍板、但本工作流未询问的点(如受众/时长/范围),**自行做合理默认假设并继续**,
138
+ 在正文开头用「## 假设(如需调整请告知)」一节列出假设,**绝不中断提问**。
125
139
  capture:
126
- content: plan_text
140
+ # llm 步(无 output_schema)的 raw 只有 {text,result},没有 content(那是 tool 步的字段)。
141
+ # 用错字段会 capture 到 undefined → 下游 Write 写 0 字节(已被端到端测试抓到)。
142
+ text: plan_text
143
+
144
+ # [Synthesize-2] 满足 Write 的"覆盖前须先 Read"守卫:PLAN.md 已存在则先 Read;
145
+ # 首跑文件不存在 → Read 报错 → onError:continue 略过(不影响后续 Write 新建)。
146
+ - id: read_existing_plan
147
+ tool: Read
148
+ args:
149
+ file_path: ./PLAN.md
150
+ onError: continue
151
+
152
+ # [Synthesize-3] 落盘:tool:Write 确定性写入(不经 agent 循环,弱模型也稳)
153
+ - id: write_plan
154
+ tool: Write
155
+ args:
156
+ file_path: ./PLAN.md
157
+ content: "${plan_text}"
127
158
 
128
159
  # ===================== Critic =====================
129
- # [Critic] 反向提问审稿:dogfood 仓库自带的 adversarial-review skill —— 同一套对抗审查
130
- # 方法,既能 standalone(/adversarial-review)用,也能在 workflow 里被 skill: 复用。
131
- # 产出由 skill 子 agent 直接 Edit/Write 追加进 ./PLAN.md(不 capture——下游无人消费)。
132
- # onError:continue —— 审稿出岔(含异地 cwd 缺 skill)不应推翻已写好的计划。
160
+ # [Critic] 反向提问审稿:dogfood 仓库自带 adversarial-review skill;onError:continue 兜底
133
161
  - id: critic_plan
134
162
  skill: adversarial-review
135
163
  maxTurns: 25
@@ -145,4 +173,4 @@ steps:
145
173
  - id: verify
146
174
  type: assert
147
175
  condition: 'fileExists("./PLAN.md")'
148
- onFail: "计划文件 ./PLAN.md 未生成,检查 draft_plan 步是否成功用 Write 落盘"
176
+ onFail: "计划文件 ./PLAN.md 未生成,检查 draft_plan/write_plan 步是否成功"