agents-go
Building agents

Supervisor pattern & scaling

Keep one agent in control while it routes work to specialist tasks — and scale its tools without overwhelming the model.

Most agents start as one persona with a handful of tools. That is the right place to start. This page is about what to reach for when one prompt is no longer enough — when instructions bloat, tools conflict, or a step needs its own multi-turn conversation. The answer is rarely "a bigger prompt"; it is composing small, focused pieces.

There are four building blocks, and the skill is picking the smallest one that fits.

Choosing a pattern

PatternUse it whenWhyCost
Single agent + toolsOne set of instructions and a modest tool list does the job.Simplest thing that works; no control transfer, no latency.Instructions and tool lists grow unwieldy past a point.
Tool searchThe tool list is large (dozens to hundreds).The model sees one tool_search, not everything; it finds tools by intent.A search round-trip before the right tool is callable.
Supervisor + tasks (this page)One agent should stay in control while delegating focused, multi-turn work.The supervisor keeps the whole conversation; specialists own narrow jobs and return typed results.More moving parts; each task is its own LLM loop.
HandoffOne agent's role is done and another should take over.Clean role boundary; the new agent owns the rest.One-way — the original agent does not come back.
Task groupAn ordered, multi-step flow where users may revisit earlier steps.Built-in sequencing and regression for scripted intake.Rigid; not for model-driven routing.

These combine. An intake supervisor might collect details with tasks, then hand off to a billing agent that runs its own supervisor.

The supervisor pattern

A supervisor keeps one agent in long-lived control of the session and routes discrete work to specialist tasks. The supervisor decides when each task runs, reads back its result, and continues the conversation. Each task is independent — its own instructions, tools, and LLM loop — so the supervisor coordinates a set of focused sub-agents instead of reasoning about everything at once.

It has three parts:

  • The supervisor — a long-lived Agent whose instructions name each specialist, say when to invoke it, and say how to read back its result.
  • The specialists — one or more AgentTask[T] values, each with focused instructions, its own tools, and a typed result.
  • The delegation surface — how a specialist is started. The common choice is a function tool on the supervisor: its body starts a task with agents.DelegateTask, awaits the typed result, and returns a short summary to the model.

Tools and tasks are different things. A tool is plain code that runs one step — call an API, write a row, start a task. A task is a sub-conversation with its own LLM loop that can itself call tools. Rule of thumb: if the model needs to ask clarifying questions, the work is a task; if it is one call with arguments, it is a tool.

A routing supervisor

The supervisor below handles two request types by routing to specialist tasks. lookup_order collects an order number and returns its status; update_address collects and confirms a new address. Each is a function tool that starts a task, awaits it, and hands the result back to the model.

// A specialist: collects an order number, completes with a typed result.
func newLookupOrderTask(ctx context.Context) (*agents.AgentTask[OrderLookup], error) {
    task, err := agents.NewAgentTask[OrderLookup](agents.AgentTaskOptions{
        AgentOptions: agents.AgentOptions{
            Instructions: "Ask the customer for their order number.",
            // Speak first when the task takes over (on_enter → generate_reply):
            OnEnter: func(_ context.Context, ac *agents.AgentContext) error {
                _, err := ac.GenerateReply(agents.GenerateReplyOptions{
                    Instructions: "Ask for the order number.",
                })
                return err
            },
        },
    })
    if err != nil {
        return nil, err
    }

    // Task-internal tool: the task's own LLM calls this once it has the number.
    // Complete ends the task and hands the typed value back to the supervisor.
    collected, err := tool.New("order_number_collected",
        func(_ context.Context, _ *tool.RunContext, in *orderNumberIn) (*none, error) {
            return &none{}, task.Complete(OrderLookup{OrderID: in.OrderID, Status: "shipped"})
        },
        tool.WithDescription("Call when the customer has provided their order number."),
    )
    if err != nil {
        return nil, err
    }
    return task, task.UpdateTools(ctx, []tool.Entry{collected})
}

// The supervisor's routing tool: start the task, await it, return a summary.
lookupOrder, err := tool.New("lookup_order",
    func(toolCtx context.Context, run *tool.RunContext, _ *noArgs) (*summary, error) {
        task, err := newLookupOrderTask(toolCtx)
        if err != nil {
            return nil, err
        }
        order, err := agents.DelegateTask(toolCtx, run, task) // blocks until Complete
        if err != nil {
            return nil, err
        }
        return &summary{Text: fmt.Sprintf("Order %s is %s.", order.OrderID, order.Status)}, nil
    },
    tool.WithDescription("Use when the customer wants to check the status of an order."),
)
if err != nil {
    log.Fatal(err)
}

DelegateTask hands control to the task: the supervisor is parked (its speech locked against interruption), the task becomes the active agent and runs its own turns, and when its tool calls Complete, control returns to the supervisor with the typed value. Takeovers nest — a task can delegate to another — and unwind LIFO.

Because the specialist's OnEnter calls GenerateReply, it prompts the caller the moment it takes over ("What's your order number?") rather than waiting for the next turn — the reference on_entergenerate_reply behaviour.

The full runnable version, including an escalate_to_human handoff, is examples/supervisor.

Designing a good supervisor

  • Size the pieces. Deterministic single steps are tools. Multi-turn, reasoning-heavy sub-conversations are tasks. The supervisor holds the conversational frame and routing — keep domain reasoning out of it.
  • Write routing instructions precisely. Name each specialist tool and say when to use it; routing quality depends on these descriptions. Say how to interpret each result ("after lookup_order returns, summarize the status and ask if they want changes").
  • Validate results. A typed result is still untrusted input — check the values before continuing, and define a recovery path for bad or empty output.

History is shared across the supervisor and its tasks — there is one AgentSession history, not a private transcript per agent (see Chat context). Seeding a task's ChatContext adds to the shared conversation rather than scoping to that task.

Next

On this page