# Understanding the Tool Execution Loop and the max_turns Parameter in aisuite

> Explore the aisuite tool execution loop and max_turns parameter. Learn how max_turns limits automatic tool calls for efficient chatbot responses.

- Repository: [Andrew Ng/aisuite](https://github.com/andrewyng/aisuite)
- Tags: deep-dive
- Published: 2026-08-04

---

**The `max_turns` parameter caps the number of back‑and‑forth tool calls aisuite performs automatically, returning a bundled `ChatCompletionResponse` once the limit is reached or the model stops requesting tools.**

aisuite unifies multiple LLM providers under a single client interface. When you enable automatic tool handling via the `max_turns` argument, the library enters a **tool execution loop** that manages multi‑turn function calls, message history, and response finalization without manual intervention.

## How the Tool Execution Loop Works

The loop is implemented in [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py) and triggered whenever `max_turns` appears in the arguments to `Client.create`.

### Entry Point and Initialization

When you call `client.chat.completions.create()`, the method extracts `max_turns` (along with optional `tools` and `tool_policy`) from the keyword arguments at lines 94‑98. If present, the request is routed to the internal `_tool_runner` (synchronous) or `_atool_runner` (asynchronous) rather than the standard single‑shot completion path.

Before the first provider call, `_init_tool_runner` normalizes the supplied callables or tool specifications into a **tools instance**. This instance wraps the executable functions and enforces any policy constraints defined by the caller.

### The Iteration Cycle

The runner implements a `for` loop that executes up to `max_turns` iterations. During each turn, the following sequence occurs:

1. **Provider invocation** – The current message list is sent to the provider via `provider.chat_completions_create`.
2. **Response handling** – `_handle_model_response` inspects the model output.
3. **Tool execution** – If the model emitted tool calls, `tools_instance.execute_tool` runs the requested functions (respecting `tool_policy` settings).
4. **History update** – Results are converted into tool‑message objects and appended to the conversation history.
5. **Counter increment** – The turn counter advances.

This cycle repeats until the model produces a response with no tool calls or the loop exhausts its allocated turns. The synchronous implementation occupies lines 334‑376, while the asynchronous counterpart spans lines 383‑445 in [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py).

### Termination and Finalization

Once the loop exits, `_finalize_runner_response` bundles three artifacts into a single `ChatCompletionResponse`:

- The **final model response** (text content).
- All **intermediate responses** generated during tool rounds.
- The complete **message trace** including user prompts, assistant replies, and tool results.

This unified return value allows you to inspect the full reasoning chain or simply read the final answer.

## Why the max_turns Parameter Matters

Setting a turn limit serves critical production needs:

- **Safety** – Prevents runaway loops where a model might recursively request new tools indefinitely.
- **Predictability** – Provides a hard bound on latency and token consumption, essential for cost‑conscious deployments.
- **Control** – Lets you choose between fully automatic handling (`max_turns` > 0) and manual orchestration (omit the parameter).

If you omit `max_turns`, aisuite operates in **manual mode**. The raw provider response—including OpenAI‑style tool‑call JSON—is returned immediately, leaving loop management to your application code. This distinction is validated in [`tests/client/test_manual_tool_calling.py`](https://github.com/andrewyng/aisuite/blob/main/tests/client/test_manual_tool_calling.py).

## Implementation Reference

Key source files governing the loop behavior include:

- **[`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py)** – Contains `_tool_runner`, `_atool_runner`, and the `create` method that parses `max_turns` at lines 298‑300.
- **[`examples/agents/simple_agent.py`](https://github.com/andrewyng/aisuite/blob/main/examples/agents/simple_agent.py)** – Demonstrates practical usage with `max_turns=3` at lines 33‑38.
- **[`README.md`](https://github.com/andrewyng/aisuite/blob/main/README.md)** – Lines 104‑126 provide user‑facing documentation of the parameter.
- **`tests/mcp/test_*_e2e.py`** – End‑to‑end tests exercising various turn limits against live providers.

## Practical Code Examples

### Automatic Tool Execution with max_turns

Enable the loop by passing `max_turns` and a list of tools:

```python
import aisuite as ai

def get_weather(city: str) -> str:
    """Simple weather lookup tool."""
    return f"The weather in {city} is sunny."

client = ai.Client()
response = client.chat.completions.create(
    model="openai:gpt-4o",
    messages=[{"role": "user", "content": "Plan a picnic in San Francisco tomorrow."}],
    tools=[get_weather],
    max_turns=2,  # Allow up to two tool‑call rounds

)

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

```

In this example, the model may request the weather, aisuite executes `get_weather`, injects the result into the message history, and proceeds to the final answer within the allotted turns.

### Manual Mode Without max_turns

Omit `max_turns` to receive the raw tool‑call payload and handle execution yourself:

```python
response = client.chat.completions.create(
    model="openai:gpt-4o",
    messages=[{"role": "user", "content": "What is the weather?"}],
    tools=[{"type": "function", "function": {"name": "get_weather",
                                             "parameters": {"type": "object",
                                                            "properties": {"city": {"type": "string"}}}}}],
    # No max_turns → returns raw tool‑call JSON

)

# Inspect and handle manually

if response.choices[0].message.tool_calls:
    # Caller must parse and execute tools

    pass

```

### Agent-Based Usage

The `Agent` and `Runner` classes wrap the same logic for higher‑level orchestration:

```python
agent = ai.Agent(
    name="weather_assistant",
    model="openai:gpt-4o",
    instructions="Use tools when they help.",
    tools=[get_weather],
)

result = ai.Runner.run_sync(
    agent,
    "Is it going to rain in Seattle tomorrow?",
    max_turns=3
)
print(result.final_output)
result.print_trace()  # Displays full tool‑call history

```

## Summary

- The **tool execution loop** in aisuite is driven by `_tool_runner` and `_atool_runner` in [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py), automatically handling multi‑turn tool interactions.
- **`max_turns`** acts as a safety cap, preventing infinite loops and bounding token usage; it is parsed at lines 94‑98 and enforced throughout the iteration cycle.
- When `max_turns` is provided, the library manages tool calling, history updates, and response finalization; when omitted, the raw model output is returned for manual handling.
- The final `ChatCompletionResponse` includes the complete message trace, enabling full auditability of the tool execution flow.

## Frequently Asked Questions

### What happens when max_turns is reached before the model finishes?

If the iteration count hits the `max_turns` limit while the model still requests tools, aisuite exits the loop and returns the current state in the `ChatCompletionResponse`. The final response will contain the last assistant message, and the message trace will show all tool calls executed up to that point. You can inspect `response.choices[0].message` to determine whether the conversation completed or requires additional rounds.

### Can I mix automatic and manual tool calling in the same application?

Yes. You decide per‑request by either including or omitting `max_turns`. For workflows requiring tight control over specific tool invocations, omit the parameter and parse `response.choices[0].message.tool_calls` manually. For autonomous agent behavior, set `max_turns` to an appropriate integer and let aisuite drive the interaction.

### How does aisuite handle asynchronous tool execution?

The async path uses `_atool_runner` (lines 383‑445 in [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py)), which awaits both the provider call (`provider.chat_completions_create`) and the tool execution (`tools_instance.execute_tool`). This ensures that I/O‑bound tools—such as HTTP requests or database queries—do not block the event loop, making it suitable for high‑concurrency applications.

### Where is the loop logic located in the source code?

The core loop implementation resides in [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py). The synchronous version spans lines 334‑376, while the asynchronous version occupies lines 383‑445. Parameter extraction occurs earlier in the same file at lines 94‑98, and response finalization is handled by `_finalize_runner_response` immediately after the loop terminates.