Vineet Ganti
Agents that work together: supervisor, agent as tool, graph and swarm
All posts

AI & LLMs/Agentic AI · Part 4 of 8

·23 min read

Agents that work together: supervisor, agent as tool, graph and swarm

A first-timer's guide to multi-agent patterns with Strands and Groq: a supervisor with workers, an agent used as a tool, a graph with explicit routing, and a swarm that hands off work, with the lessons each one teaches.

Agentic AILLMsAI EngineeringStrands

So far in this series one agent did everything. In my first agent a model, a tool and a loop were enough to act. In giving agents tools that grew to several tools, and in how agents remember the agent gained memory. This post is about what happens when one agent is not enough: several agents, each with one job, working on the same problem.

All four examples use a model hosted on Groq and the Strands SDK, as before. Every pattern comes with a diagram first, then the code. Long outputs are trimmed to a few lines, marked with ….

#Why split one agent into many?

One big agentSeveral small agents
One long prompt that covers every jobOne short prompt per job, so each one is easier to get right
Every tool is visible to the model, so it can pick the wrong oneEach agent sees only its own tools
Everything lands in one context windowEach agent has its own context, and only results travel between them
Hard to test one partEach agent can be tested alone

The price: more model calls, more cost and more places for something to go wrong. Use several agents when the jobs are really different, not just to look sophisticated.

PatternWho decides the next stepBest forNotebook
Supervisor + workersA supervisor agentA question that needs several kinds of researchMarket research
Agent as toolAn orchestrator agentRouting a question to the right expertTutoring assistant
GraphYour Python codeFixed, auditable proceduresOrder processing
SwarmThe agents themselvesCreative, back-and-forth workGame level design

#The setup every example shares

Each notebook starts with the same model setup, so it is worth reading once.

python
import os
from dotenv import load_dotenv
from strands.models.openai import OpenAIModel

load_dotenv()  # reads GROQ_API_KEY from a .env file, if present

MODEL_ID = os.getenv("GROQ_MODEL_ID", "openai/gpt-oss-120b")

model = OpenAIModel(
    client_args={
        "api_key": os.environ["GROQ_API_KEY"],
        "base_url": "https://api.groq.com/openai/v1",  # Groq speaks the OpenAI protocol
        "max_retries": 5,                               # retry dropped connections
    },
    model_id=MODEL_ID,
    # Reasoning models stream their thinking by default; hide it so only the answer prints.
    params={"extra_body": {"include_reasoning": False}},
)

Two details that are easy to miss:

  • OpenAIModel works for Groq because Groq offers an OpenAI-compatible API. Changing base_url is all it takes to point at another provider or a local server.
  • callback_handler=None (used on every worker below) stops an agent from printing its own streaming output. Without it, a worker's whole answer would be printed in the middle of the supervisor's work.

#Pattern 1: Supervisor and workers

A market-research assistant. A supervisor splits the question and delegates to three workers, then writes one recommendation.

Request or acknowledgmentData

WorkerJobTools
News agentRecent news and developmentsTavily web search
Financial agentPrices, history, fundamentalsyfinance functions: price, history, financials
Sentiment agentWhat investors are sayingTavily web search

#Workers are agents with a name and a description

python
news_agent = Agent(
    name="news_agent",
    description="Researches recent news and developments about companies and industries.",
    model=model,
    system_prompt="You are a news research specialist. ... Make at most two web_search calls.",
    tools=[web_search],
    callback_handler=None,
)

The name and description matter here. When the supervisor sees its tools, those two fields are all it has to decide which one fits the job.

#The supervisor wraps them with as_tool()

python
supervisor = Agent(
    name="research_supervisor",
    model=model,
    system_prompt="""You are a market research coordinator. ...
Do not ask the user clarifying questions: pick sensible defaults and delegate straight away.""",
    tools=[news_agent.as_tool(), financial_agent.as_tool(), sentiment_agent.as_tool()],
)

response = supervisor("Should I invest in electric vehicle companies?")

agent.as_tool() turns a worker into something the supervisor can call like any other tool, so the idea from the tools post carries over unchanged: a tool is just something with a name, a description and an input.

#What the run looked like

The supervisor called news_agent twice, and the notebook printed each Tavily search as a large boxed table, which I have cut down to this:

text
Tool #1: news_agent
DEPRECATION WARNING: tavily is deprecated ... use the official Tavily MCP server
  Query: Tesla BYD Nio Rivian Lucid Ford GM electric vehicle news week
  Results: 3 found   [1] EV news site (2021 posts)  [2] BYD video page  [3] InsideEVs: Rivian vs Lucid earnings
Unexpected error in tavily_search: closing tag '[/caption]' doesn't match any open tag
agent invocation failed: tool call validation failed: attempted to call tool 'open_file'
                         which was not in request.tools

Tool #2: news_agent
  Query: Tesla news week electric vehicle developments ... October 2026
  Results: 3 found   [1] EVWorld headlines (Oct 2026)  [2] Roadster interface  [3] Tesla hiring in Vietnam

Three lessons from that short trace:

  • The first search returned 2021 pages. A recent-news query can still return old content, so a worker should check dates, and the prompt should say so.
  • A parser error inside a tool and a model that called a tool it didn't have (open_file) both failed one worker call, not the whole run. The agent was invoked again and carried on. Failures inside workers are normal, so a supervisor should be able to survive one.
  • Warnings are worth reading. The Tavily tool is deprecated, and the message names the replacement (the Tavily MCP server; the next post explains MCP).

#Pattern 2: Agent as tool

The simplest multi-agent idea. A teacher receives a student's question and hands it to the right specialist: a math tutor, a writing coach or a science helper.

Request or acknowledgmentData

python
math_tutor = Agent(
    name="math_tutor",
    description="Solves mathematical equations and explains math concepts step by step",
    model=model,
    system_prompt="You are a math tutor. Solve math problems and explain your solutions clearly.",
    callback_handler=None,
)
# writing_coach and science_helper are built the same way

teacher = Agent(
    model=model,
    system_prompt="""You are a teacher who helps students by delegating to specialist tutors.
Never solve problems yourself. Always delegate to the appropriate specialist: ...
After receiving the specialist's response, present it to the student in a friendly way.""",
    tools=[math_tutor, writing_coach, science_helper],   # Strands wraps each agent as a tool
)

teacher("Can you help me solve this quadratic equation: x^2 + 5x + 6 = 0?")

Here the agents go straight into tools=[...], with no as_tool() call. Strands wraps them for you. In the supervisor example, as_tool() was written out, which is the same thing done by hand and lets you customise it.

QuestionTool the teacher called
A quadratic equationmath_tutor
How to write a thesis statementwriting_coach
Why does ice float?science_helper

Each answer arrived as a long, formatted lesson, so only the shape is kept here:

text
Tool #1: math_tutor      → steps: identify a, b, c → factor → roots x = −2 and x = −3 → quick check
Tool #2: writing_coach   → a table of what makes a thesis effective → pick an angle → a template → checklist
Tool #3: science_helper  → density → hydrogen bonds → open crystal lattice → ice is less dense, so it floats

Why this is called agent as tool:

  • The name becomes the tool name and the description tells the teacher when to call it. A vague description means wrong routing.
  • The teacher stays in control. The specialists never talk to the student or to each other. They return text to the teacher, and the teacher is the only voice the student hears.
  • Each specialist has its own prompt and its own context, so the math tutor's reasoning doesn't clutter the writing coach's.

#Pattern 3: Graph, explicit routing in code

An order-processing system, where every order must follow an auditable path. A graph is made of nodes (agents) and edges (the arrows between them). Some edges have a condition: a plain Python function that returns True or False, deciding whether the edge is followed.

Data

#Make the model's decision easy to read

The risk analyzer and the inventory agent end their answer with a fixed final line:

text
RISK_CATEGORY: <HIGH_VALUE|SUSPICIOUS|LOW_RISK>
STOCK_STATUS: <IN_STOCK|OUT_OF_STOCK>

The routing code reads only that line, so an explanation that merely mentions another category can't send the order the wrong way.

python
import re

def decision(state, node_id: str, label: str) -> str:
    """Return the value of the final 'LABEL: VALUE' line written by a node."""
    text = str(state.results.get(node_id, ""))
    matches = re.findall(rf"{label}\s*:\s*([A-Z_]+)", text.upper())
    return matches[-1] if matches else ""      # the LAST match wins

def is_high_value(state):
    return decision(state, "risk_analysis", "RISK_CATEGORY") == "HIGH_VALUE"
# is_suspicious, is_low_risk, is_in_stock and is_out_of_stock follow the same pattern

#Build it

python
from strands.multiagent import GraphBuilder

builder = GraphBuilder()

builder.add_node(risk_analyzer, "risk_analysis")      # add_node(agent, node_id)
builder.add_node(manual_review_agent, "manual_review")
builder.add_node(fraud_detection_agent, "fraud_check")
builder.add_node(inventory_agent, "inventory_check")
builder.add_node(shipping_agent, "shipping")
builder.add_node(refund_agent, "refund")

# conditional edges out of the risk analysis
builder.add_edge("risk_analysis", "manual_review",   condition=is_high_value)
builder.add_edge("risk_analysis", "fraud_check",     condition=is_suspicious)
builder.add_edge("risk_analysis", "inventory_check", condition=is_low_risk)

# unconditional edges
builder.add_edge("manual_review", "inventory_check")
builder.add_edge("fraud_check",   "inventory_check")

# inventory decides: ship or refund
builder.add_edge("inventory_check", "shipping", condition=is_in_stock)
builder.add_edge("inventory_check", "refund",   condition=is_out_of_stock)

builder.set_entry_point("risk_analysis")
graph = builder.build()

#What each node receives

A node doesn't see the whole run. The entry node gets the original order text. Every other node gets the original order plus the outputs of the nodes that led to it. That is how the inventory agent knows what the risk analyzer decided without anyone passing it along by hand, and it is also why each node's output should be short: it becomes part of the next node's request.

#Three orders, three paths

graph("Order #12345: ...") returns a result with a status, the nodes in the order they ran, and each node's output. A small helper prints the path and the last step:

python
def show_path(result):
    path = " → ".join(node.node_id for node in result.execution_order)
    print(f"Status: {result.status}\nPath:   {path}")
    last = result.execution_order[-1].node_id
    print(f"\nFinal step ({last}):\n{result.results[last].result}")
OrderPath taken
$1,500 laptop, existing customer, matching addressesrisk_analysis → manual_review → inventory_check → shipping
$45 book, returning customer with 50+ ordersrisk_analysis → inventory_check → shipping
$800 limited-edition headphones, account 5 minutes old, different shipping countryrisk_analysis → fraud_check → inventory_check → refund

All three finished with Status.COMPLETED. The last step's output was a long confirmation, so here is the refund one, trimmed:

text
Refund Confirmation – Order #11111
- Item: Limited edition headphones   - Amount: $800   - Reason: out of stock
- What's next: payment reversal in 3–5 business days, a confirmation email, a reference number

Two things to notice. The suspicious order never reached manual review, because the edge conditions sent it to fraud check. And the inventory prompt treats the words "limited edition" as out of stock, which is why that order ended in a refund.

StrengthWhy
Explicit routingEvery path is visible in code
Auditable"How do you handle fraud?" can be answered by pointing at the graph
AcyclicEdges only point forward, so every order reaches an end state

Use it when you have an established procedure (compliance, approvals, order processing). Don't use it when the path can't be predicted up front.

#Pattern 4: Swarm, agents that hand off to each other

A game-level design team with four agents: a level designer, a game designer (playtests), a narrative designer and a difficulty balancer. There is no central coordinator. Each agent does its part and then decides who should go next.

Each agent's prompt says what to do, what not to do, and who to hand off to:

python
level_designer = Agent(
    name="level_designer",
    model=model,
    callback_handler=None,
    system_prompt="""You are a Level Designer in a game design swarm.
Focus ONLY on layout and visual design. Do NOT write narrative or balance difficulty.
After completing your layout, hand off to narrative_designer ... and difficulty_balancer ...""",
)
# game_designer: playtests, hands issues to the right specialist, gives the final summary
# narrative_designer, difficulty_balancer: same shape

The "Focus ONLY on ..." line keeps each agent in its lane. Without it, every agent tries to do the whole job.

#Create the swarm, with safety limits

python
from strands.multiagent import Swarm

swarm = Swarm(
    [level_designer, game_designer, narrative_designer, difficulty_balancer],
    entry_point=level_designer,                 # who starts
    max_handoffs=20,                            # stop runaway loops
    max_iterations=20,
    execution_timeout=1200.0,                   # seconds for the whole swarm
    node_timeout=300.0,                         # seconds per agent turn
    repetitive_handoff_detection_window=8,      # look at the last 8 handoffs...
    repetitive_handoff_min_unique_agents=3,     # ...and require at least 3 different agents
)

The swarm gives every agent one extra tool and a shared context: the original task, which agents have worked on it, what each one contributed, and the list of agents it can hand off to. The extra tool looks like this:

python
handoff_to_agent(
    agent_name="difficulty_balancer",
    message="Patrol patterns too predictable - need more variation",
)

#What happened

The notebook turned on debug logging for strands.multiagent, which printed about 30 lines. The useful part is the handoff chain, which a short loop over the result prints:

python
for node in result.node_history:      # every agent turn, in order
    print(f"  → {node.node_id}")
text
→ level_designer → difficulty_balancer → game_designer → difficulty_balancer → game_designer

Status: COMPLETED | iterations: 5 | time: 154s
  • The run ended on its own: the game designer finished without handing off, and the swarm treats "no handoff" as "done".
  • The level designer named two possible next agents (narrative and difficulty), and the swarm went to the difficulty balancer. Only one agent runs at a time.
  • The difficulty balancer and game designer passed the work back and forth once, which is the kind of loop the limits exist to cap.
  • The final answer was a long play-test report. Here is its outline:
text
"Derelict Echo": stealth mission in an abandoned space station
1. Gameplay feel: three zones (Docking Bay, Engineering Core, Bio-Lab), playstyles: aggressive, patient, ghost
2. Narrative: why the station was abandoned, told through logs and environment
3. Difficulty curve: guards, cameras, puzzle steps and time limits grow per zone
4. Verdict: all three playstyles viable   5. Summary for production

#Which pattern should I use?

Supervisor / agent as toolGraphSwarm
ControlOne agent in chargeYour codeShared between agents
Predictable?MostlyFullyNot at all
Auditable?By reading the tool callsBy reading the graphBy reading the handoff chain
Cost and speedModerateLowestHighest
Failure modeWrong specialist pickedA wrong label breaks the routeEndless handoffs
Use forResearch, routing questionsCompliance, approvals, ordersCreative, iterative work

#What each agent sees

The patterns also differ in what travels between agents, and that decides both what a worker can know and how big its requests get.

PatternA worker receivesWhat it sends back
Supervisor / agent as toolOnly the request the coordinator writes for it, as the tool's inputIts answer, as a tool result to the coordinator
GraphThe original task plus the outputs of the nodes that led to itIts result, stored under its node id for the conditions to read
SwarmThe original task, the handoff message and the shared context of everyone's workA handoff to the next agent, or the final answer

A swarm agent sees the most, and its context grows with every turn. That is the other half of why a swarm is the slowest and most expensive pattern.

#Things that can trip you up

  • Vague names and descriptions. The name and description are all a supervisor sees. "Helper" tells it nothing.
  • The orchestrator answers itself. Say "never solve problems yourself, always delegate" in its prompt.
  • The orchestrator asks the user instead of working. Tell it to pick sensible defaults.
  • Routing on free text. Make the model end with a fixed LABEL: VALUE line, and read the last match only.
  • Unbounded runs. Set max_handoffs, max_iterations and timeouts on a swarm, and a limit on any graph that could loop.
  • Context size limits. On Groq's free tier a request over 8,000 tokens fails, and each tool result is sent again with the next request. Cap results. There is a daily token budget too, which post 5 runs into.
  • Prompts that overlap. If two agents both think a job is theirs, they'll both do it. Say what each one must not do.
  • Not reading failures. A tool can fail inside a worker while the run carries on, so read the trace.

#Summary

Several small agents beat one big one when the jobs differ, but someone has to decide who does what. A supervisor or an agent used as a tool keeps one agent in charge: the others are tools with a name, a description and their own prompt, and their answers go back to the boss. A graph moves the decision into your code: agents are nodes, edges are arrows, and conditions are Python functions that read a fixed label from the previous answer, which makes every path auditable. A swarm gives the decision to the agents: each gets a handoff tool and a shared context, the path emerges from the work, and limits keep it from looping.

#Concepts at a glance

ConceptIn one lineWhy it matters
Multi-agent systemSeveral agents, each with one jobSmaller prompts, fewer tools per agent
Name and descriptionWhat a coordinator sees of a workerThey decide whether it gets called
as_tool()Wraps an agent so another agent can call itThe bridge from agents to tools
SupervisorSplits a question, delegates, combinesResearch that needs several skills
Agent as toolA teacher routes one question to an expertThe simplest pattern
callback_handler=NoneSilences a worker's own printingKeeps the output readable
Token capLimit results and searches per workerAvoids Groq's 8,000-token request limit
GraphNodes (agents) and edges (arrows)Fixed procedures
ConditionA Python function on the previous node's outputRouting lives in code, not in the model
Final label lineRISK_CATEGORY: ...Reliable parsing of a model's decision
AcyclicEdges only point forwardEvery run reaches an end
Node inputThe original task plus the outputs of the nodes that led to itHow a graph passes results along
SwarmAgents hand control to each otherCreative and iterative work
handoff_to_agentThe tool that passes control onHow a swarm moves
Shared contextThe task, who worked on it and what each one addedEvery swarm agent sees it, so it grows each turn
Safety limitsMax handoffs, iterations, timeouts, repeat detectionStops infinite loops and surprise bills

#Test yourself

What does a supervisor know about its workers?

Only their names and descriptions (and what they return). That is why both must be specific.

What is the difference between a supervisor and a graph?

In a supervisor, a model decides which worker to call next. In a graph, your code decides, through edges and conditions.

Why does the graph read a final LABEL: VALUE line instead of searching the whole answer?

An explanation might mention another category in passing. Only the last labelled line is the real decision, so reading just that line keeps the routing correct.

Which path does a suspicious order for an out-of-stock item take?

risk_analysis → fraud_check → inventory_check → refund.

What does a node in the graph receive as input?

The entry node gets the original task. Every other node gets the original task plus the outputs of the nodes that led to it.

In a swarm, how does the run end?

An agent finishes without calling handoff_to_agent. The limits (max handoffs, iterations, timeouts, repeat detection) stop it if it never does.

Why was each worker's search capped at 3 results and 2 searches?

Groq's free tier rejects a request over 8,000 tokens, and every tool result is sent again with the next request, so uncapped searches overflowed the limit.

When would you choose a graph over a swarm?

When the procedure is known and has to be auditable, such as compliance, approvals or order processing. A swarm suits work where the next step depends on what turns up.

Next: How agents talk, where tools and agents move into other programs: MCP for tools, A2A for other agents.

#Resources