# How `max_turns` Controls Multi-Turn Agent Conversations in aisuite

> Learn how max_turns controls multi-turn agent conversations in aisuite by limiting LLM tool cycles for a final response. Master controlled conversational loops.

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

---

**`max_turns` activates automatic multi-turn tool execution by triggering a controlled conversation loop in the `Completions` class, strictly limiting how many tool-call cycles the LLM may perform before returning a final response.**

In the aisuite library, the `max_turns` parameter transforms single-shot API calls into autonomous agent workflows. When provided alongside a `tools` list, it instructs the client to manage the request-response cycle across multiple iterations automatically. This article examines the implementation details in [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py) to explain how `max_turns` orchestrates multi-turn agent conversations, enforces execution limits, and integrates with both synchronous and asynchronous providers.

## How `max_turns` Activates the Automatic Tool Runner

### Parameter Extraction and Mode Selection

The `create` method in [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py) intercepts `max_turns` before forwarding the request to any provider. At lines 94–100, the method pops both `max_turns` and `tools` from the keyword arguments:

```python
max_turns = kwargs.pop("max_turns", None)          # ← lines 94‑100

tools      = kwargs.pop("tools", None)

```

This extraction determines the execution mode. If `max_turns` is provided **and** a `tools` collection exists, the client invokes `Completions._tool_runner` (or `_atool_runner` for async), launching an automatic conversation loop. If `max_turns` is omitted, the call falls back to **manual tool-calling mode**, where the provider receives only the tool schemas and the caller must drive each tool invocation manually.

## The Execution Loop and Turn Limiting

Inside `_tool_runner`, aisuite implements a hard limit on conversation depth using a `while` loop that increments a `turns` counter after each tool execution round:

```python
turns = 0
while turns < max_turns:
    response = provider.chat_completions_create(model_name, messages, **kwargs)
    tool_calls = self._response_tool_calls(response)
    if not tool_calls:
        return self._finalize_runner_response(...)
    results, tool_messages = tools_instance.execute_tool(tool_calls, ...)
    messages.extend([response.choices[0].message, *tool_messages])
    turns += 1

```

*Source:* [`aisuite/client.py#L34-L73`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py#L34-L73)

The loop terminates under two conditions:

- **Completion:** The model returns no tool calls, indicating the conversation has reached a natural conclusion.
- **Limit reached:** The `turns` counter equals the user-supplied `max_turns`, forcing an immediate return regardless of whether additional tool calls remain pending.

Throughout the loop, aisuite accumulates state by extending the `messages` list with both the assistant's response and the tool execution results, ensuring the final output contains a complete trace of the multi-turn interaction.

## Constraints and Error Handling

The `max_turns` parameter cannot be combined with streaming responses. The helper method `_prepare_stream_kwargs` explicitly validates this constraint at lines 49–55, raising a clear error before any generator is created:

```python
if max_turns is not None:
    raise ValueError("stream=True cannot be combined with max_turns …")

```

*Source:* [`aisuite/client.py#L49-L55`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py#L49-L55)

This design choice prevents partial stream consumption during automatic tool execution, ensuring deterministic turn counting and message accumulation.

## Async Multi-Turn Support

For asynchronous workflows, aisuite provides `_atool_runner` (lines 84–111), which mirrors the synchronous logic using `await` for provider calls and tool execution. The `max_turns` limit applies identically in async contexts, allowing non-blocking multi-turn conversations with the same hard stop guarantees.

*Source:* [`aisuite/client.py#L84-L111`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py#L84-L111)

## Practical Implementation Examples

### Basic Synchronous Multi-Turn Conversation

The following example allows up to three tool-execution rounds to answer a time-related query:

```python
from aisuite import Client

client = Client()

def get_current_time():
    import datetime
    return {"time": datetime.datetime.utcnow().isoformat()}

response = client.chat.completions.create(
    model="openai:gpt-4o-mini",
    messages=[{"role": "user", "content": "What time is it?"}],
    tools=[get_current_time],      # callable tool

    max_turns=3,                  # allow up to 3 tool‑execution rounds

)
print(response.choices[0].message.content)

```

### Async Usage with Turn Limiting

For async providers, use `acreate` with `max_turns` to control conversation depth:

```python
import asyncio
from aisuite import Client

async def main():
    client = Client()
    
    async def fetch_weather():
        return {"weather": "sunny"}

    resp = await client.chat.completions.acreate(
        model="anthropic:claude-3-5-sonnet",
        messages=[{"role": "user", "content": "Will it rain tomorrow?"}],
        tools=[fetch_weather],
        max_turns=2,
    )
    print(resp.choices[0].message.content)

asyncio.run(main())

```

### Manual Tool Calling (Without `max_turns`)

When `max_turns` is omitted, the client operates in manual mode. The provider receives tool schemas, but you must inspect the response and invoke tools yourself:

```python
client = Client()
response = client.chat.completions.create(
    model="openai:gpt-4o-mini",
    messages=[{"role": "user", "content": "Tell me a joke"}],
    tools=[lambda: {"joke": "Why did the chicken..."}]   # schemas only, no max_turns

)

# Check response.choices[0].message.tool_calls manually and invoke as needed

```

## Summary

- **`max_turns` triggers automation:** Setting this parameter alongside `tools` activates `_tool_runner`, which manages the conversation loop automatically instead of requiring manual orchestration.
- **Hard limit enforcement:** The loop increments a counter after each tool execution, exiting immediately when `turns` reaches `max_turns` or when no tool calls remain.
- **State preservation:** All intermediate assistant messages and tool results are appended to the conversation history, providing complete traceability.
- **Streaming incompatibility:** `max_turns` cannot be used with `stream=True`; attempting this combination raises a `ValueError` during parameter preparation.
- **Async parity:** The `_atool_runner` method implements identical turn-limiting logic for asynchronous clients, ensuring consistent behavior across sync and async implementations.

## Frequently Asked Questions

### What happens when `max_turns` is reached before the conversation completes?

When the turn counter equals the `max_turns` value, the loop terminates immediately and returns the current state of the conversation. According to the implementation in [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py), the final response includes all accumulated messages up to that point, but any pending tool calls are not executed. This prevents infinite loops while preserving the conversation history for inspection or resumption.

### Can I use `max_turns` with streaming responses?

No. The aisuite source code explicitly prohibits this combination. In `_prepare_stream_kwargs` (lines 49–55), the code checks for the presence of `max_turns` when `stream=True` and raises a `ValueError` stating that these features cannot be combined. This restriction ensures that the client can accurately count turns and accumulate complete messages rather than handling partial stream chunks.

### How does `max_turns` differ from manual tool calling?

Manual tool calling occurs when you provide a `tools` list without specifying `max_turns`. In this mode, [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py) forwards the tool schemas to the provider but returns control to your code immediately after the first response. You must manually check for `tool_calls` in the response, execute the functions, and submit new requests. Setting `max_turns` activates `_tool_runner`, which automates this cycle for the specified number of iterations.

### Does `max_turns` work with asynchronous clients?

Yes. The `aisuite` library provides `_atool_runner` (lines 84–111 in [`client.py`](https://github.com/andrewyng/aisuite/blob/main/client.py)), which implements the same turn-counting logic using async/await patterns. When using `client.chat.completions.acreate()`, passing `max_turns` invokes this async runner, allowing non-blocking multi-turn conversations with identical limit enforcement to the synchronous implementation.