limited-planet-15554
03/05/2026, 4:45 AMtall-airline-16756
03/05/2026, 4:45 AMtall-airline-16756
03/05/2026, 4:46 AM# 1. 말하기/플레이아웃 대기 완료 후
await session.wait_for_playout()
# 2. 세션 안전 종료(모든 shutdown callbacks 실행 보장)
await session.aclose() # 또는 await session.shutdown(drain=True)
# 3. 후처리(녹음 중지, MQ 전송)가 shutdown_callback에서 실행되도록 보장
# 4. 모든 정리 완료 후 방 삭제
await api_client.room.delete_room(room_id)
2) 또는 delete_room_on_close 옵션 사용
• 세션을 생성할 때 `delete_room_on_close=True`로 설정하면 session.aclose() 호출 시 세션 정리 후 자동으로 방이 삭제되도록 할 수 있습니다. 단, 이 역시 반드시 세션 종료가 먼저 호출되는 흐름이어야 합니다.
주의 / 고려사항
• shutdown_callback 내부에서 시간 소요가 큰 작업이 있다면(예: 외부 네트워크 호출), 타임아웃을 적절히 설정하거나 작업 완료를 보장할 수 있는 재시도/확인 메커니즘을 두세요.
• AgentServer의 프로세스 모델(예: job 프로세스가 즉시 종료되는 동작)을 고려해, session.aclose()가 완료될 때까지 프로세스가 종료되지 않도록 보장해야 합니다. 필요하면 세션 종료가 완료될 때까지 블로킹하거나, 별도의 supervisor/worker 형태로 후처리를 위임할 수 있습니다.
• delete_room 호출 경로가 여러 곳에 흩어져 있다면(말씀하신 여러 종료 경로) 모든 경로에서 동일한 안전 패턴을 적용해야 합니다.
다음 제안(원하시면 도와드릴 수 있는 부분)
• 현재 코드의 실제 종료 경로(end_call_by_system 등) 중 하나를 골라 제가 구체적으로 변경할 코드 스니펫을 제공해 드리겠습니다.
• shutdown_callback에서 수행하는 작업(녹음 중지, MQ 전송 등)의 실행 시간을 측정해 타임아웃/재시도 전략을 설계해 드릴 수 있습니다.
• delete_room 호출을 중앙화해서 실수로 잘못된 순서로 호출되는 것을 방지하는 리팩토링 제안도 도와드릴 수 있습니다.
원하시면, 특정 파일(예: base_agent.py의 해당 함수들)이나 한 경로를 골라 그 부분을 함께 수정한 코드 예시와 테스트 케이스(로컬에서 재현하는 방법 포함)를 제공하겠습니다. 어떤 경로부터 시작할까요?
Sources: Job lifecycle | LiveKit Documentation | Server-side | livekit/livekit_composite | DeepWiki | Agent session | LiveKit Documentation | Why is there extra audio at the end of my recordings of an agent session? |... | EndCallTool | LiveKit Documentationlimited-planet-15554
03/05/2026, 4:48 AMsession.shutdown(drain=True)
으로 통화가 바로 종료되지 않는데? 왜 그런거야?tall-airline-16756
03/05/2026, 4:48 AMsession.shutdown() 자체는 비동기적으로 종료 시퀀스를 시작하지만, 백그라운드에서 재생 종료를 기다립니다. 따라서 "즉시 끊음"을 기대하면 지연이 발생합니다.
즉시 종료가 필요하면
• 즉시 연결을 끊고 싶다면 `await session.aclose()`를 사용하세요.
◦ `aclose()`는 현재 재생 중인 음성도 취소하고 즉시 I/O 연결을 닫아 통화를 강제로 종료합니다.
◦ `await`로 호출하므로 종료가 완료될 때까지 호출 측에서 대기합니다.
간단 예시
• 우아한 종료(플레이아웃 대기):
session.shutdown(drain=True)
• 즉시 종료(강제 연결 종료):
await session.aclose()
추가 팁
• 만약 "플레이아웃 종료를 기다리되, 최대 제한 시간까지만 대기" 같은 동작이 필요하면:
◦ 현재 재생 핸들(SpeechHandle)의 `wait_for_playout()`을 직접 사용해 재생 완료를 기다리거나,
◦ 타임아웃 로직을 추가한 뒤 타임아웃 시 `aclose()`로 강제 종료하는 패턴을 적용할 수 있습니다.
• shutdown_callback(녹음 중지, MQ 전송 등)을 항상 보장하려면:
◦ 우선 session.aclose() 또는 `session.shutdown(drain=True)`로 세션 정리를 완료(또는 대기)한 뒤 `delete_room()`을 호출하세요.
원하시면
• 현재 사용 중인 종료 흐름(코드 스니펫)을 보여주시면, 즉시 종료 혹은 안전 종료를 만족하도록 구체적인 수정 예시를 드리겠습니다.
• 또는 "플레이아웃을 최대 N초만 기다리고 그 이후엔 강제로 종료" 같은 정책을 적용하고 싶으시면 그에 맞는 코드 패턴을 만들어 드릴게요.
원하시는 방향(즉시 종료 vs. 플레이아웃 우선)을 알려주시면 다음 단계로 구체적인 코드 변경안을 드리겠습니다.
Sources: Job lifecycle | LiveKit Documentation | How to detect when an agent has finished speaking | LiveKitlimited-planet-15554
03/05/2026, 4:50 AMdelete_room_on_close=True 설정하고 self.session.shutdown(drain=True) 로 하니깐 통화가 종료되는데, 이렇게 하면 위에서 언급한 문제는 없는거지?tall-airline-16756
03/05/2026, 4:50 AMdelete_room_on_close=True 적용 예시와 shutdown 콜백 타임아웃 패턴을 포함한 구체적 코드 수정안을 같이 드리겠습니다. Sources: Agent session | LiveKit Documentationlimited-planet-15554
03/05/2026, 4:51 AMtall-airline-16756
03/05/2026, 4:51 AMdelete_room_on_close=True + session.shutdown(drain=True) 조합은 `delete_room`이 `shutdown_callback`보다 먼저 실행되는 문제를 제거합니다.
간단한 이유:
• `session.shutdown(drain=True)`는 플레이아웃을 끝까지 재생하고 세션의 shutdown 흐름(등록된 shutdown callbacks 포함)을 실행하도록 보장합니다.
• `delete_room_on_close=True`는 세션이 완전히 닫힌 뒤(모든 shutdown callback이 완료된 뒤)에 방 삭제를 자동으로 수행합니다.
• 따라서 수동으로 중간에 `delete_room()`을 호출해 연결을 즉시 끊어버리는 상황이 없어지고, 녹음 중지·MQ 전송 같은 후처리가 누락될 위험이 사라집니다.
운영상 유의사항(간단히)
• shutdown callbacks 내부에 오래 걸리는 외부 작업이 있으면 타임아웃/재시도 전략을 두세요.
• 코드 베이스에 아직 수동 delete_room() 호출(다른 경로)이 남아있지 않은지 확인해 중복 삭제/레ース를 방지하세요.
원하시면 현재 종료 흐름(코드 스니펫)을 보여주시면, 안전성 점검해 드리고 shutdown callback 타임아웃 패턴이나 중앙화된 종료 처리 예시도 함께 제안해 드리겠습니다.limited-planet-15554
03/05/2026, 8:54 AMtall-airline-16756
03/05/2026, 8:55 AM@ctx.room.on("disconnected")
async def on_disconnect():
# 즉시 녹음 중지
await stop_recording_safely()
# MQ 메시지 동기 전송(또는 await 가능한 전송)
await send_mq_message(payload)
# 세션 강제 종료 대기
await session.aclose()
• 이유: 이벤트 루프가 바쁠 때도 이 블록이 실행되면 후처리가 보장됩니다.
2) session.aclose() 사용
• `session.shutdown(drain=True)`는 플레이아웃을 기다리므로 즉시 종료를 보장하지 않습니다.
• `await session.aclose()`는 연결을 닫고 I/O 정리를 기다리므로, 그 이후에 후처리를 하도록 하면 누락을 줄일 수 있습니다.
3) 장기 실행 작업(도구 호출, TTS 등)은 disconnect 상태를 체크하고 중단/무시하도록 설계
• 도구 호출 시작 시 세션 상태를 확인하고, disconnect이면 작업을 취소하거나 반환.
• LLM/TTS 처리 전에 if session.closed: abort 체크.
• 또는 작업을 cancellable task로 만들고 disconnect 이벤트에서 cancel() 호출.
4) shutdown_callback은 보조(비핵심) 동작으로만 사용
• 로그 전송, 비핵심 메트릭, best-effort 업로드 등에만 사용하세요.
• 핵심적인 자원 정리/알림은 위 1번 방식으로 보장.
5) 타임아웃/재시도 정책 적용
• 외부 네트워크 호출은 적절한 타임아웃을 두고 실패 시 재시도/로그를 남겨 상태 불일치 위험을 줄이세요.
• 예: MQ 전송 실패 시 로컬 큐에 쌓아서 백그라운드에서 재전송.
디버깅/증거 수집 팁
• disconnect 이벤트 발생 시점, 각 task의 상태(시작/완료/취소) 로그를 상세히 남기세요.
• uploading session report, finished uploading 같은 로그가 없으면 종료가 비정상적으로 중단된 것으로 판단.
• 재현을 위해 호출 지연(의도적 sleep)나 도구 호출 인위적 지연을 넣어 race window를 늘려 테스트하세요.
권장 코드 패턴(간단 요약)
• Disconnect 핸들러에서 핵심 정리 동작을 await로 수행 → 그 후 session.aclose() → (필요 시) delete_room 호출 또는 delete_room_on_close 사용.
• 도구 호출마다 session 상태 체크 또는 cancellable tasks 적용.
다음 단계 제안(제가 도와드릴 수 있는 항목)
• 현재 코드(base_agent.py의 관련 함수: end_call_by_system, on_disconnect 핸들러, shutdown_callback 등록부 등)를 보여주시면, 구체적으로 어디를 바꿀지 코드 패치 형태로 제안드리겠습니다.
• 또는 한 가지 패턴(즉시 동기 정리 + session.aclose() + 도구 취소)을 기준으로 리팩토링 가이드를 만들어 드릴게요.
• 재현 가능한 테스트 시나리오(로컬 혹은 스테이징) 설계도 도와드릴 수 있습니다.
원하시면 지금 쓰시는 disconnect 핸들러 또는 BookingAgent의 해당 코드 스니펫을 붙여주세요. 거기서 바로 안전한 즉시 정리 흐름으로 바꿔드리겠습니다.
Sources: How to detect and handle silence during calls | LiveKit | Job lifecycle | LiveKit Documentation | Agent session | LiveKit Documentation | Why is my Agent insights data missing? | LiveKitlimited-planet-15554
03/05/2026, 9:01 AMtall-airline-16756
03/05/2026, 9:02 AMif ongoing_task and not ongoing_task.done():
ongoing_task.cancel()
2) disconnect 핸들러에서 발화 큐 정리 및 강제 종료
• 플레이아웃 큐에 남아있는 항목을 즉시 취소하고, 재생 중인 TTS가 있다면 중지 API 호출.
• 이후 `await session.aclose()`로 연결을 즉시 닫아 추가 출력이 발생하지 않게 함.
3) 세션 상태 체크를 도구/응답 생성 흐름에 삽입
• 각 단계에서 if session.closed or session.disconnecting: abort 체크를 넣어 불필요한 작업 생성 자체를 막음.
4) session.shutdown(drain=True) 대신 상황별 aclose() 사용
• drain=True는 플레이아웃을 끝까지 재생하므로 의도적으로 발화를 종료시키고 싶으면 `await session.aclose()`를 호출해 즉시 연결을 닫으세요.
5) 플레이아웃 핸들 제어
• `SpeechHandle`같은 재생 핸들을 사용하면 handle.cancel() 또는 재생 중단 API로 현재 TTS를 중지할 수 있음. (해당 API가 있다면 바로 호출)
간단한 흐름 예시
• on_participant_disconnect:
a. set session.state = "disconnecting"
b. cancel ongoing tasks (LLM/tool/TTS)
c. stop current playback (playout cancel)
d. await session.aclose()
e. perform synchronous cleanup (stop_recording, send MQ)
운영상 고려사항
• 취소 처리 시 LLM/tool 호출은 적절히 취소/타임아웃 되도록 설계하세요(외부 API는 즉시 취소가 안될 수 있음).
• shutdown_callback은 보조 수단으로만 사용하고, 핵심 후처리는 위 disconnect 경로에서 보장하세요.
• 테스트: 의도적으로 disconnect 시점을 도구 호출 중이나 TTS 재생 중으로 맞춰 race를 재현해 취소 로직이 잘 동작하는지 확인하세요.
원하시면 제가 도와드릴 수 있는 것
• 현재 에이전트의 disconnect 핸들러나 TTS/도구 호출 코드 스니펫을 주시면, 취소 로직과 session.aclose() 적용을 포함한 구체적 코드 패치를 제안해 드리겠습니다.
• 또는 발화 큐/플레이아웃을 중단하는 구체 API가 무엇인지 알려주시면 그에 맞춘 예시를 만들어 드릴게요.
원하시는 방식(즉시 강제 중단 vs 플레이아웃 허용)을 알려주시면 바로 구체 코드로 도와드리겠습니다.limited-planet-15554
03/05/2026, 9:03 AMtall-airline-16756
03/05/2026, 9:04 AMimport asyncio
import logging
logger = logging.getLogger("agent")
# 전역 또는 세션 스코프에서 관리되는 set
active_tasks = set()
def create_cancellable_task(coro):
task = asyncio.create_task(coro)
active_tasks.add(task)
# 태스크 정리
def _on_done(t):
active_tasks.discard(t)
task.add_done_callback(_on_done)
return task
2) 장기 작업(도구 호출, LLM, TTS) 예시 — CancelledError 처리 포함
async def long_running_tool_call(...):
try:
# 예: 외부 HTTP 호출을 asyncio-friendly하게 수행
# 여기선 단순 sleep로 시뮬레이트
for i in range(30):
await asyncio.sleep(0.5)
# 중간에 주기적으로 취소 확인(선택적)
# if session.closed: raise asyncio.CancelledError()
return "done"
except asyncio.CancelledError:
# 필요하면 여기에서 자원 정리(파일/스트림 닫기 등)
<http://logger.info|logger.info>("Tool call cancelled, cleaning up...")
# 반드시 예외 재전파하여 태스크가 취소됨을 보장
raise
3) TTS/플레이아웃 취소(플레이어 핸들 제어 가능하면 사용)
• TTS를 생성 후 플레이아웃 핸들을 얻을 수 있다면, 해당 핸들에 cancel/stop API가 있다면 호출하세요.
• 예:
# 예시: play_handle = await session.say("...") 반환값이 핸들일 경우
play_handle.cancel() # 또는 await play_handle.stop()
4) participant disconnected 이벤트에서의 취소 및 정리
@ctx.room.on("disconnected")
async def on_disconnected():
<http://logger.info|logger.info>("Participant disconnected — cancelling active tasks")
# 1) 취소 시도
for t in list(active_tasks):
if not t.done():
t.cancel()
# 2) 모든 태스크가 정리될 때까지 짧은 대기 (선택적)
try:
await asyncio.wait_for(asyncio.gather(*active_tasks, return_exceptions=True), timeout=5.0)
except asyncio.TimeoutError:
logger.warning("Some tasks did not finish within timeout after cancel")
# 3) 즉시 세션 종료(강제)
try:
await session.aclose()
except Exception as e:
logger.exception("Error during session.aclose(): %s", e)
# 4) 핵심 후처리(동기/await)
try:
await stop_recording_safely()
await send_mq_message(...)
except Exception as e:
logger.exception("Post-disconnect cleanup failed: %s", e)
추가 권장 패턴
• 도구 호출 진입 지점에서 session 상태 확인:
◦ if session.closed: return 또는 취소로 반환.
• 장기 작업에 대해 반드시 타임아웃 적용:
◦ asyncio.wait_for(tool_call(), timeout=10)
• 외부 라이브러리가 취소 토큰을 지원하면 그 토큰을 전달하여 조기 종료를 요청.
• 테스트: 인위적인 딜레이를 걸어 disconnect와 작업 취소가 제대로 동작하는지 반복적으로 확인하세요.
마무리(요약)
• participant disconnected 이벤트에서 task.cancel() + awaited cleanup(세션 강제 종료 + 핵심 후처리)을 적용하면, 에이전트가 사용자가 떠난 뒤에도 계속 발화하거나 shutdown_callback이 누락되는 문제를 크게 줄일 수 있습니다.
• 코드 스니펫을 실제 base_agent.py 또는 BookingAgent의 관련 메서드에 맞춰서 적용해 드릴 수 있습니다. 원하시면 현재 사용 중인 코드 일부(도구 호출 부분, TTS 호출 부분, disconnect 핸들러)를 붙여 주세요. 그걸 바탕으로 구체적 패치 제안을 드리겠습니다. Sources: Job lifecycle | LiveKit Documentation | Turns overview | LiveKit Documentation