Multi-agent architectures
Four patterns for putting agents together, when to use each, and a full build of Jarvis as a supervisor over four specialist sub-agents. This is the module everything else has been building towards.
1. Four patterns
Almost every multi-agent system in the wild is one of these four shapes (or a combination):
Supervisor
One boss routes each turn to one specialist, then sees its result.
Network
Each agent can hand off to any other. No central authority.
Hierarchical
Supervisors all the way down. Each layer narrows scope.
Swarm
Active agent hands off to another by name, who becomes active.
| Pattern | Best for | Watch out for |
|---|---|---|
| Supervisor | Clear domains, one user-facing voice. Jarvis fits here. | Supervisor latency on every turn. Make it use a fast model. |
| Network | Peer collaboration, e.g. researcher ↔ critic ↔ writer. | Can loop forever. Need stop conditions. |
| Hierarchical | Very large tool surfaces (50+ tools). | Each layer adds latency. Don't over-nest. |
| Swarm | Pipelines where one agent's output naturally hands off (sales → onboarding → support). | Whoever's "active" must know when to hand off. |
We're using the supervisor pattern: a top-level supervisor that hands each user turn to one of four specialists (IT, HR, Calendar, Knowledge). Why supervisor for Jarvis? Clean domain boundaries, one voice to the user, easy to add a fifth specialist later, easy to attach approvals at the supervisor layer.
2. The supervisor, by hand
A supervisor is just an agent whose tools are other agents. The supervisor's LLM picks which sub-agent to call; the call runs the sub-agent's graph; the result comes back as a tool result. That's it.
Two ways to build it: (a) by hand with StateGraph, or (b) using the prebuilt langgraph-supervisor package. We'll show both. By hand first because once you see it, you'll never be confused by the prebuilt.
By-hand supervisor
from typing import Literal
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import create_react_agent
# Each specialist is its own ReAct agent with focused tools.
it_agent = create_react_agent(model=fast_model, tools=it_tools,
prompt="You are the IT specialist for Acme. Use your tools to diagnose and fix issues.")
hr_agent = create_react_agent(model=fast_model, tools=hr_tools,
prompt="You are the HR specialist for Acme. Answer policy & leave questions.")
calendar_agent = create_react_agent(model=fast_model, tools=cal_tools,
prompt="You are the scheduling specialist. Book meetings, find rooms, check availability.")
kb_agent = create_react_agent(model=fast_model, tools=[lookup_policy],
prompt="You are the company knowledge specialist. Answer questions from official docs only.")
SPECIALISTS = {
"it": it_agent,
"hr": hr_agent,
"calendar": calendar_agent,
"knowledge": kb_agent,
}
class Route(BaseModel):
"""Which specialist should handle the next step?"""
next: Literal["it", "hr", "calendar", "knowledge", "FINISH"]
supervisor_model = model.with_structured_output(Route)
SUP_PROMPT = """You are the Jarvis supervisor. Read the conversation and pick the next worker:
- it: hardware/software/network/access issues
- hr: leave, payroll, policies about people
- calendar: scheduling meetings, finding rooms
- knowledge: factual questions answered from Acme's documents
- FINISH: the user's request is fully handled; produce no more steps.
"""
def supervisor_node(state):
decision = supervisor_model.invoke([SystemMessage(SUP_PROMPT)] + state["messages"])
return {"next": decision.next}
def specialist_node(name):
async def node(state):
sub = SPECIALISTS[name]
result = await sub.ainvoke({"messages": state["messages"]})
# Wrap the sub-agent's last message as if a tool returned it:
last = result["messages"][-1]
return {"messages": [AIMessage(content=last.content, name=name)]}
return node
graph = StateGraph(JarvisState)
graph.add_node("supervisor", supervisor_node)
for name in SPECIALISTS:
graph.add_node(name, specialist_node(name))
graph.add_edge(START, "supervisor")
graph.add_conditional_edges("supervisor", lambda s: s["next"],
{**{n: n for n in SPECIALISTS}, "FINISH": END})
for name in SPECIALISTS:
graph.add_edge(name, "supervisor") # back to the boss after each specialist
jarvis = graph.compile(checkpointer=checkpointer, store=store)
import { StateGraph, START, END } from "@langchain/langgraph";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { AIMessage, SystemMessage } from "@langchain/core/messages";
const itAgent = createReactAgent({ llm: fastModel, tools: itTools,
prompt: "You are the IT specialist for Acme. Use your tools to diagnose and fix issues." });
const hrAgent = createReactAgent({ llm: fastModel, tools: hrTools,
prompt: "You are the HR specialist for Acme. Answer policy & leave questions." });
const calendarAgent = createReactAgent({ llm: fastModel, tools: calTools,
prompt: "You are the scheduling specialist. Book meetings, find rooms, check availability." });
const kbAgent = createReactAgent({ llm: fastModel, tools: [lookupPolicy],
prompt: "You are the company knowledge specialist. Answer questions from official docs only." });
const SPECIALISTS = { it: itAgent, hr: hrAgent, calendar: calendarAgent, knowledge: kbAgent } as const;
const Route = z.object({ next: z.enum(["it", "hr", "calendar", "knowledge", "FINISH"]) });
const supervisorModel = model.withStructuredOutput(Route);
const SUP_PROMPT = `You are the Jarvis supervisor. Pick the next worker:
- it / hr / calendar / knowledge / FINISH (when fully handled).`;
async function supervisor(state: any) {
const decision = await supervisorModel.invoke([new SystemMessage(SUP_PROMPT), ...state.messages]);
return { next: decision.next };
}
function specialistNode(name: keyof typeof SPECIALISTS) {
return async (state: any) => {
const result = await SPECIALISTS[name].invoke({ messages: state.messages });
const last = result.messages[result.messages.length - 1];
return { messages: [new AIMessage({ content: last.content, name })] };
};
}
const graph = new StateGraph(JarvisState)
.addNode("supervisor", supervisor);
for (const name of Object.keys(SPECIALISTS)) graph.addNode(name, specialistNode(name as any));
graph
.addEdge(START, "supervisor")
.addConditionalEdges("supervisor", (s: any) => s.next,
{ it: "it", hr: "hr", calendar: "calendar", knowledge: "knowledge", FINISH: END });
for (const name of Object.keys(SPECIALISTS)) graph.addEdge(name as any, "supervisor");
const jarvis = graph.compile({ checkpointer, store });
3. The prebuilt supervisor
Once you've built one by hand, the prebuilt does the same wiring in five lines:
from langgraph_supervisor import create_supervisor
jarvis = create_supervisor(
[it_agent, hr_agent, calendar_agent, kb_agent],
model=model,
prompt=SUP_PROMPT,
).compile(checkpointer=checkpointer, store=store)
import { createSupervisor } from "@langchain/langgraph-supervisor";
const jarvis = createSupervisor({
agents: [itAgent, hrAgent, calendarAgent, kbAgent],
llm: model,
prompt: SUP_PROMPT,
}).compile({ checkpointer, store });
Use the prebuilt for production. Use the by-hand version when you understand it well enough to debug it. Most teams end up with a "by-hand-ish" supervisor because they need custom logic between routes (logging, custom auth checks, custom routing rules).
4. Handoffs — when specialists need to chain
A clean supervisor architecture lets the supervisor route every turn. But sometimes a specialist needs to delegate without bouncing back to the supervisor — e.g. the calendar agent finishes booking a meeting and then hands off to the knowledge agent to fetch the meeting room's WiFi password.
This is the swarm pattern: specialists call a handoff "tool" that switches the active agent.
from langgraph_swarm import create_handoff_tool
to_kb = create_handoff_tool(agent_name="knowledge", description="Hand off to the knowledge agent for factual lookups.")
calendar_agent = create_react_agent(
model=fast_model,
tools=[*cal_tools, to_kb], # adds handoff as another tool
prompt="You are the scheduling specialist…",
)
import { createHandoffTool } from "@langchain/langgraph-swarm";
const toKb = createHandoffTool({ agentName: "knowledge", description: "Hand off to the knowledge agent." });
const calendarAgent = createReactAgent({
llm: fastModel,
tools: [...calTools, toKb],
prompt: "You are the scheduling specialist…",
});
You can mix patterns. Jarvis is supervisor-led at the top, but the calendar specialist can hand off directly to knowledge for a quick lookup without re-asking permission from the boss.
5. Subgraphs — composing graphs as graphs
A compiled graph is just another runnable; you can use it as a node inside a bigger graph. That's what we already did above (each specialist is a graph inside Jarvis), and that's exactly the hierarchical pattern.
Important: by default subgraphs have their own state schema. If the parent state has a messages field and so does the child, LangGraph maps them automatically when fields overlap. For non-overlapping fields, write a small wrapper node that translates.
6. Choosing between patterns — a decision tree
- Total tools < 10? → One ReAct agent. Don't over-architect.
- Tools group into 2–6 clean domains? → Supervisor. (Jarvis.)
- Lots of domains, each itself has many tools? → Hierarchical supervisor. Top-level supervisor routes to mid-level supervisors.
- Sequential pipeline (A then B then C; rarely backwards)? → Swarm with handoffs.
- Peers collaborating on one artifact (write/critique/revise)? → Network.
7. Cost & latency notes
- The supervisor runs an LLM call on every turn. Use a small fast model (Haiku/Mini class) for it, even if specialists use a bigger model.
- Each specialist call is itself a full agent loop — easily 3–6 LLM calls. Plan for 6–15 LLM calls per user turn in a busy supervisor system.
- Use
stream_mode="updates"so the UI shows partial progress ("Routing to IT…"). Users tolerate latency much better when they can see what's happening.
8. Jarvis: end of Module 7
Jarvis is now a real multi-agent system: a supervisor routes each turn to IT / HR / Calendar / Knowledge specialists. Each specialist has its own tools, prompt, and (next module) approval rules. Memory persists across threads and across users. We're 70% of the way to production.
Quick check
1. You have ~20 tools across 4 clean domains and one chat surface. Which pattern?
2. In a supervisor architecture, what role does the specialist's reply play, from the supervisor's perspective?
3. What's a handoff?
4. Where does picking a small fast model save you the most cost?