Repository navigation
fix(workflow): pass a single_turn transfer target's reply downstream - #7789
Open
vishal-bulbule wants to merge 3 commits into
Open
vishal-bulbule wants to merge 3 commits into
vishal-bulbule wants to merge 3 commits into
Conversation
A top-level LlmAgent node with sub_agents runs in single_turn mode. Its transfer target already runs inline inside the node and sets the node output, but the transfer action also reached DynamicNodeScheduler, which ran the target a second time as a chat-mode hop. That hop sets no output and its context is the one the workflow completes with, so the downstream node received None and the target model was called twice per turn. Clear the transfer at the end of the single_turn run so the inline reply is the node output and the target runs once. Fixes google#7782
Clearing the transfer after the inline run fixed fresh runs only. On resume the workflow resumes the transfer target as the scheduler hop, not inline, so a target paused on a tool confirmation or a long-running tool still passed None downstream, and a target transferring back to the router tripped the no_events_after_cancel invariant when resumable. A single_turn node now stops on transfer_to_agent, as chat and task modes do, so the target never runs inline. When the transfer chain starts at a single_turn node, each LlmAgent hop runs as a single_turn copy with rerun_on_resume, so the reply of the agent that ends the chain is the node output and a hop paused on a tool call runs its model again on resume. Fixes google#7782
Running a hop of a single_turn transfer chain as single_turn applied to any LlmAgent target, so a task-mode target reached through a tool-driven transfer lost its task mode and the workflow failed, and a target's own rerun_on_resume=False was overridden. Switch only targets whose mode is None or chat. A parent stamps chat on every LlmAgent sub-agent without a mode, so explicit and defaulted chat look the same and are both switched; task and single_turn sub-agents are tools, not transfer targets, and are left alone. Set rerun_on_resume only when the target did not set it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
Problem:
A top-level
LlmAgentnode withsub_agentsgets thesingle_turnmode frombuild_node. When it transfers, the transfer is handled twice:run_llm_agent_as_nodedoes not stop ontransfer_to_agentinsingle_turnmode, so the function-call postprocessor runs the target inline inside the node.NodeRunneralso copiestransfer_to_agentonto the node context, soDynamicNodeSchedulerruns the target again as a separate hop. That hop runs inchatmode, sets no output, and its context is the oneWorkflow._handle_completiongets.So for
START -> lead(sub_agents) -> capture,capturereceivesNoneand every transfer calls the target model twice per turn. On resume (a tool confirmation or long-running tool inside the target) the workflow fast-forwardsleadand resumes the target as the scheduler hop only, so the reply is lost there as well.Solution:
Let the scheduler own the transfer in both the fresh and the resumed path:
_llm_agent_wrapper.py: thesingle_turnbranch stops after an event withtransfer_to_agent, as thechatandtaskbranches already do. The target no longer runs inline._dynamic_node_scheduler.py: when the transfer chain starts at asingle_turnnode, eachLlmAgenthop whose mode isNoneorchatruns as asingle_turncopy, withrerun_on_resume=Trueunless the target set it (whatbuild_nodegives anLlmAgentnode). Its reply goes throughprocess_llm_agent_output, so the reply of the agent that ends the chain is the node output, and a hop paused on a tool call runs its model again on resume. A parent stampschaton everyLlmAgentsub-agent without a mode, so explicit and defaultedchattargets are both switched;taskandsingle_turnsub-agents are tools rather than transfer targets and keep their mode.Chains that start in
chatortaskmode are unchanged. I also tried giving these nodeschatmode inbuild_nodeinstead, but achatnode waits for an output that never comes, so the downstream node never runs.Testing Plan
Unit Tests:
New tests in
tests/unittests/workflow/test_llm_agent_as_node.py(16 cases, most parametrized overis_resumable):test_single_turn_transfer_output_reaches_downstream(two turns, different targets, each target called once)test_single_turn_transfer_terminal_node_replies_oncetest_single_turn_transfer_back_to_router_reaches_downstreamtest_single_turn_nested_transfer_reaches_downstreamtest_single_turn_transfer_target_structured_output_reaches_downstream(output_schemaplusoutput_key)test_single_turn_transfer_target_resumes_after_confirmationtest_single_turn_transfer_target_resumes_after_long_running_tooltest_single_turn_transfer_explicit_chat_target_reaches_downstreamtest_single_turn_transfer_keeps_task_mode_targettest_single_turn_transfer_respects_target_rerun_on_resumetests/unittests/workflow,agents,flows,runnersandtest_runners.py: all pass (Python 3.11).Full
tests/unittests, each top-level directory in its own process (minimal venv, so tests that need optional extras fail the same way on main): 16,280 passed on this branch and 16,269 on main, and the failing set is identical except the new tests, which fail only on main.Manual End-to-End (E2E) Tests:
The graph from #7782 with
gemini-3.5-flashon Agent Platform, abefore_model_callbackcounting model calls per agent:echogetsNoneboth turns; lead 5, translate 2, summarize 7 callsechogets both replies; lead 2, translate 1, summarize 1 callsechogetsNoneboth turns; lead 6, translate 9, summarize 3 callstranslatetool needs confirmation, resume, not resumableechogetsNoneafter the resumeechogets the translation after the resume; translate 2 calls (before and after the pause)On main the call counts vary between runs because transfers bounce between agents; one earlier run on main ended with
RecursionErrorin the postprocessor's inline transfer during the first turn.Checklist
Additional context
The first version of this PR cleared the transfer after the inline run. That fixed fresh runs only: on resume the target runs as the scheduler hop, so the resume cases above still failed. This version removes the inline run for
single_turnnodes instead, so there is one path.