agents-go
Building agents

Agents & handoffs

The persona, per-agent models and hooks, and transferring control.

An Agent is the persona: instructions, tools, optional per-agent models, and lifecycle hooks. It holds no live connection — one agent is reusable across sessions, and a session can swap which agent is active. Keeping the persona separate from the runtime (AgentSession) is what makes handoffs cheap: you switch the active agent — even its models — without tearing down the session or its transport.

Define an agent

agent, err := agents.NewAgent(agents.AgentOptions{
    Instructions: "You are a friendly booking assistant.",
    Tools:        []tool.Entry{bookTable},
    LLM:          model, // optional per-agent model; else the session default
    OnEnter: func(ctx context.Context, ac *agents.AgentContext) error {
        // Speak first when this agent becomes active (on_enter → generate_reply):
        _, err := ac.GenerateReply(agents.GenerateReplyOptions{
            Instructions: "Greet the caller and offer to book a table.",
        })
        return err
    },
})
if err != nil {
    log.Fatal(err)
}
  • Per-agent models (STT/LLM/TTS/VAD) override the session default while this agent is active — so a handoff can change persona and the underlying model/voice.
  • Hooks: OnEnter / OnExit fire as the agent becomes / stops being active (the hooks a handoff runs); OnUserTurnCompleted runs after a user turn commits, before generation — the place to inspect or augment context (e.g. RAG).

OnEnter can speak first. The AgentContext exposes the same speech family as the session — ac.GenerateReply(...) (LLM reply), ac.Say(...) (fixed text), ac.SayStream(...) (streamed text), and ac.SayAudio(...) (raw PCM) — so you can greet the moment the agent becomes active with the identical API you'd use at the session level (the reference on_entergenerate_reply). These are valid only while the agent is active (from OnEnter or OnUserTurnCompleted); a suspended or already-exited agent's context returns an error instead. On a tool-driven handoff you can alternatively pass a non-nil returns value (below) to trigger a follow-up reply on the new agent.

Handoff — replace the active agent

A handoff transfers control for good — the new agent owns the rest of the conversation. Two ways:

From a tool (graceful drain)

Inside a tool, agents.RequestHandoff transfers control after the tool batch. The current reply isn't cut off — it drains onto the new agent.

transfer, err := tool.New("transfer_to_billing",
    func(ctx context.Context, run *tool.RunContext, _ *struct{}) (*struct{}, error) {
        agents.RequestHandoff(run, billingAgent, "Transferring you to billing.")
        return &struct{}{}, nil
    },
    tool.WithDescription("Hand the caller off to the billing agent."),
)
if err != nil {
    log.Fatal(err)
}

The third argument (returns any, typically a short string) becomes the tool call's output and triggers a follow-up reply on the new agent; pass nil to hand off silently and let the new agent's OnEnter drive the next turn (e.g. by calling ac.GenerateReply(...)).

Two tools in one batch requesting different target agents is a conflict — the switch is ignored and a warning is logged. Request at most one handoff per turn.

Explicitly (hard swap)

Outside a tool — from your own control logic — swap the active agent directly. This is a hard handoff: in-flight speech is cancelled, OnExit/OnEnter run.

if err := session.UpdateAgent(ctx, specialist); err != nil {
    return err
}

Swaps are serialised against session close and tool-driven handoffs, so they never interleave.

Handoff vs. delegate

Handoff is one-way. When you instead need a bounded answer back — collect an address, run one survey step — delegate to a sub-agent and resume; see Tasks & sub-agents.

RequestHandoff

Route/triage from inside a tool, graceful drain. One-way.

UpdateAgent

Hard swap from outside a tool. Cancels current speech. One-way.

DelegateTask

Delegate a bounded job and come back with a value.

A graceful handoff appears in context in examples/supervisor (escalate_to_human).

Next

On this page