openai / openai/openai-agents-python
Realtime: agent_end names the post-handoff agent instead of the agent whose turn ended
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 29.6k
- Forks
- 4.8k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 123
Description
Please read this first
- Have you read the docs? Yes.
- Have you searched for related issues? Yes. No open or closed issue covers which agent
agent_endnames after an in-response handoff.
Describe the bug
When a Realtime response contains a handoff tool call, the handoff completes before the model's turn_ended event and moves _current_agent to the target agent. The turn_ended branch in RealtimeSession then emits RealtimeAgentEndEvent(agent=self._current_agent), so the caller sees:
agent_start(A) → handoff(A → B) → agent_end(B) → agent_start(B) → agent_end(B)
RealtimeAgentEndEvent.agent is documented as "The agent that ended", but A never ends and B ends without having started. Any consumer that pairs agent_start / agent_end per agent (turn markers in a UI, per-agent timing, per-agent usage attribution read on agent_end) attributes A's turn to B.
The session already knows which agent produced the turn: _active_output_response_agent is captured at turn_started and is what the output guardrails use for the same reason. It is cleared one line before the agent_end event is built.
Debug information
- Agents SDK version:
mainat 58a6d2c9 (also present in v0.22.2) - Python version: 3.12.13
- Operating system: macOS 15
- Model and model provider: any Realtime model; reproduced with
ScriptedRealtimeModelfromagents.realtime.testing - Does the issue reproduce with the latest Agents SDK release? Yes
- Does the issue occur consistently or intermittently? Consistently, whenever a handoff runs inside a response
Repro steps
Self-contained, no network.
import asyncio
from agents.realtime.agent import RealtimeAgent
from agents.realtime.events import RealtimeAgentEndEvent, RealtimeAgentStartEvent, RealtimeHandoffEvent
from agents.realtime.model_events import (
RealtimeModelToolCallEvent,
RealtimeModelTurnEndedEvent,
RealtimeModelTurnStartedEvent,
)
from agents.realtime.session import RealtimeSession
from agents.realtime.testing import ScriptedRealtimeModel
async def main():
agent_b = RealtimeAgent(name="b")
agent_a = RealtimeAgent(name="a", handoffs=[agent_b])
model = ScriptedRealtimeModel(strict=False)
session = RealtimeSession(model, agent_a, None, run_config={"async_tool_calls": False})
await session.enter()
await model.emit(RealtimeModelTurnStartedEvent(response_id="response_1"))
await model.emit(RealtimeModelToolCallEvent(name="transfer_to_b", call_id="call_1", arguments="{}"))
await model.emit(RealtimeModelTurnEndedEvent(response_id="response_1"))
while not session._event_queue.empty():
event = session._event_queue.get_nowait()
if isinstance(event, RealtimeAgentStartEvent):
print("agent_start", event.agent.name)
elif isinstance(event, RealtimeHandoffEvent):
print("handoff", event.from_agent.name, "->", event.to_agent.name)
elif isinstance(event, RealtimeAgentEndEvent):
print("agent_end", event.agent.name)
await session.close()
asyncio.run(main())
Output on main:
agent_start a
handoff a -> b
agent_end b
Expected behavior
agent_start a
handoff a -> b
agent_end a
The turn that ended was produced by A; the handoff only decides who speaks next. I have a one-line fix with a regression test ready and will open a PR referencing this issue.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the RealtimeSession turn_ended branch and inspect how _active_output_response_agent is captured and cleared around RealtimeAgentEndEvent. Use the self-contained ScriptedRealtimeModel reproduction and add the described regression test. Done means the ended event names agent A after a handoff from A to B, with the existing event sequence otherwise preserved.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- audio-video-rtc, backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100