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 generation | 01-greeting |
| Watch the model ask for a tool | 02-weather-tool |
| Run the tool loop yourself | 03-tool-loop |
| Let an Agent run the loop | 04-agent |
| Watch events, thinking, and continuation | agent/tool-loop |
| Drive a 150+ line interactive coding agent | agent/coding-agent |
| Send image bytes with a prompt | image-input/data |
| Send an image URL instead | image-input/url |
Run commands and notes for all examples live in the examples overview.
Exploring next steps
- Using a Generator covers stream events, error handling, and tool definitions
- Observing generation events shows text and thinking arriving live
- Running an Agent covers observing runs and continuing conversations
- Configuring the OpenAI-compatible adapter covers endpoint and provider options