Skip to content

Multi-Turn Conversations ​

GoAI supports multi-turn conversations through WithMessages. After each generation call, ResponseMessages provides the full assistant and tool message history, ready to append to your conversation.

The Problem ​

When using tools with WithMaxSteps, GoAI runs an automatic tool loop: generate, execute tools, re-generate. The result contains the final text, but your conversation history needs the intermediate tool call and tool result messages too. Without them, the model may re-execute tools on the next turn.

ResponseMessages ​

ResponseMessages on TextResult (and ObjectResult) contains all assistant and tool messages produced during a generation call. Append it directly to your message history:

go
var messages []provider.Message

// Turn 1
messages = append(messages, goai.UserMessage("What's the weather in Tokyo?"))
result, err := goai.GenerateText(ctx, model,
    goai.WithMessages(messages...),
    goai.WithTools(weatherTool),
    goai.WithMaxSteps(5),
)
if err != nil {
    log.Fatal(err)
}

// Append the full response history (assistant + tool messages).
messages = append(messages, result.ResponseMessages...)

// Turn 2 - model sees the tool was already called, won't repeat it.
messages = append(messages, goai.UserMessage("Is that warm enough for a picnic?"))
result, err = goai.GenerateText(ctx, model,
    goai.WithMessages(messages...),
    goai.WithTools(weatherTool),
    goai.WithMaxSteps(5),
)

What ResponseMessages Contains ​

The contents depend on what happened during generation:

ScenarioResponseMessages
Simple text (no tools)1 assistant message with text
Tool loop (2 steps)assistant (tool calls) + tool (results) + assistant (final text)
Parallel tool callsTool results merged into a single message per round-trip
MaxSteps exhaustedAll completed round-trips (no duplicate final message)
Streaming with reasoningReasoning boundaries preserved as one or more PartReasoning values with metadata

Streaming ​

ResponseMessages works the same way with StreamText. Call Result() after consuming the stream:

go
stream, err := goai.StreamText(ctx, model,
    goai.WithMessages(messages...),
    goai.WithTools(weatherTool),
    goai.WithMaxSteps(5),
)
if err != nil {
    log.Fatal(err)
}
// Consume the stream.
for chunk := range stream.Stream() {
    fmt.Print(chunk.Text)
}
// Check for errors before using ResponseMessages.
if err := stream.Err(); err != nil {
    log.Fatal(err)
}
// Append to history.
messages = append(messages, stream.Result().ResponseMessages...)

On stream errors, ResponseMessages may be partial. Always check Err() first.

Structured Output ​

GenerateObject and StreamObject also populate ResponseMessages:

go
result, err := goai.GenerateObject[MyStruct](ctx, model,
    goai.WithMessages(messages...),
    goai.WithTools(helperTool),
    goai.WithMaxSteps(5),
)
messages = append(messages, result.ResponseMessages...)

Without ResponseMessages ​

If you prefer manual control, you can build messages from result.Steps. Completed tool outputs are available in StepResult.ToolResults; WithOnToolCall is only needed for real-time observation. ResponseMessages handles reconstruction automatically.

TIP

See examples/multi-turn/ for a runnable example.

Released under the MIT License.