Skip to content

Supervisor pattern

A supervisor agent dispatches to specialist sub-agents (research, math) by including them in tools=[...]. There's no separate create_supervisor primitive — agents-as-tools is the same mechanism. The supervisor's LLM picks which specialist to call (and may call them sequentially or in parallel — that decision is the model's, not the framework's).

The LazyBridge equivalent of LangGraph's create_supervisor recipe.

Source

"""LangGraph multi-agent supervisor (research + math), ported to LazyBridge.

Original (langgraph-supervisor-py README):

    from langchain_openai import ChatOpenAI
    from langgraph_supervisor import create_supervisor
    from langgraph.prebuilt import create_react_agent

    model = ChatOpenAI(model="gpt-4o")

    def add(a: float, b: float) -> float: ...
    def multiply(a: float, b: float) -> float: ...
    def web_search(query: str) -> str: ...

    math_agent = create_react_agent(
        model=model, tools=[add, multiply], name="math_expert",
        prompt="You are a math expert. Always use one tool at a time.",
    )
    research_agent = create_react_agent(
        model=model, tools=[web_search], name="research_expert",
        prompt="You are a world class researcher with access to web search...",
    )
    workflow = create_supervisor(
        [research_agent, math_agent], model=model,
        prompt="You are a team supervisor managing a research expert and a math expert...",
    )
    app = workflow.compile()
    result = app.invoke({"messages": [{"role": "user",
        "content": "what's the combined headcount of the FAANG companies in 2024?"}]})

LazyBridge equivalent: there's no separate ``create_supervisor`` primitive —
specialist agents are simply tools of the supervisor agent. The supervisor's
LLM picks which specialist to call (and may call them sequentially or in
parallel — that decision is the model's, not the framework's).
"""

from lazybridge import Agent, LLMEngine


def add(a: float, b: float) -> float:
    """Add two numbers."""
    return a + b


def multiply(a: float, b: float) -> float:
    """Multiply two numbers."""
    return a * b


def web_search(query: str) -> str:
    """Search the web for ``query`` (stub matching the original example)."""
    return (
        "Here are the headcounts for each of the FAANG companies in 2024:\n"
        "1. **Facebook (Meta)**: 67,317 employees.\n"
        "2. **Apple**: 164,000 employees.\n"
        "3. **Amazon**: 1,551,000 employees.\n"
        "4. **Netflix**: 14,000 employees.\n"
        "5. **Google (Alphabet)**: 181,269 employees."
    )


def main() -> None:
    math_agent = Agent(
        engine=LLMEngine(
            "gpt-4o",
            system="You are a math expert. Always use one tool at a time.",
        ),
        tools=[add, multiply],
        name="math_expert",
        description="Solves arithmetic problems using add/multiply tools.",
    )

    research_agent = Agent(
        engine=LLMEngine(
            "gpt-4o",
            system=("You are a world class researcher with access to web search. Do not do any math."),
        ),
        tools=[web_search],
        name="research_expert",
        description="Looks up current facts via web_search; never does math.",
    )

    supervisor = Agent(
        engine=LLMEngine(
            "gpt-4o",
            system=(
                "You are a team supervisor managing a research expert and a "
                "math expert. For current events, use research_expert. "
                "For math problems, use math_expert."
            ),
        ),
        tools=[research_agent, math_agent],  # agents-as-tools, no special primitive
        name="supervisor",
        verbose=True,
    )

    result = supervisor("what's the combined headcount of the FAANG companies in 2024?")
    print("\nFinal answer:", result.text())


if __name__ == "__main__":
    main()

Walkthrough

  • tools=[research_agent, math_agent] on the supervisor — agents are passed directly. Each specialist's name= becomes the surface tool name the supervisor's LLM sees. No as_tool() call required.
  • Per-agent description= is what the supervisor's model reads when deciding which specialist fits the request. Distinct, precise descriptions are the lever — vague ones cause routing mistakes.
  • Each specialist has its own system prompt scoped to its job ("You are a math expert. Always use one tool at a time."). The supervisor's prompt focuses on dispatch, not domain.
  • The model decides parallelism. When the supervisor's LLM emits two tool calls in one turn, the engine dispatches them concurrently via asyncio.gather — no config knob.

Variations

  • Add a verify=judge on a specialist via agent.as_tool(name="research", verify=judge) to gate every research invocation through a judge-and-retry loop.
  • Promote the supervisor to a Plan if you want explicit ordering or a parallel band — tools=[] is for LLM-decided dispatch; Plan(Step(...)) is for declared dispatch.
  • Cap the supervisor's loop with LLMEngine(max_turns=N) if a bad model decision risks unbounded re-dispatch.

See also

  • As tool — implicit tools=[agent] vs explicit agent.as_tool(...), and when each is right.
  • Researcher → reporter — the previous rung: fixed-order chain instead of LLM-directed dispatch.
  • Plan — the alternative when you want declared ordering instead of LLM choice.