autosnippet 3.2.3 → 3.2.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.
Files changed (64) hide show
  1. package/README.md +2 -4
  2. package/bin/cli.js +164 -145
  3. package/config/constitution.yaml +3 -0
  4. package/dashboard/dist/assets/{index-DNOHYBhy.css → index-BaGY7kJI.css} +1 -1
  5. package/dashboard/dist/assets/{index-6itPuGFl.js → index-DfHY_3ln.js} +25 -25
  6. package/dashboard/dist/index.html +2 -2
  7. package/lib/cli/CliLogger.js +78 -0
  8. package/lib/cli/SetupService.js +9 -719
  9. package/lib/cli/UpgradeService.js +23 -398
  10. package/lib/cli/deploy/FileDeployer.js +562 -0
  11. package/lib/cli/deploy/FileManifest.js +272 -0
  12. package/lib/external/mcp/McpServer.js +22 -26
  13. package/lib/external/mcp/autoApproveInjector.js +1 -0
  14. package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +5 -5
  15. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +25 -3
  16. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +6 -6
  17. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +4 -0
  18. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +5 -5
  19. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +89 -44
  20. package/lib/external/mcp/handlers/consolidated.js +8 -9
  21. package/lib/external/mcp/handlers/dimension-complete-external.js +4 -4
  22. package/lib/external/mcp/handlers/guard.js +283 -5
  23. package/lib/external/mcp/handlers/task.js +183 -9
  24. package/lib/external/mcp/tools.js +32 -81
  25. package/lib/http/routes/task.js +55 -0
  26. package/lib/service/chat/AnalystAgent.js +12 -12
  27. package/lib/service/chat/ChatAgent.js +227 -545
  28. package/lib/service/chat/ChatAgentPrompts.js +9 -11
  29. package/lib/service/chat/ContextWindow.js +2 -296
  30. package/lib/service/chat/EpisodicConsolidator.js +15 -15
  31. package/lib/service/chat/ExplorationTracker.js +1262 -0
  32. package/lib/service/chat/HandoffProtocol.js +8 -9
  33. package/lib/service/chat/Memory.js +4 -0
  34. package/lib/service/chat/ProducerAgent.js +9 -6
  35. package/lib/service/chat/ProjectSemanticMemory.js +4 -0
  36. package/lib/service/chat/ReasoningTrace.js +182 -0
  37. package/lib/service/chat/WorkingMemory.js +4 -0
  38. package/lib/service/chat/memory/ActiveContext.js +910 -0
  39. package/lib/service/chat/memory/MemoryCoordinator.js +662 -0
  40. package/lib/service/chat/memory/PersistentMemory.js +450 -0
  41. package/lib/service/chat/memory/SessionStore.js +896 -0
  42. package/lib/service/chat/memory/index.js +13 -0
  43. package/lib/service/chat/tools/ast-graph.js +17 -16
  44. package/lib/service/cursor/AgentInstructionsGenerator.js +75 -40
  45. package/lib/service/cursor/FileProtection.js +4 -1
  46. package/lib/service/guard/GuardCheckEngine.js +10 -3
  47. package/lib/service/task/TaskGraphService.js +3 -3
  48. package/lib/shared/LanguageService.js +2 -1
  49. package/package.json +1 -1
  50. package/skills/autosnippet-intent/SKILL.md +1 -3
  51. package/skills/autosnippet-recipes/SKILL.md +1 -3
  52. package/templates/claude-code/commands/prime.md +19 -0
  53. package/templates/claude-code/hooks/autosnippet-session.sh +63 -0
  54. package/templates/claude-code/settings.json +21 -0
  55. package/templates/copilot-instructions.md +66 -177
  56. package/templates/cursor-hooks/commands/prime.md +12 -0
  57. package/templates/cursor-hooks/hooks/session-start.sh +10 -0
  58. package/templates/cursor-hooks/hooks.json +11 -0
  59. package/templates/cursor-rules/autosnippet-conventions.mdc +52 -3
  60. package/templates/cursor-rules/autosnippet-workflow.mdc +51 -27
  61. package/lib/external/mcp/handlers/decide.js +0 -109
  62. package/lib/external/mcp/handlers/ready.js +0 -42
  63. package/lib/service/chat/ReasoningLayer.js +0 -888
  64. package/templates/claude-hooks.yaml +0 -19
@@ -0,0 +1,1262 @@
1
+ /**
2
+ * ExplorationTracker — 统一的 AI 探索生命周期控制器
3
+ *
4
+ * 合并了三个原本各自为政的系统:
5
+ * 1. PhaseRouter (ContextWindow.js) — 阶段状态机
6
+ * 2. 探索进度追踪 (ChatAgent.js 内联) — 信息增量检测
7
+ * 3. ReasoningLayer 行为控制部分 — 反思/规划/停滞 nudge
8
+ *
9
+ * 职责:
10
+ * - 信号收集: 统一的 recordToolCall() 替代 ~120 行内联 if-else
11
+ * - 阶段路由: 策略模式,不同角色使用不同阶段策略
12
+ * - Nudge 生成: 优先级队列,每轮最多注入一条 nudge
13
+ * - Graceful exit: 管理轮次耗尽后的优雅退出流程
14
+ *
15
+ * 不拥有的职责:
16
+ * - 推理链数据收集 → ReasoningTrace (纯数据,不影响行为)
17
+ * - 上下文压缩 → ContextWindow
18
+ * - 工具注册与执行 → ToolRegistry
19
+ * - 跨对话记忆 → Memory / WorkingMemory
20
+ *
21
+ * @module ExplorationTracker
22
+ */
23
+
24
+ import Logger from '../../infrastructure/logging/Logger.js';
25
+
26
+ // ─── 常量 ──────────────────────────────────────────────
27
+
28
+ /** 反思间隔(每 N 轮触发一次) */
29
+ const DEFAULT_REFLECTION_INTERVAL = 5;
30
+ /** 连续无新信息 N 轮触发停滞反思 */
31
+ const DEFAULT_STALE_THRESHOLD = 2;
32
+ /** 最少经过 N 轮后才允许触发停滞反思 */
33
+ const MIN_ITERS_FOR_STALE_REFLECTION = 4;
34
+ /** 默认重规划间隔 */
35
+ const DEFAULT_REPLAN_INTERVAL = 8;
36
+ /** 默认偏差阈值 */
37
+ const DEFAULT_DEVIATION_THRESHOLD = 0.6;
38
+ /** 默认最少探索轮次(冷启动质量保障) */
39
+ const DEFAULT_MIN_EXPLORE_ITERS = 16;
40
+ /** 默认停滞收敛阈值 */
41
+ const DEFAULT_CONVERGENCE_STALE_THRESHOLD = 3;
42
+
43
+ // ─── 内置策略 ────────────────────────────────────────────
44
+
45
+ /**
46
+ * Bootstrap 策略(原始 ChatAgent,有 submit 阶段)
47
+ * @param {boolean} isSkillOnly — skill-only 维度跳过 PRODUCE 阶段
48
+ * @returns {object} 策略配置
49
+ */
50
+ function createBootstrapStrategy(isSkillOnly = false) {
51
+ return {
52
+ name: 'bootstrap',
53
+ phases: isSkillOnly
54
+ ? ['EXPLORE', 'SUMMARIZE']
55
+ : ['EXPLORE', 'PRODUCE', 'SUMMARIZE'],
56
+ transitions: {
57
+ ...(isSkillOnly
58
+ ? {
59
+ 'EXPLORE→SUMMARIZE': {
60
+ onMetrics: (m, b) =>
61
+ m.submitCount > 0 ||
62
+ m.searchRoundsInPhase >= b.searchBudget,
63
+ onTextResponse: true,
64
+ },
65
+ }
66
+ : {
67
+ 'EXPLORE→PRODUCE': {
68
+ onMetrics: (m, b) =>
69
+ m.submitCount > 0 ||
70
+ m.searchRoundsInPhase >= b.searchBudget,
71
+ onTextResponse: true,
72
+ },
73
+ 'PRODUCE→SUMMARIZE': {
74
+ onMetrics: (m, b) =>
75
+ m.submitCount >= b.maxSubmits ||
76
+ (m.submitCount > 0 && m.roundsSinceSubmit >= b.idleRoundsToExit) ||
77
+ (m.phaseRounds >= b.searchBudgetGrace && m.submitCount === 0),
78
+ onTextResponse: (m, b) =>
79
+ m.submitCount >= b.softSubmitLimit,
80
+ },
81
+ }),
82
+ },
83
+ getToolChoice: (phase, m, b) => {
84
+ if (phase === 'SUMMARIZE') return 'none';
85
+ if (phase === 'EXPLORE') {
86
+ return m.searchRoundsInPhase >= b.searchBudget - 1 ? 'auto' : 'required';
87
+ }
88
+ return 'auto'; // PRODUCE
89
+ },
90
+ enableReflection: true,
91
+ reflectionInterval: DEFAULT_REFLECTION_INTERVAL,
92
+ enablePlanning: true,
93
+ replanInterval: DEFAULT_REPLAN_INTERVAL,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Analyst 策略(纯探索,无 submit 阶段)
99
+ * 4 阶段: SCAN → EXPLORE → VERIFY → SUMMARIZE
100
+ */
101
+ const STRATEGY_ANALYST = {
102
+ name: 'analyst',
103
+ phases: ['SCAN', 'EXPLORE', 'VERIFY', 'SUMMARIZE'],
104
+ transitions: {
105
+ 'SCAN→EXPLORE': {
106
+ onMetrics: (m) => m.iteration >= 3,
107
+ onTextResponse: false, // SCAN 阶段文本回复不触发转换
108
+ },
109
+ 'EXPLORE→VERIFY': {
110
+ onMetrics: (m, b) =>
111
+ m.searchRoundsInPhase >= Math.floor(b.maxIterations * 0.6) ||
112
+ m.roundsSinceNewInfo >= 3,
113
+ onTextResponse: false, // 允许中间分析文本
114
+ },
115
+ 'VERIFY→SUMMARIZE': {
116
+ onMetrics: (m, b) =>
117
+ m.iteration >= Math.floor(b.maxIterations * 0.8) ||
118
+ m.roundsSinceNewInfo >= 2,
119
+ onTextResponse: true,
120
+ },
121
+ },
122
+ getToolChoice: (phase) => {
123
+ if (phase === 'SUMMARIZE') return 'none';
124
+ if (phase === 'SCAN') return 'required';
125
+ if (phase === 'EXPLORE') return 'required';
126
+ return 'auto'; // VERIFY
127
+ },
128
+ enableReflection: true,
129
+ reflectionInterval: DEFAULT_REFLECTION_INTERVAL,
130
+ enablePlanning: true,
131
+ replanInterval: DEFAULT_REPLAN_INTERVAL,
132
+ };
133
+
134
+ /**
135
+ * Producer 策略(格式化+提交,不搜索)
136
+ * 2 阶段: PRODUCE → SUMMARIZE
137
+ */
138
+ const STRATEGY_PRODUCER = {
139
+ name: 'producer',
140
+ phases: ['PRODUCE', 'SUMMARIZE'],
141
+ transitions: {
142
+ 'PRODUCE→SUMMARIZE': {
143
+ onMetrics: (m, b) =>
144
+ m.submitCount >= b.maxSubmits ||
145
+ (m.submitCount > 0 && m.roundsSinceSubmit >= b.idleRoundsToExit) ||
146
+ (m.phaseRounds >= b.searchBudgetGrace && m.submitCount === 0),
147
+ onTextResponse: (m, b) =>
148
+ m.submitCount >= b.softSubmitLimit,
149
+ },
150
+ },
151
+ getToolChoice: (phase) => (phase === 'SUMMARIZE' ? 'none' : 'auto'),
152
+ enableReflection: false,
153
+ reflectionInterval: 0,
154
+ enablePlanning: false,
155
+ replanInterval: 0,
156
+ };
157
+
158
+ // ─── 搜索工具白名单(用于判断"搜索轮次")───────────────
159
+
160
+ const SEARCH_TOOLS = new Set([
161
+ 'search_project_code',
162
+ 'semantic_search_code',
163
+ 'get_class_info',
164
+ 'get_class_hierarchy',
165
+ 'get_protocol_info',
166
+ 'get_method_overrides',
167
+ 'get_category_map',
168
+ 'list_project_structure',
169
+ 'get_project_overview',
170
+ 'get_file_summary',
171
+ ]);
172
+
173
+ // ─── ExplorationTracker 主类 ─────────────────────────────
174
+
175
+ export class ExplorationTracker {
176
+ /** @type {object} 策略配置 */
177
+ #strategy;
178
+ /** @type {object} 预算配置 */
179
+ #budget;
180
+ /** @type {string} 当前阶段 */
181
+ #phase;
182
+ /** @type {object} 日志器 */
183
+ #logger;
184
+
185
+ // ── 信号指标 ──
186
+ #metrics = {
187
+ uniqueFiles: new Set(),
188
+ uniquePatterns: new Set(),
189
+ uniqueQueries: new Set(),
190
+ totalToolCalls: 0,
191
+ submitCount: 0,
192
+ roundsSinceNewInfo: 0,
193
+ roundsSinceSubmit: 0,
194
+ iteration: 0,
195
+ searchRoundsInPhase: 0,
196
+ phaseRounds: 0,
197
+ };
198
+
199
+ // ── 阶段控制 ──
200
+ /** @type {boolean} 是否刚完成阶段转换(用于 pending nudge) */
201
+ #justTransitioned = false;
202
+ /** @type {string|null} 转换前的旧阶段 */
203
+ #transitionFromPhase = null;
204
+ /** @type {boolean} 是否已注入收敛 nudge(防止重复触发) */
205
+ #convergenceNudged = false;
206
+ /** @type {boolean} 是否已注入预算警告(防止重复触发) */
207
+ #budgetWarningInjected = false;
208
+
209
+ // ── Graceful exit 控制 ──
210
+ /** @type {number|null} 进入 graceful exit 的轮次 */
211
+ #gracefulExitRound = null;
212
+
213
+ // ── Planning 跟踪 ──
214
+ /** @type {boolean} 等待 AI 输出 replan */
215
+ #pendingReplan = false;
216
+ /** @type {object} 计划进度 */
217
+ #planProgress = {
218
+ coveredSteps: 0,
219
+ totalSteps: 0,
220
+ deviationScore: 0,
221
+ unplannedActions: 0,
222
+ lastReplanIteration: null,
223
+ consecutiveOffPlan: 0,
224
+ };
225
+
226
+ /** @type {boolean} tick 是否已调用(用于 rollback) */
227
+ #ticked = false;
228
+
229
+ /**
230
+ * @param {object} strategy — 策略配置对象
231
+ * @param {object} budget — 预算配置 { maxIterations, searchBudget, ... }
232
+ */
233
+ constructor(strategy, budget) {
234
+ this.#strategy = strategy;
235
+ this.#budget = budget;
236
+ this.#phase = strategy.phases[0];
237
+ this.#logger = Logger.getInstance();
238
+ }
239
+
240
+ // ─── 静态工厂 ─────────────────────────────────────────
241
+
242
+ /**
243
+ * 根据调用参数解析应使用的策略
244
+ * @param {object} opts — ChatAgent execute 的选项
245
+ * @param {object} budget — 预算配置
246
+ * @returns {ExplorationTracker|null} — User 模式返回 null
247
+ */
248
+ static resolve(opts, budget) {
249
+ const { source = 'user', strategy: strategyName, dimensionMeta } = opts;
250
+ const isSystem = source === 'system';
251
+
252
+ if (!isSystem) {
253
+ return null; // User 模式不需要 ExplorationTracker
254
+ }
255
+
256
+ let resolvedStrategy;
257
+
258
+ if (strategyName === 'analyst') {
259
+ resolvedStrategy = STRATEGY_ANALYST;
260
+ } else if (strategyName === 'producer') {
261
+ resolvedStrategy = STRATEGY_PRODUCER;
262
+ } else {
263
+ // 默认 bootstrap(strategyName === 'bootstrap' 或未指定)
264
+ const isSkillOnly = dimensionMeta?.outputType === 'skill';
265
+ resolvedStrategy = createBootstrapStrategy(isSkillOnly);
266
+ }
267
+
268
+ return new ExplorationTracker(resolvedStrategy, budget);
269
+ }
270
+
271
+ // ─── 核心 API:主循环调用点 ────────────────────────────
272
+
273
+ /**
274
+ * 每轮迭代开始时调用 — 递增计数
275
+ * 可通过 rollbackTick() 撤销(AI 失败/空响应时)
276
+ */
277
+ tick() {
278
+ this.#metrics.iteration++;
279
+ this.#metrics.phaseRounds++;
280
+ this.#ticked = true;
281
+ // 安全: 清除上一轮可能遗留的 justTransitioned (文本路径不调 endRound)
282
+ this.#justTransitioned = false;
283
+ }
284
+
285
+ /**
286
+ * 撤销 tick(AI 调用失败或空响应时,不计入迭代)
287
+ */
288
+ rollbackTick() {
289
+ if (this.#ticked) {
290
+ this.#metrics.iteration--;
291
+ this.#metrics.phaseRounds--;
292
+ this.#ticked = false;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * 是否应退出主循环
298
+ * @returns {boolean}
299
+ */
300
+ shouldExit() {
301
+ // 终结阶段 + 已给了 2 轮 grace → 退出
302
+ if (this.#isTerminalPhase() && this.#metrics.phaseRounds >= 2) {
303
+ return true;
304
+ }
305
+
306
+ // 硬上限兜底(maxIterations + 2 grace 都耗尽)
307
+ if (this.#metrics.iteration >= this.#budget.maxIterations + 2) {
308
+ return true;
309
+ }
310
+
311
+ // 达到 maxIterations 但未在终结阶段 → 强制转入终结阶段
312
+ if (this.#metrics.iteration >= this.#budget.maxIterations && !this.#isTerminalPhase()) {
313
+ this.#logger.info(
314
+ `[ExplorationTracker] maxIterations reached (${this.#metrics.iteration}/${this.#budget.maxIterations}), forcing → ${this.#getTerminalPhase()}`
315
+ );
316
+ this.#transitionTo(this.#getTerminalPhase());
317
+ this.#justTransitioned = false; // shouldExit 的转换由 force_exit nudge 覆盖,不重复
318
+ this.#gracefulExitRound = this.#metrics.iteration;
319
+ // 返回 false,让主循环运行终结阶段的 grace 轮次
320
+ return false;
321
+ }
322
+
323
+ return false;
324
+ }
325
+
326
+ /**
327
+ * 获取本轮的 Nudge(每轮最多一条)
328
+ * 调用时机:AI 调用前
329
+ *
330
+ * 优先级 (高→低):
331
+ * 1. force_exit — 轮次耗尽
332
+ * 2. convergence — 信息饱和
333
+ * 3. budget_warning — 75% 预算消耗(无条件)
334
+ * 4. reflection — 周期反思 / 停滞反思
335
+ * 5. planning — 首轮规划 / 偏差重规划
336
+ *
337
+ * @param {import('./memory/ActiveContext.js').ActiveContext} trace — 推理链(供反思用)
338
+ * @returns {{ type: string, text: string }|null}
339
+ */
340
+ getNudge(trace) {
341
+ const m = this.#metrics;
342
+ const b = this.#budget;
343
+
344
+ // 1. 强制退出
345
+ if (this.#gracefulExitRound != null && m.iteration === this.#gracefulExitRound) {
346
+ const submitCount = m.submitCount;
347
+ return {
348
+ type: 'force_exit',
349
+ text: `⚠️ 你已使用 ${m.iteration}/${b.maxIterations} 轮次,**必须立即结束**。请在回复中直接输出 dimensionDigest JSON 总结(用 \`\`\`json 包裹),不要再调用任何工具。\n` +
350
+ `\`\`\`json\n{"dimensionDigest":{"summary":"分析总结","candidateCount":${submitCount},"keyFindings":["发现"],"crossRefs":{},"gaps":["缺口"],"remainingTasks":[{"signal":"未处理信号","reason":"轮次耗尽","priority":"high","searchHints":["搜索词"]}]}}\n\`\`\`\n> remainingTasks: 列出未来得及处理的信号。已覆盖则留空 \`[]\`。`,
351
+ };
352
+ }
353
+
354
+ // 2. 收敛引导(信息饱和 — 仅非终结阶段)
355
+ if (
356
+ !this.#isTerminalPhase() &&
357
+ !this.#convergenceNudged &&
358
+ m.roundsSinceNewInfo >= DEFAULT_CONVERGENCE_STALE_THRESHOLD &&
359
+ m.iteration >= DEFAULT_MIN_EXPLORE_ITERS
360
+ ) {
361
+ this.#convergenceNudged = true;
362
+ this.#logger.info(
363
+ `[ExplorationTracker] 📊 Exploration saturated at iter ${m.iteration}/${b.maxIterations} — ` +
364
+ `files=${m.uniqueFiles.size}, patterns=${m.uniquePatterns.size}, staleRounds=${m.roundsSinceNewInfo}`
365
+ );
366
+ return {
367
+ type: 'convergence',
368
+ text: `你已经充分探索了项目代码(${m.uniqueFiles.size} 个文件,${m.uniquePatterns.size} 次不同搜索,${m.uniqueQueries.size} 次结构化查询)。` +
369
+ `最近 ${m.roundsSinceNewInfo} 轮没有发现新信息,建议开始撰写分析总结。\n` +
370
+ `如果你确信还有重要方面未覆盖,可以继续探索(剩余 ${b.maxIterations - m.iteration} 轮);否则请直接输出你的分析发现。`,
371
+ };
372
+ }
373
+
374
+ // 3. 预算警告(75% 消耗,无条件,一次性)
375
+ if (
376
+ !this.#isTerminalPhase() &&
377
+ !this.#budgetWarningInjected &&
378
+ m.iteration >= Math.floor(b.maxIterations * 0.75)
379
+ ) {
380
+ this.#budgetWarningInjected = true;
381
+ this.#logger.info(
382
+ `[ExplorationTracker] 📌 Budget warning at ${m.iteration}/${b.maxIterations}`
383
+ );
384
+ return {
385
+ type: 'budget_warning',
386
+ text: `📌 进度提醒:你已使用 ${m.iteration}/${b.maxIterations} 轮次(${Math.round((m.iteration / b.maxIterations) * 100)}%)。` +
387
+ `请确保核心方面已覆盖,开始准备总结。剩余 ${b.maxIterations - m.iteration} 轮,优先填补最重要的分析空白。`,
388
+ };
389
+ }
390
+
391
+ // 4. 反思(周期性 + 停滞)
392
+ if (this.#strategy.enableReflection) {
393
+ const reflectionNudge = this.#checkReflection(trace);
394
+ if (reflectionNudge) return reflectionNudge;
395
+ }
396
+
397
+ // 5. 规划(首轮 plan elicitation / 偏差 replan)
398
+ if (this.#strategy.enablePlanning) {
399
+ const planningNudge = this.#checkPlanning(trace);
400
+ if (planningNudge) return planningNudge;
401
+ }
402
+
403
+ return null;
404
+ }
405
+
406
+ /**
407
+ * 获取当前阶段的上下文状态行(注入 systemPrompt 尾部)
408
+ * 轻量级,每轮都注入,不含行为指令
409
+ * @returns {string}
410
+ */
411
+ getPhaseContext() {
412
+ const m = this.#metrics;
413
+ const b = this.#budget;
414
+ const remaining = b.maxIterations - m.iteration;
415
+
416
+ // 接近上限时的紧急警告
417
+ if (remaining <= 2 && remaining > 0 && !this.#isTerminalPhase()) {
418
+ return `\n\n## 当前状态\n⚠️ 仅剩 ${remaining} 轮次即达上限,请尽快完成当前工作并准备输出总结。`;
419
+ }
420
+
421
+ // 阶段特定提示
422
+ const phaseHint = this.#getPhaseHint();
423
+ if (phaseHint) {
424
+ return `\n\n## 当前状态\n${phaseHint}`;
425
+ }
426
+
427
+ // 通用进度行
428
+ const phaseLabel = this.#getPhaseLabel();
429
+ return `\n\n## 当前进度\n第 ${m.iteration}/${b.maxIterations} 轮 | ${phaseLabel} | 剩余 ${remaining} 轮`;
430
+ }
431
+
432
+ /**
433
+ * 获取当前阶段的 toolChoice
434
+ * @returns {'required'|'auto'|'none'}
435
+ */
436
+ getToolChoice() {
437
+ if (this.isGracefulExit) return 'none';
438
+ return this.#strategy.getToolChoice(this.#phase, this.#metrics, this.#budget);
439
+ }
440
+
441
+ /**
442
+ * 记录一次工具调用结果,更新内部指标
443
+ * 替代 ChatAgent 中内联的 ~120 行 if-else 逻辑
444
+ *
445
+ * @param {string} toolName
446
+ * @param {object} args
447
+ * @param {*} result — 工具原始返回
448
+ * @returns {{ isNew: boolean }}
449
+ */
450
+ recordToolCall(toolName, args, result) {
451
+ this.#metrics.totalToolCalls++;
452
+ const isNew = this.#detectNewInfo(toolName, args, result);
453
+
454
+ // Submit 追踪(只记成功提交)
455
+ if (toolName === 'submit_knowledge' || toolName === 'submit_with_check') {
456
+ const status = typeof result === 'object' ? result?.status : 'ok';
457
+ const isRejected = status === 'rejected';
458
+ const isError = status === 'error';
459
+ if (!isRejected && !isError) {
460
+ this.#metrics.submitCount++;
461
+ this.#metrics.roundsSinceSubmit = 0;
462
+ }
463
+ }
464
+
465
+ return { isNew };
466
+ }
467
+
468
+ /**
469
+ * 结束本轮迭代 — 更新轮次级指标 + 检查阶段转换
470
+ *
471
+ * @param {object} roundStats
472
+ * @param {boolean} roundStats.hasNewInfo — 本轮是否获取到新信息
473
+ * @param {number} roundStats.submitCount — 本轮成功提交数
474
+ * @param {string[]} [roundStats.toolNames] — 本轮调用的工具名列表
475
+ * @param {boolean} [roundStats.skipped] — 标记为跳过的轮次(错误/空响应)
476
+ * @returns {{ type: string, text: string }|null} — 阶段转换 nudge(如有)
477
+ */
478
+ endRound({ hasNewInfo = false, submitCount = 0, toolNames = [], skipped = false } = {}) {
479
+ this.#ticked = false;
480
+
481
+ if (skipped) {
482
+ // 跳过的轮次不更新任何指标
483
+ return null;
484
+ }
485
+
486
+ // 1. 更新轮次级指标
487
+ if (hasNewInfo) {
488
+ this.#metrics.roundsSinceNewInfo = 0;
489
+ } else {
490
+ this.#metrics.roundsSinceNewInfo++;
491
+ }
492
+ if (submitCount > 0) {
493
+ this.#metrics.roundsSinceSubmit = 0;
494
+ } else {
495
+ this.#metrics.roundsSinceSubmit++;
496
+ }
497
+
498
+ // 2. 搜索轮次计数
499
+ const hasSearchTool = toolNames.some((t) => SEARCH_TOOLS.has(t));
500
+ if (hasSearchTool) {
501
+ this.#metrics.searchRoundsInPhase++;
502
+ }
503
+
504
+ // 3. 检查 metrics 驱动的阶段转换
505
+ this.#checkMetricsTransition();
506
+
507
+ // 4. 如果发生了转换,生成 nudge 立即返回给主循环注入
508
+ if (this.#justTransitioned) {
509
+ this.#justTransitioned = false;
510
+ return {
511
+ type: 'phase_transition',
512
+ text: this.#buildTransitionNudge(),
513
+ };
514
+ }
515
+
516
+ return null;
517
+ }
518
+
519
+ /**
520
+ * 处理 AI 返回纯文本响应(无工具调用)
521
+ * @returns {{ isFinalAnswer: boolean, needsDigestNudge: boolean, shouldContinue: boolean, nudge: string|null }}
522
+ */
523
+ onTextResponse() {
524
+ const m = this.#metrics;
525
+
526
+ // 检查文本触发的阶段转换
527
+ const transitioned = this.#checkTextTransition();
528
+ // 文本路径不调 endRound(),需要主动清除 justTransitioned 防止泄漏
529
+ if (transitioned) {
530
+ this.#justTransitioned = false;
531
+ }
532
+
533
+ const isTerminal = this.#isTerminalPhase();
534
+
535
+ if (isTerminal && !transitioned) {
536
+ // 已在终结阶段且非刚转入 → 最终回答
537
+ return { isFinalAnswer: true, needsDigestNudge: false, shouldContinue: false, nudge: null };
538
+ }
539
+
540
+ if (isTerminal && transitioned) {
541
+ // 刚转入终结阶段 → 需要 digest nudge,不是最终回答
542
+ const submitCount = m.submitCount;
543
+ return {
544
+ isFinalAnswer: false,
545
+ needsDigestNudge: true,
546
+ shouldContinue: true,
547
+ nudge: `请在回复中直接输出 dimensionDigest JSON 总结(用 \`\`\`json 包裹):\n` +
548
+ `\`\`\`json\n{"dimensionDigest":{"summary":"分析总结(100-200字)","candidateCount":${submitCount},"keyFindings":["关键发现"],"crossRefs":{},"gaps":["未覆盖方面"],"remainingTasks":[{"signal":"未处理的信号/主题","reason":"未完成原因","priority":"high|medium|low","searchHints":["建议搜索词"]}]}}\n\`\`\`\n> 如果所有信号都已覆盖,remainingTasks 留空数组 \`[]\`。`,
549
+ };
550
+ }
551
+
552
+ // 非终结阶段收到文本
553
+ if (this.#phase === 'PRODUCE' || this.#phase === 'EXPLORE') {
554
+ // PRODUCE 阶段中间文本 → 继续循环,注入提交引导
555
+ const nudge = this.#phase === 'PRODUCE'
556
+ ? '你的分析很好。请继续调用 submit_knowledge 提交你发现的知识候选,每个值得记录的模式/实践都应该提交。'
557
+ : null;
558
+ return { isFinalAnswer: false, needsDigestNudge: false, shouldContinue: true, nudge };
559
+ }
560
+
561
+ // 其他阶段的文本(SCAN / VERIFY 等)→ 继续循环
562
+ return { isFinalAnswer: false, needsDigestNudge: false, shouldContinue: true, nudge: null };
563
+ }
564
+
565
+ /**
566
+ * 记录被截断的工具调用数量
567
+ * @param {number} count
568
+ */
569
+ recordTruncatedCalls(count) {
570
+ if (count > 0) {
571
+ this.#logger.warn(
572
+ `[ExplorationTracker] ${count} tool calls truncated (MAX_TOOL_CALLS_PER_ITER)`
573
+ );
574
+ }
575
+ }
576
+
577
+ // ─── 状态查询 ─────────────────────────────────────────
578
+
579
+ /** 是否处于 graceful exit 模式 */
580
+ get isGracefulExit() {
581
+ return this.#gracefulExitRound != null;
582
+ }
583
+
584
+ /** 是否应硬退出(graceful exit + 2 轮 grace 耗尽) */
585
+ get isHardExit() {
586
+ return (
587
+ this.#gracefulExitRound != null &&
588
+ this.#metrics.iteration >= this.#gracefulExitRound + 2
589
+ );
590
+ }
591
+
592
+ /** 当前阶段 */
593
+ get phase() {
594
+ return this.#phase;
595
+ }
596
+
597
+ /** 当前迭代次数 */
598
+ get iteration() {
599
+ return this.#metrics.iteration;
600
+ }
601
+
602
+ /** 总提交数 */
603
+ get totalSubmits() {
604
+ return this.#metrics.submitCount;
605
+ }
606
+
607
+ /** 策略名称 */
608
+ get strategyName() {
609
+ return this.#strategy.name;
610
+ }
611
+
612
+ /**
613
+ * 获取当前指标的快照(供外部使用,如 #produceForcedSummary)
614
+ * @returns {object}
615
+ */
616
+ getMetrics() {
617
+ return {
618
+ iteration: this.#metrics.iteration,
619
+ phase: this.#phase,
620
+ submitCount: this.#metrics.submitCount,
621
+ uniqueFiles: this.#metrics.uniqueFiles.size,
622
+ uniquePatterns: this.#metrics.uniquePatterns.size,
623
+ uniqueQueries: this.#metrics.uniqueQueries.size,
624
+ totalToolCalls: this.#metrics.totalToolCalls,
625
+ roundsSinceNewInfo: this.#metrics.roundsSinceNewInfo,
626
+ };
627
+ }
628
+
629
+ /**
630
+ * 获取计划进度
631
+ * @returns {object}
632
+ */
633
+ getPlanProgress() {
634
+ return { ...this.#planProgress };
635
+ }
636
+
637
+ // ─── 信号收集内部方法 ──────────────────────────────────
638
+
639
+ /**
640
+ * 检测工具调用是否产生了新信息
641
+ * 合并了 ChatAgent 内联的探索追踪 + ReasoningLayer.buildObservationMeta 的逻辑
642
+ *
643
+ * @param {string} toolName
644
+ * @param {object} args
645
+ * @param {*} result
646
+ * @returns {boolean}
647
+ * @private
648
+ */
649
+ #detectNewInfo(toolName, args, result) {
650
+ switch (toolName) {
651
+ case 'search_project_code': {
652
+ let foundNew = false;
653
+ const pattern = args?.pattern || '';
654
+ const patterns = args?.patterns || [];
655
+ // 单模式
656
+ if (pattern && !this.#metrics.uniquePatterns.has(pattern)) {
657
+ this.#metrics.uniquePatterns.add(pattern);
658
+ foundNew = true;
659
+ }
660
+ // 批量模式
661
+ for (const p of patterns) {
662
+ if (!this.#metrics.uniquePatterns.has(p)) {
663
+ this.#metrics.uniquePatterns.add(p);
664
+ foundNew = true;
665
+ }
666
+ }
667
+ // 检查搜索结果是否有新文件
668
+ if (result && typeof result === 'object') {
669
+ const matches = result.matches || [];
670
+ const batchResults = result.batchResults || {};
671
+ for (const m of matches) {
672
+ if (m.file && !this.#metrics.uniqueFiles.has(m.file)) {
673
+ this.#metrics.uniqueFiles.add(m.file);
674
+ foundNew = true;
675
+ }
676
+ }
677
+ for (const sub of Object.values(batchResults)) {
678
+ for (const m of sub.matches || []) {
679
+ if (m.file && !this.#metrics.uniqueFiles.has(m.file)) {
680
+ this.#metrics.uniqueFiles.add(m.file);
681
+ foundNew = true;
682
+ }
683
+ }
684
+ }
685
+ }
686
+ return foundNew;
687
+ }
688
+
689
+ case 'read_project_file': {
690
+ let foundNew = false;
691
+ const fp = args?.filePath || '';
692
+ const fps = args?.filePaths || [];
693
+ if (fp && !this.#metrics.uniqueFiles.has(fp)) {
694
+ this.#metrics.uniqueFiles.add(fp);
695
+ foundNew = true;
696
+ }
697
+ for (const f of fps) {
698
+ if (!this.#metrics.uniqueFiles.has(f)) {
699
+ this.#metrics.uniqueFiles.add(f);
700
+ foundNew = true;
701
+ }
702
+ }
703
+ return foundNew;
704
+ }
705
+
706
+ case 'list_project_structure': {
707
+ const dir = args?.directory || '/';
708
+ const qKey = `list:${dir}`;
709
+ if (!this.#metrics.uniqueQueries.has(qKey)) {
710
+ this.#metrics.uniqueQueries.add(qKey);
711
+ return true;
712
+ }
713
+ return false;
714
+ }
715
+
716
+ case 'get_class_info':
717
+ case 'get_class_hierarchy':
718
+ case 'get_protocol_info':
719
+ case 'get_method_overrides':
720
+ case 'get_category_map': {
721
+ const queryTarget =
722
+ args?.className || args?.protocolName || args?.name || '';
723
+ const qKey = `${toolName}:${queryTarget}`;
724
+ if (!this.#metrics.uniqueQueries.has(qKey)) {
725
+ this.#metrics.uniqueQueries.add(qKey);
726
+ return true;
727
+ }
728
+ return false;
729
+ }
730
+
731
+ case 'get_project_overview': {
732
+ const qKey = 'overview';
733
+ if (!this.#metrics.uniqueQueries.has(qKey)) {
734
+ this.#metrics.uniqueQueries.add(qKey);
735
+ return true;
736
+ }
737
+ return false;
738
+ }
739
+
740
+ case 'submit_knowledge':
741
+ case 'submit_with_check':
742
+ // Submit 本身不算"新信息"(阶段转换由 submitCount 驱动)
743
+ return false;
744
+
745
+ default: {
746
+ // 其他工具 — 首次调用算新信息,同名+同参数去重
747
+ const qKey = `${toolName}:${JSON.stringify(args || {}).substring(0, 80)}`;
748
+ if (!this.#metrics.uniqueQueries.has(qKey)) {
749
+ this.#metrics.uniqueQueries.add(qKey);
750
+ return true;
751
+ }
752
+ return false;
753
+ }
754
+ }
755
+ }
756
+
757
+ // ─── 阶段路由内部方法 ──────────────────────────────────
758
+
759
+ /**
760
+ * 检查 metrics 驱动的阶段转换
761
+ * @private
762
+ */
763
+ #checkMetricsTransition() {
764
+ const transitions = this.#strategy.transitions;
765
+ const nextPhaseIndex = this.#strategy.phases.indexOf(this.#phase) + 1;
766
+ if (nextPhaseIndex >= this.#strategy.phases.length) return;
767
+
768
+ const nextPhase = this.#strategy.phases[nextPhaseIndex];
769
+ const transKey = `${this.#phase}→${nextPhase}`;
770
+ const rule = transitions[transKey];
771
+ if (!rule) return;
772
+
773
+ const condition = typeof rule === 'function' ? rule : rule.onMetrics;
774
+ if (condition && condition(this.#metrics, this.#budget)) {
775
+ this.#transitionTo(nextPhase);
776
+ }
777
+ }
778
+
779
+ /**
780
+ * 检查文本响应触发的阶段转换
781
+ * @returns {boolean} 是否发生了转换
782
+ * @private
783
+ */
784
+ #checkTextTransition() {
785
+ const transitions = this.#strategy.transitions;
786
+ const nextPhaseIndex = this.#strategy.phases.indexOf(this.#phase) + 1;
787
+ if (nextPhaseIndex >= this.#strategy.phases.length) return false;
788
+
789
+ const nextPhase = this.#strategy.phases[nextPhaseIndex];
790
+ const transKey = `${this.#phase}→${nextPhase}`;
791
+ const rule = transitions[transKey];
792
+ if (!rule) return false;
793
+
794
+ let shouldTransition = false;
795
+ if (typeof rule === 'object' && rule.onTextResponse !== undefined) {
796
+ if (typeof rule.onTextResponse === 'function') {
797
+ shouldTransition = rule.onTextResponse(this.#metrics, this.#budget);
798
+ } else {
799
+ shouldTransition = !!rule.onTextResponse;
800
+ }
801
+ }
802
+
803
+ if (shouldTransition) {
804
+ this.#transitionTo(nextPhase);
805
+ return true;
806
+ }
807
+ return false;
808
+ }
809
+
810
+ /**
811
+ * 执行阶段转换
812
+ * @param {string} newPhase
813
+ * @private
814
+ */
815
+ #transitionTo(newPhase) {
816
+ const oldPhase = this.#phase;
817
+ this.#transitionFromPhase = oldPhase;
818
+ this.#phase = newPhase;
819
+ this.#metrics.phaseRounds = 0;
820
+ this.#metrics.searchRoundsInPhase = 0;
821
+ this.#justTransitioned = true;
822
+ this.#logger.info(
823
+ `[ExplorationTracker] ${oldPhase} → ${newPhase} (iter=${this.#metrics.iteration}, submits=${this.#metrics.submitCount})`
824
+ );
825
+ }
826
+
827
+ /**
828
+ * 构建阶段转换 nudge 文本
829
+ * @returns {string}
830
+ * @private
831
+ */
832
+ #buildTransitionNudge() {
833
+ const m = this.#metrics;
834
+ const fromPhase = this.#transitionFromPhase;
835
+ const toPhase = this.#phase;
836
+
837
+ if (toPhase === 'PRODUCE') {
838
+ return '你已充分探索了项目代码,现在请开始调用 submit_knowledge 工具来提交你发现的知识候选。不要再搜索,直接提交。';
839
+ }
840
+
841
+ if (toPhase === 'SUMMARIZE') {
842
+ const submitCount = m.submitCount;
843
+ return `你已完成分析探索。请在回复中直接输出 dimensionDigest JSON(用 \`\`\`json 包裹),包含以下字段:\n` +
844
+ `\`\`\`json\n{"dimensionDigest":{"summary":"分析总结(100-200字)","candidateCount":${submitCount},"keyFindings":["关键发现"],"crossRefs":{},"gaps":["未覆盖方面"],"remainingTasks":[{"signal":"未处理的信号/主题","reason":"未完成原因(如:提交上限已达)","priority":"high|medium|low","searchHints":["建议搜索词"]}]}}\n\`\`\`\n> 如果所有信号都已覆盖,remainingTasks 留空数组 \`[]\`。如果有未来得及处理的信号,请在此标记,系统会在下次运行时续传。`;
845
+ }
846
+
847
+ if (toPhase === 'EXPLORE' && fromPhase === 'SCAN') {
848
+ return '全局扫描完成。现在开始定向搜索——根据你发现的项目结构,搜索关键模式和类。';
849
+ }
850
+
851
+ if (toPhase === 'VERIFY') {
852
+ return '搜索阶段信息已饱和。现在进入验证阶段——读取最关键的源文件,确认细节和实现逻辑。';
853
+ }
854
+
855
+ return `阶段切换: ${fromPhase} → ${toPhase}`;
856
+ }
857
+
858
+ /**
859
+ * 获取当前阶段的 hint(补充到 systemPrompt)
860
+ * @returns {string|null}
861
+ * @private
862
+ */
863
+ #getPhaseHint() {
864
+ const m = this.#metrics;
865
+ const b = this.#budget;
866
+
867
+ switch (this.#phase) {
868
+ case 'EXPLORE':
869
+ if (m.searchRoundsInPhase >= b.searchBudget - 2) {
870
+ return `搜索预算即将耗尽 (${m.searchRoundsInPhase}/${b.searchBudget}),请准备提交候选或产出摘要。`;
871
+ }
872
+ return null;
873
+
874
+ case 'PRODUCE':
875
+ if (m.submitCount === 0 && m.phaseRounds >= 1) {
876
+ return '⚠️ 探索阶段已结束。你已收集了足够的项目信息,请 **立即** 调用 submit_knowledge 提交候选。不要继续搜索,直接提交。';
877
+ }
878
+ if (m.submitCount >= b.softSubmitLimit && b.softSubmitLimit > 0) {
879
+ const remaining = b.maxSubmits - m.submitCount;
880
+ return `已提交 ${m.submitCount} 个候选(上限 ${b.maxSubmits})。${remaining > 0 ? `还可提交 ${remaining} 个。` : ''}如果还有值得记录的发现可以继续提交,否则请产出 dimensionDigest 总结。\n⚠️ 如果还有未处理的信号,请在 dimensionDigest 的 remainingTasks 字段中标记,下次运行时会续传。`;
881
+ }
882
+ return null;
883
+
884
+ case 'SCAN':
885
+ return '当前处于全局扫描阶段,请先获取项目概览和目录结构。';
886
+
887
+ case 'VERIFY':
888
+ return '当前处于验证阶段,请阅读关键源文件确认实现细节。';
889
+
890
+ default:
891
+ return null;
892
+ }
893
+ }
894
+
895
+ /**
896
+ * 获取用户友好的阶段标签
897
+ * @returns {string}
898
+ * @private
899
+ */
900
+ #getPhaseLabel() {
901
+ switch (this.#phase) {
902
+ case 'SCAN': return '扫描阶段';
903
+ case 'EXPLORE': return '探索阶段';
904
+ case 'PRODUCE': return '提交阶段';
905
+ case 'VERIFY': return '验证阶段';
906
+ case 'SUMMARIZE': return '⚠ 总结阶段 — 请停止工具调用,直接输出分析文本';
907
+ default: return this.#phase;
908
+ }
909
+ }
910
+
911
+ /** 是否为终结阶段 */
912
+ #isTerminalPhase() {
913
+ return this.#phase === this.#getTerminalPhase();
914
+ }
915
+
916
+ /** 获取策略定义的终结阶段(最后一个) */
917
+ #getTerminalPhase() {
918
+ return this.#strategy.phases[this.#strategy.phases.length - 1];
919
+ }
920
+
921
+ // ─── 反思/规划内部方法 ─────────────────────────────────
922
+
923
+ /**
924
+ * 检查是否需要触发反思 + 生成反思 nudge
925
+ * @param {import('./memory/ActiveContext.js').ActiveContext} trace
926
+ * @returns {{ type: string, text: string }|null}
927
+ * @private
928
+ */
929
+ #checkReflection(trace) {
930
+ const m = this.#metrics;
931
+ const b = this.#budget;
932
+ const interval = this.#strategy.reflectionInterval || DEFAULT_REFLECTION_INTERVAL;
933
+
934
+ // 触发条件
935
+ const periodicTrigger = m.iteration > 1 && interval > 0 && m.iteration % interval === 0;
936
+ const staleTrigger =
937
+ m.roundsSinceNewInfo >= DEFAULT_STALE_THRESHOLD &&
938
+ m.iteration >= MIN_ITERS_FOR_STALE_REFLECTION;
939
+
940
+ if (!periodicTrigger && !staleTrigger) return null;
941
+
942
+ const summary = trace?.getRecentSummary?.(interval || 3);
943
+ if (!summary) return null;
944
+
945
+ const stats = trace?.getStats?.() || {};
946
+ const remaining = b.maxIterations - m.iteration;
947
+ const progressPct = Math.round((m.iteration / b.maxIterations) * 100);
948
+
949
+ const parts = [];
950
+ if (staleTrigger) {
951
+ parts.push(
952
+ `📊 停滞反思 (第 ${m.iteration}/${b.maxIterations} 轮, 连续 ${m.roundsSinceNewInfo} 轮无新信息):`
953
+ );
954
+ } else {
955
+ parts.push(`📊 中期反思 (第 ${m.iteration}/${b.maxIterations} 轮, ${progressPct}% 预算):`);
956
+ }
957
+
958
+ if (summary.thoughts?.length > 0) {
959
+ parts.push(
960
+ `\n你最近的思考方向:\n${summary.thoughts.map((t, i) => ` ${i + 1}. ${t}`).join('\n')}`
961
+ );
962
+ }
963
+
964
+ parts.push(
965
+ `\n行动效率: 最近 ${summary.roundCount} 轮中 ${Math.round(summary.newInfoRatio * 100)}% 获取到新信息`
966
+ );
967
+ parts.push(
968
+ `累计: ${m.uniqueFiles.size} 文件, ${m.uniquePatterns.size} 搜索模式, ${stats.totalActions || 0} 次工具调用`
969
+ );
970
+
971
+ // Planning 进度附加
972
+ if (this.#strategy.enablePlanning) {
973
+ const plan = trace?.getPlan?.();
974
+ if (plan?.steps?.length > 0) {
975
+ const doneCount = plan.steps.filter((s) => s.status === 'done').length;
976
+ parts.push(`\n📋 计划进度: ${doneCount}/${plan.steps.length} 步骤已完成`);
977
+ }
978
+ }
979
+
980
+ // 阶段化评估问题
981
+ if (this.#phase === 'EXPLORE' || this.#phase === 'SCAN' || this.#phase === 'VERIFY') {
982
+ parts.push(
983
+ `\n请评估:\n1. 到目前为止最重要的发现是什么?\n2. 还有哪些关键方面未覆盖?\n3. 剩余 ${remaining} 轮,最有价值的下一步是什么?`
984
+ );
985
+ } else if (this.#phase === 'PRODUCE') {
986
+ parts.push(
987
+ `\n请评估:\n1. 已提交的候选是否覆盖了核心发现?\n2. 是否有高价值知识点被遗漏?`
988
+ );
989
+ }
990
+
991
+ const reflectionText = parts.join('\n');
992
+ trace?.setReflection?.(reflectionText);
993
+ this.#logger.info(
994
+ `[ExplorationTracker] 💭 reflection triggered at iteration ${m.iteration} (${staleTrigger ? 'stale' : 'periodic'})`
995
+ );
996
+
997
+ return { type: 'reflection', text: reflectionText };
998
+ }
999
+
1000
+ /**
1001
+ * 检查是否需要触发规划 + 生成规划 nudge
1002
+ * @param {import('./memory/ActiveContext.js').ActiveContext} trace
1003
+ * @returns {{ type: string, text: string }|null}
1004
+ * @private
1005
+ */
1006
+ #checkPlanning(trace) {
1007
+ const m = this.#metrics;
1008
+ const b = this.#budget;
1009
+
1010
+ // 第 1 轮: plan elicitation
1011
+ if (m.iteration === 1) {
1012
+ return {
1013
+ type: 'planning',
1014
+ text: this.#buildPlanElicitationPrompt(),
1015
+ };
1016
+ }
1017
+
1018
+ // 有计划时: 检查 replan
1019
+ const plan = trace?.getPlan?.();
1020
+ if (!plan) return null;
1021
+
1022
+ const progress = this.#planProgress;
1023
+ const interval = this.#strategy.replanInterval || DEFAULT_REPLAN_INTERVAL;
1024
+ const deviationThreshold = DEFAULT_DEVIATION_THRESHOLD;
1025
+
1026
+ const baseIteration = progress.lastReplanIteration || plan.createdAtIteration;
1027
+ const periodicTrigger = interval > 0 && m.iteration > 1 && m.iteration - baseIteration >= interval;
1028
+ const deviationTrigger =
1029
+ progress.consecutiveOffPlan >= 3 ||
1030
+ (progress.totalSteps > 0 && progress.deviationScore > deviationThreshold);
1031
+
1032
+ if (!periodicTrigger && !deviationTrigger) return null;
1033
+
1034
+ const remaining = b.maxIterations - m.iteration;
1035
+ const parts = [];
1036
+ if (deviationTrigger) {
1037
+ parts.push(`📋 计划偏差检查 (第 ${m.iteration}/${b.maxIterations} 轮):`);
1038
+ if (progress.consecutiveOffPlan >= 3) {
1039
+ parts.push(`你的行为已连续 ${progress.consecutiveOffPlan} 轮偏离原定计划。`);
1040
+ }
1041
+ } else {
1042
+ parts.push(`📋 计划进度回顾 (第 ${m.iteration}/${b.maxIterations} 轮):`);
1043
+ }
1044
+
1045
+ const doneSteps = plan.steps.filter((s) => s.status === 'done');
1046
+ const pendingSteps = plan.steps.filter((s) => s.status === 'pending');
1047
+ if (doneSteps.length > 0) {
1048
+ parts.push(`\n✅ 已完成 (${doneSteps.length}/${plan.steps.length}):`);
1049
+ for (const s of doneSteps) parts.push(` - ${s.description}`);
1050
+ }
1051
+ if (pendingSteps.length > 0) {
1052
+ parts.push(`\n⏳ 未完成 (${pendingSteps.length}/${plan.steps.length}):`);
1053
+ for (const s of pendingSteps) parts.push(` - ${s.description}`);
1054
+ }
1055
+ if (progress.unplannedActions > 0) {
1056
+ parts.push(`\n⚡ 计划外行为: ${progress.unplannedActions} 次`);
1057
+ }
1058
+ parts.push(`\n剩余 ${remaining} 轮。请评估:`);
1059
+ parts.push(`1. 未完成的步骤是否仍然相关?`);
1060
+ parts.push(`2. 是否需要根据新发现调整后续步骤?`);
1061
+ parts.push(`3. 请更新你的探索计划(用编号列表)。`);
1062
+
1063
+ progress.lastReplanIteration = m.iteration;
1064
+ this.#pendingReplan = true;
1065
+
1066
+ this.#logger.info(
1067
+ `[ExplorationTracker] 📋 replan triggered at iteration ${m.iteration} (${deviationTrigger ? 'deviation' : 'periodic'})`
1068
+ );
1069
+
1070
+ return { type: 'planning', text: parts.join('\n') };
1071
+ }
1072
+
1073
+ /**
1074
+ * 构建首轮 plan elicitation prompt
1075
+ * @returns {string}
1076
+ * @private
1077
+ */
1078
+ #buildPlanElicitationPrompt() {
1079
+ const maxIter = this.#budget.maxIterations || 30;
1080
+ return [
1081
+ `📋 在开始探索前,请先制定一个简要的探索计划。`,
1082
+ ``,
1083
+ `你有 ${maxIter} 轮工具调用机会。请在你的回复中用编号列表简述 3-6 个探索步骤:`,
1084
+ `- 每个步骤应描述要搜索/阅读的目标(具体的类名、模式、文件路径)`,
1085
+ `- 步骤应从宏观到微观递进(先概览 → 再搜索关键模式 → 再深入关键文件)`,
1086
+ `- 最后一步应是"总结分析发现"`,
1087
+ ``,
1088
+ `例如:`,
1089
+ `1. 获取项目概览和目录结构,识别核心模块`,
1090
+ `2. 搜索网络请求相关类,分析请求模式`,
1091
+ `3. 搜索错误处理和响应解析模式`,
1092
+ `4. 深入阅读 3-5 个典型实现文件,确认关键细节`,
1093
+ `5. 总结分析发现`,
1094
+ ``,
1095
+ `制定计划后请立即开始执行第 1 步(可在同一轮中同时输出计划文本并调用工具)。`,
1096
+ ].join('\n');
1097
+ }
1098
+
1099
+ /**
1100
+ * 更新计划进度(从 ReasoningLayer 迁入)
1101
+ * 将本轮工具调用与 plan 步骤进行模糊匹配
1102
+ *
1103
+ * @param {import('./memory/ActiveContext.js').ActiveContext} trace
1104
+ */
1105
+ updatePlanProgress(trace) {
1106
+ if (!this.#strategy.enablePlanning) return;
1107
+
1108
+ const steps = trace?.getPlanStepsMutable?.() || [];
1109
+ if (steps.length === 0) return;
1110
+
1111
+ const actions = trace?.getCurrentRoundActions?.() || [];
1112
+ if (actions.length === 0) return;
1113
+
1114
+ let matchedThisRound = false;
1115
+
1116
+ for (const action of actions) {
1117
+ const matchedStep = this.#findMatchingStep(steps, action);
1118
+ if (matchedStep) {
1119
+ matchedStep.status = 'done';
1120
+ matchedThisRound = true;
1121
+ } else {
1122
+ this.#planProgress.unplannedActions++;
1123
+ }
1124
+ }
1125
+
1126
+ if (matchedThisRound) {
1127
+ this.#planProgress.consecutiveOffPlan = 0;
1128
+ } else {
1129
+ this.#planProgress.consecutiveOffPlan++;
1130
+ }
1131
+
1132
+ this.#planProgress.coveredSteps = steps.filter((s) => s.status === 'done').length;
1133
+ this.#planProgress.totalSteps = steps.length;
1134
+ this.#planProgress.deviationScore =
1135
+ steps.length > 0 ? 1 - this.#planProgress.coveredSteps / steps.length : 0;
1136
+
1137
+ // 处理 pending replan
1138
+ if (this.#pendingReplan) {
1139
+ const plan = trace?.getPlan?.();
1140
+ if (plan) {
1141
+ this.#planProgress.coveredSteps = plan.steps.filter((s) => s.status === 'done').length;
1142
+ this.#planProgress.totalSteps = plan.steps.length;
1143
+ this.#planProgress.unplannedActions = 0;
1144
+ this.#planProgress.consecutiveOffPlan = 0;
1145
+ this.#pendingReplan = false;
1146
+ }
1147
+ }
1148
+ }
1149
+
1150
+ /**
1151
+ * 模糊匹配: 将工具调用匹配到 plan 步骤(从 ReasoningLayer 迁入)
1152
+ * @param {Array} steps
1153
+ * @param {object} action — { tool, params }
1154
+ * @returns {object|null}
1155
+ * @private
1156
+ */
1157
+ #findMatchingStep(steps, action) {
1158
+ const toolName = action.tool;
1159
+ const argsStr = JSON.stringify(action.params || {}).toLowerCase();
1160
+
1161
+ for (const step of steps) {
1162
+ if (step.status === 'done') continue;
1163
+
1164
+ // 策略 1: 关键词匹配
1165
+ if (step.keywords?.length > 0) {
1166
+ const matched = step.keywords.some((kw) => argsStr.includes(kw.toLowerCase()));
1167
+ if (matched) return step;
1168
+ }
1169
+
1170
+ // 策略 2: 工具类型 → 步骤描述的语义匹配
1171
+ const desc = step.description.toLowerCase();
1172
+ if (
1173
+ toolName === 'get_project_overview' &&
1174
+ (desc.includes('概览') || desc.includes('overview') || desc.includes('结构') || desc.includes('项目'))
1175
+ ) return step;
1176
+ if (
1177
+ toolName === 'list_project_structure' &&
1178
+ (desc.includes('目录') || desc.includes('结构') || desc.includes('structure'))
1179
+ ) return step;
1180
+ if (
1181
+ (toolName === 'get_class_info' || toolName === 'get_class_hierarchy') &&
1182
+ (desc.includes('继承') || desc.includes('类') || desc.includes('hierarchy') || desc.includes('class'))
1183
+ ) return step;
1184
+ if (
1185
+ toolName === 'read_project_file' &&
1186
+ (desc.includes('阅读') || desc.includes('read') || desc.includes('深入') || desc.includes('查看') || desc.includes('文件'))
1187
+ ) return step;
1188
+ if (
1189
+ toolName === 'search_project_code' &&
1190
+ (desc.includes('搜索') || desc.includes('search') || desc.includes('查找') || desc.includes('分析'))
1191
+ ) return step;
1192
+ }
1193
+
1194
+ return null;
1195
+ }
1196
+
1197
+ // ─── 质量评分(从 ReasoningLayer 迁入,委托给 trace)────
1198
+
1199
+ /**
1200
+ * 推理质量评分
1201
+ * @param {import('./memory/ActiveContext.js').ActiveContext} trace
1202
+ * @returns {{ score: number, breakdown: object }}
1203
+ */
1204
+ getQualityMetrics(trace) {
1205
+ const stats = trace?.getStats?.() || { totalRounds: 0, thoughtCount: 0, totalActions: 0, totalObservations: 0, reflectionCount: 0 };
1206
+ const totalRounds = stats.totalRounds || 1;
1207
+
1208
+ const thoughtRatio = stats.thoughtCount / totalRounds;
1209
+ const reflectionRatio = stats.reflectionCount / totalRounds;
1210
+ const actionEfficiency = Math.min(stats.totalActions / totalRounds / 3, 1);
1211
+ const observationCoverage = stats.totalObservations > 0 ? 1 : 0;
1212
+
1213
+ const plan = trace?.getPlan?.();
1214
+ const hasPlan = plan && plan.steps.length > 0;
1215
+ let planScore = 0;
1216
+ if (hasPlan) {
1217
+ const completionRate =
1218
+ this.#planProgress.totalSteps > 0
1219
+ ? this.#planProgress.coveredSteps / this.#planProgress.totalSteps
1220
+ : 0;
1221
+ const adherenceRate = 1 - (this.#planProgress.deviationScore || 0);
1222
+ planScore = completionRate * 0.6 + adherenceRate * 0.4;
1223
+ }
1224
+
1225
+ const score = hasPlan
1226
+ ? Math.round(
1227
+ (thoughtRatio * 0.3 +
1228
+ reflectionRatio * 0.15 +
1229
+ actionEfficiency * 0.15 +
1230
+ observationCoverage * 0.15 +
1231
+ planScore * 0.25) * 100
1232
+ )
1233
+ : Math.round(
1234
+ (thoughtRatio * 0.4 +
1235
+ reflectionRatio * 0.2 +
1236
+ actionEfficiency * 0.2 +
1237
+ observationCoverage * 0.2) * 100
1238
+ );
1239
+
1240
+ const breakdown = {
1241
+ ...stats,
1242
+ thoughtRatio: Math.round(thoughtRatio * 100),
1243
+ reflectionRatio: Math.round(reflectionRatio * 100),
1244
+ actionEfficiency: Math.round(actionEfficiency * 100),
1245
+ observationCoverage: Math.round(observationCoverage * 100),
1246
+ };
1247
+
1248
+ if (hasPlan) {
1249
+ breakdown.planCompletion = Math.round(
1250
+ (this.#planProgress.totalSteps > 0
1251
+ ? this.#planProgress.coveredSteps / this.#planProgress.totalSteps
1252
+ : 0) * 100
1253
+ );
1254
+ breakdown.planAdherence = Math.round((1 - (this.#planProgress.deviationScore || 0)) * 100);
1255
+ breakdown.planScore = Math.round(planScore * 100);
1256
+ }
1257
+
1258
+ return { score, breakdown };
1259
+ }
1260
+ }
1261
+
1262
+ export default ExplorationTracker;