autosnippet 2.18.0 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dashboard/dist/assets/{icons-C6kshpB1.js → icons-C7FN32VL.js} +1 -1
  2. package/dashboard/dist/assets/index-D8dCXLzr.js +129 -0
  3. package/dashboard/dist/index.html +2 -2
  4. package/lib/external/ai/AiProvider.js +42 -11
  5. package/lib/external/ai/providers/ClaudeProvider.js +4 -2
  6. package/lib/external/ai/providers/GoogleGeminiProvider.js +66 -8
  7. package/lib/external/ai/providers/OpenAiProvider.js +48 -2
  8. package/lib/external/mcp/handlers/bootstrap.js +1 -2
  9. package/lib/http/HttpServer.js +4 -0
  10. package/lib/http/routes/candidates.js +405 -0
  11. package/lib/http/routes/search.js +113 -0
  12. package/lib/infrastructure/vector/Chunker.js +3 -8
  13. package/lib/infrastructure/vector/JsonVectorAdapter.js +2 -9
  14. package/lib/service/candidate/SimilarityService.js +7 -35
  15. package/lib/service/chat/ChatAgent.js +28 -686
  16. package/lib/service/chat/ContextWindow.js +87 -3
  17. package/lib/service/chat/ConversationStore.js +3 -4
  18. package/lib/service/chat/ProjectSemanticMemory.js +9 -14
  19. package/lib/service/chat/ReasoningLayer.js +10 -54
  20. package/lib/service/chat/ToolRegistry.js +0 -52
  21. package/lib/service/chat/tools.js +7 -6
  22. package/lib/service/cursor/TokenBudget.js +4 -21
  23. package/lib/service/search/CrossEncoderReranker.js +163 -0
  24. package/lib/service/search/RetrievalFunnel.js +9 -36
  25. package/lib/service/skills/SignalCollector.js +28 -28
  26. package/lib/shared/similarity.js +101 -0
  27. package/lib/shared/token-utils.js +46 -0
  28. package/package.json +1 -1
  29. package/dashboard/dist/assets/index-9byoG7kd.js +0 -129
@@ -62,55 +62,6 @@ const DEFAULT_BUDGET = Object.freeze({
62
62
  idleRoundsToExit: IDLE_ROUNDS_TO_EXIT,
63
63
  });
64
64
 
65
- /**
66
- * 系统调用续跑提示 — 当 AI 输出纯文本计划而未执行工具调用时注入
67
- * 告诉 AI 不要只写文字描述,而要实际调用工具
68
- */
69
- const SYSTEM_CONTINUATION_PROMPT = `你的分析计划很好。但你需要 **实际执行工具调用** 来完成任务,而不是只写文字描述。
70
-
71
- 请现在开始执行:
72
- 1. 用 \`search_project_code\` 搜索项目代码获取真实示例
73
- 2. 用 \`read_project_file\` 查看完整文件内容
74
- 3. 对每个值得保留的信号,用 \`submit_knowledge\` 提交候选
75
-
76
- ⚡ 推荐使用 batch_actions 一次提交多条候选:
77
- \`\`\`batch_actions
78
- [
79
- {"tool": "submit_knowledge", "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}}},
80
- {"tool": "submit_knowledge", "params": {"title": "...", "code": "...", ...}}
81
- ]
82
- \`\`\`
83
-
84
- 请立即开始执行,不要再输出分析文字。`;
85
-
86
- /**
87
- * 系统调用提交提示 — 当 AI 做了工具调用(search/read)、写了分析文本,但没调 submit_knowledge 时注入
88
- * 引导 AI 将已有分析转化为实际的 submit_knowledge 调用
89
- */
90
- const SYSTEM_SUBMIT_PROMPT = `你的分析很好,已经获取了足够的项目信息。但你还没有调用 \`submit_knowledge\` 提交任何候选。
91
-
92
- **你的分析不能只停留在文字描述层面** — 必须通过工具调用将分析结果持久化。
93
-
94
- 请根据你刚才的分析,立即使用 batch_actions 提交候选:
95
-
96
- \`\`\`batch_actions
97
- [
98
- {"tool": "submit_knowledge", "params": {
99
- "title": "[Bootstrap] 维度/子主题",
100
- "code": "# 标题 — 项目特写\\n\\n> 本项目使用 XX 模式, N 个文件采用此写法\\n\\n描述...\\n\\n\`\`\`objc\\n// 真实代码示例\\n\`\`\`\\n\\n要点说明...",
101
- "language": "objectivec",
102
- "category": "Tool",
103
- "summary": "≤80字精准摘要,引用真实类名和数字",
104
- "tags": ["bootstrap", "维度id"],
105
- "source": "bootstrap",
106
- "reasoning": {"whyStandard": "为什么值得保留", "sources": ["真实文件名"], "confidence": 0.7}
107
- }},
108
- {"tool": "submit_knowledge", "params": {...}}
109
- ]
110
- \`\`\`
111
-
112
- 将你上面分析出的每个有价值的发现都转化为一条 submit_knowledge 调用。code 字段写「项目特写」风格: 描述和代码交织,用项目真实类名和代码。`;
113
-
114
65
  export class ChatAgent {
115
66
  #toolRegistry;
116
67
  #aiProvider;
@@ -156,6 +107,9 @@ export class ChatAgent {
156
107
  */
157
108
  this.hasRealAI = !!aiProvider && aiProvider.name !== 'mock';
158
109
 
110
+ /** AI Provider 引用(只读)— 用于外部模块直接调用 structuredOutput / extractJSON */
111
+ this.aiProvider = aiProvider || null;
112
+
159
113
  // 初始化跨对话记忆 + 对话持久化
160
114
  try {
161
115
  const projectRoot = container?.singletons?._projectRoot || process.cwd();
@@ -243,27 +197,18 @@ export class ChatAgent {
243
197
  // 每次对话刷新项目概况(不是每轮 ReAct)
244
198
  this.#projectBriefingCache = await this.#buildProjectBriefing();
245
199
 
246
- // ── 双模路由: 原生函数调用 vs 文本解析 ──
247
- // 支持原生函数调用的 Provider (如 Gemini) 走结构化路径,
248
- // 其他 Provider 走传统文本 ReAct 解析路径
200
+ // ── 统一原生函数调用路径(v5.0: 移除文本解析路径) ──
201
+ // 所有 Provider 均通过 chatWithTools() 进行结构化工具调用。
202
+ // 不支持原生函数调用的 Provider 在基类 chatWithTools() 中降级为 chat(),
203
+ // 返回 { text, functionCalls: null },被 native 循环视为最终回答。
204
+ this.#logger.info(`[ChatAgent] ✨ using NATIVE tool calling mode (${this.#aiProvider.name})`);
249
205
  let result;
250
- if (this.#aiProvider.supportsNativeToolCalling) {
251
- this.#logger.info(`[ChatAgent] using NATIVE tool calling mode (${this.#aiProvider.name})`);
252
- result = await this.#executeWithNativeTools(prompt, {
253
- effectiveHistory, conversationId, source, execStartTime, budget, dimensionMeta,
254
- // v3.0 新增
255
- systemPromptOverride, allowedTools, disablePhaseRouter, temperatureOverride,
256
- projectLanguage,
257
- // v4.0: Agent Memory
258
- workingMemory, episodicMemory, toolResultCache,
259
- });
260
- } else {
261
- this.#logger.info(`[ChatAgent] 📝 using TEXT parsing mode (${this.#aiProvider.name})`);
262
- result = await this.#executeWithTextParsing(prompt, {
263
- effectiveHistory, conversationId, source, execStartTime,
264
- dimensionMeta, projectLanguage,
265
- });
266
- }
206
+ result = await this.#executeWithNativeTools(prompt, {
207
+ effectiveHistory, conversationId, source, execStartTime, budget, dimensionMeta,
208
+ systemPromptOverride, allowedTools, disablePhaseRouter, temperatureOverride,
209
+ projectLanguage,
210
+ workingMemory, episodicMemory, toolResultCache,
211
+ });
267
212
 
268
213
  // 持久化 assistant 回复
269
214
  if (conversationId && this.#conversations) {
@@ -333,7 +278,11 @@ export class ChatAgent {
333
278
 
334
279
  // ── Layer 1: ContextWindow ──
335
280
  // messages[0] = prompt(不可压缩),历史消息在前面
336
- const ctx = new ContextWindow(isSystem ? 24000 : 16000);
281
+ // token 预算按模型动态适配:大窗口模型(Gemini/Claude)给更多预算,小窗口(Ollama)限制预算
282
+ const tokenBudget = ContextWindow.resolveTokenBudget(
283
+ this.#aiProvider?.model, { isSystem },
284
+ );
285
+ const ctx = new ContextWindow(tokenBudget);
337
286
  for (const h of effectiveHistory) {
338
287
  if (h.role === 'assistant') {
339
288
  ctx.appendAssistantText(h.content);
@@ -568,7 +517,7 @@ export class ChatAgent {
568
517
  consecutiveAiErrors = 0;
569
518
 
570
519
  // ── ReasoningLayer Hook 2: afterAICall — 提取 Thought ──
571
- reasoning.afterAICall(aiResult, 'native');
520
+ reasoning.afterAICall(aiResult);
572
521
  } catch (aiErr) {
573
522
  consecutiveAiErrors++;
574
523
  this.#logger.warn(`[ChatAgent] AI call failed (attempt ${consecutiveAiErrors}): ${aiErr.message}`);
@@ -1052,202 +1001,10 @@ ${submitSummary ? `已提交候选:\n${submitSummary}\n` : ''}
1052
1001
  return { reply: finalReply, toolCalls, hasContext: toolCalls.length > 0 };
1053
1002
  }
1054
1003
 
1055
- // ─── Text Parsing ReAct 循环 (legacy) ─────────────────
1056
-
1057
- /**
1058
- * 文本解析 ReAct 循环 传统模式
1059
- * 适用于不支持原生函数调用的 Provider (DeepSeek, OpenAI 兼容等)
1060
- * AI 输出文本 → #parseActions() 正则解析 → 执行工具 → 循环
1061
- */
1062
- async #executeWithTextParsing(prompt, { effectiveHistory, conversationId, source, execStartTime, dimensionMeta, projectLanguage }) {
1063
- const toolSchemas = this.#toolRegistry.getToolSchemas();
1064
- const systemPrompt = this.#buildSystemPrompt(toolSchemas);
1065
-
1066
- const messages = [
1067
- ...effectiveHistory,
1068
- { role: 'user', content: prompt },
1069
- ];
1070
-
1071
- const toolCalls = [];
1072
- let iterations = 0;
1073
- let currentPrompt = prompt;
1074
- let consecutiveAiErrors = 0;
1075
- const maxIter = source === 'system' ? MAX_ITERATIONS_SYSTEM : MAX_ITERATIONS;
1076
-
1077
- // ── ReasoningLayer (文本解析模式) ──
1078
- const reasoning = new ReasoningLayer({
1079
- enabled: true,
1080
- reflectionEnabled: false, // 文本模式暂不启用反思(避免复杂度)
1081
- });
1082
-
1083
- while (iterations < maxIter) {
1084
- iterations++;
1085
- const iterStartTime = Date.now();
1086
-
1087
- // ── ReasoningLayer Hook 1: beforeAICall ──
1088
- reasoning.beforeAICall(iterations);
1089
-
1090
- let response;
1091
- try {
1092
- this.#logger.info(`[ChatAgent] 🔄 text iteration ${iterations}/${maxIter} — calling AI (${messages.length} messages)`);
1093
- response = await this.#aiProvider.chat(currentPrompt, {
1094
- history: messages.slice(0, -1),
1095
- systemPrompt,
1096
- });
1097
- const aiDuration = Date.now() - iterStartTime;
1098
- const responsePreview = (response || '').substring(0, 120).replace(/\n/g, '↵');
1099
- this.#logger.info(`[ChatAgent] ✓ AI responded in ${aiDuration}ms (${(response || '').length} chars) — "${responsePreview}…"`);
1100
- consecutiveAiErrors = 0;
1101
-
1102
- // ── ReasoningLayer Hook 2: afterAICall ──
1103
- reasoning.afterAICall(response, 'text');
1104
- } catch (aiErr) {
1105
- consecutiveAiErrors++;
1106
- this.#logger.warn(`[ChatAgent] AI call failed (attempt ${consecutiveAiErrors}): ${aiErr.message}`);
1107
-
1108
- // 熔断器已打开 → 立即返回
1109
- if (aiErr.code === 'CIRCUIT_OPEN') {
1110
- reasoning.afterRound();
1111
- return {
1112
- reply: `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`,
1113
- toolCalls,
1114
- hasContext: toolCalls.length > 0,
1115
- reasoningTrace: reasoning.trace,
1116
- reasoningQuality: reasoning.getQualityMetrics(),
1117
- };
1118
- }
1119
-
1120
- if (consecutiveAiErrors >= 2) {
1121
- reasoning.afterRound();
1122
- return {
1123
- reply: `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`,
1124
- toolCalls,
1125
- hasContext: toolCalls.length > 0,
1126
- reasoningTrace: reasoning.trace,
1127
- reasoningQuality: reasoning.getQualityMetrics(),
1128
- };
1129
- }
1130
- await new Promise(r => setTimeout(r, 2000));
1131
- continue;
1132
- }
1133
-
1134
- const actions = this.#parseActions(response);
1135
-
1136
- if (!actions) {
1137
- // ── 系统调用自动续跑 ──
1138
- const hasSubmits = toolCalls.some(tc => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check');
1139
- if (source === 'system' && iterations < maxIter && !hasSubmits) {
1140
- if (this.#looksLikeIncompleteStep(response)) {
1141
- this.#logger.info(`[ChatAgent] 🔄 detected planning-only response at iteration ${iterations}, injecting continuation prompt`);
1142
- messages.push({ role: 'assistant', content: response });
1143
- currentPrompt = SYSTEM_CONTINUATION_PROMPT;
1144
- messages.push({ role: 'user', content: currentPrompt });
1145
- continue;
1146
- }
1147
- if (toolCalls.length > 0) {
1148
- this.#logger.info(`[ChatAgent] 🔄 detected analysis-without-submission at iteration ${iterations} (${toolCalls.length} tool calls, 0 submits), injecting submission prompt`);
1149
- messages.push({ role: 'assistant', content: response });
1150
- currentPrompt = SYSTEM_SUBMIT_PROMPT;
1151
- messages.push({ role: 'user', content: currentPrompt });
1152
- continue;
1153
- }
1154
- }
1155
-
1156
- const reply = this.#cleanFinalAnswer(response);
1157
- const totalDuration = Date.now() - execStartTime;
1158
- reasoning.afterRound();
1159
- this.#logger.info(`[ChatAgent] ✅ text final answer — ${reply.length} chars, ${iterations} iterations, ${toolCalls.length} tool calls, ${totalDuration}ms total`);
1160
-
1161
- return { reply, toolCalls, hasContext: toolCalls.length > 0, reasoningTrace: reasoning.trace, reasoningQuality: reasoning.getQualityMetrics() };
1162
- }
1163
-
1164
- // 执行工具
1165
- const isBatch = actions.length > 1;
1166
- if (isBatch) {
1167
- this.#logger.info(`[ChatAgent] 📦 batch tool call: ${actions.length} actions [${actions.map(a => a.tool).join(', ')}]`, { iteration: iterations });
1168
- }
1169
-
1170
- const batchResults = [];
1171
- for (const action of actions) {
1172
- this.#logger.info(`[ChatAgent] 🔧 tool call: ${action.tool}(${JSON.stringify(action.params).substring(0, 100)})`, {
1173
- iteration: iterations,
1174
- batch: isBatch,
1175
- });
1176
-
1177
- let toolResult;
1178
- const toolStartTime = Date.now();
1179
- try {
1180
- toolResult = await this.#toolRegistry.execute(
1181
- action.tool,
1182
- action.params,
1183
- this.#getToolContext({ _sessionToolCalls: toolCalls, _dimensionMeta: dimensionMeta, _projectLanguage: projectLanguage }),
1184
- );
1185
- const toolDuration = Date.now() - toolStartTime;
1186
- const resultSize = typeof toolResult === 'string' ? toolResult.length : JSON.stringify(toolResult).length;
1187
- this.#logger.info(`[ChatAgent] 🔧 tool done: ${action.tool} → ${resultSize} chars in ${toolDuration}ms`);
1188
- } catch (toolErr) {
1189
- this.#logger.warn(`[ChatAgent] 🔧 tool FAILED: ${action.tool} — ${toolErr.message} (${Date.now() - toolStartTime}ms)`);
1190
- toolResult = `Error: tool "${action.tool}" failed — ${toolErr.message}. Try a different approach or provide your answer based on available information.`;
1191
- }
1192
-
1193
- const summarized = this.#summarizeResult(toolResult);
1194
- toolCalls.push({
1195
- tool: action.tool,
1196
- params: action.params,
1197
- result: summarized,
1198
- });
1199
- batchResults.push({ tool: action.tool, result: toolResult });
1200
-
1201
- // ── ReasoningLayer Hook 3: afterToolExec ──
1202
- reasoning.afterToolExec(action.tool, action.params, toolResult);
1203
- }
1204
-
1205
- // ── ReasoningLayer Hook 4: afterRound ──
1206
- reasoning.afterRound({
1207
- totalCalls: actions.length,
1208
- submitCount: 0,
1209
- });
1210
-
1211
- // 将工具结果注入为下一轮 prompt
1212
- let observation;
1213
- if (batchResults.length === 1) {
1214
- const r = batchResults[0];
1215
- const obsText = typeof r.result === 'string' ? r.result : JSON.stringify(r.result, null, 2);
1216
- observation = `Observation from tool "${r.tool}":\n${this.#truncate(obsText, 4000)}`;
1217
- } else {
1218
- observation = `Batch observation (${batchResults.length} tools):\n` +
1219
- batchResults.map((r, i) => {
1220
- const obsText = typeof r.result === 'string' ? r.result : JSON.stringify(r.result, null, 2);
1221
- return `[${i + 1}] ${r.tool}: ${this.#truncate(obsText, 2000)}`;
1222
- }).join('\n\n');
1223
- }
1224
-
1225
- 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.`;
1226
-
1227
- messages.push({ role: 'assistant', content: response });
1228
- messages.push({ role: 'user', content: currentPrompt });
1229
-
1230
- this.#condenseIfNeeded(messages);
1231
- }
1232
-
1233
- // 达到最大迭代次数
1234
- const summaryPrompt = `You have used ${iterations} tool calls. Summarize what you found and answer the user's original question: "${prompt}"`;
1235
- let finalResponse;
1236
- try {
1237
- finalResponse = await this.#aiProvider.chat(summaryPrompt, {
1238
- history: messages,
1239
- systemPrompt: '直接回答用户问题,不要再调用工具。',
1240
- });
1241
- } catch (err) {
1242
- this.#logger.warn(`[ChatAgent] Final summary AI call failed: ${err.message}`);
1243
- finalResponse = `根据 ${toolCalls.length} 次工具调用的结果,以下是收集到的信息:\n\n` +
1244
- toolCalls.map(tc => `• ${tc.tool}: ${typeof tc.result === 'string' ? tc.result.substring(0, 200) : JSON.stringify(tc.result).substring(0, 200)}`).join('\n') +
1245
- '\n\n(注:AI 总结服务暂时不可用,上述为原始工具输出摘要)';
1246
- }
1247
-
1248
- const finalReply = this.#cleanFinalAnswer(finalResponse);
1249
- return { reply: finalReply, toolCalls, hasContext: toolCalls.length > 0, reasoningTrace: reasoning.trace, reasoningQuality: reasoning.getQualityMetrics() };
1250
- }
1004
+ // ─── Text Parsing 已移除 (v5.0) ────────────────────────
1005
+ // 所有 Provider 统一走 chatWithTools() 原生函数调用路径。
1006
+ // 不支持 native tool calling 的 Provider 在基类 chatWithTools()
1007
+ // 中降级为 chat(),返回 { text, functionCalls: null }。
1251
1008
 
1252
1009
  /**
1253
1010
  * 程序化直接调用指定工具(跳过 ReAct 循环)
@@ -1580,8 +1337,9 @@ ${code.substring(0, 3000)}
1580
1337
 
1581
1338
  请用 JSON 数组格式返回建议: [{"violation": "...", "suggestion": "...", "fixExample": "..."}]`;
1582
1339
 
1583
- const raw = await this.#aiProvider.chat(prompt, { temperature: 0.3 });
1584
- suggestions = this.#aiProvider.extractJSON(raw, '[', ']') || [];
1340
+ suggestions = await this.#aiProvider.chatWithStructuredOutput(prompt, {
1341
+ openChar: '[', closeChar: ']', temperature: 0.3,
1342
+ }) || [];
1585
1343
  } catch { /* AI suggestions optional */ }
1586
1344
  }
1587
1345
 
@@ -1623,100 +1381,7 @@ ${code.substring(0, 3000)}
1623
1381
  ]));
1624
1382
  }
1625
1383
 
1626
- // ─── ReAct 内部方法 ────────────────────────────────────
1627
-
1628
- /**
1629
- * 构建系统提示词(含工具描述 + Skills 感知)
1630
- *
1631
- * 工具注入策略(Lazy Tool Schema — 类似 Cline .clinerules 按需加载):
1632
- * - 首屏只注入工具名 + 一行描述(compact list)
1633
- * - 系统提示词中告知 LLM 可通过 get_tool_details 获取完整参数
1634
- * - 少量核心工具(search_project_code, read_project_file, search_knowledge,
1635
- * submit_with_check, analyze_code, bootstrap_knowledge, load_skill,
1636
- * suggest_skills)直接展开完整 schema
1637
- *
1638
- * 效果: 44 个工具的 prompt 从 ~5000 tokens 降到 ~1500 tokens
1639
- */
1640
- #buildSystemPrompt(toolSchemas) {
1641
- // 核心工具 — 使用最频繁,直接展示完整 schema
1642
- const coreTools = new Set([
1643
- 'search_project_code', 'read_project_file',
1644
- 'search_knowledge', 'submit_knowledge', 'submit_with_check', 'analyze_code',
1645
- 'bootstrap_knowledge', 'load_skill', 'suggest_skills',
1646
- 'create_skill', 'knowledge_overview', 'get_tool_details',
1647
- 'plan_task', 'review_my_output',
1648
- ]);
1649
-
1650
- const compactDescriptions = [];
1651
- const detailedDescriptions = [];
1652
-
1653
- for (const t of toolSchemas) {
1654
- if (coreTools.has(t.name)) {
1655
- const paramsDesc = Object.entries(t.parameters.properties || {})
1656
- .map(([k, v]) => ` - ${k} (${v.type}): ${v.description || ''}`)
1657
- .join('\n');
1658
- detailedDescriptions.push(`- **${t.name}**: ${t.description}\n Parameters:\n${paramsDesc || ' (none)'}`);
1659
- } else {
1660
- compactDescriptions.push(`- ${t.name}: ${t.description}`);
1661
- }
1662
- }
1663
-
1664
- const toolDescriptions = `### 核心工具(完整参数)\n\n${detailedDescriptions.join('\n\n')}\n\n### 其他工具(调用 get_tool_details 获取参数详情)\n\n${compactDescriptions.join('\n')}`;
1665
-
1666
- // Skills 清单 — 让 LLM 知道有哪些领域知识可加载
1667
- const skillList = this.#listAvailableSkills();
1668
- const skillSection = skillList.length > 0
1669
- ? `\n## 可用 Skills\n通过 load_skill 工具按需加载领域知识文档,获取操作指南和最佳实践参考。\n\n| Skill | 说明 |\n|---|---|\n${skillList.map(s => `| ${s.name} | ${s.summary || '-'} |`).join('\n')}\n\n**场景 → Skill 推荐**:\n- 冷启动、初始化 → autosnippet-coldstart\n- 深度项目分析 → autosnippet-analysis\n- 候选生成 → autosnippet-candidates + autosnippet-create\n- 代码规范审计 → autosnippet-guard\n- Snippet 概念解释 → autosnippet-concepts\n- 生命周期管理 → autosnippet-lifecycle\n- Swift/ObjC/JS·TS 语言参考 → autosnippet-reference-{swift,objc,jsts}\n- 项目结构分析 → autosnippet-structure\n- 不确定该用哪个 → autosnippet-intent\n`
1670
- : '';
1671
-
1672
- // SOUL — AI 人格注入(如果 SOUL.md 存在)
1673
- let soulSection = '';
1674
- try {
1675
- if (fs.existsSync(SOUL_PATH)) {
1676
- soulSection = '\n' + fs.readFileSync(SOUL_PATH, 'utf-8').trim() + '\n';
1677
- }
1678
- } catch { /* SOUL.md not available */ }
1679
-
1680
- return `${soulSection}
1681
- 你是 AutoSnippet 项目的统一 AI 中心。项目内所有 AI 推理和分析都通过你执行。
1682
- 你拥有 ${toolSchemas.length} 个工具覆盖知识库管理全链路:搜索、提交、审核、质量评估、Guard 检查、知识图谱、冷启动等。
1683
- ${this.#projectBriefingCache}${this.#memory?.toPromptSection({ source: this.#currentSource === 'system' ? undefined : 'user' }) || ''}${this.#semanticMemory?.toPromptSection({ source: this.#currentSource === 'system' ? undefined : 'user' }) || ''}
1684
- 可用工具:
1685
-
1686
- ${toolDescriptions}
1687
- ${skillSection}
1688
- ## 使用规则
1689
- 1. 当用户的问题需要查询数据时,使用工具获取信息后再回答。
1690
- 2. 调用工具时,使用以下格式(必须严格遵循):
1691
-
1692
- \`\`\`action
1693
- {"tool": "tool_name", "params": {"key": "value"}}
1694
- \`\`\`
1695
-
1696
- 3. 当需要连续调用多个**同类工具**(如批量提交候选)时,可使用批量格式:
1697
-
1698
- \`\`\`batch_actions
1699
- [
1700
- {"tool": "submit_knowledge", "params": {"title": "...", "code": "..."}},
1701
- {"tool": "submit_knowledge", "params": {"title": "...", "code": "..."}}
1702
- ]
1703
- \`\`\`
1704
-
1705
- 4. 如果不需要工具就能回答,直接回答,不要输出 action 块。
1706
- 5. 回答时使用用户的语言(中文/英文)。
1707
- 6. 回答要简洁、有依据(引用工具返回的数据)。
1708
- 7. 当涉及以下领域问题时,**必须**先 load_skill 加载对应 Skill,再执行操作:
1709
- - 冷启动/初始化 → load_skill("autosnippet-coldstart")
1710
- - 深度分析/扫描 → load_skill("autosnippet-analysis")
1711
- - 候选创建/提交 → load_skill("autosnippet-candidates")
1712
- - 代码规范/Guard → load_skill("autosnippet-guard")
1713
- - 不确定做什么 → load_skill("autosnippet-intent")
1714
- 8. 你可以组合多个工具完成复杂任务(如:查重 → 提交 → 质量评分 → 知识图谱关联)。
1715
- 9. 当工具返回 _meta.confidence = "none" 时,告知用户无匹配并建议下一步,不要凭空编造。当 _meta.confidence = "low" 时,明确标注结果不确定性。
1716
- 10. 优先使用组合工具(analyze_code, knowledge_overview, submit_with_check)减少调用轮次。
1717
- 11. 当你发现用户在重复解释编码规范、操作约定或项目特有模式时,主动调用 suggest_skills 检查是否需要创建 Skill。如果有高优先级建议,向用户说明并在确认后调用 create_skill 创建。
1718
- 12. 当对话中出现值得长期记忆的信息(用户偏好、项目规范、关键决策、技术栈事实),在回复中嵌入记忆标签:\`[MEMORY:type] 内容 [/MEMORY]\`,type 可选 preference/decision/context。这些标签会被自动提取并持久化,不会显示给用户。`;
1719
- }
1384
+ // ─── Native Tool Calling 内部方法 ──────────────────────
1720
1385
 
1721
1386
  /**
1722
1387
  * 构建原生函数调用模式的系统提示词
@@ -1785,245 +1450,6 @@ submit_knowledge 的 code 字段必须是「项目特写」— 将技术的基
1785
1450
  - 高效利用步数 (≤${budget.maxIterations} 轮)`;
1786
1451
  }
1787
1452
 
1788
- /**
1789
- * 从 LLM 响应中解析 Action 块(单条)
1790
- *
1791
- * 兼容多家 AI 服务商的工具调用格式:
1792
- * 1. ```action {"tool":"...", "params":{...}} ``` — 标准格式
1793
- * 2. ```tool_code tool_name(key="value") ``` — Gemini 常用
1794
- * 3. ```python / ```javascript 围栏内函数调用 — 各家偶发
1795
- * 4. Action: tool_name / Action Input: {...} — ReAct (GPT/DeepSeek)
1796
- * 5. <tool_call>{"name":"...", "arguments":{...}}</tool_call> — 训练遗留 XML
1797
- * 6. ```json {"name":"...", "arguments":{...}} ``` — GPT function_call 文本化
1798
- * 7. {"tool":"...", "params":{...}} 裸 JSON — 通用降级
1799
- * 8. response 末尾裸函数调用 tool_name(key="value") — 通用降级
1800
- */
1801
- #parseAction(response) {
1802
- if (!response) return null;
1803
-
1804
- // ── 1. 标准 ```action {...} ``` ──
1805
- const blockMatch = response.match(/```action\s*\n?([\s\S]*?)```/);
1806
- if (blockMatch) {
1807
- const parsed = this.#tryParseToolJson(blockMatch[1].trim());
1808
- if (parsed) return parsed;
1809
- }
1810
-
1811
- // ── 2. ```tool_code fn(k=v) ``` (Gemini 常用) ──
1812
- const toolCodeMatch = response.match(/```tool_code\s*\n?([\s\S]*?)```/);
1813
- if (toolCodeMatch) {
1814
- const parsed = this.#parseToolCodeBlock(toolCodeMatch[1].trim());
1815
- if (parsed) return parsed;
1816
- }
1817
-
1818
- // ── 3. ```python / ```javascript / ```js 围栏内函数调用 ──
1819
- const langFenceMatch = response.match(/```(?:python|javascript|js|typescript|ts)\s*\n?([\s\S]*?)```/);
1820
- if (langFenceMatch) {
1821
- const inner = langFenceMatch[1].trim();
1822
- const parsed = this.#parseToolCodeBlock(inner);
1823
- if (parsed) return parsed;
1824
- // JS 对象字面量: tool_name({key: "value"})
1825
- const jsObjMatch = inner.match(/^(\w+)\(\s*(\{[\s\S]*\})\s*\)$/s);
1826
- if (jsObjMatch) {
1827
- const toolName = jsObjMatch[1];
1828
- if (this.#toolRegistry.has(toolName)) {
1829
- try {
1830
- let params;
1831
- try { params = JSON.parse(jsObjMatch[2]); } catch {
1832
- const normalized = jsObjMatch[2]
1833
- .replace(/,\s*([}\]])/g, '$1')
1834
- .replace(/'/g, '"')
1835
- .replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":');
1836
- params = JSON.parse(normalized);
1837
- }
1838
- return { tool: toolName, params };
1839
- } catch { /* parse failed */ }
1840
- }
1841
- }
1842
- }
1843
-
1844
- // ── 4. ReAct: Action: tool_name\nAction Input: {...} (GPT/DeepSeek) ──
1845
- const reactMatch = response.match(/Action\s*:\s*(\w+)\s*\n+Action\s*Input\s*:\s*([\s\S]*?)(?:\n\s*(?:Thought|Observation|$))/i);
1846
- if (reactMatch) {
1847
- const toolName = reactMatch[1];
1848
- if (this.#toolRegistry.has(toolName)) {
1849
- try {
1850
- return { tool: toolName, params: JSON.parse(reactMatch[2].trim()) };
1851
- } catch {
1852
- const parsed = this.#parseToolCodeBlock(`${toolName}(${reactMatch[2].trim()})`);
1853
- if (parsed) return parsed;
1854
- }
1855
- }
1856
- }
1857
- // Action/Action Input 在末尾(无后续 Thought)
1858
- const reactEndMatch = response.match(/Action\s*:\s*(\w+)\s*\n+Action\s*Input\s*:\s*(\{[\s\S]*\})\s*$/i);
1859
- if (reactEndMatch) {
1860
- const toolName = reactEndMatch[1];
1861
- if (this.#toolRegistry.has(toolName)) {
1862
- try { return { tool: toolName, params: JSON.parse(reactEndMatch[2].trim()) }; } catch { /* ignore */ }
1863
- }
1864
- }
1865
-
1866
- // ── 5. XML: <tool_call>...</tool_call> / <function_call>...</function_call> ──
1867
- const xmlMatch = response.match(/<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/);
1868
- if (xmlMatch) {
1869
- const parsed = this.#tryParseToolJson(xmlMatch[1].trim());
1870
- if (parsed) return parsed;
1871
- }
1872
- const fcMatch = response.match(/<function_call>\s*([\s\S]*?)\s*<\/function_call>/);
1873
- if (fcMatch) {
1874
- const parsed = this.#tryParseToolJson(fcMatch[1].trim());
1875
- if (parsed) return parsed;
1876
- }
1877
-
1878
- // ── 6. ```json {...} ``` 内的 function_call 格式 ──
1879
- const jsonFenceMatch = response.match(/```json\s*\n?([\s\S]*?)```/);
1880
- if (jsonFenceMatch) {
1881
- const parsed = this.#tryParseToolJson(jsonFenceMatch[1].trim());
1882
- if (parsed) return parsed;
1883
- }
1884
-
1885
- // ── 7. 裸 JSON: {"tool":"..."} 或 {"name":"..."} ──
1886
- const jsonMatch = response.match(/\{\s*"(?:tool|name|function)"\s*:\s*"([^"]+)"[\s\S]*?\}/);
1887
- if (jsonMatch) {
1888
- const parsed = this.#tryParseToolJson(jsonMatch[0]);
1889
- if (parsed) return parsed;
1890
- }
1891
-
1892
- // ── 8. 末尾裸函数调用: tool_name(key="value") ──
1893
- const trailingFnMatch = response.match(/\b(\w+)\(([^)]*)\)\s*$/);
1894
- if (trailingFnMatch) {
1895
- const parsed = this.#parseToolCodeBlock(`${trailingFnMatch[1]}(${trailingFnMatch[2]})`);
1896
- if (parsed) return parsed;
1897
- }
1898
-
1899
- return null;
1900
- }
1901
-
1902
- /**
1903
- * 尝试从 JSON 文本解析工具调用
1904
- * 兼容多种 key 命名:
1905
- * - {"tool": "x", "params": {...}} — 标准格式
1906
- * - {"name": "x", "arguments": {...}} — OpenAI function_call
1907
- * - {"function": "x", "parameters": {...}} — 变体
1908
- * - {"tool": "x", "input": {...}} — Claude 变体
1909
- */
1910
- #tryParseToolJson(text) {
1911
- if (!text) return null;
1912
- try {
1913
- const obj = JSON.parse(text);
1914
- const toolName = obj.tool || obj.name || obj.function;
1915
- if (!toolName || !this.#toolRegistry.has(toolName)) return null;
1916
- const params = obj.params || obj.arguments || obj.parameters || obj.input || {};
1917
- return { tool: toolName, params };
1918
- } catch { return null; }
1919
- }
1920
-
1921
- /**
1922
- * 解析 tool_code 函数调用格式
1923
- * 支持三种参数格式:
1924
- * 1. key=value: search_project_code(query="xxx", language="objc")
1925
- * 2. JSON 对象: read_project_file({"file_path": "Code/X.m"})
1926
- * 3. 单字符串: read_project_file("Code/X.m")
1927
- */
1928
- #parseToolCodeBlock(text) {
1929
- if (!text) return null;
1930
- const fnMatch = text.match(/^(\w+)\((.*)\)$/s);
1931
- if (!fnMatch) return null;
1932
-
1933
- const toolName = fnMatch[1];
1934
- if (!this.#toolRegistry.has(toolName)) return null;
1935
-
1936
- const argsStr = fnMatch[2].trim();
1937
- if (!argsStr) return { tool: toolName, params: {} };
1938
-
1939
- // 尝试 1: key=value 格式 (Python 风格)
1940
- const params = {};
1941
- const argRegex = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^,\s]+))/g;
1942
- let m;
1943
- while ((m = argRegex.exec(argsStr)) !== null) {
1944
- params[m[1]] = m[2] ?? m[3] ?? m[4];
1945
- }
1946
- if (Object.keys(params).length > 0) return { tool: toolName, params };
1947
-
1948
- // 尝试 2: JSON 对象参数 — read_project_file({"file_path": "..."})
1949
- if (argsStr.startsWith('{')) {
1950
- try {
1951
- const jsonParams = JSON.parse(argsStr);
1952
- if (typeof jsonParams === 'object' && jsonParams !== null) {
1953
- return { tool: toolName, params: jsonParams };
1954
- }
1955
- } catch { /* not valid JSON, fall through */ }
1956
- }
1957
-
1958
- // 尝试 3: 单字符串参数 — read_project_file("Code/X.m") → 映射到首个 required 参数
1959
- const strMatch = argsStr.match(/^["'](.+?)["']$/);
1960
- if (strMatch) {
1961
- const toolDef = this.#toolRegistry.getToolSchemas().find(t => t.name === toolName);
1962
- const firstRequired = toolDef?.parameters?.required?.[0];
1963
- if (firstRequired) {
1964
- return { tool: toolName, params: { [firstRequired]: strMatch[1] } };
1965
- }
1966
- }
1967
-
1968
- return { tool: toolName, params };
1969
- }
1970
-
1971
- /**
1972
- * 从 LLM 响应中解析 Action 块(支持批量)
1973
- *
1974
- * 优先匹配:
1975
- * ```batch_actions [...]```
1976
- * 降级匹配:
1977
- * - 多个 <tool_call> XML 标签
1978
- * - 多个 ReAct Action 块
1979
- * - 单条 #parseAction()
1980
- *
1981
- * @returns {Array<{tool:string, params:object}>|null}
1982
- */
1983
- #parseActions(response) {
1984
- if (!response) return null;
1985
-
1986
- // 1. 优先尝试 ```batch_actions``` 块
1987
- const batchMatch = response.match(/```batch_actions\s*\n?([\s\S]*?)```/);
1988
- if (batchMatch) {
1989
- try {
1990
- const arr = JSON.parse(batchMatch[1].trim());
1991
- if (Array.isArray(arr) && arr.length > 0) {
1992
- const valid = arr.filter(a => a.tool && this.#toolRegistry.has(a.tool));
1993
- if (valid.length > 0) {
1994
- return valid.map(a => ({ tool: a.tool, params: a.params || {} }));
1995
- }
1996
- }
1997
- } catch { /* batch parse failed, fall through */ }
1998
- }
1999
-
2000
- // 2. 多个 <tool_call> XML 块 (DeepSeek/Qwen)
2001
- const xmlMatches = [...response.matchAll(/<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g)];
2002
- if (xmlMatches.length > 1) {
2003
- const results = xmlMatches
2004
- .map(m => this.#tryParseToolJson(m[1].trim()))
2005
- .filter(Boolean);
2006
- if (results.length > 0) return results;
2007
- }
2008
-
2009
- // 3. 多个 ReAct Action 块
2010
- const reactMatches = [...response.matchAll(/Action\s*:\s*(\w+)\s*\n+Action\s*Input\s*:\s*(\{[\s\S]*?\})/gi)];
2011
- if (reactMatches.length > 1) {
2012
- const results = reactMatches
2013
- .map(m => {
2014
- const toolName = m[1];
2015
- if (!this.#toolRegistry.has(toolName)) return null;
2016
- try { return { tool: toolName, params: JSON.parse(m[2].trim()) }; } catch { return null; }
2017
- })
2018
- .filter(Boolean);
2019
- if (results.length > 0) return results;
2020
- }
2021
-
2022
- // 4. 降级到单 action
2023
- const single = this.#parseAction(response);
2024
- return single ? [single] : null;
2025
- }
2026
-
2027
1453
  /**
2028
1454
  * 清理最终回答(去除 Thought/preamble + MEMORY 标签)
2029
1455
  */
@@ -2036,42 +1462,6 @@ submit_knowledge 的 code 字段必须是「项目特写」— 将技术的基
2036
1462
  .trim();
2037
1463
  }
2038
1464
 
2039
- /**
2040
- * 检测 AI 回复是否为「未完成的中间步骤」— 输出分析/计划文本但未实际调用工具
2041
- *
2042
- * Gemini 常见行为: 收到 production prompt 后先输出一段纯文本的
2043
- * "执行计划" 或 "信号审视" 而不包含任何 action/tool_code block,
2044
- * 导致 #parseActions() 返回 null,被误判为 final answer。
2045
- *
2046
- * 检测策略: 回复包含计划/分析关键词 + 不包含 dimensionDigest JSON
2047
- */
2048
- #looksLikeIncompleteStep(response) {
2049
- if (!response || response.length < 100) return false;
2050
-
2051
- // 如果已包含 dimensionDigest → 是真正的最终回答
2052
- if (response.includes('"dimensionDigest"') || response.includes('dimensionDigest')) return false;
2053
-
2054
- // 计划/分析性关键词 (中文 Gemini 常用)
2055
- const planningPatterns = [
2056
- /制定执行计划/,
2057
- /信号质量预判/,
2058
- /执行计划/,
2059
- /我将按照/,
2060
- /开始分析/,
2061
- /我将分析/,
2062
- /接下来我将/,
2063
- /我来分析/,
2064
- /首先[,,]?\s*我/,
2065
- /\*\*0\.\s*制定/,
2066
- /\*\*Signal\s+\d+/, // 信号列表分析
2067
- /质量[::]\s*(高|中|低)/, // 信号质量评估
2068
- /保留[。;]|丢弃[。;]|跳过[。;]/, // 信号去留判断
2069
- ];
2070
-
2071
- const matchCount = planningPatterns.filter(p => p.test(response)).length;
2072
- return matchCount >= 2; // 至少匹配 2 个模式才认为是计划性回复
2073
- }
2074
-
2075
1465
  /**
2076
1466
  * 获取工具执行上下文
2077
1467
  * @param {object} [extras] — 额外注入到上下文的字段(如 _sessionToolCalls)
@@ -2305,54 +1695,6 @@ submit_knowledge 的 code 字段必须是「项目特写」— 将技术的基
2305
1695
  }
2306
1696
  }
2307
1697
 
2308
- /**
2309
- * Context Window 自动压缩(受 Cline AutoCondense 启发)
2310
- *
2311
- * 在 ReAct 循环中实时检测消息总 token 数。
2312
- * 当超过 TOKEN_BUDGET 时,保留:
2313
- * - 首条消息(可能是 system / 用户首问)
2314
- * - 最后 4 条消息(当前推理上下文)
2315
- * 中间消息压缩为一条摘要。
2316
- *
2317
- * 策略: 非阻塞、纯规则(不调 AI),避免 ReAct 循环内引入额外 AI 调用。
2318
- */
2319
- #condenseIfNeeded(messages, tokenBudget = 10000) {
2320
- const estimateTokens = (text) => Math.ceil((text || '').length / 3.5);
2321
-
2322
- let totalTokens = 0;
2323
- for (const m of messages) totalTokens += estimateTokens(m.content);
2324
-
2325
- if (totalTokens <= tokenBudget || messages.length <= 6) return;
2326
-
2327
- // 保留首条 + 最后 4 条,压缩中间
2328
- const keepTail = 4;
2329
- const first = messages[0];
2330
- const tail = messages.slice(-keepTail);
2331
- const middle = messages.slice(1, -keepTail);
2332
-
2333
- if (middle.length === 0) return;
2334
-
2335
- // 生成摘要
2336
- const toolCallSummary = middle
2337
- .filter(m => m.role === 'user' && m.content.startsWith('Observation from tool'))
2338
- .map(m => {
2339
- const toolMatch = m.content.match(/Observation from tool "([^"]+)"/);
2340
- return toolMatch ? toolMatch[1] : null;
2341
- })
2342
- .filter(Boolean);
2343
-
2344
- const condensed = {
2345
- role: 'system',
2346
- content: `[上下文压缩] 省略了 ${middle.length} 条中间消息(含工具调用: ${toolCallSummary.join(', ') || '无'})。请基于最近的 observation 继续推理。`,
2347
- };
2348
-
2349
- // 原地修改数组
2350
- messages.length = 0;
2351
- messages.push(first, condensed, ...tail);
2352
-
2353
- this.#logger.debug(`[ChatAgent] condensed ${middle.length} messages (${totalTokens} → ~${estimateTokens(first.content) + estimateTokens(condensed.content) + tail.reduce((s, m) => s + estimateTokens(m.content), 0)} tokens)`);
2354
- }
2355
-
2356
1698
  /**
2357
1699
  * 截断长文本
2358
1700
  */