Skip to content

Commit eaffb0e

Browse files
DeanChensjcopybara-github
authored andcommitted
feat: Validate that no old orchestrators are used inside Workflow graphs
Adds validation to `validate_graph` to raise an error if any of the deprecated orchestrators (`SequentialAgent`, `ParallelAgent`, `LoopAgent`) are included as nodes in a `Workflow` static graph. Also includes logic to recursively unwrap wrapped nodes (e.g. `_ParallelWorker`) to ensure the underlying agent types are validated. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 947237776
1 parent 45a77dc commit eaffb0e

2 files changed

Lines changed: 110 additions & 0 deletions

File tree

src/google/adk/workflow/utils/_graph_validation.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,43 @@ def _validate_chat_agent_wiring(edges: list[Edge]) -> None:
198198
)
199199

200200

201+
def _validate_no_old_orchestrators(nodes: list[BaseNode]) -> None:
202+
"""Validates that deprecated orchestrators are not used in the workflow graph."""
203+
from ...agents.loop_agent import LoopAgent
204+
from ...agents.parallel_agent import ParallelAgent
205+
from ...agents.sequential_agent import SequentialAgent
206+
207+
for node in nodes:
208+
target = node
209+
visited = set()
210+
while True:
211+
if id(target) in visited:
212+
break
213+
visited.add(id(target))
214+
215+
next_target = None
216+
for attr in ("node", "_node", "agent", "_agent"):
217+
val = getattr(target, attr, None)
218+
if isinstance(val, BaseNode):
219+
next_target = val
220+
break
221+
222+
if next_target is not None:
223+
target = next_target
224+
else:
225+
break
226+
227+
if isinstance(target, (SequentialAgent, ParallelAgent, LoopAgent)):
228+
agent_type_name = type(target).__name__
229+
raise ValueError(
230+
f"Graph validation failed. Node '{node.name}' uses deprecated"
231+
f" orchestrator '{agent_type_name}'. Old orchestrators"
232+
" (SequentialAgent, ParallelAgent, LoopAgent) cannot be used inside"
233+
" a Workflow static graph. Please migrate the orchestration logic to"
234+
" Workflow nodes and edges."
235+
)
236+
237+
201238
def _compute_terminal_nodes(
202239
nodes: list[BaseNode], edges: list[Edge]
203240
) -> set[str]:
@@ -219,4 +256,5 @@ def validate_graph(nodes: list[BaseNode], edges: list[Edge]) -> set[str]:
219256
_detect_unconditional_cycles(edges, node_names)
220257
_validate_static_schemas(edges)
221258
_validate_chat_agent_wiring(edges)
259+
_validate_no_old_orchestrators(nodes)
222260
return _compute_terminal_nodes(nodes, edges)

tests/unittests/workflow/utils/test_graph_validation.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
import logging
1818

19+
from google.adk.agents.loop_agent import LoopAgent
20+
from google.adk.agents.parallel_agent import ParallelAgent
21+
from google.adk.agents.sequential_agent import SequentialAgent
22+
from google.adk.workflow import BaseNode
1923
from google.adk.workflow import Edge
2024
from google.adk.workflow import START
2125
from google.adk.workflow._graph import DEFAULT_ROUTE
@@ -388,3 +392,71 @@ def test_chat_agent_wiring_validation_only_runs_on_llm_agent() -> None:
388392
validate_graph(
389393
graph.nodes, graph.edges
390394
) # Should not raise because node_b is a TestingNode, not LlmAgent
395+
396+
397+
@pytest.mark.parametrize(
398+
'agent_class, name, expected_class_name',
399+
[
400+
(SequentialAgent, 'SeqAgent', 'SequentialAgent'),
401+
(ParallelAgent, 'ParAgent', 'ParallelAgent'),
402+
(LoopAgent, 'LoopAgent', 'LoopAgent'),
403+
],
404+
)
405+
def test_old_orchestrators_fail_validation(
406+
agent_class: type[BaseNode],
407+
name: str,
408+
expected_class_name: str,
409+
) -> None:
410+
"""Tests that a graph containing deprecated orchestrators fails validation."""
411+
agent = agent_class(name=name, sub_agents=[])
412+
graph = Graph(
413+
edges=[
414+
Edge(from_node=START, to_node=agent),
415+
],
416+
)
417+
with pytest.raises(
418+
ValueError,
419+
match=(
420+
rf"Graph validation failed\. Node '{name}' uses deprecated"
421+
rf" orchestrator '{expected_class_name}'\."
422+
),
423+
):
424+
validate_graph(graph.nodes, graph.edges)
425+
426+
427+
def test_old_orchestrator_wrapped_in_parallel_worker_fails_validation() -> None:
428+
"""Tests that a graph with old orchestrators in ParallelWorker fails validation."""
429+
from google.adk.agents.sequential_agent import SequentialAgent
430+
from google.adk.workflow._parallel_worker import _ParallelWorker
431+
432+
seq_agent = SequentialAgent(name='SeqAgent', sub_agents=[])
433+
worker_node = _ParallelWorker(node=seq_agent)
434+
graph = Graph(
435+
edges=[
436+
Edge(from_node=START, to_node=worker_node),
437+
],
438+
)
439+
with pytest.raises(
440+
ValueError,
441+
match=(
442+
r"Graph validation failed\. Node 'SeqAgent' uses deprecated"
443+
r" orchestrator 'SequentialAgent'\."
444+
),
445+
):
446+
validate_graph(graph.nodes, graph.edges)
447+
448+
449+
def test_self_referential_wrapped_node_safely_terminates() -> None:
450+
"""Tests that a self-referential wrapped node safely breaks without infinite loop."""
451+
node_a = TestingNode(name='NodeA')
452+
# Mock a self-referential node property
453+
object.__setattr__(node_a, 'node', node_a)
454+
455+
graph = Graph(
456+
edges=[
457+
Edge(from_node=START, to_node=node_a),
458+
],
459+
)
460+
validate_graph(
461+
graph.nodes, graph.edges
462+
) # Should not spin forever and should pass

0 commit comments

Comments
 (0)