diff --git a/src/agents/realtime/session.py b/src/agents/realtime/session.py index 89f63b02fa..7ddf19813c 100644 --- a/src/agents/realtime/session.py +++ b/src/agents/realtime/session.py @@ -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=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 c1c919a866..bdbac69657 100644 --- a/tests/realtime/test_session.py +++ b/tests/realtime/test_session.py @@ -565,16 +565,9 @@ 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 ignored events (connection_status, other) only generate raw events.""" 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) - # Test connection status (should be ignored) connection_event = RealtimeModelConnectionStatusEvent(status="connected") await session.on_event(connection_event) @@ -583,13 +576,33 @@ 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) + @pytest.mark.asyncio + async def test_transcript_delta_emits_history_updated(self, mock_model, mock_agent): + """transcript_delta updates the live history and must emit RealtimeHistoryUpdated + so subscribers following history_added/history_updated stay in sync (issue #2940).""" + session = RealtimeSession(mock_model, mock_agent, None) + + transcript_event = RealtimeModelTranscriptDeltaEvent( + item_id="item_1", delta="hello", response_id="resp_1" + ) + await session.on_event(transcript_event) + + # Expect a raw event plus a history_updated event. + assert session._event_queue.qsize() == 2 + + events = [await session._event_queue.get() for _ in range(2)] + assert any(isinstance(e, RealtimeRawModelEvent) for e in events) + history_updated = next((e for e in events if isinstance(e, RealtimeHistoryUpdated)), None) + assert history_updated is not None + assert history_updated.history == session._history + @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"""