# max_turns in aisuite: Automated Tool Execution vs. Manual Tool Calling Explained

> Understand the difference between max_turns automated tool execution and manual tool calling in aisuite. Learn how to control tool execution effectively.

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

---

**In aisuite, `max_turns` limits how many automatic tool calls the `Client` will execute before stopping, while manual tool calling gives developers full control with no built-in recursion limit.**

The `aisuite` library provides two distinct patterns for integrating LLMs with external tools. Understanding when to use automated execution versus manual control helps you build reliable agent workflows without unexpected loops or runaway API calls.

## What `max_turns` Controls in Automated Tool Execution

When you use **automated tool execution**, the `Client` class manages the entire conversation loop. The `max_turns` parameter acts as a safety guard that caps how many times the client will automatically invoke tools before returning control to your code.

According to the aisuite source code, the recursion logic lives in [`aisuite/mcp/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/mcp/client.py). The client's `run()` method contains a loop that checks `self.max_turns` after each successful tool execution. If the model repeatedly requests tool calls, the loop exits once the limit is reached and returns the final response state.

This prevents infinite recursion when a model gets stuck in a tool-calling pattern or when tool results don't satisfy the model's completion criteria.

## How Automated Tool Execution Works

**Automated execution** is the "fire-and-forget" approach. You provide a prompt and the client handles all tool interactions internally.

The workflow follows these steps:

1. `client.run()` sends your prompt to the LLM
2. If the response contains a tool call, the client executes the registered tool function
3. The tool result is appended to the conversation context and sent back to the LLM
4. Steps 2-3 repeat until the model stops requesting tools **or** `max_turns` is exhausted

When the limit is exceeded, the client raises a `MaxTurnsExceededError` (or returns a partial trace), allowing your code to decide how to proceed.

```python
from aisuite.mcp import Client

# Allow up to 3 automatic tool calls

client = Client(max_turns=3)

response = client.run(
    "Get the current weather for London and summarize it."
)

print(response.text)   # Final LLM response

print(response.turns)  # Actual tool calls used: 0-3

```

This pattern suits simple data lookups, single-step transformations, or any scenario where you trust the model to resolve its needs within a bounded number of interactions.

## Manual Tool Calling: Full Developer Control

**Manual tool calling** removes the automatic loop entirely. You retrieve raw LLM output, inspect it for tool suggestions, and explicitly invoke `run_tool()` when and how you choose.

No `max_turns` parameter applies because you write the control flow yourself. This gives you:

- Fine-grained conditional logic between calls
- Custom error handling and retry policies
- Ability to transform or validate tool arguments before execution
- Explicit budget control over API usage

```python
from aisuite.mcp import Client

client = Client()  # No max_turns needed for manual mode

# Get raw LLM output without automatic tool execution

raw = client.run_raw("What is the population of Tokyo?")
print(raw)  # Inspect: model may suggest a "search" tool

# Manually execute the suggested tool

if "search" in raw.suggested_tools:
    result = client.run_tool("search", query="Tokyo population 2024")
    print(result)

    # Decide whether to continue, modify, or stop

    follow_up = client.run_raw(f"Based on this data: {result}, what trends do you see?")

```

The loop implementation—whether single-shot, fixed-iteration, or dynamically conditioned—is entirely your responsibility.

## Key Implementation Differences

| Aspect | Automated (`max_turns`) | Manual |
|--------|------------------------|--------|
| Entry point | `client.run()` | `client.run_raw()` + `client.run_tool()` |
| Recursion control | `max_turns` parameter enforces hard limit | Developer implements all looping logic |
| Error on limit | `MaxTurnsExceededError` raised | Not applicable |
| Typical use case | Quick, trusted workflows | Complex multi-step reasoning, custom orchestration |
| Code complexity | Minimal client code | Requires explicit state management |

## Where to Find the Implementation

The core differentiation logic resides in these source files:

- **[`aisuite/mcp/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/mcp/client.py)** — Contains the `Client` class, the `max_turns` attribute, and the automated execution loop in `run()`
- **[`aisuite/mcp/schema_converter.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/mcp/schema_converter.py)** — Defines message schemas that the client parses to detect tool call requests
- **[`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py)** — Registry of available tool functions for invocation

Test coverage illustrating both patterns:

- [`tests/client/test_manual_tool_calling.py`](https://github.com/andrewyng/aisuite/blob/main/tests/client/test_manual_tool_calling.py) — Demonstrates explicit tool calls without automatic recursion
- [`tests/client/test_async_client.py`](https://github.com/andrewyng/aisuite/blob/main/tests/client/test_async_client.py) — Shows `max_turns` behavior in async client configurations

## Choosing Between the Two Patterns

Use **automated tool execution with `max_turns`** when you want minimal boilerplate and can tolerate bounded automatic behavior. Set `max_turns` conservatively—usually 3-5 turns suffice for simple tasks.

Use **manual tool calling** when you need visibility into intermediate states, must inject business logic between calls, or want to implement sophisticated retry and fallback strategies. The extra code investment pays off in deterministic, auditable workflows.

## Summary

- **`max_turns`** caps automatic tool recursion in `Client.run()`, preventing runaway loops
- **Automated execution** handles tool detection, invocation, and context management internally
- **Manual calling** via `run_raw()` and `run_tool()` gives complete control but requires explicit loop implementation
- Both patterns share the same underlying tool registry in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py)
- Error handling differs: automated mode raises `MaxTurnsExceededError`; manual mode delegates all error handling to your code

## Frequently Asked Questions

### What happens when `max_turns` is reached during automated execution?

The client stops the automatic loop and raises a `MaxTurnsExceededError` (or returns a partial response trace depending on configuration). Your calling code can catch this exception and decide whether to extend the conversation, return a degraded response, or escalate to human review.

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

Yes. Instantiate separate `Client` instances for each pattern, or use a single client and switch between `run()` for automated passages and `run_raw()`/`run_tool()` for sections requiring manual control. The client state is preserved across calls, allowing hybrid workflows.

### Does `max_turns` count LLM response turns or tool execution turns?

Tool execution turns. Each time the client invokes a registered tool and feeds the result back to the model counts as one turn toward the `max_turns` limit. Pure text responses from the LLM without tool requests do not increment the counter.

### Is there a default `max_turns` value if I don't specify one?

The aisuite source code configures a default—typically conservative to prevent unexpected costs. Check the `Client` constructor in [`aisuite/mcp/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/mcp/client.py) for the current release's specific default, or explicitly set `max_turns` to control behavior regardless of version changes.