Tools
Typed function tools with schema-from-types, toolsets, and per-reply scoping.
A tool lets the model do something — look up an order, book a table, hand off to another agent. In agents-go a tool is a typed Go function; its JSON Schema is derived from the input type by reflection, so there's no schema to hand-write or keep in sync. The usual tool bug — the schema the model sees and the code that runs drifting apart — simply can't happen: the contract is the type.
Define a tool
Define the input and output types
Fields become parameters. Struct tags add the model-visible description and
validation constraints (description, min/max, enum, format, pattern).
type WeatherInput struct {
City string `json:"city" description:"city name" min:"1"`
Units string `json:"units" description:"temperature units" enum:"celsius|fahrenheit"`
}
type WeatherOutput struct {
Summary string `json:"summary"`
TempC float64 `json:"temp_c"`
}Write the handler
The signature is func(context.Context, *tool.RunContext, *In) (*Out, error).
Return an error to tell the model the call failed; the runtime surfaces it as the
tool result so the model can recover.
getWeather, err := tool.New("get_weather",
func(ctx context.Context, run *tool.RunContext, in *WeatherInput) (*WeatherOutput, error) {
// ... call your weather API ...
return &WeatherOutput{Summary: "clear", TempC: 21.5}, nil
},
tool.WithDescription("Get the current weather for a city."),
)
if err != nil {
return err
}The schema is derived once at construction and cached. It's deterministic — sorted keys, stable ordering — so it's safe to snapshot.
Attach it to an agent
AgentOptions.Tools accepts a single tool, a []tool.Entry, a toolset, or a
*tool.Context.
agent, err := agents.NewAgent(agents.AgentOptions{
Instructions: "Help users with the weather. Use get_weather for live data.",
Tools: []tool.Entry{getWeather},
})
if err != nil {
log.Fatal(err)
}When the model calls the tool during a turn, the runtime decodes the arguments
into *WeatherInput, runs your handler, and folds the result back into the reply
(Speech & the turn).
RunContext: reach the turn and request-scoped data
The handler's *tool.RunContext is the tool's handle on the live turn:
run.WaitForPlayout(ctx)— finish speaking this step before continuing.run.DisallowInterruptions()— make this step's speech uninterruptible.- Hand off or delegate —
agents.RequestHandoff(run, target, returns)/agents.DelegateTask(ctx, run, task)(Agents & handoffs).
Set Config.UserData (an any) to pass request-scoped dependencies — a DB
handle, the authenticated user, a tenant ID — into your tools, and read it back
off run.UserData:
type deps struct{ db *sql.DB; userID string }
session, err := agents.NewSession(agents.Config{
LLM: "openai/gpt-4o",
UserData: &deps{db: db, userID: "u_123"},
})
if err != nil {
log.Fatal(err)
}
lookup, err := tool.New("lookup_orders",
func(ctx context.Context, run *tool.RunContext, _ *struct{}) (*Orders, error) {
d := run.UserData.(*deps) // the value set on the session
return queryOrders(ctx, d.db, d.userID)
},
)
if err != nil {
log.Fatal(err)
}WaitForPlayout holds up the turn until the audio finishes. Use it only when
ordering against speech matters ("say it, then act") — not by default.
Restrict tools for one reply
You can limit which tools a single reply may call, by tool ID (the function name), without changing the agent — useful to force a step:
session.GenerateReply(agents.GenerateReplyOptions{
UserInput: "Book it.",
Tools: []string{"book_table"}, // only this tool available this reply
})See Speech & the turn for other per-reply controls.
Toolsets: group tools that share a lifecycle
A toolset groups related tools that share setup and teardown — a database
pool, an MCP client, an authenticated API session. Build one with
tool.NewStaticToolset (no lifecycle) or tool.NewToolset (with Setup/Close
hooks that run off the turn path). A toolset is accepted anywhere a tool is:
billing, err := tool.NewStaticToolset("billing", issueRefund, getInvoice)
if err != nil {
log.Fatal(err)
}
agent, err := agents.NewAgent(agents.AgentOptions{
Instructions: "Help with billing.",
Tools: []any{billing, escalate}, // toolsets and tools mix freely
})
if err != nil {
log.Fatal(err)
}Toolsets are flattened before tools are sent to the model, and tool names must be unique across everything mounted.
Swap tools at runtime
The active tool list isn't fixed. Agent.UpdateTools (or AgentContext.UpdateTools
from inside a hook) replaces it mid-session — the natural home is
OnUserTurnCompleted, where you can classify the user's intent and narrow the
tool surface to just that domain before the model generates:
agent, err := agents.NewAgent(agents.AgentOptions{
Instructions: "…",
OnUserTurnCompleted: func(ctx context.Context, ac *agents.AgentContext, _ *llm.ChatContext, msg *llm.ChatMessage) error {
domain := classify(msg.TextContent()) // your keyword/embedding classifier
return ac.UpdateTools(ctx, toolsetFor(domain))
},
})
if err != nil {
log.Fatal(err)
}Scaling to many tools: tool search
Showing a model hundreds of tools is slow and error-prone. tool.NewToolSearchToolset
puts a whole catalogue behind a single tool_search tool: the model searches
by intent, and only the matching tools are loaded and become callable.
catalog, err := tool.NewToolSearchToolset(tool.ToolSearchOptions{
ID: "catalog",
Tools: []any{billing, orders, account}, // any number of toolsets
MaxResults: 5,
})
if err != nil {
log.Fatal(err)
}
agent, err := agents.NewAgent(agents.AgentOptions{
Instructions: "Use tool_search to find the right tool, then call it.",
Tools: []any{catalog},
})
if err != nil {
log.Fatal(err)
}A related variant, tool.NewToolProxyToolset, exposes a fixed pair —
tool_search plus call_tool — so remote tools (e.g. from an MCP server) appear
as native ones without ever being individually mounted.
Custom ranking with SearchStrategy
How search ranks candidates is pluggable via the tool.SearchStrategy interface
(BuildIndex / Search / Cleanup). The default is BM25; tool.NewKeywordSearchStrategy
is also built in. To scale past what lexical matching handles — hundreds of tools,
ranked by meaning — implement the interface with an embedding model and cosine
similarity, caching each tool's vector on SearchItem.IndexData (persist them in
pgvector for a large catalogue):
type embeddingSearch struct{ /* your embedder */ }
func (s *embeddingSearch) BuildIndex(ctx context.Context, items []*tool.SearchItem) error {
for _, it := range items {
it.IndexData = s.embed(ctx, it.Name+" "+it.Description) // []float32
}
return nil
}
func (s *embeddingSearch) Search(ctx context.Context, query string, items []*tool.SearchItem, n int) ([]*tool.SearchItem, error) {
// embed(query), cosine against each it.IndexData, sort desc, return top-n
}
func (s *embeddingSearch) Cleanup(context.Context) error { return nil }Pass it as ToolSearchOptions.SearchStrategy. A complete, runnable cosine
strategy is in
examples/toolsearch;
the supervisor pattern page shows the
agent side.
Runtime tools with no Go type (MCP-provided, fully dynamic) use tool.RawTool,
which carries an explicit schema instead of a reflected one.
Long-running tools
When a tool takes a while (a slow API, a lookup), a voice agent shouldn't go
silent. Three controls on the tool and its RunContext handle this:
AgentOptions.ToolFiller— interstitial speech ("let me check that…") played afterToolFillerDelaywhile the tool runs.run.WaitForPlayout(ctx)— finish speaking the current step before the tool's result advances the turn ("say it, then act").run.DisallowInterruptions()— make this step's speech uninterruptible, for a confirmation that must not be cut off.
Next
- Supervisor pattern & scaling — compose tools, tasks, and handoffs.
- Models & providers — give the agent a brain and a voice.
- Full API: Reference → Tools.