How `max_turns` Controls Multi-Turn Agent Conversations in aisuite
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 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 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:
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:
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
The loop terminates under two conditions:
- Completion: The model returns no tool calls, indicating the conversation has reached a natural conclusion.
- Limit reached: The
turnscounter equals the user-suppliedmax_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:
if max_turns is not None:
raise ValueError("stream=True cannot be combined with max_turns …")
Source: 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
Practical Implementation Examples
Basic Synchronous Multi-Turn Conversation
The following example allows up to three tool-execution rounds to answer a time-related query:
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:
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:
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_turnstriggers automation: Setting this parameter alongsidetoolsactivates_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
turnsreachesmax_turnsor 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_turnscannot be used withstream=True; attempting this combination raises aValueErrorduring parameter preparation. - Async parity: The
_atool_runnermethod 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, 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 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), 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →