Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/agents/realtime/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ async def on_event(self, event: RealtimeModelEvent) -> None:
content=[AssistantAudio(transcript=self._item_transcripts[item_id])],
),
)
await self._put_event(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 318: history=self._history passes a direct reference to the session's internal _history list rather than a copy. If any subscriber mutates this list, it silently corrupts RealtimeSession._history without the session knowing.

Consider: history=list(self._history) to prevent external mutation from affecting internal state.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7f3eff8. RealtimeHistoryUpdated now receives list(self._history), so consumers can no longer mutate the session's internal history list through the event payload.

I also made the snapshot behavior consistent across all full-history RealtimeHistoryUpdated emissions and added a regression test for the transcript_delta path.

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])
Expand Down
34 changes: 27 additions & 7 deletions tests/realtime/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,17 +564,37 @@ 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_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)
Expand All @@ -583,10 +603,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)

Expand Down