@@ -176,6 +176,11 @@ class NextStepHandoff:
176176 new_agent : Agent [Any ]
177177
178178
179+ @dataclass
180+ class NextStepHandoffReturnControl :
181+ previous_agent : Agent [Any ]
182+
183+
179184@dataclass
180185class NextStepFinalOutput :
181186 output : Any
@@ -201,7 +206,9 @@ class SingleStepResult:
201206 new_step_items : list [RunItem ]
202207 """Items generated during this current step."""
203208
204- next_step : NextStepHandoff | NextStepFinalOutput | NextStepRunAgain
209+ next_step : (
210+ NextStepHandoff | NextStepFinalOutput | NextStepRunAgain | NextStepHandoffReturnControl
211+ )
205212 """The next step to take."""
206213
207214 @property
@@ -238,6 +245,7 @@ async def execute_tools_and_side_effects(
238245 hooks : RunHooks [TContext ],
239246 context_wrapper : RunContextWrapper [TContext ],
240247 run_config : RunConfig ,
248+ previous_agents : list [Agent ],
241249 ) -> SingleStepResult :
242250 # Make a copy of the generated items
243251 pre_step_items = list (pre_step_items )
@@ -286,6 +294,7 @@ async def execute_tools_and_side_effects(
286294 hooks = hooks ,
287295 context_wrapper = context_wrapper ,
288296 run_config = run_config ,
297+ previous_agents = previous_agents ,
289298 )
290299
291300 # Next, we'll check if the tool use should result in a final output
@@ -316,6 +325,7 @@ async def execute_tools_and_side_effects(
316325 final_output = check_tool_use .final_output ,
317326 hooks = hooks ,
318327 context_wrapper = context_wrapper ,
328+ previous_agents = previous_agents ,
319329 )
320330
321331 # Now we can check if the model also produced a final output
@@ -340,6 +350,7 @@ async def execute_tools_and_side_effects(
340350 final_output = final_output ,
341351 hooks = hooks ,
342352 context_wrapper = context_wrapper ,
353+ previous_agents = previous_agents ,
343354 )
344355 elif (
345356 not output_schema or output_schema .is_plain_text ()
@@ -353,6 +364,7 @@ async def execute_tools_and_side_effects(
353364 final_output = potential_final_output_text or "" ,
354365 hooks = hooks ,
355366 context_wrapper = context_wrapper ,
367+ previous_agents = previous_agents ,
356368 )
357369 else :
358370 # If there's no final output, we can just run again
@@ -663,6 +675,7 @@ async def execute_handoffs(
663675 hooks : RunHooks [TContext ],
664676 context_wrapper : RunContextWrapper [TContext ],
665677 run_config : RunConfig ,
678+ previous_agents : list [Agent [TContext ]],
666679 ) -> SingleStepResult :
667680 # If there is more than one handoff, add tool responses that reject those handoffs
668681 multiple_handoffs = len (run_handoffs ) > 1
@@ -684,6 +697,8 @@ async def execute_handoffs(
684697 actual_handoff = run_handoffs [0 ]
685698 with handoff_span (from_agent = agent .name ) as span_handoff :
686699 handoff = actual_handoff .handoff
700+ if handoff .should_return_control :
701+ previous_agents .append (agent )
687702 new_agent : Agent [Any ] = await handoff .on_invoke_handoff (
688703 context_wrapper , actual_handoff .tool_call .arguments
689704 )
@@ -825,16 +840,21 @@ async def execute_final_output(
825840 final_output : Any ,
826841 hooks : RunHooks [TContext ],
827842 context_wrapper : RunContextWrapper [TContext ],
843+ previous_agents : list [Agent [TContext ]],
828844 ) -> SingleStepResult :
845+ is_returning_control = len (previous_agents ) > 0
829846 # Run the on_end hooks
830- await cls .run_final_output_hooks (agent , hooks , context_wrapper , final_output )
831-
847+ await cls .run_final_output_hooks (
848+ agent , hooks , context_wrapper , final_output , is_returning_control
849+ )
832850 return SingleStepResult (
833851 original_input = original_input ,
834852 model_response = new_response ,
835853 pre_step_items = pre_step_items ,
836854 new_step_items = new_step_items ,
837- next_step = NextStepFinalOutput (final_output ),
855+ next_step = NextStepHandoffReturnControl (previous_agents .pop ())
856+ if is_returning_control
857+ else NextStepFinalOutput (final_output ),
838858 )
839859
840860 @classmethod
@@ -844,13 +864,19 @@ async def run_final_output_hooks(
844864 hooks : RunHooks [TContext ],
845865 context_wrapper : RunContextWrapper [TContext ],
846866 final_output : Any ,
867+ is_returning_control : bool ,
847868 ):
848- await asyncio .gather (
849- hooks .on_agent_end (context_wrapper , agent , final_output ),
850- agent .hooks .on_end (context_wrapper , agent , final_output )
851- if agent .hooks
852- else _coro .noop_coroutine (),
853- )
869+ # If the agent is not returning control, run the hooks
870+ if not is_returning_control :
871+ await asyncio .gather (
872+ hooks .on_agent_end (context_wrapper , agent , final_output ),
873+ agent .hooks .on_end (context_wrapper , agent , final_output )
874+ if agent .hooks
875+ else _coro .noop_coroutine (),
876+ )
877+ # If the agent is returning control, only run the current agent's hooks
878+ elif agent .hooks :
879+ await agent .hooks .on_end (context_wrapper , agent , final_output )
854880
855881 @classmethod
856882 async def run_single_input_guardrail (
0 commit comments