feat(engine): complete stability overhaul - added state resume, deduplication, and P-Reinforce formatting
This commit is contained in:
+130
-24
@@ -5,6 +5,7 @@ import { lockManager } from '../core/lock';
|
||||
import { actionQueue } from '../core/queue';
|
||||
import { logInfo, logError } from '../utils';
|
||||
import { AgentDataValidator, PerformanceProfiler } from './diagnostics';
|
||||
import { WikiFormatter } from './formatter';
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 1. 에이전트 인터페이스 확장 (Interface Extensibility)
|
||||
@@ -63,6 +64,7 @@ export class MissionState {
|
||||
private _stage: PipelineStage = 'idle';
|
||||
private _auditTrail: AuditEntry[] = [];
|
||||
private _lastTransitionTime: number = Date.now();
|
||||
private _results: Record<string, string> = {};
|
||||
private _failureReason?: string;
|
||||
|
||||
public readonly missionId: string;
|
||||
@@ -127,6 +129,39 @@ export class MissionState {
|
||||
this.saveToDisk();
|
||||
}
|
||||
|
||||
/**
|
||||
* 중간 결과물을 저장합니다 (Resumption용).
|
||||
*/
|
||||
public setResult(key: string, value: string): void {
|
||||
this._results[key] = value;
|
||||
this.saveToDisk();
|
||||
}
|
||||
|
||||
public getResult(key: string): string | undefined {
|
||||
return this._results[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장된 미션 상태를 불러옵니다 (Resumption용).
|
||||
*/
|
||||
public static loadFromDisk(missionId: string): MissionState | null {
|
||||
try {
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
|
||||
if (!workspacePath) return null;
|
||||
const filePath = path.join(workspacePath, '.astra', 'missions', `${missionId}.json`);
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
const state = new MissionState(missionId);
|
||||
state._stage = data.status;
|
||||
state._results = data.results || {};
|
||||
return state;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 전체 미션의 경과 시간을 반환합니다.
|
||||
@@ -168,6 +203,7 @@ export class MissionState {
|
||||
startTime: new Date(this.startTime).toISOString(),
|
||||
totalElapsedMs: this.getElapsedMs(),
|
||||
failureReason: this._failureReason,
|
||||
results: this._results,
|
||||
transitionCount: this._auditTrail.length,
|
||||
transitions: this._auditTrail.map(e => ({
|
||||
from: e.from,
|
||||
@@ -322,6 +358,47 @@ export class ErrorClassifier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CacheManager: 중복 수집 방지 (Deduplication)를 위한 캐시 레이어.
|
||||
* 동일한 프롬프트와 컨텍스트에 대해 중복된 LLM 호출을 방지합니다.
|
||||
*/
|
||||
export class CacheManager {
|
||||
private static getCacheDir(): string {
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
|
||||
const cacheDir = path.join(workspacePath, '.astra', 'cache');
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
private static getHash(key: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < key.length; i++) {
|
||||
const char = key.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash |= 0;
|
||||
}
|
||||
return hash.toString(36);
|
||||
}
|
||||
|
||||
public static get(prompt: string, context: string): string | null {
|
||||
const key = this.getHash(prompt + context);
|
||||
const filePath = path.join(this.getCacheDir(), `${key}.cache`);
|
||||
if (fs.existsSync(filePath)) {
|
||||
return fs.readFileSync(filePath, 'utf-8');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static set(prompt: string, context: string, result: string): void {
|
||||
const key = this.getHash(prompt + context);
|
||||
const filePath = path.join(this.getCacheDir(), `${key}.cache`);
|
||||
fs.writeFileSync(filePath, result, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 4. AgentEngine 본체
|
||||
// ─────────────────────────────────────────────
|
||||
@@ -359,8 +436,14 @@ export class AgentEngine {
|
||||
onProgress: (stage: PipelineStage, message: string) => void
|
||||
): Promise<string> {
|
||||
|
||||
// 상태 객체 초기화
|
||||
this.state = new MissionState(missionId);
|
||||
// 0. 상태 복원 시도 (Resumption)
|
||||
const existingState = MissionState.loadFromDisk(missionId);
|
||||
if (existingState && existingState.stage !== 'completed') {
|
||||
logInfo(`[AgentEngine] 기존 미션 발견. '${existingState.stage}' 단계부터 재개합니다.`);
|
||||
this.state = existingState;
|
||||
} else {
|
||||
this.state = new MissionState(missionId);
|
||||
}
|
||||
|
||||
// 1. 명시적 락 획득 (Mutex) - 동일 미션의 중복 실행 방지
|
||||
const release = await lockManager.acquire(`mission_${missionId}`);
|
||||
@@ -371,30 +454,49 @@ export class AgentEngine {
|
||||
logInfo(`[AgentEngine] 미션 시작: ${missionId}`);
|
||||
|
||||
// --- Phase 1: Planner ---
|
||||
this.transition('planner', '전략 수립 중...', onProgress);
|
||||
this.checkAbort(signal);
|
||||
logInfo(`[AgentEngine] [Planner] Input Prompt: ${this.summarizeLog(prompt, 50)}`);
|
||||
const plan = await this.resilientExecute(
|
||||
this.planner, 'Planner', prompt, brainContext, signal, onProgress,
|
||||
{ context: brainContext, signal, config: { role: 'planner' } }
|
||||
);
|
||||
let plan = this.state!.getResult('plan');
|
||||
if (!plan) {
|
||||
this.transition('planner', '전략 수립 중...', onProgress);
|
||||
this.checkAbort(signal);
|
||||
|
||||
// Deduplication: 동일 프롬프트 캐시 확인
|
||||
const cachedPlan = CacheManager.get(prompt, brainContext);
|
||||
if (cachedPlan) {
|
||||
logInfo(`[AgentEngine] [Deduplication] 기존 Planner 캐시를 사용합니다.`);
|
||||
plan = cachedPlan;
|
||||
} else {
|
||||
plan = await this.resilientExecute(
|
||||
this.planner, 'Planner', prompt, brainContext, signal, onProgress,
|
||||
{ context: brainContext, signal, config: { role: 'planner' } }
|
||||
);
|
||||
CacheManager.set(prompt, brainContext, plan);
|
||||
}
|
||||
this.state!.setResult('plan', plan);
|
||||
}
|
||||
this.validateResult(plan, 'Planner');
|
||||
logInfo(`[AgentEngine] [Planner] Output: ${this.summarizeLog(plan, 100)}`);
|
||||
|
||||
// --- Phase 2 & 3: Parallel Prep + Sequential Execution ---
|
||||
this.transition('researcher', '핵심 정보 수집 및 분석 중...', onProgress);
|
||||
this.checkAbort(signal);
|
||||
logInfo(`[AgentEngine] [Researcher] BrainContext Size: ${brainContext?.length || 0} chars`);
|
||||
// --- Phase 2 & 3: Researcher ---
|
||||
let research = this.state!.getResult('research');
|
||||
let writerPrep = this.state!.getResult('writerPrep');
|
||||
|
||||
const [research, writerPrep] = await Promise.all([
|
||||
this.resilientExecute(
|
||||
this.researcher, 'Researcher', plan, brainContext, signal, onProgress,
|
||||
{ context: brainContext, signal, config: { role: 'researcher' } }
|
||||
),
|
||||
this.prepareWriterContext(prompt, plan, brainContext)
|
||||
]);
|
||||
if (!research || !writerPrep) {
|
||||
this.transition('researcher', '핵심 정보 수집 및 분석 중...', onProgress);
|
||||
this.checkAbort(signal);
|
||||
|
||||
const [res, prep] = await Promise.all([
|
||||
research || this.resilientExecute(
|
||||
this.researcher, 'Researcher', plan, brainContext, signal, onProgress,
|
||||
{ context: brainContext, signal, config: { role: 'researcher' } }
|
||||
),
|
||||
writerPrep || this.prepareWriterContext(prompt, plan, brainContext)
|
||||
]);
|
||||
|
||||
research = res;
|
||||
writerPrep = prep;
|
||||
this.state!.setResult('research', research);
|
||||
this.state!.setResult('writerPrep', writerPrep);
|
||||
}
|
||||
this.validateResult(research, 'Researcher');
|
||||
logInfo(`[AgentEngine] [Researcher] Output: ${this.summarizeLog(research, 100)}`);
|
||||
|
||||
// --- Phase 3: Writer ---
|
||||
this.transition('writer', '최종 리포트 작성 및 편집 중...', onProgress);
|
||||
@@ -404,12 +506,16 @@ export class AgentEngine {
|
||||
{ context: brainContext, signal, config: { role: 'writer' }, priorResults: { plan, writerPrep } }
|
||||
);
|
||||
this.validateResult(finalReport, 'Writer');
|
||||
logInfo(`[AgentEngine] [Writer] Output: ${this.summarizeLog(finalReport, 100)}`);
|
||||
|
||||
// 3. 지식 저장 포맷 표준화 (Standardization: Astra 피드백)
|
||||
const standardizedReport = WikiFormatter.format(finalReport, missionId);
|
||||
|
||||
this.transition('completed', '미션 완료', onProgress);
|
||||
logInfo(`[AgentEngine] 미션 완료: ${missionId} (총 ${this.state!.getElapsedMs()}ms)`);
|
||||
return finalReport;
|
||||
return standardizedReport;
|
||||
});
|
||||
|
||||
|
||||
} catch (error: any) {
|
||||
const { type, rule } = ErrorClassifier.classify(error);
|
||||
const stageName = (this.state?.stage || 'unknown').toUpperCase();
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class WikiFormatter {
|
||||
/**
|
||||
* 최종 에이전트 출력물을 P-Reinforce v3.0 표준 포맷으로 변환합니다.
|
||||
*/
|
||||
public static format(content: string, missionId: string): string {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// 1. Frontmatter가 없는 경우 주입
|
||||
let formatted = content;
|
||||
if (!content.trim().startsWith('---')) {
|
||||
const frontmatter = [
|
||||
'---',
|
||||
`id: ${missionId}`,
|
||||
`date: ${now}`,
|
||||
'type: knowledge_artifact',
|
||||
'standard: P-Reinforce v3.0',
|
||||
'tags: [automated, connect_ai, brain_sync]',
|
||||
'---',
|
||||
'',
|
||||
''
|
||||
].join('\n');
|
||||
formatted = frontmatter + content;
|
||||
}
|
||||
|
||||
// 2. 필수 헤더 보정 (예: Brief Summary가 없는 경우 상단에 자동 생성 시도)
|
||||
if (!formatted.includes('## 📌 Brief Summary')) {
|
||||
// 간단한 요약 추출 시도 (첫 문장 등)
|
||||
const summary = content.split('\n').find(l => l.trim().length > 10) || '요약이 생성되지 않았습니다.';
|
||||
const summarySection = `## 📌 Brief Summary\n${summary.substring(0, 200)}...\n\n`;
|
||||
formatted = formatted.replace(/---\n\n/, `---\n\n${summarySection}`);
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user