# How max_turns Controls Multi-Turn Tool Calling Loops in aisuite

> Discover how max_turns controls multi-turn tool calling loops in aisuite by capping LLM to tool interaction cycles, ensuring timely loop termination and final response delivery.

- Repository: [Andrew Ng/aisuite](https://github.com/andrewyng/aisuite)
- Tags: internals
- Published: 2026-07-28

---

**The `max_turns` parameter caps the number of LLM-to-tool interaction cycles in aisuite, automatically terminating the loop and returning the final response when the limit is reached.**

In the aisuite Python library, `max_turns` serves as the primary safety mechanism and control knob for autonomous agent behaviors. This parameter determines exactly how many back-and-forth exchanges can occur between the language model and registered tools before the client forcibly terminates the loop, preventing runaway execution while enabling complex multi-step reasoning.

## How max_turns Works in Automatic Mode

When you instantiate `ChatClient` or `AsyncChatClient` with a `max_turns` value, aisuite enters **automatic tool-execution mode**. The client runs an internal loop that manages the entire conversation flow without manual intervention.

### The LLM ↔ Tool Interaction Cycle

The automatic loop follows this exact sequence as implemented in [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py):

1. The client sends the current conversation history and tool schema to the LLM provider.
2. If the LLM returns a tool call (function name and arguments), the client executes the registered function.
3. The client constructs a tool-result message and feeds it back to the LLM.
4. Steps 2-3 repeat until either the LLM stops requesting tools or the cycle count reaches `max_turns`.

Each complete LLM response followed by a tool execution constitutes one "turn" toward the limit.

### Loop Termination Behavior

When the interaction counter equals the `max_turns` value, the client immediately exits the loop. It returns the **last LLM message** (the one that would have triggered the next tool invocation) without executing any further tools. This hard stop prevents infinite loops in scenarios where the model might otherwise keep requesting tools indefinitely.

## Manual Mode: When max_turns Is Absent

Omitting `max_turns` switches the client to **manual mode**. In this configuration, as demonstrated in [`tests/client/test_manual_tool_calling.py`](https://github.com/andrewyng/aisuite/blob/main/tests/client/test_manual_tool_calling.py), the client does not run the automatic loop. Instead, it forwards the tool schema to the provider exactly once and expects the caller—typically a UI or external orchestrator—to handle tool invocation manually via `client.run_tool()`.

This distinction is deliberate: the absence of `max_turns` signals that the developer wants full control over the execution flow, while its presence delegates loop management to aisuite.

## Validation Rules and Constraints

The client validates `max_turns` at construction time in [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py). The parameter must be an integer greater than or equal to 2. If you provide a value less than 2 or a non-integer, the constructor raises a `ValueError`. This constraint ensures that every automatic loop includes at least one LLM turn and the possibility of one tool execution.

According to the test suite in [`tests/client/test_client.py`](https://github.com/andrewyng/aisuite/blob/main/tests/client/test_client.py) (specifically around line 71), this validation is strictly enforced before any network requests occur.

## Practical Configuration Examples

The following patterns demonstrate how to configure `max_turns` for different operational modes:

### Automatic Mode with Safety Cap

Use this configuration for autonomous agents that need to perform research or multi-step data gathering without human intervention:

```python
import aisuite

# Limit to 3 turns: LLM → tool → LLM → tool → final LLM

client = aisuite.ChatClient(
    model="openai:gpt-4o",
    tools=[get_weather, get_current_time],
    max_turns=3,
)

resp = client.chat.completions.create(
    messages=[{"role": "user", "content": "What's the weather and current time?"}]
)

# Returns after at most 3 complete cycles

```

### Manual Mode for UI-Driven Workflows

Use this when your application handles tool execution separately:

```python
import aisuite

# No max_turns → caller must invoke tools manually

client = aisuite.ChatClient(
    model="openai:gpt-4o",
    tools=[get_weather, get_current_time],
)

# Provider receives tool schemas, but execution is external

```

## Implementation Details and Testing

The `max_turns` logic resides in the `ChatClient` and `AsyncChatClient` classes within [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py). The implementation maintains an internal counter that increments with each completed tool execution, comparing it against the supplied threshold before initiating subsequent LLM requests.

The test coverage spans multiple files:

- **[`tests/client/test_client.py`](https://github.com/andrewyng/aisuite/blob/main/tests/client/test_client.py)** (lines 236-308): Verifies that `max_turns` propagates correctly to providers and that loops terminate at the specified limit.
- **[`tests/client/test_manual_tool_calling.py`](https://github.com/andrewyng/aisuite/blob/main/tests/client/test_manual_tool_calling.py)**: Confirms that omitting `max_turns` disables automatic execution while still registering tools with the provider.
- **[`tests/mcp/test_e2e.py`](https://github.com/andrewyng/aisuite/blob/main/tests/mcp/test_e2e.py)**, **[`tests/mcp/test_llm_e2e.py`](https://github.com/andrewyng/aisuite/blob/main/tests/mcp/test_llm_e2e.py)**, **[`tests/mcp/test_http_llm_e2e.py`](https://github.com/andrewyng/aisuite/blob/main/tests/mcp/test_http_llm_e2e.py)**: Contain end-to-end examples using `max_turns=2` through `max_turns=5` in real multi-turn MCP interactions.

Documentation references in [`guides/openai.md`](https://github.com/andrewyng/aisuite/blob/main/guides/openai.md) and [`guides/anthropic.md`](https://github.com/andrewyng/aisuite/blob/main/guides/anthropic.md) also illustrate provider-specific usage patterns for this parameter.

## Summary

- **`max_turns`** acts as a hard ceiling on LLM-tool interaction cycles, preventing infinite loops while enabling autonomous agent workflows.
- When provided, the client enters **automatic mode**, executing tools and feeding results back to the LLM until the limit is reached or the LLM stops calling tools.
- When omitted, the client operates in **manual mode**, requiring external callers to handle tool invocation via the exposed API.
- The parameter requires an integer **≥ 2**, validated at construction time in [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py).
- End-to-end tests in the MCP test suite demonstrate practical usage with values ranging from 2 to 30 turns depending on workflow complexity.

## Frequently Asked Questions

### What happens when the max_turns limit is reached?

When the interaction counter reaches the specified `max_turns` value, the client returns the last LLM response without executing any pending tool calls. This final message represents the model's output at the moment the safety limit triggered, effectively capping the reasoning depth while preserving the conversation state.

### Can max_turns be set to 1?

No. The aisuite client explicitly validates that `max_turns` must be an integer greater than or equal to 2. Attempting to set `max_turns=1` raises a `ValueError` during client initialization, ensuring that every automatic loop supports at least one complete LLM-to-tool exchange cycle.

### How does manual tool calling differ from automatic mode?

In manual mode (when `max_turns` is omitted), the `ChatClient` does not run the internal execution loop. It transmits the tool schema to the provider but returns control immediately after the LLM response, expecting the caller to parse any tool calls and invoke `client.run_tool()` externally. Automatic mode handles this parsing and execution internally, cycling until completion or until hitting the `max_turns` limit.

### Where is max_turns validated in the codebase?

The validation occurs in the `ChatClient` constructor within [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py). The implementation checks that the value is an integer ≥ 2, raising a `ValueError` otherwise. Unit tests in [`tests/client/test_client.py`](https://github.com/andrewyng/aisuite/blob/main/tests/client/test_client.py) (around line 71) verify this behavior, while integration tests throughout the MCP test suite confirm the parameter's effect on loop termination.