feat: v2.2.74 → v2.2.82 — chunked writer + 코드 리뷰 패치 + /youtube 확장
주요 변경: [chunked writer 아키텍처 (v2.2.74~v2.2.75)] - 5-stage 다중 에이전트(planner/researcher/reflector/writer/synthesizer) 파이프라인 제거 → 단일 ChunkedWriter 의 outline → section[N] → polish 3-step 으로 교체. 본문 분석에서 추상화 손실 / 토큰 폭증 문제 해소 - 답변 길이 자동 분기: 짧은 prompt 는 fast-path direct 1회 호출, 본문 분석은 chunked. outline 빈 배열도 direct 폴백 [코드 리뷰 9개 항목 일괄 패치 (v2.2.76)] - /research polling hang 방어 (heartbeat + status 정규화 + 연속 실패 abort) - 회사 모드 dispatcher abort 신호를 AIService.chat 까지 전달 - bridgeFetch 에 onHeartbeat 콜백 도입 (slow endpoint 사용자 친화적) - dead code 정리: reflectionPersister.ts 제거 + enableReflection 등 좀비 config 키 - parseOutline 의 empty vs fallback reason 명시적 분리 - chatHandlers 의 회사 모드 케이스 ~325줄을 src/sidebar/companyHandlers.ts 로 분리 - Intent Alignment 라운드 한도 도달 시 smart 모드 자동 진행 - LM Studio doSwitch unload 실패 시 currentModel 정리 + load 강행 - retrieval informationDensity → queryCoverage 정합화 [/youtube 채널 지원 (v2.2.77~v2.2.82)] - 채널/플레이리스트 URL 자동 감지 + n:N 으로 영상 개수 지정 (최대 50) - 채널 루트 URL 에 /videos 탭 자동 append (yt-dlp enumeration 정상화) - 영상별 순차 처리 (queue 패턴) + i/N 진행 표시 + 마지막 통계 요약 - mode:info / mode:benchmark / mode:both 분석 모드 분기 - info: 영상 내용을 지식 카드로 추출 (튜토리얼·강의·뉴스용) - benchmark: 4-렌즈 대본 역기획서 (콘텐츠 제작 벤치마크용) - both: 둘 다 (기본) - bare keyword 도 허용: /youtube <url> n:1 info - bridge 에러 메시지 [object Object] 깨짐 수정 (구조화 에러 추출) - "패키지 없음" 등 환경 의존성 에러에 자동 가이드 첨부 [Astra: Setup Datacollect Dependencies 명령 추가 (v2.2.80)] - Python 자동 감지 + yt-dlp / youtube-transcript-api 자동 설치 - macOS PEP 668 환경 자동 폴백 (--user --break-system-packages) - /youtube 등에서 패키지 미설치 감지 시 "Install Now" 버튼 notification [테스트] - tests/agentEngine.test.ts 를 chunked flow 에 맞춰 전체 재작성 - tests/resilience_stress.test.ts Scenario B/D 를 role-aware mock 으로 갱신 - 399/399 통과 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+241
-626
File diff suppressed because it is too large
Load Diff
@@ -59,14 +59,14 @@ describe('Retrieval Orchestrator Phase 5 Integration Tests', () => {
|
||||
const denseChunk = result.selectedChunks.find(c => c.title.includes('doc1'));
|
||||
expect(denseChunk).toBeDefined();
|
||||
if (denseChunk) {
|
||||
expect(denseChunk.metadata.informationDensity).toBeGreaterThan(0);
|
||||
expect(denseChunk.metadata.queryCoverage).toBeGreaterThan(0);
|
||||
expect(denseChunk.metadata.conflictDetected).toBe(false);
|
||||
}
|
||||
|
||||
// 5. Verify Assembled Context String
|
||||
const contextString = orchestrator.buildContextString(result);
|
||||
expect(contextString).toContain('[⚠️ CONFLICT: HIGH]');
|
||||
expect(contextString).toContain('(Density:');
|
||||
expect(contextString).toContain('(Coverage:');
|
||||
});
|
||||
|
||||
test('Score Normalization: should normalize scores across brain sources', () => {
|
||||
|
||||
@@ -115,61 +115,75 @@ describe('Resilience & Boundary Stress Tests', () => {
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
test('[Scenario B] 네트워크 블랙아웃 시 Fallback(이전데이터)으로 자동 복구되어야 한다', async () => {
|
||||
const plannerOutput = 'Plan OK passes validation and meets all length requirements.';
|
||||
const context = 'Resilience Context';
|
||||
const fallbackData = 'Emergency Fallback Data from Previous Step';
|
||||
test('[Scenario B] 섹션 단계 네트워크 블랙아웃 시 Fallback(이전데이터)으로 자동 복구되어야 한다', async () => {
|
||||
// chunked flow 에서는 outline / section / polish 모두 동일 writer 가 처리.
|
||||
// section 단계만 transient 실패하도록 role-aware mock 으로 분기하고,
|
||||
// caller 가 previousValidData 를 주입해 fallback 경로가 활성화되는지 검증.
|
||||
const fallbackData = 'Emergency Fallback Data from Previous Step. Long enough to satisfy validators.';
|
||||
const roleAwareMock: IAgent = {
|
||||
execute: async (_input, _ctx, _signal, options) => {
|
||||
const role = (options?.config?.role as string | undefined) ?? 'section';
|
||||
if (role === 'outline') {
|
||||
return '[{"heading":"본문","scope":"전체 답변"}]';
|
||||
}
|
||||
if (role === 'section') {
|
||||
throw new Error('ECONNREFUSED: Connection refused by peer');
|
||||
}
|
||||
// polish
|
||||
return 'Final Report — polished output that includes recovered data summary.';
|
||||
},
|
||||
};
|
||||
|
||||
const engine = new AgentEngine(
|
||||
new MockSuccessAgent(plannerOutput),
|
||||
new NetworkBlackoutAgent(), // Researcher (여기서 실패 발생)
|
||||
new MockSuccessAgent('Final Report')
|
||||
);
|
||||
|
||||
// [Intelligent Resilience] priorResults를 통해 이전 데이터를 주입하여 Fallback 유도
|
||||
const engine = new AgentEngine(roleAwareMock);
|
||||
const missionId = `stress_fallback_${Date.now()}`;
|
||||
// fast-path 휴리스틱을 우회해 outline → section → polish 가 모두 돌도록 분석 키워드 포함 prompt 사용.
|
||||
const chunkedPrompt = '다음 보고서 본문을 종합적으로 분석해서 핵심 사안을 정리하고 향후 개선 방향을 상세히 제안해 주세요. 리뷰는 가능한 한 꼼꼼하게 작성되어야 합니다.';
|
||||
const result = await engine.runMission(
|
||||
missionId,
|
||||
'Prompt',
|
||||
context,
|
||||
new AbortController().signal,
|
||||
missionId,
|
||||
chunkedPrompt,
|
||||
'Resilience Context',
|
||||
new AbortController().signal,
|
||||
noopProgress,
|
||||
{ priorResults: { previousValidData: fallbackData }, config: { allowFallback: true } }
|
||||
);
|
||||
|
||||
// 최종 결과물에 Writer의 결과가 포함되어야 함 (Researcher는 fallbackData를 반환했을 것임)
|
||||
// polish 가 정상 실행돼야 하고, fallback 카운트가 최소 1 (section 실패 → fallback).
|
||||
expect(result).toContain('Final Report');
|
||||
|
||||
|
||||
const missionPath = path.join(getBaseDir(), '.astra', 'missions', `${missionId}.json`);
|
||||
const state = JSON.parse(fs.readFileSync(missionPath, 'utf-8'));
|
||||
|
||||
expect(state.resilienceMetrics.fallbacks).toBeGreaterThanOrEqual(1);
|
||||
|
||||
console.log(` ✅ Fallback Recovery (priorResults) Verified: ${state.resilienceMetrics.fallbacks} instances`);
|
||||
|
||||
console.log(` ✅ Fallback Recovery (chunked section step) Verified: ${state.resilienceMetrics.fallbacks} instances`);
|
||||
}, 15000);
|
||||
|
||||
test('[Scenario D] 데이터 충돌 발생 시 Conflict Score가 🚨 High로 기록되어야 한다', async () => {
|
||||
const validPlan = 'Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.';
|
||||
const engine = new AgentEngine(
|
||||
new MockSuccessAgent(validPlan),
|
||||
{
|
||||
execute: async () => {
|
||||
// 명시적 충돌 및 수치 모순 유발
|
||||
test('[Scenario D] 섹션 응답에 충돌 신호 있을 때 Conflict Score 가 🚨 High 로 기록되어야 한다', async () => {
|
||||
// 섹션 단계 응답에 [CONFLICT WARNING] + 수치 모순 + 상충 용어를 같이 넣어
|
||||
// AgentDataValidator.validateHandoff 의 점수가 50 을 넘기는지 확인.
|
||||
const roleAwareMock: IAgent = {
|
||||
execute: async (_input, _ctx, _signal, options) => {
|
||||
const role = (options?.config?.role as string | undefined) ?? 'section';
|
||||
if (role === 'outline') {
|
||||
return '[{"heading":"본문","scope":"전체 답변"}]';
|
||||
}
|
||||
if (role === 'section') {
|
||||
return "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.";
|
||||
}
|
||||
return 'Final report with inconsistencies. This should be long enough to pass validation.';
|
||||
},
|
||||
new MockSuccessAgent('Final report with inconsistencies. This should be long enough to pass validation.')
|
||||
);
|
||||
};
|
||||
|
||||
const engine = new AgentEngine(roleAwareMock);
|
||||
const missionId = `stress_conflict_${Date.now()}`;
|
||||
const result = await engine.runMission(missionId, 'Conflict Test', 'ctx', new AbortController().signal, noopProgress);
|
||||
const chunkedPrompt = '다음 사항을 종합 분석해서 상충 지점과 충돌 위험을 꼼꼼히 보고하고, 정합성 검증 결과를 상세히 정리해 주세요. 리뷰는 가능한 한 자세하게 작성되어야 합니다.';
|
||||
await engine.runMission(missionId, chunkedPrompt, 'ctx', new AbortController().signal, noopProgress);
|
||||
|
||||
const missionPath = path.join(getBaseDir(), '.astra', 'missions', `${missionId}.json`);
|
||||
const state = JSON.parse(fs.readFileSync(missionPath, 'utf-8'));
|
||||
|
||||
// 수치 모순(25) + 상충 용어(15) + 경고 태그(30) = 70점 예상
|
||||
// 수치 모순(25) + 상충 용어(15) + 경고 태그(30) = 70 점 예상
|
||||
expect(state.resilienceMetrics.maxConflictScore).toBeGreaterThan(50);
|
||||
|
||||
|
||||
console.log(` 🚨 High Conflict Detected: Risk Score ${state.resilienceMetrics.maxConflictScore}`);
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user