DEV Community

Abhishek Banerjee
Abhishek Banerjee

Posted on Originally published at Medium on

Orchestration Mechanics in Google ADK: Hierarchical State Machines vs. Graph-Based Agent Execution

An in-depth systems breakdown of agent lifecycles, DAG workflows, sub-agent scope isolation, and async scheduling under the hood.

When moving beyond toy agent scripts, multi-agent frameworks often degrade into fragile abstractions. Developers typically start with a root prompt instructing a model to “coordinate” specialized sub-agents. However, as task complexity scales, pure prompt-based routing collapses under non-deterministic tool dispatch, context drift, and unconstrained execution loops.

The Google Agent Development Kit (ADK) addresses this instability by separating high-level cognitive decision-making from deterministic control flow. ADK provides two primary execution models: Hierarchical State Transfer (dynamic LLM-coordinated delegation) and Graph-Based Workflow Execution (declarative state machines using explicit nodes, fan-out/fan-in barriers, and directed edges).

Underneath these abstractions, the ADK runtime manages state isolation, asynchronous event-loop scheduling, memory persistence, and dynamic call-stack traversal.

Here is an operational deep dive into ADK execution mechanics, comparing hierarchical delegation against graph workflows, analyzing runtime lifecycle passes, and breaking down lower-level systems friction.

1. Two Paradigms: Hierarchical Delegation vs. Graph Workflows

ADK categorizes orchestration into two distinct control structures: Prompt-Coordinated Hierarchical Delegation and Graph-Based Workflow Agents.

Hierarchical State Transfer (Dynamic LLM Delegation)

In a hierarchical structure, a root agent holds sub-agents in its execution context (sub_agents=[agent_a, agent_b]).

  • Control Transfer Mechanics: When the root agent determines a sub-agent is required, it triggers a control transfer event. Control drops into the sub-agent’s execution loop.
  • The “Manager Fallacy”: In basic sub-agent routing, once control transfers to Agent A, Agent A assumes full control over the session history. Unless explicitly configured with transfer-back primitives, the root agent loses loop ownership, causing multi-step execution pipelines (e.g., Discovery $\rightarrow$ Grounding $\rightarrow$ Synthesis) to stall prematurely after step one.

Graph-Based Workflows (Declarative DAG Execution)

To achieve deterministic control flow, ADK implements a Directed Acyclic Graph (DAG) runtime engine. Execution steps are explicitly defined as Nodes (wrapping AI Agents, Python/Go functions, or MCP tools) connected by Edges :

2. Lifecycle Breakdown: A 3-Stage Pipeline Pass

Consider an agent pipeline executing three distinct phases: Discovery (scraping data), Grounding (searching docs via MCP), and Synthesis (drafting content).

Stage 1: Discovery (Root Ingress & Context Initialization)

  1. The runtime receives an incoming trigger via the runner interface (e.g., InMemoryRunner or FastAPIApp).
  2. An InvocationContext object is instantiated, locking the user session ID, pulling short-term context, and fetching long-term memory embeddings (e.g., via Vertex AI Memory Bank).
  3. The Discovery node executes. In a graph workflow, output parameters are validated against strict type constraints (e.g., Pydantic or Zod schemas).

Stage 2: Grounding (State Writes & Parallel Tool Invocations)

  1. Upon completion of Discovery, the node emits an Output event.
  2. The ADK scheduler interceptor parses the event payload. In sequential chains, payload bytes route directly to the Grounding node’s typed input signature.
  3. If Grounding invokes tools (such as an external MCP Developer Knowledge server), the engine suspends agent evaluation, executes the tool standard input/output or SSE call, appends the tool response event to the session event log, and resumes evaluation.

Stage 3: Synthesis (Context Aggregation & Final Yield)

  1. Grounding emits validated context. The Synthesis node receives this output alongside optional read-only state slices.
  2. The model synthesizes the final result, emitting a TerminalResponse event.
  3. The ADK runner captures the final event, writes execution traces to telemetry (e.g., OpenTelemetry spans), and persists updated session state to the storage backend.

3. Systems Friction: Memory, Scheduling, and Cycles

While high-level SDK syntax hides execution complexity, running multi-agent topologies at scale introduces lower-level systems friction.

1. Sub-Agent Scope Isolation & Memory Contamination

In naive implementations, sub-agents write directly to a global session history string. This introduces context leak : intermediate scratchpad steps, failed tool trials, or raw JSON payloads from DiscoveryAgent pollute the system prompt of SynthesisAgent.

Global Shared Memory (Bad — High Noise): [User Input] -> [Discovery Scratchpad & Raw JSON] -> [Grounding Failures] -> [Synthesis Input] Isolated Node State (ADK Best Practice): [Discovery Node] ──> Emits Typed Output Only ──> Synthesis Node Input

  • Isolation Mechanics: ADK enforces scope isolation by decoupling Node Local Execution Frames from Global Session State.
  • State-Bound Decorators: Agents access global state through explicit, tagged state bindings (NewFunctionNodeFromState). Intermediate tool iterations remain localized to the node's internal frame; only explicitly returned values are wrapped in a session.Event and published to downstream nodes.

2. Event-Loop Scheduling During Parallel Fan-Out

When executing parallel branches (e.g., running three concurrent Grounding sub-agents across different doc sets), ADK leverages asynchronous event loops to manage execution.

  • Fan-Out / Fan-In Barriers: In graph workflows, a FanOut edge spawns parallel async tasks across a collection of inputs.
  • Join Node Synchronization: Downstream nodes acting as Join barriers halt execution until all predecessor tasks emit completion events.
  • Event Loop Starvation: If a sub-agent triggers a blocking synchronous tool call (e.g., heavy CPU serialization or a blocking network call), it starves the main asyncio event loop, delaying execution across unrelated parallel branches. All tool primitives in ADK must use non-blocking async execution drivers (asyncio).

3. Debugging Non-Deterministic Cyclic Dependencies

In recursive agent trees (e.g., an agent looping between Synthesis and Reviewer until an evaluation score passes), non-deterministic LLM behavior can trigger infinite execution loops.

  • The Problem: Without bounded constraints, cyclic transitions consume token budgets and exhaust container memory limits.
  • Mitigation (Graph Guardrails & Backoff):
  • Node Execution Limits: Enforce hard recursion caps directly on the runtime configuration:
cfg = workflow.NodeConfig(
    max_retries=3,
    timeout=30.0
)
Enter fullscreen mode Exit fullscreen mode
  1. Circuit Breaker Nodes: Inject a deterministic evaluator node into the loop that increments a counter state variable (state["iteration_count"] += 1). If iteration_count > max_iterations, the edge dynamically reroutes to an error mitigation node.

4. Implementation: Graph-Based Multi-Agent Workflow in ADK Python

Below is a complete, runnable Python implementation demonstrating a Graph-Based Workflow in Google ADK with explicit node chaining, schema validation, and parallel fan-out handling:

import asyncio
from typing import Dict, Any, List
from pydantic import BaseModel, Field

# Imports from Google Agent Development Kit
from google.adk.agents import LlmAgent
from google.adk.workflows import Workflow, Node, Chain, FanOut, Join
from google.adk.events import Event, InvocationContext

# --- 1. Define Typed Input/Output Contracts ---
class DiscoveryOutput(BaseModel):
    query: str
    target_topics: List[str] = Field(description="Key search topics identified")

class GroundingOutput(BaseModel):
    topic: str
    grounded_facts: List[str]

class SynthesisInput(BaseModel):
    research_data: List[GroundingOutput]

class FinalArticle(BaseModel):
    title: str
    content: str

# --- 2. Instantiate Base Specialist LLM Agents ---
discovery_agent = LlmAgent(
    name="DiscoveryAgent",
    model="gemini-2.5-flash",
    instruction="Analyze user prompt and extract 2 key technical sub-topics to research.",
    output_schema=DiscoveryOutput
)

grounding_agent = LlmAgent(
    name="GroundingAgent",
    model="gemini-2.5-flash",
    instruction="Provide 3 grounded factual points for the given technical topic.",
    output_schema=GroundingOutput
)

synthesis_agent = LlmAgent(
    name="SynthesisAgent",
    model="gemini-2.5-flash",
    instruction="Synthesize the grounded facts into an in-depth technical summary.",
    output_schema=FinalArticle
)

# --- 3. Custom Node Execution Functions for Graph Orchestration ---
async def discovery_node_fn(ctx: InvocationContext, user_input: str) -> DiscoveryOutput:
    """Executes Discovery Agent and returns typed output."""
    result = await discovery_agent.run(ctx, input_text=user_input)
    return DiscoveryOutput.model_validate_json(result.text)

async def grounding_worker_fn(ctx: InvocationContext, topic: str) -> GroundingOutput:
    """Worker node executed in parallel across target topics."""
    result = await grounding_agent.run(ctx, input_text=f"Research topic: {topic}")
    return GroundingOutput.model_validate_json(result.text)

async def join_synthesis_fn(ctx: InvocationContext, aggregated_results: List[Any]) -> FinalArticle:
    """Join barrier function gathering parallel outputs and running Synthesis."""
    grounded_data = [GroundingOutput.model_validate(r) for r in aggregated_results]

    synthesis_payload = SynthesisInput(research_data=grounded_data)
    result = await synthesis_agent.run(ctx, input_text=synthesis_payload.model_dump_json())
    return FinalArticle.model_validate_json(result.text)

# --- 4. Construct the ADK Execution Graph ---
def build_adk_orchestration_graph() -> Workflow:
    # Wrap functions into explicit Workflow Nodes
    node_discovery = Node(name="DiscoveryNode", func=discovery_node_fn)
    node_grounding_worker = Node(name="GroundingWorker", func=grounding_worker_fn)
    node_join_synthesis = Node(name="JoinSynthesisNode", func=join_synthesis_fn)

    # Build Graph Structure:
    # Start -> Discovery -> FanOut across target_topics -> GroundingWorkers -> Join -> Synthesis
    graph = Workflow(name="Agentic_Research_Pipeline")

    graph.add_edge(graph.START, node_discovery)
    graph.add_fan_out(
        source=node_discovery,
        target=node_grounding_worker,
        split_fn=lambda discovery_out: discovery_out.target_topics
    )
    graph.add_fan_in(
        sources=[node_grounding_worker],
        target=node_join_synthesis
    )
    graph.add_edge(node_join_synthesis, graph.END)

    return graph

# --- 5. Execution Driver ---
async def main():
    pipeline_graph = build_adk_orchestration_graph()

    # Simulate Invocation Context
    ctx = InvocationContext(session_id="session_adk_001")
    user_prompt = "Build an architectural summary of Transformer Attention Mechanisms."

    print(f"Executing ADK Graph Pipeline for query: '{user_prompt}'...\n")

    # Run the declarative graph pipeline
    final_output: FinalArticle = await pipeline_graph.run(ctx, input_data=user_prompt)

    print("=== Pipeline Execution Complete ===")
    print(f"Title: {final_output.title}")
    print(f"Content Body:\n{final_output.content}")

if __name__ == " __main__":
    # Note: Requires configured google-adk environment & API credentials
    print("ADK Graph Orchestration script ready for execution.")
Enter fullscreen mode Exit fullscreen mode

Building production multi-agent systems requires moving past unstructured prompt delegation. While hierarchical state transfers work well for lightweight conversational routing, enterprise agent pipelines demand the predictability of Graph-Based Workflow Execution.

By explicitly mapping control flow into directed graphs, isolating sub-agent scopes via typed state outputs, and bounding cyclic execution loops with node configs, engineers can build resilient, observable agent runtimes on top of Google ADK.

Need High-Impact Technical Content for Your Engineering Team?

I partner with developer-tooling startups, SaaS platforms, and engineering teams to translate complex infrastructure, agentic systems, and backend architecture into publication-grade technical writing.

Whether you need deep-dive architecture essays, hands-on developer tutorials, or technical counter-narratives:

Top comments (0)