autosnippet 3.2.9 → 3.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli/deploy/FileManifest.js +1 -1
- package/lib/external/ai/providers/GoogleGeminiProvider.js +6 -4
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +6 -2
- package/lib/http/routes/recipes.js +1 -5
- package/lib/service/agent/AgentFactory.js +137 -92
- package/lib/service/agent/AgentRuntime.js +43 -3
- package/lib/service/agent/capabilities.js +3 -2
- package/lib/service/agent/context/ExplorationTracker.js +87 -19
- package/lib/service/agent/core/ChatAgentPrompts.js +4 -2
- package/lib/service/agent/domain/ChatAgentTasks.js +2 -6
- package/lib/service/agent/domain/insight-producer.js +4 -1
- package/lib/service/agent/domain/scan-prompts.js +368 -29
- package/lib/service/agent/memory/ActiveContext.js +29 -1
- package/lib/service/agent/presets.js +3 -1
- package/lib/service/agent/strategies.js +41 -2
- package/package.json +1 -1
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* scan-prompts.js — scanKnowledge 任务配置
|
|
2
|
+
* scan-prompts.js — scanKnowledge 任务配置 + 统一管线工厂 + 关系发现管线
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Analyze
|
|
6
|
-
*
|
|
4
|
+
* scan pipeline (extract/summarize):
|
|
5
|
+
* 共享 Insight Pipeline (Analyze → QualityGate → Produce → RejectionGate),
|
|
6
|
+
* Analyze 使用与冷启动一致的 ANALYST_SYSTEM_PROMPT + ExplorationTracker 四阶段管理,
|
|
7
|
+
* Produce 阶段均为工具驱动 (collect_scan_recipe),与冷启动 submit_knowledge 对齐。
|
|
8
|
+
*
|
|
9
|
+
* relations pipeline (独立):
|
|
10
|
+
* 知识图谱关系发现: Explore → Synthesize 两阶段,
|
|
11
|
+
* 通过查询知识库 + 读取源码发现条目间语义关系。
|
|
7
12
|
*
|
|
8
13
|
* @module scan-prompts
|
|
9
14
|
*/
|
|
10
15
|
|
|
16
|
+
import { insightGateEvaluator, buildRetryPrompt } from './insight-gate.js';
|
|
17
|
+
import { producerRejectionGateEvaluator, buildCodeContextSection } from './insight-producer.js';
|
|
18
|
+
import { ANALYST_SYSTEM_PROMPT } from './insight-analyst.js';
|
|
19
|
+
|
|
11
20
|
/**
|
|
12
21
|
* @typedef {Object} ScanTaskConfig
|
|
13
22
|
* @property {string} producePrompt — Produce 阶段的 systemPrompt
|
|
@@ -15,11 +24,11 @@
|
|
|
15
24
|
*/
|
|
16
25
|
|
|
17
26
|
/**
|
|
18
|
-
* task → Produce 阶段配置
|
|
27
|
+
* task → Produce 阶段配置 (extract + summarize)
|
|
19
28
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
29
|
+
* 两种 task 均为工具驱动 (collect_scan_recipe),Recipe 格式与冷启动 submit_knowledge 对齐:
|
|
30
|
+
* - extract: 多文件 target 扫描 → 多个 Recipe
|
|
31
|
+
* - summarize: 单文件/代码片段 → 1~2 个 Recipe
|
|
23
32
|
*
|
|
24
33
|
* @type {Record<string, ScanTaskConfig>}
|
|
25
34
|
*/
|
|
@@ -68,38 +77,368 @@ content.markdown 字段必须是「项目特写」:
|
|
|
68
77
|
fallback: (label) => ({ targetName: label, extracted: 0, recipes: [] }),
|
|
69
78
|
},
|
|
70
79
|
|
|
71
|
-
// ─── summarize:
|
|
80
|
+
// ─── summarize: 代码摘要(工具驱动,与 extract 管线对齐) ──────
|
|
72
81
|
|
|
73
82
|
summarize: {
|
|
74
|
-
producePrompt:
|
|
83
|
+
producePrompt: `你是技术文档专家。你会收到一段代码分析文本,需要将其转化为高质量的知识候选。
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
- 输出纯 JSON,不含 markdown 包装
|
|
78
|
-
- title 简洁(30 字内),summary 完整(200 字内),usageGuide 实用(300 字内)
|
|
79
|
-
- 基于分析中的实际发现,不要臆造功能
|
|
85
|
+
核心原则: 分析文本已经包含了所有发现,你的唯一工作是将它们格式化为 collect_scan_recipe 调用。
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
这是单文件/代码片段的深度分析,提交一个(或少量)高质量的知识候选:
|
|
88
|
+
1. 清晰的标题(描述代码的核心功能,使用项目真实类名)
|
|
89
|
+
2. 完整的技术文档正文(content.markdown 字段,≥200 字符)
|
|
90
|
+
3. 实用的使用指南(usageGuide 字段,含示例)
|
|
91
|
+
4. 准确的分类(category)和标签(tags)
|
|
92
|
+
|
|
93
|
+
## content.markdown 写作要求
|
|
94
|
+
1. **功能概述** — 这段代码做什么,解决什么问题
|
|
95
|
+
2. **核心实现** — 关键代码逻辑,含代码块 (\`\`\`)
|
|
96
|
+
3. **使用方式** — 如何调用/集成,含示例代码
|
|
97
|
+
4. **注意事项** — 边界条件、性能考量、已知限制
|
|
98
|
+
|
|
99
|
+
## 工作流程
|
|
100
|
+
1. 阅读分析文本,理解代码的核心功能和设计决策
|
|
101
|
+
2. 如需验证细节,用 read_project_file 获取代码片段
|
|
102
|
+
3. 调用 collect_scan_recipe 提交知识候选
|
|
103
|
+
|
|
104
|
+
## 关键规则
|
|
105
|
+
- 单文件通常提交 1 个候选,除非代码明确包含多个独立知识点
|
|
106
|
+
- reasoning.sources 必须是非空数组,填写源文件路径
|
|
107
|
+
- kind 选择: 优先 pattern(代码模式)或 fact(技术事实)
|
|
108
|
+
- 必填: trigger (@kebab-case)、doClause (英文祈使句)、content.rationale
|
|
109
|
+
- content.markdown 必须包含代码块,展示核心实现`,
|
|
110
|
+
fallback: (label) => ({ targetName: label, extracted: 0, recipes: [] }),
|
|
84
111
|
},
|
|
85
112
|
|
|
86
|
-
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ──────────────────────────────────────────────────────────────────
|
|
116
|
+
// 统一管线工厂 — 生成标准 4 阶段 Pipeline (与冷启动对齐)
|
|
117
|
+
// ──────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 构建 scanKnowledge 的标准 4 阶段 Pipeline stages
|
|
121
|
+
*
|
|
122
|
+
* 与冷启动 orchestrator 完全对齐:
|
|
123
|
+
* 1. analyze — 代码分析 (ExplorationTracker 四阶段管理)
|
|
124
|
+
* 2. quality_gate — 分析质量门控 (insightGateEvaluator + buildAnalysisArtifact)
|
|
125
|
+
* 3. produce — 知识生产 (artifact-aware promptBuilder + 工具驱动提交)
|
|
126
|
+
* 4. rejection_gate — 拒绝率门控 (producerRejectionGateEvaluator)
|
|
127
|
+
*
|
|
128
|
+
* 与冷启动对齐的关键节点:
|
|
129
|
+
* - quality_gate 通过 strategyContext.activeContext 走 buildAnalysisArtifact
|
|
130
|
+
* (而非降级的 buildAnalysisReport),保留 findings/evidenceMap/negativeSignals
|
|
131
|
+
* - produce 使用 promptBuilder (而非 promptTransform),
|
|
132
|
+
* 从 gateArtifact 注入结构化发现和代码证据到 prompt
|
|
133
|
+
* - strategyContext 需要包含 activeContext / outputType / dimId
|
|
134
|
+
* (由 AgentFactory.buildSystemContext 设置)
|
|
135
|
+
*
|
|
136
|
+
* @param {Object} opts
|
|
137
|
+
* @param {'extract'|'summarize'} opts.task — 任务类型
|
|
138
|
+
* @param {string} opts.producePrompt — Produce 阶段 systemPrompt
|
|
139
|
+
* @param {string[]} opts.analyzeCaps — Analyze 阶段 capabilities
|
|
140
|
+
* @param {string[]} opts.produceCaps — Produce 阶段 capabilities
|
|
141
|
+
* @param {Array} [opts.files] — 源文件 (fallback prompt 用)
|
|
142
|
+
* @param {number} [opts.analyzeMaxIter=24] — Analyze 最大迭代
|
|
143
|
+
* @returns {Object[]} PipelineStrategy stages 数组
|
|
144
|
+
*/
|
|
145
|
+
export function buildScanPipelineStages({
|
|
146
|
+
task,
|
|
147
|
+
producePrompt,
|
|
148
|
+
analyzeCaps,
|
|
149
|
+
produceCaps,
|
|
150
|
+
files,
|
|
151
|
+
analyzeMaxIter = 24,
|
|
152
|
+
} = {}) {
|
|
153
|
+
// ── Stage 1: Analyze ──
|
|
154
|
+
const analyzeStage = {
|
|
155
|
+
name: 'analyze',
|
|
156
|
+
capabilities: analyzeCaps,
|
|
157
|
+
budget: {
|
|
158
|
+
maxIterations: analyzeMaxIter,
|
|
159
|
+
maxTokens: 8192,
|
|
160
|
+
temperature: 0.3,
|
|
161
|
+
timeoutMs: 300_000, // 5 min (与冷启动对齐)
|
|
162
|
+
},
|
|
163
|
+
systemPrompt: ANALYST_SYSTEM_PROMPT,
|
|
164
|
+
retryPromptBuilder: (retryCtx, _origPrompt, prev) => {
|
|
165
|
+
const prevAnalysis = prev.analyze?.reply || '';
|
|
166
|
+
const retryHint = buildRetryPrompt(retryCtx.reason);
|
|
167
|
+
return `${prevAnalysis}\n\n⚠️ 上述分析未通过质量检查: ${retryCtx.reason}\n\n${retryHint}`;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// ── Stage 2: Quality Gate ──
|
|
172
|
+
// insightGateEvaluator 读取 strategyContext.activeContext:
|
|
173
|
+
// - 有 activeContext → buildAnalysisArtifact (完整: findings/evidenceMap/negativeSignals)
|
|
174
|
+
// - 无 activeContext → buildAnalysisReport (降级: 仅文本 + 文件列表)
|
|
175
|
+
// buildSystemContext 通过 trace 设置 activeContext,确保走完整路径
|
|
176
|
+
const qualityGateStage = {
|
|
177
|
+
name: 'quality_gate',
|
|
178
|
+
gate: {
|
|
179
|
+
evaluator: insightGateEvaluator,
|
|
180
|
+
maxRetries: 1,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// ── Stage 3: Produce ──
|
|
185
|
+
// extract 和 summarize 都是工具驱动 (collect_scan_recipe)
|
|
186
|
+
const isToolDriven = task === 'extract' || task === 'summarize';
|
|
187
|
+
const isSummarize = task === 'summarize';
|
|
188
|
+
const submitToolNames = isToolDriven ? ['collect_scan_recipe'] : [];
|
|
189
|
+
|
|
190
|
+
const produceStage = {
|
|
191
|
+
name: 'produce',
|
|
192
|
+
submitToolName: 'collect_scan_recipe', // 透传给 ExplorationTracker nudge 文本
|
|
193
|
+
capabilities: produceCaps,
|
|
194
|
+
budget: {
|
|
195
|
+
maxIterations: isSummarize ? 12 : 24,
|
|
196
|
+
temperature: 0.2,
|
|
197
|
+
timeoutMs: isSummarize ? 120_000 : 180_000,
|
|
198
|
+
// 显式传入 tracker 阈值,避免依赖默认值 (softSubmitLimit:8) 导致转换不触发
|
|
199
|
+
maxSubmits: isSummarize ? 3 : 10,
|
|
200
|
+
softSubmitLimit: isSummarize ? 2 : 8,
|
|
201
|
+
idleRoundsToExit: 2,
|
|
202
|
+
},
|
|
203
|
+
systemPrompt: producePrompt,
|
|
204
|
+
// 使用 promptBuilder (而非 promptTransform) — 与冷启动对齐
|
|
205
|
+
// promptBuilder 接收 gateArtifact (来自 quality_gate 的 AnalysisArtifact),
|
|
206
|
+
// 注入结构化 findings + 代码证据到 prompt,而非仅传入 analyze.reply 纯文本
|
|
207
|
+
promptBuilder: (ctx) => {
|
|
208
|
+
return buildScanProducerPrompt(ctx, files, task);
|
|
209
|
+
},
|
|
210
|
+
// retry 配置 (拒绝率过高时缩减预算)
|
|
211
|
+
...(isToolDriven ? {
|
|
212
|
+
retryBudget: { maxIterations: isSummarize ? 3 : 5, temperature: 0.3, timeoutMs: isSummarize ? 60_000 : 120_000 },
|
|
213
|
+
retryPromptBuilder: (retryCtx, _origPrompt, prev) => {
|
|
214
|
+
const prevProduce = prev.produce;
|
|
215
|
+
const submitCalls = (prevProduce?.toolCalls || []).filter(tc =>
|
|
216
|
+
submitToolNames.includes(tc.tool || tc.name));
|
|
217
|
+
const rejected = submitCalls.filter(tc => {
|
|
218
|
+
const res = tc.result;
|
|
219
|
+
if (!res) return false;
|
|
220
|
+
if (typeof res === 'string') return res.includes('rejected') || res.includes('error');
|
|
221
|
+
return res.status === 'rejected' || res.status === 'error';
|
|
222
|
+
}).length;
|
|
223
|
+
return `你的 ${rejected} 个提交被拒绝了。请根据拒绝原因改进后重新提交,确保:
|
|
224
|
+
1. content 必须是对象: { markdown: "...", rationale: "...", pattern: "..." }
|
|
225
|
+
2. content.markdown 字段 ≥ 200 字符,含代码块 (\`\`\`)
|
|
226
|
+
3. content.rationale 必填 — 设计原理说明
|
|
227
|
+
4. reasoning.sources 必须是非空数组
|
|
228
|
+
5. 标题使用项目真实类名
|
|
229
|
+
6. 必填: trigger (@kebab-case)、kind (rule/pattern/fact)、doClause (英文祈使句)`;
|
|
230
|
+
},
|
|
231
|
+
skipOnDegrade: true,
|
|
232
|
+
} : {
|
|
233
|
+
skipOnDegrade: true,
|
|
234
|
+
}),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const stages = [analyzeStage, qualityGateStage, produceStage];
|
|
238
|
+
|
|
239
|
+
// ── Stage 4: Rejection Gate (仅工具驱动模式) ──
|
|
240
|
+
if (isToolDriven) {
|
|
241
|
+
stages.push({
|
|
242
|
+
name: 'rejection_gate',
|
|
243
|
+
gate: {
|
|
244
|
+
evaluator: (source, phaseResults, ctx) =>
|
|
245
|
+
producerRejectionGateEvaluator(source, phaseResults, {
|
|
246
|
+
...ctx,
|
|
247
|
+
submitToolNames,
|
|
248
|
+
}),
|
|
249
|
+
maxRetries: 1,
|
|
250
|
+
},
|
|
251
|
+
skipOnDegrade: true,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return stages;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ──────────────────────────────────────────────────────────────────
|
|
259
|
+
// Scan Producer Prompt Builder — artifact-aware (与冷启动 buildProducerPromptV2 对齐)
|
|
260
|
+
// ──────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 构建 scan produce 阶段的 prompt — 从 gateArtifact 注入结构化信息
|
|
264
|
+
*
|
|
265
|
+
* 与冷启动 buildProducerPromptV2 对齐的关键:
|
|
266
|
+
* - 优先使用 gateArtifact 中的 findings (结构化发现)
|
|
267
|
+
* - 注入 evidenceMap (Analyst 已读取的代码证据)
|
|
268
|
+
* - 注入 negativeSignals (搜索但未找到的模式)
|
|
269
|
+
* - 当 artifact 不可用时 fallback 到 analyze.reply 纯文本
|
|
270
|
+
*
|
|
271
|
+
* @param {Object} ctx — promptBuilder 上下文 (含 gateArtifact, phaseResults, ...)
|
|
272
|
+
* @param {Array} [files] — 源文件 (fallback 用)
|
|
273
|
+
* @param {'extract'|'summarize'} task — 任务类型
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
function buildScanProducerPrompt(ctx, files, task) {
|
|
277
|
+
const artifact = ctx.gateArtifact;
|
|
278
|
+
const analysis = ctx.phaseResults?.analyze?.reply || '';
|
|
279
|
+
|
|
280
|
+
// ── 有完整 artifact 时 (走 buildAnalysisArtifact 路径) ──
|
|
281
|
+
if (artifact && artifact.analysisText) {
|
|
282
|
+
const parts = [];
|
|
283
|
+
|
|
284
|
+
// §1 分析文本
|
|
285
|
+
parts.push(`将以下代码分析转化为 collect_scan_recipe 调用。\n\n---\n${artifact.analysisText}\n---`);
|
|
286
|
+
|
|
287
|
+
// §2 结构化发现 (来自 ActiveContext scratchpad)
|
|
288
|
+
if (artifact.findings?.length > 0) {
|
|
289
|
+
const findingLines = ['## 关键发现 (Analyst 已确认)'];
|
|
290
|
+
const sorted = [...artifact.findings].sort((a, b) => (b.importance || 0) - (a.importance || 0));
|
|
291
|
+
for (const f of sorted) {
|
|
292
|
+
const badge = (f.importance || 0) >= 8 ? '⚠️' : '📋';
|
|
293
|
+
findingLines.push(`${badge} **[${f.importance || 5}/10]** ${f.finding}`);
|
|
294
|
+
if (f.evidence) {
|
|
295
|
+
findingLines.push(` 证据: ${f.evidence}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
findingLines.push('');
|
|
299
|
+
findingLines.push('☝️ 上述每个发现都应至少转化为一个候选。');
|
|
300
|
+
parts.push(findingLines.join('\n'));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// §3 代码证据 (来自 EvidenceCollector)
|
|
304
|
+
if (artifact.evidenceMap && artifact.evidenceMap.size > 0) {
|
|
305
|
+
const codeContext = buildCodeContextSection(artifact.evidenceMap);
|
|
306
|
+
if (codeContext) {
|
|
307
|
+
parts.push(codeContext);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// §4 负空间信号 (搜索但未找到的模式 — 不要猜测)
|
|
312
|
+
if (artifact.negativeSignals?.length > 0) {
|
|
313
|
+
const nsLines = ['## ⛔ 不存在的模式 (不要猜测)'];
|
|
314
|
+
for (const ns of artifact.negativeSignals.slice(0, 5)) {
|
|
315
|
+
nsLines.push(`- "${ns.searchPattern}" — ${ns.implication}`);
|
|
316
|
+
}
|
|
317
|
+
parts.push(nsLines.join('\n'));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// §5 引用文件
|
|
321
|
+
if (artifact.referencedFiles?.length > 0) {
|
|
322
|
+
parts.push(`分析中引用的关键文件: ${artifact.referencedFiles.slice(0, 15).join(', ')}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return parts.join('\n\n');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Fallback: 无 artifact 时退回纯文本 (不应该发生,但防御性保留) ──
|
|
329
|
+
if (analysis.length >= 200) {
|
|
330
|
+
return `将以下代码分析转化为结构化输出。\n\n## 代码分析\n${analysis}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Fallback: analyze reply 不足时直接提供源代码
|
|
334
|
+
const fileCtx = (files || []).slice(0, 15).map(f => {
|
|
335
|
+
const body = (f.content || '').length > 1200
|
|
336
|
+
? f.content.slice(0, 1200) + '\n// ... (truncated)'
|
|
337
|
+
: (f.content || '');
|
|
338
|
+
return `### ${f.relativePath || f.name}\n\`\`\`\n${body}\n\`\`\``;
|
|
339
|
+
}).join('\n\n');
|
|
340
|
+
const preamble = analysis
|
|
341
|
+
? `## 部分分析\n${analysis}\n\n`
|
|
342
|
+
: '';
|
|
343
|
+
return `${preamble}分析以下 ${files?.length || 0} 个源文件,提取知识 Recipe。\n\n${fileCtx}`;
|
|
344
|
+
}
|
|
87
345
|
|
|
88
|
-
|
|
89
|
-
|
|
346
|
+
// ──────────────────────────────────────────────────────────────────
|
|
347
|
+
// Relations Pipeline — 知识图谱关系发现(独立管线)
|
|
348
|
+
// ──────────────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
/** Explore 阶段: 查询知识库,分析条目间关联 */
|
|
351
|
+
const RELATIONS_EXPLORE_PROMPT = `你是知识图谱架构师。你的任务是探索项目知识库中的知识条目,发现它们之间的语义关系。
|
|
352
|
+
|
|
353
|
+
## 工作流程
|
|
354
|
+
1. 使用 search_knowledge 查询知识库中的条目分类
|
|
355
|
+
2. 逐组分析相关知识条目的内容、依赖、关联代码
|
|
356
|
+
3. 使用 read_project_file 验证跨条目的代码引用关系
|
|
357
|
+
4. 详细记录发现的所有关系及其代码证据
|
|
90
358
|
|
|
91
359
|
## 关系类型
|
|
92
|
-
requires
|
|
360
|
+
- requires: A 需要 B 才能正常工作
|
|
361
|
+
- extends: A 扩展了 B 的功能
|
|
362
|
+
- enforces: A 强制规范了 B 的使用方式
|
|
363
|
+
- depends_on: A 依赖 B
|
|
364
|
+
- inherits: A 继承自 B
|
|
365
|
+
- implements: A 实现了 B 的接口/协议
|
|
366
|
+
- calls: A 调用了 B
|
|
367
|
+
- prerequisite: 理解 A 之前需要先了解 B
|
|
93
368
|
|
|
94
|
-
##
|
|
95
|
-
-
|
|
96
|
-
-
|
|
369
|
+
## 分析要求
|
|
370
|
+
- 每个关系必须有明确的代码证据(文件名 + 代码片段)
|
|
371
|
+
- 不要臆造不存在的关系
|
|
372
|
+
- 优先发现强关联(requires, implements, inherits),再发现弱关联(calls, prerequisite)
|
|
373
|
+
- 将发现以结构化文本记录: "FromTitle → ToTitle (type): evidence"`;
|
|
97
374
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
fallback: () => ({ analyzed: 0, relations: [] }),
|
|
101
|
-
},
|
|
375
|
+
/** Synthesize 阶段: 将探索结果转化为 JSON */
|
|
376
|
+
const RELATIONS_SYNTHESIZE_PROMPT = `你是结构化数据专家。将知识图谱探索结果转化为 JSON 格式的关系列表。
|
|
102
377
|
|
|
103
|
-
|
|
378
|
+
## 输出格式(纯 JSON,不含 markdown 包装)
|
|
379
|
+
{
|
|
380
|
+
"analyzed": 知识条目数量,
|
|
381
|
+
"relations": [
|
|
382
|
+
{
|
|
383
|
+
"from": "知识条目A精确标题",
|
|
384
|
+
"to": "知识条目B精确标题",
|
|
385
|
+
"type": "关系类型",
|
|
386
|
+
"evidence": "具体代码证据描述"
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
## 规则
|
|
392
|
+
- 严格对照探索阶段的发现,不添加未被提及的关系
|
|
393
|
+
- type 必须是: requires / extends / enforces / depends_on / inherits / implements / calls / prerequisite
|
|
394
|
+
- evidence 必须引用具体的代码或文件
|
|
395
|
+
- from 和 to 使用知识条目的精确标题`;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 构建知识图谱关系发现的独立 Pipeline stages
|
|
399
|
+
*
|
|
400
|
+
* 与 scan pipeline 不同,relations pipeline:
|
|
401
|
+
* - 不需要源文件输入 (从知识库查询)
|
|
402
|
+
* - 2 阶段: explore (工具驱动) → synthesize (文本输出)
|
|
403
|
+
* - 无质量门控 (探索结果质量由工具返回保证)
|
|
404
|
+
*
|
|
405
|
+
* @param {Object} [opts]
|
|
406
|
+
* @param {string[]} [opts.exploreCaps] — Explore 阶段 capabilities
|
|
407
|
+
* @param {number} [opts.exploreMaxIter=20] — Explore 最大迭代
|
|
408
|
+
* @returns {Object[]} PipelineStrategy stages 数组
|
|
409
|
+
*/
|
|
410
|
+
export function buildRelationsPipelineStages({
|
|
411
|
+
exploreCaps = ['knowledge_production', 'code_analysis'],
|
|
412
|
+
exploreMaxIter = 20,
|
|
413
|
+
} = {}) {
|
|
414
|
+
return [
|
|
415
|
+
{
|
|
416
|
+
name: 'explore',
|
|
417
|
+
capabilities: exploreCaps,
|
|
418
|
+
budget: {
|
|
419
|
+
maxIterations: exploreMaxIter,
|
|
420
|
+
maxTokens: 8192,
|
|
421
|
+
temperature: 0.3,
|
|
422
|
+
timeoutMs: 300_000,
|
|
423
|
+
},
|
|
424
|
+
systemPrompt: RELATIONS_EXPLORE_PROMPT,
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: 'synthesize',
|
|
428
|
+
capabilities: [],
|
|
429
|
+
budget: {
|
|
430
|
+
maxIterations: 4,
|
|
431
|
+
maxTokens: 8192,
|
|
432
|
+
temperature: 0.2,
|
|
433
|
+
timeoutMs: 60_000,
|
|
434
|
+
},
|
|
435
|
+
systemPrompt: RELATIONS_SYNTHESIZE_PROMPT,
|
|
436
|
+
promptTransform: (_input, prev) => {
|
|
437
|
+
const exploration = prev.explore?.reply || '';
|
|
438
|
+
return `基于以下知识图谱探索结果,输出结构化关系 JSON。\n\n## 探索结果\n${exploration}`;
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
];
|
|
442
|
+
}
|
|
104
443
|
|
|
105
444
|
export default SCAN_TASK_CONFIGS;
|
|
@@ -148,6 +148,8 @@ export class ActiveContext {
|
|
|
148
148
|
#plan = null;
|
|
149
149
|
/** @type {Array<Plan>} */
|
|
150
150
|
#planHistory = [];
|
|
151
|
+
/** @type {boolean} 是否期待下一次响应包含计划 (由 ExplorationTracker 设置) */
|
|
152
|
+
#expectingPlan = false;
|
|
151
153
|
|
|
152
154
|
// ── 配置 ──
|
|
153
155
|
/** @type {number} 保留最近 N 轮原始观察 */
|
|
@@ -342,6 +344,11 @@ export class ActiveContext {
|
|
|
342
344
|
|
|
343
345
|
/**
|
|
344
346
|
* 从 AI 响应文本中提取计划,自动调用 setPlan/updatePlan
|
|
347
|
+
*
|
|
348
|
+
* 防御措施: 已存在计划时,仅在 #expectingPlan 为 true 时才覆盖。
|
|
349
|
+
* 这防止 reflection 回复中的编号列表(非计划的回应文本)污染已有计划。
|
|
350
|
+
* ExplorationTracker 在发送 plan elicitation / replan 时调用 expectPlan() 授权更新。
|
|
351
|
+
*
|
|
345
352
|
* @param {string} text — AI 完整响应文本
|
|
346
353
|
* @param {number} iteration — 当前轮次
|
|
347
354
|
* @returns {boolean} — 是否成功提取到计划
|
|
@@ -350,6 +357,11 @@ export class ActiveContext {
|
|
|
350
357
|
const planText = this.#extractPlanFromText(text);
|
|
351
358
|
if (!planText) return false;
|
|
352
359
|
|
|
360
|
+
// Guard: 已有计划时,仅在 expectPlan 授权下才覆盖
|
|
361
|
+
// 防止 reflection/convergence 回复中的编号列表被误捕获为 plan
|
|
362
|
+
if (this.#plan && !this.#expectingPlan) return false;
|
|
363
|
+
|
|
364
|
+
this.#expectingPlan = false;
|
|
353
365
|
if (this.#plan) {
|
|
354
366
|
this.#updatePlan(planText, iteration);
|
|
355
367
|
} else {
|
|
@@ -358,6 +370,14 @@ export class ActiveContext {
|
|
|
358
370
|
return true;
|
|
359
371
|
}
|
|
360
372
|
|
|
373
|
+
/**
|
|
374
|
+
* 标记「下一次响应可能包含计划」— 授权 extractAndSetPlan 覆盖已有计划
|
|
375
|
+
* 由 ExplorationTracker 在发送 plan elicitation / replan nudge 时调用。
|
|
376
|
+
*/
|
|
377
|
+
expectPlan() {
|
|
378
|
+
this.#expectingPlan = true;
|
|
379
|
+
}
|
|
380
|
+
|
|
361
381
|
/**
|
|
362
382
|
* 直接设置计划 (公开接口,供 ExplorationTracker 和测试使用)
|
|
363
383
|
* @param {string} planText
|
|
@@ -905,7 +925,15 @@ export class ActiveContext {
|
|
|
905
925
|
}
|
|
906
926
|
}
|
|
907
927
|
|
|
908
|
-
|
|
928
|
+
if (planLines.length < 2) return null;
|
|
929
|
+
|
|
930
|
+
// 防御: 拒绝 "大部分是疑问句" 的编号列表
|
|
931
|
+
// reflection nudge 的 "请评估: 1. ...是什么? 2. ...?" 会被 LLM 回显,
|
|
932
|
+
// 不是真正的探索计划,不能捕获为 plan steps
|
|
933
|
+
const questionCount = planLines.filter(l => /[??]\s*$/.test(l.trim())).length;
|
|
934
|
+
if (questionCount > planLines.length * 0.5) return null;
|
|
935
|
+
|
|
936
|
+
return planLines.join('\n').trim();
|
|
909
937
|
}
|
|
910
938
|
}
|
|
911
939
|
|
|
@@ -124,7 +124,9 @@ export const PRESETS = Object.freeze({
|
|
|
124
124
|
{
|
|
125
125
|
name: 'produce',
|
|
126
126
|
capabilities: ['knowledge_production'],
|
|
127
|
-
|
|
127
|
+
// 透传完整 PRODUCER_BUDGET (searchBudget/maxSubmits/softSubmitLimit/idleRoundsToExit)
|
|
128
|
+
// 供 ExplorationTracker 精确控制 PRODUCE→SUMMARIZE 转换时机
|
|
129
|
+
budget: { ...PRODUCER_BUDGET, temperature: 0.3, timeoutMs: 180_000 },
|
|
128
130
|
systemPrompt: PRODUCER_SYSTEM_PROMPT,
|
|
129
131
|
promptBuilder: (ctx) => buildProducerPromptV2(
|
|
130
132
|
ctx.gateArtifact, // 来自 quality_gate 的 AnalysisArtifact
|
|
@@ -26,6 +26,9 @@ import { randomUUID } from 'node:crypto';
|
|
|
26
26
|
import { AgentEventBus, AgentEvents } from './AgentEventBus.js';
|
|
27
27
|
import { AgentMessage } from './AgentMessage.js';
|
|
28
28
|
import { ExplorationTracker } from './context/ExplorationTracker.js';
|
|
29
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
30
|
+
|
|
31
|
+
const _pipelineLogger = Logger.getInstance();
|
|
29
32
|
|
|
30
33
|
// ─── Base Strategy ─────────────────────────────
|
|
31
34
|
|
|
@@ -152,6 +155,7 @@ export class PipelineStrategy extends Strategy {
|
|
|
152
155
|
let gateArtifact = null;
|
|
153
156
|
let degraded = false;
|
|
154
157
|
let execStageCount = 0; // 已执行的阶段计数 (用于阶段隔离)
|
|
158
|
+
let lastExecutedStageName = null; // 上一个执行阶段名 (用于区分 retry vs 新阶段)
|
|
155
159
|
|
|
156
160
|
for (let i = 0; i < this.#stages.length; i++) {
|
|
157
161
|
const stage = this.#stages[i];
|
|
@@ -281,27 +285,54 @@ export class PipelineStrategy extends Strategy {
|
|
|
281
285
|
|
|
282
286
|
// ── 阶段隔离 (v3.2) ──
|
|
283
287
|
// 避免 ContextWindow / ExplorationTracker 状态在阶段间泄漏
|
|
288
|
+
// B4 fix: retry 同一阶段时不清空 ContextWindow,保留上次分析上下文
|
|
284
289
|
const ctxWin = strategyContext.contextWindow || null;
|
|
285
|
-
|
|
290
|
+
const isNewStage = lastExecutedStageName !== stage.name;
|
|
291
|
+
if (ctxWin && execStageCount > 0 && isNewStage) {
|
|
286
292
|
ctxWin.resetForNewStage();
|
|
293
|
+
} else if (ctxWin && execStageCount > 0 && !isNewStage) {
|
|
294
|
+
_pipelineLogger.info(
|
|
295
|
+
`[PipelineStrategy] ♻️ Retry stage "${stage.name}" — preserving ContextWindow (${ctxWin.tokenCount || 0} tokens)`,
|
|
296
|
+
);
|
|
287
297
|
}
|
|
288
298
|
|
|
289
299
|
// 为每个阶段创建适当范围的 ExplorationTracker:
|
|
290
300
|
// - analyze → 复用 orchestrator 创建的 bootstrap tracker (首个阶段)
|
|
291
301
|
// - produce → 创建 producer 策略的独立 tracker
|
|
292
302
|
// - 其他 → 创建 analyst 策略的独立 tracker
|
|
303
|
+
// B4 fix: 从 stage 配置 + strategyContext 双重来源解析 submitToolName,
|
|
304
|
+
// 确保 retry 时 tracker 的 submitToolName 与原阶段一致
|
|
293
305
|
let stageTracker = strategyContext.tracker || null;
|
|
306
|
+
const submitToolName = stage.submitToolName || strategyContext.submitToolName || undefined;
|
|
294
307
|
if (stageTracker && execStageCount > 0) {
|
|
295
308
|
const trackerStrategy = (stage.name === 'produce' || stage.name === 'producer')
|
|
296
309
|
? 'producer'
|
|
297
310
|
: 'analyst';
|
|
298
311
|
stageTracker = ExplorationTracker.resolve(
|
|
299
312
|
{ source: strategyContext.source || 'system', strategy: trackerStrategy },
|
|
300
|
-
effectiveBudget || {},
|
|
313
|
+
{ ...(effectiveBudget || {}), ...(submitToolName ? { submitToolName } : {}) },
|
|
301
314
|
);
|
|
315
|
+
} else if (stageTracker && execStageCount === 0 && submitToolName) {
|
|
316
|
+
// 首阶段: 如果 stage 指定了 submitToolName 但 orchestrator 创建的 tracker 未设置,
|
|
317
|
+
// 确保首阶段 tracker 也感知到正确的 submitToolName
|
|
318
|
+
if (stageTracker.submitToolName !== submitToolName) {
|
|
319
|
+
stageTracker = ExplorationTracker.resolve(
|
|
320
|
+
{ source: strategyContext.source || 'system', strategy: 'analyst' },
|
|
321
|
+
{ ...(effectiveBudget || {}), submitToolName },
|
|
322
|
+
);
|
|
323
|
+
}
|
|
302
324
|
}
|
|
325
|
+
lastExecutedStageName = stage.name;
|
|
303
326
|
execStageCount++;
|
|
304
327
|
|
|
328
|
+
_pipelineLogger.info(
|
|
329
|
+
`[PipelineStrategy] ▶ Stage "${stage.name}"${isRetry ? ' (retry)' : ''} — ` +
|
|
330
|
+
`budget: ${effectiveBudget?.maxIterations || '∞'} iters, ` +
|
|
331
|
+
`timeout: ${effectiveBudget?.timeoutMs ? (effectiveBudget.timeoutMs / 1000) + 's' : '∞'}, ` +
|
|
332
|
+
`tracker: ${stageTracker?.constructor?.name || 'none'}` +
|
|
333
|
+
`${submitToolName ? `, submitTool: ${submitToolName}` : ''}`,
|
|
334
|
+
);
|
|
335
|
+
|
|
305
336
|
// ── 执行 reactLoop (含 per-stage 硬超时保护) ──
|
|
306
337
|
const reactPromise = runtime.reactLoop(stagePrompt, {
|
|
307
338
|
history: message.history,
|
|
@@ -367,6 +398,14 @@ export class PipelineStrategy extends Strategy {
|
|
|
367
398
|
totalTokenUsage.output += stageResult.tokenUsage.output || 0;
|
|
368
399
|
}
|
|
369
400
|
|
|
401
|
+
const stageToolCount = stageResult.toolCalls?.length || 0;
|
|
402
|
+
const stageReplyLen = stageResult.reply?.length || 0;
|
|
403
|
+
_pipelineLogger.info(
|
|
404
|
+
`[PipelineStrategy] ✅ Stage "${stage.name}" done — ` +
|
|
405
|
+
`${stageResult.iterations || 0} iters, ${stageToolCount} tool calls, ` +
|
|
406
|
+
`reply: ${stageReplyLen} chars${stageResult.timedOut ? ' (TIMED OUT)' : ''}`,
|
|
407
|
+
);
|
|
408
|
+
|
|
370
409
|
bus.publish(AgentEvents.PROGRESS, {
|
|
371
410
|
type: 'pipeline_stage_done',
|
|
372
411
|
stage: stage.name,
|