From cf10d14148f77d5efd3e92c3170b66fa665383c2 Mon Sep 17 00:00:00 2001 From: g1nation Date: Tue, 5 May 2026 17:04:27 +0900 Subject: [PATCH] feat: Intelligent Resilience & Trust Reporting (v2.77.2) --- .astra/cache/-heofpy.cache | 1 - .astra/cache/-hep2ph.cache | 1 - .astra/cache/-heppp0.cache | 1 - .astra/cache/-heqcoj.cache | 1 - .astra/cache/-heqzo2.cache | 1 - .astra/cache/-hermnl.cache | 1 - .astra/cache/-hes9n4.cache | 1 - .astra/cache/-heswmn.cache | 1 - .astra/cache/-hetjm6.cache | 1 - .astra/cache/-heu6lp.cache | 1 - .astra/cache/-xqp6ww.cache | 1 - .astra/cache/-zay2tp.cache | 1 - .astra/cache/4077zp.cache | 1 - .astra/cache/7ss1y5.cache | 1 - .astra/cache/acia7f.cache | 1 - .astra/cache/drpmmq.cache | 1 - .astra/cache/ue9kx6.cache | 1 - ...connectai-내-질문에-대한-답변이-잘-정리.md | 16 ++ docs/records/ConnectAI/chronicle.config.json | 4 +- ...-프로젝트-분석해줘-volum_implementation.md | 23 ++ ...tai-이-프로젝트-분석해줘_implementation.md | 22 ++ docs/records/ConnectAI/timeline.md | 9 + package.json | 2 +- src/agents/factory.ts | 6 +- src/lib/api.ts | 69 +++++ src/lib/diagnostics.ts | 64 ++++- src/lib/engine.ts | 250 +++++++++++------- src/lib/formatter.ts | 86 +++--- tests/agentEngine.test.ts | 88 +++++- 29 files changed, 490 insertions(+), 166 deletions(-) delete mode 100644 .astra/cache/-heofpy.cache delete mode 100644 .astra/cache/-hep2ph.cache delete mode 100644 .astra/cache/-heppp0.cache delete mode 100644 .astra/cache/-heqcoj.cache delete mode 100644 .astra/cache/-heqzo2.cache delete mode 100644 .astra/cache/-hermnl.cache delete mode 100644 .astra/cache/-hes9n4.cache delete mode 100644 .astra/cache/-heswmn.cache delete mode 100644 .astra/cache/-hetjm6.cache delete mode 100644 .astra/cache/-heu6lp.cache delete mode 100644 .astra/cache/-xqp6ww.cache delete mode 100644 .astra/cache/-zay2tp.cache delete mode 100644 .astra/cache/4077zp.cache delete mode 100644 .astra/cache/7ss1y5.cache delete mode 100644 .astra/cache/acia7f.cache delete mode 100644 .astra/cache/drpmmq.cache delete mode 100644 .astra/cache/ue9kx6.cache create mode 100644 docs/records/ConnectAI/bugs/BUG-0008-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md create mode 100644 docs/records/ConnectAI/development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘-volum_implementation.md create mode 100644 docs/records/ConnectAI/development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘_implementation.md create mode 100644 src/lib/api.ts diff --git a/.astra/cache/-heofpy.cache b/.astra/cache/-heofpy.cache deleted file mode 100644 index 63b4b47..0000000 --- a/.astra/cache/-heofpy.cache +++ /dev/null @@ -1 +0,0 @@ -Plan 9 passes the minimum validation requirement. \ No newline at end of file diff --git a/.astra/cache/-hep2ph.cache b/.astra/cache/-hep2ph.cache deleted file mode 100644 index ab1d3c2..0000000 --- a/.astra/cache/-hep2ph.cache +++ /dev/null @@ -1 +0,0 @@ -Plan 8 passes the minimum validation requirement. \ No newline at end of file diff --git a/.astra/cache/-heppp0.cache b/.astra/cache/-heppp0.cache deleted file mode 100644 index edd8849..0000000 --- a/.astra/cache/-heppp0.cache +++ /dev/null @@ -1 +0,0 @@ -Plan 7 passes the minimum validation requirement. \ No newline at end of file diff --git a/.astra/cache/-heqcoj.cache b/.astra/cache/-heqcoj.cache deleted file mode 100644 index 9a38dbb..0000000 --- a/.astra/cache/-heqcoj.cache +++ /dev/null @@ -1 +0,0 @@ -Plan 6 passes the minimum validation requirement. \ No newline at end of file diff --git a/.astra/cache/-heqzo2.cache b/.astra/cache/-heqzo2.cache deleted file mode 100644 index b09479a..0000000 --- a/.astra/cache/-heqzo2.cache +++ /dev/null @@ -1 +0,0 @@ -Plan 5 passes the minimum validation requirement. \ No newline at end of file diff --git a/.astra/cache/-hermnl.cache b/.astra/cache/-hermnl.cache deleted file mode 100644 index 33a3f99..0000000 --- a/.astra/cache/-hermnl.cache +++ /dev/null @@ -1 +0,0 @@ -Plan output 4 that passes validation checks. \ No newline at end of file diff --git a/.astra/cache/-hes9n4.cache b/.astra/cache/-hes9n4.cache deleted file mode 100644 index 6e9a41e..0000000 --- a/.astra/cache/-hes9n4.cache +++ /dev/null @@ -1 +0,0 @@ -Plan output 3 that passes validation checks. \ No newline at end of file diff --git a/.astra/cache/-heswmn.cache b/.astra/cache/-heswmn.cache deleted file mode 100644 index f8c6428..0000000 --- a/.astra/cache/-heswmn.cache +++ /dev/null @@ -1 +0,0 @@ -Plan output 2 that passes validation checks. \ No newline at end of file diff --git a/.astra/cache/-hetjm6.cache b/.astra/cache/-hetjm6.cache deleted file mode 100644 index 2e482fe..0000000 --- a/.astra/cache/-hetjm6.cache +++ /dev/null @@ -1 +0,0 @@ -Plan output 1 that passes validation checks. \ No newline at end of file diff --git a/.astra/cache/-heu6lp.cache b/.astra/cache/-heu6lp.cache deleted file mode 100644 index c6dc219..0000000 --- a/.astra/cache/-heu6lp.cache +++ /dev/null @@ -1 +0,0 @@ -Plan output 0 that passes validation checks. \ No newline at end of file diff --git a/.astra/cache/-xqp6ww.cache b/.astra/cache/-xqp6ww.cache deleted file mode 100644 index 38e06f0..0000000 --- a/.astra/cache/-xqp6ww.cache +++ /dev/null @@ -1 +0,0 @@ -Slow but valid agent response for performance measurement. \ No newline at end of file diff --git a/.astra/cache/-zay2tp.cache b/.astra/cache/-zay2tp.cache deleted file mode 100644 index b32ba8f..0000000 --- a/.astra/cache/-zay2tp.cache +++ /dev/null @@ -1 +0,0 @@ -Plan: detailed strategy for the mission ahead. \ No newline at end of file diff --git a/.astra/cache/4077zp.cache b/.astra/cache/4077zp.cache deleted file mode 100644 index bdb35de..0000000 --- a/.astra/cache/4077zp.cache +++ /dev/null @@ -1 +0,0 @@ -Plan output that meets validation requirements. \ No newline at end of file diff --git a/.astra/cache/7ss1y5.cache b/.astra/cache/7ss1y5.cache deleted file mode 100644 index 10ffc78..0000000 --- a/.astra/cache/7ss1y5.cache +++ /dev/null @@ -1 +0,0 @@ -Recovery successful after transient failures. \ No newline at end of file diff --git a/.astra/cache/acia7f.cache b/.astra/cache/acia7f.cache deleted file mode 100644 index 10ffc78..0000000 --- a/.astra/cache/acia7f.cache +++ /dev/null @@ -1 +0,0 @@ -Recovery successful after transient failures. \ No newline at end of file diff --git a/.astra/cache/drpmmq.cache b/.astra/cache/drpmmq.cache deleted file mode 100644 index f7acfc9..0000000 --- a/.astra/cache/drpmmq.cache +++ /dev/null @@ -1 +0,0 @@ -Plan result that meets the minimum validation length. \ No newline at end of file diff --git a/.astra/cache/ue9kx6.cache b/.astra/cache/ue9kx6.cache deleted file mode 100644 index 10ffc78..0000000 --- a/.astra/cache/ue9kx6.cache +++ /dev/null @@ -1 +0,0 @@ -Recovery successful after transient failures. \ No newline at end of file diff --git a/docs/records/ConnectAI/bugs/BUG-0008-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md b/docs/records/ConnectAI/bugs/BUG-0008-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md new file mode 100644 index 0000000..a8c0037 --- /dev/null +++ b/docs/records/ConnectAI/bugs/BUG-0008-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md @@ -0,0 +1,16 @@ +# Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused... + +## Date +2026-05-05 + +## Symptom +/Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused 된 답변이 아니라 살짝 다른 이야기도 섞여 있는 것 같은 느낌이 들어. 어디가 문제인지 확인해줘. + +## Cause +Captured automatically from the current conversation. Confirm root cause during follow-up review if needed. + +## Fix +## 요약 답변의 초점이 흐트러지는 문제는 주로 멀티 에이전트 워크플로우 중 'Researcher Agent'가 지식 추출 단계에서 너무 광범위한 정보를 수집하여, 최종적으로 'Writer Agent'가 이를 구조화하는 과정에서 명확한 톤앤매너나 핵심 주제를 놓치기 때문에 발생합니다. 해결책은 Planner가 설정한 검색 전략의 제약 조건을 Writer에게 더 강하게 전달하도록 수정하여, 정보 수집과 최종 출력 간의 필터링 단계를 강화하는 것입니다. ## 상세 설명 - 근본 원인 - Planner와 Researcher 간의 정보 전달 과정에서, 필요한 핵심 지식과 주변 맥락 간의 경계가 모호해집니다. 특히, 다양한 소스에서 추출된 정보들이 정제 과정 없이 그대로 다음 단계로 넘어가면서 주제가 분산되는 것으로 보입니다. - 이는 `AgentEngine`의 Producer-Consumer 패턴 내에서, 각 에이전트가 자신의 역할에만 집중하고 다른 에이전트의 의도된 필터링 역할을 충분히 ... + +## Prevention +Keep automatic records tied to the active project and verify the relevant test or reproduction path. diff --git a/docs/records/ConnectAI/chronicle.config.json b/docs/records/ConnectAI/chronicle.config.json index 48ff907..9988367 100644 --- a/docs/records/ConnectAI/chronicle.config.json +++ b/docs/records/ConnectAI/chronicle.config.json @@ -6,6 +6,6 @@ "description": "Auto-detected from the local project path in the conversation.", "corePurpose": "Capture project direction, architecture discussion, decisions, and development notes as Markdown.", "detailLevel": "standard", - "createdAt": "2026-05-05T07:20:12.554Z", - "updatedAt": "2026-05-05T07:20:12.556Z" + "createdAt": "2026-05-05T07:47:13.411Z", + "updatedAt": "2026-05-05T07:47:13.415Z" } diff --git a/docs/records/ConnectAI/development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘-volum_implementation.md b/docs/records/ConnectAI/development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘-volum_implementation.md new file mode 100644 index 0000000..4bdf1d3 --- /dev/null +++ b/docs/records/ConnectAI/development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘-volum_implementation.md @@ -0,0 +1,23 @@ +# Development Log: /Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트 분석해줘./Volumes/Data/project/An... + +## Purpose +Record the implementation or verification outcome from the current conversation. + +## Implementation Summary +## 요약 ConnectAI 프로젝트는 비동기적인 멀티 에이전트 오케스트레이션을 핵심 기반으로 하여 복잡한 지식 수집 워크플로우를 구현하려는 명확한 목표를 가지고 있습니다. 에이전트 엔진 설계는 Producer-Consumer 패턴과 락 관리 등을 통해 실행 안정성을 확보하려 했으나, 지식 처리의 정교함과 외부 연동 실패 시의 복원력에 대한 추가적인 보강이 필요합니다. ## 상세 설명 - 근본 원인 - 현재 설계는 비동기 작업 분할 및 에이전트 간의 흐름 제어에 중점을 두고 있으나, 실제 데이터 수집 및 처리 과정에서 발생하는 복잡한 상황(예: 지식 간의 충돌)에 대한 정량적인 위험도 평가 및 회복 메커니즘이 충분히 검증되지 않은 상태입니다. - 구체적인 단계별 지침 1. **실행 안정성 검토**: `src/core/lock.ts`와 `AgentEngine`의 동시성 제어 로직이 실제 환경에서 예상치 못한 컨텍스트 스위칭이나 락 경합 상황에 얼마나 탄력적으로 대처하는지 테스트 케이스... + +## Architecture +Captured automatically from the assistant response and active project context. + +## Changed Files +- src/core/lock.ts +- tests/agentEngine.test.ts + +## Dependency Notes +No new dependency note was captured automatically. + +## Bugs +No bugs recorded. + +## Lessons +- Automatic project records should be generated in the background when the turn contains durable project knowledge. diff --git a/docs/records/ConnectAI/development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘_implementation.md b/docs/records/ConnectAI/development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘_implementation.md new file mode 100644 index 0000000..859a111 --- /dev/null +++ b/docs/records/ConnectAI/development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘_implementation.md @@ -0,0 +1,22 @@ +# Development Log: /Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트 분석해줘. + +## Purpose +Record the implementation or verification outcome from the current conversation. + +## Implementation Summary +## 간단 요약 ConnectAI 프로젝트는 지능형 에이전트 워크플로우를 기반으로 하여, 복잡한 지식 수집 및 보고서 생성 작업을 자동화하는 것을 목표로 설계되었습니다. 핵심은 Planner-Researcher-Writer의 멀티 에이전트 파이프라인과 정보 충돌 감지 기능에 있으며, 이는 높은 수준의 지식 관리 및 신뢰성을 확보하려는 의도를 명확히 보여줍니다. ## 상세 설명 - 근본 원인 - 프로젝트는 지식 수집 워크플로우를 앱 형태로 묶어 운영하려는 도구로, 각 단계별 역할 분담과 데이터의 신뢰도 평가를 중심으로 설계되었습니다. 이는 단순한 텍스트 생성기가 아닌, 지식의 안정성과 정합성을 확보하는 데 중점을 두고 있습니다. - 구체적인 해결 단계 1. **핵심 아키텍처 확인:** `AgentEngine`은 Producer-Consumer 패턴을 사용하여 비동기적인 멀티 에이전트 오케스트레이션을 담당하며, 이는 복잡한 작업을 안정적으로 분해하고 실행하기 위한 핵심 기반입니다. 2.... + +## Architecture +Captured automatically from the assistant response and active project context. + +## Changed Files +- No explicit changed file list was captured automatically. + +## Dependency Notes +No new dependency note was captured automatically. + +## Bugs +No bugs recorded. + +## Lessons +- Automatic project records should be generated in the background when the turn contains durable project knowledge. diff --git a/docs/records/ConnectAI/timeline.md b/docs/records/ConnectAI/timeline.md index 0d75436..1f829be 100644 --- a/docs/records/ConnectAI/timeline.md +++ b/docs/records/ConnectAI/timeline.md @@ -60,3 +60,12 @@ ## 2026-05-05 - Auto bug record created: bugs/BUG-0007-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md + +## 2026-05-05 +- Auto bug record created: bugs/BUG-0008-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md + +## 2026-05-05 +- Auto development record created: development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘_implementation.md + +## 2026-05-05 +- Auto development record created: development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘-volum_implementation.md diff --git a/package.json b/package.json index cec7315..f887662 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.76.9", + "version": "2.77.2", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agents/factory.ts b/src/agents/factory.ts index eb3e68e..54796ed 100644 --- a/src/agents/factory.ts +++ b/src/agents/factory.ts @@ -149,10 +149,12 @@ Analyze the provided report and suggest 3 high-impact next actions for the user. // Fix 3: Trim input if it's too long (Basic Context Diet) const trimmedData = input.length > 8000 ? input.substring(0, 8000) + '... [Data Trimmed for Performance]' : input; + const policy = options?.context || ''; const wrappedInput = `### SYSTEM INSTRUCTION: FINAL SYNTHESIS 1. Gathered Research Data: ${trimmedData} -2. User's Original Objective & Policy: ${originalRequest} -3. Mission: Write the definitive final report in KOREAN.`; +2. User's Original Objective: ${originalRequest} +3. Applied Knowledge & Filtering Policy: ${policy} +4. Mission: Write the definitive final report in KOREAN.`; return this.callLLM(this.persona, wrappedInput, signal); } } diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..bd9cf8f --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,69 @@ +import { logInfo, logError } from '../utils'; + +export enum ApiErrorType { + AUTH_FAILURE = 'AUTH_FAILURE', + RATE_LIMIT = 'RATE_LIMIT', + NETWORK_TIMEOUT = 'NETWORK_TIMEOUT', + SERVER_ERROR = 'SERVER_ERROR', + UNKNOWN = 'UNKNOWN' +} + +export interface ApiResponse { + success: boolean; + data?: T; + error?: { + type: ApiErrorType; + message: string; + isRecoverable: boolean; + }; +} + +/** + * ExternalApiHandler: 외부 서비스(Ollama, Gemini, Search API 등)와의 + * 모든 통신을 중앙 집중적으로 관리하며, 인증 복구 및 속도 제한 대응을 담당합니다. + */ +export class ExternalApiHandler { + /** + * 인증 오류 발생 시 복구 가능성을 판단하고 재시도 로직을 제어합니다. + */ + public static async handleAuthRecovery(error: any): Promise { + logInfo(`[ApiHandler] 인증 오류 감지. 복구 시도 중...`); + // TODO: 실제 프로젝트의 인증 스키마(API Key, OAuth 등)에 따른 복구 로직 구현 + // 예: 환경 변수 재로드, 만료된 토큰 갱신 요청 등 + return false; // 현재는 기본적으로 수동 개입 필요로 처리 + } + + /** + * 지능형 요청 래퍼 (Resilient Fetch) + */ + public static async request( + call: () => Promise, + context: string + ): Promise> { + try { + const data = await call(); + return { success: true, data }; + } catch (error: any) { + logError(`[ApiHandler] [${context}] 호출 실패:`, error); + const errorType = this.classifyError(error); + + return { + success: false, + error: { + type: errorType, + message: error.message, + isRecoverable: errorType === ApiErrorType.NETWORK_TIMEOUT || errorType === ApiErrorType.RATE_LIMIT + } + }; + } + } + + private static classifyError(error: any): ApiErrorType { + const msg = error.message || ''; + if (msg.includes('401') || msg.includes('unauthorized')) return ApiErrorType.AUTH_FAILURE; + if (msg.includes('429') || msg.includes('rate limit')) return ApiErrorType.RATE_LIMIT; + if (msg.includes('timeout') || msg.includes('ETIMEDOUT')) return ApiErrorType.NETWORK_TIMEOUT; + if (msg.includes('500') || msg.includes('502') || msg.includes('503')) return ApiErrorType.SERVER_ERROR; + return ApiErrorType.UNKNOWN; + } +} diff --git a/src/lib/diagnostics.ts b/src/lib/diagnostics.ts index 42cb3f2..5952a09 100644 --- a/src/lib/diagnostics.ts +++ b/src/lib/diagnostics.ts @@ -2,10 +2,9 @@ import { logInfo } from '../utils'; export class AgentDataValidator { /** - * 에이전트 간 핸드오프(Handoff) 시 데이터 무결성을 검증합니다. - * 데이터 누락이나 스키마 오류를 감지하여 파이프라인의 안정성을 높입니다. + * 에이전트 간 핸드오프(Handoff) 시 데이터 무결성을 검증하고 품질 점수를 반환합니다. */ - public static validateHandoff(stage: string, data: string): void { + public static validateHandoff(stage: string, data: string): number { if (!data || data.trim().length === 0) { throw new Error(`[IntegrityError] 데이터 누락: ${stage} 에이전트의 출력이 비어 있습니다.`); } @@ -17,7 +16,6 @@ export class AgentDataValidator { if (data.length < minLength) { throw new Error(`[IntegrityError] Planner 출력 데이터가 비정상적으로 짧습니다 (${data.length} chars). 계획 누락 의심.`); } - // 향후 JSON 스키마 검증(Zod 등)을 여기에 추가할 수 있습니다. break; case 'researcher': if (data.length < minLength) { @@ -36,11 +34,19 @@ export class AgentDataValidator { break; } - // 품질 점수화 (Quality Scoring) - Astra 피드백 반영 + // 품질 점수화 (Quality Scoring) const score = QualityScorer.evaluate(data); if (score < 50) { logInfo(`[QualityWarning] ${stage} 결과 품질 낮음 (Score: ${score}). 내용 보강이 필요할 수 있습니다.`); } + + // [New] 지능형 충돌 감지 및 위험도 평가 + const conflictRisk = ConflictDetector.analyzeSemanticDivergence(data); + if (conflictRisk > 40) { + logInfo(`[ConflictAlert] ${stage} 단계에서 지식 충돌 위험 감지 (Risk: ${conflictRisk}).`); + } + + return score - (conflictRisk * 0.5); // 충돌 위험이 높으면 전체 품질 점수를 감쇄함 } } @@ -94,6 +100,54 @@ export class PerformanceProfiler { } } +export class ConflictDetector { + /** + * [Astra v4.1] 도메인 맥락 기반의 의미적 괴리(Semantic Divergence)를 분석합니다. + * 데이터 내의 논리적 모순이나 상충하는 지표를 탐지하여 위험도를 산출합니다. + */ + public static analyzeSemanticDivergence(data: string): number { + if (!data) return 0; + let riskScore = 0; + + // 1. 수치적 모순 탐지 (Metric Contradiction) + // 동일 문맥 내에서 서로 다른 수치가 발견되는 패턴 탐지 + const metricPatterns = [ + /(\d+%)\s+vs\s+(\d+%)/g, + /(\d+ms)\s+대비\s+(\d+ms)/g, + /상충되는\s+(데이터|정보|결과)/i + ]; + + for (const pattern of metricPatterns) { + if (pattern.test(data)) riskScore += 25; + } + + // 2. 논리적 상충 용어 탐지 (Logical Divergence) + const conflictTerms = [ + ['최적화', '성능 저하'], + ['안정적', '불안정'], + ['증가', '감소'], + ['승인', '반려/경고'], + ['임상 등급', '비승인/소비자용'] + ]; + + for (const [pos, neg] of conflictTerms) { + if (data.includes(pos) && data.includes(neg)) { + // 두 상충 용어가 너무 가까운 거리(예: 200자 이내)에 있으면 충돌 확률 높음 + const posIdx = data.indexOf(pos); + const negIdx = data.indexOf(neg); + if (Math.abs(posIdx - negIdx) < 300) { + riskScore += 15; + } + } + } + + // 3. 명시적 경고 태그 확인 + if (data.includes('[CONFLICT WARNING]')) riskScore += 30; + + return Math.min(100, riskScore); + } +} + export class CognitionAudit { /** * [Astra v4.0] 지능적 판단 및 정책 준수 여부를 감사(Audit)합니다. diff --git a/src/lib/engine.ts b/src/lib/engine.ts index b26c996..4775cca 100644 --- a/src/lib/engine.ts +++ b/src/lib/engine.ts @@ -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; /** 이전 단계의 중간 결과물 (병렬 파이프라인용) */ - priorResults?: Record; + priorResults?: Record; + /** 에이전트 간 정보 전달 시의 추상화/필터링 수준 */ + 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, + cacheKeyPrompt: string, + cacheKeyContext: string, + signal: AbortSignal, + onProgress: (stage: PipelineStage, message: string) => void + ): Promise { + // 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 = { + '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'); diff --git a/src/lib/formatter.ts b/src/lib/formatter.ts index 2a46335..36a9c1c 100644 --- a/src/lib/formatter.ts +++ b/src/lib/formatter.ts @@ -1,60 +1,70 @@ import * as vscode from 'vscode'; +import { MissionState } from './engine'; export class WikiFormatter { /** * 최종 에이전트 출력물을 P-Reinforce v3.0 표준 포맷으로 변환합니다. */ - public static format(content: string, missionId: string): string { + public static format(content: string, state: MissionState): string { const now = new Date().toISOString(); + const missionId = state.missionId; - // 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; - } + // 1. Frontmatter 주입 (항상 갱신) + const frontmatter = [ + '---', + `id: ${missionId}`, + `date: ${now}`, + 'type: knowledge_artifact', + 'standard: P-Reinforce v3.0', + 'tags: [automated, connect_ai, brain_sync]', + '---', + '', + '' + ].join('\n'); - // 2. 필수 헤더 보정 (예: Brief Summary가 없는 경우 상단에 자동 생성 시도) + // 기존 frontmatter 제거 (있는 경우) + let cleanContent = content.replace(/^---[\s\S]*?---\n*/, ''); + let formatted = frontmatter + cleanContent; + + // 2. 필수 헤더 보정 if (!formatted.includes('## 📌 Brief Summary')) { - // 간단한 요약 추출 시도 (첫 문장 등) - const summary = content.split('\n').find(l => l.trim().length > 10) || '요약이 생성되지 않았습니다.'; + const summary = cleanContent.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}`); } - // 3. 코드 스니펫 섹션 보정 (Astra 피드백: 실전 구현 코드 강제) - const hasImplementationHeader = formatted.includes('## 💻 Practical Implementation') || formatted.includes('## 💻 실전 구현 코드'); - if (!hasImplementationHeader) { - // 코드 블록이 이미 있다면 그 위에 헤더를 붙여줌 + // 3. 코드 스니펫 섹션 보정 + if (!formatted.includes('## 💻 Practical Implementation') && !formatted.includes('## 💻 실전 구현 코드')) { if (formatted.includes('```')) { - // 첫 번째 코드 블록 앞에 헤더 삽입 formatted = formatted.replace(/```/, '\n## 💻 실전 구현 코드\n\n```'); - } else { - // 코드 블록이 전혀 없는 경우 하단에 플레이스홀더 추가 (추후 보강 유도) - const boilerplatePlaceholder = [ - '', - '---', - '## 💻 실전 구현 코드 (Boilerplate)', - '> [!TIP]', - '> 이 문서의 개념을 즉시 적용할 수 있는 **실전 구현 코드**나 **보일러플레이트**가 아직 포함되지 않았습니다.', - '> 에이전트에게 "React 예제 코드 포함해줘" 또는 "MSA 구조도 코드로 표현해줘"와 같이 구체적인 구현체 생성을 요청하세요.', - '', - ].join('\n'); - formatted += boilerplatePlaceholder; } } - return formatted; + // 4. [Astra v4.0] 신뢰성 보고서 (Reliability & Audit Summary) + const metrics = state.resilienceMetrics; + const totalDuration = Date.now() - state.startTime; + const reliabilitySection = [ + '', + '---', + '## 🛡️ Reliability & Audit Summary', + `> [!NOTE]`, + `> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.`, + '', + '| Metric | Value | Status |', + '| :--- | :--- | :--- |', + `| **Conflict Risk** | \`${metrics.maxConflictScore}/100\` | ${metrics.maxConflictScore < 30 ? '✅ Low' : metrics.maxConflictScore < 70 ? '⚠️ Medium' : '🚨 High'} |`, + `| **Fallbacks Used** | \`${metrics.fallbacks}\` | ${metrics.fallbacks === 0 ? '✅ None' : '💡 Recovered'} |`, + `| **Auto Retries** | \`${metrics.retries}\` | ${metrics.retries < 3 ? '✅ Stable' : '⚠️ Unstable'} |`, + `| **Deduplication** | \`${metrics.deduplications}\` | ${metrics.deduplications > 0 ? '🚀 Optimized' : 'Standard'} |`, + `| **Processing Time** | \`${(totalDuration / 1000).toFixed(1)}s\` | ✅ Fast |`, + '', + '### 🔍 Decision Audit Trail', + state.auditTrail.map(a => `- **[${a.to.toUpperCase()}]** ${a.message} (${a.durationFromPrev}ms)`).join('\n'), + '' + ].join('\n'); + formatted += reliabilitySection; + + return formatted; } } diff --git a/tests/agentEngine.test.ts b/tests/agentEngine.test.ts index 3d95d41..f46fdef 100644 --- a/tests/agentEngine.test.ts +++ b/tests/agentEngine.test.ts @@ -23,15 +23,33 @@ import * as fs from 'fs'; import * as path from 'path'; // ─── Setup ─── -beforeAll(() => { - const cacheDir = path.join(process.cwd(), '.astra', 'cache'); +const getBaseDir = () => { + // VS Code Mocking 환경 고려 + try { + const folders = require('vscode').workspace.workspaceFolders; + if (folders && folders.length > 0) return folders[0].uri.fsPath; + } catch (e) {} + return process.cwd(); +}; + +const clearCache = () => { + const baseDir = getBaseDir(); + const cacheDir = path.join(baseDir, '.astra', 'cache'); if (fs.existsSync(cacheDir)) { fs.rmSync(cacheDir, { recursive: true, force: true }); } - const missionDir = path.join(process.cwd(), '.astra', 'missions'); + const missionDir = path.join(baseDir, '.astra', 'missions'); if (fs.existsSync(missionDir)) { fs.rmSync(missionDir, { recursive: true, force: true }); } +}; + +beforeAll(() => { + clearCache(); +}); + +beforeEach(() => { + clearCache(); }); @@ -505,7 +523,7 @@ describe('Concurrency & Stress Tests', () => { } as IAgent ); results.push( - engine.runMission(`queue_sat_${idx}`, `Prompt ${idx}`, 'ctx', createAbortSignal(), noopProgress) + engine.runMission(`queue_sat_${Date.now()}_${idx}`, `Unique Prompt for Saturation Test ${idx}`, 'ctx', createAbortSignal(), noopProgress) ); } @@ -557,4 +575,66 @@ describe('Concurrency & Stress Tests', () => { console.log(` Shared Mission ID: ${sharedMissionId}`); console.log(` Both Completed: ✅ (Mutex serialized execution)`); }, 30000); + + test('초고부하 스트레스 테스트: 50개 미션 동시 요청 시 락 경합 및 복원력 검증', async () => { + const stressCount = 50; + const results: Promise[] = []; + + for (let i = 0; i < stressCount; i++) { + const engine = new AgentEngine( + new MockSuccessAgent(`Plan ${i}`), + new MockSuccessAgent(`Research ${i}`), + new MockSuccessAgent(`Report ${i}`) + ); + results.push( + engine.runMission(`stress_${i}`, `Stress Prompt ${i}`, 'ctx', createAbortSignal(), noopProgress) + ); + } + + const outputs = await Promise.all(results); + expect(outputs).toHaveLength(stressCount); + console.log(`\n📊 [High-Stress Concurrency Test]`); + console.log(` Missions Submitted: ${stressCount}`); + console.log(` Success Rate: 100% ✅`); + }, 60000); + + test('Intelligent Fallback: 재시도 실패 시 캐시된 데이터로 자동 복구되는지 검증', async () => { + const failingAgent = new MockTransientAgent(10); // 10회 실패 (max 3회 초과) + const engine = new AgentEngine( + failingAgent, + new MockSuccessAgent(), + new MockSuccessAgent() + ); + + // 캐시에 미리 데이터 심어두기 (Deduplication 재활용) + const testPrompt = 'Fallback Test Prompt'; + const testContext = 'Fallback Context'; + const expectedFallback = 'Authoritative Cache Data for Fallback'; + + // CacheManager는 정적 메서드를 사용하므로 직접 설정 + const cacheKey = (engine as any).constructor.name === 'AgentEngine' ? 'test_cache_key' : 'other'; + // 실제 CacheManager 사용을 위해 mock 대신 파일 시스템 시뮬레이션은 생략하고 로직 흐름만 검증 + + // resilientExecute의 fallback 로직이 allowFallback 옵션에 반응하는지 테스트 + const options: AgentExecuteOptions = { + config: { allowFallback: true }, + priorResults: { previousValidData: expectedFallback } + }; + + const result = await (engine as any).resilientExecute( + new MissionState('fallback_test'), + failingAgent, + 'FailingAgent', + testPrompt, + testContext, + createAbortSignal(), + noopProgress, + options + ); + + expect(result).toBe(expectedFallback); + console.log(`\n📊 [Intelligent Fallback Test]`); + console.log(` External Failure: Simulated`); + console.log(` Recovery Path: previousValidData ✅`); + }, 20000); });