Skip to content

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

Description

@kosesena

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 58a6d2c (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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions