feat: Intelligent Resilience & Trust Reporting (v2.77.2)

This commit is contained in:
g1nation
2026-05-05 17:04:27 +09:00
parent 037eafa02b
commit cf10d14148
29 changed files with 490 additions and 166 deletions
+153 -97
View File
@@ -11,6 +11,8 @@ import { WikiFormatter } from './formatter';
// 1. 에이전트 인터페이스 확장 (Interface Extensibility)
// ─────────────────────────────────────────────
export type AbstractionLevel = 'laser-focused' | 'balanced' | 'comprehensive';
/**
* 에이전트 실행 시 전달되는 확장 옵션 객체.
* 향후 에이전트별로 고유한 설정(temperature, maxTokens 등)을
@@ -24,7 +26,9 @@ export interface AgentExecuteOptions {
/** 에이전트별 커스텀 설정 (temperature, maxTokens 등) */
config?: Record<string, unknown>;
/** 이전 단계의 중간 결과물 (병렬 파이프라인용) */
priorResults?: Record<string, string>;
priorResults?: Record<string, string | undefined>;
/** 에이전트 간 정보 전달 시의 추상화/필터링 수준 */
abstractionLevel?: AbstractionLevel;
}
/**
@@ -69,6 +73,12 @@ export class MissionState {
public readonly missionId: string;
public readonly startTime: number;
public resilienceMetrics = {
fallbacks: 0,
retries: 0,
maxConflictScore: 0,
deduplications: 0
};
constructor(missionId: string) {
this.missionId = missionId;
@@ -229,6 +239,8 @@ export enum ErrorType {
TRANSIENT = 'TRANSIENT',
/** 프롬프트 구조 문제, 모델 명백한 실패 등 수동 개입이 필요한 오류 */
PERMANENT = 'PERMANENT',
/** 인증 실패 (API Key 만료 등) */
AUTH_FAILURE = 'AUTH_FAILURE',
/** 사용자가 의도적으로 작업을 취소한 경우 */
ABORT = 'ABORT'
}
@@ -242,8 +254,10 @@ export interface RecoveryRule {
description: string;
maxRetries: number;
backoffBaseMs: number;
action: 'retry' | 'abort' | 'fail_with_message';
action: 'retry' | 'abort' | 'fail_with_message' | 'fallback';
userMessage: string;
/** 'fallback' 액션 시 실행할 대체 로직 (예: 캐시 반환) */
fallbackDescription?: string;
}
/**
@@ -423,8 +437,7 @@ export class AgentEngine {
) {}
/**
* 멀티 에이전트 워크플로우 실행
* @param missionId 작업을 식별하기 위한 고유 ID (Mutex 락에 사용)
* 멀티 에이전트 워크플로우 실행 (Refactored: Atomic State Machine)
*/
public async runMission(
missionId: string,
@@ -437,124 +450,135 @@ export class AgentEngine {
// 0. 상태 복원 시도 (Resumption)
const existingState = MissionState.loadFromDisk(missionId);
if (existingState && existingState.stage !== 'completed') {
logInfo(`[AgentEngine] 기존 미션 발견. '${existingState.stage}' 단계부터 재개합니다.`);
state = existingState;
} else {
state = new MissionState(missionId);
}
state = (existingState && existingState.stage !== 'completed') ? existingState : new MissionState(missionId);
// 1. 명시적 락 획득 (Mutex) - 동일 미션의 중복 실행 방지
// 1. 명시적 락 획득 (Mutex)
const release = await lockManager.acquire(`mission_${missionId}`);
try {
// 2. 작업을 비동기 큐에 등록 (Producer-Consumer)
return await actionQueue.enqueue(async () => {
logInfo(`[AgentEngine] 미션 시작: ${missionId}`);
// --- Phase 1: Planner ---
let plan = state.getResult('plan');
if (!plan) {
this.transition(state, '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(
state, this.planner, 'Planner', prompt, brainContext, signal, onProgress,
{ context: brainContext, signal, config: { role: 'planner' } }
);
CacheManager.set(prompt, brainContext, plan);
}
state.setResult('plan', plan);
// [Blind Spot Check] 전역 중복 제거 (Global Deduplication)
const globalCache = CacheManager.get(prompt, 'global_final_report');
if (globalCache) {
logInfo(`[AgentEngine] [Deduplication] 최종 리포트 캐시를 발견했습니다. 실행을 스킵합니다.`);
this.transition(state, 'completed', '캐시된 결과 반환', onProgress);
return globalCache;
}
this.validateResult(plan, 'Planner');
// --- Phase 2 & 3: Researcher ---
let research = state.getResult('research');
// --- Phase 1: Planner ---
const plan = await this.executeStep(
state, 'planner', '전략 수립 중...',
() => this.resilientExecute(state, this.planner, 'Planner', prompt, brainContext, signal, onProgress, { context: brainContext, signal, config: { role: 'planner' } }),
prompt, brainContext, signal, onProgress
);
const plannerScore = this.validateResult(plan, 'Planner');
const researcherLevel: AbstractionLevel = plannerScore < 70 ? 'laser-focused' : 'balanced';
// --- Phase 2: Researcher ---
const research = await this.executeStep(
state, 'researcher', '핵심 정보 수집 및 분석 중...',
() => this.resilientExecute(state, this.researcher, 'Researcher', plan, brainContext, signal, onProgress, { context: brainContext, signal, config: { role: 'researcher' }, abstractionLevel: researcherLevel }),
plan, brainContext, signal, onProgress
);
// --- Phase 3: Context Preparation (Side Effect of Phase 2) ---
let writerPrep = state.getResult('writerPrep');
if (!research || !writerPrep) {
this.transition(state, 'researcher', '핵심 정보 수집 및 분석 중...', onProgress);
this.checkAbort(signal);
const [res, prep] = await Promise.all([
research || this.resilientExecute(
state, this.researcher, 'Researcher', plan, brainContext, signal, onProgress,
{ context: brainContext, signal, config: { role: 'researcher' } }
),
writerPrep || this.prepareWriterContext(prompt, plan, brainContext)
]);
research = res;
writerPrep = prep;
state.setResult('research', research);
if (!writerPrep) {
writerPrep = await this.prepareWriterContext(prompt, plan, brainContext);
state.setResult('writerPrep', writerPrep);
}
this.validateResult(research, 'Researcher');
// --- Phase 3: Writer ---
this.transition(state, 'writer', '최종 리포트 작성 및 편집 중...', onProgress);
this.checkAbort(signal);
const finalReport = await this.resilientExecute(
state, this.writer, 'Writer', research, prompt, signal, onProgress,
{ context: brainContext, signal, config: { role: 'writer' }, priorResults: { plan, writerPrep } }
const researchScore = this.validateResult(research, 'Researcher');
const writerLevel: AbstractionLevel = researchScore < 65 ? 'laser-focused' : 'balanced';
// --- Phase 4: Writer ---
const finalReport = await this.executeStep(
state, 'writer', '최종 리포트 작성 및 편집 중...',
() => this.resilientExecute(state, this.writer, 'Writer', research, prompt, signal, onProgress, {
context: brainContext, signal, config: { role: 'writer', allowFallback: true },
priorResults: { plan, writerPrep, previousValidData: state.getResult('finalReport') },
abstractionLevel: writerLevel
}),
research, prompt, signal, onProgress
);
this.validateResult(finalReport, 'Writer');
state.setResult('finalReport', finalReport);
// --- Phase 4: Proactive Advisor (Astra v4.0) ---
// 리포트 작성 후, 사용자의 다음 행동을 선제적으로 제안합니다.
// --- Phase 5: Advice & Standardization ---
const proactiveAdvice = await this.generateProactiveAdvice(finalReport, prompt, brainContext, signal);
const enrichedReport = `${finalReport}\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\n${proactiveAdvice}`;
const standardizedReport = WikiFormatter.format(enrichedReport, state);
// 3. 지식 저장 포맷 표준화 (Standardization: Astra 피드백)
const standardizedReport = WikiFormatter.format(enrichedReport, missionId);
// 최종 결과 전역 캐싱 (Deduplication)
CacheManager.set(prompt, 'global_final_report', standardizedReport);
// [Astra v4.0] 지능적 판단 감사 (Cognition Audit)
CognitionAudit.auditPolicyCompliance('MissionComplete', standardizedReport);
this.transition(state, 'completed', '미션 완료', onProgress);
logInfo(`[AgentEngine] 미션 완료: ${missionId} (총 ${state.getElapsedMs()}ms)`);
return standardizedReport;
});
} catch (error: any) {
const { type, rule } = ErrorClassifier.classify(error);
const stageName = (state!.stage || 'unknown').toUpperCase();
this.transition(state!, 'error', `오류 발생: ${error.message}`, onProgress);
// 실패 원인 명시적 기록 (Astra 피드백: Record failure reason)
state!.setFailureReason(`[${type}] ${rule.description} - 세부원인: ${error.message}`);
// Error Recovery Matrix 기반 세분화된 로깅
switch (type) {
case ErrorType.ABORT:
logInfo(`[AgentEngine] [ABORT] 미션 취소됨 (${missionId}) at ${stageName} stage.`);
break;
case ErrorType.TRANSIENT:
logError(`[AgentEngine] [TRANSIENT] 재시도 소진 후 실패 (${missionId}) at ${stageName} stage — ${rule.description}:`, error);
break;
case ErrorType.PERMANENT:
logError(`[AgentEngine] [PERMANENT] 복구 불가 오류 (${missionId}) at ${stageName} stage — ${rule.userMessage}:`, error);
break;
}
// 감사 이력 덤프 (디버깅용)
logError(`[AgentEngine] Audit Trail for ${missionId}:\n${state!.summarizeAudit()}`);
this.handleMissionFailure(missionId, state!, error, onProgress);
throw error;
} finally {
// 3. 락 해제
release();
}
}
/**
* [Atomic Step Execution]
* 개별 작업 단계를 캡슐화하여 상태 확인, 캐시 체크, 실행, 검증을 일관되게 처리합니다.
*/
private async executeStep(
state: MissionState,
stage: PipelineStage,
progressMessage: string,
action: () => Promise<string>,
cacheKeyPrompt: string,
cacheKeyContext: string,
signal: AbortSignal,
onProgress: (stage: PipelineStage, message: string) => void
): Promise<string> {
// 1. 기존 결과 확인 (Resumption)
let result = state.getResult(stage);
if (result) {
logInfo(`[AgentEngine] [Resumption] '${stage}' 단계 결과가 이미 존재합니다.`);
return result;
}
this.transition(state, stage, progressMessage, onProgress);
this.checkAbort(signal);
// 2. 캐시 확인 (Deduplication)
const cached = CacheManager.get(cacheKeyPrompt, cacheKeyContext);
if (cached) {
logInfo(`[AgentEngine] [Deduplication] '${stage}' 단계 캐시를 사용합니다.`);
state.resilienceMetrics.deduplications++;
result = cached;
} else {
// 3. 실행
result = await action();
CacheManager.set(cacheKeyPrompt, cacheKeyContext, result);
}
// 4. 저장
state.setResult(stage, result);
return result;
}
private handleMissionFailure(missionId: string, state: MissionState, error: any, onProgress: (stage: PipelineStage, message: string) => void) {
const { type, rule } = ErrorClassifier.classify(error);
const stageName = (state.stage || 'unknown').toUpperCase();
this.transition(state, 'error', `오류 발생: ${error.message}`, onProgress);
state.setFailureReason(`[${type}] ${rule.description} - 세부원인: ${error.message}`);
logError(`[AgentEngine] [${type}] ${missionId} 실패 at ${stageName} stage: ${error.message}`);
logError(`[AgentEngine] Audit Trail:\n${state.summarizeAudit()}`);
}
/**
* @deprecated 이제 미션 상태는 로컬 스코프에서 관리됩니다.
*/
@@ -586,6 +610,7 @@ export class AgentEngine {
let lastError: any;
for (let attempt = 0; attempt <= transientRule.maxRetries; attempt++) {
if (attempt > 0) state.resilienceMetrics.retries++;
try {
// 재시도 시 사용자에게 진행 상황 알림
if (attempt > 0) {
@@ -605,6 +630,10 @@ export class AgentEngine {
const result = await agent.execute(input, amplifiedContext, signal, options);
const durationMs = Date.now() - startTime;
// [Reliability Check] 충돌 위험도 추적
const conflictScore = AgentDataValidator.validateHandoff(agentName, result);
state.resilienceMetrics.maxConflictScore = Math.max(state.resilienceMetrics.maxConflictScore, conflictScore);
PerformanceProfiler.logLLMLatency(agentName, durationMs, result.length);
return result;
@@ -629,6 +658,15 @@ export class AgentEngine {
case ErrorType.TRANSIENT:
// 일시적 오류 → 재시도 가능 여부 확인
if (attempt >= transientRule.maxRetries) {
// [Intelligent Resilience] 재시도 실패 시 대체 경로(Fallback) 시도
if (transientRule.action === 'fallback' || options?.config?.allowFallback) {
logInfo(`[AgentEngine] [FALLBACK] ${agentName} 재시도 소진. 대체 데이터(Cache/Stale) 경로를 가동합니다.`);
state.resilienceMetrics.fallbacks++;
const cached = CacheManager.get(input, context);
if (cached) return cached;
if (options?.priorResults?.['previousValidData']) return options.priorResults['previousValidData'];
}
logError(`[AgentEngine] [TRANSIENT] ${agentName} 최대 재시도 횟수(${transientRule.maxRetries}) 소진.`);
const exhaustedError = new Error(
`[${agentName}] 일시적 연결 오류가 지속됩니다. ` +
@@ -689,9 +727,9 @@ export class AgentEngine {
return clean.length > length ? clean.substring(0, length) + '...' : clean;
}
private validateResult(data: string, step: string) {
private validateResult(data: string, step: string): number {
// Error Recovery Matrix: Permanent 오류 발생을 방지하기 위한 선제적 핸드오프 검증
AgentDataValidator.validateHandoff(step, data);
return AgentDataValidator.validateHandoff(step, data);
}
/**
@@ -699,10 +737,28 @@ export class AgentEngine {
* 지식 관리 정책 v4.0을 LLM 지시사항으로 변환하여 주입합니다.
*/
private amplifyContext(context: string, options?: AgentExecuteOptions): string {
const level = options?.abstractionLevel || 'balanced';
const levelDirectives: Record<AbstractionLevel, string[]> = {
'laser-focused': [
"- [INTENSITY: HIGH] 불필요한 배경 설명과 중간 과정을 생략하고, 즉각적인 결론과 실행 가능한 To-Do 위주로 압축하십시오.",
"- [QUALITY] 정보의 양보다 '추론 기여 밀도'를 극한으로 높여 핵심 위주로 서술하십시오."
],
'balanced': [
"- [INTENSITY: MEDIUM] 핵심 논리를 중심으로 설명하되, 판단 근거가 되는 주요 데이터는 포함하십시오.",
"- [QUALITY] 지식의 양보다 '추론 기여 밀도'를 중시하여 서술하십시오."
],
'comprehensive': [
"- [INTENSITY: LOW] 전체 맥락을 파악할 수 있도록 배경 지식과 세부 데이터를 충분히 포함하십시오.",
"- [QUALITY] 포괄적인 분석을 위해 가용한 모든 데이터를 구조화하여 제공하십시오."
]
};
const policyDirectives = [
"\n### 🏛️ Knowledge Management Policy v4.0 Applied",
"\n### 🏛️ Knowledge Management Policy v4.1 (Filtering Applied)",
`- [ABSTRACTION LEVEL] 현재 에이전트 간 인터페이스 정책은 '${level}' 모드입니다.`,
...levelDirectives[level],
"- [CREDIBILITY] 정보 출처가 의도적으로 작성된 글인 경우 Medium 이상의 신뢰도를 부여하고 우선적으로 인용하십시오.",
"- [QUALITY] 지식의 양보다 '추론 기여 밀도'를 중시하여 핵심 위주로 서술하십시오.",
"- [CONFLICT] 상충되는 지식 발견 시 스스로 판단하지 말고 반드시 [CONFLICT WARNING] 플래그와 함께 두 관점을 모두 보고하십시오."
].join('\n');