Understanding the Tool Execution Loop and the max_turns Parameter in aisuite
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 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:
- Provider invocation – The current message list is sent to the provider via
provider.chat_completions_create. - Response handling –
_handle_model_responseinspects the model output. - Tool execution – If the model emitted tool calls,
tools_instance.execute_toolruns the requested functions (respectingtool_policysettings). - History update – Results are converted into tool‑message objects and appended to the conversation history.
- 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.
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.
Implementation Reference
Key source files governing the loop behavior include:
aisuite/client.py– Contains_tool_runner,_atool_runner, and thecreatemethod that parsesmax_turnsat lines 298‑300.examples/agents/simple_agent.py– Demonstrates practical usage withmax_turns=3at lines 33‑38.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:
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:
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:
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_runnerand_atool_runnerinaisuite/client.py, automatically handling multi‑turn tool interactions. max_turnsacts 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_turnsis provided, the library manages tool calling, history updates, and response finalization; when omitted, the raw model output is returned for manual handling. - The final
ChatCompletionResponseincludes 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), 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. 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.
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 →