Skip to main content

StopCondition

A function type that determines whether the tool-calling loop should stop after a step. Pass one or more StopCondition values to the StopWhen field in GenerateTextOptions or AgentConfig.

Type Definition​

type StopCondition func(state StopConditionState) string

A StopCondition is called after each step. Return a non-empty reason string to stop the loop; return "" to continue.

StopConditionState​

StopConditionState is passed to every StopCondition after each step completes.

type StopConditionState struct {
// Steps completed so far. The step that just finished is the last element.
Steps []types.StepResult

// Full message history including the latest tool-result messages.
Messages []types.Message

// Accumulated token usage across all completed steps.
Usage types.Usage
}
FieldTypeDescription
Steps[]types.StepResultCompleted steps; last element is the most recent step
Messages[]types.MessageFull conversation history at this point in the loop
Usagetypes.UsageToken usage summed across all steps so far

EvaluateStopConditions​

func EvaluateStopConditions(conditions []StopCondition, state StopConditionState) string

The loop engine calls EvaluateStopConditions internally after every step. It runs all conditions before inspecting any result, then returns the first non-empty reason string (or "" if none fired).

Key invariant: every condition always executes, regardless of order. Side-effectful conditions (logging, metrics) are therefore safe to place anywhere in the slice and are guaranteed to run even when an earlier condition fires first.

Examples​

Custom closure​

Any function with the right signature is a valid StopCondition:

stopOnError := func(state ai.StopConditionState) string {
if len(state.Steps) == 0 {
return ""
}
last := state.Steps[len(state.Steps)-1]
for _, result := range last.ToolResults {
if result.Error != nil {
return "tool returned an error"
}
}
return ""
}

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Search for recent Go releases",
Tools: []types.Tool{searchTool},
StopWhen: []ai.StopCondition{stopOnError, ai.IsStepCount(10)},
})

Token budget​

Stop before the model runs out of context by inspecting state.Usage:

tokenBudget := func(maxTokens int) ai.StopCondition {
return func(state ai.StopConditionState) string {
if int(state.Usage.GetTotalTokens()) >= maxTokens {
return fmt.Sprintf("token budget of %d reached", maxTokens)
}
return ""
}
}

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Summarize this document",
Tools: tools,
StopWhen: []ai.StopCondition{
tokenBudget(50_000),
ai.HasToolCall("finish"),
ai.IsStepCount(20),
},
})
if err != nil {
log.Fatal(err)
}

fmt.Printf("StopReason: %s\n", result.StopReason)

Side-effectful condition (metrics)​

Because EvaluateStopConditions always runs all conditions, you can record metrics in a StopCondition without worrying about evaluation order:

recordStepMetric := func(state ai.StopConditionState) string {
metrics.RecordStep(len(state.Steps), int(state.Usage.GetTotalTokens()))
return "" // never stops the loop on its own
}

StopWhen: []ai.StopCondition{
recordStepMetric, // always runs; records every step
ai.HasToolCall("done"), // may stop early
ai.IsStepCount(15), // hard ceiling
},

Condition ordering guarantee​

EvaluateStopConditions collects all reason strings in a single pass, then returns the first non-empty one. This means:

  1. Every condition runs on every step — no short-circuit evaluation.
  2. The first condition with a non-empty reason wins (slice order matters for which reason is surfaced, not for which conditions execute).
  3. The winning reason is returned as GenerateTextResult.StopReason.
// Both conditions always run; the first non-empty reason is returned.
StopWhen: []ai.StopCondition{
ai.HasToolCall("finish"), // fires → "tool 'finish' was called"
ai.IsStepCount(20), // also evaluated, but result ignored if finish fired first
},

See Also​