마지막 업데이트 2026-08-04
agent-input-policy.ts가 듣기 의도 · AI 발화 · 끼어들기 허용 · AEC 차단 네 상태에서
effectiveInputEnabled를 단일 산출하고, LiveKit/Realtime/TTS 어댑터는 그 결과만 적용합니다.
livekit-input-gate.ts는 정책 재판정을 버리고 턴 lifecycle 전담으로 축소됐습니다.
conversationMode 분기와 "첫 AI 멘트" 예외는 의도적으로 만들지 않았습니다.
활동 전환 직후 예약된 마이크 ON이 AI 첫 응답 도중에 입력을 열어, VAD/STT가 AI 목소리를 아동 발화로 수집할 수 있었습니다.
use-step-transition.ts가 듣기 의도(configured 또는 진행자 부재 fallback)가 있으면 새 세션에
scheduleMicOnAfterFirstResponse()를 예약한다.applyDeferredListening() → applyListeningIntent()를 호출한다.정책은 상태 4개 + 파생값 1개, 이벤트 4종, 이펙트 1종으로 끝나는 순수 함수입니다. 타이머·트랙·RPC는 하나도 알지 못합니다.
// lib/voice-agent/agent-input-policy.ts interface AgentInputPolicyState { listeningIntent: boolean; // 진행자 '듣기' 의도 (configured + fallback 합산 결과) isAiSpeaking: boolean; // AI 발화 중 allowInterruptions: boolean; // 끼어들기 허용 aecBlocked: boolean; // AEC 안정화 대기 등으로 차단 effectiveInputEnabled: boolean; // ← 파생값. 어댑터가 적용하는 유일한 결론 } function resolveEffectiveInputEnabled(state) { return ( state.listeningIntent && !state.aecBlocked && (!state.isAiSpeaking || state.allowInterruptions) ); } // 이벤트 4종 → reduceAgentInputPolicy() listening_intent_changed | ai_speaking_changed | allow_interruptions_changed | aec_blocked_changed // 이펙트: 결론이 '바뀔 때만' 1종 발행 (같은 값이면 빈 배열 → 중복 RPC/게인 적용 없음) { type: "apply_effective_input", enabled: boolean }
state.effectiveInputEnabled === next ? [] : [...]).
덕분에 상위에서 같은 이벤트를 여러 번 보내도 트랙 토글·RPC가 중복되지 않습니다.
agent-input-policy.ts:51–61, 76–106
shouldHoldInputAfterAssistantAudio() — AI 오디오 종료 직후에도 끼어들기 OFF면 입력을 계속 닫아둘지.
첫 응답 대기 중인 의도(pendingFirstResponseListeningIntent)도 의도로 취급합니다. :25–39shouldDeferFirstResponseListeningRestore() — AEC 안정화가 남아 있고 위 hold가 참이면
첫 응답 복원을 미룹니다. :41–49| 듣기 의도 | AI 발화 | 끼어들기 | AEC | 실효 입력 | 비고 |
|---|---|---|---|---|---|
| OFF | — | — | — | false | 의도가 없으면 무조건 닫힘 |
| ON | idle | ON/OFF | 해제 | true | 정상 청취 |
| ON | 발화 중 | ON | 해제 | true | 끼어들기 허용 시 열림 |
| ON | 발화 중 | OFF | 해제 | false | 첫 AI 멘트도 이 행 |
| ON | — | — | 차단 | false | AEC 안정화 대기 우선 |
allowInterruptions) 축으로 표현해야 합니다. 첫 응답 예외를 되살리는 순간
§01의 결함이 그대로 재발합니다. 진리표는 agent-input-policy.test.ts가 고정합니다.
agentInputGain만 0으로 내려 AI 입력만 차단하고 진행자 릴레이는 유지합니다.
게인 적용 → RPC 순서로 보내며, 최신 상태만 남게 coalescing합니다.MediaStreamTrack.enabled를 토글하고, 끄는 방향일 때만
clearRealtimeInputBuffer로 이미 쌓인 입력 버퍼를 비웁니다(TTS도 Realtime 입력 연결을 재사용).livekit-input-gate.ts는 desired_input_changed·input_policy_changed를
정책에 위임하고, 자신은 speech_started/stopped · turn_pending/committed/dropped · disconnect만 처리합니다.
livekit-input-gate.ts:110–210superseded_by_new_turn으로 정리됩니다. 최근 종결 턴 20개를 기억해 stale 이벤트를 무시합니다.
활동이 바뀌면 세션이 새로 뜨므로, 의도 전달은 상위(page-session)가 하고 타이밍 판단은 정책이 합니다.
use-step-transition.ts는 듣기 의도가 있을 때 정본 ref의 configured 값을 인자로 명시 전달합니다
(scheduleMicOnAfterFirstResponse({ configuredIntent })). 사본 ref를 읽지 않는 것이 PPI-1167의 교훈입니다.restoreListening()으로 세션 gate를 먼저 열어 예약이 적용될 수 있게 합니다.DEFER_MIC_FALLBACK_MS)이 AEC 차단을 풀고 적용합니다.
명시적으로 마이크를 끈 상태면 fallback은 건너뜁니다.AEC_STABILIZATION_DELAY_MS) 뒤 aec_blocked_changed:false를 보내며,
이때도 열림 여부는 §03 진리표가 결정합니다.| 증상 | 확인 지점 | 해석 |
|---|---|---|
| 활동 전환 후 아동이 말해도 반응 없음 | Defer mic: will apply after first AI response or fallback 뒤에
Defer mic: applied…가 있는지 |
예약만 있고 적용 로그가 없으면 의도 전달(configuredIntent) 문제, 적용됐는데도 무반응이면 어댑터/미디어 경로 문제 |
| AI가 자기 발화에 반응해 스스로 끊김 | 첫 응답 구간의 실효 입력 값 · LiveKit setInputEnabled RPC 값 |
끼어들기 OFF인데 RPC가 true면 정책 우회 경로가 남아 있다는 뜻(§03 계약 위반) |
| 듣기 OFF인데 전사가 남는다 | gate의 turn 종결 사유(turn_committed/dropped) |
OFF 이전에 accepted된 턴은 정상적으로 보존·전사됩니다(설계된 동작) |
| 진행자 모니터에서 아동 소리가 안 들림 | 이 정책과 무관 — 릴레이 경로 확인 | agentInputGain은 AI 입력만 줄입니다. 릴레이 무음은 브리지 재발행 계열 이슈 |
listeningIntent(진행자가 원한 상태)와 effectiveInputEnabled(지금 실제로 여는지)를
로그·payload에서 혼동하면 "켰는데 안 켜졌다"는 오진이 납니다. gate의 desiredInputEnabled가 의도 쪽입니다.conversationMode나 runtime으로 정책을 갈라놓지 않습니다. 차이는 어댑터 계층에서만 존재해야 합니다.| 파일 | 역할 |
|---|---|
| lib/voice-agent/agent-input-policy.ts | 상태·이벤트·reducer·보조 판정 (정책 단일 소스) |
| lib/voice-agent/agent-input-policy.test.ts | 진리표 및 전이 회귀 고정 |
| lib/voice-agent/livekit-input-gate.ts | 턴 lifecycle · stale 보호 · disconnect 정리 (정책 위임) |
| entities/guest-session/model/use-ai-session.ts | 정책 이벤트 발행 · 어댑터 적용 · AEC/defer 타이머 |
| entities/guest-page-session/model/use-step-transition.ts | 활동 전환 시 configuredIntent 전달 · 예약 |
| apps/livekit-agent/input_gate.py · agent.py | 워커 측 gate — RPC 결과 적용 및 턴 감사 |
| docs/plans/livekit-input-policy.md | 설계 계획서(결정 사유·검증 매트릭스) |