Skip to content

Tools

Tools let the model call functions defined in your code. The model decides when to use a tool based on the conversation, generates structured input, and GoAI executes the function and feeds the result back.

Defining a Tool

A goai.Tool has a name, description, JSON Schema for input, and an Execute function:

go
weatherTool := goai.Tool{
    Name:        "get_weather",
    Description: "Get the current weather for a city.",
    InputSchema: json.RawMessage(`{
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "City name"}
        },
        "required": ["city"]
    }`),
    Execute: func(ctx context.Context, input json.RawMessage) (string, error) {
        var params struct {
            City string `json:"city"`
        }
        if err := json.Unmarshal(input, &params); err != nil {
            return "", err
        }
        // Call your weather API here.
        return fmt.Sprintf("72F and sunny in %s", params.City), nil
    },
}

Pass tools to GenerateText or StreamText with WithTools:

go
result, err := goai.GenerateText(ctx, model,
    goai.WithPrompt("What's the weather in Tokyo?"),
    goai.WithTools(weatherTool),
)

Single Step vs Auto Loop

By default, MaxSteps is 1.

  • GenerateText: if the model requests tool calls and matching tools have Execute, GoAI executes those tools in that step, then returns (no follow-up model step).
  • StreamText: automatic tool looping requires WithMaxSteps(2+); with MaxSteps(1), tool calls are surfaced in the stream/result for you to handle.

Set WithMaxSteps(n) to enable the auto tool loop. GoAI will:

  1. Send the prompt to the model
  2. If the model requests tool calls, execute each tool's Execute function
  3. Append the tool results to the conversation
  4. Send the updated conversation back to the model
  5. Repeat until the model stops requesting tools, or n steps are reached
go
result, err := goai.GenerateText(ctx, model,
    goai.WithPrompt("What's the weather in Tokyo and London?"),
    goai.WithTools(weatherTool),
    goai.WithMaxSteps(5),
)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result.Text)
fmt.Printf("Steps: %d\n", len(result.Steps))

Tool Choice

Control how the model selects tools with WithToolChoice:

go
// Model decides whether to use tools (default)
goai.WithToolChoice("auto")

// Model must use at least one tool
goai.WithToolChoice("required")

// Model must not use any tools
goai.WithToolChoice("none")

// Model must use a specific tool
goai.WithToolChoice("get_weather")

Step Callbacks

WithOnStepFinish is called after each step completes, including after tool execution. Use it for logging or progress tracking:

go
result, err := goai.GenerateText(ctx, model,
    goai.WithPrompt("Research the weather in 3 cities."),
    goai.WithTools(weatherTool),
    goai.WithMaxSteps(10),
    goai.WithOnStepFinish(func(step goai.StepResult) {
        fmt.Printf("Step %d: %d tool calls, finish=%s\n",
            step.Number, len(step.ToolCalls), step.FinishReason)
    }),
)

Tool Call Hooks

WithOnToolCallStart is called before each individual tool execution:

go
goai.WithOnToolCallStart(func(info goai.ToolCallStartInfo) {
    fmt.Printf("[step %d] starting tool %s (call %s)\n", info.Step, info.ToolName, info.ToolCallID)
})

WithOnToolCall is called after each individual tool execution:

go
import "time"

// ...

goai.WithOnToolCall(func(info goai.ToolCallInfo) {
    fmt.Printf("[step %d] tool %s took %v\n", info.Step, info.ToolName, info.Duration)
    fmt.Printf("  input:  %s\n", info.Input)
    fmt.Printf("  output: %s\n", info.Output)
    if info.Error != nil {
        fmt.Printf("  error: %v\n", info.Error)
    }
})

ToolCallInfo Fields

FieldTypeDescription
ToolCallIDstringProvider-assigned identifier for this tool call
ToolNamestringName of the tool that was executed
Stepint1-based index of the generation step in which this tool was called
Inputjson.RawMessageRaw JSON arguments passed to the tool
OutputstringString result returned by the tool (empty if the tool errored)
OutputObjectanyParsed JSON value of Output when valid JSON; nil otherwise
StartTimetime.TimeWhen the tool execution began
Durationtime.DurationTime taken to execute the tool
ErrorerrorError returned by the tool, if any

Note: When multiple tools execute in a single step, OnToolCall callbacks fire concurrently from separate goroutines. Order is non-deterministic.

Security: ToolCallInfo contains the full Input and Output of tool executions, which may include sensitive data. Consumers that log or export hook data should sanitize accordingly. Tool Execute errors are also forwarded to the model as tool result messages — do not include credentials or internal paths in error strings.

Tools Without Execute

If a tool has no Execute function, GoAI sends the tool definition to the model but does not participate in the auto loop. The model can still request the tool call, and it appears in result.ToolCalls. This is useful when you manage the tool loop yourself.

StepResult

Each step in a multi-step tool loop produces a goai.StepResult:

FieldTypeDescription
Numberint1-based step index
TextstringText generated in this step
ToolCalls[]provider.ToolCallTool calls requested in this step
FinishReasonprovider.FinishReasonWhy this step stopped
Usageprovider.UsageToken usage for this step
Responseprovider.ResponseMetadataProvider metadata for this step
ProviderMetadatamap[string]map[string]anyProvider-specific data for this step
Sources[]provider.SourceCitations from this step

Streaming Tool Loops

StreamText supports the same auto tool loop as GenerateText. Pass WithMaxSteps and tools with Execute functions:

go
stream, err := goai.StreamText(ctx, model,
    goai.WithPrompt("What's the weather in Tokyo?"),
    goai.WithTools(weatherTool),
    goai.WithMaxSteps(5),
)
if err != nil {
    log.Fatal(err)
}

for chunk := range stream.Stream() {
    switch chunk.Type {
    case provider.ChunkText:
        fmt.Print(chunk.Text)
    case provider.ChunkStepFinish:
        fmt.Println("\n[step complete]")
    }
}

result := stream.Result()
fmt.Printf("Steps: %d\n", len(result.Steps))

All steps stream through a single unified channel. Step boundaries are marked by ChunkStepFinish chunks. The initial DoStream failure returns (nil, error). Subsequent step errors flow through the stream as ChunkError chunks — check stream.Err() after consuming.

Released under the MIT License.