autosnippet 3.2.9 → 3.2.10

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.
@@ -39,6 +39,8 @@ const DEFAULT_DEVIATION_THRESHOLD = 0.6;
39
39
  const DEFAULT_MIN_EXPLORE_ITERS = 10;
40
40
  /** 默认停滞收敛阈值 */
41
41
  const DEFAULT_CONVERGENCE_STALE_THRESHOLD = 3;
42
+ /** 最少经过 N 轮后才允许再次触发 replan(防止 replan 风暴) */
43
+ const MIN_REPLAN_GAP = 3;
42
44
 
43
45
  // ─── 内置策略 ────────────────────────────────────────────
44
46
 
@@ -227,6 +229,8 @@ export class ExplorationTracker {
227
229
 
228
230
  /** @type {boolean} tick 是否已调用(用于 rollback) */
229
231
  #ticked = false;
232
+ /** @type {string} 提交工具名(用于 nudge 文本生成) */
233
+ #submitToolName = 'submit_knowledge';
230
234
 
231
235
  /**
232
236
  * @param {object} strategy — 策略配置对象
@@ -246,6 +250,8 @@ export class ExplorationTracker {
246
250
  idleRoundsToExit: 3,
247
251
  ...budget,
248
252
  };
253
+ /** @type {string} 提交工具名 — bootstrap 用 submit_knowledge,scan 用 collect_scan_recipe */
254
+ this.#submitToolName = budget.submitToolName || 'submit_knowledge';
249
255
  this.#phase = strategy.phases[0];
250
256
  this.#logger = Logger.getInstance();
251
257
  }
@@ -306,11 +312,20 @@ export class ExplorationTracker {
306
312
  }
307
313
  }
308
314
 
315
+ /** 提交工具名 (供外部判断是否为 scan / bootstrap pipeline) */
316
+ get submitToolName() {
317
+ return this.#submitToolName;
318
+ }
319
+
309
320
  /**
310
321
  * 是否应退出主循环
311
322
  * @returns {boolean}
312
323
  */
313
324
  shouldExit() {
325
+ // Scan pipeline: SUMMARIZE 无消费方,recipes 已通过工具调用收集,直接退出
326
+ if (this.#isTerminalPhase() && this.#submitToolName === 'collect_scan_recipe') {
327
+ return true;
328
+ }
314
329
  // 终结阶段 + 已给了 2 轮 grace → 退出
315
330
  if (this.#isTerminalPhase() && this.#metrics.phaseRounds >= 2) {
316
331
  return true;
@@ -357,7 +372,7 @@ export class ExplorationTracker {
357
372
  // 1. 强制退出
358
373
  if (this.#gracefulExitRound != null && m.iteration === this.#gracefulExitRound) {
359
374
  const submitCount = m.submitCount;
360
- // v5.1: Analyst 策略使用纯文本输出,其他策略使用 dimensionDigest JSON
375
+ // v5.1: Analyst 策略使用纯文本输出
361
376
  if (this.#strategy.name === 'analyst') {
362
377
  return {
363
378
  type: 'force_exit',
@@ -371,6 +386,17 @@ export class ExplorationTracker {
371
386
  `⛔ 严禁在回复中复制或引用本条指令的任何文字,只输出你自己的分析。`,
372
387
  };
373
388
  }
389
+ // Scan pipeline (collect_scan_recipe): 使用纯文本总结,不需要 dimensionDigest
390
+ if (this.#submitToolName === 'collect_scan_recipe') {
391
+ return {
392
+ type: 'force_exit',
393
+ text: `⚠️ **轮次耗尽** (${m.iteration}/${b.maxIterations})。你必须**立即停止工具调用**。\n\n` +
394
+ `已通过 collect_scan_recipe 提交了 ${submitCount} 个知识候选。` +
395
+ `请直接输出你的分析总结(Markdown 格式),列出已发现和未覆盖的关键模式。\n` +
396
+ `⛔ 不要再调用任何工具,直接输出文本。`,
397
+ };
398
+ }
399
+ // Bootstrap 策略: 使用 dimensionDigest JSON (供维度编排消费)
374
400
  return {
375
401
  type: 'force_exit',
376
402
  text: `⚠️ 你已使用 ${m.iteration}/${b.maxIterations} 轮次,**必须立即结束**。请在回复中直接输出 dimensionDigest JSON 总结(用 \`\`\`json 包裹),不要再调用任何工具。\n` +
@@ -537,6 +563,13 @@ export class ExplorationTracker {
537
563
  // 4. 如果发生了转换,生成 nudge 立即返回给主循环注入
538
564
  if (this.#justTransitioned) {
539
565
  this.#justTransitioned = false;
566
+ // Scan pipeline: 不注入转换 nudge,shouldExit 会在下一轮直接退出
567
+ if (this.#submitToolName === 'collect_scan_recipe' && this.#isTerminalPhase()) {
568
+ this.#logger.info(
569
+ `[ExplorationTracker] scan pipeline: skip SUMMARIZE nudge, will exit on next tick (submits=${this.#metrics.submitCount})`
570
+ );
571
+ return null;
572
+ }
540
573
  return {
541
574
  type: 'phase_transition',
542
575
  text: this.#buildTransitionNudge(),
@@ -568,8 +601,16 @@ export class ExplorationTracker {
568
601
  }
569
602
 
570
603
  if (isTerminal && transitioned) {
571
- // 刚转入终结阶段 → 需要 digest nudge,不是最终回答
604
+ // 刚转入终结阶段
572
605
  const submitCount = m.submitCount;
606
+
607
+ // Scan pipeline (collect_scan_recipe): 不需要 dimensionDigest
608
+ // 所有 recipes 已通过工具调用收集,LLM 当前输出的文本就是最终分析报告
609
+ // 直接标记 isFinalAnswer,避免浪费一轮迭代要求无用的 JSON digest
610
+ if (this.#submitToolName === 'collect_scan_recipe') {
611
+ return { isFinalAnswer: true, needsDigestNudge: false, shouldContinue: false, nudge: null };
612
+ }
613
+
573
614
  // v5.1: Analyst 策略要求自然语言输出
574
615
  const nudge = this.#strategy.name === 'analyst'
575
616
  ? `请**停止调用工具**,直接输出你的完整分析报告。用 Markdown 格式,包含具体文件路径、类名和代码模式。至少涵盖 3 个核心发现。\n\n**现在开始输出你的分析报告。**\n⚠️ 严禁在回复中复制本条指令文字,只输出你自己的分析。`
@@ -586,9 +627,10 @@ export class ExplorationTracker {
586
627
 
587
628
  // 非终结阶段收到文本
588
629
  if (this.#phase === 'PRODUCE' || this.#phase === 'EXPLORE') {
589
- // PRODUCE 阶段中间文本 继续循环,注入提交引导
590
- const nudge = this.#phase === 'PRODUCE'
591
- ? '你的分析很好。请继续调用 submit_knowledge 提交你发现的知识候选,每个值得记录的模式/实践都应该提交。'
630
+ // Scan pipeline: PRODUCE 阶段不注入 continue nudge — 避免与即将到来的转换 nudge 矛盾
631
+ // Bootstrap pipeline: 保留提交引导
632
+ const nudge = (this.#phase === 'PRODUCE' && this.#submitToolName !== 'collect_scan_recipe')
633
+ ? `你的分析很好。请继续调用 ${this.#submitToolName} 提交你发现的知识候选,每个值得记录的模式/实践都应该提交。`
592
634
  : null;
593
635
  return { isFinalAnswer: false, needsDigestNudge: false, shouldContinue: true, nudge };
594
636
  }
@@ -652,6 +694,7 @@ export class ExplorationTracker {
652
694
  return {
653
695
  iteration: this.#metrics.iteration,
654
696
  phase: this.#phase,
697
+ phaseRounds: this.#metrics.phaseRounds,
655
698
  submitCount: this.#metrics.submitCount,
656
699
  uniqueFiles: this.#metrics.uniqueFiles.size,
657
700
  uniquePatterns: this.#metrics.uniquePatterns.size,
@@ -661,6 +704,11 @@ export class ExplorationTracker {
661
704
  };
662
705
  }
663
706
 
707
+ /** 当前指标快照 (便捷 getter, 与 getMetrics() 相同) */
708
+ get metrics() {
709
+ return this.getMetrics();
710
+ }
711
+
664
712
  /**
665
713
  * 获取计划进度
666
714
  * @returns {object}
@@ -872,7 +920,7 @@ export class ExplorationTracker {
872
920
  const toPhase = this.#phase;
873
921
 
874
922
  if (toPhase === 'PRODUCE') {
875
- return '你已充分探索了项目代码,现在请开始调用 submit_knowledge 工具来提交你发现的知识候选。不要再搜索,直接提交。';
923
+ return `你已充分探索了项目代码,现在请开始调用 ${this.#submitToolName} 工具来提交你发现的知识候选。不要再搜索,直接提交。`;
876
924
  }
877
925
 
878
926
  if (toPhase === 'SUMMARIZE') {
@@ -889,6 +937,13 @@ export class ExplorationTracker {
889
937
  `**现在开始输出你的分析报告。不要再调用任何工具。**\n` +
890
938
  `⚠️ 以上是行为指令,严禁在回复中复制或引用这段文字,只输出你自己的分析内容。`;
891
939
  }
940
+ // Scan pipeline (collect_scan_recipe): 使用纯文本总结
941
+ if (this.#submitToolName === 'collect_scan_recipe') {
942
+ return `你已通过 collect_scan_recipe 提交了 ${submitCount} 个知识候选。` +
943
+ `请**停止调用工具**,直接输出你的分析总结(Markdown 格式)。\n` +
944
+ `⚠️ 不要再调用任何工具,直接输出文本。`;
945
+ }
946
+ // Bootstrap: 使用 dimensionDigest JSON (供维度编排消费)
892
947
  return `你已完成分析探索。请在回复中直接输出 dimensionDigest JSON(用 \`\`\`json 包裹),包含以下字段:\n` +
893
948
  `\`\`\`json\n{"dimensionDigest":{"summary":"分析总结(100-200字)","candidateCount":${submitCount},"keyFindings":["关键发现"],"crossRefs":{},"gaps":["未覆盖方面"],"remainingTasks":[{"signal":"未处理的信号/主题","reason":"未完成原因(如:提交上限已达)","priority":"high|medium|low","searchHints":["建议搜索词"]}]}}\n\`\`\`\n> 如果所有信号都已覆盖,remainingTasks 留空数组 \`[]\`。如果有未来得及处理的信号,请在此标记,系统会在下次运行时续传。\n` +
894
949
  `⚠️ 严禁在回复中复制本条指令文字,只输出 JSON。`;
@@ -923,10 +978,13 @@ export class ExplorationTracker {
923
978
 
924
979
  case 'PRODUCE':
925
980
  if (m.submitCount === 0 && m.phaseRounds >= 1) {
926
- return '⚠️ 探索阶段已结束。你已收集了足够的项目信息,请 **立即** 调用 submit_knowledge 提交候选。不要继续搜索,直接提交。';
981
+ return `⚠️ 探索阶段已结束。你已收集了足够的项目信息,请 **立即** 调用 ${this.#submitToolName} 提交候选。不要继续搜索,直接提交。`;
927
982
  }
928
983
  if (m.submitCount >= b.softSubmitLimit && b.softSubmitLimit > 0) {
929
984
  const remaining = b.maxSubmits - m.submitCount;
985
+ if (this.#submitToolName === 'collect_scan_recipe') {
986
+ return `已提交 ${m.submitCount} 个候选(上限 ${b.maxSubmits})。${remaining > 0 ? `还可提交 ${remaining} 个。` : ''}如果还有值得记录的发现可以继续提交,否则请输出分析总结。`;
987
+ }
930
988
  return `已提交 ${m.submitCount} 个候选(上限 ${b.maxSubmits})。${remaining > 0 ? `还可提交 ${remaining} 个。` : ''}如果还有值得记录的发现可以继续提交,否则请产出 dimensionDigest 总结。\n⚠️ 如果还有未处理的信号,请在 dimensionDigest 的 remainingTasks 字段中标记,下次运行时会续传。`;
931
989
  }
932
990
  return null;
@@ -1060,6 +1118,7 @@ export class ExplorationTracker {
1060
1118
 
1061
1119
  // 第 1 轮: plan elicitation
1062
1120
  if (m.iteration === 1) {
1121
+ trace?.expectPlan?.(); // 授权 ActiveContext 捕获计划
1063
1122
  return {
1064
1123
  type: 'planning',
1065
1124
  text: this.#buildPlanElicitationPrompt(),
@@ -1082,6 +1141,12 @@ export class ExplorationTracker {
1082
1141
 
1083
1142
  if (!periodicTrigger && !deviationTrigger) return null;
1084
1143
 
1144
+ // 冷却间隔: 上次 replan 后至少 MIN_REPLAN_GAP 轮才允许再次触发
1145
+ // 防止“坏 plan 步骤永远不匹配 → 每轮 deviation → replan 风暴”
1146
+ if (progress.lastReplanIteration && m.iteration - progress.lastReplanIteration < MIN_REPLAN_GAP) {
1147
+ return null;
1148
+ }
1149
+
1085
1150
  const remaining = b.maxIterations - m.iteration;
1086
1151
  const parts = [];
1087
1152
  if (deviationTrigger) {
@@ -1113,6 +1178,7 @@ export class ExplorationTracker {
1113
1178
 
1114
1179
  progress.lastReplanIteration = m.iteration;
1115
1180
  this.#pendingReplan = true;
1181
+ trace?.expectPlan?.(); // 授权 ActiveContext 捕获 replan
1116
1182
 
1117
1183
  this.#logger.info(
1118
1184
  `[ExplorationTracker] 📋 replan triggered at iteration ${m.iteration} (${deviationTrigger ? 'deviation' : 'periodic'})`
@@ -1159,6 +1225,20 @@ export class ExplorationTracker {
1159
1225
  const steps = trace?.getPlanStepsMutable?.() || [];
1160
1226
  if (steps.length === 0) return;
1161
1227
 
1228
+ // 处理 pending replan — 必须在 actions 检查前处理
1229
+ // 因为 LLM 回复 replan 时可能会同时输出新计划 + 纯文本(无工具调用)
1230
+ // 此时 extractAndSetPlan 已更新 plan steps,需要重新计算进度
1231
+ if (this.#pendingReplan) {
1232
+ const plan = trace?.getPlan?.();
1233
+ if (plan) {
1234
+ this.#planProgress.coveredSteps = plan.steps.filter((s) => s.status === 'done').length;
1235
+ this.#planProgress.totalSteps = plan.steps.length;
1236
+ this.#planProgress.unplannedActions = 0;
1237
+ this.#planProgress.consecutiveOffPlan = 0;
1238
+ this.#pendingReplan = false;
1239
+ }
1240
+ }
1241
+
1162
1242
  const actions = trace?.getCurrentRoundActions?.() || [];
1163
1243
  if (actions.length === 0) return;
1164
1244
 
@@ -1184,18 +1264,6 @@ export class ExplorationTracker {
1184
1264
  this.#planProgress.totalSteps = steps.length;
1185
1265
  this.#planProgress.deviationScore =
1186
1266
  steps.length > 0 ? 1 - this.#planProgress.coveredSteps / steps.length : 0;
1187
-
1188
- // 处理 pending replan
1189
- if (this.#pendingReplan) {
1190
- const plan = trace?.getPlan?.();
1191
- if (plan) {
1192
- this.#planProgress.coveredSteps = plan.steps.filter((s) => s.status === 'done').length;
1193
- this.#planProgress.totalSteps = plan.steps.length;
1194
- this.#planProgress.unplannedActions = 0;
1195
- this.#planProgress.consecutiveOffPlan = 0;
1196
- this.#pendingReplan = false;
1197
- }
1198
- }
1199
1267
  }
1200
1268
 
1201
1269
  /**
@@ -152,8 +152,10 @@ export function cleanFinalAnswer(response) {
152
152
  .replace(/^\*{0,2}(?:请在|请直接|请确保|请务必|现在开始|输出你的|不要输出|不要再|不要包含|重要\s*[::]).*(?:分析文本|分析总结|JSON|工具|输出|文本|报告)\*{0,2}[。.]?\s*$/gm, '')
153
153
  .replace(/^注意[::]\s*到达第\s*\d+\s*轮时.*$/gm, '')
154
154
  .replace(/^第\s*\d+\/\d+\s*轮\s*\|[^\n]*$/gm, '')
155
- // 清理 dimensionDigest JSON 块(Analyst 回显)
156
- .replace(/```json\s*\n\s*\{\s*"dimensionDigest"\s*:[\s\S]*?\n```/g, '')
155
+ // v5.2: 移除 dimensionDigest JSON 剥离
156
+ // 之前会把 SUMMARIZE 阶段 LLM 按要求产出的 dimensionDigest JSON 全部删掉 → 0 chars
157
+ // dimensionDigest 是 SUMMARIZE 的预期输出,不应被 cleanFinalAnswer 清理
158
+ // Analyst 策略的 SUMMARIZE 使用自然语言 Markdown(不含 dimensionDigest),不受影响
157
159
  .replace(/\n{3,}/g, '\n\n')
158
160
  .trim();
159
161
  }
@@ -61,16 +61,12 @@ ${highSim.map((s) => `- ${s.title} (相似度: ${s.similarity})`).join('\n')}
61
61
 
62
62
  /**
63
63
  * 任务: 批量发现 Recipe 间的知识图谱关系
64
- * 委托给 AgentFactory.scanKnowledge (Agent 多轮推理)
64
+ * 委托给 AgentFactory.discoverRelations (独立 Explore → Synthesize 管线)
65
65
  */
66
66
  export async function taskDiscoverAllRelations(context, { batchSize = 20 } = {}) {
67
67
  const { container } = context;
68
68
  const agentFactory = container.get('agentFactory');
69
- return agentFactory.scanKnowledge({
70
- label: 'knowledge-graph',
71
- files: [],
72
- task: 'relations',
73
- });
69
+ return agentFactory.discoverRelations({ batchSize });
74
70
  }
75
71
 
76
72
  /**
@@ -249,8 +249,11 @@ export function producerRejectionGateEvaluator(source, _phaseResults, _strategyC
249
249
  return { action: 'pass', reason: '' };
250
250
  }
251
251
 
252
+ // 可配置的提交工具名 — bootstrap 用 submit_knowledge/submit_with_check,scan 用 collect_scan_recipe
253
+ const submitToolNames = _strategyContext.submitToolNames
254
+ || ['submit_knowledge', 'submit_with_check'];
252
255
  const submitCalls = (source.toolCalls || []).filter(tc =>
253
- ['submit_knowledge', 'submit_with_check'].includes(tc.tool || tc.name),
256
+ submitToolNames.includes(tc.tool || tc.name),
254
257
  );
255
258
  const rejected = submitCalls.filter(tc => {
256
259
  const res = tc.result;