ADR-002: AgentTool vs Sub-Agents for Multi-Agent Composition¶
Status: Superseded by ADR-003 Date: 2026-03-22 Author: Taoufiq
Superseded (2026-06-07): The LLM root orchestrator this ADR describes was replaced by a graph-based
Workflowroot in ADR-003. The AgentTool-vs-sub-agent guidance below no longer applies to the orrery-assistant root, but is retained for historical context and still informs standalone multi-agent composition.
Context¶
The orrery-assistant root orchestrator composes six specialist agents. Two distinct delegation patterns exist in Google ADK:
-
Sub-agents (
sub_agents=[]) — Agents listed as structural children of a parent. Used withSequentialAgent,ParallelAgent, andLoopAgentfor deterministic, orchestrator-driven workflows. -
AgentTool (
tools=[AgentTool(agent=...)]) — An agent wrapped as a callable tool. The parent LLM decides whether and when to invoke it, just like any other function call.
Previously, all six agents were in sub_agents, mixing two fundamentally different delegation modes: - incident_triage_agent — A deterministic SequentialAgent pipeline (always runs health checks → summarize → save). Orchestrator-driven. - kafka_agent, k8s_agent, observability_agent, docker_agent, journal_agent — The LLM reads their descriptions and picks one based on the user's query. LLM-driven.
This violated the ADK best practice: use sub-agents for fixed workflows, use AgentTool for LLM-driven routing.
Reference: Where to use sub-agents versus agents as tools (Google Cloud Blog). These patterns are formally audited in Agentic Design Patterns.
Decision¶
Convert the five LLM-routed specialist agents from sub_agents to AgentTool. Keep incident_triage_agent as the sole sub-agent (deterministic workflow).
Decision criteria (general rule)¶
| Question | Sub-agent | AgentTool |
|---|---|---|
| Who decides the execution order? | The orchestrator (fixed pipeline) | The LLM (dynamic routing) |
| Is the workflow deterministic? | Yes — always runs A → B → C | No — depends on user query |
| Do agents need shared state? | Yes — pipeline data flow via output_key | No — self-contained request/response |
| Can the agent have multiple parents? | No — ADK enforces single parent_agent | Yes — AgentTool can wrap any agent |
| Coupling level | Tight — part of the parent's structure | Loose — called like a function |
When to use sub-agents¶
SequentialAgentpipelines: fetch → process → saveParallelAgentfan-out/gather: run N checks concurrently, collect resultsLoopAgentiterations: retry/refine until a condition is met- Any workflow where the execution order is predetermined
When to use AgentTool¶
- The parent LLM should choose which specialist to call based on user intent
- The specialist is a self-contained expert with clear input/output boundaries
- The same agent may be invoked by multiple parent agents
- You want cleaner context — tool signatures are more structured than sub-agent descriptions for LLM function calling
What this looks like in code¶
Before (all sub-agents):
root_agent = create_agent(
name="orrery_assistant",
tools=[],
sub_agents=[
incident_triage_agent, # deterministic workflow
kafka_agent, # LLM-routed
k8s_agent, # LLM-routed
observability_agent, # LLM-routed
docker_agent, # LLM-routed
journal_agent, # LLM-routed
],
)
After (mixed — sub-agents for workflows, AgentTool for specialists):
from orrery_core import AgentTool
root_agent = create_agent(
name="orrery_assistant",
tools=[
AgentTool(agent=kafka_agent),
AgentTool(agent=k8s_agent),
AgentTool(agent=observability_agent),
AgentTool(agent=docker_agent),
AgentTool(agent=journal_agent),
],
sub_agents=[
incident_triage_agent, # deterministic SequentialAgent pipeline
],
)
Consequences¶
Positive¶
- Correct delegation semantics — Deterministic workflows stay as sub-agents; LLM-routed specialists become tools. Each pattern is used for its intended purpose.
- Better LLM routing — AgentTool exposes agents as function signatures, which is more natural for LLM function calling than sub-agent description matching.
- Reusability — Specialist agents are no longer bound to a single parent. They can be wrapped in
AgentToolby any future orchestrator without hitting ADK's single-parent constraint. - Cleaner separation — The root agent's
sub_agentslist now only contains structural workflows, making the architecture self-documenting.
Negative¶
- State sharing changes — AgentTool forwards state/artifact changes back to the parent, but the interaction is more transactional than shared-state sub-agents. For this project, this is fine since the specialist agents don't rely on shared session state with the root.
Neutral¶
- No behavioral change — The LLM still routes to the same specialists for the same queries. The delegation mechanism changes, but the user experience is identical.
- Callbacks still per-agent — As noted in ADR-001, each agent's
before_tool_callbackfires independently. This is unchanged by the AgentTool pattern.
Implementation¶
core/orrery_core/agent/base.py— Widenedtoolstype toSequence[Callable[..., Any] | BaseTool], re-exportedAgentToolcore/orrery_core/__init__.py— AddedAgentToolexportagents/orrery-assistant/orrery_assistant/agent.py— Converted 5 specialist agents fromsub_agentstoAgentTool