// lesson: routing

Routing โ€” The Coordinator

Your team has three specialists now, but every task marches through the same fixed pipeline. Real systems add a coordinator: something that looks at the incoming task and decides which agent (or pipeline) should handle it. "Find sources on X" should go straight to the researcher; "tighten this paragraph" needs only the writer; running the full research-write-review machine on either would waste three model calls.

Anthropic's agents article calls this pattern routing โ€” classify the incoming task, then dispatch it to a specialized handler. That classification step splits into two standard implementations:

  1. A classifier LLM call. Ask a model "which of these agents should handle this task? Reply with exactly one name." โ€” an agent whose entire job is dispatch. This is what LangGraph's conditional edges usually wrap and how CrewAI's hierarchical process uses its manager agent.
  2. Deterministic rules. Keyword or pattern matching on the task. No extra API call, no latency, no chance of the router itself hallucinating โ€” and trivially testable.

Production systems very often start with (2), then graduate specific hard cases to (1). We'll build (2) as the graded function, with (1) as a one-liner on top of make_agent for your real app.

The routing table declares each agent's capabilities as keywords, and the router picks the first agent whose capability appears in the task:

def route(task, agents, default):
    haystack = task.lower()
    for name, keywords, agent in agents:
        if any(keyword.lower() in haystack for keyword in keywords):
            return name, agent
    return default

Three deliberate decisions, all of which the tests below pin down:

  • Case-insensitive matching, both sides โ€” users type "Research", your table says "research"; that must not matter.
  • First match wins, in declaration order. When a task matches two agents, the table's order is the tiebreak โ€” deterministic and visible in one place, instead of emergent from dict ordering or scoring.
  • A default you choose explicitly. The interesting design question in any router is what happens when nothing matches. Silently dropping the task is a bug; crashing is rude. Declaring a fallback agent makes the policy explicit โ€” ship a generalist, or an agent whose role is to ask a clarifying question.

Wire it into team.py:

ROUTES = [
    ("researcher", ["research", "find", "sources", "facts"], researcher),
    ("writer", ["write", "draft", "article", "rewrite"], writer),
    ("critic", ["review", "critique", "feedback"], critic),
]

generalist = make_agent(
    "generalist",
    "You are a capable general assistant. Answer directly and concisely.",
    anthropic_client,
)

if __name__ == "__main__":
    while True:
        task = input("task> ").strip()
        if task in ("quit", "exit"):
            break
        if not task:
            continue
        name, agent = route(task, ROUTES, ("generalist", generalist))
        print(f"[routed to {name}]")
        print(agent(task))

Run it. Type "find sources on HNSW indexes" and watch it hit the researcher; type "what's 2+2" and watch it fall through to the generalist. The [routed to ...] line is your friend โ€” routing bugs are invisible without it.

And the LLM-router upgrade, when keyword rules stop being enough? It's an agent like any other:

router = make_agent(
    "router",
    "Classify the task. Reply with exactly one word โ€” researcher, "
    "writer, critic, or generalist โ€” naming the best agent for it.",
    anthropic_client,
)

Call it, match its reply against your table (with the same fallback when it names nobody โ€” models misspell), and you've reproduced the dispatch layer of a hierarchical CrewAI crew. Note that route itself only selects โ€” it returns the agent without calling it. The caller decides what to do with the choice: call it, log it, or route into a whole pipeline. Selection and execution are separate concerns, and the tests enforce that separation.

โ€บ Route the Task

15 pts

Implement route(task, agents, default), where agents is a list of (name, keywords, agent) triples and default is a (name, agent) pair:

  • Return (name, agent) for the first entry (in list order) where any keyword occurs as a case-insensitive substring of task.
  • If no entry matches, return default.
  • Only select โ€” never call any agent.

Log in to submit a solution and earn points.