---
title: "Building AI Agents for DevOps Part 5: Sub-Agents vs AgentTool"
url: "https://tirraflow.com/posts/building-ai-agents-for-devops-part-5-sub-agents-vs-agenttool"
author: "taoufiq"
published: "2026-03-22T12:53:41Z"
updated: "2026-10-15T21:16:04Z"
category: "AI Agents"
difficulty: "intermediate"
series: "Building AI Agents for DevOps"
series_part: 5
series_url: "https://tirraflow.com/series/building-ai-agents-for-devops"
tags: ["Agents", "ADK"]
---

# Building AI Agents for DevOps Part 5: Sub-Agents vs AgentTool

> **Updated for Orrery 0.6.0 (October 2026).** AgentTool won. Every specialist on the chat root is an AgentTool today. The sub-agent pipeline described here was replaced by an ADK 2.0 graph workflow, where plain Python decides the routing. One consequence of AgentTool's separate Runner, that the parent can't see inside a specialist, is what [Part 16](https://tirraflow.com/posts/building-ai-agents-for-devops-part-16-streaming-what-the-agent-is-doing) solves with a plugin.

In [Part 4](https://tirraflow.com/posts/building-ai-agents-for-devops-part-4-prometheus-metrics), I added Prometheus metrics. The platform now monitors itself. But while reviewing the architecture, I noticed a design flaw hiding in plain sight: I was using the same delegation pattern for two fundamentally different routing modes.

This post fixes that — by applying the right Google ADK pattern to each situation.

---

## The Problem: One Pattern, Two Jobs

Here's how the devops-assistant orchestrator looked before this change:

```python
root_agent = create_agent(
    name="devops_assistant",
    tools=[],
    sub_agents=[
        incident_triage_agent,   # deterministic pipeline
        kafka_agent,             # LLM picks this
        k8s_agent,               # LLM picks this
        observability_agent,     # LLM picks this
        docker_agent,            # LLM picks this
        journal_agent,           # LLM picks this
    ],
)
```

Six agents, all in `sub_agents`. But they serve two different purposes:

- `incident_triage_agent` — A deterministic `SequentialAgent` pipeline. It _always_ runs the same steps: parallel health checks, then summarize, then save. The **orchestrator** controls execution order.
- **The other five** — Specialist agents. The **LLM reads their descriptions** and picks the right one based on the user's query. This is dynamic routing, not fixed orchestration.

I was using sub-agents for both. Google's ADK has a better pattern for the second case: **AgentTool**.

---

## Sub-Agents vs AgentTool: The Decision

The ADK offers two ways to compose agents. The key question: **who decides the routing?**

| Pattern | Who routes? | When to use |
| --- | --- | --- |
| Sub-agents | Orchestrator | Fixed pipelines (A → B → C) |
| AgentTool | The LLM | Dynamic “pick the right expert” |

### Sub-agents

Agents listed in a parent's `sub_agents` parameter. Used with `SequentialAgent`, `ParallelAgent`, and `LoopAgent` for **deterministic workflows**. All sub-agents share the same session state — earlier agents write results via `output_key`, later agents read them.

```mermaid
flowchart TB
    subgraph seq["SequentialAgent: incident_triage"]
        direction TB
        subgraph par["ParallelAgent"]
            direction LR
            p1["kafka_status"]
            p2["k8s_status"]
            p3["docker_status"]
            p4["observability_status"]
        end
        sum["Triage Summarizer<br/>(reads all 4 status keys)"]
        jw["Journal Writer<br/>(saves triage_report)"]
        par --> sum --> jw
    end
```

_The incident\_triage SequentialAgent: four health checks run in parallel, then a triage summarizer reads all four status keys, then a journal writer saves the report_

This is the right pattern here. The execution order is **predetermined**. The LLM doesn't decide which health checker to run — all four run in parallel, every time.

### AgentTool

An agent wrapped in `AgentTool()` and placed in the parent's `tools` list. The parent LLM decides **whether and when** to invoke it, just like calling a function.

```
User: "what's the consumer lag on the orders topic?"

Root LLM thinks: this is a Kafka question
Root LLM calls: AgentTool(kafka_health_agent)
                 → kafka_health_agent runs get_consumer_lag("orders")
                 → returns result to root
```

This is the right pattern for specialist agents. The LLM routes based on intent, not a fixed sequence.

---

## The Differences That Matter

| Criterion | Sub-agent | AgentTool |
| --- | --- | --- |
| Routing | Orchestrator (fixed) | LLM (dynamic) |
| State sharing | Full shared session state | Transactional — result forwarded back |
| Reusability | Single parent only (ADK throws `ValueError`) | Can be used by multiple parents |
| LLM context | Descriptions stuffed into context | Structured function signatures |
| Coupling | Tight — structural relationship | Loose — called like a function |

The **single parent constraint** is worth highlighting. In ADK, an agent instance can only be added as a `sub_agent` once. If I ever wanted `kafka_agent` used in another orchestrator, the old approach would throw an error. `AgentTool` has no such limitation.

---

## The Refactoring

Three files changed. Here's the core of it.

### 1. Widen the factory's type hint

The `create_agent()` factory accepted `Sequence[Callable]` for tools. `AgentTool` extends `BaseTool`, not `Callable`, so I widened the type:

```python
# core/ai_agents_core/base.py
from google.adk.tools.base_tool import BaseTool

def create_agent(
    *,
    tools: Sequence[Callable[..., Any] | BaseTool],  # was: Sequence[Callable]
    ...
)
```

And re-exported `AgentTool` from the core package so agents can import it cleanly:

```python
# core/ai_agents_core/__init__.py
from google.adk.tools.agent_tool import AgentTool as AgentTool
```

### 2. Refactor the orchestrator

```python
# agents/devops-assistant/devops_assistant/agent.py
from ai_agents_core import AgentTool, create_agent

root_agent = create_agent(
    name="devops_assistant",
    tools=[
        AgentTool(agent=kafka_agent),        # LLM-routed
        AgentTool(agent=k8s_agent),          # LLM-routed
        AgentTool(agent=observability_agent), # LLM-routed
        AgentTool(agent=docker_agent),        # LLM-routed
        AgentTool(agent=journal_agent),       # LLM-routed
    ],
    sub_agents=[
        incident_triage_agent,  # deterministic workflow — stays as sub-agent
    ],
)
```

The separation is now self-documenting. `sub_agents` = fixed workflows. `tools` = LLM-routed specialists.

### 3. Update the instruction

The root agent's instruction now explicitly distinguishes the two modes:

```
## Structured Workflows (sub-agents)
- incident_triage_agent: Runs a comprehensive health check across
  Kafka, K8s, Docker, and Observability in parallel, then summarizes
  and saves a report.

## Specialist Tools (agent tools)
Call these tools for targeted queries on individual systems:
- kafka_health_agent: Kafka cluster health, topics, consumer groups, lag.
- k8s_health_agent: Kubernetes cluster info, nodes, pods, deployments.
- observability_agent: Prometheus metrics/alerts, Loki logs.
- docker_agent: Docker containers, logs, stats, compose status.
- ops_journal_agent: Notes, past findings, session activity.
```

---

## The Updated Architecture

```mermaid
flowchart LR
    root["devops_assistant<br/>(root orchestrator)"]

    subgraph pipeline["sub-agent — fixed order"]
        direction TB
        it["incident_triage_agent<br/>(SequentialAgent)"]
        hc["health_check_agent<br/>(ParallelAgent)"]
        k1["kafka_health_checker<br/>output_key=kafka_status"]
        k2["k8s_health_checker<br/>output_key=k8s_status"]
        k3["docker_health_checker<br/>output_key=docker_status"]
        k4["observability_checker<br/>output_key=observability_status"]
        ts["triage_summarizer<br/>output_key=triage_report"]
        jw["journal_writer"]
        it --> hc
        hc --> k1 & k2 & k3 & k4
        it --> ts --> jw
    end

    subgraph tools["AgentTool — the LLM picks the specialist"]
        direction TB
        t1["kafka_health_agent"]
        t2["k8s_health_agent"]
        t3["observability_agent"]
        t4["docker_agent"]
        t5["ops_journal_agent"]
    end

    root --> pipeline
    root --> tools
```

_The root orchestrator wires one sub-agent pipeline for incident triage and five AgentTools the LLM routes to dynamically_

---

## How to Choose: A Quick Checklist

Next time you're composing agents in ADK, ask yourself:

| Question | Answer |
| --- | --- |
| Is the execution order predetermined? | `sub_agents` |
| Does the LLM decide which agent to invoke? | `AgentTool` |
| Do agents need to share state via `output_key`? | `sub_agents` |
| Is the agent a self-contained expert? | `AgentTool` |
| Might this agent be reused by other parents? | `AgentTool` |
| Are you building a pipeline (A → B → C)? | `sub_agents` |

If you're mixing both patterns in one orchestrator — as I am — that's fine. Use `sub_agents` for your workflows and `tools` with `AgentTool` for your specialists. The two patterns compose naturally.

---

## What Didn't Change

From the user's perspective: nothing. The same queries hit the same agents and return the same results. The change is structural — correct semantics, better reusability, cleaner separation of concerns.

All 308 tests pass unchanged. The refactoring touched the composition layer, not the tool implementations.

---

## What's Next

With the architecture cleaned up, [Part 6](https://tirraflow.com/posts/building-ai-agents-for-devops-part-6-security-hardening) takes a security pass over every tool — input validation, a confirmation-bypass fix, secret redaction, and role trust. Verifying that the LLM actually routes to the right specialist comes in [Part 8](https://tirraflow.com/posts/building-ai-agents-for-devops-part-8-agent-evaluations) with agent evals.

---

_Source code on_ [_GitHub_](https://github.com/BAHALLA/orrery)_. Architecture decision:_ [_ADR-002_](https://bahalla.github.io/orrery/adr/002-agent-tool-vs-sub-agents/)_. Google's reference:_ [_Sub-agents vs agents-as-tools_](https://cloud.google.com/blog/topics/developers-practitioners/where-to-use-sub-agents-versus-agents-as-tools)_._
