Skip to content

Commit 058233c

Browse files
committed
fix(#2009): emit truncation on VAD interrupt
1 parent 553c1bf commit 058233c

File tree

2 files changed

+30
-4
lines changed

2 files changed

+30
-4
lines changed

src/agents/realtime/openai_realtime.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -621,10 +621,27 @@ async def _handle_ws_event(self, event: dict[str, Any]):
621621
last_audio = self._audio_state_tracker.get_last_audio_item()
622622
if last_audio is not None:
623623
item_id, content_index = last_audio
624+
playback_state = self._get_playback_state()
625+
playback_item_id = playback_state.get("current_item_id")
626+
playback_content_index = playback_state.get("current_item_content_index") or 0
627+
playback_elapsed_ms = playback_state.get("elapsed_ms")
624628
await self._emit_event(
625629
RealtimeModelAudioInterruptedEvent(item_id=item_id, content_index=content_index)
626630
)
627631

632+
if (
633+
playback_item_id
634+
and playback_elapsed_ms is not None
635+
):
636+
truncated_ms = max(int(playback_elapsed_ms), 0)
637+
await self._send_raw_message(
638+
_ConversionHelper.convert_interrupt(
639+
playback_item_id,
640+
playback_content_index,
641+
truncated_ms,
642+
)
643+
)
644+
628645
# Reset trackers so subsequent playback state queries don't
629646
# reference audio that has been interrupted client‑side.
630647
self._audio_state_tracker.on_interrupted()
@@ -643,9 +660,6 @@ async def _handle_ws_event(self, event: dict[str, Any]):
643660
)
644661
if not automatic_response_cancellation_enabled:
645662
await self._cancel_response()
646-
# Avoid sending conversation.item.truncate here. When the session's
647-
# turn_detection.interrupt_response is enabled (GA default), the server emits
648-
# conversation.item.truncated after the VAD start and takes care of history updates.
649663
elif parsed.type == "response.created":
650664
self._ongoing_response = True
651665
await self._emit_event(RealtimeModelTurnStartedEvent())

tests/realtime/test_openai_realtime.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pytest
77
import websockets
8+
from openai.types.realtime.conversation_item_truncate_event import ConversationItemTruncateEvent
89

910
from agents import Agent
1011
from agents.exceptions import UserError
@@ -443,7 +444,7 @@ async def test_transcription_related_and_timeouts_and_speech_started(self, model
443444

444445
# Prepare tracker state to simulate ongoing audio
445446
model._audio_state_tracker.set_audio_format("pcm16")
446-
model._audio_state_tracker.on_audio_delta("i1", 0, b"aaaa")
447+
model._audio_state_tracker.on_audio_delta("i1", 0, b"a" * 48)
447448
model._ongoing_response = True
448449

449450
# Patch sending to avoid websocket dependency
@@ -464,6 +465,17 @@ async def test_transcription_related_and_timeouts_and_speech_started(self, model
464465
}
465466
)
466467

468+
truncate_events = [
469+
call.args[0]
470+
for call in model._send_raw_message.await_args_list
471+
if isinstance(call.args[0], ConversationItemTruncateEvent)
472+
]
473+
assert truncate_events
474+
truncate_event = truncate_events[0]
475+
assert truncate_event.item_id == "i1"
476+
assert truncate_event.content_index == 0
477+
assert truncate_event.audio_end_ms == 1 or truncate_event.audio_end_ms == 0
478+
467479
# Output transcript delta
468480
await model._handle_ws_event(
469481
{

0 commit comments

Comments
 (0)