The Agent Loop
The loop is the heart of the agent. It receives messages, decides actions, calls tools, and repeats until done.
Here’s the pseudocode:
while not done:
send messages + tool definitions to model
if model asks for tool:
call local function
append tool result to messages
else:
return final answer
Full Implementation
Here’s the complete agent loop in Python:
import json
import openai
from typing import List, Dict, Any, Optional
def run_agent_conversation(
user_message: str,
user_id: Optional[str] = None,
max_steps: int = 5
) -> Dict[str, Any]:
"""
Run the agent loop until we get a final answer or escalation.
Returns:
{
"type": "answered" | "escalated",
"reply": str,
"trace": List[Dict] # Step-by-step trace
}
"""
# Initialize
messages = [
{"role": "system", "content": SYSTEM_PROMPT}
]
if user_id:
messages.append({
"role": "system",
"content": f"User ID: {user_id}"
})
messages.append({
"role": "user",
"content": user_message
})
trace = []
step = 0
# Main loop
while step < max_steps:
step += 1
# Call LLM
response = openai.ChatCompletion.create(
model="gpt-4",
messages=messages,
tools=TOOLS,
tool_choice="auto"
)
message = response.choices[0].message
assistant_message = {"role": "assistant", "content": message.content}
# Check for tool calls
if message.tool_calls:
tool_calls_data = []
for tool_call in message.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
# Log to trace
trace.append({
"step": step,
"action": "tool_call",
"tool_name": tool_name,
"tool_args": tool_args,
"timestamp": get_timestamp()
})
# Execute tool
result = execute_tool(tool_name, tool_args, user_id)
tool_calls_data.append({
"tool_call_id": tool_call.id,
"name": tool_name,
"result": result
})
# Add tool result to messages
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
# Add assistant message with tool calls
assistant_message["tool_calls"] = [
{
"id": tc["tool_call_id"],
"type": "function",
"function": {
"name": tc["name"],
"arguments": json.dumps(tc["result"])
}
}
for tc in tool_calls_data
]
messages.append(assistant_message)
# Continue loop to process tool results
continue
# No tool calls - check if it's escalation or answer
content = message.content.lower()
if should_escalate(content, user_message):
trace.append({
"step": step,
"action": "escalation",
"reason": "Detected escalation trigger",
"timestamp": get_timestamp()
})
# Log escalation
log_escalation(user_id or "unknown", user_message, "auto_detected")
return {
"type": "escalated",
"reply": "I'm connecting you with our support team. They'll be able to help you better.",
"trace": trace
}
# Final answer
trace.append({
"step": step,
"action": "final_answer",
"final_reply": message.content,
"timestamp": get_timestamp()
})
return {
"type": "answered",
"reply": message.content,
"trace": trace
}
# Max steps reached
return {
"type": "escalated",
"reply": "I'm having trouble processing your request. Let me connect you with a human agent.",
"trace": trace
}
def execute_tool(tool_name: str, args: Dict, user_id: Optional[str]) -> Dict:
"""Execute a tool by name."""
if tool_name == "get_faq_answer":
return get_faq_answer(args["question"])
elif tool_name == "get_subscription_status":
# Use user_id from context if not in args
uid = args.get("user_id") or user_id
if not uid:
return {"error": "user_id required"}
return get_subscription_status(uid)
elif tool_name == "log_escalation":
return log_escalation(
args.get("user_id") or user_id or "unknown",
args["message"],
args["reason"]
)
else:
return {"error": f"Unknown tool: {tool_name}"}
def should_escalate(content: str, original_message: str) -> bool:
"""Check if message should be escalated."""
risky_keywords = [
"refund", "chargeback", "cancel", "delete account",
"legal", "lawsuit", "lawyer"
]
return any(keyword in content for keyword in risky_keywords)
Message History
The agent keeps a conversation history. Each message is added to the list:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": "What's my subscription?"},
{"role": "assistant", "content": None, "tool_calls": [...]},
{"role": "tool", "tool_call_id": "...", "content": "..."},
{"role": "assistant", "content": "Your plan is Pro..."}
]
This history gives the agent context. It can see what tools it called and what results it got.
Max Steps Protection
The max_steps parameter prevents infinite loops:
max_steps = 5 # Stop after 5 iterations
If the agent keeps calling tools without answering, it escalates. This prevents:
- Infinite tool calling
- Cost overruns
- Poor user experience
Trace System
Every step is logged to a trace:
trace = [
{
"step": 1,
"action": "tool_call",
"tool_name": "get_subscription_status",
"tool_args": {"user_id": "user-123"},
"timestamp": "10:23:15"
},
{
"step": 2,
"action": "final_answer",
"final_reply": "Your plan is Pro...",
"timestamp": "10:23:16"
}
]
This trace helps you:
- Debug what went wrong
- Understand agent decisions
- Show users what happened
- Improve the agent
Example Runs
Example 1: Simple FAQ
result = run_agent_conversation(
"What is SimpleSaaS?",
user_id=None
)
# Result:
# {
# "type": "answered",
# "reply": "SimpleSaaS is a subscription management platform...",
# "trace": [{"step": 1, "action": "final_answer", ...}]
# }
Example 2: Subscription Check
result = run_agent_conversation(
"What's my current plan?",
user_id="user-123"
)
# Result:
# {
# "type": "answered",
# "reply": "Your current plan is Pro, active until Dec 1, 2025.",
# "trace": [
# {"step": 1, "action": "tool_call", "tool_name": "get_subscription_status", ...},
# {"step": 2, "action": "final_answer", ...}
# ]
# }
Example 3: Escalation
result = run_agent_conversation(
"I want a refund for last month",
user_id="user-123"
)
# Result:
# {
# "type": "escalated",
# "reply": "I'm connecting you with our support team...",
# "trace": [{"step": 1, "action": "escalation", ...}]
# }
Visualizing the Loop
Here’s how the loop flows:
Key Implementation Details
- Message format - Follow the LLM provider’s format exactly
- Tool execution - Call local functions, not remote APIs
- Error handling - Handle tool errors gracefully
- Step limits - Prevent infinite loops
- Trace logging - Log everything for debugging
What’s Next?
The loop is working, but it’s not very smart yet. The next page adds memory and guardrails to make it safer and more consistent.
Progress 63%
Page 5 of 8
← Previous
→ Next