Skip to main content

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.
  • Content is 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.

BlockUserAssistantTool resultUse
message.TextContentyesyesyesordinary text
message.ImageContentyesnoyesURL or inline bytes
message.ThinkingContentnoyesnoprovider-returned reasoning
message.ToolCallnoyesnoID, 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.

API reference