feat: v2.62.0 - Astra Autonomous Loop (AAL) foundation & enhanced file analysis
This commit is contained in:
+469
-26
@@ -3,25 +3,311 @@ import { lockManager } from '../core/lock';
|
||||
import { actionQueue } from '../core/queue';
|
||||
import { logInfo, logError } from '../utils';
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 1. 에이전트 인터페이스 확장 (Interface Extensibility)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 에이전트 인터페이스 정의 (의존성 주입을 위함)
|
||||
* 에이전트 실행 시 전달되는 확장 옵션 객체.
|
||||
* 향후 에이전트별로 고유한 설정(temperature, maxTokens 등)을
|
||||
* IAgent 시그니처를 변경하지 않고 유연하게 주입할 수 있습니다.
|
||||
*/
|
||||
export interface AgentExecuteOptions {
|
||||
/** 에이전트 실행의 추가 컨텍스트 문자열 */
|
||||
context?: string;
|
||||
/** 실행 중단 시그널 */
|
||||
signal?: AbortSignal;
|
||||
/** 에이전트별 커스텀 설정 (temperature, maxTokens 등) */
|
||||
config?: Record<string, unknown>;
|
||||
/** 이전 단계의 중간 결과물 (병렬 파이프라인용) */
|
||||
priorResults?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 에이전트 인터페이스 정의 (의존성 주입을 위함).
|
||||
* execute()는 기존 시그니처를 유지하면서, 확장 옵션도 수용합니다.
|
||||
*/
|
||||
export interface IAgent {
|
||||
execute(input: string, context?: string, signal?: AbortSignal): Promise<string>;
|
||||
execute(input: string, context?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string>;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 2. 상태 관리의 명시적 분리 (Explicit State Management)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 파이프라인 단계 상태 정의
|
||||
*/
|
||||
export type PipelineStage = 'idle' | 'planner' | 'researcher' | 'writer' | 'completed' | 'error';
|
||||
|
||||
/**
|
||||
* 감사(Audit) 이력에 기록되는 단일 상태 전환 엔트리.
|
||||
*/
|
||||
export interface AuditEntry {
|
||||
from: PipelineStage;
|
||||
to: PipelineStage;
|
||||
message: string;
|
||||
timestamp: number;
|
||||
durationFromPrev?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* MissionState: 엔진의 내부 상태를 캡슐화하는 독립 객체.
|
||||
* 상태 전환의 모든 이력(Audit Trail)을 자동으로 기록하며,
|
||||
* 외부 모니터링 시스템과 연동하여 투명한 파이프라인 추적을 가능하게 합니다.
|
||||
*/
|
||||
export class MissionState {
|
||||
private _stage: PipelineStage = 'idle';
|
||||
private _auditTrail: AuditEntry[] = [];
|
||||
private _lastTransitionTime: number = Date.now();
|
||||
|
||||
public readonly missionId: string;
|
||||
public readonly startTime: number;
|
||||
|
||||
constructor(missionId: string) {
|
||||
this.missionId = missionId;
|
||||
this.startTime = Date.now();
|
||||
this._lastTransitionTime = this.startTime;
|
||||
}
|
||||
|
||||
get stage(): PipelineStage {
|
||||
return this._stage;
|
||||
}
|
||||
|
||||
get auditTrail(): ReadonlyArray<AuditEntry> {
|
||||
return this._auditTrail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태를 전환하고, 감사 이력에 자동으로 기록합니다.
|
||||
*/
|
||||
public transition(to: PipelineStage, message: string): void {
|
||||
const now = Date.now();
|
||||
const entry: AuditEntry = {
|
||||
from: this._stage,
|
||||
to,
|
||||
message,
|
||||
timestamp: now,
|
||||
durationFromPrev: now - this._lastTransitionTime
|
||||
};
|
||||
this._auditTrail.push(entry);
|
||||
this._stage = to;
|
||||
this._lastTransitionTime = now;
|
||||
|
||||
logInfo(`[MissionState] ${this.missionId}: ${entry.from} → ${entry.to} (${entry.durationFromPrev}ms) — ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 미션의 경과 시간을 반환합니다.
|
||||
*/
|
||||
public getElapsedMs(): number {
|
||||
return Date.now() - this.startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 감사 이력을 요약 문자열로 반환합니다 (디버깅/모니터링 용).
|
||||
*/
|
||||
public summarizeAudit(): string {
|
||||
return this._auditTrail
|
||||
.map(e => `[${e.from}→${e.to}] ${e.durationFromPrev}ms: ${e.message}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 감사 이력을 구조화된 JSON 포맷으로 출력합니다.
|
||||
* 외부 모니터링 시스템(ELK Stack, Prometheus, Loki 등)과 연동 시
|
||||
* 파싱 없이 바로 인제스트할 수 있는 표준 로그 포맷입니다.
|
||||
*
|
||||
* 출력 예시:
|
||||
* ```json
|
||||
* {
|
||||
* "missionId": "mission_1714...",
|
||||
* "status": "completed",
|
||||
* "totalElapsedMs": 12450,
|
||||
* "transitions": [
|
||||
* { "from": "idle", "to": "planner", "durationMs": 0, "message": "...", "ts": "..." }
|
||||
* ]
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public toStructuredLog(): object {
|
||||
return {
|
||||
missionId: this.missionId,
|
||||
status: this._stage,
|
||||
startTime: new Date(this.startTime).toISOString(),
|
||||
totalElapsedMs: this.getElapsedMs(),
|
||||
transitionCount: this._auditTrail.length,
|
||||
transitions: this._auditTrail.map(e => ({
|
||||
from: e.from,
|
||||
to: e.to,
|
||||
durationMs: e.durationFromPrev,
|
||||
message: e.message,
|
||||
ts: new Date(e.timestamp).toISOString()
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 3. Error Recovery Matrix (오류 복구 매트릭스)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 오류 유형 분류.
|
||||
* 에이전트 간 통신 실패 시 발생하는 오류를 세 범주로 분류합니다.
|
||||
*/
|
||||
export enum ErrorType {
|
||||
/** 네트워크 타임아웃, API 응답 지연 등 재시도로 복구 가능한 오류 */
|
||||
TRANSIENT = 'TRANSIENT',
|
||||
/** 프롬프트 구조 문제, 모델 명백한 실패 등 수동 개입이 필요한 오류 */
|
||||
PERMANENT = 'PERMANENT',
|
||||
/** 사용자가 의도적으로 작업을 취소한 경우 */
|
||||
ABORT = 'ABORT'
|
||||
}
|
||||
|
||||
/**
|
||||
* Error Recovery Matrix의 단일 규칙 정의.
|
||||
* 오류 유형별 대응 전략을 선언적으로 공식화합니다.
|
||||
*/
|
||||
export interface RecoveryRule {
|
||||
type: ErrorType;
|
||||
description: string;
|
||||
maxRetries: number;
|
||||
backoffBaseMs: number;
|
||||
action: 'retry' | 'abort' | 'fail_with_message';
|
||||
userMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ┌─────────────────────────────────────────────────────────────────────┐
|
||||
* │ Error Recovery Matrix (오류 복구 매트릭스) │
|
||||
* ├──────────────┬──────────┬──────────┬─────────────────────────────────┤
|
||||
* │ Error Type │ Retries │ Backoff │ Action │
|
||||
* ├──────────────┼──────────┼──────────┼─────────────────────────────────┤
|
||||
* │ TRANSIENT │ 3 │ 1000ms │ Exponential Backoff 자동 재시도 │
|
||||
* │ PERMANENT │ 0 │ N/A │ 즉시 중단 + 사용자 안내 메시지 │
|
||||
* │ ABORT │ 0 │ N/A │ 조용한 종료 (Graceful Exit) │
|
||||
* └──────────────┴──────────┴──────────┴─────────────────────────────────┘
|
||||
*/
|
||||
export const ERROR_RECOVERY_MATRIX: ReadonlyArray<RecoveryRule> = [
|
||||
{
|
||||
type: ErrorType.TRANSIENT,
|
||||
description: '네트워크 타임아웃, 일시적 API 지연, 연결 거부 등',
|
||||
maxRetries: 3,
|
||||
backoffBaseMs: 1000,
|
||||
action: 'retry',
|
||||
userMessage: '일시적인 연결 문제가 감지되었습니다. 자동으로 재시도 중입니다...'
|
||||
},
|
||||
{
|
||||
type: ErrorType.PERMANENT,
|
||||
description: '모델 응답 형식 오류, 프롬프트 구조 문제, 인증 실패 등',
|
||||
maxRetries: 0,
|
||||
backoffBaseMs: 0,
|
||||
action: 'fail_with_message',
|
||||
userMessage: '모델 응답에 근본적인 문제가 발생했습니다. 모델 설정을 확인하거나 다른 모델로 변경해 주세요.'
|
||||
},
|
||||
{
|
||||
type: ErrorType.ABORT,
|
||||
description: '사용자의 의도적 작업 취소',
|
||||
maxRetries: 0,
|
||||
backoffBaseMs: 0,
|
||||
action: 'abort',
|
||||
userMessage: '작업이 취소되었습니다.'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* ErrorClassifier: 에러 객체를 분석하여 유형을 자동 판별합니다.
|
||||
*/
|
||||
export class ErrorClassifier {
|
||||
/** Transient Error로 분류되는 패턴 */
|
||||
private static readonly TRANSIENT_PATTERNS: RegExp[] = [
|
||||
/timeout/i,
|
||||
/ECONNREFUSED/i,
|
||||
/ECONNRESET/i,
|
||||
/ETIMEDOUT/i,
|
||||
/network/i,
|
||||
/fetch failed/i,
|
||||
/Failed to fetch/i,
|
||||
/503/, // Service Unavailable
|
||||
/502/, // Bad Gateway
|
||||
/429/, // Too Many Requests (Rate Limit)
|
||||
/ENOTFOUND/i,
|
||||
/socket hang up/i,
|
||||
];
|
||||
|
||||
/** Permanent Error로 분류되는 패턴 */
|
||||
private static readonly PERMANENT_PATTERNS: RegExp[] = [
|
||||
/401/, // Unauthorized
|
||||
/403/, // Forbidden
|
||||
/404/, // Not Found (잘못된 모델명 등)
|
||||
/유효한 응답을 받지 못했습니다/,
|
||||
/Ollama URL이 설정되지 않았습니다/,
|
||||
/invalid.*model/i,
|
||||
/model.*not found/i,
|
||||
/parse error/i,
|
||||
];
|
||||
|
||||
/**
|
||||
* 에러를 분류하고 해당하는 복구 규칙을 반환합니다.
|
||||
*/
|
||||
public static classify(error: any): { type: ErrorType; rule: RecoveryRule } {
|
||||
// 1. Abort 확인
|
||||
if (error.name === 'AbortError' || error.message === 'AbortError') {
|
||||
return {
|
||||
type: ErrorType.ABORT,
|
||||
rule: ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.ABORT)!
|
||||
};
|
||||
}
|
||||
|
||||
const message = error.message || String(error);
|
||||
|
||||
// 2. Permanent Error 확인 (우선 순위 높음)
|
||||
for (const pattern of this.PERMANENT_PATTERNS) {
|
||||
if (pattern.test(message)) {
|
||||
return {
|
||||
type: ErrorType.PERMANENT,
|
||||
rule: ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.PERMANENT)!
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Transient Error 확인
|
||||
for (const pattern of this.TRANSIENT_PATTERNS) {
|
||||
if (pattern.test(message)) {
|
||||
return {
|
||||
type: ErrorType.TRANSIENT,
|
||||
rule: ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.TRANSIENT)!
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 분류 불가 → 안전하게 Permanent로 처리 (보수적 접근)
|
||||
return {
|
||||
type: ErrorType.PERMANENT,
|
||||
rule: ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.PERMANENT)!
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 4. AgentEngine 본체
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* AgentEngine:
|
||||
* Producer-Consumer 패턴을 기반으로 멀티 에이전트 워크플로우를 오케스트레이션하는 핵심 엔진.
|
||||
* 명시적 락(Mutex)과 의존성 주입(DI)을 통해 안정성과 유연성을 확보합니다.
|
||||
* 명시적 락(Mutex), 의존성 주입(DI), 독립 상태 객체(MissionState),
|
||||
* Error Recovery Matrix를 통해 안정성, 유연성, 투명성, 복원력을 동시에 확보합니다.
|
||||
*
|
||||
* 아키텍처 특징:
|
||||
* - IAgent 인터페이스의 옵션 확장으로 에이전트별 커스텀 설정 지원
|
||||
* - MissionState를 통한 감사(Audit) 이력 자동 기록
|
||||
* - 병렬 준비 단계(Parallel Prep)를 통한 비동기 흐름 정교화
|
||||
* - Error Recovery Matrix 기반의 Transient/Permanent 오류 자동 분류 및 복구
|
||||
*/
|
||||
export class AgentEngine {
|
||||
private stage: PipelineStage = 'idle';
|
||||
private state: MissionState | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly planner: IAgent,
|
||||
@@ -41,6 +327,9 @@ export class AgentEngine {
|
||||
onProgress: (stage: PipelineStage, message: string) => void
|
||||
): Promise<string> {
|
||||
|
||||
// 상태 객체 초기화
|
||||
this.state = new MissionState(missionId);
|
||||
|
||||
// 1. 명시적 락 획득 (Mutex) - 동일 미션의 중복 실행 방지
|
||||
const release = await lockManager.acquire(`mission_${missionId}`);
|
||||
|
||||
@@ -50,51 +339,205 @@ export class AgentEngine {
|
||||
logInfo(`[AgentEngine] 미션 시작: ${missionId}`);
|
||||
|
||||
// --- Phase 1: Planner ---
|
||||
this.updateStage('planner', '전략 수립 중...', onProgress);
|
||||
if (signal.aborted) throw new Error('AbortError');
|
||||
const plan = await this.planner.execute(prompt, brainContext, signal);
|
||||
this.transition('planner', '전략 수립 중...', onProgress);
|
||||
this.checkAbort(signal);
|
||||
logInfo(`[AgentEngine] [Planner] Input Prompt: ${this.summarizeLog(prompt, 50)}`);
|
||||
const plan = await this.resilientExecute(
|
||||
this.planner, 'Planner', prompt, brainContext, signal, onProgress,
|
||||
{ context: brainContext, signal, config: { role: 'planner' } }
|
||||
);
|
||||
this.validateResult(plan, 'Planner');
|
||||
logInfo(`[AgentEngine] [Planner] Output: ${this.summarizeLog(plan, 100)}`);
|
||||
|
||||
// --- Phase 2: Researcher ---
|
||||
this.updateStage('researcher', '핵심 정보 수집 및 분석 중...', onProgress);
|
||||
if (signal.aborted) throw new Error('AbortError');
|
||||
await this.delay(500); // 시스템 부하 분산을 위한 미세 지연
|
||||
const research = await this.researcher.execute(plan, brainContext, signal);
|
||||
// --- Phase 2 & 3: Parallel Prep + Sequential Execution ---
|
||||
this.transition('researcher', '핵심 정보 수집 및 분석 중...', onProgress);
|
||||
this.checkAbort(signal);
|
||||
logInfo(`[AgentEngine] [Researcher] BrainContext Size: ${brainContext?.length || 0} chars`);
|
||||
|
||||
const [research, writerPrep] = await Promise.all([
|
||||
this.resilientExecute(
|
||||
this.researcher, 'Researcher', plan, brainContext, signal, onProgress,
|
||||
{ context: brainContext, signal, config: { role: 'researcher' } }
|
||||
),
|
||||
this.prepareWriterContext(prompt, plan, brainContext)
|
||||
]);
|
||||
this.validateResult(research, 'Researcher');
|
||||
logInfo(`[AgentEngine] [Researcher] Output: ${this.summarizeLog(research, 100)}`);
|
||||
|
||||
// --- Phase 3: Writer ---
|
||||
this.updateStage('writer', '최종 리포트 작성 및 편집 중...', onProgress);
|
||||
if (signal.aborted) throw new Error('AbortError');
|
||||
await this.delay(500);
|
||||
const finalReport = await this.writer.execute(research, prompt, signal);
|
||||
this.transition('writer', '최종 리포트 작성 및 편집 중...', onProgress);
|
||||
this.checkAbort(signal);
|
||||
const finalReport = await this.resilientExecute(
|
||||
this.writer, 'Writer', research, prompt, signal, onProgress,
|
||||
{ context: brainContext, signal, config: { role: 'writer' }, priorResults: { plan, writerPrep } }
|
||||
);
|
||||
this.validateResult(finalReport, 'Writer');
|
||||
logInfo(`[AgentEngine] [Writer] Output: ${this.summarizeLog(finalReport, 100)}`);
|
||||
|
||||
this.updateStage('completed', '미션 완료', onProgress);
|
||||
this.transition('completed', '미션 완료', onProgress);
|
||||
logInfo(`[AgentEngine] 미션 완료: ${missionId} (총 ${this.state!.getElapsedMs()}ms)`);
|
||||
return finalReport;
|
||||
});
|
||||
} catch (error: any) {
|
||||
this.updateStage('error', `오류 발생: ${error.message}`, onProgress);
|
||||
logError(`[AgentEngine] 미션 실패 (${missionId}):`, error);
|
||||
const { type, rule } = ErrorClassifier.classify(error);
|
||||
const stageName = (this.state?.stage || 'unknown').toUpperCase();
|
||||
|
||||
this.transition('error', `오류 발생: ${error.message}`, onProgress);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 감사 이력 덤프 (디버깅용)
|
||||
if (this.state) {
|
||||
logError(`[AgentEngine] Audit Trail for ${missionId}:\n${this.state.summarizeAudit()}`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
// 3. 락 해제
|
||||
release();
|
||||
this.stage = 'idle';
|
||||
if (this.state) {
|
||||
this.state = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateStage(stage: PipelineStage, message: string, onProgress: (stage: PipelineStage, message: string) => void) {
|
||||
this.stage = stage;
|
||||
/**
|
||||
* 현재 미션의 상태 객체를 외부에서 읽을 수 있도록 노출합니다.
|
||||
* 외부 모니터링 시스템 연동에 활용됩니다.
|
||||
*/
|
||||
public getMissionState(): MissionState | null {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
// ─── Resilience Layer ───
|
||||
|
||||
/**
|
||||
* Error Recovery Matrix 기반의 탄력적 에이전트 실행.
|
||||
*
|
||||
* - Transient Error: 지수 백오프(Exponential Backoff)를 적용하여 최대 N회 자동 재시도.
|
||||
* - Permanent Error: 즉시 중단하고 명확한 사용자 메시지를 첨부하여 예외를 전파.
|
||||
* - Abort: 조용하게 예외를 전파 (Graceful Exit).
|
||||
*/
|
||||
private async resilientExecute(
|
||||
agent: IAgent,
|
||||
agentName: string,
|
||||
input: string,
|
||||
context: string,
|
||||
signal: AbortSignal,
|
||||
onProgress: (stage: PipelineStage, message: string) => void,
|
||||
options?: AgentExecuteOptions
|
||||
): Promise<string> {
|
||||
const transientRule = ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.TRANSIENT)!;
|
||||
let lastError: any;
|
||||
|
||||
for (let attempt = 0; attempt <= transientRule.maxRetries; attempt++) {
|
||||
try {
|
||||
// 재시도 시 사용자에게 진행 상황 알림
|
||||
if (attempt > 0) {
|
||||
const backoffMs = transientRule.backoffBaseMs * Math.pow(2, attempt - 1);
|
||||
logInfo(`[AgentEngine] [RETRY] ${agentName} 재시도 ${attempt}/${transientRule.maxRetries} (${backoffMs}ms 후)`);
|
||||
onProgress(this.state?.stage || 'error', `${agentName} 재시도 중... (${attempt}/${transientRule.maxRetries})`);
|
||||
await new Promise(r => setTimeout(r, backoffMs));
|
||||
this.checkAbort(signal);
|
||||
}
|
||||
|
||||
return await agent.execute(input, context, signal, options);
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
const { type, rule } = ErrorClassifier.classify(error);
|
||||
|
||||
switch (type) {
|
||||
case ErrorType.ABORT:
|
||||
// 사용자 취소 → 재시도 없이 즉시 전파
|
||||
logInfo(`[AgentEngine] [ABORT] ${agentName} 실행 취소됨.`);
|
||||
throw error;
|
||||
|
||||
case ErrorType.PERMANENT:
|
||||
// 영구 오류 → 재시도 없이 즉시 중단, 사용자 메시지 첨부
|
||||
logError(`[AgentEngine] [PERMANENT] ${agentName} 복구 불가: ${rule.userMessage}`);
|
||||
const enrichedError = new Error(`[${agentName}] ${rule.userMessage} (원인: ${error.message})`);
|
||||
(enrichedError as any).originalError = error;
|
||||
(enrichedError as any).errorType = ErrorType.PERMANENT;
|
||||
throw enrichedError;
|
||||
|
||||
case ErrorType.TRANSIENT:
|
||||
// 일시적 오류 → 재시도 가능 여부 확인
|
||||
if (attempt >= transientRule.maxRetries) {
|
||||
logError(`[AgentEngine] [TRANSIENT] ${agentName} 최대 재시도 횟수(${transientRule.maxRetries}) 소진.`);
|
||||
const exhaustedError = new Error(
|
||||
`[${agentName}] 일시적 연결 오류가 지속됩니다. ` +
|
||||
`${transientRule.maxRetries}회 재시도 후에도 복구되지 않았습니다. ` +
|
||||
`네트워크 연결 및 모델 서버 상태를 확인해 주세요. (원인: ${error.message})`
|
||||
);
|
||||
(exhaustedError as any).originalError = error;
|
||||
(exhaustedError as any).errorType = ErrorType.TRANSIENT;
|
||||
throw exhaustedError;
|
||||
}
|
||||
logInfo(`[AgentEngine] [TRANSIENT] ${agentName}에서 일시적 오류 감지: ${error.message}`);
|
||||
break; // continue to next attempt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 이론적으로 도달 불가하지만 안전장치
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
// ─── Private Helpers ───
|
||||
|
||||
/**
|
||||
* MissionState를 통한 상태 전환 + 외부 콜백 호출.
|
||||
*/
|
||||
private transition(stage: PipelineStage, message: string, onProgress: (stage: PipelineStage, message: string) => void) {
|
||||
if (this.state) {
|
||||
this.state.transition(stage, message);
|
||||
}
|
||||
onProgress(stage, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* AbortSignal 확인을 일관되게 처리합니다.
|
||||
*/
|
||||
private checkAbort(signal: AbortSignal): void {
|
||||
if (signal.aborted) {
|
||||
throw new Error('AbortError');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writer가 사용할 초기 컨텍스트를 사전에 구성합니다.
|
||||
* Researcher와 병렬로 실행되어 Phase 3 진입 시 즉시 활용 가능합니다.
|
||||
*/
|
||||
private async prepareWriterContext(prompt: string, plan: string, brainContext: string): Promise<string> {
|
||||
const contextSummary = [
|
||||
`[Original Prompt] ${prompt.substring(0, 200)}`,
|
||||
`[Plan Summary] ${plan.substring(0, 300)}`,
|
||||
`[Brain Context Available] ${brainContext ? 'Yes' : 'No'} (${brainContext?.length || 0} chars)`
|
||||
].join('\n');
|
||||
|
||||
logInfo(`[AgentEngine] [WriterPrep] 초기 컨텍스트 준비 완료 (${contextSummary.length} chars)`);
|
||||
return contextSummary;
|
||||
}
|
||||
|
||||
private summarizeLog(data: string | undefined, length: number = 100): string {
|
||||
if (!data) return 'empty';
|
||||
const clean = data.replace(/\n/g, ' ').trim();
|
||||
return clean.length > length ? clean.substring(0, length) + '...' : clean;
|
||||
}
|
||||
|
||||
private validateResult(data: string, step: string) {
|
||||
if (!data || data.trim().length < 10) {
|
||||
throw new Error(`${step} 에이전트로부터 유효한 응답을 받지 못했습니다.`);
|
||||
}
|
||||
}
|
||||
|
||||
private delay(ms: number) {
|
||||
return new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user