Skip to main content

Writing

The Agent-as-Tool Pattern

16 min read

Sequential and parallel pipelines nail the shape of the work up front. Loops let an agent decide when to stop iterating. Coordinators let an agent decide who runs.

The agent-as-tool pattern goes one step further. It hands the LLM a set of callable specialists and lets it decide who runs, in what order, and how many times.

The wiring isn’t new. Both the coordinator and this pattern wrap sub-agents in AgentTool. What changes is what the root is allowed to do. A coordinator dispatches once and synthesises. An agent-as-tool root dispatches, reads the result, decides the work isn’t done, and calls again.

It’s reflection, bolted together out of function calls.

What is the agent-as-tool pattern?

Every sub-agent gets wrapped as an AgentTool and dropped on the root’s tools list. No subAgents. No SequentialAgent. No LoopAgent. The orchestration lives entirely in the root’s instruction. When the root wants to re-run an agent, it calls the tool again.

Three moving parts:

  • Specialist sub-agents. Each one has narrow tools and a focused job. Scout. Validator. Critic.
  • An orchestrator root. An LlmAgent whose instruction spells out the workflow (including the “if this fails, try again” branch).
  • Tool responses, not session state. Sub-agents return their output as tool results. The root reads those results and decides what to do next.

Done properly, you get LLM-driven reflection. The root runs scout, reads the output, runs validator, reads the output. If the validator flags gaps, the root re-runs scout with a sharper request. No EXIT_LOOP. No maxIterations. The root decides when it’s done.

Agent-as-Tool Pattern Root orchestrates specialists via function calling Mission Request LlmAgent (root) SpaceCrewArchitect reads each tool's output, picks the next call no LoopAgent no EXIT_LOOP no maxIterations tools: AgentTool wrappers AgentTool CrewScoutAgent search_people + get_character_details + get_starship AgentTool CrewValidatorAgent get_character_details + get_species call (1..N) shortlist call (1..N) verdict APPROVED Final Crew Manifest Architect re-calls any tool based on intermediate output. NEEDS_REVISION: call scout again with a sharper brief. Iteration is instruction-driven.

Agent-as-tool vs coordinator

The coordinator post already used AgentTool. So what’s actually different?

A coordinator’s job is routing. Read the request, pick the matching pipeline(s), run them once, synthesise. Each sub-agent runs at most once per request. The instruction is about who to call.

An agent-as-tool root’s job is orchestration with feedback. It runs a specialist, reads the output, decides whether the work is done. If it isn’t, it calls another specialist (or the same one again) with a sharper request. The instruction is about how to converge.

Same wiring. Different behaviour, because the instruction asks for different things.

Agent-as-tool vs loop

LoopAgent runs the same set of sub-agents on repeat until EXIT_LOOP fires or maxIterations hits. Iteration is deterministic. The framework caps it.

An agent-as-tool root does the same thing in spirit (call, check, call again) without the scaffolding. The “loop” is implicit, baked into the instruction. That’s weaker in one way (no hard iteration cap) and stronger in another (the root can call different agents each time, and reshape the arguments per iteration instead of re-running the same fixed pair).

Reach for LoopAgent when iteration is symmetric and the same two agents alternate. Reach for agent-as-tool when the next step depends on what the last step returned.

When does this beat the alternatives?

  • The next action depends on the last result. Scout finds 4 of 5 needed roles, and the root asks scout for the missing one specifically. A fixed pipeline can’t decide that.
  • The loop shape varies. The root might call scout twice in a row, or validator twice, or interleave them however the situation needs.
  • Critic feedback is partial. Loops sing with binary verdicts (PASS/FAIL). Agent-as-tool shines when the critic says “4 of 5, missing a mechanic, too many humans” and the root has to act on each specific gap.

How this compares to the rest of the series

Two of the six patterns (coordinator and agent-as-tool) use identical wiring, so the distinction between them lives in the prompt, not the code. That’s the bit that trips people up. Here’s the clean disambiguation.

One line per pattern:

  • Single agent. One LLM, a handful of tools, one task. No orchestration.
  • SequentialAgent. Fixed A then B. The framework enforces the order.
  • ParallelAgent. Fixed fan-out. The framework runs the sub-agents at the same time.
  • LoopAgent. Fixed pair, repeat until EXIT_LOOP or maxIterations.
  • Coordinator. LlmAgent routes to sub-agent tools. Picks who runs. Runs each once.
  • Agent-as-tool. LlmAgent orchestrates sub-agent tools. Picks who runs, how often, with what arguments.

What each pattern lets the LLM decide:

PatternLLM decides
Single agentWhich tool to call next
SequentialAgentNothing about the flow
ParallelAgentNothing about the flow
LoopAgentWhen to exit, via EXIT_LOOP
CoordinatorWho runs (each sub-agent runs at most once)
Agent-as-toolWho runs, how many times, and with what arguments

Coordinator vs agent-as-tool (the confusing pair)

Both wire up the same way:

tools: [
  new AgentTool({ agent: subAgentA }),
  new AgentTool({ agent: subAgentB }),
]

The split shows up in the instruction.

A coordinator’s instruction says: “read the request, pick the matching tool(s), synthesise the results.” Every sub-agent runs at most once per request. The LLM’s job stops at routing.

An agent-as-tool root’s instruction says: “run scout, read the output, run validator, read the output. If the verdict flags gaps, run scout again with a sharper request.” Sub-agents can run multiple times. The arguments change between calls. The LLM’s job continues until the work converges.

Delete Step 4 from the Space Crew Architect, and the architect collapses into a coordinator. Scout, validate, stop. Same code, smaller job.

The quick test

Three questions to pin down which pattern fits:

  1. Is the flow fixed? Yes: pick SequentialAgent or ParallelAgent.
  2. Does the flow iterate on a fixed pair with a binary exit condition? Yes: pick LoopAgent.
  3. Does the flow branch by request shape? Yes, and every branch runs once: coordinator. Sub-agents re-run based on intermediate results: agent-as-tool.

The project

We’re building a Space Crew Architect. Type a mission (a smuggling run, a diplomatic escort, a heist) and the architect assembles a Star Wars crew that fits.

Two specialists:

  • CrewScoutAgent searches SWAPI for characters, infers their crew role (pilot, fighter, leader, mechanic, diplomat, wildcard), and returns a shortlist.
  • CrewValidatorAgent checks the shortlist for role coverage and species diversity, flags gaps, and returns a verdict.

The root orchestrates. Scout, validate, scout again if there are gaps, re-validate, present the final crew. No LoopAgent. The “try again” step is the root’s instruction doing the work.

Click Run to replay a real session for the prompt “Assemble a 5-person crew to smuggle medical supplies through Imperial-controlled space.” The root calls CrewScoutAgent with a refined brief, scout hits SWAPI a handful of times and returns a shortlist, the root hands that shortlist to CrewValidatorAgent, validator approves on the first pass, and the root writes the final manifest. About 48 seconds of model time end-to-end.

$

Validator approving first-pass is the fast path. The section below walks through what happens when it doesn’t.

Setup

package.json:

{
  "name": "agent-as-tool",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "npx adk run --log_level ERROR agent.ts",
    "web": "npx adk web agent.ts"
  },
  "dependencies": {
    "@google/adk": "^0.5.0",
    "zod": "^4.3.6"
  }
}

Same two deps as the rest of the series. SWAPI is free and key-less. Only the Gemini agents need GOOGLE_API_KEY.

The code

agent.ts has 4 tools, 2 specialists, and 1 root. We’ll walk through it piece by piece.

Tools

Four thin SWAPI wrappers. Each one targets a single endpoint and reshapes the response for the agent that uses it.

const searchPeople = new FunctionTool({
  name: "search_people",
  description:
    "Search for Star Wars characters by name. Returns matching characters with name, height, mass, birth_year, gender, and URLs to homeworld, starships, species, and films.",
  parameters: z.object({
    query: z.string().describe("Character name to search for (e.g. 'luke', 'leia', 'han')"),
  }),
  execute: async ({ query }) => {
    const response = await fetch(
      `https://swapi.dev/api/people/?search=${encodeURIComponent(query)}`
    );
    // ...parse, extract id from URL, return list of candidates
  },
});

const getCharacterDetails = new FunctionTool({ /* id -> full profile with resolved homeworld, starships, species */ });
const getStarship = new FunctionTool({ /* id -> ship + resolved pilots */ });
const getSpecies = new FunctionTool({ /* id -> species + resolved homeworld */ });

SWAPI returns URLs all the way down. A character’s starships field is a list of URLs to starships, not ship names. The tools resolve those inner references before handing anything back, so the agents see names, roles, and species. No raw URLs.

The scout

const crewScoutAgent = new LlmAgent({
  name: "CrewScoutAgent",
  model: "gemini-2.5-flash",
  instruction: `You are a Star Wars crew scout...

For each candidate, infer their crew role:
- **Pilot**: Known for flying ships
- **Fighter**: Combat-oriented characters
- **Leader**: Command and strategy
- **Mechanic**: Technical characters
- **Diplomat**: Skilled in negotiation
- **Wildcard**: Unpredictable, brings unique skills

Return a structured list of candidates with name, role, justification, notable ships, species.
Search broadly. Try multiple search queries to find diverse candidates.`,
  tools: [searchPeople, getCharacterDetails, getStarship],
});

Three things to flag.

The scout has its own tools. searchPeople, getCharacterDetails, getStarship. The validator doesn’t get any of them. Same discipline as the sequential post: a scout that can also check species classifications is a scout that sometimes wanders into validation.

No outputKey. This one surprised me. Without outputKey, the scout’s final text answer comes back as the tool’s response value. No session state plumbing in between. The root reads the return value of its tool call like it would for any FunctionTool. Simpler, and it keeps the sub-agents composable without tying them to a named state slot.

The instruction asks for candidates tagged with roles. That’s the shape the validator needs to work against. The output format is engineered for the next specialist in the chain.

The validator

const crewValidatorAgent = new LlmAgent({
  name: "CrewValidatorAgent",
  model: "gemini-2.5-flash",
  instruction: `You are a Star Wars crew validator...

1. **Role Coverage**: A well-balanced crew needs at minimum: Pilot, Fighter, Leader, Mechanic, Wildcard.
2. **Species Diversity**: An all-human crew is functional but less interesting. Use get_species and get_character_details to verify.
3. **Redundancies**: Flag if too many crew members fill the same role without covering others.

Return a validation report:
- Roles that ARE covered (and by whom)
- Roles that are MISSING
- Redundancies or concerns
- Species diversity assessment
- Overall verdict: APPROVED or NEEDS_REVISION with specific suggestions`,
  tools: [getCharacterDetails, getSpecies],
});

The validator gets getCharacterDetails and getSpecies. No searchPeople. It can only assess candidates the scout already proposed. Gap-filling is somebody else’s job (the root’s). That split is deliberate: validator evaluates, scout fills gaps, root picks who runs when.

The verdict comes back as prose. The loop post used a rigid CritiqueSchema with PASS/FAIL; keeping the verdict free-form here lets the validator say “missing a mechanic, and the crew is overwhelmingly human” in one breath. That’s exactly the kind of partial feedback an agent-as-tool root can act on.

The root

export const rootAgent = new LlmAgent({
  name: "SpaceCrewArchitect",
  model: "gemini-2.5-flash",
  instruction: `You are the **Space Crew Architect**...

You have two powerful tools:
- **CrewScoutAgent**: Finds and profiles Star Wars characters, inferring their crew roles
- **CrewValidatorAgent**: Validates that a proposed crew covers all required roles

## Your Process:

### Step 1: Deconstruct the Mission
Analyse the request. Mission type, crew size, constraints.

### Step 2: Scout Candidates
Call **CrewScoutAgent** with the mission details.

### Step 3: Validate the Crew
Call **CrewValidatorAgent** with the scout's candidates.

### Step 4: SELF-CORRECT (CRITICAL!)
If the validator flags missing roles or issues:
- Call **CrewScoutAgent** AGAIN, specifically requesting characters to fill the gaps
- Then re-validate if needed
- Repeat until the crew passes validation

### Step 5: Present the Final Crew
Once validated, present the crew with names, roles, justification, and a lore tidbit per member.

## Rules:
- NEVER present an unvalidated crew
- ALWAYS fill gaps if the validator finds missing roles
- Make it fun and lore-accurate`,
  tools: [
    new AgentTool({ agent: crewScoutAgent }),
    new AgentTool({ agent: crewValidatorAgent }),
  ],
  // NO subAgents, this is the Agent as Tool pattern
});

Three things to call out.

The instruction is the pipeline. Step 1 through Step 5 are the workflow itself. The LLM reads them at runtime and executes each step in order. No SequentialAgent wiring scout to validator. No LoopAgent wiring validator to refiner.

Step 4 is the reflection step. “If the validator flags missing roles, call CrewScoutAgent AGAIN.” That’s an instruction to re-invoke a tool based on the previous tool’s output. The root decides when and how to loop. No framework scaffolding involved.

tools, with AgentTool wrappers. Same call-out as the coordinator post. AgentTool gives the root function-calling semantics. Scout once, validator once, scout again with a sharper query, validator again, all as normal tool calls. With subAgents, the transfer flow is single-hop, so “call scout again” gets awkward.

How the iteration plays out

The smuggling run above validated on the first pass. Scout proposed Han, Chewie, Leia, Lando, and Luke, and those 5 collectively covered pilot, co-pilot, mechanic, fighter, leader, and diplomat. Validator saw full role coverage and an acceptable species mix (4 Humans, 1 Wookiee) and stamped it APPROVED. Step 4 never fired.

That’s the fast path. Here’s what Step 4 looks like when it does fire, say on a tighter brief like “build a 3-person crew for a diplomatic mission to Naboo”:

  1. Root calls CrewScoutAgent with the mission brief. Scout pulls Padmé, Bail Organa, Mon Mothma. Returns a shortlist tagged Diplomat / Leader / Leader.
  2. Root calls CrewValidatorAgent with the shortlist. Validator runs role coverage, returns “NEEDS_REVISION: missing Pilot, Fighter, Mechanic; 3 Humans”.
  3. Root reads the verdict. NEEDS_REVISION kicks Step 4 into gear.
  4. Root calls CrewScoutAgent AGAIN with “need a pilot and a combat-capable member; prefer non-human”. Scout searches, returns a swapped crew with Poe Dameron added and one senator dropped.
  5. Root calls CrewValidatorAgent AGAIN with the updated list.
  6. Validator returns APPROVED. Root jumps to Step 5 and writes the final manifest.

Under the hood, every call to CrewScoutAgent is one tool call from the root’s perspective. The sub-agent runs its own internal tool-using loop (search, get details, return), finishes, and hands its answer back as the tool’s return value. The root sees a string of tool responses, just like it would with a plain FunctionTool. The difference is that each response is the output of a small specialist agent rather than a SWAPI wrapper.

Iteration count isn’t fixed. Specific briefs (the smuggling run above spelled out “pilot, navigator, combat, mechanic, negotiator”) tend to validate first-pass. Vague or constrained briefs take 2 or 3 scout/validate cycles. The loop exits when the validator says APPROVED. If the root gets stuck, it eventually stops and returns what it has. (No maxIterations to pull the handbrake, so watch your API bill.)

Why not just use a LoopAgent?

Fair question. The pattern looks like a loop: scout, validate, scout, validate. Why not wrap scout and validator in LoopAgent and be done?

The loop shape varies. LoopAgent alternates its sub-agents in a fixed order. Here the root sometimes calls scout twice in a row (to seed multiple search terms) before calling validator. A rigid scout/validate alternation doesn’t match the real flow.

Follow-up calls need different arguments. The second scout call is built from the validator’s feedback: “search for a diplomat to fill the missing role”. A generic “scout again” wouldn’t cut it. LoopAgent has no clean way to reshape the sub-agent’s input between iterations. With agent-as-tool, the root passes whatever prompt it wants each time.

The exit condition isn’t binary. The validator returns a list of gaps. The root acts on each gap directly. LoopAgent with a PASS/FAIL critic is the right call when the verdict is binary (that’s what the playlist curator used).

The trade-off: with LoopAgent you get a hard iteration cap and a clear framework-level signal when the loop exits. With agent-as-tool you get flexibility and have to trust the root’s instruction. If the root gets confused, maxIterations won’t save you. You have to fix the prompt.

Why not just hand the root all the tools directly?

Same argument as the coordinator post, worth repeating.

  • Tool count blow-up. Scout has 3 tools. Validator has 2. A root with all 5, plus the responsibility to do role inference and validation inline, ends up with a prompt that reads like a manual.
  • Mixed concerns. Role inference and role coverage checking are different mental models. Splitting them across agents keeps each prompt tight.
  • Reflection becomes visible. When the architect calls scout twice, you can see it in the trace: first scout call, validator call, second scout call with a narrower query. With everything inlined, the model’s reasoning gets buried and you’re guessing why it picked the characters it did.
  • Sub-agents are reusable. Once CrewScoutAgent exists as a unit, it drops into a SequentialAgent, a ParallelAgent, a LoopAgent, or another agent-as-tool root. Inlined tools can’t compose.

When to reach for this pattern

  • The pipeline is dynamic. The number of scout calls depends on the first scout’s output. The number of validator calls depends on whether the first pass approved.
  • Feedback is partial. The critic returns a list of gaps. The orchestrator fills them one at a time.
  • Specialists are reusable. Each sub-agent has its own job and its own tools, and you’d want to reuse them in other pipelines.

Skip it when the pipeline is fixed (use SequentialAgent or ParallelAgent), when iteration is deterministic and bounded (use LoopAgent), or when a single agent with 2 tools can handle it without losing clarity.

Wrapping up

Agent-as-tool is the most LLM-driven pattern in the series so far. Every other pattern offloads some of the orchestration to the framework. SequentialAgent fixes the order. ParallelAgent forces the fan-out. LoopAgent manages iteration. Coordinator dispatches once and stops. Agent-as-tool hands almost everything to the root’s instruction: when to call, how often, with what arguments, and when to stop.

That’s a lot of responsibility on one prompt. In exchange, you get a system that can reflect on partial feedback and course-correct without framework scaffolding.

Single, sequential, parallel, loop, coordinator, agent-as-tool. 6 patterns, enough to build most agentic systems. Next up: stacking them. A root coordinator that routes to an agent-as-tool pipeline for one kind of request and a loop for another.