Skip to content

Commit ef8a5de

Browse files
DeanChensjcopybara-github
authored andcommitted
fix(workflow): Prevent silent drain of routed nodes in wait_for_output
A route-only node parked WAITING because completion handlers only checked output is None. Added a check for route is None to satisfy the BaseNode docstring contract that transitions to COMPLETED upon yielding output or route. Includes corresponding unit test. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 930918695
1 parent 4810ddc commit ef8a5de

3 files changed

Lines changed: 44 additions & 5 deletions

File tree

src/google/adk/workflow/_dynamic_node_scheduler.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,11 @@ def _record_result(
512512
elif child_ctx.actions.transfer_to_agent:
513513
state.status = NodeStatus.COMPLETED
514514
run.transfer_to_agent = child_ctx.actions.transfer_to_agent
515-
elif node.wait_for_output and child_ctx.output is None:
515+
elif (
516+
node.wait_for_output
517+
and child_ctx.output is None
518+
and child_ctx.route is None
519+
):
516520
state.status = NodeStatus.WAITING
517521
else:
518522
state.status = NodeStatus.COMPLETED

src/google/adk/workflow/_workflow.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,11 @@ def _handle_completion(
653653
loop_state.interrupt_ids.update(child_ctx.interrupt_ids)
654654
return
655655

656-
if node.wait_for_output and child_ctx.output is None:
656+
if (
657+
node.wait_for_output
658+
and child_ctx.output is None
659+
and child_ctx.route is None
660+
):
657661
node_state.status = NodeStatus.WAITING
658662
return
659663

tests/unittests/workflow/test_workflow_routes.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"""Testings for the Workflow routes."""
1616

1717
from typing import Any
18-
from typing import Dict
1918

2019
from google.adk.agents.context import Context
2120
from google.adk.apps.app import App
@@ -28,7 +27,6 @@
2827
import pytest
2928

3029
from .. import testing_utils
31-
from .workflow_testing_utils import create_parent_invocation_context
3230
from .workflow_testing_utils import simplify_events_with_node
3331
from .workflow_testing_utils import TestingNode
3432

@@ -37,7 +35,7 @@
3735
async def test_run_async_with_edge_routes(request: pytest.FixtureRequest):
3836
route_holder = {'route': 'route_b'}
3937

40-
def dynamic_router(ctx: Context, node_input: Any):
38+
def dynamic_router(_ctx: Context, _node_input: Any):
4139
return route_holder['route']
4240

4341
node_a = TestingNode(name='NodeA', output='A', route=dynamic_router)
@@ -140,6 +138,39 @@ async def test_output_route_bool(request: pytest.FixtureRequest):
140138
]
141139

142140

141+
@pytest.mark.asyncio
142+
async def test_wait_for_output_with_route_only_completes_successfully(
143+
request: pytest.FixtureRequest,
144+
):
145+
"""A node with wait_for_output=True that yields only a route should complete and continue the workflow."""
146+
node_a = TestingNode(name='NodeA', route='go_next', wait_for_output=True)
147+
node_b = TestingNode(name='NodeB', output='B_done')
148+
149+
agent = Workflow(
150+
name='test_wait_for_output_route',
151+
edges=[
152+
(START, node_a),
153+
(node_a, {'go_next': node_b}),
154+
],
155+
)
156+
app = App(name=request.function.__name__, root_agent=agent)
157+
runner = testing_utils.InMemoryRunner(app=app)
158+
159+
# NodeA should yield no output, and NodeB should yield its output.
160+
events = await runner.run_async(testing_utils.get_user_content('start'))
161+
162+
assert simplify_events_with_node(events) == [
163+
(
164+
'test_wait_for_output_route@1/NodeA@1',
165+
{'output': None},
166+
),
167+
(
168+
'test_wait_for_output_route@1/NodeB@1',
169+
{'output': 'B_done'},
170+
),
171+
]
172+
173+
143174
@pytest.mark.asyncio
144175
async def test_output_route_no_data(request: pytest.FixtureRequest):
145176
node_a = TestingNode(name='NodeA', route='route_b')

0 commit comments

Comments
 (0)