Does eval support testing transition between agent...
# ask-ai
p
Does eval support testing transition between agent a and agent b?
✅ 1
t
Thanks for reaching out, Mario. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — you're asking whether eval supports testing a transition (handoff) from agent A to agent B. Yes—eval supports testing agent handoffs. • Use the test framework assertions
is_agent_handoff()
or
contains_agent_handoff()
to verify a handoff event. • Example (Python):
Copy code
result.expect.next_event().is_agent_handoff(new_agent_type=MyAgent)
This checks the next event is a handoff to the specified agent type and validates the transition and context preservation. If you want, tell me which agents or a snippet of your test and I can suggest the exact assertion to use. Sources: Test framework | LiveKit Documentation | Agents and handoffs | LiveKit Documentation, with interactive elements
p
This is my flow: • result = await agent_session.start(agent=starting_agent, capture_run=True) • result.expect.next_event().is_agent_handoff(new_agent_type=starting_agent.class) • result.expect.next_event().is_message(role="assistant") • result.expect.next_event().is_agent_handoff(new_agent_type=my_next_agent.class) starting_agent looks like: class StartingAgent(Agent): async def on_enter(self) -> None: await self.session.say( text="Remain silent" ) # it needs to generate/say something, otherwise the the session will hang await hydrate_chat_ctx() self.session.update_agent(agent=my_next_agent) I'm getting: E AssertionError: Expected another event, but none left. E Context around failure: E [0] AgentHandoffEvent(old_agent=None, new_agent=<test_escalate.starting_agent.<locals>.StartingAgent object at 0x119e487c0>) E [1] ChatMessageEvent(item={'role': 'assistant', 'content': ['Remain silent'], 'metrics': {'started_speaking_at': 1778257301.322795, 'stopped_speaking_at': 1778257301.3228521}})
t
Thanks — this is helpful. You're asking why the test stops after the initial handoff and message (no further events). A few likely causes and quick checks: 1. Timing/order: Your StartingAgent calls
self.session.update_agent(agent=my_next_agent)
after
hydrate_chat_ctx()
— ensure that update_agent is actually awaited or executed before the session ends. If update happens after the session finishes producing events, no handoff event will be emitted. 2. Event consumption: The recorded events show: ◦ AgentHandoff (old_agent=None -> StartingAgent) ◦ ChatMessage (assistant: "Remain silent") That means the handoff to
my_next_agent
never happened (or happened but after capture ended). Try using
contains_agent_handoff()
which searches remaining events instead of strict next-event ordering: • Replace the failing assertion with
result.expect.contains_agent_handoff(new_agent_type=my_next_agent.__class__)
. 1. Confirm update call and side effects: ◦ Verify
self.session.update_agent(agent=my_next_agent)
runs (add a log or a short sleep after it to ensure event loop scheduling). ◦ Ensure
hydrate_chat_ctx()
does not block or swallow the update call. 1. If your expected sequence must be strict (handoff → message → handoff), ensure the first
is_agent_handoff(new_agent_type=starting_agent.__class__)
is actually correct — the first handoff shows old_agent=None → StartingAgent, so that assertion is fine. For the final handoff, confirm
my_next_agent.__class__
matches the agent instance passed. If you want, share: • the full StartingAgent.on_enter code (exact indentation/awaits), • whether
update_agent
is async or sync, and I'll suggest the precise assertion or minimal code change. , with interactive elements
p
Do I need to add any kind of sleep before asserting on the result, so that it gives the agent time to transiton?
t
Good question — you should not rely on arbitrary sleeps. The test framework records events as they happen; if the handoff isn't in the captured events it means the handoff did not occur before capture ended, not that you need a fixed sleep. What to do instead: • Ensure
update_agent(...)
is actually executed (and awaited if it’s async) inside
on_enter
. If
update_agent
is async, call
await self.session.update_agent(...)
. • Keep capture running until the handoff occurs. Use
contains_agent_handoff(...)
instead of strict
next_event()
when ordering/timing may vary: ◦
result.expect.contains_agent_handoff(new_agent_type=my_next_agent.__class__)
• If you need strict ordering, wait for the specific event with the test framework’s event assertions rather than sleeping. For example, poll
result.events
or use
result.expect.next_event()
repeatedly until you see the handoff (the framework’s assertions already provide that pattern). • Add lightweight diagnostics (logging or a short await after
update_agent
) only to confirm the call executes during tests — not long sleeps. If you share whether
update_agent
is async or paste the exact
on_enter
code, I’ll suggest the minimal change to ensure the handoff event is emitted. , with interactive elements
p
For some reason update_agent is returning and not transfering to the next agent. Can you double check if there is a example test case in github I can use to test this?
t
Thanks — you're asking for example tests you can use to debug why
update_agent
returns but doesn't trigger a handoff. Quick answers and resources: • There isn’t a single GitHub test that directly asserts
update_agent
, but the test framework supports handoff assertions (
is_agent_handoff()
/
contains_agent_handoff()
) you can use. • Example repos with agent-switching logic you can copy patterns from: ◦ Medical Office Triage: https://github.com/livekit-examples/python-agents-examples/tree/main/complex-agents/medical_office_triage ◦ Front-desk example: https://github.com/livekit/agents/blob/main/examples/frontdesk ◦ Drive-thru example: https://github.com/livekit/agents/blob/main/examples/drive-thru Suggested next steps to test locally (no arbitrary sleep): 1. Replace strict ordering with a contains check: ◦
result.expect.contains_agent_handoff(new_agent_type=my_next_agent.__class__)
1. Verify
update_agent
is awaited if it’s async: ◦ If
update_agent
is async:
await self.session.update_agent(agent=my_next_agent)
1. Add a short diagnostic await or log right after update to confirm it runs (not a long sleep). 2. If you want a small test snippet to assert handoff order, I can craft one matching your agents. If you’d like, paste the exact
on_enter
method (including awaits) and whether
update_agent
is async — I’ll suggest the minimal code/test change. Sources: Agents and handoffs | LiveKit Documentation | Test framework | LiveKit Documentation | The Handoff Pattern for Voice Agents That Replaces IVR Menus | LiveKit, with interactive elements
p
by the time the update_agent executes and enters on_enter for the new agent, there is no agent_session running anymore...
t
Thanks — I hear you: the session is ending before the new agent’s
on_enter
runs. That usually means the handoff call completed without keeping the session alive long enough for the new agent to run. Here are concise, actionable fixes and a small test pattern to try. Quick checklist (try in this order): • Ensure you await
update_agent
if it’s async: ◦ If
update_agent
is async:
await self.session.update_agent(agent=my_next_agent)
• Keep the session alive until the new agent starts: ◦ Return control to the session only after the handoff is requested (i.e., don’t let
on_enter
finish immediately if the session should continue). ◦ If your framework offers a wait-for-handoff primitive, use it (e.g.,
await self.session.wait_for_handoff()
), otherwise add a short await that yields control (not a long sleep) after
update_agent
to let the event loop process the handoff. • Prefer tool-based handoffs if appropriate: tools that return a new Agent instance automatically trigger a handoff and avoid lifecycle races. • Use
contains_agent_handoff(...)
in tests when exact ordering/timing is flaky. Minimal test pattern (pseudo-Python) to assert handoff without sleeps:
Copy code
result = await agent_session.start(agent=starting_agent, capture_run=True)
# ensure the session captured the initial message
result.expect.next_event().is_message(role="assistant")
# assert that a handoff to the next agent occurred somewhere in the events
result.expect.contains_agent_handoff(new_agent_type=my_next_agent.__class__)
# optionally assert the next agent produced a message
# result.expect.contains_event(lambda e: isinstance(e, ChatMessageEvent) and e.item['role']=='assistant' ...)
If you want, paste the full
StartingAgent.on_enter
(with awaits and
update_agent
call) and tell me whether
update_agent
is async; I’ll suggest the exact minimal code change to ensure the session remains active and the new agent’s
on_enter
runs. , with interactive elements
p
Copy code
class StartingAgent(Agent):
        async def on_enter(self) -> None:
            await self.session.say(
                text="Hello"
            )  # it needs to generate/say something, otherwise the the session will hang
           self.session.update_agent(agent=my_next_agent)
update_agent is sync.
t
Thanks — I see your
on_enter
and that
update_agent
is synchronous. The session ends before the new agent runs because
on_enter
returns immediately after calling the sync
update_agent
, so the event loop may not schedule the new agent before capture stops. Try this minimal change to ensure the handoff runs before
on_enter
finishes:
Copy code
class StartingAgent(Agent):
    async def on_enter(self) -> None:
        await self.session.say(text="Hello")
        self.session.update_agent(agent=my_next_agent)
        # yield control so the session can schedule the new agent's on_enter
        await asyncio.sleep(0)  # short, non-blocking
Why this helps: •
await asyncio.sleep(0)
yields to the event loop so the session can process the handoff and invoke the new agent’s
on_enter
before the starting agent returns. If your framework provides a wait-for-handoff primitive (e.g.,
await self.session.wait_for_handoff()
), prefer that instead of
sleep(0)
. After this change, keep your test assertions as: •
result.expect.next_event().is_message(role="assistant")
(for "Hello") •
result.expect.contains_agent_handoff(new_agent_type=my_next_agent.__class__)
• Optionally assert the new agent’s message with
contains
or further
next_event()
checks. Would you like me to craft a short test snippet that includes these assertions? , with interactive elements
p
I ran it and got the same result.
t
Thanks — I see the session is ending before the new agent’s
on_enter
runs even though
update_agent
is sync. That means the handoff can occur but the new agent’s lifecycle runs asynchronously outside the current run, so you won't reliably see the new agent's events in the same captured run. What to try next (minimal, ordered): 1. Verify the handoff happened (in the same run) ◦ Keep your existing flow but assert the handoff occurred: ▪︎
result.expect.contains_agent_handoff(new_agent_type=my_next_agent.__class__)
1. Test the new agent separately (recommended) ◦ After confirming the handoff event, start a new run or session to exercise
my_next_agent.on_enter()
and assert its messages/events. Example pattern: i. Run that triggers handoff → assert
contains_agent_handoff(...)
ii. Start/run with
my_next_agent
directly → assert its
on_enter
behavior 1. If you need end-to-end in one run (race avoidance) ◦ Use a handshake so the session stays alive until the new agent finishes: ▪︎ Make the starting agent await a confirmation after calling
update_agent
, e.g. set a session flag or await an async primitive the framework provides (preferable). ▪︎ If no primitive exists, yield to the loop after update and wait for a short condition rather than a blind sleep:
Copy code
self.session.update_agent(agent=my_next_agent)
       await asyncio.wait_for(self.session.wait_until_agent_is(my_next_agent.__class__), timeout=1)
(If
wait_until_agent_is
doesn't exist, implement a small session flag the new agent sets in
on_enter
that the starter polls with
asyncio.wait_for
.) 1. Debugging logs (if still failing) ◦ Log
type(session.current_agent)
before/after
update_agent
. ◦ Print
result.events
to verify what the test framework captured. If you want, paste: • the test code that starts/runs the session, and • whether you can add a small session flag or primitive the new agent can set in
on_enter
, and I’ll write the exact minimal test snippet or a handshake implementation you can drop into your code. Sources: Agents and handoffs | LiveKit Documentation | Testing and evaluation | LiveKit Documentation, with interactive elements