diff --git a/src/agents/realtime/session.py b/src/agents/realtime/session.py index 89f63b02fa..b8ea9f1f04 100644 --- a/src/agents/realtime/session.py +++ b/src/agents/realtime/session.py @@ -186,7 +186,7 @@ async def __aenter__(self) -> RealtimeSession: # Emit initial history update await self._put_event( RealtimeHistoryUpdated( - history=self._history, + history=list(self._history), info=self._event_info, ) ) @@ -290,7 +290,7 @@ async def on_event(self, event: RealtimeModelEvent) -> None: await self._put_event(RealtimeHistoryAdded(info=self._event_info, item=new_item)) else: await self._put_event( - RealtimeHistoryUpdated(info=self._event_info, history=self._history) + RealtimeHistoryUpdated(info=self._event_info, history=list(self._history)) ) elif event.type == "input_audio_timeout_triggered": await self._put_event( @@ -313,6 +313,9 @@ async def on_event(self, event: RealtimeModelEvent) -> None: content=[AssistantAudio(transcript=self._item_transcripts[item_id])], ), ) + await self._put_event( + RealtimeHistoryUpdated(info=self._event_info, history=list(self._history)) + ) # Check if we should run guardrails based on debounce threshold current_length = len(self._item_transcripts[item_id]) @@ -384,13 +387,13 @@ async def on_event(self, event: RealtimeModelEvent) -> None: await self._put_event(RealtimeHistoryAdded(info=self._event_info, item=new_item)) else: await self._put_event( - RealtimeHistoryUpdated(info=self._event_info, history=self._history) + RealtimeHistoryUpdated(info=self._event_info, history=list(self._history)) ) elif event.type == "item_deleted": deleted_id = event.item_id self._history = [item for item in self._history if item.item_id != deleted_id] await self._put_event( - RealtimeHistoryUpdated(info=self._event_info, history=self._history) + RealtimeHistoryUpdated(info=self._event_info, history=list(self._history)) ) elif event.type == "connection_status": pass diff --git a/tests/realtime/test_session.py b/tests/realtime/test_session.py index c1c919a866..3d82841080 100644 --- a/tests/realtime/test_session.py +++ b/tests/realtime/test_session.py @@ -564,17 +564,56 @@ async def test_item_deleted_event_removes_item(self, mock_model, mock_agent): assert len(history_event.history) == 1 @pytest.mark.asyncio - async def test_ignored_events_only_generate_raw_events(self, mock_model, mock_agent): - """Test that ignored events (transcript_delta, connection_status, other) only generate raw - events""" + async def test_transcript_delta_updates_history_and_emits_history_updated( + self, mock_model, mock_agent + ): + """Test that transcript deltas keep high-level history subscribers in sync.""" session = RealtimeSession(mock_model, mock_agent, None) - # Test transcript delta (should be ignored per TODO comment) transcript_event = RealtimeModelTranscriptDeltaEvent( item_id="item_1", delta="hello", response_id="resp_1" ) await session.on_event(transcript_event) + assert len(session._history) == 1 + updated_item = cast(AssistantMessageItem, session._history[0]) + assert updated_item.item_id == "item_1" + assert cast(AssistantAudio, updated_item.content[0]).transcript == "hello" + + # Should have raw + high-level history_updated + assert session._event_queue.qsize() == 2 + + raw_event = await session._event_queue.get() + assert isinstance(raw_event, RealtimeRawModelEvent) + history_event = await session._event_queue.get() + assert isinstance(history_event, RealtimeHistoryUpdated) + updated_history_item = cast(AssistantMessageItem, history_event.history[0]) + assert cast(AssistantAudio, updated_history_item.content[0]).transcript == "hello" + + @pytest.mark.asyncio + async def test_transcript_delta_history_updated_uses_list_snapshot( + self, mock_model, mock_agent + ): + session = RealtimeSession(mock_model, mock_agent, None) + + await session.on_event( + RealtimeModelTranscriptDeltaEvent(item_id="item_1", delta="hello", response_id="resp_1") + ) + + await session._event_queue.get() # raw event + history_event = await session._event_queue.get() + assert isinstance(history_event, RealtimeHistoryUpdated) + + history_event.history.clear() + + assert len(session._history) == 1 + assert session._history[0].item_id == "item_1" + + @pytest.mark.asyncio + async def test_ignored_events_only_generate_raw_events(self, mock_model, mock_agent): + """Test that ignored events (connection_status, other) only generate raw events""" + session = RealtimeSession(mock_model, mock_agent, None) + # Test connection status (should be ignored) connection_event = RealtimeModelConnectionStatusEvent(status="connected") await session.on_event(connection_event) @@ -583,10 +622,10 @@ async def test_ignored_events_only_generate_raw_events(self, mock_model, mock_ag other_event = RealtimeModelOtherEvent(data={"custom": "data"}) await session.on_event(other_event) - # Should only have 3 raw events (no transformed events) - assert session._event_queue.qsize() == 3 + # Should only have 2 raw events (no transformed events) + assert session._event_queue.qsize() == 2 - for _ in range(3): + for _ in range(2): event = await session._event_queue.get() assert isinstance(event, RealtimeRawModelEvent)