agents-go

Handoffs

Handoffs let an agent delegate the rest of the run to another agent. The model sees each handoff as a tool named transfer_to_<agent_name>; when it calls one, the runner switches the active agent and continues the loop with the full conversation.

Creating a handoff

billing := &agents.Agent{Name: "Billing agent", Instructions: agents.StaticInstructions("…")}
refund := &agents.Agent{Name: "Refund agent", HandoffDescription: "Handles refund requests end to end.", Instructions: agents.StaticInstructions("…")}

triage := &agents.Agent{
	Name:     "Triage agent",
	Handoffs: []agents.Handoff{agents.HandoffTo(billing), agents.HandoffTo(refund)},
}

agents.HandoffTo(target) builds a no-input tool named transfer_to_<sanitized name> whose description includes the target’s HandoffDescription.

Customizing a handoff

For a custom tool name, an input schema, side effects or dynamic targets, build the Handoff struct directly:

type escalationInput struct {
	Reason string `json:"reason" jsonschema:"why the conversation is being escalated"`
}

schema, _ := agents.SchemaFor[escalationInput](true)

h := agents.Handoff{
	ToolName:        "escalate_to_human_review",
	ToolDescription: "Escalate the conversation for human review.",
	InputJSONSchema: schema,
	AgentName:       escalation.Name,
	Target:          escalation,
	OnHandoff: func(ctx context.Context, rc *agents.RunContext, argsJSON string) error {
		var in escalationInput
		_ = json.Unmarshal([]byte(argsJSON), &in)
		log.Printf("escalating: %s", in.Reason)
		return nil // an error here aborts the run
	},
}

A handoff whose target depends on the arguments sets OnInvoke instead of Target — it runs when the model selects the handoff and its return value is the agent switched to. Leave Target nil in that case: it is the static declaration, and a consumer enumerating the handoff graph (an approval UI rebuilding an agent registry, say) trusts it without invoking any callback.

A hand-built Handoff is strict by default — the zero value of NonStrictSchema opts in to strict mode. Set NonStrictSchema: true only for a schema strict mode cannot express.

Field Purpose
ToolName / ToolDescription What the model sees
InputJSONSchema / NonStrictSchema Optional typed handoff input (strict by default)
Target The agent switched to, as a static declaration (HandoffTo fills it)
OnInvoke Resolves the target at runtime; overrides Target when set
OnHandoff Side-effect callback when the handoff fires (e.g. prefetch data)
InputFilter Rewrites the conversation the next agent sees (below)
IsEnabled Gates whether the handoff is offered to the model this run

A Handoff with neither Target nor OnInvoke has no one to switch to; selecting it fails the run with a *UserError.

Input filters

By default the next agent sees the entire conversation. An InputFilter rewrites it — for example to drop earlier tool noise before delegating:

h := agents.HandoffTo(faq)
h.InputFilter = func(d agents.HandoffInputData) agents.HandoffInputData {
	d.InputHistory = removeToolItems(d.InputHistory)
	return d
}

HandoffInputData.InputHistory is the full conversation as input items, up to and including the handoff. The filter affects only what the next agent sees — what is saved to a session is unaffected.

Note: the filter receives one flattened InputHistory list, not a pre/post split — a filter that needs the boundary can find it by identity.

Nesting handoff history

For multi-agent chains, agents.NestHandoffHistory is a ready-made filter that folds the prior conversation into one compact summary message for the next agent, cutting tokens and tool-call noise:

h := agents.HandoffTo(billing)
h.InputFilter = agents.NestHandoffHistory(agents.NestHistoryOptions{})

The default folds the transcript into a single assistant message wrapped in fixed <CONVERSATION HISTORY> markers. On a later handoff the filter flattens any earlier summary back into its transcript before re-folding, so a chain of handoffs yields one flat summary rather than a summary-of-summaries. Customize via NestHistoryOptions:

The transcript is serialized one JSON item per line, which round-trips through UnmarshalInputItem when flattened — a line-delimited format nests reliably, where free text does not.

Models follow handoffs better when the instructions mention them:

triage.Instructions = agents.StaticInstructions(`You are a triage agent for a customer support system.
You can transfer the conversation to specialist agents using the transfer tools.
Transfers are seamless: do not mention or draw attention to them.`)

Semantics worth knowing