# Loop Control

The Go AI SDK provides several ways to control how long an agent's tool-calling loop runs.
The recommended approach is `StopWhen` — a declarative list of stop conditions evaluated after
each step that contains tool results. For lower-level control, `PrepareCall` lets you modify
each generation call dynamically. Manual loops using `ai.GenerateText` directly give you full
control for advanced use cases.

---

## Stop Conditions

`StopWhen` accepts a slice of `StopCondition` functions. After every step that produces tool
results, the SDK evaluates the conditions and stops the loop as soon as one returns a non-empty
reason string. The reason is available as `result.StopReason`.

### IsStepCount

The most common condition: stop after *n* steps.

```go
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
    Model:  model,
    Prompt: "Analyze this dataset and create a summary report",
    Tools:  tools,
    StopWhen: []ai.StopCondition{
        ai.IsStepCount(5),
    },
})
if err != nil {
    log.Fatal(err)
}

fmt.Println(result.Text)
fmt.Printf("Stopped because: %s\n", result.StopReason)
// → "Stopped because: maximum number of steps (5) reached"
```

`IsStepCount(n)` stops the loop once exactly *n* steps have completed. Because it is
evaluated after every step, it is checked against every step count from 1 up to *n* and
never skips the threshold.

### HasToolCall

Stop when the model calls a specific tool — useful for semantic completion signals where the
model is expected to invoke a "finish" or "submit" tool when it's done.

```go
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
    Model:  model,
    Prompt: "Research the topic and call the finish tool when done",
    Tools: []types.Tool{
        researchTool,
        finishTool, // model calls this to signal completion
    },
    StopWhen: []ai.StopCondition{
        ai.HasToolCall("finish"),
        ai.IsStepCount(10), // safety ceiling
    },
})
```

### Combining Conditions

Conditions are evaluated OR — the loop stops on the **first match**. Place conditions you care
about most, or that have side effects, **before** safety ceilings like `IsStepCount`.

```go
StopWhen: []ai.StopCondition{
    ai.HasToolCall("submit"),  // semantic completion
    ai.IsStepCount(20),        // hard limit
},
```

### Custom Closure

Any function with signature `func(ai.StopConditionState) string` qualifies as a `StopCondition`.
Return a non-empty string to stop, or an empty string to continue.

```go
// Token-budget guard — placed BEFORE IsStepCount so it always runs.
// Because evaluation is eager (all conditions run before the first match is
// returned), this condition fires even if IsStepCount would also match.
tokenBudget := func(state ai.StopConditionState) string {
    if state.Usage.GetTotalTokens() > 5000 {
        return fmt.Sprintf("token budget exceeded (%d tokens)", state.Usage.GetTotalTokens())
    }
    return ""
}

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
    Model:  model,
    Prompt: "Perform a deep analysis",
    Tools:  tools,
    StopWhen: []ai.StopCondition{
        tokenBudget,          // side-effectful or priority condition first
        ai.IsStepCount(10),   // safety ceiling last
    },
})
```

`StopConditionState` gives you full context:

```go
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 steps.
    Usage types.Usage
}
```

### Condition Evaluation Order and Side Effects

> **Important:** All conditions are always evaluated before the first match is returned.
> This matches the TypeScript SDK's `Promise.all` behavior and ensures side-effectful
> conditions (e.g. recording metrics, sending alerts) always execute regardless of their
> position in the slice.

Place conditions you want to guarantee run **before** conditions that might pre-empt them,
and rely on the evaluation guarantee rather than ordering for correctness.

### StopReason

`result.StopReason` (on `GenerateTextResult`) and `agentResult.StopReason` (on `AgentResult`)
contain the reason string returned by whichever condition fired. Empty if the loop ended
naturally (no tool calls) or no `StopWhen` was set.

```go
result, err := myAgent.Execute(ctx, "Complete the task")
if err != nil {
    log.Fatal(err)
}

switch {
case result.StopReason != "":
    fmt.Printf("Stopped by condition: %s\n", result.StopReason)
case result.FinishReason == types.FinishReasonStop:
    fmt.Println("Agent finished naturally")
}
```

### Defaults

| Scenario | Behavior |
|----------|----------|
| Neither `StopWhen` nor `MaxSteps` set | `ai.GenerateText` / `ai.StreamText` default to `IsStepCount(1)`: one model call, whose tool calls still execute (as in TS). `agent.NewToolLoopAgent` defaults to `IsStepCount(20)`. |
| `MaxSteps` set, `StopWhen` not set | Converts to `StopWhen{IsStepCount(MaxSteps)}` |
| Both set | `StopWhen` takes precedence; `MaxSteps` is ignored |

### MaxSteps (Deprecated)

`MaxSteps` is shorthand for `StopWhen: []ai.StopCondition{ai.IsStepCount(n)}`. It is still
supported for backwards compatibility but **new code should use `StopWhen` directly**.

```go
// Deprecated — works but prefer StopWhen
maxSteps := 5
ai.GenerateTextOptions{MaxSteps: &maxSteps}

// Preferred
ai.GenerateTextOptions{StopWhen: []ai.StopCondition{ai.IsStepCount(5)}}
```

---

## PrepareCall

`PrepareCall` is called immediately before each generation step. Use it to adjust the system
prompt, modify available tools, or change model parameters based on how many steps have already
run or how many tokens have been consumed.

```go
myAgent := agent.NewToolLoopAgent(agent.AgentConfig{
    Model: model,
    Tools: tools,
    StopWhen: []ai.StopCondition{ai.IsStepCount(10)},
    PrepareCall: func(ctx context.Context, config agent.PrepareCallConfig) agent.PrepareCallConfig {
        // Switch to a more concise prompt after the first few steps
        if config.StepNumber > 3 {
            config.System = "Be concise. Finalize your answer."
        }

        // Restrict available tools in later steps
        if config.StepNumber > 7 {
            config.Tools = finalizeTools
        }

        return config
    },
})
```

`PrepareCallConfig` exposes:

| Field | Type | Description |
|-------|------|-------------|
| `StepNumber` | `int` | Zero-based index of the upcoming step |
| `System` | `string` | System prompt for this call |
| `Messages` | `[]types.Message` | Conversation history so far |
| `Tools` | `[]types.Tool` | Tool set for this call |
| `Temperature` | `*float64` | Sampling temperature override |
| `MaxTokens` | `*int` | Token limit override |
| `AccumulatedUsage` | `types.Usage` | Total tokens used so far |
| `CustomData` | `interface{}` | Pass-through state between invocations |

---

## Manual Loop

For cases where you need complete control outside the agent abstraction — dynamic model
selection, custom context trimming, interleaved human input — you can implement the loop
yourself using `ai.GenerateText`.

```go
func manualAgentLoop(ctx context.Context, prompt string, tools []types.Tool) (string, error) {
    provider := openai.New(openai.Config{APIKey: os.Getenv("OPENAI_API_KEY")})
    model, _ := provider.LanguageModel("gpt-4")

    messages := []types.Message{
        {
            Role:    types.RoleUser,
            Content: []types.ContentPart{types.TextContent{Text: prompt}},
        },
    }

    for step := 0; step < 10; step++ {
        select {
        case <-ctx.Done():
            return "", ctx.Err()
        default:
        }

        result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
            Model:    model,
            Messages: messages,
            Tools:    tools,
        })
        if err != nil {
            return "", fmt.Errorf("step %d failed: %w", step, err)
        }

        // Natural completion — no tool calls
        if len(result.ToolCalls) == 0 {
            return result.Text, nil
        }

        // Append assistant turn
        messages = append(messages, types.Message{
            Role:    types.RoleAssistant,
            Content: []types.ContentPart{types.TextContent{Text: result.Text}},
        })

        // Execute tools and append results
        for _, toolCall := range result.ToolCalls {
            var toolResult interface{}
            var toolErr error
            for _, tool := range tools {
                if tool.Name == toolCall.ToolName {
                    toolResult, toolErr = tool.Execute(ctx, toolCall.Arguments, types.ToolExecutionOptions{})
                    break
                }
            }
            resultContent := map[string]interface{}{"result": toolResult}
            if toolErr != nil {
                resultContent = map[string]interface{}{"error": toolErr.Error()}
            }
            messages = append(messages, types.Message{
                Role: types.RoleTool,
                Content: []types.ContentPart{
                    types.ToolResultContent{
                        ToolCallID: toolCall.ID,
                        ToolName:   toolCall.ToolName,
                        Result:     resultContent,
                    },
                },
            })
        }
    }

    return "", fmt.Errorf("reached maximum steps without completion")
}
```

### When to Use a Manual Loop

Use a manual loop when you need:

- **Dynamic model selection** — swap models between steps based on complexity
- **Context trimming** — prune the message history to stay within token limits
- **Tool phase control** — expose different tool sets in different phases
- **Interleaved human input** — pause for approval between steps

For standard tool-calling agents, `StopWhen` + `PrepareCall` cover most needs without the
boilerplate.

---

## See Also

- [Stop-When Example](../../examples/agents/callbacks/stop-when/main.go) — focused example showing all three stop patterns
- [Configuring Call Options](https://goaisdk.com/docs/agents/configuring-call-options.md) — temperature, max tokens, and other options
- [Workflow Patterns](https://goaisdk.com/docs/agents/workflows.md) — structured agent designs
- [Error Handling](https://goaisdk.com/docs/ai-sdk-core/error-handling.md) — robust error handling
