@@ -848,9 +848,14 @@ def _handle_task_done(_: asyncio.Task[Any]) -> None:
848848
849849 # TODO(theomonnom): could the RunResult watcher & the blocked_tasks share the same logic?
850850 self .__inactive_ev .clear ()
851+ suspended_handles : list [SpeechHandle | asyncio .Task [Any ]] = []
852+ pending_on_enter_task : asyncio .Task [None ] | None = None
851853 try :
854+ # use wait_on_enter=False to avoid deadlock: on_enter may spawn nested
855+ # AgentTasks that require user input, but session.run() can't return until
856+ # all watched handles complete — creating a circular wait.
852857 await session ._update_activity (
853- self , previous_activity = "pause" , blocked_tasks = blocked_tasks
858+ self , previous_activity = "pause" , blocked_tasks = blocked_tasks , wait_on_enter = False
854859 )
855860
856861 if not self ._activity and not self .done ():
@@ -860,14 +865,29 @@ def _handle_task_done(_: asyncio.Task[Any]) -> None:
860865 )
861866 )
862867
863- # NOTE: _update_activity is calling the on_enter method, so the RunResult can capture all speeches
864868 run_state = session ._global_run_state
865- if speech_handle and run_state and not run_state .done ():
866- # make sure to not deadlock on the current speech handle
867- run_state ._unwatch_handle (speech_handle )
868- # it is OK to call _mark_done_if_needed here, the above _update_activity will call on_enter
869- # so handles added inside the on_enter will make sure we're not completing the run_state too early.
870- run_state ._mark_done_if_needed (None )
869+
870+ if self ._activity and (on_enter_task := self ._activity ._on_enter_task ):
871+ if run_state and not run_state .done ():
872+ # watch the on_enter task as a guard so RunResult won't complete
873+ # before on_enter has registered its own speech handles
874+ run_state ._watch_handle (on_enter_task )
875+ pending_on_enter_task = on_enter_task
876+ else :
877+ # no active run to guard — just wait for on_enter directly
878+ await asyncio .shield (on_enter_task )
879+
880+ # now unwatch the parent speech handle and blocked tasks that belong to the
881+ # old activity — they can't complete while this AgentTask is running, and
882+ # keeping them watched would block RunResult from completing.
883+ if run_state and not run_state .done ():
884+ if speech_handle and run_state ._unwatch_handle (speech_handle ):
885+ suspended_handles .append (speech_handle )
886+ for task in blocked_tasks :
887+ if run_state ._unwatch_handle (task ):
888+ suspended_handles .append (task )
889+ if suspended_handles :
890+ run_state ._mark_done_if_needed (None )
871891 except Exception :
872892 self .__inactive_ev .set ()
873893 raise
@@ -883,24 +903,32 @@ def _handle_task_done(_: asyncio.Task[Any]) -> None:
883903 # run_state could have changed after self.__fut
884904 run_state = session ._global_run_state
885905
906+ # re-watch the suspended handles so the resumed parent activity
907+ # is tracked by the current RunResult again
908+ if run_state and not run_state .done ():
909+ for handle in suspended_handles :
910+ run_state ._watch_handle (handle )
911+
912+ if pending_on_enter_task :
913+ try :
914+ await asyncio .shield (pending_on_enter_task )
915+ except BaseException :
916+ logger .exception ("error in on_enter task of agent %s" , self .id )
917+
886918 if session .current_agent != self :
887919 logger .warning (
888920 f"{ self .__class__ .__name__ } completed, but the agent has changed in the meantime. "
889921 "Ignoring handoff to the previous agent, likely due to `AgentSession.update_agent` being invoked."
890922 )
891923 await old_activity .aclose ()
892924 else :
893- if speech_handle and run_state and not run_state .done ():
894- run_state ._watch_handle (speech_handle )
895-
896925 merged_chat_ctx = old_agent .chat_ctx .merge (
897926 self .chat_ctx ,
898927 exclude_function_call = not self ._preserve_function_call_history ,
899928 exclude_instructions = True ,
900929 )
901930 # set the chat_ctx directly, `session._update_activity` will sync it to the rt_session if needed
902931 old_agent ._chat_ctx .items [:] = merged_chat_ctx .items
903- # await old_agent.update_chat_ctx(merged_chat_ctx)
904932
905933 await session ._update_activity (
906934 old_agent , new_activity = "resume" , wait_on_enter = False
0 commit comments