Building the Agentic Loop: Autonomous Multi-Step Reasoning
Demystify AI agents from first principles. Learn how the fundamental 'while True' agentic loop enables models to iterate, call multiple tools, and solve complex goals autonomously.

TL;DR: A chatbot stops after one response. An AI Agent runs inside an iterative loop: it assesses the current state, decides which tools to call, inspects the tool results, reasons about what is still missing, and continues calling tools until the goal is fully achieved. Learn how to build a production-grade agent loop from scratch without heavy frameworks.
π¦ Interactive Notebook: Test and run this code live on GitHub:
09_agentic_loop.ipynbin the ai-architect-foundation-gemini repository.
What You'll Learn
What transforms an LLM into an autonomous agent, how the core while True agentic loop operates, how to handle parallel tool calls, and how to implement iteration guardrails to prevent infinite loops.
The Core Concept: Iteration vs. Single-Turn
Consider a multi-step customer request:
"Hi, my email is alex@example.com. I want a refund for order ORD-1001 because the keyboard arrived damaged."
To resolve this ticket, an application needs to:
- Look up the customer's account using their email.
- Fetch details for order
ORD-1001. - Check whether the order is eligible for a return.
- Process the refund transaction.
- Send a confirmation message to the customer.
In traditional software, you would hardcode an rigid if/else procedural pipeline.
In an Agentic Architecture, you provide the model with tools for each step. The model autonomously plans and executes the required sequence, adapting dynamically if an order is missing, already refunded, or requires escalation.
The Anatomy of the Agentic Loop
The entire foundation of frameworks like LangChain, CrewAI, and AutoGen reduces to this clean pattern:
from google import genai
from google.genai import types
client = genai.Client()
model = "gemini-2.5-flash"
def run_agentic_loop(
user_goal: str,
tools: list[types.Tool],
tool_map: dict,
system_instruction: str,
max_iterations: int = 8
) -> str:
"""Executes an autonomous agentic loop until completion or limit reached."""
# 1. Initialize conversation state
contents = [
types.Content(
role="user",
parts=[types.Part(text=user_goal)]
)
]
config = types.GenerateContentConfig(
tools=tools,
temperature=0.0,
system_instruction=system_instruction,
max_output_tokens=1000
)
iteration = 0
# 2. The Core Agent Loop
while iteration < max_iterations:
iteration += 1
print(f"\n--- Iteration {iteration} ---")
# Invoke the model with current state and available tools
response = client.models.generate_content(
model=model,
contents=contents,
config=config
)
candidate_content = response.candidates[0].content
contents.append(candidate_content) # Store model's thinking & tool requests
# Check for function calls
function_calls = [
part.function_call
for part in candidate_content.parts
if part.function_call
]
# If no tool calls were made, the agent has finished its task!
if not function_calls:
print("Agent has concluded reasoning.")
text_parts = [p.text for p in candidate_content.parts if p.text]
return "".join(text_parts)
# Execute each requested function call
tool_response_parts = []
for fc in function_calls:
tool_name = fc.name
tool_args = dict(fc.args)
print(f"π οΈ Agent calling tool: {tool_name}({tool_args})")
if tool_name in tool_map:
try:
result = tool_map[tool_name](**tool_args)
except Exception as e:
result = {"status": "error", "message": str(e)}
else:
result = {"status": "error", "message": f"Unknown tool '{tool_name}'"}
tool_response_parts.append(
types.Part.from_function_response(
name=tool_name,
response=result
)
)
# Feed tool execution results back to the model as a user turn
contents.append(
types.Content(role="user", parts=tool_response_parts)
)
raise RuntimeError(f"Agent exceeded maximum iterations ({max_iterations}). Possible loop detected.")Live Simulation: Multi-Step Return Case
Let's test our agent with mock customer tools:
# Mock Backend Functions
def lookup_customer(email: str) -> dict:
return {"customer_id": "CUST-441", "name": "Alex Smith", "email": email}
def lookup_order(order_id: str) -> dict:
return {"order_id": order_id, "customer_id": "CUST-441", "item": "Mechanical Keyboard", "amount": 89.99, "status": "DELIVERED"}
def check_return_policy(item: str) -> dict:
return {"eligible": True, "policy_window_days": 30}
def process_refund(order_id: str, amount: float) -> dict:
return {"status": "SUCCESS", "refund_id": "REF-9941", "amount_refunded": amount}
# Tool Declarations
tool_declarations = [
types.FunctionDeclaration(
name="lookup_customer",
description="Find customer profile by email.",
parameters=types.Schema(type="OBJECT", properties={"email": types.Schema(type="STRING")}, required=["email"])
),
types.FunctionDeclaration(
name="lookup_order",
description="Retrieve order details by order_id.",
parameters=types.Schema(type="OBJECT", properties={"order_id": types.Schema(type="STRING")}, required=["order_id"])
),
types.FunctionDeclaration(
name="check_return_policy",
description="Verify if an item is eligible for return.",
parameters=types.Schema(type="OBJECT", properties={"item": types.Schema(type="STRING")}, required=["item"])
),
types.FunctionDeclaration(
name="process_refund",
description="Process payment refund for an order.",
parameters=types.Schema(type="OBJECT", properties={"order_id": types.Schema(type="STRING"), "amount": types.Schema(type="NUMBER")}, required=["order_id", "amount"])
)
]
agent_tools = [types.Tool(function_declarations=tool_declarations)]
agent_tool_map = {
"lookup_customer": lookup_customer,
"lookup_order": lookup_order,
"check_return_policy": check_return_policy,
"process_refund": process_refund
}
system_prompt = """
You are EcommAssist AI. You have access to backend tools to verify customers, check orders, inspect policies, and process refunds.
Resolve the customer's request step-by-step. Only process a refund if the item is eligible.
"""
final_answer = run_agentic_loop(
user_goal="Hi, my email is alex@example.com. I want a refund for order ORD-1001 β the keyboard arrived damaged.",
tools=agent_tools,
tool_map=agent_tool_map,
system_instruction=system_prompt
)
print(f"\nFinal Response to Customer:\n{final_answer}")
# --- Execution Output ---
# π€ [Agent Turn 1] Tool Request: lookup_customer(email='alex@example.com')
# π§ [Tool Result] lookup_customer -> {'customer_id': 'CUST-441', 'name': 'Alex Smith', 'email': 'alex@example.com'}
#
# π€ [Agent Turn 2] Tool Request: lookup_order(order_id='ORD-1001')
# π§ [Tool Result] lookup_order -> {'order_id': 'ORD-1001', 'customer_id': 'CUST-441', 'item': 'Mechanical Keyboard', 'amount': 89.99, 'status': 'DELIVERED'}
#
# π€ [Agent Turn 3] Tool Request: check_return_policy(item='Mechanical Keyboard')
# π§ [Tool Result] check_return_policy -> {'eligible': True, 'policy_window_days': 30}
#
# π€ [Agent Turn 4] Tool Request: process_refund(order_id='ORD-1001', amount=89.99)
# π§ [Tool Result] process_refund -> {'status': 'SUCCESS', 'refund_id': 'REF-9941', 'amount_refunded': 89.99}
#
# π€ [Agent Turn 5] Final Text Response!
#
# Final Response to Customer:
# Hello Alex! I have verified your account and checked order ORD-1001 for the Mechanical Keyboard.
# Because the item is within our 30-day return window, your refund of $89.99 (Refund ID: REF-9941) has been approved and processed.
# The credit will appear on your statement within 3-5 business days.Execution Log Output
--- Iteration 1 ---
π οΈ Agent calling tool: lookup_customer({'email': 'alex@example.com'})
π οΈ Agent calling tool: lookup_order({'order_id': 'ORD-1001'})
--- Iteration 2 ---
π οΈ Agent calling tool: check_return_policy({'item': 'Mechanical Keyboard'})
--- Iteration 3 ---
π οΈ Agent calling tool: process_refund({'amount': 89.99, 'order_id': 'ORD-1001'})
--- Iteration 4 ---
Agent has concluded reasoning.
Final Response to Customer:
Hi Alex, I've verified your order ORD-1001 for the Mechanical Keyboard. Because it arrived damaged and meets our return policy, I have processed a full refund of $89.99 (Refund Ref: REF-9941). You should see the credit in 3-5 business days.Critical Production Guardrails
- Iteration Caps (
max_iterations): Always cap the loop (typically 6β10 turns) to prevent infinite loops from repetitive tool calls. - Batching Parallel Tool Calls: The model may return 2+ tool calls in a single turn. Execute all of them and return their results together in one user turn.
- Audit Trails: Persist every turn, tool argument, and tool response to a database for observability and compliance.
Key Takeaways
- Agents Iterate Autonomously: The model plans the multi-step trajectory dynamically based on tool responses.
- Framework-Free Core: The agentic loop is a concise
while Trueloop that you can easily write and customize in pure Python. - Always Add Guardrails: Protect against runaway loops, network timeouts, and tool exceptions.
What's Next
In Part 10: Handling Tool Errors Gracefully: Structured Resilient Conventions, we learn what happens when tools fail and how to teach agents to recover without crashing.