feat: ConnectAI structural hardening and retrieval precision improvements

This commit is contained in:
g1nation
2026-05-05 21:37:45 +09:00
parent c2f17cfb03
commit 466e9e4d5f
17 changed files with 424 additions and 160 deletions
+5 -5
View File
@@ -1,6 +1,6 @@
---
id: stress_conflict_1777968986934
date: 2026-05-05T08:16:26.963Z
id: stress_conflict_1777970539221
date: 2026-05-05T08:42:19.251Z
type: knowledge_artifact
standard: P-Reinforce v3.0
tags: [automated, connect_ai, brain_sync]
@@ -28,6 +28,6 @@ Final report with inconsistencies. This should be long enough to pass validation
| **Processing Time** | `0.0s` | ✅ Fast |
### 🔍 Decision Audit Trail
- **[PLANNER]** 전략 수립 중... (11ms)
- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (4ms)
- **[WRITER]** 최종 리포트 작성 및 편집 중... (7ms)
- **[PLANNER]** 전략 수립 중... (12ms)
- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (3ms)
- **[WRITER]** 최종 리포트 작성 및 편집 중... (8ms)
@@ -1,7 +1,7 @@
{
"missionId": "stress_conflict_1777968986934",
"missionId": "stress_conflict_1777970539221",
"status": "completed",
"startTime": "2026-05-05T08:16:26.934Z",
"startTime": "2026-05-05T08:42:19.221Z",
"totalElapsedMs": 30,
"results": {
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
@@ -15,30 +15,30 @@
{
"from": "idle",
"to": "planner",
"durationMs": 11,
"durationMs": 12,
"message": "전략 수립 중...",
"ts": "2026-05-05T08:16:26.945Z"
"ts": "2026-05-05T08:42:19.233Z"
},
{
"from": "planner",
"to": "researcher",
"durationMs": 4,
"durationMs": 3,
"message": "핵심 정보 수집 및 분석 중...",
"ts": "2026-05-05T08:16:26.949Z"
"ts": "2026-05-05T08:42:19.236Z"
},
{
"from": "researcher",
"to": "writer",
"durationMs": 7,
"durationMs": 8,
"message": "최종 리포트 작성 및 편집 중...",
"ts": "2026-05-05T08:16:26.956Z"
"ts": "2026-05-05T08:42:19.244Z"
},
{
"from": "writer",
"to": "completed",
"durationMs": 8,
"durationMs": 7,
"message": "미션 완료",
"ts": "2026-05-05T08:16:26.964Z"
"ts": "2026-05-05T08:42:19.251Z"
}
],
"resilienceMetrics": {
@@ -0,0 +1,16 @@
# Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 **ConnectAI 프로젝트**에만 완전히 집중하겠습니다. ...
## Date
2026-05-05
## Symptom
문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 **ConnectAI 프로젝트**에만 완전히 집중하겠습니다. Astra가 경로를 제공받았음에도 불구하고 왜 그런 심각한 할루시네이션(환각)을 일으켰는지, 그 구조적 원인을 분석하고 이를 방지하기 위한 해결책을 제시하겠습니다. --- ### 1. 할루시네이션 원인 분석 (Root Cause Analysis) 보고해주신 답변의 패턴을 분석했을 때, 다음과 같은 **3가지 복합적 원인**으로 인해 발생했습니다. #### ① 컨텍스트 오염 (Context Contamination) 사용자님께서 질문하실 때 이미 에디터에 `ConnectAI/src/lib/engine.ts`가 열려 있었습니다. AI 모델은 "현재 열린 파일"을 가장 강력한 힌트로 인식합니다. `블로그_v3` 프로젝트가 초기 단계이거나 파일이 적었을 경우, AI는 부족한 정보를 보충하기 위해 눈앞에 보이는 `ConnectAI`의 복잡한 로직을 `...
## Cause
Captured automatically from the current conversation. Confirm root cause during follow-up review if needed.
## Fix
## 간단 요약 이 요청은 프로젝트 지식 생성이 아니라 코드리뷰와 제품 평가 요청입니다. 확인된 파일 구조 기준으로 보면, 이 프로젝트는 지식 수집 워크플로우를 앱 형태로 묶어 운영하려는 도구로 보이며, 먼저 데이터 수집 흐름의 안정성, 외부 연동 실패 처리, 수집 결과의 저장/재처리 가능성을 중심으로 평가해야 합니다. ## 확인된 근거 대상 경로: `/Volumes/Data/project/Antigravity/ConnectAI` 확인된 우선 파일: - `package.json` - `docs/docs/records/docs/README.md` - `docs/records/ConnectAI/README.md` - `README.md` - `docs/Advanced_Features_Implementation_Guide.md` - `docs/AgentEngine_Architecture.md` - `docs/docs/records/docs/bugs/BUG-0001-viewed-integr...
## Prevention
Keep automatic records tied to the active project and verify the relevant test or reproduction path.
@@ -0,0 +1,16 @@
# Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 **ConnectAI 프로젝트**에만 완전히 집중하겠습니다. ...
## Date
2026-05-05
## Symptom
문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 **ConnectAI 프로젝트**에만 완전히 집중하겠습니다. Astra가 경로를 제공받았음에도 불구하고 왜 그런 심각한 할루시네이션(환각)을 일으켰는지, 그 구조적 원인을 분석하고 이를 방지하기 위한 해결책을 제시하겠습니다. --- ### 1. 할루시네이션 원인 분석 (Root Cause Analysis) 보고해주신 답변의 패턴을 분석했을 때, 다음과 같은 **3가지 복합적 원인**으로 인해 발생했습니다. #### ① 컨텍스트 오염 (Context Contamination) 사용자님께서 질문하실 때 이미 에디터에 `ConnectAI/src/lib/engine.ts`가 열려 있었습니다. AI 모델은 "현재 열린 파일"을 가장 강력한 힌트로 인식합니다. `블로그_v3` 프로젝트가 초기 단계이거나 파일이 적었을 경우, AI는 부족한 정보를 보충하기 위해 눈앞에 보이는 `ConnectAI`의 복잡한 로직을 `...
## Cause
Captured automatically from the current conversation. Confirm root cause during follow-up review if needed.
## Fix
--- id: mission_1777980988810 date: 2026-05-05T11:38:09.342Z type: knowledge_artifact standard: P-Reinforce v3.0 tags: [automated, connect_ai, brain_sync] --- ## 📌 Brief Summary ## 최종 합성 보고서: LLM 할루시네이션 발생 가능성 분석 및 안정화 전략... ## 최종 합성 보고서: LLM 할루시네이션 발생 가능성 분석 및 안정화 전략 ### 📝 Executive Summary (요약) 제공해주신 코드는 멀티 에이전트 워크플로우를 오케스트레이션하기 위해 **매우 정교하고 견고하게 설계된 아키텍처**를 보여줍니다. `MissionState`를 통한 명시적인 상태 추적, `Error Recovery Matrix` 기반의 탄력적인 복구 로직, 그리고 `CacheManager`를 통한 중복 방지 기능 등은 시스템의 안정성과 투명성을 극대화하는 강...
## Prevention
Keep automatic records tied to the active project and verify the relevant test or reproduction path.
@@ -0,0 +1,16 @@
# Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 **ConnectAI 프로젝트**에만 완전히 집중하겠습니다. ...
## Date
2026-05-05
## Symptom
문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 **ConnectAI 프로젝트**에만 완전히 집중하겠습니다. Astra가 경로를 제공받았음에도 불구하고 왜 그런 심각한 할루시네이션(환각)을 일으켰는지, 그 구조적 원인을 분석하고 이를 방지하기 위한 해결책을 제시하겠습니다. --- ### 1. 할루시네이션 원인 분석 (Root Cause Analysis) 보고해주신 답변의 패턴을 분석했을 때, 다음과 같은 **3가지 복합적 원인**으로 인해 발생했습니다. #### ① 컨텍스트 오염 (Context Contamination) 사용자님께서 질문하실 때 이미 에디터에 `ConnectAI/src/lib/engine.ts`가 열려 있었습니다. AI 모델은 "현재 열린 파일"을 가장 강력한 힌트로 인식합니다. `블로그_v3` 프로젝트가 초기 단계이거나 파일이 적었을 경우, AI는 부족한 정보를 보충하기 위해 눈앞에 보이는 `ConnectAI`의 복잡한 로직을 `...
## Cause
Captured automatically from the current conversation. Confirm root cause during follow-up review if needed.
## Fix
--- id: mission_1777982222535 date: 2026-05-05T11:58:51.779Z type: knowledge_artifact standard: P-Reinforce v3.0 tags: [automated, connect_ai, brain_sync] --- ## 📌 Brief Summary # 최종 합성 보고서: Multi-Agent Orchestration Engine 아키텍처 심층 검토... # 최종 합성 보고서: Multi-Agent Orchestration Engine 아키텍처 심층 검토 ## 📝 Executive Summary (요약) 본 보고서는 제공된 `AgentEngine` 구현체에 대한 포괄적인 아키텍처 및 기능 건전성 검토 결과를 담고 있습니다. 검토 결과, 해당 엔진은 **동시성 제어, 상태 영속성, 오류 복구 메커니즘** 측면에서 매우 견고하고 정교하게 설계되었습니다. 특히, `MissionState`를 통한 감사 추적 기능과 `E...
## Prevention
Keep automatic records tied to the active project and verify the relevant test or reproduction path.
+2 -2
View File
@@ -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:47:13.411Z",
"updatedAt": "2026-05-05T07:47:13.415Z"
"createdAt": "2026-05-05T11:58:51.782Z",
"updatedAt": "2026-05-05T11:58:51.792Z"
}
+9
View File
@@ -69,3 +69,12 @@
## 2026-05-05
- Auto development record created: development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘-volum_implementation.md
## 2026-05-05
- Auto bug record created: bugs/BUG-0009-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md
## 2026-05-05
- Auto bug record created: bugs/BUG-0010-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md
## 2026-05-05
- Auto bug record created: bugs/BUG-0011-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md
+8 -24
View File
@@ -1,18 +1,12 @@
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'
}
import { ErrorType } from '../types/interfaces';
import { ErrorClassifier } from './engine';
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
type: ApiErrorType;
type: ErrorType;
message: string;
isRecoverable: boolean;
};
@@ -33,9 +27,6 @@ export class ExternalApiHandler {
return false; // 현재는 기본적으로 수동 개입 필요로 처리
}
/**
* 지능형 요청 래퍼 (Resilient Fetch)
*/
public static async request<T>(
call: () => Promise<T>,
context: string
@@ -45,25 +36,18 @@ export class ExternalApiHandler {
return { success: true, data };
} catch (error: any) {
logError(`[ApiHandler] [${context}] 호출 실패:`, error);
const errorType = this.classifyError(error);
// ErrorClassifier를 통해 시스템 통합 분류 적용
const { type, rule } = ErrorClassifier.classify(error);
return {
success: false,
error: {
type: errorType,
type,
message: error.message,
isRecoverable: errorType === ApiErrorType.NETWORK_TIMEOUT || errorType === ApiErrorType.RATE_LIMIT
isRecoverable: type === ErrorType.TRANSIENT || type === ErrorType.AUTH_FAILURE
}
};
}
}
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;
}
}
+35 -4
View File
@@ -48,6 +48,27 @@ export class AgentDataValidator {
return { score, conflictRisk };
}
/**
* [New] 오류 발생 시 안전하게 데이터를 진단합니다 (에러를 던지지 않음).
*/
public static audit(stage: string, data: string): { score: number, conflictRisk: number, issues: string[] } {
const issues: string[] = [];
if (!data || data.trim().length === 0) {
issues.push('데이터가 완전히 비어 있음');
return { score: 0, conflictRisk: 100, issues };
}
if (data.length < 50) issues.push('데이터 길이가 너무 짧음 (잠재적 누락)');
const score = QualityScorer.evaluate(data);
const conflictRisk = ConflictDetector.analyzeSemanticDivergence(data);
if (score < 40) issues.push('품질 점수 매우 낮음');
if (conflictRisk > 60) issues.push('심각한 지식 충돌 감지');
return { score, conflictRisk, issues };
}
}
export class QualityScorer {
@@ -132,17 +153,27 @@ export class ConflictDetector {
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) {
if (Math.abs(posIdx - negIdx) < 400) {
riskScore += 15;
}
}
}
// 3. 명시적 경고 태그 확인
if (data.includes('[CONFLICT WARNING]')) riskScore += 30;
// 3. 기술적 상충 (Technical Divergence)
const technicalConflicts = [
/동기(식)?.*비동기(식)?/g,
/로컬.*클라우드/g,
/정적.*동적/g,
/Success.*Fail/i
];
for (const pattern of technicalConflicts) {
if (pattern.test(data)) riskScore += 10;
}
// 4. 명시적 경고 태그 확인
if (data.includes('[CONFLICT WARNING]') || data.includes('[ERROR]')) riskScore += 30;
return Math.min(100, riskScore);
}
+123 -72
View File
@@ -1,11 +1,14 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { createHash } from 'crypto';
import { lockManager } from '../core/lock';
import { actionQueue } from '../core/queue';
import { logInfo, logError } from '../utils';
import { AgentDataValidator, PerformanceProfiler, CognitionAudit } from './diagnostics';
import { WikiFormatter } from './formatter';
import { ErrorType, RecoveryRule } from '../types/interfaces';
export { ErrorType, RecoveryRule };
// ─────────────────────────────────────────────
// 1. 에이전트 인터페이스 확장 (Interface Extensibility)
@@ -73,6 +76,7 @@ export class MissionState {
public readonly missionId: string;
public readonly startTime: number;
public readonly promptHash: string; // [Structural Fix] 정식 필드로 추가
public resilienceMetrics = {
fallbacks: 0,
retries: 0,
@@ -80,8 +84,9 @@ export class MissionState {
deduplications: 0
};
constructor(missionId: string) {
constructor(missionId: string, promptHash: string = '') {
this.missionId = missionId;
this.promptHash = promptHash; // 생성 시점에 즉시 할당
this.startTime = Date.now();
this._lastTransitionTime = this.startTime;
}
@@ -152,15 +157,24 @@ export class MissionState {
/**
* 저장된 미션 상태를 불러옵니다 (Resumption용).
* 프롬프트 해시를 비교하여 질문이 변경되었을 경우 무효화합니다.
*/
public static loadFromDisk(missionId: string): MissionState | null {
public static loadFromDisk(missionId: string, currentPrompt: string): MissionState | null {
try {
const workspacePath = process.env.ASTRA_TEST_ROOT || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
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);
// [Structural Fix] 프롬프트 무결성 검증
const currentPromptHash = createHash('sha256').update(currentPrompt).digest('hex').slice(0, 16);
if (data.promptHash && data.promptHash !== currentPromptHash) {
logInfo(`[MissionState] 프롬프트 변경 감지. 이전 상태를 폐기하고 새로 시작합니다.`);
return null;
}
const state = new MissionState(missionId, currentPromptHash);
state._stage = data.status;
state._results = data.results || {};
state.resilienceMetrics = data.resilienceMetrics || {
@@ -218,6 +232,7 @@ export class MissionState {
totalElapsedMs: this.getElapsedMs(),
failureReason: this._failureReason,
results: this._results,
promptHash: this.promptHash, // 정식 필드 참조
transitionCount: this._auditTrail.length,
transitions: this._auditTrail.map(e => ({
from: e.from,
@@ -232,39 +247,9 @@ export class MissionState {
}
// ─────────────────────────────────────────────
// 3. Error Recovery Matrix (오류 복구 매트릭스)
// 3. Error Recovery Logic (오류 복구 로직)
// ─────────────────────────────────────────────
/**
* 오류 유형 분류.
* 에이전트 간 통신 실패 시 발생하는 오류를 세 범주로 분류합니다.
*/
export enum ErrorType {
/** 네트워크 타임아웃, API 응답 지연 등 재시도로 복구 가능한 오류 */
TRANSIENT = 'TRANSIENT',
/** 프롬프트 구조 문제, 모델 명백한 실패 등 수동 개입이 필요한 오류 */
PERMANENT = 'PERMANENT',
/** 인증 실패 (API Key 만료 등) */
AUTH_FAILURE = 'AUTH_FAILURE',
/** 사용자가 의도적으로 작업을 취소한 경우 */
ABORT = 'ABORT'
}
/**
* Error Recovery Matrix의 단일 규칙 정의.
* 오류 유형별 대응 전략을 선언적으로 공식화합니다.
*/
export interface RecoveryRule {
type: ErrorType;
description: string;
maxRetries: number;
backoffBaseMs: number;
action: 'retry' | 'abort' | 'fail_with_message' | 'fallback';
userMessage: string;
/** 'fallback' 액션 시 실행할 대체 로직 (예: 캐시 반환) */
fallbackDescription?: string;
}
/**
* ┌─────────────────────────────────────────────────────────────────────┐
* │ Error Recovery Matrix (오류 복구 매트릭스) │
@@ -318,9 +303,13 @@ export class ErrorClassifier {
/Failed to fetch/i,
/503/, // Service Unavailable
/502/, // Bad Gateway
/500/, // Internal Server Error (일시적일 수 있음)
/429/, // Too Many Requests (Rate Limit)
/ENOTFOUND/i,
/socket hang up/i,
/out of memory/i, // GPU/System OOM
/oom/i,
/failed to allocate/i
];
/** Permanent Error로 분류되는 패턴 */
@@ -333,6 +322,10 @@ export class ErrorClassifier {
/invalid.*model/i,
/model.*not found/i,
/parse error/i,
/context.*length.*exceeded/i,
/max.*token.*limit/i,
/safety.*filter/i,
/blocked.*content/i
];
/**
@@ -392,28 +385,46 @@ export class CacheManager {
}
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);
return createHash('sha256').update(key).digest('hex');
}
public static get(prompt: string, context: string): string | null {
public static get(prompt: string, context: string, currentModel: string = 'unknown'): 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');
const filePath = path.join(this.getCacheDir(), `${key}.json`); // 확장자 .json으로 변경
if (!fs.existsSync(filePath)) return null;
try {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
// 1. TTL 검증 (기본 7일)
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
if (Date.now() - data.createdAt > SEVEN_DAYS_MS) {
logInfo(`[CacheManager] 캐시 만료됨 (TTL 초과).`);
return null;
}
// 2. 모델 버전 검증
if (data.modelVersion !== currentModel && currentModel !== 'unknown') {
logInfo(`[CacheManager] 모델 버전 불일치 (${data.modelVersion} -> ${currentModel}). 캐시 스킵.`);
return null;
}
return data.result;
} catch (err) {
return null;
}
return null;
}
public static set(prompt: string, context: string, result: string): void {
public static set(prompt: string, context: string, result: string, modelVersion: string = 'unknown'): void {
const key = this.getHash(prompt + context);
const filePath = path.join(this.getCacheDir(), `${key}.cache`);
fs.writeFileSync(filePath, result, 'utf-8');
const filePath = path.join(this.getCacheDir(), `${key}.json`);
const cacheData = {
result,
createdAt: Date.now(),
modelVersion
};
fs.writeFileSync(filePath, JSON.stringify(cacheData, null, 2), 'utf-8');
}
}
@@ -453,10 +464,12 @@ export class AgentEngine {
options?: AgentExecuteOptions
): Promise<string> {
let state: MissionState;
const promptHash = createHash('sha256').update(prompt).digest('hex').slice(0, 16);
// 0. 상태 복원 시도 (Resumption) - 프롬프트 무결성 검사 포함
const existingState = MissionState.loadFromDisk(missionId, prompt);
state = (existingState && existingState.stage !== 'completed') ? existingState : new MissionState(missionId, promptHash);
// 0. 상태 복원 시도 (Resumption)
const existingState = MissionState.loadFromDisk(missionId);
state = (existingState && existingState.stage !== 'completed') ? existingState : new MissionState(missionId);
// 1. 명시적 락 획득 (Mutex)
const release = await lockManager.acquire(`mission_${missionId}`);
@@ -465,10 +478,12 @@ export class AgentEngine {
return await actionQueue.enqueue(async () => {
logInfo(`[AgentEngine] 미션 시작: ${missionId}`);
// [Blind Spot Check] 전역 중복 제거 (Global Deduplication)
const globalCache = CacheManager.get(prompt, 'global_final_report');
// [Structural Fix] 글로벌 캐시 키에 프롬프트 해시 결합 (네임스페이스 분리)
const globalCacheKey = `global_final_report_${promptHash}`;
const currentModel = (options?.config?.model as string) || 'unknown';
const globalCache = CacheManager.get(prompt, globalCacheKey, currentModel);
if (globalCache) {
logInfo(`[AgentEngine] [Deduplication] 최종 리포트 캐시를 발견했습니다. 실행을 스킵합니다.`);
logInfo(`[AgentEngine] [Deduplication] '${globalCacheKey}' 캐시를 발견했습니다.`);
this.transition(state, 'completed', '캐시된 결과 반환', onProgress);
return globalCache;
}
@@ -478,20 +493,25 @@ export class AgentEngine {
state, 'planner', '전략 수립 중...',
() => this.resilientExecute(state, this.planner, 'Planner', prompt, brainContext, signal, onProgress, {
...options,
context: brainContext, signal, config: { ...options?.config, role: 'planner' }
context: brainContext,
signal,
config: { ...options?.config, role: 'planner', isSamePrompt: true }
}),
prompt, brainContext, signal, onProgress
);
const plannerScore = this.validateResult(plan, 'Planner');
const researcherLevel: AbstractionLevel = plannerScore < 70 ? 'laser-focused' : 'balanced';
// [Structural Fix] 점수가 낮을수록 더 상세한 근거를 요구(comprehensive)하도록 로직 역전
const researcherLevel: AbstractionLevel = plannerScore < 70 ? 'comprehensive' : 'balanced';
// --- Phase 2: Researcher ---
const research = await this.executeStep(
state, 'researcher', '핵심 정보 수집 및 분석 중...',
() => this.resilientExecute(state, this.researcher, 'Researcher', plan, brainContext, signal, onProgress, {
...options,
context: brainContext, signal, config: { ...options?.config, role: 'researcher' },
context: brainContext,
signal,
config: { ...options?.config, role: 'researcher', isSamePrompt: true },
abstractionLevel: researcherLevel
}),
plan, brainContext, signal, onProgress
@@ -505,14 +525,17 @@ export class AgentEngine {
}
const researchScore = this.validateResult(research, 'Researcher');
const writerLevel: AbstractionLevel = researchScore < 65 ? 'laser-focused' : 'balanced';
// [Structural Fix] 점수가 낮을수록 더 상세한 근거를 요구(comprehensive)하도록 로직 역전
const writerLevel: AbstractionLevel = researchScore < 65 ? 'comprehensive' : 'balanced';
// --- Phase 4: Writer ---
const finalReport = await this.executeStep(
state, 'writer', '최종 리포트 작성 및 편집 중...',
() => this.resilientExecute(state, this.writer, 'Writer', research, prompt, signal, onProgress, {
...options,
context: brainContext, signal, config: { role: 'writer', allowFallback: true, ...options?.config },
context: brainContext,
signal,
config: { role: 'writer', allowFallback: true, isSamePrompt: true, ...options?.config },
priorResults: { plan, writerPrep, previousValidData: state.getResult('finalReport'), ...options?.priorResults },
abstractionLevel: writerLevel
}),
@@ -523,11 +546,16 @@ export class AgentEngine {
// --- 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}`;
// [Structural Fix] 생성된 제안의 무결성 검증 (최소 길이 50자 이상일 때만 append)
const enrichedReport = proactiveAdvice && proactiveAdvice.length > 50
? `${finalReport}\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\n${proactiveAdvice}`
: finalReport;
const standardizedReport = WikiFormatter.format(enrichedReport, state);
// 최종 결과 전역 캐싱 (Deduplication)
CacheManager.set(prompt, 'global_final_report', standardizedReport);
// 최종 결과 전역 캐싱 (Deduplication - Secure Key with Model Awareness)
CacheManager.set(prompt, `global_final_report_${promptHash}`, standardizedReport, currentModel);
CognitionAudit.auditPolicyCompliance('MissionComplete', standardizedReport);
this.transition(state, 'completed', '미션 완료', onProgress);
@@ -664,6 +692,11 @@ export class AgentEngine {
case ErrorType.PERMANENT:
// 영구 오류 → 재시도 없이 즉시 중단, 사용자 메시지 첨부
logError(`[AgentEngine] [PERMANENT] ${agentName} 복구 불가: ${rule.userMessage}`);
// [Self-Correction] 에러 발생 직전의 데이터 상태를 짧게 진단 (안전한 audit 모드 사용)
const auditResult = AgentDataValidator.audit(agentName, input.substring(0, 500));
logInfo(`[AgentEngine] [Pre-Failure Audit] Risk: ${auditResult.conflictRisk} | Issues: ${auditResult.issues.join(', ') || 'None'}`);
const enrichedError = new Error(`[${agentName}] ${rule.userMessage} (원인: ${error.message})`);
(enrichedError as any).originalError = error;
(enrichedError as any).errorType = ErrorType.PERMANENT;
@@ -676,9 +709,19 @@ export class AgentEngine {
if (transientRule.action === 'fallback' || options?.config?.allowFallback) {
logInfo(`[AgentEngine] [FALLBACK] ${agentName} 재시도 소진. 대체 데이터(Cache/Stale) 경로를 가동합니다.`);
state.resilienceMetrics.fallbacks++;
const cached = CacheManager.get(input, context);
// 1. 캐시 시도 (현재 프롬프트/컨텍스트 및 모델에 종속됨)
const currentModel = (options?.config?.model as string) || 'unknown';
const cached = CacheManager.get(input, context, currentModel);
if (cached) return cached;
if (options?.priorResults?.['previousValidData']) return options.priorResults['previousValidData'];
// 2. [Structural Fix] 플래그 대신 입력/컨텍스트 해시를 직접 검증하여 Fallback 안전성 확보
const inputHash = createHash('sha256').update(input).digest('hex').slice(0, 16);
const contextHash = createHash('sha256').update(context || '').digest('hex').slice(0, 16);
if (options?.priorResults?.['previousValidData'] && (state.promptHash === inputHash || state.promptHash === contextHash)) {
return options.priorResults['previousValidData'];
}
}
logError(`[AgentEngine] [TRANSIENT] ${agentName} 최대 재시도 횟수(${transientRule.maxRetries}) 소진.`);
@@ -785,16 +828,24 @@ export class AgentEngine {
* 수행된 작업 결과를 분석하여 다음 단계의 의사결정 포크를 제안합니다.
*/
private async generateProactiveAdvice(report: string, originalPrompt: string, context: string, signal: AbortSignal): Promise<string> {
logInfo(`[AgentEngine] 선제적 제안 생성 중...`);
// 본 로직은 별도의 가벼운 추론 단계를 거치거나, WriterAgent의 기능을 확장하여 구현할 수 있습니다.
// 현재는 WriterAgent에게 한 번 더 질의하거나, 리포트에서 액션 아이템을 추출하는 구조로 시작합니다.
const advicePrompt = `다음 리포트를 읽고, 사용자가 다음에 내려야 할 '전략적 의사결정'이나 '실행 작업' 3가지를 제안해줘.
// [Structural Fix] 절단 없는 컨텍스트 전달 (LLM 상상력 제한)
const advicePrompt = `사용자의 원래 요청과 작성된 최종 리포트를 바탕으로,
사용자가 다음에 내려야 할 '전략적 의사결정'이나 '실행 작업' 3가지를 구체적으로 제안해주십시오.
존재하지 않는 사실을 지어내지 말고, 리포트에 명시된 근거만을 활용하십시오.
원래 요청: ${originalPrompt}
리포트 요약: ${report.substring(0, 1000)}...`;
리포트 내용:
${report}`;
try {
// WriterAgent를 Advisor 모드로 재활용
return await this.writer.execute(advicePrompt, context, signal, { config: { role: 'advisor' } });
// Advisor 전용 설정을 주입하여 역할 혼용 방지
return await this.writer.execute(advicePrompt, context, signal, {
config: {
role: 'advisor',
temperature: 0.1, // 창의성 억제, 사실성 강화
maxTokens: 500
}
});
} catch (err) {
return "다음 단계에 대한 자동 제안을 생성하지 못했습니다. 리포트의 결론 섹션을 참고해 주세요.";
}
+15 -4
View File
@@ -26,10 +26,17 @@ export class WikiFormatter {
let cleanContent = content.replace(/^---[\s\S]*?---\n*/, '');
let formatted = frontmatter + cleanContent;
// 2. 필수 헤더 보정
// 2. 지능형 요약 추출 및 보정
if (!formatted.includes('## 📌 Brief Summary')) {
const summary = cleanContent.split('\n').find(l => l.trim().length > 10) || '요약이 생성되지 않았습니다.';
const summarySection = `## 📌 Brief Summary\n${summary.substring(0, 200)}...\n\n`;
const lines = cleanContent.split('\n');
// '📌', '요약', 'Summary'가 포함된 줄을 먼저 찾거나, 20자 이상의 첫 단락을 찾음
let summary = lines.find(l => l.includes('📌') || l.includes('Summary') || l.includes('요약'))?.replace(/^[#\s📌]*/, '');
if (!summary) {
summary = lines.find(l => l.trim().length > 30 && !l.startsWith('#')) || '요약 내용을 추출할 수 없습니다.';
}
const summarySection = `## 📌 Brief Summary\n${summary.substring(0, 300)}${summary.length > 300 ? '...' : ''}\n\n`;
formatted = formatted.replace(/---\n\n/, `---\n\n${summarySection}`);
}
@@ -59,7 +66,11 @@ export class WikiFormatter {
`| **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'),
// 재시도(retry) 등 반복적인 로그는 제외하고 핵심 단계 전환만 필터링하여 출력
state.auditTrail
.filter(a => !a.message.includes('재시도') && a.from !== a.to)
.map(a => `- **[${a.to.toUpperCase()}]** ${a.message} (${a.durationFromPrev}ms)`)
.join('\n'),
''
].join('\n');
+10 -6
View File
@@ -52,12 +52,16 @@ export function selectWithinBudget(
// 1. Sort by score descending
const sorted = [...chunks].sort((a, b) => b.score - a.score);
// 2. Deduplicate by filePath
const seen = new Set<string>();
const deduped = sorted.filter((chunk) => {
// 2. [Structural Fix] 파일당 청크 수 제한 완화 (Deduplication -> Multi-context)
const fileChunkCounts = new Map<string, number>();
const filtered = sorted.filter((chunk) => {
const key = chunk.metadata.filePath || chunk.id;
if (seen.has(key)) return false;
seen.add(key);
const count = fileChunkCounts.get(key) || 0;
// 파일당 최대 3개까지의 주요 맥락 허용 (정보 유실 방지)
if (count >= 3) return false;
fileChunkCounts.set(key, count + 1);
return true;
});
@@ -66,7 +70,7 @@ export function selectWithinBudget(
const dropped: RetrievalChunk[] = [];
let tokensUsed = 0;
for (const chunk of deduped) {
for (const chunk of filtered) {
const chunkTokens = chunk.tokenEstimate || estimateTokens(chunk.content);
if (selected.length >= cfg.maxChunks) {
+33 -29
View File
@@ -65,14 +65,12 @@ const SCORING_CONFIG = {
// ─── Global Search State & Cache ───
const TOKEN_CACHE = new Map<string, string[]>();
const IDF_CACHE = new Map<string, number>();
/**
* 캐시를 명시적으로 비웁니다. 문서 집합이 크게 변경되었을 때 사용합니다.
*/
export function clearScoringCache() {
TOKEN_CACHE.clear();
IDF_CACHE.clear();
}
/**
@@ -85,16 +83,18 @@ export function tokenize(text: string): string[] {
const normalized = text
.toLowerCase()
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.replace(/[^\w\s가-힣_.-]/g, ' ');
.replace(/[^\w\s가-힣_+#.-]/g, ' ');
// [Refinement] 영문/숫자와 한글 경계에서 분리하도록 개선
const splitText = normalized.replace(/([a-z0-9]+)([가-힣]+)/gi, '$1 $2').replace(/([가-힣]+)([a-z0-9]+)/gi, '$1 $2');
const tokens = splitText
.split(/[^a-z0-9가-힣]+/g)
.map((t) => t.trim())
.split(/[^a-z0-9가-힣+#.-]+/g) // [Structural Fix] C++, C#, .net 등 특수 기호 보존
.map((t) => t.trim().replace(/[.,]$/g, '')) // [Refinement] 문장 끝 마침표/쉼표 제거
.filter((t) => {
if (!t) return false;
// 특수문자만 남은 토큰 제거 (단일 + 나 . 등)
if (/^[+#.-]+$/.test(t)) return false;
// 한글이 포함된 경우 한 글자라도 허용, 그 외(영문/숫자)는 2글자 이상
if (/[가-힣]/.test(t)) return t.length >= 1;
return t.length >= 2;
@@ -110,29 +110,9 @@ const synonymMap = new Map<string, string[]>(SCORING_CONFIG.SYNONYM_DATA);
/**
* 동의어/관련어 확장을 수행합니다.
* SCORING_CONFIG의 중앙 데이터를 참조합니다.
*/
export function expandQuery(tokens: string[]): string[] {
const synonymMap = new Map<string, string[]>([
['성능', ['performance', 'optimization', '최적화', 'speed']],
['performance', ['성능', '최적화', 'optimization', 'speed']],
['아키텍처', ['architecture', '구조', 'structure', 'design']],
['architecture', ['아키텍처', '구조', 'structure', 'design']],
['메모리', ['memory', '기억', 'cache', 'storage']],
['memory', ['메모리', '기억', 'cache', 'storage']],
['버그', ['bug', 'error', '오류', 'issue', 'defect']],
['bug', ['버그', 'error', '오류', 'issue']],
['설계', ['design', '아키텍처', 'architecture', 'pattern']],
['design', ['설계', '아키텍처', 'architecture', 'pattern']],
['배포', ['deploy', 'deployment', 'release', 'ci', 'cd']],
['deploy', ['배포', 'deployment', 'release']],
['테스트', ['test', 'testing', 'spec', 'jest', 'mocha']],
['test', ['테스트', 'testing', 'spec']],
['프로젝트', ['project', '프로그램', 'repo', 'repository']],
['project', ['프로젝트', '프로그램', 'repo']],
['방향', ['direction', '전략', 'strategy', '목표', 'goal']],
['direction', ['방향', '전략', 'strategy', '목표']]
]);
const expanded = new Set(tokens);
for (const token of tokens) {
const synonyms = synonymMap.get(token);
@@ -213,7 +193,7 @@ export function scoreTfIdf(
// Expand query with synonyms
const expandedQuery = expandQuery(queryTokens);
// Compute IDF for each query term
// Compute IDF for each query term (Local cache per document set)
const idfCache = new Map<string, number>();
for (const term of expandedQuery) {
if (!idfCache.has(term)) {
@@ -271,15 +251,28 @@ export function scoreTfIdf(
// Title match bonus for exact query term presence
const titleBoost = queryTokens.some((t) => titleTokens.has(t)) ? 0.2 : 0;
// [Structural Fix] Conflict Penalty 및 음수 점수 방지 (Floor Zero 정책)
const conflictPenalty = conflictSeverity === 'HIGH' ? -1.0
: conflictSeverity === 'MEDIUM' ? -0.5
: conflictSeverity === 'LOW' ? -0.2
: 0;
const finalScore = Math.max(0, score + recencyBoost + titleBoost + conflictPenalty);
// [Structural Fix] Information Density: 쿼리 커버리지 기반으로 계산 방식 정상화
const queryCoverage = expandedQuery.length > 0
? new Set(matchedTerms).size / expandedQuery.length
: 0;
return {
index,
score: score + recencyBoost + titleBoost,
score: finalScore,
titleBoost,
recencyBoost,
matchedTerms: [...new Set(matchedTerms)],
conflictDetected,
conflictSeverity,
informationDensity
informationDensity: queryCoverage // 밀도를 쿼리 커버리지로 대체
};
});
}
@@ -343,6 +336,17 @@ export function extractBestExcerpt(
}
}
// [Structural Fix] 임계값을 충족하는 윈도우가 없을 경우 Fallback (빈 컨텍스트 방지)
if (bestScore <= 0) {
const fallbackSentences = [...scored] // [Structural Fix] 원본 배열 변이 방지 (Shallow Copy)
.sort((a, b) => b.score - a.score)
.slice(0, 2) // 가장 관련성 높은 문장 2개만 추출
.map((s) => s.sentence);
const fallbackResult = fallbackSentences.join(' ');
return fallbackResult.length > maxLength ? fallbackResult.slice(0, maxLength - 3) + '...' : fallbackResult;
}
// 4. Result construction with semantic context padding
let finalStart = bestStart;
let finalEnd = bestStart + bestLen;
+29
View File
@@ -97,4 +97,33 @@ export interface IHttpService {
* HTTP POST 요청
*/
post(url: string, data: any, options?: any): Promise<any>;
}
// ─── 공용 오류 처리 타입 (Unified Error Handling) ───
/**
* 오류 유형 분류.
*/
export enum ErrorType {
/** 네트워크 타임아웃, API 응답 지연 등 재시도로 복구 가능한 오류 */
TRANSIENT = 'TRANSIENT',
/** 프롬프트 구조 문제, 모델 명백한 실패 등 수동 개입이 필요한 오류 */
PERMANENT = 'PERMANENT',
/** 인증 실패 (API Key 만료 등) */
AUTH_FAILURE = 'AUTH_FAILURE',
/** 사용자가 의도적으로 작업을 취소한 경우 */
ABORT = 'ABORT'
}
/**
* 오류 복구 규칙 정의.
*/
export interface RecoveryRule {
type: ErrorType;
description: string;
maxRetries: number;
backoffBaseMs: number;
action: 'retry' | 'abort' | 'fail_with_message' | 'fallback';
userMessage: string;
fallbackDescription?: string;
}
+3 -2
View File
@@ -21,6 +21,7 @@ import {
} from '../src/lib/engine';
import * as fs from 'fs';
import * as path from 'path';
import { createHash } from 'crypto';
// ─── Setup ───
const getBaseDir = () => {
@@ -622,12 +623,12 @@ describe('Concurrency & Stress Tests', () => {
// resilientExecute의 fallback 로직이 allowFallback 옵션에 반응하는지 테스트
const options: AgentExecuteOptions = {
config: { allowFallback: true },
config: { allowFallback: true, isSamePrompt: true },
priorResults: { previousValidData: expectedFallback }
};
const result = await (engine as any).resilientExecute(
new MissionState('fallback_test'),
new MissionState('fallback_test', createHash('sha256').update(testPrompt).digest('hex').slice(0, 16)),
failingAgent,
'FailingAgent',
testPrompt,
+84
View File
@@ -0,0 +1,84 @@
import {
AgentEngine,
IAgent,
ErrorClassifier,
ErrorType,
MissionState
} from '../src/lib/engine';
import { AgentDataValidator } from '../src/lib/diagnostics';
import * as path from 'path';
import * as fs from 'fs';
describe('ConnectAI Resilience v4.0 Validation', () => {
describe('Enhanced Error Classification', () => {
test('GPU OOM (out of memory) should be classified as TRANSIENT', () => {
const error = new Error('Ollama error: GPU out of memory, failed to allocate weights');
const result = ErrorClassifier.classify(error);
expect(result.type).toBe(ErrorType.TRANSIENT);
expect(result.rule.action).toBe('retry');
});
test('Context length exceeded should be classified as PERMANENT', () => {
const error = new Error('Validation failed: context_length_exceeded for model gemini-pro');
const result = ErrorClassifier.classify(error);
expect(result.type).toBe(ErrorType.PERMANENT);
expect(result.rule.action).toBe('fail_with_message');
});
test('Safety filter triggers should be classified as PERMANENT', () => {
const error = new Error('Response blocked by safety_filter: harmful content detected');
const result = ErrorClassifier.classify(error);
expect(result.type).toBe(ErrorType.PERMANENT);
});
test('500 Internal Server Error should be classified as TRANSIENT', () => {
const error = new Error('HTTP 500: Internal Server Error');
const result = ErrorClassifier.classify(error);
expect(result.type).toBe(ErrorType.TRANSIENT);
});
});
describe('Safe Pre-Failure Audit', () => {
class MockPermanentOOMAgent implements IAgent {
async execute(): Promise<string> {
// 이 에러는 패턴상 PERMANENT로 분류되도록 유도 (테스트용)
throw new Error('404: model not found');
}
}
test('Permanent error should trigger audit without crashing', async () => {
const engine = new AgentEngine(
new MockPermanentOOMAgent(),
{} as IAgent,
{} as IAgent
);
const state = new MissionState('audit_test_mission');
const input = 'This is a test input that should be audited upon failure.';
// audit 메서드가 에러를 던지지 않는지 확인
const auditResult = AgentDataValidator.audit('Planner', input);
expect(auditResult).toHaveProperty('score');
expect(auditResult).toHaveProperty('issues');
// 실제 resilientExecute 흐름에서 에러가 전파되는지 확인
await expect((engine as any).resilientExecute(
state,
new MockPermanentOOMAgent(),
'TestAgent',
input,
'context',
new AbortController().signal,
() => {}
)).rejects.toThrow(/TestAgent/);
});
test('Audit should handle empty input gracefully', () => {
const result = AgentDataValidator.audit('Tester', '');
expect(result.score).toBe(0);
expect(result.issues).toContain('데이터가 완전히 비어 있음');
});
});
});
+10 -2
View File
@@ -77,10 +77,18 @@ describe('Scoring Engine Unit Tests (v2.72.0)', () => {
// Language boundary split should handle alternating chars
expect(tokens).toContain('astra');
expect(tokens).toContain('v2');
expect(tokens).toContain('v2.0'); // [Structural Fix] 점(.)이 포함된 버전 번호 보존 확인
expect(tokens).toContain('한');
expect(tokens).toContain('글');
// Symbols should be filtered out
// [New Feature] 기술 기호 보존 확인 (C++, C#, .net)
const techText = 'I love C++ and C# programming on .net platform.';
const techTokens = tokenize(techText);
expect(techTokens).toContain('c++');
expect(techTokens).toContain('c#');
expect(techTokens).toContain('.net');
// Symbols should be filtered out (except the preserved ones)
expect(tokens.some(t => /^[!@#$]+$/.test(t))).toBe(false);
});