Agent Loop Basics
10 examples to get you started with the perceive → reason → act → observe rhythm in Python: one tool call, reading the result, multi-step loops, and the stop rules that keep runs finite.
Read Inside the Agent Loop: Perceive, Reason, Act, Observe for the full mental model. These examples stay provider-agnostic: any chat API with tool/function calling works the same way.
Prerequisites
You only need a Python 3.10+ environment and a mental model of chat messages (system, user, assistant, tool).
python -m venv .venv
source .venv/bin/activate
# install your preferred OpenAI-compatible client when you wire a real modelConcept focus: Group 1 pages are light on real network calls. Snippets use plain data structures so you can see the loop without a specific SDK.
Basic Examples
1. Messages Are the Agent's Working Memory
An agent does not "remember" outside the message list you pass each call.
messages = [
{"role": "system", "content": "You help with order status. Use tools when needed."},
{"role": "user", "content": "Where is order 1042?"},
]- Every later turn appends to this list (or a framework-managed equivalent)
- Dropping tool results from
messagesis the same as never having run the tool - System policy must stay present or the model cannot follow it
2. Tools Are Schemas, Not Magic
Describe tools so the model can choose them; your code still implements them.
TOOLS = [
{
"name": "get_order",
"description": "Fetch order status by numeric id",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "integer"}},
"required": ["order_id"],
},
}
]
def get_order(order_id: int) -> dict:
return {"order_id": order_id, "status": "shipped", "eta": "2026-07-18"}- Names and descriptions steer tool choice
- Keep parameters strict; loose schemas produce garbage args
- Host validates before calling the real function
3. One Reason Step: Call a Tool or Finish
The model either proposes a tool call or returns final text.
def fake_model_decide(messages, tools):
# Stand-in for a real LLM tool-calling response
last = messages[-1]["content"]
if "1042" in last and not any(m.get("role") == "tool" for m in messages):
return {"type": "tool_call", "name": "get_order", "args": {"order_id": 1042}}
return {"type": "final", "content": "Order 1042 is shipped; ETA 2026-07-18."}- Real APIs return structured
tool_callson the assistant message - Your loop must branch on
tool_callvsfinal - Never execute free-form shell strings from raw model text without a tool layer
4. Act + Observe: Run the Tool and Append the Result
decision = fake_model_decide(messages, TOOLS)
if decision["type"] == "tool_call":
result = get_order(**decision["args"])
messages.append({"role": "assistant", "content": None, "tool_call": decision})
messages.append({"role": "tool", "name": decision["name"], "content": str(result)})- Act is the function run; observe is the
toolmessage - Prefer JSON-serializable, short results over huge dumps
- Errors should become tool messages too:
{"error": "..."}
5. The Smallest Useful Loop
def run_agent(user_text: str, max_turns: int = 4) -> str:
messages = [
{"role": "system", "content": "Use tools for live order data."},
{"role": "user", "content": user_text},
]
for _ in range(max_turns):
decision = fake_model_decide(messages, TOOLS)
if decision["type"] == "final":
return decision["content"]
result = get_order(**decision["args"])
messages.append({"role": "assistant", "tool_call": decision})
messages.append({"role": "tool", "name": "get_order", "content": str(result)})
return "Stopped: max turns reached."max_turnsis a mandatory safety rail, not optional polish- Loop body = reason → maybe act/observe → repeat
- Return a clear stop reason when caps hit
Related: Stopping Conditions: How an Agent Knows When It's Done
Intermediate Examples
6. Trace One Full Turn End to End
Walk a single iteration the way logs should look in production.
trace = []
decision = fake_model_decide(messages, TOOLS)
trace.append({"phase": "reason", "decision": decision})
if decision["type"] == "tool_call":
result = get_order(**decision["args"])
trace.append({"phase": "act", "tool": "get_order", "result": result})
# next model call would be the next reason phase- Log phases with timestamps and token usage when you wire a real API
- Store tool args and redacted results for debugging
- One bad observe step is enough to derail the next reason step
7. Two Tools, Two Iterations
When the model needs intermediate IDs, the loop earns its keep.
def lookup_customer(email: str) -> dict:
return {"customer_id": "c_77", "email": email}
def list_open_orders(customer_id: str) -> list:
return [{"order_id": 1042, "status": "shipped"}]
# Iteration 1: lookup_customer → observe id
# Iteration 2: list_open_orders → observe list
# Iteration 3: final natural-language answer- Do not hardcode step order if evidence can branch
- Do hardcode order in a plain workflow if it never branches
- Pass IDs through tool results, not through model "memory" alone
8. Structured Stop Reasons
Always know why the loop ended.
from enum import Enum
class StopReason(str, Enum):
FINAL_ANSWER = "final_answer"
MAX_TURNS = "max_turns"
TOOL_ERROR_BUDGET = "tool_error_budget"
USER_CANCEL = "user_cancel"
def finish(reason: StopReason, content: str) -> dict:
return {"stop_reason": reason.value, "content": content}- Surface
stop_reasonto product UI and metrics - Treat
MAX_TURNSas a first-class outcome, not an exception dump - See Why Agents Loop Forever when stops fire too late
9. Guard Tool Arguments Before Act
def safe_get_order(args: dict) -> dict:
if "order_id" not in args:
return {"error": "missing order_id"}
try:
oid = int(args["order_id"])
except (TypeError, ValueError):
return {"error": "order_id must be int"}
if oid <= 0:
return {"error": "order_id must be positive"}
return get_order(oid)- Schema validity ≠ business validity
- Return errors as observations; do not crash the loop
- Dangerous tools need authz checks in this same gate
10. Minimal Evaluation: Did the Loop Help?
def evaluate_run(user_goal: str, final_text: str, tool_calls: list[str]) -> dict:
return {
"answered": bool(final_text.strip()),
"used_tools": tool_calls,
"looks_grounded": "1042" in final_text and "shipped" in final_text.lower(),
}- Score trajectory quality, not only final prose
- Prefer grounded claims tied to tool output
- Expand this into golden tasks as you ship real agents
Related
- Inside the Agent Loop: Perceive, Reason, Act, Observe - full loop model
- Reasoning Traces: How Agents "Think" Before Acting - what reason steps may expose
- Stopping Conditions: How an Agent Knows When It's Done - ending runs cleanly
- Single-Turn vs Multi-Turn vs Long-Running Agents - mode comparison
- Agent Loop Best Practices - habits for production loops
- AI Agents Fundamentals Basics - first LLM + tool walkthrough
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.