autosnippet 2.8.2 → 2.9.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.
- package/README.md +1 -1
- package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-CH-H9x0E.js} +1 -1
- package/dashboard/dist/assets/index-CqJRvYRL.js +197 -0
- package/dashboard/dist/assets/index-DICm9PNa.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/SetupService.js +1 -1
- package/lib/core/ast/ProjectGraph.js +160 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +1 -0
- package/lib/external/mcp/handlers/bootstrap.js +33 -36
- package/lib/external/mcp/handlers/skill.js +4 -2
- package/lib/external/mcp/handlers/system.js +3 -3
- package/lib/http/middleware/requestLogger.js +3 -3
- package/lib/http/routes/ai.js +17 -1
- package/lib/http/routes/skills.js +44 -1
- package/lib/infrastructure/cache/GraphCache.js +143 -0
- package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
- package/lib/infrastructure/realtime/RealtimeService.js +14 -2
- package/lib/injection/ServiceContainer.js +114 -2
- package/lib/repository/token/TokenUsageStore.js +162 -0
- package/lib/service/candidate/CandidateService.js +28 -0
- package/lib/service/chat/AnalystAgent.js +25 -14
- package/lib/service/chat/ChatAgent.js +237 -6
- package/lib/service/chat/ContextWindow.js +87 -3
- package/lib/service/chat/HandoffProtocol.js +26 -1
- package/lib/service/chat/ProducerAgent.js +4 -2
- package/lib/service/chat/tools.js +168 -71
- package/lib/service/skills/SignalCollector.js +3 -2
- package/lib/service/spm/SpmService.js +119 -18
- package/package.json +1 -1
- package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
- package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
|
@@ -19,20 +19,31 @@ import Logger from '../../infrastructure/logging/Logger.js';
|
|
|
19
19
|
// System Prompt — Analyst 专用 (~100 tokens)
|
|
20
20
|
// ──────────────────────────────────────────────────────────────────
|
|
21
21
|
|
|
22
|
-
const ANALYST_SYSTEM_PROMPT =
|
|
22
|
+
const ANALYST_SYSTEM_PROMPT = `你是一位高级软件架构师,正在深度分析一个真实项目的某个维度。
|
|
23
23
|
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
这些工具返回精确的类继承、协议实现、方法签名信息,比文本搜索更高效。
|
|
27
|
-
2. **文本搜索补充** — search_project_code 用于查找特定模式/关键字
|
|
28
|
-
3. **文件阅读验证** — read_project_file 用于确认具体实现细节
|
|
24
|
+
## 执行计划
|
|
25
|
+
你有 **N 轮**工具调用机会(系统会告知具体数字)。请严格按以下节奏分配:
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
| 阶段 | 轮次占比 | 目标 |
|
|
28
|
+
|------|---------|------|
|
|
29
|
+
| 1. 全局扫描 | 第 1-3 轮 | get_project_overview + list_project_structure 了解项目结构 |
|
|
30
|
+
| 2. 结构化探索 | 第 4-N×60% 轮 | get_class_hierarchy / get_class_info 理解核心类;search_project_code 批量搜索关键模式 |
|
|
31
|
+
| 3. 深度验证 | 第 N×60%-N×80% 轮 | read_project_file 阅读关键实现,确认细节 |
|
|
32
|
+
| 4. 输出总结 | 最后 20% | **停止调用工具**,直接输出你的分析文本 |
|
|
32
33
|
|
|
34
|
+
## 关键规则
|
|
35
|
+
- **到达 80% 轮次时必须开始写总结**,不要等系统提醒
|
|
36
|
+
- 每一轮都必须调用工具获取新信息,不要花轮次在纯文本思考上
|
|
37
|
+
- 不要重复搜索相同关键词或读取相同文件(系统会返回缓存并扣轮次)
|
|
38
|
+
|
|
39
|
+
## 工具效率
|
|
40
|
+
- **批量搜索**: search_project_code({ patterns: ["keywordA", "keywordB", "keywordC"] }) — 一次搜 3-5 个
|
|
41
|
+
- **批量读文件**: read_project_file({ filePaths: ["a.m", "b.m", "c.m"] }) — 一次读 3-5 个
|
|
42
|
+
- **结构化查询优先**: get_class_hierarchy / get_class_info 比文本搜索更精确高效
|
|
43
|
+
|
|
44
|
+
## 输出要求
|
|
33
45
|
输出你的分析发现,包括具体的文件路径和代码位置。
|
|
34
|
-
|
|
35
|
-
尽可能多地使用工具来获取准确信息,不要猜测。`;
|
|
46
|
+
用自然语言描述你的理解,不需要特定格式。`;
|
|
36
47
|
|
|
37
48
|
// ──────────────────────────────────────────────────────────────────
|
|
38
49
|
// Analyst 可用工具白名单 — 只做探索,不做提交
|
|
@@ -61,12 +72,12 @@ const ANALYST_TOOLS = [
|
|
|
61
72
|
// ──────────────────────────────────────────────────────────────────
|
|
62
73
|
|
|
63
74
|
const ANALYST_BUDGET = {
|
|
64
|
-
maxIterations:
|
|
65
|
-
searchBudget:
|
|
66
|
-
searchBudgetGrace:
|
|
75
|
+
maxIterations: 24, // was 18 — 大项目维度需要充足探索轮次
|
|
76
|
+
searchBudget: 18, // was 14 — 匹配更大探索空间
|
|
77
|
+
searchBudgetGrace: 10, // was 8
|
|
67
78
|
maxSubmits: 0, // Analyst 不提交候选
|
|
68
79
|
softSubmitLimit: 0,
|
|
69
|
-
idleRoundsToExit: 2, //
|
|
80
|
+
idleRoundsToExit: 2, // 减少空转
|
|
70
81
|
};
|
|
71
82
|
|
|
72
83
|
// ──────────────────────────────────────────────────────────────────
|
|
@@ -253,6 +253,31 @@ export class ChatAgent {
|
|
|
253
253
|
// 附加 token 用量统计
|
|
254
254
|
result.tokenUsage = { ...this.#currentTokenUsage };
|
|
255
255
|
|
|
256
|
+
// 持久化 token 消耗到数据库(fire-and-forget)
|
|
257
|
+
try {
|
|
258
|
+
const tokenStore = this.#container?.get?.('tokenUsageStore');
|
|
259
|
+
if (tokenStore) {
|
|
260
|
+
const aiProvider = this.#aiProvider;
|
|
261
|
+
tokenStore.record({
|
|
262
|
+
source: source || 'unknown',
|
|
263
|
+
dimension: dimensionId || dimensionMeta?.id || null,
|
|
264
|
+
provider: aiProvider?.name || null,
|
|
265
|
+
model: aiProvider?.model || null,
|
|
266
|
+
inputTokens: this.#currentTokenUsage.input,
|
|
267
|
+
outputTokens: this.#currentTokenUsage.output,
|
|
268
|
+
durationMs: Date.now() - execStartTime,
|
|
269
|
+
toolCalls: result.toolCalls?.length || 0,
|
|
270
|
+
sessionId: conversationId || null,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// 通知前端 token 用量变化
|
|
274
|
+
try {
|
|
275
|
+
const realtime = this.#container?.get?.('realtimeService');
|
|
276
|
+
realtime?.broadcastTokenUsageUpdated?.();
|
|
277
|
+
} catch { /* optional */ }
|
|
278
|
+
}
|
|
279
|
+
} catch { /* token logging should never break execution */ }
|
|
280
|
+
|
|
256
281
|
return { ...result, conversationId };
|
|
257
282
|
}
|
|
258
283
|
|
|
@@ -306,7 +331,13 @@ export class ChatAgent {
|
|
|
306
331
|
const phaseRouter = (isSystem && !disablePhaseRouter) ? new PhaseRouter(budget, isSkillOnly) : null;
|
|
307
332
|
|
|
308
333
|
// ── 系统提示词 (支持外部覆盖) ──
|
|
309
|
-
|
|
334
|
+
let baseSystemPrompt = systemPromptOverride || this.#buildNativeToolSystemPrompt(budget);
|
|
335
|
+
// 统一注入轮次预算,确保 AI 始终知道具体数字和节奏
|
|
336
|
+
if (isSystem && !baseSystemPrompt.includes('轮次预算')) {
|
|
337
|
+
const exploreEnd = Math.floor(budget.maxIterations * 0.6);
|
|
338
|
+
const verifyEnd = Math.floor(budget.maxIterations * 0.8);
|
|
339
|
+
baseSystemPrompt += `\n\n## 轮次预算\n- 总轮次: **${budget.maxIterations} 轮**\n- 探索阶段: 第 1-${exploreEnd} 轮(搜索和结构化查询)\n- 验证阶段: 第 ${exploreEnd + 1}-${verifyEnd} 轮(读取关键文件确认细节)\n- 总结阶段: 第 ${verifyEnd + 1}-${budget.maxIterations} 轮(**停止工具调用,输出分析文本**)\n\n到达第 ${verifyEnd} 轮时你必须开始输出总结,不要继续搜索。`;
|
|
340
|
+
}
|
|
310
341
|
|
|
311
342
|
// Bootstrap 场景限制可用工具集 (支持外部覆盖)
|
|
312
343
|
const effectiveAllowedTools = allowedTools || (isSystem ? [
|
|
@@ -328,6 +359,20 @@ export class ChatAgent {
|
|
|
328
359
|
const submittedTitles = new Set(this.#globalSubmittedTitles);
|
|
329
360
|
const sharedState = {}; // P2.2: 跨工具调用共享状态(搜索计数器等)
|
|
330
361
|
|
|
362
|
+
// ── 进度感知收敛 (Analyst 专用: disablePhaseRouter=true) ──
|
|
363
|
+
// 追踪搜索/读取新信息的效率,当探索饱和时提前引导 AI 收敛
|
|
364
|
+
const explorationMetrics = {
|
|
365
|
+
uniqueFiles: new Set(), // 已读取的唯一文件
|
|
366
|
+
uniquePatterns: new Set(), // 已搜索的唯一 pattern
|
|
367
|
+
uniqueQueries: new Set(), // 已查询的唯一类名/目录(AST + list_project_structure)
|
|
368
|
+
totalToolCalls: 0, // 总工具调用数
|
|
369
|
+
newInfoRounds: 0, // 最近 N 轮中获取到新信息的轮次
|
|
370
|
+
staleRounds: 0, // 连续未获取新信息的轮次
|
|
371
|
+
convergenceNudged: false, // 是否已注入收敛 nudge
|
|
372
|
+
};
|
|
373
|
+
const MIN_EXPLORE_ITERS = 16; // 最少探索轮次(冷启动质量保障)
|
|
374
|
+
const STALE_THRESHOLD = 3; // 连续无新信息轮次触发收敛
|
|
375
|
+
|
|
331
376
|
// ── 主循环 ──
|
|
332
377
|
while (true) {
|
|
333
378
|
// PhaseRouter tick + 退出检查
|
|
@@ -337,9 +382,53 @@ export class ChatAgent {
|
|
|
337
382
|
this.#logger.info(`[ChatAgent] PhaseRouter exit: phase=${phaseRouter.phase}, iter=${phaseRouter.totalIterations}, submits=${phaseRouter.totalSubmits}`);
|
|
338
383
|
break;
|
|
339
384
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
385
|
+
// PhaseRouter 因 maxIterations 强制转入 SUMMARIZE → 注入收尾 nudge
|
|
386
|
+
if (phaseRouter.consumeForcedSummarize()) {
|
|
387
|
+
const submitCount = toolCalls.filter(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check').length;
|
|
388
|
+
ctx.appendUserNudge(
|
|
389
|
+
`⚠️ 轮次即将耗尽 (${phaseRouter.totalIterations}/${budget.maxIterations}),**必须立即结束**。请在回复中直接输出 dimensionDigest JSON 总结(用 \`\`\`json 包裹),不要再调用任何工具。\n` +
|
|
390
|
+
`\`\`\`json\n{"dimensionDigest":{"summary":"分析总结","candidateCount":${submitCount},"keyFindings":["发现"],"crossRefs":{},"gaps":["缺口"],"remainingTasks":[{"signal":"未处理信号","reason":"轮次耗尽","priority":"high","searchHints":["搜索词"]}]}}\n\`\`\`\n> remainingTasks: 列出未来得及处理的信号。已覆盖则留空 \`[]\`。`
|
|
391
|
+
);
|
|
392
|
+
this.#logger.info('[ChatAgent] 📝 injected forced SUMMARIZE nudge (maxIterations reached)');
|
|
393
|
+
}
|
|
394
|
+
} else if (isSystem && !phaseRouter && !ctx.__gracefulExitInjected && !explorationMetrics.convergenceNudged
|
|
395
|
+
&& iterationCount >= MIN_EXPLORE_ITERS && explorationMetrics.staleRounds >= STALE_THRESHOLD) {
|
|
396
|
+
// ── 进度感知收敛:探索已饱和,提前引导 AI 总结 ──
|
|
397
|
+
this.#logger.info(
|
|
398
|
+
`[ChatAgent] 📊 Exploration saturated at iter ${iterationCount}/${maxIter} — ` +
|
|
399
|
+
`files=${explorationMetrics.uniqueFiles.size}, patterns=${explorationMetrics.uniquePatterns.size}, ` +
|
|
400
|
+
`queries=${explorationMetrics.uniqueQueries.size}, staleRounds=${explorationMetrics.staleRounds} — nudging convergence`
|
|
401
|
+
);
|
|
402
|
+
ctx.appendUserNudge(
|
|
403
|
+
`你已经充分探索了项目代码(${explorationMetrics.uniqueFiles.size} 个文件,${explorationMetrics.uniquePatterns.size} 次不同搜索,${explorationMetrics.uniqueQueries.size} 次结构化查询)。` +
|
|
404
|
+
`最近 ${explorationMetrics.staleRounds} 轮没有发现新信息,建议开始撰写分析总结。\n` +
|
|
405
|
+
`如果你确信还有重要方面未覆盖,可以继续探索(剩余 ${maxIter - iterationCount} 轮);否则请直接输出你的分析发现。`
|
|
406
|
+
);
|
|
407
|
+
explorationMetrics.convergenceNudged = true;
|
|
408
|
+
// 不 break,不设 toolChoice=none — 这是软 nudge,AI 仍可继续探索
|
|
409
|
+
} else if (isSystem && !phaseRouter && !ctx.__budgetWarningInjected
|
|
410
|
+
&& iterationCount >= Math.floor(maxIter * 0.75)) {
|
|
411
|
+
// ── 预算感知提醒:75% 预算消耗时轻量提示 ──
|
|
412
|
+
ctx.appendUserNudge(
|
|
413
|
+
`📌 进度提醒:你已使用 ${iterationCount}/${maxIter} 轮次(${Math.round(iterationCount / maxIter * 100)}%)。` +
|
|
414
|
+
`请确保核心方面已覆盖,开始准备总结。剩余 ${maxIter - iterationCount} 轮,优先填补最重要的分析空白。`
|
|
415
|
+
);
|
|
416
|
+
ctx.__budgetWarningInjected = true;
|
|
417
|
+
this.#logger.info(`[ChatAgent] 📌 Budget warning at ${iterationCount}/${maxIter} (${Math.round(iterationCount / maxIter * 100)}%)`);
|
|
418
|
+
} else if (isSystem && iterationCount >= maxIter && !ctx.__gracefulExitInjected) {
|
|
419
|
+
// 达到上限 → 注入收尾消息让 AI 快速总结(而非硬中断)
|
|
420
|
+
this.#logger.info(`[ChatAgent] Iteration cap reached (${iterationCount}/${maxIter}) — injecting graceful exit nudge`);
|
|
421
|
+
const submitCount = toolCalls.filter(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check').length;
|
|
422
|
+
ctx.appendUserNudge(
|
|
423
|
+
`⚠️ 你已使用 ${iterationCount}/${maxIter} 轮次,**必须立即结束**。请在回复中直接输出 dimensionDigest JSON 总结(用 \`\`\`json 包裹),不要再调用任何工具。\n` +
|
|
424
|
+
`\`\`\`json\n{"dimensionDigest":{"summary":"分析总结","candidateCount":${submitCount},"keyFindings":["发现"],"crossRefs":{},"gaps":["缺口"],"remainingTasks":[{"signal":"未处理信号","reason":"轮次耗尽","priority":"high","searchHints":["搜索词"]}]}}\n\`\`\`\n> remainingTasks: 列出未来得及处理的信号。已覆盖则留空 \`[]\`。`
|
|
425
|
+
);
|
|
426
|
+
ctx.__gracefulExitInjected = true;
|
|
427
|
+
// 不 break,给 AI 2 轮 grace 来产出总结
|
|
428
|
+
// 继续 while 循环
|
|
429
|
+
} else if (isSystem && iterationCount >= maxIter + 2) {
|
|
430
|
+
// 硬上限兜底:grace 轮次也耗尽
|
|
431
|
+
this.#logger.info(`[ChatAgent] Hard cap reached: ${iterationCount}/${maxIter + 2}`);
|
|
343
432
|
break;
|
|
344
433
|
} else if (!isSystem && ctx.length > maxIter * 2 + 2) {
|
|
345
434
|
// 用户对话模式: 简单的消息数限制
|
|
@@ -352,7 +441,10 @@ export class ChatAgent {
|
|
|
352
441
|
|
|
353
442
|
// ── 动态 toolChoice (由 PhaseRouter 决定) ──
|
|
354
443
|
let currentChoice;
|
|
355
|
-
if (
|
|
444
|
+
if (ctx.__gracefulExitInjected) {
|
|
445
|
+
// graceful exit: 禁止工具调用,强制 AI 输出文本总结
|
|
446
|
+
currentChoice = 'none';
|
|
447
|
+
} else if (phaseRouter) {
|
|
356
448
|
currentChoice = phaseRouter.getToolChoice();
|
|
357
449
|
} else {
|
|
358
450
|
currentChoice = 'auto';
|
|
@@ -371,6 +463,19 @@ export class ChatAgent {
|
|
|
371
463
|
if (hint) {
|
|
372
464
|
systemPrompt += `\n\n## 当前状态\n${hint}`;
|
|
373
465
|
}
|
|
466
|
+
} else if (isSystem) {
|
|
467
|
+
// 非 PhaseRouter 路径:注入实时轮次计数器,让 AI 始终感知进度
|
|
468
|
+
const verifyStart = Math.floor(maxIter * 0.8);
|
|
469
|
+
const remaining = maxIter - iterationCount;
|
|
470
|
+
let phaseLabel;
|
|
471
|
+
if (iterationCount <= Math.floor(maxIter * 0.6)) {
|
|
472
|
+
phaseLabel = '探索阶段';
|
|
473
|
+
} else if (iterationCount <= verifyStart) {
|
|
474
|
+
phaseLabel = '验证阶段';
|
|
475
|
+
} else {
|
|
476
|
+
phaseLabel = '⚠ 总结阶段 — 请停止工具调用,直接输出分析文本';
|
|
477
|
+
}
|
|
478
|
+
systemPrompt += `\n\n## 当前进度\n第 ${iterationCount}/${maxIter} 轮 | ${phaseLabel} | 剩余 ${remaining} 轮`;
|
|
374
479
|
}
|
|
375
480
|
|
|
376
481
|
// ── AI 调用 ──
|
|
@@ -438,6 +543,20 @@ export class ChatAgent {
|
|
|
438
543
|
|
|
439
544
|
// ── 处理 functionCalls ──
|
|
440
545
|
if (aiResult.functionCalls && aiResult.functionCalls.length > 0) {
|
|
546
|
+
// ── Graceful exit 保护: Gemini 有时会无视 toolChoice='none' 继续返回工具调用
|
|
547
|
+
// 强制忽略工具调用,将附带文本视为最终回复
|
|
548
|
+
if (ctx.__gracefulExitInjected) {
|
|
549
|
+
this.#logger.warn(`[ChatAgent] ⚠ AI returned ${aiResult.functionCalls.length} tool calls despite toolChoice=none (graceful exit) — ignoring tools, treating as text`);
|
|
550
|
+
if (aiResult.text) {
|
|
551
|
+
const reply = this.#cleanFinalAnswer(aiResult.text);
|
|
552
|
+
const totalDuration = Date.now() - execStartTime;
|
|
553
|
+
this.#logger.info(`[ChatAgent] ✅ final answer (graceful exit, forced) — ${reply.length} chars, ${toolCalls.length} tool calls, ${totalDuration}ms`);
|
|
554
|
+
return { reply, toolCalls, hasContext: toolCalls.length > 0 };
|
|
555
|
+
}
|
|
556
|
+
// 无文本时继续循环,下一轮硬上限兜底
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
|
|
441
560
|
// 限制单次工具调用数量(防上下文溢出)
|
|
442
561
|
const MAX_TOOL_CALLS_PER_ITER = 8;
|
|
443
562
|
let activeCalls = aiResult.functionCalls;
|
|
@@ -450,6 +569,7 @@ export class ChatAgent {
|
|
|
450
569
|
ctx.appendAssistantWithToolCalls(aiResult.text || null, activeCalls);
|
|
451
570
|
|
|
452
571
|
let roundSubmitCount = 0;
|
|
572
|
+
let roundHasNewInfo = false; // 本轮是否获取到新信息
|
|
453
573
|
|
|
454
574
|
for (const fc of activeCalls) {
|
|
455
575
|
const toolStartTime = Date.now();
|
|
@@ -474,6 +594,99 @@ export class ChatAgent {
|
|
|
474
594
|
const summarized = this.#summarizeResult(toolResult);
|
|
475
595
|
toolCalls.push({ tool: fc.name, params: fc.args, result: summarized });
|
|
476
596
|
|
|
597
|
+
// ── 探索进度追踪 (非 PhaseRouter 路径) ──
|
|
598
|
+
if (!phaseRouter && isSystem) {
|
|
599
|
+
explorationMetrics.totalToolCalls++;
|
|
600
|
+
let foundNewInfo = false;
|
|
601
|
+
|
|
602
|
+
if (fc.name === 'search_project_code') {
|
|
603
|
+
const pattern = fc.args?.pattern || '';
|
|
604
|
+
const patterns = fc.args?.patterns || [];
|
|
605
|
+
// 单模式
|
|
606
|
+
if (pattern && !explorationMetrics.uniquePatterns.has(pattern)) {
|
|
607
|
+
explorationMetrics.uniquePatterns.add(pattern);
|
|
608
|
+
foundNewInfo = true;
|
|
609
|
+
}
|
|
610
|
+
// 批量模式
|
|
611
|
+
for (const p of patterns) {
|
|
612
|
+
if (!explorationMetrics.uniquePatterns.has(p)) {
|
|
613
|
+
explorationMetrics.uniquePatterns.add(p);
|
|
614
|
+
foundNewInfo = true;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// 检查搜索结果是否有新文件
|
|
618
|
+
if (toolResult && typeof toolResult === 'object') {
|
|
619
|
+
const matches = toolResult.matches || [];
|
|
620
|
+
const batchResults = toolResult.batchResults || {};
|
|
621
|
+
for (const m of matches) {
|
|
622
|
+
if (m.file && !explorationMetrics.uniqueFiles.has(m.file)) {
|
|
623
|
+
explorationMetrics.uniqueFiles.add(m.file);
|
|
624
|
+
foundNewInfo = true;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
for (const sub of Object.values(batchResults)) {
|
|
628
|
+
for (const m of (sub.matches || [])) {
|
|
629
|
+
if (m.file && !explorationMetrics.uniqueFiles.has(m.file)) {
|
|
630
|
+
explorationMetrics.uniqueFiles.add(m.file);
|
|
631
|
+
foundNewInfo = true;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} else if (fc.name === 'read_project_file') {
|
|
637
|
+
const fp = fc.args?.filePath || '';
|
|
638
|
+
const fps = fc.args?.filePaths || [];
|
|
639
|
+
if (fp && !explorationMetrics.uniqueFiles.has(fp)) {
|
|
640
|
+
explorationMetrics.uniqueFiles.add(fp);
|
|
641
|
+
foundNewInfo = true;
|
|
642
|
+
}
|
|
643
|
+
for (const f of fps) {
|
|
644
|
+
if (!explorationMetrics.uniqueFiles.has(f)) {
|
|
645
|
+
explorationMetrics.uniqueFiles.add(f);
|
|
646
|
+
foundNewInfo = true;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} else if (fc.name === 'list_project_structure') {
|
|
650
|
+
// 目录结构:同一目录只算一次新信息
|
|
651
|
+
const dir = fc.args?.directory || '/';
|
|
652
|
+
const qKey = `list:${dir}`;
|
|
653
|
+
if (!explorationMetrics.uniqueQueries.has(qKey)) {
|
|
654
|
+
explorationMetrics.uniqueQueries.add(qKey);
|
|
655
|
+
foundNewInfo = true;
|
|
656
|
+
}
|
|
657
|
+
} else if (fc.name === 'get_class_info' || fc.name === 'get_class_hierarchy'
|
|
658
|
+
|| fc.name === 'get_protocol_info' || fc.name === 'get_method_overrides'
|
|
659
|
+
|| fc.name === 'get_category_map') {
|
|
660
|
+
// AST 结构化查询:同一类名/协议名只算一次新信息
|
|
661
|
+
const queryTarget = fc.args?.className || fc.args?.protocolName || fc.args?.name || '';
|
|
662
|
+
const qKey = `${fc.name}:${queryTarget}`;
|
|
663
|
+
if (!explorationMetrics.uniqueQueries.has(qKey)) {
|
|
664
|
+
explorationMetrics.uniqueQueries.add(qKey);
|
|
665
|
+
foundNewInfo = true;
|
|
666
|
+
}
|
|
667
|
+
} else if (fc.name === 'get_project_overview') {
|
|
668
|
+
// 项目概览只需调一次
|
|
669
|
+
const qKey = 'overview';
|
|
670
|
+
if (!explorationMetrics.uniqueQueries.has(qKey)) {
|
|
671
|
+
explorationMetrics.uniqueQueries.add(qKey);
|
|
672
|
+
foundNewInfo = true;
|
|
673
|
+
}
|
|
674
|
+
} else if (fc.name !== 'submit_candidate' && fc.name !== 'submit_with_check') {
|
|
675
|
+
// 其他未分类工具 — 首次算新信息,之后同工具名+同参数去重
|
|
676
|
+
const qKey = `${fc.name}:${JSON.stringify(fc.args || {}).substring(0, 80)}`;
|
|
677
|
+
if (!explorationMetrics.uniqueQueries.has(qKey)) {
|
|
678
|
+
explorationMetrics.uniqueQueries.add(qKey);
|
|
679
|
+
foundNewInfo = true;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (foundNewInfo) {
|
|
684
|
+
explorationMetrics.staleRounds = 0;
|
|
685
|
+
explorationMetrics.newInfoRounds++;
|
|
686
|
+
roundHasNewInfo = true;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
477
690
|
// ── Layer 3: ToolResultLimiter — 动态配额压缩 ──
|
|
478
691
|
const quota = ctx.getToolResultQuota();
|
|
479
692
|
let resultStr = limitToolResult(fc.name, toolResult, quota);
|
|
@@ -503,6 +716,11 @@ export class ChatAgent {
|
|
|
503
716
|
ctx.appendToolResult(fc.id, fc.name, resultStr);
|
|
504
717
|
}
|
|
505
718
|
|
|
719
|
+
// ── 探索饱和度更新 (非 PhaseRouter 路径) ──
|
|
720
|
+
if (!phaseRouter && isSystem && !roundHasNewInfo) {
|
|
721
|
+
explorationMetrics.staleRounds++;
|
|
722
|
+
}
|
|
723
|
+
|
|
506
724
|
// ── PhaseRouter 更新 ──
|
|
507
725
|
if (phaseRouter) {
|
|
508
726
|
const transition = phaseRouter.update({
|
|
@@ -602,7 +820,15 @@ export class ChatAgent {
|
|
|
602
820
|
continue;
|
|
603
821
|
}
|
|
604
822
|
|
|
605
|
-
//
|
|
823
|
+
// 用户对话 / graceful exit: 文字回答即最终回答
|
|
824
|
+
if (ctx.__gracefulExitInjected || !isSystem) {
|
|
825
|
+
const reply = this.#cleanFinalAnswer(aiResult.text || '');
|
|
826
|
+
const totalDuration = Date.now() - execStartTime;
|
|
827
|
+
this.#logger.info(`[ChatAgent] ✅ final answer (${ctx.__gracefulExitInjected ? 'graceful exit' : 'user'}) — ${reply.length} chars, ${toolCalls.length} tool calls, ${totalDuration}ms`);
|
|
828
|
+
return { reply, toolCalls, hasContext: toolCalls.length > 0 };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// system 模式非 graceful exit 的文字回答(理论上不应到这里)
|
|
606
832
|
const reply = this.#cleanFinalAnswer(aiResult.text || '');
|
|
607
833
|
const totalDuration = Date.now() - execStartTime;
|
|
608
834
|
this.#logger.info(`[ChatAgent] ✅ final answer — ${reply.length} chars, ${toolCalls.length} tool calls, ${totalDuration}ms`);
|
|
@@ -1392,6 +1618,11 @@ ${this.#projectBriefingCache}
|
|
|
1392
1618
|
4. **语义发现** → semantic_search_code 在知识库查找相关知识
|
|
1393
1619
|
5. **知识产出** → submit_candidate 提交有价值的发现
|
|
1394
1620
|
|
|
1621
|
+
## 高效使用工具(节省轮次)
|
|
1622
|
+
- **批量搜索**: search_project_code({ patterns: ["keywordA", "keywordB", "keywordC"] })
|
|
1623
|
+
- **批量读文件**: read_project_file({ filePaths: ["path/a.m", "path/b.m"] })
|
|
1624
|
+
- 合并同类请求为一次调用,避免逐个搜索/读取浪费轮次。
|
|
1625
|
+
|
|
1395
1626
|
## 「项目特写」= 基本用法 + 项目特征融合
|
|
1396
1627
|
submit_candidate 的 code 字段必须是「项目特写」— 将技术的基本用法与本项目的特征融合为一体:
|
|
1397
1628
|
1. **项目选择了什么**: 采用了哪种写法/模式/约定
|
|
@@ -399,13 +399,27 @@ export function limitToolResult(toolName, result, quota) {
|
|
|
399
399
|
return raw.length > 500 ? raw.substring(0, 500) : raw;
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
// search_project_code: 限制匹配数 +
|
|
402
|
+
// search_project_code: 限制匹配数 + 截断上下文(支持批量模式)
|
|
403
403
|
if (toolName === 'search_project_code') {
|
|
404
|
+
if (result && typeof result === 'object' && result.batchResults) {
|
|
405
|
+
// 批量模式:对每个 pattern 的结果独立限制(直接操作对象,避免 stringify→parse 往返)
|
|
406
|
+
const limited = { ...result };
|
|
407
|
+
const perKeyChars = Math.floor(maxChars / Object.keys(limited.batchResults).length);
|
|
408
|
+
for (const [key, sub] of Object.entries(limited.batchResults)) {
|
|
409
|
+
limited.batchResults[key] = limitSearchResultObj(sub, Math.min(maxMatches, 3), perKeyChars);
|
|
410
|
+
}
|
|
411
|
+
const raw = JSON.stringify(limited);
|
|
412
|
+
return raw.length > maxChars ? raw.substring(0, maxChars) + '\n... [batch truncated]' : raw;
|
|
413
|
+
}
|
|
404
414
|
return limitSearchResult(result, maxMatches, maxChars);
|
|
405
415
|
}
|
|
406
416
|
|
|
407
|
-
// read_project_file:
|
|
417
|
+
// read_project_file: 限制字符数(支持批量模式)
|
|
408
418
|
if (toolName === 'read_project_file') {
|
|
419
|
+
if (result && typeof result === 'object' && result.batchResults) {
|
|
420
|
+
const raw = JSON.stringify(result);
|
|
421
|
+
return raw.length > maxChars ? raw.substring(0, maxChars) + '\n... [batch truncated]' : raw;
|
|
422
|
+
}
|
|
409
423
|
return limitFileContent(result, maxChars);
|
|
410
424
|
}
|
|
411
425
|
|
|
@@ -465,6 +479,41 @@ function limitSearchResult(result, maxMatches, maxChars) {
|
|
|
465
479
|
return str;
|
|
466
480
|
}
|
|
467
481
|
|
|
482
|
+
/**
|
|
483
|
+
* 限制搜索结果(返回对象) — 用于批量模式,避免 JSON.stringify → JSON.parse 往返
|
|
484
|
+
* 当源码含控制字符时,stringify→substring 截断会破坏 JSON 结构导致 parse 失败
|
|
485
|
+
*/
|
|
486
|
+
function limitSearchResultObj(result, maxMatches, maxChars) {
|
|
487
|
+
if (!result || typeof result !== 'object') return result || {};
|
|
488
|
+
if (typeof result === 'string') return { _raw: result.substring(0, maxChars) };
|
|
489
|
+
|
|
490
|
+
const limited = { ...result };
|
|
491
|
+
if (Array.isArray(limited.matches)) {
|
|
492
|
+
limited.matches = limited.matches.slice(0, maxMatches).map(m => {
|
|
493
|
+
const copy = { ...m };
|
|
494
|
+
if (copy.context && typeof copy.context === 'string') {
|
|
495
|
+
const contextLines = copy.context.split('\n');
|
|
496
|
+
if (contextLines.length > 7) {
|
|
497
|
+
copy.context = contextLines.slice(0, 7).join('\n') + '\n... [truncated]';
|
|
498
|
+
}
|
|
499
|
+
// 按字符上限截断 context(防止单个代码块过大)
|
|
500
|
+
if (copy.context.length > 500) {
|
|
501
|
+
copy.context = copy.context.substring(0, 500) + '\n... [truncated]';
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (Array.isArray(copy.lines) && copy.lines.length > 5) {
|
|
505
|
+
copy.lines = copy.lines.slice(0, 5);
|
|
506
|
+
copy._truncated = true;
|
|
507
|
+
}
|
|
508
|
+
return copy;
|
|
509
|
+
});
|
|
510
|
+
if (result.matches.length > maxMatches) {
|
|
511
|
+
limited._note = `Showing ${maxMatches} of ${result.matches.length} matches`;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return limited;
|
|
515
|
+
}
|
|
516
|
+
|
|
468
517
|
/**
|
|
469
518
|
* 限制文件内容 — 截断 content 字段
|
|
470
519
|
*/
|
|
@@ -526,6 +575,9 @@ export class PhaseRouter {
|
|
|
526
575
|
/** @type {Object} 日志器 */
|
|
527
576
|
#logger;
|
|
528
577
|
|
|
578
|
+
/** @type {boolean} 是否因 maxIterations 强制进入 SUMMARIZE */
|
|
579
|
+
#forcedSummarize = false;
|
|
580
|
+
|
|
529
581
|
/**
|
|
530
582
|
* @param {Object} budget — 预算配置
|
|
531
583
|
* @param {boolean} isSkillOnly — 是否为 skill-only 维度
|
|
@@ -561,6 +613,19 @@ export class PhaseRouter {
|
|
|
561
613
|
return this.#totalSubmits;
|
|
562
614
|
}
|
|
563
615
|
|
|
616
|
+
/**
|
|
617
|
+
* 是否因 maxIterations 触顶而强制进入 SUMMARIZE(一次性标记)
|
|
618
|
+
* 调用后自动复位
|
|
619
|
+
* @returns {boolean}
|
|
620
|
+
*/
|
|
621
|
+
consumeForcedSummarize() {
|
|
622
|
+
if (this.#forcedSummarize) {
|
|
623
|
+
this.#forcedSummarize = false;
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
|
|
564
629
|
/**
|
|
565
630
|
* 获取当前阶段的 toolChoice
|
|
566
631
|
* @returns {'required'|'auto'|'none'}
|
|
@@ -585,8 +650,21 @@ export class PhaseRouter {
|
|
|
585
650
|
* @returns {boolean}
|
|
586
651
|
*/
|
|
587
652
|
shouldExit() {
|
|
588
|
-
|
|
653
|
+
// 已在 SUMMARIZE 阶段 → 给 2 轮输出总结后退出
|
|
589
654
|
if (this.#phase === 'SUMMARIZE' && this.#phaseRounds >= 2) return true;
|
|
655
|
+
|
|
656
|
+
// 达到 maxIterations → 不硬退出,而是强制转入 SUMMARIZE 让 AI 在完整上下文中收尾
|
|
657
|
+
if (this.#totalIterations >= this.#budget.maxIterations && this.#phase !== 'SUMMARIZE') {
|
|
658
|
+
this.#logger.info(`[PhaseRouter] maxIterations reached (${this.#totalIterations}/${this.#budget.maxIterations}), forcing → SUMMARIZE for graceful exit`);
|
|
659
|
+
this.#forcedSummarize = true;
|
|
660
|
+
this.#transitionTo('SUMMARIZE');
|
|
661
|
+
// 返回 false 让主循环继续运行 SUMMARIZE 阶段的 2 轮收尾
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// SUMMARIZE 阶段超限兜底(maxIterations + 2 轮 grace)
|
|
666
|
+
if (this.#totalIterations >= this.#budget.maxIterations + 2) return true;
|
|
667
|
+
|
|
590
668
|
return false;
|
|
591
669
|
}
|
|
592
670
|
|
|
@@ -688,6 +766,12 @@ export class PhaseRouter {
|
|
|
688
766
|
* @returns {string|null}
|
|
689
767
|
*/
|
|
690
768
|
getPhaseHint() {
|
|
769
|
+
// 接近 maxIterations 上限时,无论处于哪个阶段都警告 AI
|
|
770
|
+
const remaining = this.#budget.maxIterations - this.#totalIterations;
|
|
771
|
+
if (remaining <= 2 && remaining > 0 && this.#phase !== 'SUMMARIZE') {
|
|
772
|
+
return `⚠️ 仅剩 ${remaining} 轮次即达上限,请尽快完成当前工作并准备输出总结。`;
|
|
773
|
+
}
|
|
774
|
+
|
|
691
775
|
switch (this.#phase) {
|
|
692
776
|
case 'EXPLORE':
|
|
693
777
|
if (this.#phaseRounds >= this.#budget.searchBudget - 2) {
|
|
@@ -13,6 +13,31 @@
|
|
|
13
13
|
// AnalysisReport 构建
|
|
14
14
|
// ──────────────────────────────────────────────────────────────────
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* 清理 Analyst 分析文本中可能泄漏的系统 nudge / graceful exit 指令。
|
|
18
|
+
* 这些内容如果传给 Producer,会干扰其正常工作流。
|
|
19
|
+
*/
|
|
20
|
+
function sanitizeAnalysisText(text) {
|
|
21
|
+
if (!text) return '';
|
|
22
|
+
// 移除 graceful exit nudge 及 digest 模板指令
|
|
23
|
+
const patterns = [
|
|
24
|
+
/\*{0,2}⚠️?\s*(?:你已使用|轮次即将耗尽|仅剩|请立即停止|必须立即结束)[^\n]*\n?/gi,
|
|
25
|
+
/\*{0,2}请立即停止所有工具调用[^\n]*\*{0,2}\n?/gi,
|
|
26
|
+
/请在回复中直接输出\s*dimensionDigest\s*JSON[^\n]*\n?/gi,
|
|
27
|
+
/> ?(?:remainingTasks|如果所有信号都已覆盖)[^\n]*\n?/gi,
|
|
28
|
+
/> ?⚠️ 严禁输出任何非 JSON 内容[^\n]*\n?/gi,
|
|
29
|
+
// 移除 AI 回显的 dimensionDigest JSON 块(对 Producer 无价值且会干扰)
|
|
30
|
+
/```json\s*\n\s*\{\s*"dimensionDigest"\s*:[\s\S]*?\n```/g,
|
|
31
|
+
];
|
|
32
|
+
let cleaned = text;
|
|
33
|
+
for (const pat of patterns) {
|
|
34
|
+
cleaned = cleaned.replace(pat, '');
|
|
35
|
+
}
|
|
36
|
+
// 移除可能残留的空行堆积
|
|
37
|
+
cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim();
|
|
38
|
+
return cleaned;
|
|
39
|
+
}
|
|
40
|
+
|
|
16
41
|
/**
|
|
17
42
|
* 从 Analyst 的执行结果构建 AnalysisReport
|
|
18
43
|
*
|
|
@@ -73,7 +98,7 @@ export function buildAnalysisReport(analystResult, dimensionId, projectGraph = n
|
|
|
73
98
|
}
|
|
74
99
|
|
|
75
100
|
// 从分析文本中提取文件路径
|
|
76
|
-
const text = analystResult.reply || '';
|
|
101
|
+
const text = sanitizeAnalysisText(analystResult.reply || '');
|
|
77
102
|
const textFileRefs = text.match(/[\w/.-]+\.[mhswift]+/g);
|
|
78
103
|
if (textFileRefs) {
|
|
79
104
|
for (const f of textFileRefs) {
|
|
@@ -31,12 +31,14 @@ const PRODUCER_SYSTEM_PROMPT = `你是知识管理专家。你会收到一段代
|
|
|
31
31
|
|
|
32
32
|
工作流程:
|
|
33
33
|
1. 阅读分析文本,识别每个独立的知识点/发现
|
|
34
|
-
2.
|
|
34
|
+
2. 用 read_project_file 批量获取关键代码片段:
|
|
35
|
+
read_project_file({ filePaths: ["FileA.m", "FileB.m"], maxLines: 80 })
|
|
35
36
|
3. 立刻调用 submit_candidate 提交
|
|
36
37
|
4. 重复直到分析中的所有知识点都已提交
|
|
37
38
|
|
|
38
39
|
关键规则:
|
|
39
40
|
- 分析中的每个要点/段落都应转化为至少一个候选
|
|
41
|
+
- read_project_file 支持 filePaths 数组批量读取多个文件,一次调用完成
|
|
40
42
|
- read_project_file 时读取足够多的行数(startLine + maxLines 至少 30 行)
|
|
41
43
|
- reasoning.sources 必须是非空数组,填写相关文件路径如 ["FileName.m"]
|
|
42
44
|
- 如果分析提到了 3 个模式,就应该提交 3 个候选,不要合并
|
|
@@ -63,7 +65,7 @@ const PRODUCER_TOOLS = [
|
|
|
63
65
|
// ──────────────────────────────────────────────────────────────────
|
|
64
66
|
|
|
65
67
|
const PRODUCER_BUDGET = {
|
|
66
|
-
maxIterations:
|
|
68
|
+
maxIterations: 24, // was 18 — 与 Analyst 统一
|
|
67
69
|
searchBudget: 4,
|
|
68
70
|
searchBudgetGrace: 3,
|
|
69
71
|
maxSubmits: 10,
|