agents-go
Building agents

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 := agents.MustNewAgent(agents.AgentOptions{
    Instructions: "Help users with the weather. Use get_weather for live data.",
    Tools:        []tool.Entry{getWeather},
})

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.RunTask(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, _ := agents.NewSession(agents.Config{
    LLM:      "openai/gpt-4o",
    UserData: &deps{db: db, userID: "u_123"},
})

lookup, _ := 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)
    },
)

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 & dynamic tools

A Toolset provides tools that can change at runtime and share setup/teardown — build one with tool.NewToolset. Two are built in:

  • Tool proxy (tool.NewToolProxyToolset) — forwards to tools resolved elsewhere (e.g. an MCP server), so remote tools appear as native ones.
  • Tool search (tool.NewToolSearchToolset) — exposes a large catalogue behind a search step, so the model isn't shown hundreds of tools at once.

Runtime tools with no Go type (MCP-provided, fully dynamic) use tool.RawTool, which carries an explicit schema instead of a reflected one.

Next

On this page