豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
11 changes: 7 additions & 4 deletions src/agents/realtime/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
)
Expand Down Expand Up @@ -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(
Expand All @@ -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=list(self._history))
)

# Check if we should run guardrails based on debounce threshold
current_length = len(self._item_transcripts[item_id])
Expand Down Expand Up @@ -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
Expand Down
53 changes: 46 additions & 7 deletions tests/realtime/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand Down