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:
g1nation
2026-05-23 23:13:21 +09:00
parent 0712014fcb
commit ded3eea7ce
39 changed files with 2098 additions and 1820 deletions
+2 -1
View File
@@ -227,7 +227,7 @@ export async function runCeoPlanner(
ai: IAIService,
userPrompt: string,
state: CompanyState,
options: { model?: string; timeoutMs?: number; contractBlock?: string } = {},
options: { model?: string; timeoutMs?: number; contractBlock?: string; signal?: AbortSignal } = {},
): Promise<PlannerResult> {
const baseSystem = applyPromptVars(CEO_PLANNER_PROMPT, { company: state.companyName });
// Contract가 있으면 planner 시스템 프롬프트 끝에 prepend. planner는 task
@@ -243,6 +243,7 @@ export async function runCeoPlanner(
user: userPrompt,
model: options.model,
timeoutMs: options.timeoutMs,
signal: options.signal,
});
raw = result.content || '';
} catch (e: any) {
+2 -1
View File
@@ -99,7 +99,7 @@ export async function runCeoReporter(
plan: CompanyTaskPlan,
outputs: AgentTurnOutput[],
state: CompanyState,
options: { model?: string; timeoutMs?: number } = {},
options: { model?: string; timeoutMs?: number; signal?: AbortSignal } = {},
): Promise<ReportResult> {
const system = applyPromptVars(CEO_REPORT_PROMPT, { company: state.companyName });
const user = _buildReportUserMessage(plan, outputs, state);
@@ -109,6 +109,7 @@ export async function runCeoReporter(
user,
model: options.model,
timeoutMs: options.timeoutMs,
signal: options.signal,
});
const text = (result.content || '').trim();
if (!text) {
+8 -2
View File
@@ -337,6 +337,7 @@ export async function runCompanyTurn(
contractBlock: deps.requirementContract
? formatContractForPrompt(deps.requirementContract)
: undefined,
signal: deps.signal,
});
plan = plannerResult.plan;
plannerRaw = plannerResult.raw;
@@ -445,7 +446,7 @@ export async function runCompanyTurn(
plan,
outputs,
state,
{ model: reportModel },
{ model: reportModel, signal: deps.signal },
);
writeReport(sessionDir, reportResult.report);
emit({ phase: 'report-done', report: reportResult.report, ok: reportResult.ok });
@@ -660,6 +661,7 @@ async function _dispatchOne(
system,
user: task,
model,
signal: deps.signal,
});
let rawResponse = (result.content || '').trim();
@@ -697,6 +699,7 @@ async function _dispatchOne(
try {
const retryRes = await deps.ai.chat({
system, user: retryTask, model,
signal: deps.signal,
});
const retried = (retryRes.content || '').trim();
if (retried) {
@@ -777,7 +780,7 @@ async function _dispatchOne(
try {
const { formatIssuesForRetry } = await import('../selfReflector/selfReflectorVerifier');
const retryTask = `${formatIssuesForRetry(verifierIssues)}\n\n[원래 지시]\n${task}`;
const retryRes = await deps.ai.chat({ system, user: retryTask, model });
const retryRes = await deps.ai.chat({ system, user: retryTask, model, signal: deps.signal });
const retried = (retryRes.content || '').trim();
if (retried) {
// 재작업 결과로 본문 갱신 + action-tag 다시 실행.
@@ -937,6 +940,7 @@ async function _resolveStageAgent(
const result = await deps.ai.chat({
system, user,
model: modelForAgent(state, 'ceo', deps.defaultModel),
signal: deps.signal,
});
const raw = (result.content || '').trim();
// 가벼운 파서 — 코드펜스 / 잡문 제거 후 첫 {…} 추출.
@@ -1085,6 +1089,7 @@ async function _runReviewCycle(args: {
system: inspectorSystem,
user: inspectorUser,
model: modelForAgent(state, inspector.agentId, deps.defaultModel),
signal: deps.signal,
});
inspectorText = (res.content || '').trim();
} catch (e: any) {
@@ -1108,6 +1113,7 @@ async function _runReviewCycle(args: {
system: ceoSystem,
user: ceoUser,
model: modelForAgent(state, 'ceo', deps.defaultModel),
signal: deps.signal,
});
ceoText = (res.content || '').trim();
} catch (e: any) {
+9
View File
@@ -19,6 +19,15 @@ import { IAIService } from '../../core/services';
import { logError, logInfo } from '../../utils';
import { RequirementContract } from './types';
/**
* Alignment 라운드 기본 상한. config 의 `company.intentAlignmentMaxRounds`
* 미지정 시 fallback 값. config 시 [1,5] 범위로 clamp.
*
* 의도: 사용자가 명시 설정 없이도 무한정 질문받지 않도록 *코드 레벨* 에서 보장.
* 라운드 한도 도달 시 smart 모드에선 자동 진행, strict 모드에선 확인 카드.
*/
export const ALIGNMENT_DEFAULT_MAX_ROUNDS = 3;
/**
* 분석 한 회차의 결과. contract는 항상 채워서 돌아오고, 추가 정보가 필요한
* 경우만 confidence가 medium/low이고 openQuestions가 비어 있지 않다. 호출자가