autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
796 lines (795 loc) • 34.2 kB
JavaScript
/**
* ActiveContext — 合并 WorkingMemory + ReasoningTrace 为统一的会话工作记忆
*
* 三个内部子区:
* 1. Scratchpad — Agent 通过 note_finding 主动标记的发现 (不可压缩)
* 2. ObservationLog — 每轮 ReAct 记录 (合并原 RT.rounds + WM.observations,滑动窗口压缩)
* 3. Plan — 从 ReasoningTrace 继承的规划追踪
*
* 替代关系:
* WorkingMemory.js → Scratchpad + 工具压缩策略 + buildContext + distill
* ReasoningTrace.js → rounds + plan + thoughts + extractAndSetPlan + observations
*
* 兼容性:
* - 提供所有 ReasoningTrace 和 WorkingMemory 的公共方法
* - ExplorationTracker 可直接使用 ActiveContext 作为 trace 参数 (L5 缓解)
* - MemoryCoordinator 通过 createDimensionScope 创建实例
*
* 生命周期: 单次 execute() 调用 (由 MemoryCoordinator 管理创建/蒸馏/销毁)
*
* @module ActiveContext
*/
import Logger from '#infra/logging/Logger.js';
// ═══════════════════════════════════════════════════════════
// §1: 工具压缩策略 (从 WorkingMemory 迁入)
// ═══════════════════════════════════════════════════════════
/** 工具特化压缩策略 — 不同工具返回不同结构,压缩时保留最有价值的部分 */
const TOOL_COMPRESS_STRATEGIES = {
search_project_code(result) {
if (typeof result !== 'object' || result === null) {
return String(result).substring(0, 600);
}
const r = result;
const matches = (Array.isArray(r.matches) ? r.matches : []);
const batchResults = (r.batchResults || {});
const lines = [];
if (matches.length > 0) {
lines.push(`搜索到 ${matches.length} 个匹配`);
const fileGroups = {};
for (const m of matches) {
if (!fileGroups[m.file]) {
fileGroups[m.file] = [];
}
fileGroups[m.file].push(m.line);
}
for (const [file, lineNums] of Object.entries(fileGroups).slice(0, 8)) {
lines.push(` ${file}: L${lineNums.slice(0, 3).join(',')}`);
}
}
for (const [pattern, sub] of Object.entries(batchResults).slice(0, 5)) {
const subMatches = (Array.isArray(sub.matches) ? sub.matches : []);
lines.push(` [${pattern}] ${subMatches.length} 个匹配`);
for (const m of subMatches.slice(0, 3)) {
lines.push(` ${m.file}:${m.line}`);
}
}
return lines.join('\n');
},
read_project_file(result) {
if (typeof result !== 'object' || result === null) {
return String(result).substring(0, 600);
}
const r = result;
if (Array.isArray(r.files)) {
const files = r.files;
const lines = [`读取 ${files.length} 个文件`];
for (const f of files.slice(0, 5)) {
const totalLines = (f.content || '').split('\n').length;
lines.push(` ${f.path} (${totalLines} 行)`);
}
return lines.join('\n');
}
const content = r.content || String(result);
const totalLines = content.split('\n').length;
return `文件 ${r.path || '?'} (${totalLines} 行)`;
},
get_class_info(result) {
if (typeof result !== 'object' || result === null) {
return String(result).substring(0, 600);
}
const r = result;
const lines = [`类 ${r.className || '?'}`];
if (r.superClass) {
lines.push(` 继承: ${r.superClass}`);
}
if (Array.isArray(r.protocols) && r.protocols.length) {
lines.push(` 协议: ${r.protocols.join(', ')}`);
}
if (Array.isArray(r.methods) && r.methods.length) {
lines.push(` 方法数: ${r.methods.length}`);
}
if (Array.isArray(r.properties) && r.properties.length) {
lines.push(` 属性数: ${r.properties.length}`);
}
return lines.join('\n');
},
get_class_hierarchy(result) {
if (typeof result !== 'object' || result === null) {
return String(result).substring(0, 600);
}
const r = result;
const classes = r.classes || r.hierarchy || [];
return `类层级: ${Array.isArray(classes) ? classes.length : 0} 个类`;
},
get_project_overview(result) {
if (typeof result !== 'object' || result === null) {
return String(result).substring(0, 800);
}
return JSON.stringify(result).substring(0, 800);
},
list_project_structure(result) {
if (typeof result !== 'object' || result === null) {
return String(result).substring(0, 600);
}
const r = result;
const entries = r.entries || r.children || [];
return `目录结构: ${Array.isArray(entries) ? entries.length : 0} 个条目`;
},
};
/** 默认压缩 — 截断到 maxChars */
function defaultCompress(result, maxChars = 600) {
const str = typeof result === 'string' ? result : JSON.stringify(result);
if (str.length <= maxChars) {
return str;
}
return `${str.substring(0, maxChars)}…(truncated)`;
}
export class ActiveContext {
// ── 子区 1: Scratchpad (从 WorkingMemory 继承, 不可压缩) ──
#scratchpad = [];
// ── 子区 2: ObservationLog (合并 RT.rounds + WM.observations) ──
#rounds = [];
#currentRound = null;
// ── WM 滑动窗口 (保留最近 N 轮原始结果,旧的压缩) ──
#recentObservations = [];
#compressedObservations = [];
// ── 子区 3: Plan (从 ReasoningTrace 继承) ──
#plan = null;
#planHistory = [];
/** 是否期待下一次响应包含计划 (由 ExplorationTracker 设置) */
#expectingPlan = false;
// ── 配置 ──
/** 保留最近 N 轮原始观察 */
#maxRecentRounds;
/** 轻量模式 (User Chat: 仅 RT 功能,禁用 WM 压缩/Scratchpad) */
#lightweight;
/** 总观察计数 */
#totalObservations = 0;
#logger;
/**
* @param [options.maxRecentRounds=3] 保留最近 N 轮原始结果 (WM 滑动窗口)
* @param [options.lightweight=false] 轻量模式: 跳过 WM 的压缩/Scratchpad 逻辑 (D5)
*/
constructor(options = {}) {
this.#maxRecentRounds = options.maxRecentRounds ?? 3;
this.#lightweight = options.lightweight ?? false;
this.#logger = Logger.getInstance();
}
// ═══════════════════════════════════════════════════════
// §2.1: 轮次管理 (合并 RT.startRound/endRound)
// ═══════════════════════════════════════════════════════
/**
* 开始新一轮推理
* @param iteration 轮次编号
*/
startRound(iteration) {
if (this.#currentRound) {
this.endRound(); // 安全关闭上一轮
}
this.#currentRound = {
iteration,
thought: null,
actions: [],
observations: [],
reflection: null,
roundSummary: null,
startTime: Date.now(),
endTime: null,
};
}
/** 结束当前轮次 */
endRound() {
if (this.#currentRound) {
this.#currentRound.endTime = Date.now();
this.#rounds.push(this.#currentRound);
this.#currentRound = null;
}
}
// ═══════════════════════════════════════════════════════
// §2.2: 记录 (合并 WM.observe + RT.addAction/addObservation)
// ═══════════════════════════════════════════════════════
/** 记录 AI 的推理文本(从 aiResult.text 提取) */
setThought(text) {
if (this.#currentRound && text) {
this.#currentRound.thought = text;
}
}
/**
* 统一记录一次工具调用 — 合并原 WM.observe() + RT.addAction() + RT.addObservation()
*
* @param toolName 工具名称
* @param args 工具参数
* @param result 工具返回的原始结果
* @param isNew 是否发现新信息 (由 ExplorationTracker.recordToolCall 提供)
*/
recordToolCall(toolName, args, result, isNew) {
const round = this.#currentRound?.iteration || 0;
// ── RT 部分: Action + Observation ──
this.#currentRound?.actions.push({ tool: toolName, params: args });
const observationMeta = ActiveContext.buildObservationMeta(toolName, args, result, isNew);
this.#currentRound?.observations.push({ tool: toolName, ...observationMeta });
// ── WM 部分: 滑动窗口压缩 (非轻量模式) ──
if (!this.#lightweight) {
this.#totalObservations++;
this.#recentObservations.push({
toolName,
result,
round,
timestamp: Date.now(),
});
while (this.#recentObservations.length > this.#maxRecentRounds) {
const oldest = this.#recentObservations.shift();
if (oldest) {
const summary = this.#compressObservation(oldest);
this.#compressedObservations.push(summary);
}
}
}
}
/** 兼容旧 RT API: 记录一次工具调用 (Action only) */
addAction(toolName, params) {
this.#currentRound?.actions.push({ tool: toolName, params });
}
/** 兼容旧 RT API: 记录一次工具结果的结构化观察 */
addObservation(toolName, meta) {
this.#currentRound?.observations.push({ tool: toolName, ...meta });
}
/** 兼容旧 WM API: 记录工具调用结果 (Observe, 仅 WM 滑动窗口) */
observe(toolName, result, round) {
if (this.#lightweight) {
return;
}
this.#totalObservations++;
this.#recentObservations.push({ toolName, result, round, timestamp: Date.now() });
while (this.#recentObservations.length > this.#maxRecentRounds) {
const oldest = this.#recentObservations.shift();
if (oldest) {
const summary = this.#compressObservation(oldest);
this.#compressedObservations.push(summary);
}
}
}
/** 记录反思内容 (ExplorationTracker 使用, L5 修复) */
setReflection(text) {
if (this.#currentRound && text) {
this.#currentRound.reflection = text;
}
}
/**
* 记录轮次摘要
* @param summary { newInfoCount, totalCalls, submits, cumulativeFiles, cumulativePatterns }
*/
setRoundSummary(summary) {
if (this.#currentRound) {
this.#currentRound.roundSummary = summary;
}
}
// ═══════════════════════════════════════════════════════
// §2.3: Scratchpad (从 WorkingMemory 继承)
// ═══════════════════════════════════════════════════════
/**
* Agent 主动记录关键发现 (note_finding 工具入口)
*
* @param finding 关键发现描述
* @param [evidence] 证据 (文件路径:行号)
* @param [importance=5] 重要性 1-10
* @param [round=0] 当前轮次
*/
noteKeyFinding(finding, evidence = '', importance = 5, round = 0) {
// P0 Fix: 防御性保证 evidence 是 string (AI 可能传入 array/object)
const safeEvidence = typeof evidence === 'string'
? evidence
: Array.isArray(evidence)
? evidence.join(', ')
: evidence
? String(evidence)
: '';
this.#scratchpad.push({
finding,
evidence: safeEvidence,
importance: Math.min(10, Math.max(1, importance)),
round,
});
this.#logger.debug(`[ActiveContext] 📌 noted finding (${importance}/10): ${finding.substring(0, 80)}`);
}
// ═══════════════════════════════════════════════════════
// §2.4: Plan (从 ReasoningTrace 继承)
// ═══════════════════════════════════════════════════════
/**
* 从 AI 响应文本中提取计划,自动调用 setPlan/updatePlan
*
* 防御措施: 已存在计划时,仅在 #expectingPlan 为 true 时才覆盖。
* 这防止 reflection 回复中的编号列表(非计划的回应文本)污染已有计划。
* ExplorationTracker 在发送 plan elicitation / replan 时调用 expectPlan() 授权更新。
*
* @param text AI 完整响应文本
* @param iteration 当前轮次
* @returns 是否成功提取到计划
*/
extractAndSetPlan(text, iteration) {
const planText = this.#extractPlanFromText(text);
if (!planText) {
return false;
}
// Guard: 已有计划时,仅在 expectPlan 授权下才覆盖
// 防止 reflection/convergence 回复中的编号列表被误捕获为 plan
if (this.#plan && !this.#expectingPlan) {
return false;
}
this.#expectingPlan = false;
if (this.#plan) {
this.#updatePlan(planText, iteration);
}
else {
this.#setPlan(planText, iteration);
}
return true;
}
/**
* 标记「下一次响应可能包含计划」— 授权 extractAndSetPlan 覆盖已有计划
* 由 ExplorationTracker 在发送 plan elicitation / replan nudge 时调用。
*/
expectPlan() {
this.#expectingPlan = true;
}
/** 直接设置计划 (公开接口,供 ExplorationTracker 和测试使用) */
setPlan(planText, iteration) {
this.#setPlan(planText, iteration);
}
/** 更新计划 (保留旧 plan 到 history) */
updatePlan(replanText, iteration) {
this.#updatePlan(replanText, iteration);
}
/** 获取当前计划 (只读副本) */
getPlan() {
if (!this.#plan) {
return null;
}
return {
...this.#plan,
steps: this.#plan.steps.map((s) => ({ ...s })),
};
}
/** 获取计划步骤的可变引用 (ExplorationTracker.updatePlanProgress 使用) */
getPlanStepsMutable() {
return this.#plan?.steps || [];
}
/** 获取计划历史 (F7) */
getPlanHistory() {
return this.#planHistory.map((p) => ({ ...p, steps: p.steps.map((s) => ({ ...s })) }));
}
/**
* 获取当前轮次的 actions (ExplorationTracker.updatePlanProgress 使用, L5 修复)
* @returns >}
*/
getCurrentRoundActions() {
return this.#currentRound?.actions || [];
}
/** 获取当前轮次的 iteration 编号 (F8) */
getCurrentIteration() {
return this.#currentRound?.iteration || null;
}
// ═══════════════════════════════════════════════════════
// §2.5: 上下文构建 (合并 WM.buildContext, 增加预算控制)
// ═══════════════════════════════════════════════════════
/**
* 构建当前工作记忆的上下文快照
* 用于注入到 system prompt 或 user nudge 中
*
* @param [tokenBudget=Infinity] token 预算 (新增: 预算控制)
* @returns Markdown 格式的上下文块,空字符串表示无内容
*/
buildContext(tokenBudget = Infinity) {
if (this.#lightweight) {
return '';
}
const parts = [];
let remaining = tokenBudget;
// §1: Scratchpad (最高优先级 — 不会被压缩)
if (this.#scratchpad.length > 0) {
const sorted = [...this.#scratchpad].sort((a, b) => b.importance - a.importance);
const scratchLines = ['## 📌 已确认的关键发现'];
for (const f of sorted) {
const badge = f.importance >= 8 ? '⚠️' : f.importance >= 5 ? '📋' : '💡';
let line = `- ${badge} [${f.importance}/10] ${f.finding}`;
if (f.evidence) {
line += ` (${f.evidence})`;
}
scratchLines.push(line);
}
const scratchSection = scratchLines.join('\n');
const scratchTokens = this.#estimateTokens(scratchSection);
if (scratchTokens <= remaining) {
parts.push(scratchSection);
remaining -= scratchTokens;
}
}
// §2: 压缩后的旧观察摘要 (中等优先级)
if (this.#compressedObservations.length > 0 && remaining > 100) {
const obsLines = ['## 📂 之前的探索摘要'];
const maxItems = Math.min(15, this.#compressedObservations.length);
const recent = this.#compressedObservations.slice(-maxItems);
for (const s of recent) {
const line = `- [R${s.round}|${s.toolName}] ${s.summary.substring(0, 200)}`;
const lineTokens = this.#estimateTokens(line);
if (lineTokens > remaining) {
break;
}
obsLines.push(line);
remaining -= lineTokens;
}
if (this.#compressedObservations.length > maxItems) {
obsLines.push(` …(还有 ${this.#compressedObservations.length - maxItems} 条更早的观察)`);
}
if (obsLines.length > 1) {
parts.push(obsLines.join('\n'));
}
}
return parts.join('\n');
}
// ═══════════════════════════════════════════════════════
// §2.6: 蒸馏 (合并 WM.distill, 增强版 — 含 plan + stats)
// ═══════════════════════════════════════════════════════
/**
* 蒸馏 ActiveContext 为结构化报告
* 在 Agent execute 结束时调用,结果写入 SessionStore
*/
distill() {
return {
keyFindings: this.#scratchpad.map((f) => ({
finding: f.finding,
evidence: f.evidence,
importance: f.importance,
})),
toolCallSummary: this.#compressedObservations.map((s) => `[${s.toolName}] ${s.summary.substring(0, 150)}`),
stats: this.getStats(),
plan: this.getPlan(),
totalObservations: this.#totalObservations,
compressedCount: this.#compressedObservations.length,
};
}
// ═══════════════════════════════════════════════════════
// §2.7: 分析方法 (从 ReasoningTrace 继承)
// ═══════════════════════════════════════════════════════
/**
* 获取所有有 Thought 的轮次
* @returns >}
*/
getThoughts() {
return this.#rounds
.filter((r) => r.thought)
.map((r) => ({ iteration: r.iteration, thought: r.thought }));
}
/**
* 获取最近 N 轮的紧凑摘要 (ExplorationTracker.#checkReflection 使用)
* @param [n=3] 回看轮数
*/
getRecentSummary(n = 3) {
const recent = this.#rounds.slice(-n);
if (recent.length === 0) {
return null;
}
const thoughts = recent
.filter((r) => r.thought !== null)
.map((r) => (r.thought.length > 100 ? `${r.thought.substring(0, 100)}…` : r.thought));
const tools = recent.flatMap((r) => r.actions.map((a) => a.tool));
const newInfoCount = recent.reduce((c, r) => c + r.observations.filter((o) => o.gotNewInfo).length, 0);
const totalObs = recent.reduce((c, r) => c + r.observations.length, 0);
return {
roundCount: recent.length,
thoughts,
toolCalls: tools,
newInfoRatio: totalObs > 0 ? newInfoCount / totalObs : 0,
lastIteration: recent[recent.length - 1].iteration,
};
}
/** 统计指标 (ExplorationTracker.getQualityMetrics 使用) */
getStats() {
return {
totalRounds: this.#rounds.length,
thoughtCount: this.#rounds.filter((r) => r.thought).length,
totalActions: this.#rounds.reduce((c, r) => c + r.actions.length, 0),
totalObservations: this.#rounds.reduce((c, r) => c + r.observations.length, 0),
reflectionCount: this.#rounds.filter((r) => r.reflection).length,
totalDurationMs: this.#rounds.reduce((d, r) => d + ((r.endTime || Date.now()) - r.startTime), 0),
};
}
// ═══════════════════════════════════════════════════════
// §2.8: Scratchpad 查询 (从 WorkingMemory 继承)
// ═══════════════════════════════════════════════════════
/** 获取 scratchpad 中的关键发现数量 */
get scratchpadSize() {
return this.#scratchpad.length;
}
/** 获取总观察数 */
get totalObservations() {
return this.#totalObservations;
}
/**
* 获取 scratchpad 中的高重要性发现
* @returns >}
*/
getHighPriorityFindings(minImportance = 7) {
return this.#scratchpad
.filter((f) => f.importance >= minImportance)
.sort((a, b) => b.importance - a.importance);
}
// ═══════════════════════════════════════════════════════
// §2.9: 序列化 (从 ReasoningTrace 继承)
// ═══════════════════════════════════════════════════════
/** 可序列化输出 */
toJSON() {
return {
rounds: this.#rounds.map((r) => ({ ...r })),
stats: this.getStats(),
scratchpad: this.#scratchpad.map((f) => ({ ...f })),
compressedObservations: this.#compressedObservations.length,
totalObservations: this.#totalObservations,
...(this.#plan
? {
plan: {
text: this.#plan.text,
steps: this.#plan.steps.map((s) => ({ ...s })),
createdAtIteration: this.#plan.createdAtIteration,
lastUpdatedAtIteration: this.#plan.lastUpdatedAtIteration,
},
planHistory: this.#planHistory.length,
}
: {}),
};
}
/**
* 从 JSON 恢复 ActiveContext (断点续传)
* @param json toJSON() 的输出
*/
static fromJSON(json) {
const ctx = new ActiveContext();
if (json.rounds) {
ctx.#rounds = json.rounds.map((r) => ({ ...r }));
}
if (json.scratchpad) {
ctx.#scratchpad = json.scratchpad.map((f) => ({ ...f }));
}
if (json.totalObservations) {
ctx.#totalObservations = json.totalObservations;
}
if (json.plan) {
ctx.#plan = {
text: json.plan.text,
steps: json.plan.steps.map((s) => ({ ...s })),
createdAtIteration: json.plan.createdAtIteration,
lastUpdatedAtIteration: json.plan.lastUpdatedAtIteration,
};
}
return ctx;
}
/** 清空 ActiveContext — 释放内存 */
clear() {
this.#scratchpad.length = 0;
this.#rounds.length = 0;
this.#currentRound = null;
this.#recentObservations.length = 0;
this.#compressedObservations.length = 0;
this.#plan = null;
this.#planHistory.length = 0;
this.#totalObservations = 0;
}
// ═══════════════════════════════════════════════════════
// §2.10: 静态工具 (从 ReasoningTrace 迁入)
// ═══════════════════════════════════════════════════════
/**
* 从工具执行结果构建结构化观察元数据
* 不改变工具结果传给 AI 的内容,只影响推理链记录
*
* @param isNew 由 ExplorationTracker.recordToolCall 提供
* @returns }
*/
static buildObservationMeta(toolName, args, result, isNew) {
const meta = {
gotNewInfo: isNew,
resultType: 'unknown',
keyFacts: [],
resultSize: 0,
};
const resultStr = typeof result === 'string' ? result : JSON.stringify(result || '');
meta.resultSize = resultStr.length;
const resultObj = typeof result === 'object' && result !== null ? result : null;
switch (toolName) {
case 'search_project_code': {
meta.resultType = 'search';
const matches = Array.isArray(resultObj?.matches) ? resultObj.matches : [];
const batchResults = resultObj?.batchResults;
const totalMatches = batchResults
? Object.values(batchResults).reduce((s, br) => s + (Array.isArray(br.matches) ? br.matches.length : 0), 0)
: matches.length;
meta.keyFacts.push(`${totalMatches} matches found`);
if (isNew) {
meta.keyFacts.push('new files discovered');
}
break;
}
case 'read_project_file': {
meta.resultType = 'file_content';
const fp = args?.filePath || '';
const fps = args?.filePaths || [];
const allPaths = fp ? [fp, ...fps] : fps;
meta.keyFacts.push(`read ${allPaths.length} file(s)`);
break;
}
case 'submit_knowledge':
case 'submit_with_check': {
meta.resultType = 'submit';
meta.gotNewInfo = true;
const status = resultObj ? resultObj.status || 'ok' : 'ok';
const title = args?.title || '(untitled)';
meta.keyFacts.push(`submit "${title}": ${status}`);
break;
}
case 'list_project_structure': {
meta.resultType = 'structure';
meta.keyFacts.push(`list ${args?.directory || '/'}`);
break;
}
case 'get_class_info':
case 'get_class_hierarchy':
case 'get_protocol_info':
case 'get_method_overrides':
case 'get_category_map': {
meta.resultType = 'ast_query';
const target = args?.className ||
args?.protocolName ||
args?.name ||
'';
meta.keyFacts.push(`${toolName}(${target})`);
break;
}
case 'get_project_overview': {
meta.resultType = 'overview';
meta.keyFacts.push('project overview');
break;
}
case 'semantic_search_code':
case 'get_file_summary':
case 'get_previous_analysis': {
meta.resultType = 'query';
meta.keyFacts.push(toolName);
break;
}
default: {
meta.resultType = 'other';
meta.keyFacts.push(toolName);
}
}
return meta;
}
// ═══════════════════════════════════════════════════════
// §3: 私有方法
// ═══════════════════════════════════════════════════════
/**
* 工具结果压缩 — 使用特化策略 (从 WorkingMemory 迁入)
* @param observation
* @returns }
*/
#compressObservation(observation) {
const strategy = TOOL_COMPRESS_STRATEGIES[observation.toolName];
let summary;
try {
summary = strategy ? strategy(observation.result) : defaultCompress(observation.result);
}
catch {
summary = defaultCompress(observation.result);
}
return {
toolName: observation.toolName,
round: observation.round,
summary,
};
}
/** 粗糙 token 估算 (1 token ≈ 4 chars) */
#estimateTokens(text) {
return Math.ceil((text || '').length / 4);
}
// ── Plan 内部方法 (从 ReasoningTrace 迁入) ──
#setPlan(planText, iteration) {
this.#plan = {
text: planText,
steps: this.#parsePlanSteps(planText),
createdAtIteration: iteration,
lastUpdatedAtIteration: iteration,
};
}
#updatePlan(replanText, iteration) {
if (!this.#plan) {
this.#setPlan(replanText, iteration);
return;
}
this.#planHistory.push({ ...this.#plan, steps: this.#plan.steps.map((s) => ({ ...s })) });
this.#plan.text = replanText;
this.#plan.steps = this.#parsePlanSteps(replanText);
this.#plan.lastUpdatedAtIteration = iteration;
}
/** 从 AI 文本中解析计划步骤 */
#parsePlanSteps(text) {
if (!text) {
return [];
}
const lines = text.split('\n');
const steps = [];
for (const line of lines) {
const m = line.match(/^\s*(?:\d+[.)]\s*|[-*]\s+)(.+)/);
if (m && m[1].trim().length > 5) {
steps.push({
description: m[1].trim(),
status: 'pending',
keywords: this.#extractKeywords(m[1]),
});
}
}
return steps;
}
/** 从步骤描述中提取关键词 */
#extractKeywords(text) {
const quoted = [...text.matchAll(/[`"']([A-Za-z_]\w{2,})[`"']/g)].map((m) => m[1]);
const camelCase = [...text.matchAll(/\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g)].map((m) => m[0]);
const acronyms = [...text.matchAll(/\b([A-Z]{2,}[a-z]\w+)\b/g)].map((m) => m[0]);
return [...new Set([...quoted, ...camelCase, ...acronyms])];
}
/** 从 AI 响应文本中提取"计划"部分 */
#extractPlanFromText(text) {
if (!text || text.length < 30) {
return null;
}
const searchArea = text.substring(0, 2000);
const planMarkers = [
/(?:探索|分析)?计划[::\s]/i,
/(?:my\s+)?plan[::\s]/i,
/步骤[::\s]/i,
/以下是.*(?:计划|步骤)/i,
];
let planStart = -1;
for (const marker of planMarkers) {
const match = searchArea.match(marker);
if (match && match.index !== undefined) {
planStart = match.index + match[0].length;
break;
}
}
if (planStart === -1) {
const listMatch = searchArea.match(/\n\s*1[.)]\s+/);
if (listMatch && listMatch.index !== undefined) {
planStart = listMatch.index;
}
}
if (planStart === -1) {
return null;
}
const remaining = searchArea.substring(planStart);
const lines = remaining.split('\n');
const planLines = [];
let inList = false;
for (const line of lines) {
if (/^\s*(?:\d+[.)]\s+|[-*]\s+)/.test(line)) {
inList = true;
planLines.push(line);
}
else if (inList && line.trim() === '') {
break;
}
else if (inList) {
break;
}
}
if (planLines.length < 2) {
return null;
}
// 防御: 拒绝 "大部分是疑问句" 的编号列表
// reflection nudge 的 "请评估: 1. ...是什么? 2. ...?" 会被 LLM 回显,
// 不是真正的探索计划,不能捕获为 plan steps
const questionCount = planLines.filter((l) => /[??]\s*$/.test(l.trim())).length;
if (questionCount > planLines.length * 0.5) {
return null;
}
return planLines.join('\n').trim();
}
}
export default ActiveContext;