Skip to main content

Getting started

We will start with one generation request, then add a weather tool, see the model ask for a tool call, handle that call in the host application, and finally let an Agent run the same loop for us. Every section below changes the same main.go. Only the first section creates the file. The getting-started examples contain one runnable snapshot for each stage.

Installing Golato

Create a Go module and add Golato:

go mod init example.com/golato-hello
go get github.com/yongtenglei/golato@v0.3.0

Golato requires Go 1.26.6 or newer.

Configuring the model

Set the API key and model used by the example:

export GOLATO_API_KEY="..."
export GOLATO_MODEL="..."

# Optional for a compatible service with a different endpoint:
export GOLATO_BASE_URL="https://example.com/v1"

Greeting Golato

Create main.go. The same program also lives in 01-greeting:

// Package main sends one greeting with a Generator.
package main

import (
"context"
"fmt"
"log"
"os"

"github.com/yongtenglei/golato/adapters/openai"
"github.com/yongtenglei/golato/generation"
"github.com/yongtenglei/golato/message"
)

func main() {
apiKey := requiredEnv("GOLATO_API_KEY")
model := requiredEnv("GOLATO_MODEL")
generator, err := openai.NewGenerator(openai.GeneratorConfig{
EndpointConfig: openai.EndpointConfig{
APIKey: apiKey,
BaseURL: os.Getenv("GOLATO_BASE_URL"),
},
})
if err != nil {
log.Fatal(err)
}

ctx := context.Background()
stream, err := generator.Stream(ctx, generation.Request{
Model: model,
SystemPrompt: "You are an assistant powered by Golato, which is a small, embeddable generation, " +
"embedding, and agent runtime library for Go.",
Messages: []message.Message{
message.NewUserMessage("Introduce yourself in one sentence."),
},
})
if err != nil {
log.Fatal(err)
}

response, err := stream.Result(ctx)
if err != nil {
log.Fatal(err)
}

fmt.Println(response.Text())
}

func requiredEnv(name string) string {
value := os.Getenv(name)
if value == "" {
fmt.Fprintf(os.Stderr, "%s is required\n", name)
os.Exit(1)
}
return value
}

Run it:

go run .

This is the simplest form of generation: one question and one answer, without tools.

One possible result:

I am an intelligent assistant powered by Golato, a small, embeddable runtime library for Go that provides support for generation, embedding, and agent-related functionality.

Asking about the weather

A greeting needs nothing but words. A weather assistant needs a way to look up the weather, and that lookup is a tool.Definition: the model sees the operation get_weather, its purpose, and the JSON object it accepts. The request hands that definition over through generation.Request.Tools and asks the model to call it. The complete program for this stage also lives in 02-weather-tool:

weatherDefinition := tool.Definition{
Name: "get_weather",
Description: "Return the current weather for a city.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"],
"additionalProperties": false
}`),
}

ctx := context.Background()
stream, err := generator.Stream(ctx, generation.Request{
Model: model,
SystemPrompt: "You are a helpful weather assistant.",
Messages: []message.Message{
message.NewUserMessage("What is the weather in Beijing?"),
},
Tools: []tool.Definition{weatherDefinition},
Options: generation.Options{
ToolChoice: new(generation.ToolChoice{
Mode: generation.ToolChoiceFunction,
Name: "get_weather",
}),
},
})
if err != nil {
log.Fatal(err)
}

response, err := stream.Result(ctx)
if err != nil {
log.Fatal(err)
}

fmt.Printf("stop_reason=%s\n", response.StopReason)
for _, call := range response.ToolCalls() {
fmt.Printf("tool_call: name=%q args=%s\n", call.Name, string(call.Arguments))
}
}

Run it again:

go run .

The output now contains a tool_call. generation.Generator handles one model turn: it reports what the model wants to call, but it does not invoke the Go function or send another request.

One possible result:

stop_reason=tool_use
tool_call: name="get_weather" args={"city": "Beijing"}

Handling the call in the host

A tool call is an instruction, not an answer. The host application owns the function behind get_weather, so the next step belongs to it: run the call, append the assistant message and its matching tool result, and send the request again. The same program now keeps this conversation in a loop. The complete program for this stage also lives in 03-tool-loop:

A small struct describes the call arguments, and a function turns them into the weather text:

type weatherInput struct {
City string `json:"city"`
}

func weather(city string) string {
return fmt.Sprintf("The weather in %s is currently 24°C and clear.", city)
}

The loop keeps the whole history in request.Messages, executes each returned call, and continues until the model stops asking for tools. It reuses the weatherDefinition from the previous stage, and the complete file for this stage carries the same schema without the optional field description:

ctx := context.Background()
request := generation.Request{
Model: model,
SystemPrompt: "You are a helpful weather assistant.",
Messages: []message.Message{
message.NewUserMessage("What is the weather in Beijing?"),
},
Tools: []tool.Definition{weatherDefinition},
}

for {
stream, err := generator.Stream(ctx, request)
if err != nil {
log.Fatal(err)
}
response, err := stream.Result(ctx)
if err != nil {
log.Fatal(err)
}

if response.StopReason != message.StopReasonToolUse {
fmt.Println(response.Text())
return
}

request.Messages = append(request.Messages, response)
for _, call := range response.ToolCalls() {
args, err := tool.DecodeArguments[weatherInput](call.Arguments)
if err != nil {
log.Fatal(err)
}
request.Messages = append(request.Messages, message.ToolResultMessage{
ToolCallID: call.ID,
ToolName: call.Name,
Content: []message.ToolResultContentPart{
message.TextContent{Text: weather(args.City)},
},
})
}
}
}

Run it again. The host now handles the tool_call, sends the weather result back to the model, and prints the model's final answer. A production host should also validate the call name and arguments before executing it.

One possible result:

The weather in Beijing is currently 24°C and clear.

Letting Agent run the loop

The host loop works, but running a tool and feeding its result back is common orchestration. This is the convenience Golato provides: agent.Agent runs that loop, and a tool.Tool pairs the same model-visible definition with its Go implementation, so the Agent can execute get_weather itself. The complete program for this stage also lives in 04-agent:

weatherTool := tool.Tool{
Name: "get_weather",
Description: "Return the current weather for a city.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"],
"additionalProperties": false
}`),
Execute: func(_ context.Context, call tool.Call, _ tool.Update) (tool.Result, error) {
args, err := tool.DecodeArguments[weatherInput](call.Arguments)
if err != nil {
return tool.Result{}, err
}
return tool.TextResult(weather(args.City)), nil
},
}

With the same generator, the same weather function, and a tool that carries its own implementation, main becomes just a matter of starting a run and waiting for it:

ctx := context.Background()
assistant, err := agent.New(agent.Config{
Generator: generator,
Model: model,
Tools: []tool.Tool{weatherTool},
})
if err != nil {
log.Fatal(err)
}

run, err := assistant.Start(ctx, agent.Input{
Context: agent.Context{SystemPrompt: "You are a helpful weather assistant."},
Prompts: []message.UserMessage{
message.NewUserMessage("What is the weather in Beijing?"),
},
})
if err != nil {
log.Fatal(err)
}

result, err := run.Wait()
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Final.Text())
}

Run it again. agent.Agent executes the weather tool, adds its result to the next request, and continues until the model returns a final answer. Store result.Context when the host application wants to continue the conversation in a later run.

One possible result:

The weather in Beijing is currently 24°C and clear.

A service should pass a cancellable context or a deadline-based context instead of context.Background().

Recapping and exploring more examples

We have now walked through Golato's two core capabilities: direct generation with one request and one answer, and Agent orchestration that runs tool calls and follow-up turns on its own. Along the way we always took the final result directly, through Stream.Result and run.Wait. To watch what happens live, the Observing generation events and Observing Agent events chapters show streaming text, thinking, and tool executions as they arrive.

The repository keeps complete runnable programs for each path:

When you want to...Look at
See the simplest one-request generation01-greeting
Watch the model ask for a tool02-weather-tool
Run the tool loop yourself03-tool-loop
Let an Agent run the loop04-agent
Watch events, thinking, and continuationagent/tool-loop
Drive a 150+ line interactive coding agentagent/coding-agent
Send image bytes with a promptimage-input/data
Send an image URL insteadimage-input/url

Run commands and notes for all examples live in the examples overview.

Exploring next steps