-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat: automatic back-handoff to orchestrating agents #3584
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -765,6 +765,7 @@ def _finalize_result(result: RunResult) -> RunResult: | |
| raise | ||
|
|
||
| try: | ||
| handoff_chain: list[Agent[Any]] = [] | ||
| while True: | ||
| resuming_turn = is_resumed_state | ||
| all_input_guardrails = ( | ||
|
|
@@ -959,6 +960,20 @@ def _finalize_result(result: RunResult) -> RunResult: | |
| ) | ||
|
|
||
| if isinstance(turn_result.next_step, NextStepFinalOutput): | ||
| if handoff_chain: | ||
| parent_agent = handoff_chain.pop() | ||
| child_output_text = format_final_output_text( | ||
| current_agent, turn_result.next_step.output | ||
| ) | ||
| back_msg = create_message_output_item( | ||
| current_agent, child_output_text | ||
| ) | ||
| generated_items.append(back_msg) | ||
| session_items.append(back_msg) | ||
| current_agent = cast(Agent[TContext], parent_agent) | ||
| if run_state is not None: | ||
| run_state._current_agent = current_agent | ||
| continue | ||
| output_guardrail_results = await run_output_guardrails( | ||
| current_agent.output_guardrails | ||
| + (run_config.output_guardrails or []), | ||
|
|
@@ -1009,8 +1024,11 @@ def _finalize_result(result: RunResult) -> RunResult: | |
| result._original_input = copy_input_items(original_input) | ||
| return _finalize_result(result) | ||
| elif isinstance(turn_result.next_step, NextStepHandoff): | ||
| next_step = turn_result.next_step | ||
| if next_step.auto_handoff_back and next_step.originating_agent is not None: | ||
| handoff_chain.append(next_step.originating_agent) | ||
|
Comment on lines
+1028
to
+1029
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This stores the originating agent only in a local Useful? React with 👍 / 👎. |
||
| current_agent = cast( | ||
| Agent[TContext], turn_result.next_step.new_agent | ||
| Agent[TContext], next_step.new_agent | ||
| ) | ||
| if run_state is not None: | ||
| run_state._current_agent = current_agent | ||
|
|
@@ -1365,6 +1383,20 @@ def _finalize_result(result: RunResult) -> RunResult: | |
|
|
||
| try: | ||
| if isinstance(turn_result.next_step, NextStepFinalOutput): | ||
| if handoff_chain: | ||
| parent_agent = handoff_chain.pop() | ||
| child_output_text = format_final_output_text( | ||
| current_agent, turn_result.next_step.output | ||
| ) | ||
| back_msg = create_message_output_item( | ||
| current_agent, child_output_text | ||
| ) | ||
| generated_items.append(back_msg) | ||
| session_items.append(back_msg) | ||
| current_agent = cast(Agent[TContext], parent_agent) | ||
| if run_state is not None: | ||
| run_state._current_agent = current_agent | ||
| continue | ||
| output_guardrail_results = await run_output_guardrails( | ||
| current_agent.output_guardrails | ||
| + (run_config.output_guardrails or []), | ||
|
|
@@ -1470,7 +1502,10 @@ def _finalize_result(result: RunResult) -> RunResult: | |
| ) | ||
| return _finalize_result(result) | ||
| elif isinstance(turn_result.next_step, NextStepHandoff): | ||
| current_agent = cast(Agent[TContext], turn_result.next_step.new_agent) | ||
| next_step = turn_result.next_step | ||
| if next_step.auto_handoff_back and next_step.originating_agent is not None: | ||
| handoff_chain.append(next_step.originating_agent) | ||
| current_agent = cast(Agent[TContext], next_step.new_agent) | ||
| if run_state is not None: | ||
| run_state._current_agent = current_agent | ||
| # Next agent starts with the nested/filtered input. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -668,6 +668,7 @@ async def _save_stream_items_without_count( | |
| raise | ||
|
|
||
| try: | ||
| handoff_chain: list[Agent[Any]] = [] | ||
| while True: | ||
| all_input_guardrails = ( | ||
| starting_agent.input_guardrails + (run_config.input_guardrails or []) | ||
|
|
@@ -801,7 +802,10 @@ async def _save_stream_items_without_count( | |
| break | ||
|
|
||
| if isinstance(turn_result.next_step, NextStepHandoff): | ||
| current_agent = turn_result.next_step.new_agent | ||
| next_step = turn_result.next_step | ||
| if next_step.auto_handoff_back and next_step.originating_agent is not None: | ||
| handoff_chain.append(next_step.originating_agent) | ||
| current_agent = next_step.new_agent | ||
| if run_state is not None: | ||
| run_state._current_agent = current_agent | ||
| if current_span: | ||
|
|
@@ -815,6 +819,29 @@ async def _save_stream_items_without_count( | |
| continue | ||
|
|
||
| if isinstance(turn_result.next_step, NextStepFinalOutput): | ||
| if handoff_chain: | ||
| parent_agent = handoff_chain.pop() | ||
| child_output_text = format_final_output_text( | ||
| current_agent, turn_result.next_step.output | ||
| ) | ||
| back_msg = create_message_output_item( | ||
| current_agent, child_output_text | ||
| ) | ||
| turn_session_items.append(back_msg) | ||
| streamed_result.new_items.append(back_msg) | ||
| current_agent = parent_agent | ||
| if run_state is not None: | ||
| run_state._current_agent = current_agent | ||
| streamed_result._event_queue.put_nowait( | ||
| AgentUpdatedStreamEvent(new_agent=current_agent) | ||
| ) | ||
| await _save_resumed_items( | ||
| list(turn_session_items), | ||
| turn_result.model_response.response_id, | ||
| store_setting, | ||
| ) | ||
| run_state._current_step = NextStepRunAgain() # type: ignore[assignment] | ||
| continue | ||
| await _finalize_streamed_final_output( | ||
| streamed_result=streamed_result, | ||
| agent=current_agent, | ||
|
|
@@ -1089,12 +1116,15 @@ async def _save_stream_items_without_count( | |
| server_conversation_tracker.track_server_items(turn_result.model_response) | ||
|
|
||
| if isinstance(turn_result.next_step, NextStepHandoff): | ||
| next_step = turn_result.next_step | ||
| if next_step.auto_handoff_back and next_step.originating_agent is not None: | ||
| handoff_chain.append(next_step.originating_agent) | ||
| await _save_stream_items_without_count( | ||
| turn_session_items, | ||
| turn_result.model_response.response_id, | ||
| store_setting, | ||
| ) | ||
| current_agent = turn_result.next_step.new_agent | ||
| current_agent = next_step.new_agent | ||
| if run_state is not None: | ||
| run_state._current_agent = current_agent | ||
| current_span.finish(reset_current=True) | ||
|
|
@@ -1111,6 +1141,28 @@ async def _save_stream_items_without_count( | |
| streamed_result._event_queue.put_nowait(QueueCompleteSentinel()) | ||
| break | ||
| elif isinstance(turn_result.next_step, NextStepFinalOutput): | ||
| if handoff_chain: | ||
| parent_agent = handoff_chain.pop() | ||
| child_output_text = format_final_output_text( | ||
| current_agent, turn_result.next_step.output | ||
| ) | ||
| back_msg = create_message_output_item( | ||
| current_agent, child_output_text | ||
| ) | ||
| turn_session_items.append(back_msg) | ||
| streamed_result.new_items.append(back_msg) | ||
|
Comment on lines
+1152
to
+1153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the streaming path, this synthetic child-result message is appended only to Useful? React with 👍 / 👎. |
||
| current_agent = parent_agent | ||
| if run_state is not None: | ||
| run_state._current_agent = current_agent | ||
| streamed_result._event_queue.put_nowait( | ||
| AgentUpdatedStreamEvent(new_agent=current_agent) | ||
| ) | ||
| await _save_stream_items_with_count( | ||
| turn_session_items, | ||
| turn_result.model_response.response_id, | ||
| store_setting, | ||
| ) | ||
| continue | ||
| await _finalize_streamed_final_output( | ||
| streamed_result=streamed_result, | ||
| agent=current_agent, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This inserts
auto_handoff_backbefore the existingis_enabledpositional slot in the runtime implementation, so existing callers that passedis_enabledpositionally now bind that value toauto_handoff_backand leaveis_enabledat its defaultTrue. For example, a previously disabled handoff using positional arguments is silently re-enabled, which breaks the public API positional-compatibility contract; the new optional parameter needs to be appended afteris_enabledor handled with a compatibility shim.Useful? React with 👍 / 👎.