feat: ConnectAI structural hardening and retrieval precision improvements
This commit is contained in:
+5
-5
@@ -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)
|
||||
|
||||
+10
-10
@@ -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": {
|
||||
+16
@@ -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.
|
||||
+16
@@ -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.
|
||||
+16
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user