openai / openai/openai-agents-python

Realtime: agent_end names the post-handoff agent instead of the agent whose turn ended

Open
#5,072 1 comment 0 reactions 0 assignees View on GitHub

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_end names 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: main at 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 ScriptedRealtimeModel from agents.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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.