import type { RequirementContract } from '../../features/company'; /** * 1인 기업 모드 turn 의 runtime 상태 — *진행 중* turn 의 abort handle 과 * *방금 끝난* turn 의 요약 스냅샷을 함께 보관. * * 옛 코드는 sidebarProvider 의 두 slot(`_companyAbort`, `_lastCompanyTurnSummary`) * 으로 흩어져 있어, "turn 끝" 시 abort 정리와 summary 갱신이 서로 다른 분기에서 * 일어났음. Manager 로 묶어 turn lifecycle 의 진입/종료 책임을 한 곳에 모았다. * * Summary 슬롯은 *완료된* turn 만 채운다 (abort/error 는 비움 유지) — intent * classifier 가 "이전 turn 후속" vs "신규 task" 를 판단할 때 abort 된 turn 을 * 후속의 기준으로 삼으면 안 되기 때문. */ export class CompanyTurnRuntime { private abortController?: AbortController; private lastSummary?: { brief: string; reportTail: string; finishedAt: number; }; /** Turn 시작 — fresh AbortController 등록. dispatcher signal 로 전달됨. */ startTurn(): AbortController { const ctrl = new AbortController(); this.abortController = ctrl; return ctrl; } /** * Turn 종료(완료/실패/abort 무관) — finally 절에서 호출. 우리가 발급한 그 * controller 일 때만 비워 race 방지 (느린 cleanup 이 새 turn 의 controller * 를 잘못 지우는 경우 차단). */ endTurn(ctrl: AbortController): void { if (this.abortController === ctrl) this.abortController = undefined; } /** * Stop 버튼 — abort 신호 emit. 호출 시점에 진행 중 turn 없으면 false. * 호출자(provider) 는 추가로 ApprovalGateManager.abortAll() 도 호출해야 * dispatcher 의 stage gate await 가 함께 깨어남. */ abort(): boolean { if (!this.abortController) return false; this.abortController.abort(); return true; } /** report-done 시점에 호출. abort / error 케이스는 호출 안 됨 (intent 의도). */ recordSummary(brief: string, reportTail: string): void { this.lastSummary = { brief, reportTail, finishedAt: Date.now() }; } /** Intent classifier 가 follow-up 판단할 때 읽음. cold start 면 undefined. */ getLastSummary() { return this.lastSummary; } /** 새 chat / 다른 세션 load 시 — 옛 report 가 의도 분류 오염하지 않게. */ clearLastSummary(): void { this.lastSummary = undefined; } } /** * Intent Alignment 의 *현재 미해결* 상태. 분석기가 round 를 끝낸 후 사용자 * 확인이 필요한 경우 이 슬롯에 contract 를 보관 → 다음 사용자 메시지를 답변으로 * 해석. 한 번에 한 alignment 만 진행 (회사 모드는 sequential). * * 옛 코드는 sidebarProvider 의 단일 slot 으로 4개 메서드에 흩어져 있었음. * Manager 로 모아 set/get/clear 책임 단일화 + 외부에서는 isPending() / consume() * 으로 안전하게 사용. */ export interface PendingAlignment { userOriginalPrompt: string; contract: RequirementContract; /** 이번 alignment 동안 사용자가 답한 누적 라운드 수 — 무한 라운드 방지. */ roundsAsked: number; /** 이번 turn 한정 pipeline override (Phase 4 분류기에서 전달된 추천). */ pipelineIdOverride?: string; } export class AlignmentSession { private pending?: PendingAlignment; isPending(): boolean { return !!this.pending; } /** 카드 push 시 호출 — 분석기 결과 보관. */ set(state: PendingAlignment): void { this.pending = state; } /** 현재 보류 상태 조회 (consume 안 함). */ get(): PendingAlignment | undefined { return this.pending; } /** * 한 번에 *읽고 비움* — 사용자가 진행 / 답변 시 호출. consume 패턴으로 race * (사용자가 진행 누르고 곧바로 답변 메시지 보내는 경우) 의 두 번째 호출이 * 자동으로 noop 이 되어 안전. */ consume(): PendingAlignment | undefined { const cur = this.pending; this.pending = undefined; return cur; } /** 취소 — 카드 / streamEnd push 는 호출자(provider) 책임. */ clear(): void { this.pending = undefined; } }