Tool Call Loop
The tool call loop is the agentic reasoning cycle — the LLM calls a tool, receives a result, reasons about it, and may call another tool. The loop continues until the LLM produces a final text response (or the maximum iteration limit is reached).
The Agentic Loop
public class AgentReasoningLoop
{
private const int MaxIterations = 10;
public async Task<AgentResponse> RunAsync(
AgentComposite agent,
IReadOnlyList<LLMMessage> messages,
CancellationToken ct)
{
var workingMessages = messages.ToList();
int iterations = 0;
while (iterations < MaxIterations)
{
iterations++;
// Call the LLM with current messages + tool schemas
var response = await _llmProvider.CompleteAsync(
workingMessages,
agent.ToolRegistry.GetSchemas(),
agent.LLMConfig,
ct);
// No tool calls — LLM produced a final text response
if (!response.ToolCalls.Any())
return new AgentResponse { Content = response.Content };
// Append the LLM's assistant message (with tool_calls) to history
workingMessages.Add(new LLMMessage(Role.Assistant, response));
// Execute each tool call and append results
foreach (var toolCall in response.ToolCalls)
{
string result = await agent.ToolRegistry.ExecuteAsync(
toolCall, _conversationContext, ct);
workingMessages.Add(new LLMMessage(Role.Tool)
{
ToolCallId = toolCall.Id,
Content = result
});
}
// Loop continues — LLM sees tool results and produces next response
}
// Max iterations reached — return partial response
return new AgentResponse
{
Content = "I was unable to complete this task within the allowed steps.",
IsIncomplete = true
};
}
}
Parallel Tool Calls
Modern LLMs (Claude 3+, GPT-4o) can request multiple tool calls simultaneously in a single response. Octopus executes them in parallel:
// LLM response with parallel tool calls:
{
"role": "assistant",
"tool_calls": [
{ "id": "tc_01", "name": "get_leave_balance", "input": { "employeeId": "EMP-1042" } },
{ "id": "tc_02", "name": "get_manager_details", "input": { "employeeId": "EMP-1042" } }
]
}
// Octopus executes both in parallel:
var tasks = response.ToolCalls.Select(tc =>
agent.ToolRegistry.ExecuteAsync(tc, context, ct));
var results = await Task.WhenAll(tasks);
// Append all results (maintaining tool_call_id pairing)
foreach (var (call, result) in response.ToolCalls.Zip(results))
{
workingMessages.Add(new LLMMessage(Role.Tool)
{
ToolCallId = call.Id,
Content = result
});
}
Loop Control Settings
| Setting | Default | Purpose |
|---|---|---|
MaxToolIterations | 10 | Maximum tool call rounds before the loop exits with an incomplete response |
ToolTimeoutSeconds | 30 | Timeout per individual tool execution |
ParallelToolExecution | true | Execute parallel tool calls concurrently; disable if tools have side effects that must be sequential |
Loop Termination Conditions
| Condition | Behaviour |
|---|---|
| LLM returns text with no tool calls | Normal exit — text response returned to user |
| Max iterations reached | Partial exit — incomplete response message returned |
| Tool execution error | Error result injected into context; LLM decides how to proceed |
LLM requests end_turn stop reason | Normal exit — provider-specific stop signal |
| CancellationToken cancelled | Immediate exit; partial response or error returned |
A poorly written tool or an LLM that keeps calling the same tool can create an infinite loop. The MaxToolIterations limit is a hard safety cap. Set it to the maximum number of steps your most complex task requires — typically 5–10. If you observe agents hitting this limit, investigate whether the tool descriptions are clear enough for the LLM to know when it has enough information.