fix(voice): keep the run open while a tool holds the floor - #6694
Conversation
ctx.foreground() waits for the activity to become idle, and that same transition is what completes the RunResult. The run therefore finished while the tool was still waiting for the floor, so a handoff made inside the scope went unrecorded. _wait_for_idle_and_hold now registers a guard on the active run before it waits, and releases it on exit. AgentTask suspends the guard along with the blocked tasks it already suspends, so a foregrounded task that needs a future user turn still lets run() return.
The circular wait one level deeper than the existing case: the foreground scope holds a generate_reply, and a tool of that reply awaits a task that needs a future user turn.
|
I'd misdiagnosed this one on the issue — I reproduced without Independently verified the branch, since I had the context loaded:
One observation, not a request. In the |
The guard was taken from the session's active run at scope entry, with no link to the turn of the caller. A tool that answered early with ctx.update() and kept working could enter ctx.foreground() while a later session.run() was in flight, and hold that unrelated run open for the whole scope. RunContext now records the run its call belongs to, and a scope guards only that run while it is still the active one.
Fixes #6658
The bug
Under
session.run(), a tool that callsupdate_agent()insidectx.foreground()records no handoff and nothing after it.ctx.foreground()waits for the activity to become idle. The activity becomes idle only after the speech handle of the turn completes, and that same completion ends theRunResult. The run finishes while the tool still waits for the floor.update_agent()then finds a run that is already done, so_watch_handledoes nothing.The fix
_wait_for_idle_and_holdregisters a guard on the active run before it waits for the floor, and releases it on exit. A run cannot complete while a tool waits for the floor or holds it.AgentTasksuspends that guard with the blocked tasks it already suspends. Without this step,await AgentTask()insidectx.foreground()deadlocks. The run waits for the guard, the guard waits for the task, and the task waits for a user turn. That turn only arrives afterrun()returns._watch_handleand the methods next to it acceptasyncio.Future.asyncio.Taskis a subclass ofFuture, so every current caller stays valid.Scope
A tool that returns early with
ctx.update()and then works in the background never takes the floor. It gets no guard, so the run keeps its own timing.A tool that returns early, waits, and then takes the floor much later is still not recorded. Nothing holds the floor in that window, and the later work belongs to a new turn.