From ee13664b8981c1ab5756fe504f0690a547687804 Mon Sep 17 00:00:00 2001 From: jordanchendev Date: Wed, 3 Jun 2026 23:05:37 +0800 Subject: [PATCH] fix: emit RealtimeHistoryUpdated on transcript_delta RealtimeSession updated _history with the accumulated transcript on each transcript_delta event but never emitted RealtimeHistoryUpdated. Every other branch that mutates history (item_updated, item_deleted, transcription_completed) does emit the event, so UI subscribers following the documented history_added / history_updated pattern saw no live transcript changes. Emit RealtimeHistoryUpdated immediately after the history update in the transcript_delta branch, consistent with all other history-mutating paths. Fixes #2940 --- src/agents/realtime/session.py | 3 +++ tests/realtime/test_session.py | 42 +++++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/agents/realtime/session.py b/src/agents/realtime/session.py index ca809dd9c4..931c7449d7 100644 --- a/src/agents/realtime/session.py +++ b/src/agents/realtime/session.py @@ -354,6 +354,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=self._history) + ) # Check if we should run guardrails based on debounce threshold current_length = len(self._item_transcripts[item_id]) diff --git a/tests/realtime/test_session.py b/tests/realtime/test_session.py index 03148c739a..1e6dc7d018 100644 --- a/tests/realtime/test_session.py +++ b/tests/realtime/test_session.py @@ -591,31 +591,57 @@ async def test_item_deleted_event_removes_item(self, mock_model, mock_agent): @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""" + """Test that connection_status and other unknown events only generate raw events, + while transcript_delta also emits RealtimeHistoryUpdated.""" session = RealtimeSession(mock_model, mock_agent, None) - # Test transcript delta (should be ignored per TODO comment) + # transcript_delta emits raw event + RealtimeHistoryUpdated (2 events total). transcript_event = RealtimeModelTranscriptDeltaEvent( item_id="item_1", delta="hello", response_id="resp_1" ) await session.on_event(transcript_event) - # Test connection status (should be ignored) + # Test connection status (should only produce a raw event). connection_event = RealtimeModelConnectionStatusEvent(status="connected") await session.on_event(connection_event) - # Test other event (should be ignored) + # Test other event (should only produce a raw event). 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 + # 4 events total: raw + history_updated for transcript_delta, raw for each of the others. + assert session._event_queue.qsize() == 4 + + raw_event = await session._event_queue.get() + assert isinstance(raw_event, RealtimeRawModelEvent) + + history_event = await session._event_queue.get() + assert isinstance(history_event, RealtimeHistoryUpdated) - for _ in range(3): + for _ in range(2): event = await session._event_queue.get() assert isinstance(event, RealtimeRawModelEvent) + @pytest.mark.asyncio + async def test_transcript_delta_emits_history_updated(self, mock_model, mock_agent): + """Regression test for #2940: transcript_delta must emit RealtimeHistoryUpdated.""" + session = RealtimeSession(mock_model, mock_agent, None) + + await session.on_event( + RealtimeModelTranscriptDeltaEvent(item_id="item_1", delta="hello", response_id="resp_1") + ) + + # Expect 2 events: raw model event + 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) + assert len(history_event.history) == 1 + assert history_event.history[0].item_id == "item_1" + @pytest.mark.asyncio async def test_function_call_event_triggers_tool_handling(self, mock_model, mock_agent): """Test that function_call events trigger tool call handling synchronously when disabled"""