autosnippet 3.2.3 → 3.2.6

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 (64) hide show
  1. package/README.md +2 -4
  2. package/bin/cli.js +164 -145
  3. package/config/constitution.yaml +3 -0
  4. package/dashboard/dist/assets/{index-DNOHYBhy.css → index-BaGY7kJI.css} +1 -1
  5. package/dashboard/dist/assets/{index-6itPuGFl.js → index-DfHY_3ln.js} +25 -25
  6. package/dashboard/dist/index.html +2 -2
  7. package/lib/cli/CliLogger.js +78 -0
  8. package/lib/cli/SetupService.js +9 -719
  9. package/lib/cli/UpgradeService.js +23 -398
  10. package/lib/cli/deploy/FileDeployer.js +562 -0
  11. package/lib/cli/deploy/FileManifest.js +272 -0
  12. package/lib/external/mcp/McpServer.js +22 -26
  13. package/lib/external/mcp/autoApproveInjector.js +1 -0
  14. package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +5 -5
  15. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +25 -3
  16. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +6 -6
  17. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +4 -0
  18. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +5 -5
  19. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +89 -44
  20. package/lib/external/mcp/handlers/consolidated.js +8 -9
  21. package/lib/external/mcp/handlers/dimension-complete-external.js +4 -4
  22. package/lib/external/mcp/handlers/guard.js +283 -5
  23. package/lib/external/mcp/handlers/task.js +183 -9
  24. package/lib/external/mcp/tools.js +32 -81
  25. package/lib/http/routes/task.js +55 -0
  26. package/lib/service/chat/AnalystAgent.js +12 -12
  27. package/lib/service/chat/ChatAgent.js +227 -545
  28. package/lib/service/chat/ChatAgentPrompts.js +9 -11
  29. package/lib/service/chat/ContextWindow.js +2 -296
  30. package/lib/service/chat/EpisodicConsolidator.js +15 -15
  31. package/lib/service/chat/ExplorationTracker.js +1262 -0
  32. package/lib/service/chat/HandoffProtocol.js +8 -9
  33. package/lib/service/chat/Memory.js +4 -0
  34. package/lib/service/chat/ProducerAgent.js +9 -6
  35. package/lib/service/chat/ProjectSemanticMemory.js +4 -0
  36. package/lib/service/chat/ReasoningTrace.js +182 -0
  37. package/lib/service/chat/WorkingMemory.js +4 -0
  38. package/lib/service/chat/memory/ActiveContext.js +910 -0
  39. package/lib/service/chat/memory/MemoryCoordinator.js +662 -0
  40. package/lib/service/chat/memory/PersistentMemory.js +450 -0
  41. package/lib/service/chat/memory/SessionStore.js +896 -0
  42. package/lib/service/chat/memory/index.js +13 -0
  43. package/lib/service/chat/tools/ast-graph.js +17 -16
  44. package/lib/service/cursor/AgentInstructionsGenerator.js +75 -40
  45. package/lib/service/cursor/FileProtection.js +4 -1
  46. package/lib/service/guard/GuardCheckEngine.js +10 -3
  47. package/lib/service/task/TaskGraphService.js +3 -3
  48. package/lib/shared/LanguageService.js +2 -1
  49. package/package.json +1 -1
  50. package/skills/autosnippet-intent/SKILL.md +1 -3
  51. package/skills/autosnippet-recipes/SKILL.md +1 -3
  52. package/templates/claude-code/commands/prime.md +19 -0
  53. package/templates/claude-code/hooks/autosnippet-session.sh +63 -0
  54. package/templates/claude-code/settings.json +21 -0
  55. package/templates/copilot-instructions.md +66 -177
  56. package/templates/cursor-hooks/commands/prime.md +12 -0
  57. package/templates/cursor-hooks/hooks/session-start.sh +10 -0
  58. package/templates/cursor-hooks/hooks.json +11 -0
  59. package/templates/cursor-rules/autosnippet-conventions.mdc +52 -3
  60. package/templates/cursor-rules/autosnippet-workflow.mdc +51 -27
  61. package/lib/external/mcp/handlers/decide.js +0 -109
  62. package/lib/external/mcp/handlers/ready.js +0 -42
  63. package/lib/service/chat/ReasoningLayer.js +0 -888
  64. package/templates/claude-hooks.yaml +0 -19
@@ -0,0 +1,910 @@
1
+ /**
2
+ * ActiveContext — 合并 WorkingMemory + ReasoningTrace 为统一的会话工作记忆
3
+ *
4
+ * 设计来源: docs/copilot/memory-system-redesign.md §4.3, §6.2
5
+ *
6
+ * 三个内部子区:
7
+ * 1. Scratchpad — Agent 通过 note_finding 主动标记的发现 (不可压缩)
8
+ * 2. ObservationLog — 每轮 ReAct 记录 (合并原 RT.rounds + WM.observations,滑动窗口压缩)
9
+ * 3. Plan — 从 ReasoningTrace 继承的规划追踪
10
+ *
11
+ * 替代关系:
12
+ * WorkingMemory.js → Scratchpad + 工具压缩策略 + buildContext + distill
13
+ * ReasoningTrace.js → rounds + plan + thoughts + extractAndSetPlan + observations
14
+ *
15
+ * 兼容性:
16
+ * - 提供所有 ReasoningTrace 和 WorkingMemory 的公共方法
17
+ * - ExplorationTracker 可直接使用 ActiveContext 作为 trace 参数 (L5 缓解)
18
+ * - MemoryCoordinator 通过 createDimensionScope 创建实例
19
+ *
20
+ * 生命周期: 单次 execute() 调用 (由 MemoryCoordinator 管理创建/蒸馏/销毁)
21
+ *
22
+ * @module ActiveContext
23
+ */
24
+
25
+ import Logger from '../../../infrastructure/logging/Logger.js';
26
+
27
+ // ═══════════════════════════════════════════════════════════
28
+ // §1: 工具压缩策略 (从 WorkingMemory 迁入)
29
+ // ═══════════════════════════════════════════════════════════
30
+
31
+ /**
32
+ * 工具特化压缩策略 — 不同工具返回不同结构,压缩时保留最有价值的部分
33
+ */
34
+ const TOOL_COMPRESS_STRATEGIES = {
35
+ search_project_code(result) {
36
+ if (typeof result !== 'object') {
37
+ return String(result).substring(0, 600);
38
+ }
39
+ const matches = result.matches || [];
40
+ const batchResults = result.batchResults || {};
41
+ const lines = [];
42
+ if (matches.length > 0) {
43
+ lines.push(`搜索到 ${matches.length} 个匹配`);
44
+ const fileGroups = {};
45
+ for (const m of matches) {
46
+ if (!fileGroups[m.file]) fileGroups[m.file] = [];
47
+ fileGroups[m.file].push(m.line);
48
+ }
49
+ for (const [file, lineNums] of Object.entries(fileGroups).slice(0, 8)) {
50
+ lines.push(` ${file}: L${lineNums.slice(0, 3).join(',')}`);
51
+ }
52
+ }
53
+ for (const [pattern, sub] of Object.entries(batchResults).slice(0, 5)) {
54
+ const subMatches = sub.matches || [];
55
+ lines.push(` [${pattern}] ${subMatches.length} 个匹配`);
56
+ for (const m of subMatches.slice(0, 3)) {
57
+ lines.push(` ${m.file}:${m.line}`);
58
+ }
59
+ }
60
+ return lines.join('\n');
61
+ },
62
+
63
+ read_project_file(result) {
64
+ if (typeof result !== 'object') {
65
+ return String(result).substring(0, 600);
66
+ }
67
+ if (result.files) {
68
+ const lines = [`读取 ${result.files.length} 个文件`];
69
+ for (const f of result.files.slice(0, 5)) {
70
+ const totalLines = (f.content || '').split('\n').length;
71
+ lines.push(` ${f.path} (${totalLines} 行)`);
72
+ }
73
+ return lines.join('\n');
74
+ }
75
+ const content = result.content || String(result);
76
+ const totalLines = content.split('\n').length;
77
+ return `文件 ${result.path || '?'} (${totalLines} 行)`;
78
+ },
79
+
80
+ get_class_info(result) {
81
+ if (typeof result !== 'object') {
82
+ return String(result).substring(0, 600);
83
+ }
84
+ const lines = [`类 ${result.className || '?'}`];
85
+ if (result.superClass) lines.push(` 继承: ${result.superClass}`);
86
+ if (result.protocols?.length) lines.push(` 协议: ${result.protocols.join(', ')}`);
87
+ if (result.methods?.length) lines.push(` 方法数: ${result.methods.length}`);
88
+ if (result.properties?.length) lines.push(` 属性数: ${result.properties.length}`);
89
+ return lines.join('\n');
90
+ },
91
+
92
+ get_class_hierarchy(result) {
93
+ if (typeof result !== 'object') {
94
+ return String(result).substring(0, 600);
95
+ }
96
+ const classes = result.classes || result.hierarchy || [];
97
+ return `类层级: ${Array.isArray(classes) ? classes.length : 0} 个类`;
98
+ },
99
+
100
+ get_project_overview(result) {
101
+ if (typeof result !== 'object') {
102
+ return String(result).substring(0, 800);
103
+ }
104
+ return JSON.stringify(result).substring(0, 800);
105
+ },
106
+
107
+ list_project_structure(result) {
108
+ if (typeof result !== 'object') {
109
+ return String(result).substring(0, 600);
110
+ }
111
+ const entries = result.entries || result.children || [];
112
+ return `目录结构: ${Array.isArray(entries) ? entries.length : 0} 个条目`;
113
+ },
114
+ };
115
+
116
+ /**
117
+ * 默认压缩 — 截断到 maxChars
118
+ */
119
+ function defaultCompress(result, maxChars = 600) {
120
+ const str = typeof result === 'string' ? result : JSON.stringify(result);
121
+ if (str.length <= maxChars) return str;
122
+ return `${str.substring(0, maxChars)}…(truncated)`;
123
+ }
124
+
125
+ // ═══════════════════════════════════════════════════════════
126
+ // §2: ActiveContext 类
127
+ // ═══════════════════════════════════════════════════════════
128
+
129
+ export class ActiveContext {
130
+ // ── 子区 1: Scratchpad (从 WorkingMemory 继承, 不可压缩) ──
131
+ /** @type {Array<{finding: string, evidence: string, importance: number, round: number}>} */
132
+ #scratchpad = [];
133
+
134
+ // ── 子区 2: ObservationLog (合并 RT.rounds + WM.observations) ──
135
+ /** @type {Array<Round>} */
136
+ #rounds = [];
137
+ /** @type {Round|null} */
138
+ #currentRound = null;
139
+
140
+ // ── WM 滑动窗口 (保留最近 N 轮原始结果,旧的压缩) ──
141
+ /** @type {Array<{toolName: string, result: any, round: number, timestamp: number}>} */
142
+ #recentObservations = [];
143
+ /** @type {Array<{toolName: string, round: number, summary: string}>} */
144
+ #compressedObservations = [];
145
+
146
+ // ── 子区 3: Plan (从 ReasoningTrace 继承) ──
147
+ /** @type {Plan|null} */
148
+ #plan = null;
149
+ /** @type {Array<Plan>} */
150
+ #planHistory = [];
151
+
152
+ // ── 配置 ──
153
+ /** @type {number} 保留最近 N 轮原始观察 */
154
+ #maxRecentRounds;
155
+ /** @type {boolean} 轻量模式 (User Chat: 仅 RT 功能,禁用 WM 压缩/Scratchpad) */
156
+ #lightweight;
157
+ /** @type {number} 总观察计数 */
158
+ #totalObservations = 0;
159
+
160
+ /** @type {import('../../../infrastructure/logging/Logger.js').default} */
161
+ #logger;
162
+
163
+ /**
164
+ * @param {object} [options]
165
+ * @param {number} [options.maxRecentRounds=3] - 保留最近 N 轮原始结果 (WM 滑动窗口)
166
+ * @param {boolean} [options.lightweight=false] - 轻量模式: 跳过 WM 的压缩/Scratchpad 逻辑 (D5)
167
+ */
168
+ constructor(options = {}) {
169
+ this.#maxRecentRounds = options.maxRecentRounds ?? 3;
170
+ this.#lightweight = options.lightweight ?? false;
171
+ this.#logger = Logger.getInstance();
172
+ }
173
+
174
+ // ═══════════════════════════════════════════════════════
175
+ // §2.1: 轮次管理 (合并 RT.startRound/endRound)
176
+ // ═══════════════════════════════════════════════════════
177
+
178
+ /**
179
+ * 开始新一轮推理
180
+ * @param {number} iteration — 轮次编号
181
+ */
182
+ startRound(iteration) {
183
+ if (this.#currentRound) {
184
+ this.endRound(); // 安全关闭上一轮
185
+ }
186
+ this.#currentRound = {
187
+ iteration,
188
+ thought: null,
189
+ actions: [],
190
+ observations: [],
191
+ reflection: null,
192
+ roundSummary: null,
193
+ startTime: Date.now(),
194
+ endTime: null,
195
+ };
196
+ }
197
+
198
+ /**
199
+ * 结束当前轮次
200
+ */
201
+ endRound() {
202
+ if (this.#currentRound) {
203
+ this.#currentRound.endTime = Date.now();
204
+ this.#rounds.push(this.#currentRound);
205
+ this.#currentRound = null;
206
+ }
207
+ }
208
+
209
+ // ═══════════════════════════════════════════════════════
210
+ // §2.2: 记录 (合并 WM.observe + RT.addAction/addObservation)
211
+ // ═══════════════════════════════════════════════════════
212
+
213
+ /**
214
+ * 记录 AI 的推理文本(从 aiResult.text 提取)
215
+ * @param {string} text
216
+ */
217
+ setThought(text) {
218
+ if (this.#currentRound && text) {
219
+ this.#currentRound.thought = text;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * 统一记录一次工具调用 — 合并原 WM.observe() + RT.addAction() + RT.addObservation()
225
+ *
226
+ * @param {string} toolName - 工具名称
227
+ * @param {object} args - 工具参数
228
+ * @param {*} result - 工具返回的原始结果
229
+ * @param {boolean} isNew - 是否发现新信息 (由 ExplorationTracker.recordToolCall 提供)
230
+ */
231
+ recordToolCall(toolName, args, result, isNew) {
232
+ const round = this.#currentRound?.iteration || 0;
233
+
234
+ // ── RT 部分: Action + Observation ──
235
+ this.#currentRound?.actions.push({ tool: toolName, params: args });
236
+ const observationMeta = ActiveContext.buildObservationMeta(toolName, args, result, isNew);
237
+ this.#currentRound?.observations.push({ tool: toolName, ...observationMeta });
238
+
239
+ // ── WM 部分: 滑动窗口压缩 (非轻量模式) ──
240
+ if (!this.#lightweight) {
241
+ this.#totalObservations++;
242
+ this.#recentObservations.push({
243
+ toolName,
244
+ result,
245
+ round,
246
+ timestamp: Date.now(),
247
+ });
248
+
249
+ while (this.#recentObservations.length > this.#maxRecentRounds) {
250
+ const oldest = this.#recentObservations.shift();
251
+ const summary = this.#compressObservation(oldest);
252
+ this.#compressedObservations.push(summary);
253
+ }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * 兼容旧 RT API: 记录一次工具调用 (Action only)
259
+ * @param {string} toolName
260
+ * @param {object} params
261
+ */
262
+ addAction(toolName, params) {
263
+ this.#currentRound?.actions.push({ tool: toolName, params });
264
+ }
265
+
266
+ /**
267
+ * 兼容旧 RT API: 记录一次工具结果的结构化观察
268
+ * @param {string} toolName
269
+ * @param {object} meta
270
+ */
271
+ addObservation(toolName, meta) {
272
+ this.#currentRound?.observations.push({ tool: toolName, ...meta });
273
+ }
274
+
275
+ /**
276
+ * 兼容旧 WM API: 记录工具调用结果 (Observe, 仅 WM 滑动窗口)
277
+ * @param {string} toolName
278
+ * @param {*} result
279
+ * @param {number} round
280
+ */
281
+ observe(toolName, result, round) {
282
+ if (this.#lightweight) return;
283
+ this.#totalObservations++;
284
+ this.#recentObservations.push({ toolName, result, round, timestamp: Date.now() });
285
+ while (this.#recentObservations.length > this.#maxRecentRounds) {
286
+ const oldest = this.#recentObservations.shift();
287
+ const summary = this.#compressObservation(oldest);
288
+ this.#compressedObservations.push(summary);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * 记录反思内容 (ExplorationTracker 使用, L5 修复)
294
+ * @param {string} text
295
+ */
296
+ setReflection(text) {
297
+ if (this.#currentRound && text) {
298
+ this.#currentRound.reflection = text;
299
+ }
300
+ }
301
+
302
+ /**
303
+ * 记录轮次摘要
304
+ * @param {object} summary — { newInfoCount, totalCalls, submits, cumulativeFiles, cumulativePatterns }
305
+ */
306
+ setRoundSummary(summary) {
307
+ if (this.#currentRound) {
308
+ this.#currentRound.roundSummary = summary;
309
+ }
310
+ }
311
+
312
+ // ═══════════════════════════════════════════════════════
313
+ // §2.3: Scratchpad (从 WorkingMemory 继承)
314
+ // ═══════════════════════════════════════════════════════
315
+
316
+ /**
317
+ * Agent 主动记录关键发现 (note_finding 工具入口)
318
+ *
319
+ * @param {string} finding - 关键发现描述
320
+ * @param {string} [evidence] - 证据 (文件路径:行号)
321
+ * @param {number} [importance=5] - 重要性 1-10
322
+ * @param {number} [round=0] - 当前轮次
323
+ */
324
+ noteKeyFinding(finding, evidence = '', importance = 5, round = 0) {
325
+ this.#scratchpad.push({
326
+ finding,
327
+ evidence,
328
+ importance: Math.min(10, Math.max(1, importance)),
329
+ round,
330
+ });
331
+
332
+ this.#logger.debug(
333
+ `[ActiveContext] 📌 noted finding (${importance}/10): ${finding.substring(0, 80)}`
334
+ );
335
+ }
336
+
337
+ // ═══════════════════════════════════════════════════════
338
+ // §2.4: Plan (从 ReasoningTrace 继承)
339
+ // ═══════════════════════════════════════════════════════
340
+
341
+ /**
342
+ * 从 AI 响应文本中提取计划,自动调用 setPlan/updatePlan
343
+ * @param {string} text — AI 完整响应文本
344
+ * @param {number} iteration — 当前轮次
345
+ * @returns {boolean} — 是否成功提取到计划
346
+ */
347
+ extractAndSetPlan(text, iteration) {
348
+ const planText = this.#extractPlanFromText(text);
349
+ if (!planText) return false;
350
+
351
+ if (this.#plan) {
352
+ this.#updatePlan(planText, iteration);
353
+ } else {
354
+ this.#setPlan(planText, iteration);
355
+ }
356
+ return true;
357
+ }
358
+
359
+ /**
360
+ * 直接设置计划 (公开接口,供 ExplorationTracker 和测试使用)
361
+ * @param {string} planText
362
+ * @param {number} iteration
363
+ */
364
+ setPlan(planText, iteration) {
365
+ this.#setPlan(planText, iteration);
366
+ }
367
+
368
+ /**
369
+ * 更新计划 (保留旧 plan 到 history)
370
+ * @param {string} replanText
371
+ * @param {number} iteration
372
+ */
373
+ updatePlan(replanText, iteration) {
374
+ this.#updatePlan(replanText, iteration);
375
+ }
376
+
377
+ /**
378
+ * 获取当前计划 (只读副本)
379
+ * @returns {Plan|null}
380
+ */
381
+ getPlan() {
382
+ if (!this.#plan) return null;
383
+ return {
384
+ ...this.#plan,
385
+ steps: this.#plan.steps.map((s) => ({ ...s })),
386
+ };
387
+ }
388
+
389
+ /**
390
+ * 获取计划步骤的可变引用 (ExplorationTracker.updatePlanProgress 使用)
391
+ * @returns {Array<PlanStep>}
392
+ */
393
+ getPlanStepsMutable() {
394
+ return this.#plan?.steps || [];
395
+ }
396
+
397
+ /**
398
+ * 获取计划历史 (F7)
399
+ * @returns {Array<Plan>}
400
+ */
401
+ getPlanHistory() {
402
+ return this.#planHistory.map((p) => ({ ...p, steps: p.steps.map((s) => ({ ...s })) }));
403
+ }
404
+
405
+ /**
406
+ * 获取当前轮次的 actions (ExplorationTracker.updatePlanProgress 使用, L5 修复)
407
+ * @returns {Array<{tool: string, params: object}>}
408
+ */
409
+ getCurrentRoundActions() {
410
+ return this.#currentRound?.actions || [];
411
+ }
412
+
413
+ /**
414
+ * 获取当前轮次的 iteration 编号 (F8)
415
+ * @returns {number|null}
416
+ */
417
+ getCurrentIteration() {
418
+ return this.#currentRound?.iteration || null;
419
+ }
420
+
421
+ // ═══════════════════════════════════════════════════════
422
+ // §2.5: 上下文构建 (合并 WM.buildContext, 增加预算控制)
423
+ // ═══════════════════════════════════════════════════════
424
+
425
+ /**
426
+ * 构建当前工作记忆的上下文快照
427
+ * 用于注入到 system prompt 或 user nudge 中
428
+ *
429
+ * @param {number} [tokenBudget=Infinity] - token 预算 (新增: 预算控制)
430
+ * @returns {string} Markdown 格式的上下文块,空字符串表示无内容
431
+ */
432
+ buildContext(tokenBudget = Infinity) {
433
+ if (this.#lightweight) return '';
434
+
435
+ const parts = [];
436
+ let remaining = tokenBudget;
437
+
438
+ // §1: Scratchpad (最高优先级 — 不会被压缩)
439
+ if (this.#scratchpad.length > 0) {
440
+ const sorted = [...this.#scratchpad].sort((a, b) => b.importance - a.importance);
441
+ const scratchLines = ['## 📌 已确认的关键发现'];
442
+ for (const f of sorted) {
443
+ const badge = f.importance >= 8 ? '⚠️' : f.importance >= 5 ? '📋' : '💡';
444
+ let line = `- ${badge} [${f.importance}/10] ${f.finding}`;
445
+ if (f.evidence) line += ` (${f.evidence})`;
446
+ scratchLines.push(line);
447
+ }
448
+ const scratchSection = scratchLines.join('\n');
449
+ const scratchTokens = this.#estimateTokens(scratchSection);
450
+ if (scratchTokens <= remaining) {
451
+ parts.push(scratchSection);
452
+ remaining -= scratchTokens;
453
+ }
454
+ }
455
+
456
+ // §2: 压缩后的旧观察摘要 (中等优先级)
457
+ if (this.#compressedObservations.length > 0 && remaining > 100) {
458
+ const obsLines = ['## 📂 之前的探索摘要'];
459
+ const maxItems = Math.min(15, this.#compressedObservations.length);
460
+ const recent = this.#compressedObservations.slice(-maxItems);
461
+
462
+ for (const s of recent) {
463
+ const line = `- [R${s.round}|${s.toolName}] ${s.summary.substring(0, 200)}`;
464
+ const lineTokens = this.#estimateTokens(line);
465
+ if (lineTokens > remaining) break;
466
+ obsLines.push(line);
467
+ remaining -= lineTokens;
468
+ }
469
+ if (this.#compressedObservations.length > maxItems) {
470
+ obsLines.push(` …(还有 ${this.#compressedObservations.length - maxItems} 条更早的观察)`);
471
+ }
472
+ if (obsLines.length > 1) {
473
+ parts.push(obsLines.join('\n'));
474
+ }
475
+ }
476
+
477
+ return parts.join('\n');
478
+ }
479
+
480
+ // ═══════════════════════════════════════════════════════
481
+ // §2.6: 蒸馏 (合并 WM.distill, 增强版 — 含 plan + stats)
482
+ // ═══════════════════════════════════════════════════════
483
+
484
+ /**
485
+ * 蒸馏 ActiveContext 为结构化报告
486
+ * 在 Agent execute 结束时调用,结果写入 SessionStore
487
+ *
488
+ * @returns {DistilledContext}
489
+ */
490
+ distill() {
491
+ return {
492
+ keyFindings: this.#scratchpad.map((f) => ({
493
+ finding: f.finding,
494
+ evidence: f.evidence,
495
+ importance: f.importance,
496
+ })),
497
+ toolCallSummary: this.#compressedObservations.map(
498
+ (s) => `[${s.toolName}] ${s.summary.substring(0, 150)}`
499
+ ),
500
+ stats: this.getStats(),
501
+ plan: this.getPlan(),
502
+ totalObservations: this.#totalObservations,
503
+ compressedCount: this.#compressedObservations.length,
504
+ };
505
+ }
506
+
507
+ // ═══════════════════════════════════════════════════════
508
+ // §2.7: 分析方法 (从 ReasoningTrace 继承)
509
+ // ═══════════════════════════════════════════════════════
510
+
511
+ /**
512
+ * 获取所有有 Thought 的轮次
513
+ * @returns {Array<{iteration: number, thought: string}>}
514
+ */
515
+ getThoughts() {
516
+ return this.#rounds
517
+ .filter((r) => r.thought)
518
+ .map((r) => ({ iteration: r.iteration, thought: r.thought }));
519
+ }
520
+
521
+ /**
522
+ * 获取最近 N 轮的紧凑摘要 (ExplorationTracker.#checkReflection 使用)
523
+ * @param {number} [n=3] — 回看轮数
524
+ * @returns {object|null}
525
+ */
526
+ getRecentSummary(n = 3) {
527
+ const recent = this.#rounds.slice(-n);
528
+ if (recent.length === 0) return null;
529
+
530
+ const thoughts = recent
531
+ .filter((r) => r.thought)
532
+ .map((r) => (r.thought.length > 100 ? `${r.thought.substring(0, 100)}…` : r.thought));
533
+
534
+ const tools = recent.flatMap((r) => r.actions.map((a) => a.tool));
535
+
536
+ const newInfoCount = recent.reduce(
537
+ (c, r) => c + r.observations.filter((o) => o.gotNewInfo).length,
538
+ 0
539
+ );
540
+ const totalObs = recent.reduce((c, r) => c + r.observations.length, 0);
541
+
542
+ return {
543
+ roundCount: recent.length,
544
+ thoughts,
545
+ toolCalls: tools,
546
+ newInfoRatio: totalObs > 0 ? newInfoCount / totalObs : 0,
547
+ lastIteration: recent[recent.length - 1].iteration,
548
+ };
549
+ }
550
+
551
+ /**
552
+ * 统计指标 (ExplorationTracker.getQualityMetrics 使用)
553
+ * @returns {object}
554
+ */
555
+ getStats() {
556
+ return {
557
+ totalRounds: this.#rounds.length,
558
+ thoughtCount: this.#rounds.filter((r) => r.thought).length,
559
+ totalActions: this.#rounds.reduce((c, r) => c + r.actions.length, 0),
560
+ totalObservations: this.#rounds.reduce((c, r) => c + r.observations.length, 0),
561
+ reflectionCount: this.#rounds.filter((r) => r.reflection).length,
562
+ totalDurationMs: this.#rounds.reduce(
563
+ (d, r) => d + ((r.endTime || Date.now()) - r.startTime),
564
+ 0
565
+ ),
566
+ };
567
+ }
568
+
569
+ // ═══════════════════════════════════════════════════════
570
+ // §2.8: Scratchpad 查询 (从 WorkingMemory 继承)
571
+ // ═══════════════════════════════════════════════════════
572
+
573
+ /**
574
+ * 获取 scratchpad 中的关键发现数量
575
+ * @returns {number}
576
+ */
577
+ get scratchpadSize() {
578
+ return this.#scratchpad.length;
579
+ }
580
+
581
+ /**
582
+ * 获取总观察数
583
+ * @returns {number}
584
+ */
585
+ get totalObservations() {
586
+ return this.#totalObservations;
587
+ }
588
+
589
+ /**
590
+ * 获取 scratchpad 中的高重要性发现
591
+ * @param {number} [minImportance=7]
592
+ * @returns {Array<{finding: string, evidence: string, importance: number}>}
593
+ */
594
+ getHighPriorityFindings(minImportance = 7) {
595
+ return this.#scratchpad
596
+ .filter((f) => f.importance >= minImportance)
597
+ .sort((a, b) => b.importance - a.importance);
598
+ }
599
+
600
+ // ═══════════════════════════════════════════════════════
601
+ // §2.9: 序列化 (从 ReasoningTrace 继承)
602
+ // ═══════════════════════════════════════════════════════
603
+
604
+ /**
605
+ * 可序列化输出
606
+ * @returns {object}
607
+ */
608
+ toJSON() {
609
+ return {
610
+ rounds: this.#rounds.map((r) => ({ ...r })),
611
+ stats: this.getStats(),
612
+ scratchpad: this.#scratchpad.map((f) => ({ ...f })),
613
+ compressedObservations: this.#compressedObservations.length,
614
+ totalObservations: this.#totalObservations,
615
+ ...(this.#plan
616
+ ? {
617
+ plan: {
618
+ text: this.#plan.text,
619
+ steps: this.#plan.steps.map((s) => ({ ...s })),
620
+ createdAtIteration: this.#plan.createdAtIteration,
621
+ lastUpdatedAtIteration: this.#plan.lastUpdatedAtIteration,
622
+ },
623
+ planHistory: this.#planHistory.length,
624
+ }
625
+ : {}),
626
+ };
627
+ }
628
+
629
+ /**
630
+ * 从 JSON 恢复 ActiveContext (断点续传)
631
+ * @param {object} json - toJSON() 的输出
632
+ * @returns {ActiveContext}
633
+ */
634
+ static fromJSON(json) {
635
+ const ctx = new ActiveContext();
636
+ if (json.rounds) {
637
+ ctx.#rounds = json.rounds.map((r) => ({ ...r }));
638
+ }
639
+ if (json.scratchpad) {
640
+ ctx.#scratchpad = json.scratchpad.map((f) => ({ ...f }));
641
+ }
642
+ if (json.totalObservations) {
643
+ ctx.#totalObservations = json.totalObservations;
644
+ }
645
+ if (json.plan) {
646
+ ctx.#plan = {
647
+ text: json.plan.text,
648
+ steps: json.plan.steps.map((s) => ({ ...s })),
649
+ createdAtIteration: json.plan.createdAtIteration,
650
+ lastUpdatedAtIteration: json.plan.lastUpdatedAtIteration,
651
+ };
652
+ }
653
+ return ctx;
654
+ }
655
+
656
+ /**
657
+ * 清空 ActiveContext — 释放内存
658
+ */
659
+ clear() {
660
+ this.#scratchpad.length = 0;
661
+ this.#rounds.length = 0;
662
+ this.#currentRound = null;
663
+ this.#recentObservations.length = 0;
664
+ this.#compressedObservations.length = 0;
665
+ this.#plan = null;
666
+ this.#planHistory.length = 0;
667
+ this.#totalObservations = 0;
668
+ }
669
+
670
+ // ═══════════════════════════════════════════════════════
671
+ // §2.10: 静态工具 (从 ReasoningTrace 迁入)
672
+ // ═══════════════════════════════════════════════════════
673
+
674
+ /**
675
+ * 从工具执行结果构建结构化观察元数据
676
+ * 不改变工具结果传给 AI 的内容,只影响推理链记录
677
+ *
678
+ * @param {string} toolName
679
+ * @param {object} args
680
+ * @param {*} result
681
+ * @param {boolean} isNew — 由 ExplorationTracker.recordToolCall 提供
682
+ * @returns {{ gotNewInfo: boolean, resultType: string, keyFacts: string[], resultSize: number }}
683
+ */
684
+ static buildObservationMeta(toolName, args, result, isNew) {
685
+ const meta = {
686
+ gotNewInfo: isNew,
687
+ resultType: 'unknown',
688
+ keyFacts: [],
689
+ resultSize: 0,
690
+ };
691
+
692
+ const resultStr = typeof result === 'string' ? result : JSON.stringify(result || '');
693
+ meta.resultSize = resultStr.length;
694
+
695
+ switch (toolName) {
696
+ case 'search_project_code': {
697
+ meta.resultType = 'search';
698
+ const matches = result?.matches || [];
699
+ const batchResults = result?.batchResults;
700
+ const totalMatches = batchResults
701
+ ? Object.values(batchResults).reduce((s, br) => s + (br.matches?.length || 0), 0)
702
+ : matches.length;
703
+ meta.keyFacts.push(`${totalMatches} matches found`);
704
+ if (isNew) meta.keyFacts.push('new files discovered');
705
+ break;
706
+ }
707
+ case 'read_project_file': {
708
+ meta.resultType = 'file_content';
709
+ const fp = args?.filePath || '';
710
+ const fps = args?.filePaths || [];
711
+ const allPaths = fp ? [fp, ...fps] : fps;
712
+ meta.keyFacts.push(`read ${allPaths.length} file(s)`);
713
+ break;
714
+ }
715
+ case 'submit_knowledge':
716
+ case 'submit_with_check': {
717
+ meta.resultType = 'submit';
718
+ meta.gotNewInfo = true;
719
+ const status = typeof result === 'object' ? result?.status || 'ok' : 'ok';
720
+ const title = args?.title || '(untitled)';
721
+ meta.keyFacts.push(`submit "${title}": ${status}`);
722
+ break;
723
+ }
724
+ case 'list_project_structure': {
725
+ meta.resultType = 'structure';
726
+ meta.keyFacts.push(`list ${args?.directory || '/'}`);
727
+ break;
728
+ }
729
+ case 'get_class_info':
730
+ case 'get_class_hierarchy':
731
+ case 'get_protocol_info':
732
+ case 'get_method_overrides':
733
+ case 'get_category_map': {
734
+ meta.resultType = 'ast_query';
735
+ const target = args?.className || args?.protocolName || args?.name || '';
736
+ meta.keyFacts.push(`${toolName}(${target})`);
737
+ break;
738
+ }
739
+ case 'get_project_overview': {
740
+ meta.resultType = 'overview';
741
+ meta.keyFacts.push('project overview');
742
+ break;
743
+ }
744
+ case 'semantic_search_code':
745
+ case 'get_file_summary':
746
+ case 'get_previous_analysis': {
747
+ meta.resultType = 'query';
748
+ meta.keyFacts.push(toolName);
749
+ break;
750
+ }
751
+ default: {
752
+ meta.resultType = 'other';
753
+ meta.keyFacts.push(toolName);
754
+ }
755
+ }
756
+
757
+ return meta;
758
+ }
759
+
760
+ // ═══════════════════════════════════════════════════════
761
+ // §3: 私有方法
762
+ // ═══════════════════════════════════════════════════════
763
+
764
+ /**
765
+ * 工具结果压缩 — 使用特化策略 (从 WorkingMemory 迁入)
766
+ * @param {{toolName: string, result: any, round: number}} observation
767
+ * @returns {{toolName: string, round: number, summary: string}}
768
+ */
769
+ #compressObservation(observation) {
770
+ const strategy = TOOL_COMPRESS_STRATEGIES[observation.toolName];
771
+ let summary;
772
+ try {
773
+ summary = strategy ? strategy(observation.result) : defaultCompress(observation.result);
774
+ } catch {
775
+ summary = defaultCompress(observation.result);
776
+ }
777
+ return {
778
+ toolName: observation.toolName,
779
+ round: observation.round,
780
+ summary,
781
+ };
782
+ }
783
+
784
+ /**
785
+ * 粗糙 token 估算 (1 token ≈ 4 chars)
786
+ * @param {string} text
787
+ * @returns {number}
788
+ */
789
+ #estimateTokens(text) {
790
+ return Math.ceil((text || '').length / 4);
791
+ }
792
+
793
+ // ── Plan 内部方法 (从 ReasoningTrace 迁入) ──
794
+
795
+ /**
796
+ * @param {string} planText
797
+ * @param {number} iteration
798
+ */
799
+ #setPlan(planText, iteration) {
800
+ this.#plan = {
801
+ text: planText,
802
+ steps: this.#parsePlanSteps(planText),
803
+ createdAtIteration: iteration,
804
+ lastUpdatedAtIteration: iteration,
805
+ };
806
+ }
807
+
808
+ /**
809
+ * @param {string} replanText
810
+ * @param {number} iteration
811
+ */
812
+ #updatePlan(replanText, iteration) {
813
+ if (!this.#plan) {
814
+ this.#setPlan(replanText, iteration);
815
+ return;
816
+ }
817
+ this.#planHistory.push({ ...this.#plan, steps: this.#plan.steps.map((s) => ({ ...s })) });
818
+ this.#plan.text = replanText;
819
+ this.#plan.steps = this.#parsePlanSteps(replanText);
820
+ this.#plan.lastUpdatedAtIteration = iteration;
821
+ }
822
+
823
+ /**
824
+ * 从 AI 文本中解析计划步骤
825
+ * @param {string} text
826
+ * @returns {Array<PlanStep>}
827
+ */
828
+ #parsePlanSteps(text) {
829
+ if (!text) return [];
830
+ const lines = text.split('\n');
831
+ const steps = [];
832
+ for (const line of lines) {
833
+ const m = line.match(/^\s*(?:\d+[.)]\s*|[-*]\s+)(.+)/);
834
+ if (m && m[1].trim().length > 5) {
835
+ steps.push({
836
+ description: m[1].trim(),
837
+ status: 'pending',
838
+ keywords: this.#extractKeywords(m[1]),
839
+ });
840
+ }
841
+ }
842
+ return steps;
843
+ }
844
+
845
+ /**
846
+ * 从步骤描述中提取关键词
847
+ * @param {string} text
848
+ * @returns {string[]}
849
+ */
850
+ #extractKeywords(text) {
851
+ const quoted = [...text.matchAll(/[`"']([A-Za-z_]\w{2,})[`"']/g)].map((m) => m[1]);
852
+ const camelCase = [...text.matchAll(/\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g)].map((m) => m[0]);
853
+ const acronyms = [...text.matchAll(/\b([A-Z]{2,}[a-z]\w+)\b/g)].map((m) => m[0]);
854
+ return [...new Set([...quoted, ...camelCase, ...acronyms])];
855
+ }
856
+
857
+ /**
858
+ * 从 AI 响应文本中提取"计划"部分
859
+ * @param {string} text
860
+ * @returns {string|null}
861
+ */
862
+ #extractPlanFromText(text) {
863
+ if (!text || text.length < 30) return null;
864
+
865
+ const searchArea = text.substring(0, 2000);
866
+
867
+ const planMarkers = [
868
+ /(?:探索|分析)?计划[::\s]/i,
869
+ /(?:my\s+)?plan[::\s]/i,
870
+ /步骤[::\s]/i,
871
+ /以下是.*(?:计划|步骤)/i,
872
+ ];
873
+
874
+ let planStart = -1;
875
+ for (const marker of planMarkers) {
876
+ const match = searchArea.match(marker);
877
+ if (match) {
878
+ planStart = match.index + match[0].length;
879
+ break;
880
+ }
881
+ }
882
+
883
+ if (planStart === -1) {
884
+ const listMatch = searchArea.match(/\n\s*1[.)]\s+/);
885
+ if (listMatch) planStart = listMatch.index;
886
+ }
887
+
888
+ if (planStart === -1) return null;
889
+
890
+ const remaining = searchArea.substring(planStart);
891
+ const lines = remaining.split('\n');
892
+ const planLines = [];
893
+ let inList = false;
894
+
895
+ for (const line of lines) {
896
+ if (/^\s*(?:\d+[.)]\s+|[-*]\s+)/.test(line)) {
897
+ inList = true;
898
+ planLines.push(line);
899
+ } else if (inList && line.trim() === '') {
900
+ break;
901
+ } else if (inList) {
902
+ break;
903
+ }
904
+ }
905
+
906
+ return planLines.length >= 2 ? planLines.join('\n').trim() : null;
907
+ }
908
+ }
909
+
910
+ export default ActiveContext;