autosnippet 2.6.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +137 -65
  2. package/bin/api-server.js +5 -0
  3. package/bin/cli.js +6 -1
  4. package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-B_Xg4B-s.js} +148 -88
  5. package/dashboard/dist/assets/index-BjfUm8p9.js +197 -0
  6. package/dashboard/dist/assets/index-CkIih2CC.css +1 -0
  7. package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
  8. package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
  9. package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
  10. package/dashboard/dist/index.html +6 -6
  11. package/lib/bootstrap.js +18 -1
  12. package/lib/cli/SetupService.js +86 -8
  13. package/lib/cli/UpgradeService.js +139 -2
  14. package/lib/core/ast/ProjectGraph.js +599 -0
  15. package/lib/core/gateway/GatewayActionRegistry.js +2 -2
  16. package/lib/domain/recipe/Recipe.js +3 -0
  17. package/lib/external/ai/AiProvider.js +83 -20
  18. package/lib/external/ai/providers/ClaudeProvider.js +208 -0
  19. package/lib/external/ai/providers/GoogleGeminiProvider.js +247 -1
  20. package/lib/external/ai/providers/OpenAiProvider.js +141 -0
  21. package/lib/external/mcp/McpServer.js +6 -1
  22. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
  23. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +657 -0
  24. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +160 -0
  25. package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
  26. package/lib/external/mcp/handlers/bootstrap.js +159 -1634
  27. package/lib/external/mcp/handlers/browse.js +1 -1
  28. package/lib/external/mcp/handlers/candidate.js +1 -33
  29. package/lib/external/mcp/handlers/skill.js +58 -17
  30. package/lib/external/mcp/tools.js +4 -3
  31. package/lib/http/middleware/requestLogger.js +23 -4
  32. package/lib/http/routes/ai.js +158 -2
  33. package/lib/http/routes/auth.js +3 -2
  34. package/lib/http/routes/candidates.js +49 -25
  35. package/lib/http/routes/commands.js +0 -8
  36. package/lib/http/routes/guardRules.js +1 -16
  37. package/lib/http/routes/recipes.js +4 -17
  38. package/lib/http/routes/search.js +11 -19
  39. package/lib/http/routes/skills.js +2 -0
  40. package/lib/http/routes/snippets.js +0 -33
  41. package/lib/http/routes/spm.js +37 -63
  42. package/lib/http/utils/routeHelpers.js +31 -0
  43. package/lib/infrastructure/config/Paths.js +12 -0
  44. package/lib/infrastructure/database/DatabaseConnection.js +6 -1
  45. package/lib/infrastructure/logging/Logger.js +86 -3
  46. package/lib/infrastructure/realtime/RealtimeService.js +2 -5
  47. package/lib/infrastructure/vector/JsonVectorAdapter.js +26 -1
  48. package/lib/injection/ServiceContainer.js +55 -2
  49. package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
  50. package/lib/service/candidate/CandidateFileWriter.js +72 -27
  51. package/lib/service/candidate/CandidateService.js +156 -10
  52. package/lib/service/chat/AnalystAgent.js +245 -0
  53. package/lib/service/chat/CandidateGuardrail.js +134 -0
  54. package/lib/service/chat/ChatAgent.js +1055 -167
  55. package/lib/service/chat/ContextWindow.js +730 -0
  56. package/lib/service/chat/ConversationStore.js +3 -0
  57. package/lib/service/chat/HandoffProtocol.js +181 -0
  58. package/lib/service/chat/Memory.js +3 -0
  59. package/lib/service/chat/ProducerAgent.js +293 -0
  60. package/lib/service/chat/ToolRegistry.js +149 -5
  61. package/lib/service/chat/tools.js +1404 -61
  62. package/lib/service/guard/ExclusionManager.js +2 -0
  63. package/lib/service/guard/RuleLearner.js +2 -0
  64. package/lib/service/quality/FeedbackCollector.js +2 -0
  65. package/lib/service/recipe/RecipeFileWriter.js +16 -1
  66. package/lib/service/recipe/RecipeStatsTracker.js +2 -0
  67. package/lib/service/skills/SignalCollector.js +33 -6
  68. package/lib/service/skills/SkillAdvisor.js +2 -1
  69. package/lib/service/skills/SkillHooks.js +13 -5
  70. package/lib/service/spm/SpmService.js +2 -2
  71. package/lib/shared/PathGuard.js +314 -0
  72. package/package.json +1 -1
  73. package/resources/native-ui/combined-window.swift +494 -0
  74. package/templates/copilot-instructions.md +20 -3
  75. package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
  76. package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
  77. package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
  78. package/dashboard/dist/assets/index-DLsECfzW.css +0 -1
@@ -29,12 +29,85 @@ import Logger from '../../infrastructure/logging/Logger.js';
29
29
  import { TaskPipeline } from './TaskPipeline.js';
30
30
  import { Memory } from './Memory.js';
31
31
  import { ConversationStore } from './ConversationStore.js';
32
+ import { ContextWindow, PhaseRouter, limitToolResult } from './ContextWindow.js';
32
33
 
33
34
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
34
35
  const PROJECT_ROOT = path.resolve(__dirname, '../../..');
35
36
  const SKILLS_DIR = path.resolve(PROJECT_ROOT, 'skills');
36
37
  const SOUL_PATH = path.resolve(PROJECT_ROOT, 'SOUL.md');
37
38
  const MAX_ITERATIONS = 6;
39
+ /** 系统调用 (如 bootstrap) 允许更多迭代,因为每维度需要多次 submit_candidate */
40
+ const MAX_ITERATIONS_SYSTEM = 30;
41
+ /** 原生函数调用模式下,已提交 ≥ MIN_SUBMITS_FOR_EARLY_EXIT 个候选后,连续 N 轮无新提交则提前退出 */
42
+ const MIN_SUBMITS_FOR_EARLY_EXIT = 1;
43
+ const IDLE_ROUNDS_TO_EXIT = 2;
44
+ /** 单个维度最多提交候选数量 — 超过后跳过提交返回提醒 */
45
+ const MAX_SUBMITS_PER_DIMENSION = 6;
46
+ /** 提交达到软上限后注入收尾提示的阈值 */
47
+ const SOFT_SUBMIT_LIMIT = 4;
48
+ /** 连续搜索/阅读轮次预算 — 超过后注入提交提示并切 auto */
49
+ const SEARCH_BUDGET = 8;
50
+ /** 搜索预算耗尽后,额外容忍的轮次 — 再未提交则强制退出 */
51
+ const SEARCH_BUDGET_GRACE = 4;
52
+
53
+ /** 默认预算配置 — 可通过 execute() 的 opts.budget 覆盖 */
54
+ const DEFAULT_BUDGET = Object.freeze({
55
+ maxIterations: MAX_ITERATIONS_SYSTEM,
56
+ searchBudget: SEARCH_BUDGET,
57
+ searchBudgetGrace: SEARCH_BUDGET_GRACE,
58
+ maxSubmits: MAX_SUBMITS_PER_DIMENSION,
59
+ softSubmitLimit: SOFT_SUBMIT_LIMIT,
60
+ idleRoundsToExit: IDLE_ROUNDS_TO_EXIT,
61
+ });
62
+
63
+ /**
64
+ * 系统调用续跑提示 — 当 AI 输出纯文本计划而未执行工具调用时注入
65
+ * 告诉 AI 不要只写文字描述,而要实际调用工具
66
+ */
67
+ const SYSTEM_CONTINUATION_PROMPT = `你的分析计划很好。但你需要 **实际执行工具调用** 来完成任务,而不是只写文字描述。
68
+
69
+ 请现在开始执行:
70
+ 1. 用 \`search_project_code\` 搜索项目代码获取真实示例
71
+ 2. 用 \`read_project_file\` 查看完整文件内容
72
+ 3. 对每个值得保留的信号,用 \`submit_candidate\` 提交候选
73
+
74
+ ⚡ 推荐使用 batch_actions 一次提交多条候选:
75
+ \`\`\`batch_actions
76
+ [
77
+ {"tool": "submit_candidate", "params": {"title": "[Bootstrap] xxx/子主题", "code": "# 标题 — 项目特写\\n\\n> 摘要...\\n\\n描述和代码交织...", "language": "objectivec", "category": "Service", "summary": "...", "tags": ["bootstrap"], "source": "bootstrap", "reasoning": {"whyStandard": "...", "sources": ["file1"], "confidence": 0.7}}},
78
+ {"tool": "submit_candidate", "params": {"title": "...", "code": "...", ...}}
79
+ ]
80
+ \`\`\`
81
+
82
+ 请立即开始执行,不要再输出分析文字。`;
83
+
84
+ /**
85
+ * 系统调用提交提示 — 当 AI 做了工具调用(search/read)、写了分析文本,但没调 submit_candidate 时注入
86
+ * 引导 AI 将已有分析转化为实际的 submit_candidate 调用
87
+ */
88
+ const SYSTEM_SUBMIT_PROMPT = `你的分析很好,已经获取了足够的项目信息。但你还没有调用 \`submit_candidate\` 提交任何候选。
89
+
90
+ **你的分析不能只停留在文字描述层面** — 必须通过工具调用将分析结果持久化。
91
+
92
+ 请根据你刚才的分析,立即使用 batch_actions 提交候选:
93
+
94
+ \`\`\`batch_actions
95
+ [
96
+ {"tool": "submit_candidate", "params": {
97
+ "title": "[Bootstrap] 维度/子主题",
98
+ "code": "# 标题 — 项目特写\\n\\n> 本项目使用 XX 模式, N 个文件采用此写法\\n\\n描述...\\n\\n\`\`\`objc\\n// 真实代码示例\\n\`\`\`\\n\\n要点说明...",
99
+ "language": "objectivec",
100
+ "category": "Tool",
101
+ "summary": "≤80字精准摘要,引用真实类名和数字",
102
+ "tags": ["bootstrap", "维度id"],
103
+ "source": "bootstrap",
104
+ "reasoning": {"whyStandard": "为什么值得保留", "sources": ["真实文件名"], "confidence": 0.7}
105
+ }},
106
+ {"tool": "submit_candidate", "params": {...}}
107
+ ]
108
+ \`\`\`
109
+
110
+ 将你上面分析出的每个有价值的发现都转化为一条 submit_candidate 调用。code 字段写「项目特写」风格: 描述和代码交织,用项目真实类名和代码。`;
38
111
 
39
112
  export class ChatAgent {
40
113
  #toolRegistry;
@@ -51,6 +124,12 @@ export class ChatAgent {
51
124
  #conversations = null;
52
125
  /** @type {string|null} 当前 execute 调用的 source — 'user' | 'system' */
53
126
  #currentSource = null;
127
+ /** @type {Array|null} 内存文件缓存(bootstrap 场景注入,search_project_code/read_project_file 优先使用) */
128
+ #fileCache = null;
129
+ /** @type {Set<string>} 跨维度已提交候选标题(bootstrap 全局去重) */
130
+ #globalSubmittedTitles = new Set();
131
+ /** @type {{ input: number, output: number }} 当前 execute() 累计 token 用量 */
132
+ #currentTokenUsage = { input: 0, output: 0 };
54
133
 
55
134
  /**
56
135
  * @param {object} opts
@@ -67,6 +146,12 @@ export class ChatAgent {
67
146
  /** 是否有 AI Provider(只读) */
68
147
  this.hasAI = !!aiProvider;
69
148
 
149
+ /**
150
+ * 是否有真实(非 Mock)AI Provider
151
+ * MockProvider 不具备实际推理能力,bootstrap 编排时应视为 AI 不可用
152
+ */
153
+ this.hasRealAI = !!aiProvider && aiProvider.name !== 'mock';
154
+
70
155
  // 初始化跨对话记忆 + 对话持久化
71
156
  try {
72
157
  const projectRoot = container?.singletons?._projectRoot || process.cwd();
@@ -80,6 +165,22 @@ export class ChatAgent {
80
165
 
81
166
  // ─── 公共 API ─────────────────────────────────────────
82
167
 
168
+ /**
169
+ * 注入内存文件缓存(bootstrap 场景: allFiles 已在内存中,避免重复磁盘读取)
170
+ * 调用后 search_project_code / read_project_file 优先从缓存查找
171
+ * @param {Array|null} files — [{ relativePath, content, name }]
172
+ */
173
+ setFileCache(files) {
174
+ this.#fileCache = files;
175
+ }
176
+
177
+ /**
178
+ * 重置跨维度全局提交标题(新 bootstrap session 开始时调用)
179
+ */
180
+ resetGlobalSubmittedTitles() {
181
+ this.#globalSubmittedTitles.clear();
182
+ }
183
+
83
184
  /**
84
185
  * 交互式对话(Dashboard Chat 入口)
85
186
  * 自动带 ReAct 循环: LLM 可决定调用工具或直接回答
@@ -89,25 +190,533 @@ export class ChatAgent {
89
190
  * @param {Array} opts.history — 对话历史 [{role, content}]
90
191
  * @param {string} [opts.conversationId] — 对话 ID(启用持久化时)
91
192
  * @param {'user'|'system'} [opts.source='user'] — 调用来源(影响 Memory 隔离)
193
+ * @param {object} [opts.dimensionMeta] — Bootstrap 维度元数据 { id, outputType, allowedKnowledgeTypes }
92
194
  * @returns {Promise<{reply: string, toolCalls: Array, hasContext: boolean, conversationId?: string}>}
93
195
  */
94
- async execute(prompt, { history = [], conversationId, source = 'user' } = {}) {
196
+ async execute(prompt, { history = [], conversationId, source = 'user', budget: budgetOverrides, dimensionId, dimensionMeta,
197
+ // v3.0: Agent 分离选项
198
+ systemPromptOverride, // 覆盖默认 system prompt (Analyst/Producer 各自使用)
199
+ allowedTools, // 覆盖默认工具白名单 (string[])
200
+ disablePhaseRouter = false, // 禁用 PhaseRouter (Analyst 不需要阶段控制)
201
+ temperature: temperatureOverride, // 覆盖默认温度
202
+ } = {}) {
95
203
  this.#currentSource = source;
204
+ this.#currentTokenUsage = { input: 0, output: 0 };
205
+ const execStartTime = Date.now();
206
+ const promptPreview = prompt.length > 80 ? prompt.substring(0, 80) + '…' : prompt;
207
+ this.#logger.info(`[ChatAgent] ▶ execute — source=${source}${dimensionMeta?.id ? ', dim=' + dimensionMeta.id + '(' + dimensionMeta.outputType + ')' : (dimensionId ? ', dim=' + dimensionId : '')}, prompt="${promptPreview}", historyLen=${history.length}${conversationId ? ', convId=' + conversationId.substring(0, 8) : ''}`);
208
+
209
+ // 合并预算配置: 默认值 + 外部覆盖
210
+ const budget = budgetOverrides
211
+ ? { ...DEFAULT_BUDGET, ...budgetOverrides }
212
+ : { ...DEFAULT_BUDGET };
96
213
 
97
214
  // 对话持久化: 如果传了 conversationId,从 ConversationStore 加载历史
98
215
  let effectiveHistory = history;
99
216
  if (conversationId && this.#conversations) {
100
217
  effectiveHistory = this.#conversations.load(conversationId);
218
+ this.#logger.info(`[ChatAgent] loaded ${effectiveHistory.length} messages from conversation store`);
101
219
  this.#conversations.append(conversationId, { role: 'user', content: prompt });
102
220
  }
103
221
 
104
222
  // 每次对话刷新项目概况(不是每轮 ReAct)
105
223
  this.#projectBriefingCache = await this.#buildProjectBriefing();
106
224
 
225
+ // ── 双模路由: 原生函数调用 vs 文本解析 ──
226
+ // 支持原生函数调用的 Provider (如 Gemini) 走结构化路径,
227
+ // 其他 Provider 走传统文本 ReAct 解析路径
228
+ let result;
229
+ if (this.#aiProvider.supportsNativeToolCalling) {
230
+ this.#logger.info(`[ChatAgent] ✨ using NATIVE tool calling mode (${this.#aiProvider.name})`);
231
+ result = await this.#executeWithNativeTools(prompt, {
232
+ effectiveHistory, conversationId, source, execStartTime, budget, dimensionMeta,
233
+ // v3.0 新增
234
+ systemPromptOverride, allowedTools, disablePhaseRouter, temperatureOverride,
235
+ });
236
+ } else {
237
+ this.#logger.info(`[ChatAgent] 📝 using TEXT parsing mode (${this.#aiProvider.name})`);
238
+ result = await this.#executeWithTextParsing(prompt, {
239
+ effectiveHistory, conversationId, source, execStartTime,
240
+ });
241
+ }
242
+
243
+ // 持久化 assistant 回复
244
+ if (conversationId && this.#conversations) {
245
+ this.#conversations.append(conversationId, { role: 'assistant', content: result.reply });
246
+ this.#autoSummarize(conversationId).catch(err => {
247
+ this.#logger.debug('[ChatAgent] autoSummarize failed', { conversationId, error: err.message });
248
+ });
249
+ }
250
+
251
+ this.#extractMemory(prompt, result.reply);
252
+
253
+ // 附加 token 用量统计
254
+ result.tokenUsage = { ...this.#currentTokenUsage };
255
+
256
+ return { ...result, conversationId };
257
+ }
258
+
259
+ // ─── Native Tool Calling ReAct 循环 ──────────────────────
260
+
261
+ /**
262
+ * 原生结构化函数调用 ReAct 循环
263
+ *
264
+ * 三层架构:
265
+ * 1. ContextWindow — 消息生命周期 + 三级递进压缩
266
+ * 2. PhaseRouter — 阶段状态机 (EXPLORE→PRODUCE→SUMMARIZE)
267
+ * 3. ToolResultLimiter — 工具结果入口压缩 (动态配额)
268
+ *
269
+ * @param {string} prompt
270
+ * @param {object} opts
271
+ * @returns {Promise<{reply: string, toolCalls: Array, hasContext: boolean}>}
272
+ */
273
+ async #executeWithNativeTools(prompt, { effectiveHistory, conversationId, source, execStartTime, budget = DEFAULT_BUDGET, dimensionMeta,
274
+ // v3.0: Agent 分离新增选项
275
+ systemPromptOverride, allowedTools, disablePhaseRouter = false, temperatureOverride,
276
+ }) {
277
+ const isSystem = source === 'system';
278
+ const isSkillOnly = dimensionMeta?.outputType === 'skill';
279
+ const temperature = temperatureOverride ?? (isSystem ? 0.3 : 0.7);
280
+
281
+ // ── Layer 1: ContextWindow ──
282
+ // messages[0] = prompt(不可压缩),历史消息在前面
283
+ const ctx = new ContextWindow(isSystem ? 24000 : 16000);
284
+ for (const h of effectiveHistory) {
285
+ if (h.role === 'assistant') {
286
+ ctx.appendAssistantText(h.content);
287
+ } else {
288
+ ctx.appendUserMessage(h.content);
289
+ }
290
+ }
291
+ // prompt 作为最终 user message(Anthropic 最佳实践: 查询放在所有上下文之后)
292
+ ctx.appendUserMessage(prompt);
293
+
294
+ // ── P5: Pre-check — 首条 prompt 过大时预警 ──
295
+ const initialUsage = ctx.getTokenUsageRatio();
296
+ if (initialUsage > 0.7) {
297
+ this.#logger.warn(`[ChatAgent] ⚠ initial prompt already at ${(initialUsage * 100).toFixed(0)}% of token budget (${ctx.estimateTokens()}/${ctx.tokenBudget})`);
298
+ if (initialUsage > 0.9 && isSystem) {
299
+ // 仅 1 条消息时 compactIfNeeded 无法压缩(需 >4 条),
300
+ // 依赖 P0/P1 信号限制来控制 prompt 大小
301
+ this.#logger.warn(`[ChatAgent] ⚠ prompt exceeds 90% budget — P0/P1 signal limiting should have prevented this. Check PROMPT_LIMITS config.`);
302
+ }
303
+ }
304
+
305
+ // ── Layer 2: PhaseRouter (仅 system 源且未禁用时使用) ──
306
+ const phaseRouter = (isSystem && !disablePhaseRouter) ? new PhaseRouter(budget, isSkillOnly) : null;
307
+
308
+ // ── 系统提示词 (支持外部覆盖) ──
309
+ const baseSystemPrompt = systemPromptOverride || this.#buildNativeToolSystemPrompt(budget);
310
+
311
+ // Bootstrap 场景限制可用工具集 (支持外部覆盖)
312
+ const effectiveAllowedTools = allowedTools || (isSystem ? [
313
+ 'search_project_code', 'read_project_file',
314
+ 'submit_candidate', 'submit_with_check',
315
+ 'list_project_structure', 'get_file_summary', 'semantic_search_code',
316
+ // AST 结构化分析工具
317
+ 'get_project_overview', 'get_class_hierarchy', 'get_class_info',
318
+ 'get_protocol_info', 'get_method_overrides', 'get_category_map',
319
+ 'get_previous_analysis',
320
+ ] : null);
321
+ const toolSchemas = this.#toolRegistry.getToolSchemas(effectiveAllowedTools);
322
+
323
+ const toolCalls = [];
324
+ const maxIter = isSystem ? budget.maxIterations : MAX_ITERATIONS;
325
+ let consecutiveAiErrors = 0;
326
+ let consecutiveEmptyResponses = 0;
327
+ let iterationCount = 0; // v3: 独立迭代计数器
328
+ const submittedTitles = new Set(this.#globalSubmittedTitles);
329
+ const sharedState = {}; // P2.2: 跨工具调用共享状态(搜索计数器等)
330
+
331
+ // ── 主循环 ──
332
+ while (true) {
333
+ // PhaseRouter tick + 退出检查
334
+ if (phaseRouter) {
335
+ phaseRouter.tick();
336
+ if (phaseRouter.shouldExit()) {
337
+ this.#logger.info(`[ChatAgent] PhaseRouter exit: phase=${phaseRouter.phase}, iter=${phaseRouter.totalIterations}, submits=${phaseRouter.totalSubmits}`);
338
+ break;
339
+ }
340
+ } else if (isSystem && iterationCount >= maxIter) {
341
+ // v3: system 模式无 PhaseRouter 时用独立迭代计数器硬限制
342
+ this.#logger.info(`[ChatAgent] Iteration hard cap reached: ${iterationCount}/${maxIter}`);
343
+ break;
344
+ } else if (!isSystem && ctx.length > maxIter * 2 + 2) {
345
+ // 用户对话模式: 简单的消息数限制
346
+ break;
347
+ }
348
+ iterationCount++;
349
+
350
+ const iterStartTime = Date.now();
351
+ const currentIter = phaseRouter?.totalIterations || iterationCount;
352
+
353
+ // ── 动态 toolChoice (由 PhaseRouter 决定) ──
354
+ let currentChoice;
355
+ if (phaseRouter) {
356
+ currentChoice = phaseRouter.getToolChoice();
357
+ } else {
358
+ currentChoice = 'auto';
359
+ }
360
+
361
+ // ── 压缩检查 (每次 AI 调用前) ──
362
+ const compactResult = ctx.compactIfNeeded();
363
+ if (compactResult.level > 0) {
364
+ this.#logger.info(`[ChatAgent] context compacted: L${compactResult.level}, removed ${compactResult.removed} items`);
365
+ }
366
+
367
+ // ── 构建 systemPrompt (含阶段提示) ──
368
+ let systemPrompt = baseSystemPrompt;
369
+ if (phaseRouter) {
370
+ const hint = phaseRouter.getPhaseHint();
371
+ if (hint) {
372
+ systemPrompt += `\n\n## 当前状态\n${hint}`;
373
+ }
374
+ }
375
+
376
+ // ── AI 调用 ──
377
+ let aiResult;
378
+ try {
379
+ const messages = ctx.toMessages();
380
+ this.#logger.info(`[ChatAgent] 🔄 iteration ${currentIter}/${maxIter} — phase=${phaseRouter?.phase || 'user'}, ${messages.length} msgs, toolChoice=${currentChoice}, tokens~${ctx.estimateTokens()}`);
381
+
382
+ aiResult = await this.#aiProvider.chatWithTools(prompt, {
383
+ messages,
384
+ toolSchemas,
385
+ toolChoice: currentChoice,
386
+ systemPrompt,
387
+ temperature,
388
+ maxTokens: 8192,
389
+ });
390
+
391
+ const aiDuration = Date.now() - iterStartTime;
392
+
393
+ // 累计 token 用量
394
+ if (aiResult.usage) {
395
+ this.#currentTokenUsage.input += aiResult.usage.inputTokens || 0;
396
+ this.#currentTokenUsage.output += aiResult.usage.outputTokens || 0;
397
+ }
398
+
399
+ if (aiResult.functionCalls?.length > 0) {
400
+ this.#logger.info(`[ChatAgent] ✓ AI returned ${aiResult.functionCalls.length} function calls in ${aiDuration}ms: [${aiResult.functionCalls.map(fc => fc.name).join(', ')}]`);
401
+ } else {
402
+ const textPreview = (aiResult.text || '').substring(0, 120).replace(/\n/g, '↵');
403
+ this.#logger.info(`[ChatAgent] ✓ AI returned text in ${aiDuration}ms (${(aiResult.text || '').length} chars) — "${textPreview}…"`);
404
+ }
405
+ consecutiveAiErrors = 0;
406
+ } catch (aiErr) {
407
+ consecutiveAiErrors++;
408
+ this.#logger.warn(`[ChatAgent] AI call failed (attempt ${consecutiveAiErrors}): ${aiErr.message}`);
409
+
410
+ // 熔断器已打开时立即跳出 — 不要继续浪费重试、也避免失败计数加速累积
411
+ if (aiErr.code === 'CIRCUIT_OPEN') {
412
+ if (isSystem) {
413
+ this.#logger.warn(`[ChatAgent] 🛑 circuit breaker is OPEN — skipping to summary`);
414
+ break;
415
+ }
416
+ return {
417
+ reply: `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`,
418
+ toolCalls,
419
+ hasContext: toolCalls.length > 0,
420
+ };
421
+ }
422
+
423
+ if (consecutiveAiErrors >= 2) {
424
+ if (isSystem) {
425
+ this.#logger.warn(`[ChatAgent] 🛑 2 consecutive AI errors — resetting context, breaking to summary`);
426
+ ctx.resetToPromptOnly();
427
+ break;
428
+ }
429
+ return {
430
+ reply: `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`,
431
+ toolCalls,
432
+ hasContext: toolCalls.length > 0,
433
+ };
434
+ }
435
+ await new Promise(r => setTimeout(r, 2000));
436
+ continue;
437
+ }
438
+
439
+ // ── 处理 functionCalls ──
440
+ if (aiResult.functionCalls && aiResult.functionCalls.length > 0) {
441
+ // 限制单次工具调用数量(防上下文溢出)
442
+ const MAX_TOOL_CALLS_PER_ITER = 8;
443
+ let activeCalls = aiResult.functionCalls;
444
+ if (activeCalls.length > MAX_TOOL_CALLS_PER_ITER) {
445
+ this.#logger.warn(`[ChatAgent] ⚠ ${activeCalls.length} tool calls, capping to ${MAX_TOOL_CALLS_PER_ITER}`);
446
+ activeCalls = activeCalls.slice(0, MAX_TOOL_CALLS_PER_ITER);
447
+ }
448
+
449
+ // ContextWindow: 原子追加 assistant + tool results
450
+ ctx.appendAssistantWithToolCalls(aiResult.text || null, activeCalls);
451
+
452
+ let roundSubmitCount = 0;
453
+
454
+ for (const fc of activeCalls) {
455
+ const toolStartTime = Date.now();
456
+ this.#logger.info(`[ChatAgent] 🔧 ${fc.name}(${JSON.stringify(fc.args).substring(0, 100)})`);
457
+
458
+ let toolResult;
459
+ try {
460
+ toolResult = await this.#toolRegistry.execute(
461
+ fc.name,
462
+ fc.args,
463
+ this.#getToolContext({ _sessionToolCalls: toolCalls, _dimensionMeta: dimensionMeta, _submittedTitles: submittedTitles, _sharedState: sharedState }),
464
+ );
465
+ const toolDuration = Date.now() - toolStartTime;
466
+ const resultSize = typeof toolResult === 'string' ? toolResult.length : JSON.stringify(toolResult).length;
467
+ this.#logger.info(`[ChatAgent] 🔧 done: ${fc.name} → ${resultSize} chars in ${toolDuration}ms`);
468
+ } catch (toolErr) {
469
+ this.#logger.warn(`[ChatAgent] 🔧 FAILED: ${fc.name} — ${toolErr.message}`);
470
+ toolResult = { error: `tool "${fc.name}" failed: ${toolErr.message}` };
471
+ }
472
+
473
+ // 记录到全局 toolCalls
474
+ const summarized = this.#summarizeResult(toolResult);
475
+ toolCalls.push({ tool: fc.name, params: fc.args, result: summarized });
476
+
477
+ // ── Layer 3: ToolResultLimiter — 动态配额压缩 ──
478
+ const quota = ctx.getToolResultQuota();
479
+ let resultStr = limitToolResult(fc.name, toolResult, quota);
480
+
481
+ // ── 重复提交 / 维度范围校验 ──
482
+ if (fc.name === 'submit_candidate' || fc.name === 'submit_with_check') {
483
+ const title = fc.args?.title || fc.args?.category || '';
484
+ const isRejected = typeof toolResult === 'object' && toolResult?.status === 'rejected';
485
+ const isError = typeof toolResult === 'object' && (toolResult?.error || toolResult?.status === 'error');
486
+
487
+ if (isRejected) {
488
+ this.#logger.info(`[ChatAgent] 🚫 off-topic rejected: "${title}"`);
489
+ } else if (isError) {
490
+ // 候选创建失败(如 validation error)— 不加入 submittedTitles,允许 AI 重试
491
+ this.#logger.info(`[ChatAgent] ⚠ submit error: "${title}" — ${toolResult.error || 'unknown'}`);
492
+ } else if (submittedTitles.has(title)) {
493
+ resultStr = `⚠ 重复提交: "${title}" 已存在。`;
494
+ this.#logger.info(`[ChatAgent] 🔁 duplicate: "${title}"`);
495
+ } else {
496
+ submittedTitles.add(title);
497
+ this.#globalSubmittedTitles.add(title);
498
+ roundSubmitCount++;
499
+ }
500
+ }
501
+
502
+ // ContextWindow: 追加 tool result(与 assistant 保持原子性)
503
+ ctx.appendToolResult(fc.id, fc.name, resultStr);
504
+ }
505
+
506
+ // ── PhaseRouter 更新 ──
507
+ if (phaseRouter) {
508
+ const transition = phaseRouter.update({
509
+ functionCalls: activeCalls,
510
+ submitCount: roundSubmitCount,
511
+ isTextOnly: false,
512
+ });
513
+
514
+ // ── EXPLORE→PRODUCE 阶段过渡: 注入提交引导消息 ──
515
+ // 原生工具调用模式下,仅靠 systemPrompt 附加 hint 不够显著,
516
+ // 需要一条 user 消息明确告知 AI 切换到提交模式
517
+ if (transition.transitioned && transition.newPhase === 'PRODUCE') {
518
+ ctx.appendUserNudge(
519
+ '你已充分探索了项目代码,现在请开始调用 submit_candidate 工具来提交你发现的知识候选。不要再搜索,直接提交。'
520
+ );
521
+ this.#logger.info('[ChatAgent] 📝 injected PRODUCE transition nudge');
522
+ }
523
+
524
+ // ── EXPLORE→SUMMARIZE / PRODUCE→SUMMARIZE 阶段过渡: 注入 digest 引导 ──
525
+ // skill-only 维度从 EXPLORE 直接进入 SUMMARIZE (跳过 PRODUCE),
526
+ // 需要明确告知 AI 输出 dimensionDigest JSON
527
+ if (transition.transitioned && transition.newPhase === 'SUMMARIZE') {
528
+ const submitCount = toolCalls.filter(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check').length;
529
+ ctx.appendUserNudge(
530
+ `你已完成分析探索。请在回复中直接输出 dimensionDigest JSON(用 \`\`\`json 包裹),包含以下字段:\n\`\`\`json\n{"dimensionDigest":{"summary":"分析总结(100-200字)","candidateCount":${submitCount},"keyFindings":["关键发现"],"crossRefs":{},"gaps":["未覆盖方面"],"remainingTasks":[{"signal":"未处理的信号/主题","reason":"未完成原因(如:提交上限已达)","priority":"high|medium|low","searchHints":["建议搜索词"]}]}}\n\`\`\`\n> 如果所有信号都已覆盖,remainingTasks 留空数组 \`[]\`。如果有未来得及处理的信号,请在此标记,系统会在下次运行时续传。`
531
+ );
532
+ this.#logger.info('[ChatAgent] 📝 injected SUMMARIZE transition nudge');
533
+ }
534
+ }
535
+
536
+ continue;
537
+ }
538
+
539
+ // ── 文字回答 ──
540
+ // 空响应重试(Gemini 偶发)
541
+ if (!aiResult.text && isSystem && consecutiveEmptyResponses < 2) {
542
+ consecutiveEmptyResponses++;
543
+ this.#logger.warn(`[ChatAgent] ⚠ empty response from system source — retrying (${consecutiveEmptyResponses}/2)`);
544
+ await new Promise(r => setTimeout(r, 1500));
545
+ continue;
546
+ }
547
+ // 收到非空响应时重置空响应计数器
548
+ if (aiResult.text) {
549
+ consecutiveEmptyResponses = 0;
550
+ }
551
+
552
+ // PhaseRouter: 文字回答触发阶段转换
553
+ if (phaseRouter) {
554
+ const transition = phaseRouter.update({
555
+ functionCalls: null,
556
+ submitCount: 0,
557
+ isTextOnly: true,
558
+ });
559
+
560
+ // SUMMARIZE 阶段的文字回答 = 最终回答
561
+ if (phaseRouter.phase === 'SUMMARIZE') {
562
+ // 刚从 EXPLORE/PRODUCE 转入 SUMMARIZE 的文字回答可能不含 digest,
563
+ // 注入 nudge 让 AI 再输出一次 digest JSON
564
+ if (transition.transitioned) {
565
+ ctx.appendAssistantText(aiResult.text || '');
566
+ const submitCount = toolCalls.filter(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check').length;
567
+ ctx.appendUserNudge(
568
+ `请在回复中直接输出 dimensionDigest JSON 总结(用 \`\`\`json 包裹):\n\`\`\`json\n{"dimensionDigest":{"summary":"分析总结","candidateCount":${submitCount},"keyFindings":["发现"],"crossRefs":{},"gaps":["缺口"],"remainingTasks":[{"signal":"未处理的信号","reason":"原因","priority":"high","searchHints":["搜索词"]}]}}\n\`\`\`\n> remainingTasks: 记录未来得及处理的信号。已全部覆盖则留空 \`[]\`。`
569
+ );
570
+ this.#logger.info('[ChatAgent] 📝 injected SUMMARIZE nudge (text-triggered transition)');
571
+ continue;
572
+ }
573
+ // 已在 SUMMARIZE 阶段 — 这就是最终回答
574
+ const reply = this.#cleanFinalAnswer(aiResult.text || '');
575
+ const totalDuration = Date.now() - execStartTime;
576
+ this.#logger.info(`[ChatAgent] ✅ final answer — ${reply.length} chars, ${phaseRouter.totalIterations} iters, ${toolCalls.length} tool calls, ${totalDuration}ms`);
577
+ return { reply, toolCalls, hasContext: toolCalls.length > 0 };
578
+ }
579
+
580
+ if (!transition.transitioned) {
581
+ // 在 EXPLORE/PRODUCE 阶段收到文本但未转阶段 — 可能是 AI 的中间分析
582
+ // 注入提交引导并继续循环,而非立即返回
583
+ if (phaseRouter.phase === 'EXPLORE' || phaseRouter.phase === 'PRODUCE') {
584
+ ctx.appendAssistantText(aiResult.text || '');
585
+ if (phaseRouter.phase === 'PRODUCE') {
586
+ ctx.appendUserNudge(
587
+ '你的分析很好。请继续调用 submit_candidate 提交你发现的知识候选,每个值得记录的模式/实践都应该提交。'
588
+ );
589
+ this.#logger.info('[ChatAgent] 📝 injected submit nudge (text in PRODUCE, not transitioning)');
590
+ }
591
+ continue;
592
+ }
593
+ // 非生产阶段的未转换文本 = 最终回答
594
+ const reply = this.#cleanFinalAnswer(aiResult.text || '');
595
+ const totalDuration = Date.now() - execStartTime;
596
+ this.#logger.info(`[ChatAgent] ✅ final answer — ${reply.length} chars, ${phaseRouter.totalIterations} iters, ${toolCalls.length} tool calls, ${totalDuration}ms`);
597
+ return { reply, toolCalls, hasContext: toolCalls.length > 0 };
598
+ }
599
+
600
+ // 其他阶段的文字回答 → 继续循环(PhaseRouter 已自动转换阶段)
601
+ ctx.appendAssistantText(aiResult.text || '');
602
+ continue;
603
+ }
604
+
605
+ // 用户对话: 文字回答即最终回答
606
+ const reply = this.#cleanFinalAnswer(aiResult.text || '');
607
+ const totalDuration = Date.now() - execStartTime;
608
+ this.#logger.info(`[ChatAgent] ✅ final answer — ${reply.length} chars, ${toolCalls.length} tool calls, ${totalDuration}ms`);
609
+ return { reply, toolCalls, hasContext: toolCalls.length > 0 };
610
+ }
611
+
612
+ // ── 循环退出: 产出 dimensionDigest 总结 ──
613
+ return this.#produceForcedSummary({
614
+ source, toolCalls, toolSchemas, ctx, phaseRouter, execStartTime,
615
+ });
616
+ }
617
+
618
+ /**
619
+ * 强制退出后的摘要生成 — 独立方法,避免主循环代码膨胀
620
+ * @private
621
+ */
622
+ async #produceForcedSummary({ source, toolCalls, toolSchemas, ctx, phaseRouter, execStartTime }) {
623
+ const iterations = phaseRouter?.totalIterations || 0;
624
+ this.#logger.info(`[ChatAgent] ⚠ producing forced summary (${iterations} iters, ${toolCalls.length} calls)`);
625
+
626
+ const candidateCount = toolCalls.filter(tc =>
627
+ tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check'
628
+ ).length;
629
+
630
+ let finalReply;
631
+
632
+ // 如果熔断器已打开,跳过 AI 调用直接合成 digest(避免无用的失败 + 计数累积)
633
+ const isCircuitOpen = this.#aiProvider._circuitState === 'OPEN';
634
+ if (isCircuitOpen) {
635
+ this.#logger.warn(`[ChatAgent] circuit breaker is OPEN — skipping AI summary, using synthetic digest`);
636
+ }
637
+
638
+ try {
639
+ if (isCircuitOpen) throw new Error('circuit open — skip to synthetic digest');
640
+
641
+ const submitSummary = toolCalls
642
+ .filter(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check')
643
+ .map((tc, i) => `${i + 1}. ${tc.params?.title || tc.params?.category || 'untitled'}`)
644
+ .join('\n');
645
+
646
+ const summaryPrompt = source === 'system'
647
+ ? `你已完成 ${iterations} 轮工具调用(共 ${toolCalls.length} 次),提交了 ${candidateCount} 个候选。
648
+ ${submitSummary ? `已提交候选:\n${submitSummary}\n` : ''}
649
+ **必须**输出 dimensionDigest JSON(用 \`\`\`json 包裹):
650
+ \`\`\`json
651
+ {
652
+ "dimensionDigest": {
653
+ "summary": "本维度分析总结",
654
+ "candidateCount": ${candidateCount},
655
+ "keyFindings": ["发现1", "发现2"],
656
+ "crossRefs": {},
657
+ "gaps": ["未覆盖方面"],
658
+ "remainingTasks": [
659
+ { "signal": "未处理信号名", "reason": "达到提交上限/时间限制", "priority": "high", "searchHints": ["搜索词"] }
660
+ ]
661
+ }
662
+ }
663
+ \`\`\`
664
+ > remainingTasks: 列出本次未来得及处理的信号/主题。已全部覆盖则留空 \`[]\`。`
665
+ : `Completed ${iterations} iterations with ${toolCalls.length} tool calls. Please summarize.`;
666
+
667
+ // 用空 messages 避免累积上下文导致 400
668
+ const summaryResult = await this.#aiProvider.chatWithTools(
669
+ summaryPrompt,
670
+ {
671
+ messages: [],
672
+ toolSchemas,
673
+ toolChoice: 'none',
674
+ systemPrompt: '直接输出 dimensionDigest JSON 总结,不要调用工具。',
675
+ temperature: 0.3,
676
+ maxTokens: 8192,
677
+ },
678
+ );
679
+ // 累计 token 用量
680
+ if (summaryResult.usage) {
681
+ this.#currentTokenUsage.input += summaryResult.usage.inputTokens || 0;
682
+ this.#currentTokenUsage.output += summaryResult.usage.outputTokens || 0;
683
+ }
684
+ finalReply = this.#cleanFinalAnswer(summaryResult.text || '');
685
+ } catch (err) {
686
+ this.#logger.warn(`[ChatAgent] forced summary AI call failed: ${err.message}`);
687
+ // 合成 digest 兜底
688
+ const titles = toolCalls
689
+ .filter(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check')
690
+ .map(tc => tc.params?.title || 'untitled');
691
+ finalReply = `\`\`\`json
692
+ {
693
+ "dimensionDigest": {
694
+ "summary": "通过 ${toolCalls.length} 次工具调用分析了项目代码,提交了 ${candidateCount} 个候选。",
695
+ "candidateCount": ${candidateCount},
696
+ "keyFindings": ${JSON.stringify(titles.slice(0, 5))},
697
+ "crossRefs": {},
698
+ "gaps": ["AI 服务异常,部分分析未完成"]
699
+ }
700
+ }
701
+ \`\`\``;
702
+ }
703
+
704
+ const totalDuration = Date.now() - execStartTime;
705
+ this.#logger.info(`[ChatAgent] ✅ forced summary — ${finalReply.length} chars, ${totalDuration}ms total`);
706
+ return { reply: finalReply, toolCalls, hasContext: toolCalls.length > 0 };
707
+ }
708
+
709
+ // ─── Text Parsing ReAct 循环 (legacy) ─────────────────
710
+
711
+ /**
712
+ * 文本解析 ReAct 循环 — 传统模式
713
+ * 适用于不支持原生函数调用的 Provider (DeepSeek, OpenAI 兼容等)
714
+ * AI 输出文本 → #parseActions() 正则解析 → 执行工具 → 循环
715
+ */
716
+ async #executeWithTextParsing(prompt, { effectiveHistory, conversationId, source, execStartTime }) {
107
717
  const toolSchemas = this.#toolRegistry.getToolSchemas();
108
718
  const systemPrompt = this.#buildSystemPrompt(toolSchemas);
109
719
 
110
- // 首次 LLM 调用
111
720
  const messages = [
112
721
  ...effectiveHistory,
113
722
  { role: 'user', content: prompt },
@@ -116,96 +725,138 @@ export class ChatAgent {
116
725
  const toolCalls = [];
117
726
  let iterations = 0;
118
727
  let currentPrompt = prompt;
119
-
120
728
  let consecutiveAiErrors = 0;
729
+ const maxIter = source === 'system' ? MAX_ITERATIONS_SYSTEM : MAX_ITERATIONS;
121
730
 
122
- while (iterations < MAX_ITERATIONS) {
731
+ while (iterations < maxIter) {
123
732
  iterations++;
733
+ const iterStartTime = Date.now();
124
734
 
125
735
  let response;
126
736
  try {
737
+ this.#logger.info(`[ChatAgent] 🔄 text iteration ${iterations}/${maxIter} — calling AI (${messages.length} messages)`);
127
738
  response = await this.#aiProvider.chat(currentPrompt, {
128
- history: messages.slice(0, -1), // 不含最新 user prompt
739
+ history: messages.slice(0, -1),
129
740
  systemPrompt,
130
741
  });
742
+ const aiDuration = Date.now() - iterStartTime;
743
+ const responsePreview = (response || '').substring(0, 120).replace(/\n/g, '↵');
744
+ this.#logger.info(`[ChatAgent] ✓ AI responded in ${aiDuration}ms (${(response || '').length} chars) — "${responsePreview}…"`);
131
745
  consecutiveAiErrors = 0;
132
746
  } catch (aiErr) {
133
747
  consecutiveAiErrors++;
134
748
  this.#logger.warn(`[ChatAgent] AI call failed (attempt ${consecutiveAiErrors}): ${aiErr.message}`);
135
749
 
136
- // 连续 2 次失败则降级返回错误提示
750
+ // 熔断器已打开 立即返回
751
+ if (aiErr.code === 'CIRCUIT_OPEN') {
752
+ return {
753
+ reply: `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`,
754
+ toolCalls,
755
+ hasContext: toolCalls.length > 0,
756
+ };
757
+ }
758
+
137
759
  if (consecutiveAiErrors >= 2) {
138
- const errorReply = `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`;
139
- if (conversationId && this.#conversations) {
140
- this.#conversations.append(conversationId, { role: 'assistant', content: errorReply });
141
- }
142
- return { reply: errorReply, toolCalls, hasContext: toolCalls.length > 0, conversationId };
760
+ return {
761
+ reply: `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`,
762
+ toolCalls,
763
+ hasContext: toolCalls.length > 0,
764
+ };
143
765
  }
144
- // 首次失败:等待后重试本轮
145
766
  await new Promise(r => setTimeout(r, 2000));
146
767
  continue;
147
768
  }
148
769
 
149
- // 尝试解析 Action
150
- const action = this.#parseAction(response);
770
+ const actions = this.#parseActions(response);
771
+
772
+ if (!actions) {
773
+ // ── 系统调用自动续跑 ──
774
+ const hasSubmits = toolCalls.some(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check');
775
+ if (source === 'system' && iterations < maxIter && !hasSubmits) {
776
+ if (this.#looksLikeIncompleteStep(response)) {
777
+ this.#logger.info(`[ChatAgent] 🔄 detected planning-only response at iteration ${iterations}, injecting continuation prompt`);
778
+ messages.push({ role: 'assistant', content: response });
779
+ currentPrompt = SYSTEM_CONTINUATION_PROMPT;
780
+ messages.push({ role: 'user', content: currentPrompt });
781
+ continue;
782
+ }
783
+ if (toolCalls.length > 0) {
784
+ this.#logger.info(`[ChatAgent] 🔄 detected analysis-without-submission at iteration ${iterations} (${toolCalls.length} tool calls, 0 submits), injecting submission prompt`);
785
+ messages.push({ role: 'assistant', content: response });
786
+ currentPrompt = SYSTEM_SUBMIT_PROMPT;
787
+ messages.push({ role: 'user', content: currentPrompt });
788
+ continue;
789
+ }
790
+ }
151
791
 
152
- if (!action) {
153
- // 没有 Action → 最终回答
154
792
  const reply = this.#cleanFinalAnswer(response);
155
- this.#extractMemory(prompt, reply);
156
-
157
- // 持久化 assistant 回复
158
- if (conversationId && this.#conversations) {
159
- this.#conversations.append(conversationId, { role: 'assistant', content: reply });
160
- // 消息过多时自动压缩
161
- this.#autoSummarize(conversationId).catch(() => {});
162
- }
793
+ const totalDuration = Date.now() - execStartTime;
794
+ this.#logger.info(`[ChatAgent] ✅ text final answer — ${reply.length} chars, ${iterations} iterations, ${toolCalls.length} tool calls, ${totalDuration}ms total`);
163
795
 
164
- return { reply, toolCalls, hasContext: toolCalls.length > 0, conversationId };
796
+ return { reply, toolCalls, hasContext: toolCalls.length > 0 };
165
797
  }
166
798
 
167
799
  // 执行工具
168
- this.#logger.info('ChatAgent tool call', {
169
- tool: action.tool,
170
- iteration: iterations,
171
- });
172
-
173
- let toolResult;
174
- try {
175
- toolResult = await this.#toolRegistry.execute(
176
- action.tool,
177
- action.params,
178
- this.#getToolContext(),
179
- );
180
- } catch (toolErr) {
181
- this.#logger.warn(`[ChatAgent] Tool "${action.tool}" failed: ${toolErr.message}`);
182
- // 将错误反馈给 LLM,让它尝试其他方法
183
- toolResult = `Error: tool "${action.tool}" failed — ${toolErr.message}. Try a different approach or provide your answer based on available information.`;
800
+ const isBatch = actions.length > 1;
801
+ if (isBatch) {
802
+ this.#logger.info(`[ChatAgent] 📦 batch tool call: ${actions.length} actions [${actions.map(a => a.tool).join(', ')}]`, { iteration: iterations });
184
803
  }
185
804
 
186
- toolCalls.push({
187
- tool: action.tool,
188
- params: action.params,
189
- result: this.#summarizeResult(toolResult),
190
- });
805
+ const batchResults = [];
806
+ for (const action of actions) {
807
+ this.#logger.info(`[ChatAgent] 🔧 tool call: ${action.tool}(${JSON.stringify(action.params).substring(0, 100)})`, {
808
+ iteration: iterations,
809
+ batch: isBatch,
810
+ });
811
+
812
+ let toolResult;
813
+ const toolStartTime = Date.now();
814
+ try {
815
+ toolResult = await this.#toolRegistry.execute(
816
+ action.tool,
817
+ action.params,
818
+ this.#getToolContext({ _sessionToolCalls: toolCalls }),
819
+ );
820
+ const toolDuration = Date.now() - toolStartTime;
821
+ const resultSize = typeof toolResult === 'string' ? toolResult.length : JSON.stringify(toolResult).length;
822
+ this.#logger.info(`[ChatAgent] 🔧 tool done: ${action.tool} → ${resultSize} chars in ${toolDuration}ms`);
823
+ } catch (toolErr) {
824
+ this.#logger.warn(`[ChatAgent] 🔧 tool FAILED: ${action.tool} — ${toolErr.message} (${Date.now() - toolStartTime}ms)`);
825
+ toolResult = `Error: tool "${action.tool}" failed — ${toolErr.message}. Try a different approach or provide your answer based on available information.`;
826
+ }
827
+
828
+ const summarized = this.#summarizeResult(toolResult);
829
+ toolCalls.push({
830
+ tool: action.tool,
831
+ params: action.params,
832
+ result: summarized,
833
+ });
834
+ batchResults.push({ tool: action.tool, result: toolResult });
835
+ }
191
836
 
192
837
  // 将工具结果注入为下一轮 prompt
193
- const observation = typeof toolResult === 'string'
194
- ? toolResult
195
- : JSON.stringify(toolResult, null, 2);
838
+ let observation;
839
+ if (batchResults.length === 1) {
840
+ const r = batchResults[0];
841
+ const obsText = typeof r.result === 'string' ? r.result : JSON.stringify(r.result, null, 2);
842
+ observation = `Observation from tool "${r.tool}":\n${this.#truncate(obsText, 4000)}`;
843
+ } else {
844
+ observation = `Batch observation (${batchResults.length} tools):\n` +
845
+ batchResults.map((r, i) => {
846
+ const obsText = typeof r.result === 'string' ? r.result : JSON.stringify(r.result, null, 2);
847
+ return `[${i + 1}] ${r.tool}: ${this.#truncate(obsText, 2000)}`;
848
+ }).join('\n\n');
849
+ }
196
850
 
197
- currentPrompt = `Observation from tool "${action.tool}":\n${this.#truncate(observation, 4000)}\n\nBased on the above observation, continue reasoning about the user's question: "${prompt}".\nIf you have enough information, provide your final answer directly (without Action block). Otherwise, call another tool.`;
851
+ currentPrompt = `${observation}\n\nBased on the above observation, continue reasoning about the user's question: "${prompt}".\nIf you have enough information, provide your final answer directly (without Action block). Otherwise, call another tool.`;
198
852
 
199
- // 追加到消息历史中以保持上下文
200
853
  messages.push({ role: 'assistant', content: response });
201
854
  messages.push({ role: 'user', content: currentPrompt });
202
855
 
203
- // ── Context Window 自动压缩(Cline AutoCondense 模式)──
204
- // 每轮 ReAct 后检测消息总 token,超过预算时压缩中段消息
205
856
  this.#condenseIfNeeded(messages);
206
857
  }
207
858
 
208
- // 达到最大迭代次数,要求 LLM 总结
859
+ // 达到最大迭代次数
209
860
  const summaryPrompt = `You have used ${iterations} tool calls. Summarize what you found and answer the user's original question: "${prompt}"`;
210
861
  let finalResponse;
211
862
  try {
@@ -215,27 +866,13 @@ export class ChatAgent {
215
866
  });
216
867
  } catch (err) {
217
868
  this.#logger.warn(`[ChatAgent] Final summary AI call failed: ${err.message}`);
218
- // 降级:用工具调用结果拼一个简单回复
219
869
  finalResponse = `根据 ${toolCalls.length} 次工具调用的结果,以下是收集到的信息:\n\n` +
220
870
  toolCalls.map(tc => `• ${tc.tool}: ${typeof tc.result === 'string' ? tc.result.substring(0, 200) : JSON.stringify(tc.result).substring(0, 200)}`).join('\n') +
221
871
  '\n\n(注:AI 总结服务暂时不可用,上述为原始工具输出摘要)';
222
872
  }
223
873
 
224
874
  const finalReply = this.#cleanFinalAnswer(finalResponse);
225
- this.#extractMemory(prompt, finalReply);
226
-
227
- // 持久化 assistant 回复
228
- if (conversationId && this.#conversations) {
229
- this.#conversations.append(conversationId, { role: 'assistant', content: finalReply });
230
- this.#autoSummarize(conversationId).catch(() => {});
231
- }
232
-
233
- return {
234
- reply: finalReply,
235
- toolCalls,
236
- hasContext: toolCalls.length > 0,
237
- conversationId,
238
- };
875
+ return { reply: finalReply, toolCalls, hasContext: toolCalls.length > 0 };
239
876
  }
240
877
 
241
878
  /**
@@ -588,20 +1225,16 @@ ${code.substring(0, 3000)}
588
1225
  /**
589
1226
  * 注册内置 DAG 管线
590
1227
  *
591
- * 设计原则: 项目内 AI 都走 ChatAgent + tool,DAG 编排 AI 步骤。
592
- * bootstrapKnowledge() 只做启发式 Phase 1-5,不调 AI。
593
- * AI 增强步骤由 ChatAgent DAG 编排:
594
- *
595
- * bootstrap_full_pipeline:
596
- * Phase 0: bootstrap(SPM 扫描 + Skill 增强维度 + 候选创建,纯启发式)
597
- * Phase 1: enrich(AI 结构补齐,依赖 bootstrap 产出的候选 ID)
598
- * Phase 1: loadSkill(并行加载语言参考 Skill,用于润色提示)
599
- * Phase 2: refine(AI 内容润色,依赖 enrich + loadSkill)
1228
+ * v6 变更:
1229
+ * - 移除旧的 4 步 DAG (bootstrap enrich loadSkill → refine)
1230
+ * - 冷启动 AI 增强现在通过 orchestrator.js 中的 ChatAgent per-dimension production 完成
1231
+ * - 保留简化版 bootstrap_full_pipeline: 只做 Phase 1-4 启发式
1232
+ * (Phase 5 ChatAgent 生产由 orchestrator.js 管理,不再走 DAG 编排)
600
1233
  */
601
1234
  #registerBuiltinPipelines() {
602
- const hasAI = !!this.#aiProvider;
603
-
604
- // ── bootstrap_full_pipeline (DAG) ──────────────────────
1235
+ // ── bootstrap_full_pipeline (v6 简化版) ──────────────────
1236
+ // 只做启发式 Phase 1-5.5 (含 ChatAgent per-dimension production)
1237
+ // 不再需要 enrich/refine 后置步骤
605
1238
  this.registerPipeline(new TaskPipeline('bootstrap_full_pipeline', [
606
1239
  {
607
1240
  name: 'bootstrap',
@@ -613,69 +1246,6 @@ ${code.substring(0, 3000)}
613
1246
  loadSkills: true,
614
1247
  },
615
1248
  },
616
- {
617
- name: 'enrich',
618
- tool: 'enrich_candidate',
619
- dependsOn: ['bootstrap'],
620
- params: {
621
- candidateIds: (ctx) => {
622
- const bc = ctx._results.bootstrap?.bootstrapCandidates;
623
- return bc?.ids || bc?.candidateIds || [];
624
- },
625
- },
626
- when: (ctx) => {
627
- const ids = ctx._results.bootstrap?.bootstrapCandidates?.ids
628
- || ctx._results.bootstrap?.bootstrapCandidates?.candidateIds;
629
- return Array.isArray(ids) && ids.length > 0;
630
- },
631
- errorStrategy: 'continue',
632
- },
633
- {
634
- name: 'loadSkill',
635
- tool: 'load_skill',
636
- dependsOn: ['bootstrap'],
637
- params: {
638
- skillName: (ctx) => {
639
- const loaded = ctx._results.bootstrap?.skillsLoaded || [];
640
- return loaded.find(s => s.startsWith('autosnippet-reference-')) || 'autosnippet-coldstart';
641
- },
642
- },
643
- when: (ctx) => {
644
- const autoRefine = ctx._inputs.autoRefine;
645
- return autoRefine !== false && hasAI;
646
- },
647
- errorStrategy: 'continue',
648
- },
649
- {
650
- name: 'refine',
651
- tool: 'refine_bootstrap_candidates',
652
- dependsOn: ['enrich', 'loadSkill'],
653
- params: {
654
- userPrompt: (ctx) => {
655
- const parts = [];
656
-
657
- // Skill 业界标准参考
658
- const skillContent = ctx._results.loadSkill?.content;
659
- if (skillContent) {
660
- parts.push(`请参考以下业界最佳实践标准润色候选,确保 summary 精准、tags 丰富、confidence 合理:\n${skillContent.substring(0, 3000)}`);
661
- }
662
-
663
- // AST 代码结构分析 — 帮助 AI 理解继承体系和设计模式
664
- const astCtx = ctx._results.bootstrap?.astContext;
665
- if (astCtx) {
666
- parts.push(`\n# 项目代码结构分析 (Tree-sitter AST)\n以下是项目的 AST 分析结果,请在润色时参考类继承关系、设计模式和代码质量指标:\n${astCtx.substring(0, 2000)}`);
667
- }
668
-
669
- return parts.length > 0 ? parts.join('\n\n') : ctx._inputs.refinePrompt;
670
- },
671
- },
672
- when: (ctx) => {
673
- const autoRefine = ctx._inputs.autoRefine;
674
- const created = ctx._results.bootstrap?.bootstrapCandidates?.created || 0;
675
- return autoRefine !== false && created > 0 && hasAI;
676
- },
677
- errorStrategy: 'continue',
678
- },
679
1249
  ]));
680
1250
  }
681
1251
 
@@ -687,17 +1257,20 @@ ${code.substring(0, 3000)}
687
1257
  * 工具注入策略(Lazy Tool Schema — 类似 Cline .clinerules 按需加载):
688
1258
  * - 首屏只注入工具名 + 一行描述(compact list)
689
1259
  * - 系统提示词中告知 LLM 可通过 get_tool_details 获取完整参数
690
- * - 少量核心工具(search_knowledge, submit_with_check, analyze_code,
691
- * bootstrap_knowledge, load_skill, suggest_skills)直接展开完整 schema
1260
+ * - 少量核心工具(search_project_code, read_project_file, search_knowledge,
1261
+ * submit_with_check, analyze_code, bootstrap_knowledge, load_skill,
1262
+ * suggest_skills)直接展开完整 schema
692
1263
  *
693
- * 效果: 39 个工具的 prompt 从 ~5000 tokens 降到 ~1500 tokens
1264
+ * 效果: 44 个工具的 prompt 从 ~5000 tokens 降到 ~1500 tokens
694
1265
  */
695
1266
  #buildSystemPrompt(toolSchemas) {
696
1267
  // 核心工具 — 使用最频繁,直接展示完整 schema
697
1268
  const coreTools = new Set([
698
- 'search_knowledge', 'submit_with_check', 'analyze_code',
1269
+ 'search_project_code', 'read_project_file',
1270
+ 'search_knowledge', 'submit_candidate', 'submit_with_check', 'analyze_code',
699
1271
  'bootstrap_knowledge', 'load_skill', 'suggest_skills',
700
1272
  'create_skill', 'knowledge_overview', 'get_tool_details',
1273
+ 'plan_task', 'review_my_output',
701
1274
  ]);
702
1275
 
703
1276
  const compactDescriptions = [];
@@ -746,7 +1319,15 @@ ${skillSection}
746
1319
  {"tool": "tool_name", "params": {"key": "value"}}
747
1320
  \`\`\`
748
1321
 
749
- 3. 每次只调用一个工具。
1322
+ 3. 当需要连续调用多个**同类工具**(如批量提交候选)时,可使用批量格式:
1323
+
1324
+ \`\`\`batch_actions
1325
+ [
1326
+ {"tool": "submit_candidate", "params": {"title": "...", "code": "..."}},
1327
+ {"tool": "submit_candidate", "params": {"title": "...", "code": "..."}}
1328
+ ]
1329
+ \`\`\`
1330
+
750
1331
  4. 如果不需要工具就能回答,直接回答,不要输出 action 块。
751
1332
  5. 回答时使用用户的语言(中文/英文)。
752
1333
  6. 回答要简洁、有依据(引用工具返回的数据)。
@@ -764,36 +1345,304 @@ ${skillSection}
764
1345
  }
765
1346
 
766
1347
  /**
767
- * 从 LLM 响应中解析 Action 块
768
- * 格式: ```action\n{"tool":"...", "params":{...}}\n```
1348
+ * 构建原生函数调用模式的系统提示词
1349
+ *
1350
+ * 设计原则:
1351
+ * - 精简: bootstrap 模式不注入 SOUL.md 人格(节省 ~500 token)
1352
+ * - 分层: 静态指令放 systemPrompt,动态上下文放 user prompt
1353
+ * - 控制通过 PhaseRouter 状态机实现,不通过追加 user 消息
1354
+ * - 工具描述已通过 functionDeclarations 传递,不重复
1355
+ */
1356
+ #buildNativeToolSystemPrompt(budget = DEFAULT_BUDGET) {
1357
+ // 用户对话模式: 完整提示词(含 SOUL、Memory、项目概况)
1358
+ if (this.#currentSource !== 'system') {
1359
+ let soulSection = '';
1360
+ try {
1361
+ if (fs.existsSync(SOUL_PATH)) {
1362
+ soulSection = '\n' + fs.readFileSync(SOUL_PATH, 'utf-8').trim() + '\n';
1363
+ }
1364
+ } catch { /* SOUL.md not available */ }
1365
+
1366
+ return `${soulSection}
1367
+ 你是 AutoSnippet 项目的统一 AI 中心。项目内所有 AI 推理和分析都通过你执行。
1368
+ ${this.#projectBriefingCache}${this.#memory?.toPromptSection({ source: 'user' }) || ''}
1369
+
1370
+ ## 使用规则
1371
+ 1. 当需要查询数据时,直接调用相应工具。
1372
+ 2. 工具参数严格按照工具声明中的 schema 传递。
1373
+ 3. 对于代码分析任务,先 search_project_code 搜索,再 read_project_file 读取。
1374
+ 4. 回答时使用用户的语言(中文/英文)。
1375
+ 5. 当工具返回错误时,尝试不同参数或方法。`;
1376
+ }
1377
+
1378
+ // Bootstrap 系统模式: LLM 以领域大脑的能力处理任务
1379
+ return `你以「领域大脑」的能力来处理任务 — 你对软件工程领域拥有深厚的专家知识。
1380
+ 你将分析一个真实项目,自主发现其中有价值的代码知识。
1381
+ ${this.#projectBriefingCache}
1382
+
1383
+ ## 你的能力定位
1384
+ 你具备深度技术洞察力,能够理解代码背后的设计意图。
1385
+ 你知道什么知识对开发团队最有价值 — 不是显而易见的样板代码,
1386
+ 而是体现项目独有设计决策、架构模式和工程智慧的知识。
1387
+
1388
+ ## 你的工作方式
1389
+ 1. **全局感知** → list_project_structure 了解项目结构
1390
+ 2. **定向探索** → get_file_summary 快速了解文件角色
1391
+ 3. **深入研读** → search_project_code / read_project_file 获取真实代码
1392
+ 4. **语义发现** → semantic_search_code 在知识库查找相关知识
1393
+ 5. **知识产出** → submit_candidate 提交有价值的发现
1394
+
1395
+ ## 「项目特写」= 基本用法 + 项目特征融合
1396
+ submit_candidate 的 code 字段必须是「项目特写」— 将技术的基本用法与本项目的特征融合为一体:
1397
+ 1. **项目选择了什么**: 采用了哪种写法/模式/约定
1398
+ 2. **为什么这样选**: 统计数据(N 个文件、占比 M%)
1399
+ 3. **项目禁止什么**: 被放弃的写法、反模式、显式禁用标记
1400
+ 4. **新代码怎么写**: 可直接复制使用的代码模板
1401
+
1402
+ ## 核心原则
1403
+ - 代码必须真实,来自工具返回,不可编造
1404
+ - 引用具体类名、方法名、数字,禁止「本模块」「该文件」等泛化描述
1405
+ - 质量优先于数量,证据不足宁可不提交
1406
+ - 高效利用步数 (≤${budget.maxIterations} 轮)`;
1407
+ }
1408
+
1409
+ /**
1410
+ * 从 LLM 响应中解析 Action 块(单条)
1411
+ *
1412
+ * 兼容多家 AI 服务商的工具调用格式:
1413
+ * 1. ```action {"tool":"...", "params":{...}} ``` — 标准格式
1414
+ * 2. ```tool_code tool_name(key="value") ``` — Gemini 常用
1415
+ * 3. ```python / ```javascript 围栏内函数调用 — 各家偶发
1416
+ * 4. Action: tool_name / Action Input: {...} — ReAct (GPT/DeepSeek)
1417
+ * 5. <tool_call>{"name":"...", "arguments":{...}}</tool_call> — 训练遗留 XML
1418
+ * 6. ```json {"name":"...", "arguments":{...}} ``` — GPT function_call 文本化
1419
+ * 7. {"tool":"...", "params":{...}} 裸 JSON — 通用降级
1420
+ * 8. response 末尾裸函数调用 tool_name(key="value") — 通用降级
769
1421
  */
770
1422
  #parseAction(response) {
771
1423
  if (!response) return null;
772
1424
 
773
- // 尝试匹配 ```action ... ``` 代码块
1425
+ // ── 1. 标准 ```action {...} ``` ──
774
1426
  const blockMatch = response.match(/```action\s*\n?([\s\S]*?)```/);
775
1427
  if (blockMatch) {
776
- try {
777
- const parsed = JSON.parse(blockMatch[1].trim());
778
- if (parsed.tool && this.#toolRegistry.has(parsed.tool)) {
779
- return { tool: parsed.tool, params: parsed.params || {} };
1428
+ const parsed = this.#tryParseToolJson(blockMatch[1].trim());
1429
+ if (parsed) return parsed;
1430
+ }
1431
+
1432
+ // ── 2. ```tool_code fn(k=v) ``` (Gemini 常用) ──
1433
+ const toolCodeMatch = response.match(/```tool_code\s*\n?([\s\S]*?)```/);
1434
+ if (toolCodeMatch) {
1435
+ const parsed = this.#parseToolCodeBlock(toolCodeMatch[1].trim());
1436
+ if (parsed) return parsed;
1437
+ }
1438
+
1439
+ // ── 3. ```python / ```javascript / ```js 围栏内函数调用 ──
1440
+ const langFenceMatch = response.match(/```(?:python|javascript|js|typescript|ts)\s*\n?([\s\S]*?)```/);
1441
+ if (langFenceMatch) {
1442
+ const inner = langFenceMatch[1].trim();
1443
+ const parsed = this.#parseToolCodeBlock(inner);
1444
+ if (parsed) return parsed;
1445
+ // JS 对象字面量: tool_name({key: "value"})
1446
+ const jsObjMatch = inner.match(/^(\w+)\(\s*(\{[\s\S]*\})\s*\)$/s);
1447
+ if (jsObjMatch) {
1448
+ const toolName = jsObjMatch[1];
1449
+ if (this.#toolRegistry.has(toolName)) {
1450
+ try {
1451
+ let params;
1452
+ try { params = JSON.parse(jsObjMatch[2]); } catch {
1453
+ const normalized = jsObjMatch[2]
1454
+ .replace(/,\s*([}\]])/g, '$1')
1455
+ .replace(/'/g, '"')
1456
+ .replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":');
1457
+ params = JSON.parse(normalized);
1458
+ }
1459
+ return { tool: toolName, params };
1460
+ } catch { /* parse failed */ }
780
1461
  }
781
- } catch { /* parse failed */ }
1462
+ }
782
1463
  }
783
1464
 
784
- // 降级: 尝试匹配 JSON-like 结构 {"tool": "...", "params": {...}}
785
- const jsonMatch = response.match(/\{\s*"tool"\s*:\s*"([^"]+)"\s*,\s*"params"\s*:\s*(\{[\s\S]*?\})\s*\}/);
1465
+ // ── 4. ReAct: Action: tool_name\nAction Input: {...} (GPT/DeepSeek) ──
1466
+ const reactMatch = response.match(/Action\s*:\s*(\w+)\s*\n+Action\s*Input\s*:\s*([\s\S]*?)(?:\n\s*(?:Thought|Observation|$))/i);
1467
+ if (reactMatch) {
1468
+ const toolName = reactMatch[1];
1469
+ if (this.#toolRegistry.has(toolName)) {
1470
+ try {
1471
+ return { tool: toolName, params: JSON.parse(reactMatch[2].trim()) };
1472
+ } catch {
1473
+ const parsed = this.#parseToolCodeBlock(`${toolName}(${reactMatch[2].trim()})`);
1474
+ if (parsed) return parsed;
1475
+ }
1476
+ }
1477
+ }
1478
+ // Action/Action Input 在末尾(无后续 Thought)
1479
+ const reactEndMatch = response.match(/Action\s*:\s*(\w+)\s*\n+Action\s*Input\s*:\s*(\{[\s\S]*\})\s*$/i);
1480
+ if (reactEndMatch) {
1481
+ const toolName = reactEndMatch[1];
1482
+ if (this.#toolRegistry.has(toolName)) {
1483
+ try { return { tool: toolName, params: JSON.parse(reactEndMatch[2].trim()) }; } catch { /* ignore */ }
1484
+ }
1485
+ }
1486
+
1487
+ // ── 5. XML: <tool_call>...</tool_call> / <function_call>...</function_call> ──
1488
+ const xmlMatch = response.match(/<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/);
1489
+ if (xmlMatch) {
1490
+ const parsed = this.#tryParseToolJson(xmlMatch[1].trim());
1491
+ if (parsed) return parsed;
1492
+ }
1493
+ const fcMatch = response.match(/<function_call>\s*([\s\S]*?)\s*<\/function_call>/);
1494
+ if (fcMatch) {
1495
+ const parsed = this.#tryParseToolJson(fcMatch[1].trim());
1496
+ if (parsed) return parsed;
1497
+ }
1498
+
1499
+ // ── 6. ```json {...} ``` 内的 function_call 格式 ──
1500
+ const jsonFenceMatch = response.match(/```json\s*\n?([\s\S]*?)```/);
1501
+ if (jsonFenceMatch) {
1502
+ const parsed = this.#tryParseToolJson(jsonFenceMatch[1].trim());
1503
+ if (parsed) return parsed;
1504
+ }
1505
+
1506
+ // ── 7. 裸 JSON: {"tool":"..."} 或 {"name":"..."} ──
1507
+ const jsonMatch = response.match(/\{\s*"(?:tool|name|function)"\s*:\s*"([^"]+)"[\s\S]*?\}/);
786
1508
  if (jsonMatch) {
1509
+ const parsed = this.#tryParseToolJson(jsonMatch[0]);
1510
+ if (parsed) return parsed;
1511
+ }
1512
+
1513
+ // ── 8. 末尾裸函数调用: tool_name(key="value") ──
1514
+ const trailingFnMatch = response.match(/\b(\w+)\(([^)]*)\)\s*$/);
1515
+ if (trailingFnMatch) {
1516
+ const parsed = this.#parseToolCodeBlock(`${trailingFnMatch[1]}(${trailingFnMatch[2]})`);
1517
+ if (parsed) return parsed;
1518
+ }
1519
+
1520
+ return null;
1521
+ }
1522
+
1523
+ /**
1524
+ * 尝试从 JSON 文本解析工具调用
1525
+ * 兼容多种 key 命名:
1526
+ * - {"tool": "x", "params": {...}} — 标准格式
1527
+ * - {"name": "x", "arguments": {...}} — OpenAI function_call
1528
+ * - {"function": "x", "parameters": {...}} — 变体
1529
+ * - {"tool": "x", "input": {...}} — Claude 变体
1530
+ */
1531
+ #tryParseToolJson(text) {
1532
+ if (!text) return null;
1533
+ try {
1534
+ const obj = JSON.parse(text);
1535
+ const toolName = obj.tool || obj.name || obj.function;
1536
+ if (!toolName || !this.#toolRegistry.has(toolName)) return null;
1537
+ const params = obj.params || obj.arguments || obj.parameters || obj.input || {};
1538
+ return { tool: toolName, params };
1539
+ } catch { return null; }
1540
+ }
1541
+
1542
+ /**
1543
+ * 解析 tool_code 函数调用格式
1544
+ * 支持三种参数格式:
1545
+ * 1. key=value: search_project_code(query="xxx", language="objc")
1546
+ * 2. JSON 对象: read_project_file({"file_path": "Code/X.m"})
1547
+ * 3. 单字符串: read_project_file("Code/X.m")
1548
+ */
1549
+ #parseToolCodeBlock(text) {
1550
+ if (!text) return null;
1551
+ const fnMatch = text.match(/^(\w+)\((.*)\)$/s);
1552
+ if (!fnMatch) return null;
1553
+
1554
+ const toolName = fnMatch[1];
1555
+ if (!this.#toolRegistry.has(toolName)) return null;
1556
+
1557
+ const argsStr = fnMatch[2].trim();
1558
+ if (!argsStr) return { tool: toolName, params: {} };
1559
+
1560
+ // 尝试 1: key=value 格式 (Python 风格)
1561
+ const params = {};
1562
+ const argRegex = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^,\s]+))/g;
1563
+ let m;
1564
+ while ((m = argRegex.exec(argsStr)) !== null) {
1565
+ params[m[1]] = m[2] ?? m[3] ?? m[4];
1566
+ }
1567
+ if (Object.keys(params).length > 0) return { tool: toolName, params };
1568
+
1569
+ // 尝试 2: JSON 对象参数 — read_project_file({"file_path": "..."})
1570
+ if (argsStr.startsWith('{')) {
787
1571
  try {
788
- const tool = jsonMatch[1];
789
- const params = JSON.parse(jsonMatch[2]);
790
- if (this.#toolRegistry.has(tool)) {
791
- return { tool, params };
1572
+ const jsonParams = JSON.parse(argsStr);
1573
+ if (typeof jsonParams === 'object' && jsonParams !== null) {
1574
+ return { tool: toolName, params: jsonParams };
792
1575
  }
793
- } catch { /* parse failed */ }
1576
+ } catch { /* not valid JSON, fall through */ }
794
1577
  }
795
1578
 
796
- return null;
1579
+ // 尝试 3: 单字符串参数 — read_project_file("Code/X.m") → 映射到首个 required 参数
1580
+ const strMatch = argsStr.match(/^["'](.+?)["']$/);
1581
+ if (strMatch) {
1582
+ const toolDef = this.#toolRegistry.getToolSchemas().find(t => t.name === toolName);
1583
+ const firstRequired = toolDef?.parameters?.required?.[0];
1584
+ if (firstRequired) {
1585
+ return { tool: toolName, params: { [firstRequired]: strMatch[1] } };
1586
+ }
1587
+ }
1588
+
1589
+ return { tool: toolName, params };
1590
+ }
1591
+
1592
+ /**
1593
+ * 从 LLM 响应中解析 Action 块(支持批量)
1594
+ *
1595
+ * 优先匹配:
1596
+ * ```batch_actions [...]```
1597
+ * 降级匹配:
1598
+ * - 多个 <tool_call> XML 标签
1599
+ * - 多个 ReAct Action 块
1600
+ * - 单条 #parseAction()
1601
+ *
1602
+ * @returns {Array<{tool:string, params:object}>|null}
1603
+ */
1604
+ #parseActions(response) {
1605
+ if (!response) return null;
1606
+
1607
+ // 1. 优先尝试 ```batch_actions``` 块
1608
+ const batchMatch = response.match(/```batch_actions\s*\n?([\s\S]*?)```/);
1609
+ if (batchMatch) {
1610
+ try {
1611
+ const arr = JSON.parse(batchMatch[1].trim());
1612
+ if (Array.isArray(arr) && arr.length > 0) {
1613
+ const valid = arr.filter(a => a.tool && this.#toolRegistry.has(a.tool));
1614
+ if (valid.length > 0) {
1615
+ return valid.map(a => ({ tool: a.tool, params: a.params || {} }));
1616
+ }
1617
+ }
1618
+ } catch { /* batch parse failed, fall through */ }
1619
+ }
1620
+
1621
+ // 2. 多个 <tool_call> XML 块 (DeepSeek/Qwen)
1622
+ const xmlMatches = [...response.matchAll(/<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g)];
1623
+ if (xmlMatches.length > 1) {
1624
+ const results = xmlMatches
1625
+ .map(m => this.#tryParseToolJson(m[1].trim()))
1626
+ .filter(Boolean);
1627
+ if (results.length > 0) return results;
1628
+ }
1629
+
1630
+ // 3. 多个 ReAct Action 块
1631
+ const reactMatches = [...response.matchAll(/Action\s*:\s*(\w+)\s*\n+Action\s*Input\s*:\s*(\{[\s\S]*?\})/gi)];
1632
+ if (reactMatches.length > 1) {
1633
+ const results = reactMatches
1634
+ .map(m => {
1635
+ const toolName = m[1];
1636
+ if (!this.#toolRegistry.has(toolName)) return null;
1637
+ try { return { tool: toolName, params: JSON.parse(m[2].trim()) }; } catch { return null; }
1638
+ })
1639
+ .filter(Boolean);
1640
+ if (results.length > 0) return results;
1641
+ }
1642
+
1643
+ // 4. 降级到单 action
1644
+ const single = this.#parseAction(response);
1645
+ return single ? [single] : null;
797
1646
  }
798
1647
 
799
1648
  /**
@@ -808,22 +1657,61 @@ ${skillSection}
808
1657
  .trim();
809
1658
  }
810
1659
 
1660
+ /**
1661
+ * 检测 AI 回复是否为「未完成的中间步骤」— 输出分析/计划文本但未实际调用工具
1662
+ *
1663
+ * Gemini 常见行为: 收到 production prompt 后先输出一段纯文本的
1664
+ * "执行计划" 或 "信号审视" 而不包含任何 action/tool_code block,
1665
+ * 导致 #parseActions() 返回 null,被误判为 final answer。
1666
+ *
1667
+ * 检测策略: 回复包含计划/分析关键词 + 不包含 dimensionDigest JSON
1668
+ */
1669
+ #looksLikeIncompleteStep(response) {
1670
+ if (!response || response.length < 100) return false;
1671
+
1672
+ // 如果已包含 dimensionDigest → 是真正的最终回答
1673
+ if (response.includes('"dimensionDigest"') || response.includes('dimensionDigest')) return false;
1674
+
1675
+ // 计划/分析性关键词 (中文 Gemini 常用)
1676
+ const planningPatterns = [
1677
+ /制定执行计划/,
1678
+ /信号质量预判/,
1679
+ /执行计划/,
1680
+ /我将按照/,
1681
+ /开始分析/,
1682
+ /我将分析/,
1683
+ /接下来我将/,
1684
+ /我来分析/,
1685
+ /首先[,,]?\s*我/,
1686
+ /\*\*0\.\s*制定/,
1687
+ /\*\*Signal\s+\d+/, // 信号列表分析
1688
+ /质量[::]\s*(高|中|低)/, // 信号质量评估
1689
+ /保留[。;]|丢弃[。;]|跳过[。;]/, // 信号去留判断
1690
+ ];
1691
+
1692
+ const matchCount = planningPatterns.filter(p => p.test(response)).length;
1693
+ return matchCount >= 2; // 至少匹配 2 个模式才认为是计划性回复
1694
+ }
1695
+
811
1696
  /**
812
1697
  * 获取工具执行上下文
1698
+ * @param {object} [extras] — 额外注入到上下文的字段(如 _sessionToolCalls)
813
1699
  */
814
- #getToolContext() {
1700
+ #getToolContext(extras) {
815
1701
  return {
816
1702
  container: this.#container,
817
1703
  aiProvider: this.#aiProvider,
818
1704
  projectRoot: this.#container?.singletons?._projectRoot || process.cwd(),
819
1705
  logger: this.#logger,
820
1706
  source: this.#currentSource,
1707
+ fileCache: this.#fileCache || null,
1708
+ ...extras,
821
1709
  };
822
1710
  }
823
1711
 
824
1712
  /**
825
1713
  * 列出可用的 Skills 及其摘要(用于系统提示词)
826
- * 加载顺序: 内置 skills/ → 项目级 .autosnippet/skills/(同名覆盖)
1714
+ * 加载顺序: 内置 skills/ → 项目级 AutoSnippet/skills/(同名覆盖)
827
1715
  * @returns {{ name: string, summary: string }[]}
828
1716
  */
829
1717
  #listAvailableSkills() {