feat: Intelligent Resilience & Trust Reporting (v2.77.2)
This commit is contained in:
+153
-97
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user