Skip to main content
Lesson 2 of 25 · Building OpenAI Agents

Agent architecture: the tool-reasoning loop

Lesson 2 of 25 Updated November 3, 2026 Developer
On this page
  1. Agent architecture: the tool-reasoning loop
  2. The agent lifecycle
  3. Internal structure: items and messages
  4. Parallel vs. sequential calls
  5. The max_turns parameter
  6. Key takeaways

Updated November 3, 2026: In rechercher_produit, the prix and stock keys were missing quotes, causing a NameError on the first tool call.

Agent architecture: the tool-reasoning loop

To build effective agents, you need to understand their internal architecture. An OpenAI agent operates on a reasoning loop: it receives a task, thinks, uses a tool, observes the result, and then decides whether it’s done or needs to continue. Everything you do next—declaring tools, setting guardrails, orchestrating multiple agents—builds on this loop.

The agent lifecycle

Every execution follows six steps:

  1. Reception: The agent receives the user message and system instructions
  2. Reasoning: The model analyzes the request and decides on the next action
  3. Tool call: If needed, the agent calls one or more tools
  4. Observation: The agent receives the tool results
  5. Decision: The agent decides whether to continue (back to step 2) or respond
  6. Response: The agent produces its final answer

The key is the loop between steps 2 and 5, which can run multiple times. An agent can chain 5, 10, or even 20 tool calls before producing a final response, without you writing a single line of orchestration code. The example below shows this: two independent tools are declared, and the model decides how to chain them.

from agents import Agent, Runner, function_tool

@function_tool
def rechercher_produit(nom: str) -> str:
    """Searches for a product in the catalog."""
    # Simulation - in production, call your database
    catalogue = {
        "laptop-pro": {"prix": 1299, "stock": 45},
        "tablet-air": {"prix": 599, "stock": 120},
    }
    produit = catalogue.get(nom)
    if produit:
        return f"Produit {nom}: {produit['prix']}€, stock: {produit['stock']}"
    return f"Produit {nom} non trouvé"

@function_tool
def calculer_remise(prix: float, pourcentage: float) -> str:
    """Calculates the price after discount."""
    prix_final = prix * (1 - pourcentage / 100)
    return f"Prix après {pourcentage}% de remise : {prix_final:.2f}€"

agent = Agent(
    name="Sales Agent",
    instructions="You help customers find products and calculate discounts.",
    tools=[rechercher_produit, calculer_remise],
    model="gpt-5.6-terra",
)

# The agent will: 1) find the product, 2) calculate the discount, 3) respond
result = Runner.run_sync(
    agent,
    "What is the price of the laptop-pro with a 15% discount?"
)
print(result.final_output)

The question requires two operations that aren’t explicitly linked in the code. The agent automatically makes two tool calls in sequence: first rechercher_produit, then calculer_remise with the retrieved price. This chaining is a model decision, not your instruction.

Internal structure: items and messages

The Responses API structures every interaction as items. Reading them shows you what your agent actually did. Three types appear in the conversation:

  • message: a text message (from the user or the agent)
  • function_call: a tool call decided by the agent
  • function_call_output: the result returned by the tool
result = Runner.run_sync(agent, "Find the tablet-air")

# Inspect the reasoning steps
for item in result.raw_responses:
    print(f"Type: {type(item).__name__}")
    # You'll see: message input → function_call → function_call_output → message output

Make it a habit to trace this loop whenever an agent produces a surprising response. Nine times out of ten, the anomaly shows up in a function_call with unexpected arguments.

Parallel vs. sequential calls

Not all steps depend on each other. When two calls are independent, the agent can decide to run them in parallel.

@function_tool
def obtenir_meteo(ville: str) -> str:
    """Gets the current weather for a city."""
    meteos = {"Paris": "18°C, sunny", "Lyon": "16°C, cloudy"}
    return meteos.get(ville, "Data unavailable")

agent = Agent(
    name="Travel Agent",
    instructions="You help plan trips.",
    tools=[obtenir_meteo, rechercher_produit],
    model="gpt-5.6-terra",
)

# The agent will call obtenir_meteo("Paris") and obtenir_meteo("Lyon") in parallel
result = Runner.run_sync(
    agent,
    "What's the weather in Paris and Lyon?"
)

Lyon’s weather doesn’t depend on Paris’s: the SDK handles parallelism automatically and launches both calls simultaneously, reducing latency. Compare this with the sales example above, where the discount calculation had to wait for the price: there, the sequence imposes itself.

The max_turns parameter

An autonomous loop can, in the worst case, never stop: the agent calls a tool, the result pushes it to call another, and so on. The safeguard is a single parameter.

result = Runner.run_sync(
    agent,
    "Full catalog analysis",
    max_turns=10  # Maximum 10 loop iterations
)

If the agent hits the limit, it stops with the results obtained so far. In production, this is essential safety: it caps both response time and cost.

Key takeaways

  • An agent runs in a loop: reasoning → tool → observation → decision
  • This loop can execute multiple times before producing a response
  • The model automatically decides which tools to call and in what order
  • Independent tool calls run in parallel
  • max_turns limits the number of iterations for safety