← Back to writing

Michał Pasierbski

ADK sub_agents vs AgentTool: Two Delegation Patterns, One Critical Difference

Comparing two distinct delegation patterns in Google ADK — sub_agents transfer pattern vs AgentTool invocation pattern.

Originally published on Practically Agents

Banner reading 'ADK Sub Agent vs AgentTool' over a line drawing of a pipe that branches directly on the left and routes through a junction box on the right

Google ADK gives you two ways to delegate work from a parent agent to a child: sub_agents (transfer_to_agent, child owns the conversation) and AgentTool (child runs in isolation, returns a string, parent synthesizes). They look similar in code but have completely different control flow semantics. This article breaks down both patterns with sequence diagrams and Python examples, covers the “stickiness” gotcha where sub_agents fail to transfer back in production, and gives you a decision framework for choosing between them - including when to reach for workflow agents instead.

The Setup

You’re building a multi-agent system in Google ADK — a travel planner, a research assistant, a customer service router. You have a parent orchestrator and a few specialized child agents. You wire in a child one of two ways. It works in testing. Then in production, one of two things happens: the child never hands control back to the parent and the user is stuck talking to a specialist agent forever, or the parent is synthesizing a response it shouldn’t be touching at all. The conversation feels wrong and you’re not sure why.

The fix isn’t a bug fix. It’s a design fix. These two failure modes map directly to which delegation pattern you chose.

Two Patterns in Plain English

sub_agents is a transfer pattern. The parent puts the child agent in a list and the LLM decides when to hand off via transfer_to_agent(). At that point, the child owns the conversation — it responds directly to the user, can run multi-turn exchanges, and shares the same session history as the parent. The parent is out of the loop until and unless the child decides to transfer back. That “until and unless” is where things get interesting.

AgentTool is an invocation pattern. The child agent is wrapped as a tool. The parent calls it with a string input, the child runs its full reasoning loop in isolation, and returns a string result. The parent then synthesizes a response to the user. The child never communicates with the user directly. Control always returns to the parent — structurally guaranteed, not LLM-dependent.

Same child agent code, completely different runtime behavior depending on which pattern you use.

Control Flow Diagrams

Here’s how sub_agents delegation looks in practice:

sequenceDiagram
    actor User
    participant Parent as TravelAgent (Parent)
    participant Child as FlightAgent (Child)

    User->>Parent: "I need to book a flight to Tokyo"
    Parent->>Parent: LLM decides to transfer
    Parent->>Child: transfer_to_agent("flight_agent")
    Note over Parent: Parent exits the loop
    Child->>User: "What dates are you flying?"
    User->>Child: "March 15-22"
    Child->>User: "Got it. What's your departure city?"
    User->>Child: "New York"
    Child->>Child: LLM decides whether to transfer back
    Note over Child,Parent: Transfer back is LLM-driven -- may never happen
    Child-->>Parent: transfer_to_agent("travel_agent") [maybe]

And here’s AgentTool:

sequenceDiagram
    actor User
    participant Parent as TravelAgent (Parent)
    participant FlightTool as FlightAgent (as AgentTool)
    participant HotelTool as HotelAgent (as AgentTool)

    User->>Parent: "Plan my Tokyo trip, March 15-22, from New York"
    Parent->>FlightTool: call tool("Find flights NYC→Tokyo, March 15-22")
    FlightTool-->>Parent: "Best option: ANA NH010, $1,240, departs 11:55am"
    Parent->>HotelTool: call tool("Find hotels Tokyo, March 15-22, budget mid-range")
    HotelTool-->>Parent: "Recommended: Shinjuku Granbell, $145/night"
    Note over Parent: Parent synthesizes both results
    Parent->>User: "Here's your Tokyo trip plan: [combined itinerary]"

The structural difference is obvious once you see it. In sub_agents, the parent hands off and waits — or doesn’t wait at all. In AgentTool, the parent drives the whole conversation and the child is just a smart function call.

Code Examples

sub_agents: Transfer Pattern

from google.adk.agents import LlmAgent

# FlightAgent is a specialist -- it owns the flight booking conversation
flight_agent = LlmAgent(
    name="flight_agent",
    model="gemini-2.0-flash",
    instruction="""You are a flight booking specialist.
    Help the user find and select flights.

    When you have gathered all necessary information and completed
    flight selection, transfer back to the travel_agent to continue
    planning the rest of their trip.
    """,  # Explicit transfer-back instruction -- critical for avoiding stickiness
    tools=[search_flights, get_flight_details],
)

# TravelAgent delegates to flight_agent via transfer
travel_agent = LlmAgent(
    name="travel_agent",
    model="gemini-2.0-flash",
    instruction="""You are a full-service travel planner.

    For flight-related requests, transfer to flight_agent.
    For hotel requests, transfer to hotel_agent.
    Coordinate the full trip plan when all components are confirmed.
    """,
    sub_agents=[flight_agent, hotel_agent],  # Children available for transfer
)

When the user asks about flights, travel_agent’s LLM generates a transfer_to_agent("flight_agent") call. The session hands over. flight_agent takes the conversation from there — it can ask clarifying questions across multiple turns, run tool calls, and eventually (if the prompt does its job) transfer back.

AgentTool: Invocation Pattern

from google.adk.agents import LlmAgent
from google.adk.tools.agent_tool import AgentTool

# Same FlightAgent -- but now it's wrapped as a tool
flight_agent = LlmAgent(
    name="flight_agent",
    model="gemini-2.0-flash",
    instruction="""You are a flight search specialist.
    Given a flight query string, search for available options
    and return a structured summary of the best results.
    Return a plain-text summary -- the calling agent will present it to the user.
    """,  # No need for transfer-back instructions -- control always returns
    tools=[search_flights, get_flight_details],
)

hotel_agent = LlmAgent(
    name="hotel_agent",
    model="gemini-2.0-flash",
    instruction="""You are a hotel search specialist.
    Given a hotel query, return a structured summary of available options.
    """,
    tools=[search_hotels, get_hotel_details],
)

# Parent wraps children as AgentTools and synthesizes their output
travel_agent = LlmAgent(
    name="travel_agent",
    model="gemini-2.0-flash",
    instruction="""You are a full-service travel planner.
    Use the flight_agent_tool and hotel_agent_tool to gather options,
    then present a cohesive trip plan to the user.
    """,
    tools=[
        AgentTool(agent=flight_agent),   # Child runs in isolation, returns string
        AgentTool(agent=hotel_agent),    # Can be called in parallel or sequence
    ],
)

The parent LLM decides what to query and when. Both child agents run their full reasoning loop and return results. The parent synthesizes. The user only ever hears from the parent.

Comparison

Dimension sub_agents AgentTool
Who responds to user Child Parent
Session history sharing Shared Isolated
Multi-turn in child Yes No
LLM hops Fewer Extra hop per tool call
Control return LLM-driven (unreliable) Guaranteed
Parallelism No (use ParallelAgent) Yes, parent can call multiple
Best for Specialist owns the conversation Parent synthesizes multiple sources

The LLM hops difference matters at scale. With AgentTool, every child invocation is an additional LLM call — if your parent queries four specialists, you’ve got five LLM calls for that turn. With sub_agents, the child talks directly to the user without the parent in the middle. At high volume with expensive models, this is a real cost and latency difference.

The Stickiness Gotcha

Here’s the production failure mode for sub_agents: the child LLM never generates a transfer_to_agent() call back to the parent.

The child finishes helping the user with flights. The user says “great, now what about hotels?” — and the flight agent tries to help with hotels instead of transferring back. Or it just says “I only handle flights, please contact our hotel team” and leaves the user stuck. The parent agent never gets control back.

This is well-documented in the ADK GitHub issues (threads #147, #371, and #620 all describe variations of this). The root cause is that transfer_to_agent() is an LLM-generated action, not a guaranteed behavior. Whether the child transfers back depends on:

Three mitigations, in order of reliability:

1. Explicit transfer instructions in the child prompt. The most direct fix. Tell the child agent exactly when to transfer back and to whom. “When you have confirmed the user’s flight selection, immediately transfer back to travel_agent to continue their trip planning.” This helps but doesn’t guarantee anything — the LLM still makes the call.

2. Use workflow agents for deterministic orchestration. SequentialAgent and ParallelAgent use sub_agents but with fixed control flow — no LLM decides when to hand off. More on this in the next section.

3. Use AgentTool instead. Eliminates the problem structurally. If control return reliability matters more than multi-turn child conversations, AgentTool is the right default.

Workflow Agents — the Deterministic Middle Path

ADK provides SequentialAgent, ParallelAgent, and LoopAgent as built-in orchestration patterns. They use sub_agents under the hood, but the control flow is defined in code — not decided by an LLM at runtime.

from google.adk.agents import SequentialAgent

# Steps run in fixed order: flight → hotel → itinerary
# No LLM decides the sequence -- it's deterministic
trip_planner = SequentialAgent(
    name="trip_planner",
    sub_agents=[
        flight_research_agent,   # Runs first, always
        hotel_research_agent,    # Runs second, always
        itinerary_agent,         # Synthesizes results, runs last
    ],
)

SequentialAgent executes sub-agents in order, passing output from each to the next. ParallelAgent runs sub-agents concurrently and collects results. LoopAgent repeats until a termination condition is met.

Reach for workflow agents when the orchestration logic is fixed at build time — you know the sequence of steps before the conversation starts. They give you the specialization benefits of sub-agents (each agent owns its domain) without the non-deterministic handoff problem. The tradeoff is flexibility: you can’t dynamically decide which agents to invoke based on user intent the way an LLM orchestrator can.

Decision Framework

Work through these questions in order:

Does the child need to hold a multi-turn conversation with the user?

Does the parent need to query multiple sources and combine the results?

Is the orchestration logic fixed at build time?

Is reliable control return a hard requirement?

The default rule of thumb: start with AgentTool unless you have a specific reason to need multi-turn child conversations. It’s easier to reason about, easier to test, and avoids the stickiness problem entirely.