Building messages and content
A conversation with a model is a list of messages, and Golato keeps those messages in one small set of types that Generation, Agent, and compaction all share. A conversation can therefore move between those operations without any conversion, and what the application stores is the same thing it sends.
Composing a user prompt
For plain text, message.NewUserMessage is all you need.
prompt := message.NewUserMessage("Explain this API in one sentence.")
A message.UserMessage can interleave text and images. The order in Content is the order the model receives.
data, err := os.ReadFile("photo.png")
if err != nil {
return err
}
prompt := message.UserMessage{
Content: []message.UserContentPart{
message.TextContent{Text: "Describe this image."},
message.NewImageData(data, "image/png"),
},
}
The same message value feeds either entry point: a Generator through generation.Request.Messages, or an Agent through
agent.Input.Prompts.
Adding an image
message.NewImageData copies the inline bytes and needs a MIME type when the message is validated.
message.NewImageURL keeps a remote URL. An ImageContent carries exactly one of them, and it is content for the
model, not an image-generation request.
inline := message.NewImageData(pngBytes, "image/png")
remote := message.NewImageURL("https://example.com/image.png")
Images can also appear in a tool result. The inline image example and the remote URL example run both forms against a real model.
Reading an assistant reply
A generation.Stream gives you two ways to consume a reply, and the terminal event decides what the answer is.
Take the final answer directly with Result. The first call enters result-only mode, releases any queued events, and
waits for the stream to finish.
answer, err := stream.Result(ctx)
if err != nil {
return err
}
Stream the reply as it comes with Next. One consumer reads events in order, and the loop ends when the terminal event
has been read and Next returns io.EOF.
for {
event, err := stream.Next(ctx)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return err
}
delta, ok := event.(generation.TextDeltaEvent)
if ok {
fmt.Print(delta.Delta)
}
}
answer, err := stream.Result(ctx)
if err != nil {
return err
}
After the loop you can still call Result, it returns the same terminal fact without clearing the queue, and it may be
called repeatedly. Alternatively keep the terminal event from the loop itself, since its Message is the same
authoritative value.
The last event is the answer. Result always returns exactly the message the terminal event carried, so it never
disagrees with the last event read. Observing generation events spells out
the two terminal events and the producer errors in full.
With an Agent, run.Wait returns an agent.Result, and its Final field holds the last assistant attempt.
result, err := run.Wait()
if err != nil {
return err
}
answer := result.Final
Either way the message reads the same, because both values are a message.AssistantMessage.
fmt.Println(answer.Text())
for _, call := range answer.ToolCalls() {
fmt.Printf("tool=%s arguments=%s\n", call.Name, string(call.Arguments))
}
The message reads through three helpers:
Text()joins the visible text blocks in order and ignores thinking and tool calls.ToolCalls()returns calls in content order with copied raw arguments.Contentis the full block list to walk when the order of every block matters.
message.AssistantMessage also carries provider facts (Provider, RequestedModel, ReturnedModel, ResponseID,
Usage, StopReason, and the raw finish reason) whenever the provider reports them. See the
AssistantMessage reference for the
fields, and Observing generation events for the thinking replay rules.
Answering a tool call
When the host application runs its own tool loop, it pairs every call with a message.ToolResultMessage.
result := message.ToolResultMessage{
ToolCallID: call.ID,
ToolName: call.Name,
Content: []message.ToolResultContentPart{
message.TextContent{Text: "Customer C-100 is on the Gold plan."},
},
}
IsError marks a failed operation, and text and image content can be combined. When an Agent runs the loop, it builds
this message itself from the tool.Result of a registered function. See Defining tools.
Knowing the content types
message.Message is a closed set of values. Use the documented concrete types, never pointers or wrappers. Validation
rejects anything else.
| Block | User | Assistant | Tool result | Use |
|---|---|---|---|---|
message.TextContent | yes | yes | yes | ordinary text |
message.ImageContent | yes | no | yes | URL or inline bytes |
message.ThinkingContent | no | yes | no | provider-returned reasoning |
message.ToolCall | no | yes | no | ID, name, and raw JSON arguments |
Validating before sending or storing
message.ValidateMessage checks one value and message.Validate checks a slice. Both report through
message.ErrInvalid. They verify the allowed types and the image shape, but they do not require a tool call ID or name
and do not parse call arguments. Execution adds those checks where they matter.
validateErr := message.Validate(messages)
if validateErr != nil {
return validateErr
}
Copying values across ownership
message.Clone, CloneMessage, and CloneAssistant deep-copy message slices, image bytes, raw tool arguments, usage,
and the reasoning-token pointer. Golato copies values before concurrent use and inside returned results. Copy again when
the application wants an independently mutable snapshot.
Storing the conversation yourself
The host application owns persistence, so Golato deliberately ships no JSON codec for messages. To save a conversation,
tag every message and content block with its type, write the fields, and rebuild the message values from that record
when you read it back.
[
{
"type": "user",
"content": [{ "type": "text", "text": "Which plan does C-100 use?" }]
},
{
"type": "assistant",
"content": [
{ "type": "thinking", "thinking": "The user asks about a customer account.", "signature": "reasoning_content" },
{ "type": "tool_call", "id": "call-1", "name": "lookup_customer", "arguments": "{\"customer_id\":\"C-100\"}" }
]
}
]
The exact shape is up to the application, as long as reading it back produces valid message.Message values. Keep image
bytes or URLs, tool IDs and names, raw arguments, result pairing, finish facts, and thinking signatures when the next
request needs them. Storing and continuing conversation state describes which messages belong in the
stored state.