autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
213 lines (212 loc) • 8.54 kB
JavaScript
/**
* BootstrapSession — 外部 Agent 驱动的 Bootstrap 会话状态管理
*
* 跨多次 MCP 调用保持状态(进程生命周期内有效)。
* 通过 ServiceContainer 单例注册,每个项目同时只有一个 active session。
*
* 职责:
* - 维度完成状态跟踪
* - Phase 缓存(供 wiki_plan 复用)
* - EpisodicMemory 管理
* - Cross-dimension hints 收集与分发
* - 进度查询
* - Session 过期与恢复
*
* @module bootstrap/BootstrapSession
*/
import crypto from 'node:crypto';
import { SessionStore } from '#agent/memory/SessionStore.js';
import { ExternalSubmissionTracker } from './ExternalSubmissionTracker.js';
// ── 常量 ────────────────────────────────────────────────────
const SESSION_TTL_MS = 2 * 60 * 60 * 1000; // 2 小时
// ── BootstrapSession ────────────────────────────────────────
export class BootstrapSession {
expiresAt;
id;
projectRoot;
startedAt;
_activeSession;
completedDimensions;
crossDimensionHints;
dimensions;
snapshotCache;
sessionStore;
submissionTracker;
/**
* @param opts.projectRoot 项目根目录
* @param opts.dimensions 激活的维度定义列表
* @param [opts.projectContext] 传给 EpisodicMemory 的项目元数据
*/
constructor({ projectRoot, dimensions, projectContext = {} }) {
this.id = `bs-${crypto.randomUUID()}`;
this.projectRoot = projectRoot;
this.dimensions = dimensions;
this.completedDimensions = new Map(); // dimId → { report, completedAt, recipeIds }
this.sessionStore = new SessionStore(projectContext);
/** 外部 Agent 提交追踪 (v2: 对标内部 Agent 的 EvidenceCollector) */
this.submissionTracker = new ExternalSubmissionTracker();
/** Phase 1-4 分析结果缓存,供 wiki_plan 复用 */
this.snapshotCache = null;
/** 跨维度 hints 收集 */
this.crossDimensionHints = {}; // targetDimId → [{ fromDim, hint }]
this._activeSession = null;
this.startedAt = Date.now();
this.expiresAt = Date.now() + SESSION_TTL_MS;
}
// ── 状态查询 ──────────────────────────────────────────────
get isExpired() {
return Date.now() > this.expiresAt;
}
get isComplete() {
return this.completedDimensions.size >= this.dimensions.length;
}
getProgress() {
return {
completed: this.completedDimensions.size,
total: this.dimensions.length,
completedDimIds: [...this.completedDimensions.keys()],
remainingDimIds: this.dimensions
.map((d) => d.id)
.filter((id) => !this.completedDimensions.has(id)),
};
}
/** 检查某个维度是否已完成 */
isDimensionComplete(dimId) {
return this.completedDimensions.has(dimId);
}
// ── 维度完成 ──────────────────────────────────────────────
/**
* 标记维度完成
* @param report { analysisText, findings, referencedFiles, recipeIds, candidateCount }
* @returns } - updated=true 表示覆盖了已有记录
*/
markDimensionComplete(dimId, report) {
const updated = this.completedDimensions.has(dimId);
this.completedDimensions.set(dimId, {
...report,
completedAt: Date.now(),
});
// 写入 SessionStore
// keyFindings 是字符串数组,需转换为 SessionStore 期望的 { finding, importance } 格式
this.sessionStore.storeDimensionReport(dimId, {
analysisText: report.analysisText,
findings: (report.keyFindings || []).map((f) => ({ finding: f, importance: 7 })),
referencedFiles: report.referencedFiles || [],
candidatesSummary: [],
});
// v2: 从 analysisText 提取负空间信号并计算质量报告
this.submissionTracker.extractNegativeSignals(report.analysisText || '', dimId);
const qualityReport = this.submissionTracker.buildQualityReport(dimId, report.analysisText, report.referencedFiles || []);
return { updated, qualityReport };
}
// ── Cross-Dimension Hints ─────────────────────────────────
/**
* 存储跨维度 hints
* @param fromDimId 来源维度
* @param hints { targetDimId: hintText }
*/
storeHints(fromDimId, hints) {
if (!hints || typeof hints !== 'object') {
return;
}
for (const [targetDim, hintText] of Object.entries(hints)) {
if (!this.crossDimensionHints[targetDim]) {
this.crossDimensionHints[targetDim] = [];
}
// 去重:同源维度只保留最新 hint
this.crossDimensionHints[targetDim] = this.crossDimensionHints[targetDim].filter((h) => h.fromDim !== fromDimId);
this.crossDimensionHints[targetDim].push({
fromDim: fromDimId,
hint: String(hintText),
});
}
}
/**
* 收集与剩余维度相关的 accumulated hints
* @returns >>}
*/
getAccumulatedHints() {
const progress = this.getProgress();
const accumulated = {};
for (const remainingDim of progress.remainingDimIds) {
const hints = this.crossDimensionHints[remainingDim];
if (hints?.length > 0) {
accumulated[remainingDim] = hints;
}
}
return accumulated;
}
// ── Snapshot 缓存 ──────────────────────────────────────────
/**
* 缓存 Phase 1-4 分析结果(ProjectSnapshot 的 session cache 形式)
* @param cache toSessionCache(snapshot) 的返回值
*/
setSnapshotCache(cache) {
this.snapshotCache = cache;
}
/** 获取 Snapshot 缓存(wiki_plan / dimension-complete 复用) */
getSnapshotCache() {
return this.snapshotCache;
}
// ── 序列化 ────────────────────────────────────────────────
toJSON() {
return {
id: this.id,
projectRoot: this.projectRoot,
startedAt: this.startedAt,
expiresAt: this.expiresAt,
progress: this.getProgress(),
dimensionCount: this.dimensions.length,
};
}
}
// ── Session 管理器(进程级单例)──────────────────────────────
/**
* BootstrapSessionManager — 管理 active session
*
* 设计为进程级单例,通过 ServiceContainer 注册。
* 同时只有一个 active session(单项目场景)。
*/
export class BootstrapSessionManager {
_activeSession;
constructor() {
this._activeSession = null;
}
/**
* 创建新的 bootstrap session
* @param opts 传给 BootstrapSession 构造函数的参数
*/
createSession(opts) {
// 如果有旧的未过期 session,先标记过期
if (this._activeSession && !this._activeSession.isExpired) {
this._activeSession.expiresAt = Date.now(); // 强制过期
}
this._activeSession = new BootstrapSession(opts);
return this._activeSession;
}
/**
* 获取 active session
* @param [sessionId] 可选,用于验证 session ID
*/
getSession(sessionId) {
if (!this._activeSession) {
return null;
}
if (this._activeSession.isExpired) {
return null;
}
if (sessionId && this._activeSession.id !== sessionId) {
return null;
}
return this._activeSession;
}
/** 获取 active session,无论是否过期(用于恢复场景) */
getAnySession() {
return this._activeSession;
}
/** 清除 active session */
clearSession() {
this._activeSession = null;
}
}
export default BootstrapSession;