DEV Community

Koichi
Koichi

Posted on

ADK: AgentTool Is Now Discouraged — Use mode='single_turn' Instead

If you call one agent as a tool from another in ADK, you've probably written tools=[AgentTool(child_agent)]. As of 2.5.0 that pattern is discouraged, and the recommended form is to set mode='single_turn' on the child and attach it via sub_agents. This post walks through what actually differs between the two.

Verified with google-adk 2.6.3.

What changed

A note was added to the AgentTool docstring (see a9b4276 and f4072a4):

To expose an agent as an inline tool of a parent ``LlmAgent``, prefer
setting ``mode='single_turn'`` on the sub-agent and attaching it via
``sub_agents=[...]`` instead of wrapping it with ``AgentTool``. The
framework then exposes the sub-agent as a tool automatically and runs it
inline in the parent's session.

...

Direct usage of ``AgentTool`` is discouraged. See the single-turn
mode guide for details.
Enter fullscreen mode Exit fullscreen mode

AgentTool still behaves exactly as before. What is no longer recommended is passing it through tools=[AgentTool(...)].

Note that this change hasn't landed in the official docs yet.

Side note: what mode is

mode is an LlmAgent field introduced in 2.0.0. It decides how the agent is invoked by its parent.

mode How it is invoked
chat Control is handed over via transfer_to_agent. The classic sub-agent.
task Completes a task while conversing with the user.
single_turn Returns a result in one turn, no user conversation. Tool-like.

The default is chat when placed in sub_agents, and single_turn when placed as a node in a Workflow.

When a sub-agent is given mode='single_turn', ADK wraps it internally in _SingleTurnAgentTool and runs it as a tool. Since 2.5.0 this is the recommended way to write it.

Reference: https://adk.dev/workflows/collaboration/

Comparing the two in practice

Let's implement both the AgentTool version and the sub_agents version and see how they differ.

The AgentTool version

from google.adk import Agent
from google.adk.tools import AgentTool

def get_weather(city: str) -> str:
    if city == "Tokyo":
        return f"The weather in {city} is sunny."
    else:
        return f"The weather in {city} is cloudy."

weather_agent = Agent(
    name="weather_agent",
    instruction="Answer questions about the weather.",
    description="Answers the weather for a given city.",
    tools=[get_weather],
)

root_agent = Agent(
    name="root_agent",
    instruction="Answer the user's questions.",
    tools=[AgentTool(weather_agent)],  # called through AgentTool
)
Enter fullscreen mode Exit fullscreen mode

The sub_agents version

from google.adk import Agent

def get_weather(city: str) -> str:
    if city == "Tokyo":
        return f"The weather in {city} is sunny."
    else:
        return f"The weather in {city} is cloudy."

weather_agent = Agent(
    name="weather_agent",
    instruction="Answer questions about the weather.",
    description="Answers the weather for a given city.",
    tools=[get_weather],
    mode="single_turn",  # set the mode
)

root_agent = Agent(
    name="root_agent",
    instruction="Answer the user's questions.",
    sub_agents=[weather_agent],  # passed to sub_agents, not tools
)
Enter fullscreen mode Exit fullscreen mode

The difference in behavior

Here's how each one looks in adk web.

AgentTool:

demo_agent_tool

sub_agents:

demo_sub_agent

In both cases root_agent produces the final answer, and the content is identical.

The difference is what shows up in between. With AgentTool you only see events from root_agent (the parent), while with sub_agents the child agent's events are interleaved. In other words, the real difference is which events remain in the session. Here's why.

How events are recorded in each case

With AgentTool

Inside AgentTool, the agent runs in a method called AgentTool.run_async. Every time it's called, it builds a Runner like this:

# inside AgentTool.run_async
runner = Runner(
    app_name=child_app_name,
    agent=self.agent,
    artifact_service=ForwardingArtifactService(tool_context),
    session_service=InMemorySessionService(),
    memory_service=InMemoryMemoryService(),
    ...
)
session = await runner.session_service.create_session(...)
Enter fullscreen mode Exit fullscreen mode

The InMemorySessionService() and InMemoryMemoryService() set here are independent of the parent agent's own services. Afterwards the Runner is thrown away and only the final answer is handed back to the parent.

Because of this, none of the child agent's events survive from the parent's point of view.

With sub_agents

_SingleTurnAgentTool, which runs when mode='single_turn', executes the agent like this:

return await tool_context.run_node(
    self.agent,
    node_input=node_input,
    override_branch=tool_branch,
    use_sub_branch=False,
)
Enter fullscreen mode Exit fullscreen mode

The difference from AgentTool is that it doesn't create a new Runner. It runs inside the parent's session via run_node, as a node of the graph workflow. That's why the child's events remain.

So how are the parent and child contexts kept apart? At the entry point of the node execution path, include_contents is rewritten:

# inside the code that runs an LlmAgent as a node
include_contents_explicit = "include_contents" in agent.model_fields_set
if agent.mode == "single_turn" and not include_contents_explicit:
    agent.include_contents = "none"  # set to none when mode=single_turn
Enter fullscreen mode Exit fullscreen mode

include_contents is an LlmAgent field that controls whether the conversation history is included in requests to the model. "default" passes the history along, "none" doesn't.

Reference: https://adk.dev/agents/llm-agents/#manage-agent-context

This is how ADK gets the same session as the parent, with a separate context.

Summary

Here are the two ways to call an agent as a tool, side by side:

AgentTool mode='single_turn' + sub_agents
How it's passed tools=[AgentTool(agent)] sub_agents=[agent]
How it runs In a newly created Runner As a run_node call inside the parent's session
Session Throwaway InMemorySessionService Same as the parent
Context separation Separate session include_contents="none"
Child's events Not kept Kept
Current status Discouraged Recommended

Nowhere is the reason for discouraging AgentTool spelled out, but looking at this table it seems like a fair call. AgentTool gave up event tracing and persistence in order to separate the context. If you can get the same separation with include_contents, keeping the events is the obvious choice.

Since it's discouraged rather than deprecated, there's no rush to rewrite existing code. For anything new, go with mode='single_turn' + sub_agents. You get to see which tools the sub-agent called and with what arguments, which makes debugging a lot easier.

Top comments (0)