A Framework-Free Walkthrough of the Control Loop Behind Every Tool-Calling AI Agent A developer's guide demonstrates the core control loop behind every tool-calling AI agent, using a Gemini model and free-tier API key. The walkthrough explains the four-step cycle of sending context, evaluating responses, executing tool calls, and terminating, with code examples for tool definitions and dispatch. It emphasizes that LLMs do not execute code directly but output structured decisions for the application to run. Most "build an AI agent" tutorials skip straight to higher-level frameworks like LangGraph, CrewAI, or AutoGen. As a result, developers call .invoke without seeing the mechanics underneath. Beneath every agent framework lies a single, surprisingly straightforward control loop. To make this walkthrough accessible, the examples in this guide use a Gemini model. Because anyone with a Google Account can obtain a free-tier API key, you can run the code and follow along directly without running into paid platform barriers or complex setups. Every AI agent, regardless of framework, executes the same four-step cycle: Send Context : Pass the conversation history and tool definitions to the LLM. Evaluate Response : The LLM returns either a final text response or a tool execution request. Execute and Append : If the model requests a tool call, run the corresponding local function, append the output to the conversation history, and return to Step 1. Terminate : Repeat the cycle until the LLM produces a final answer or reaches a designated safety ceiling. LLMs do not execute code, query databases, or call external APIs directly. Instead, the model outputs structured decision data specifying which tool to execute and which parameters to pass. Your application receives this instruction, runs the corresponding local function, and returns the output to the model. An LLM evaluates available functions solely through tool definitions—API specifications provided in the request payload. High-quality descriptions act as operational instructions for the model, directly determining how accurately it selects tools: tool definition = { "name": "check service health", "description": "Checks current operational status and latency for a given service. " "Use this first when investigating incident alerts." , "parameters": { "type": "object", "properties": { "service name": { "type": "string", "description": "The target service identifier e.g., 'auth-api' " } }, "required": "service name" } } When the model returns a tool call request, your application must dispatch that request to the target function. For simple agents with few tools, a standard match/case block or lookup table handles dispatch cleanly: python def dispatch tool call tool name: str, arguments: dict : match tool name: case "check service health": return check service health arguments case "check recent deployments": return check recent deployments arguments case "search runbook": return search runbook arguments case : raise ValueError f"Unknown tool requested: {tool name}" Because LLMs are stateless, your runtime loop must manage conversation state across every turn. Each tool-calling cycle requires appending two distinct messages to the chat history: 1. Append the model's tool execution request messages.append response.message 2. Append the function output as a tool-role message messages.append { "role": "tool", "tool call id": tool call.id, "content": str tool result } Assemble the step-by-step cycle inside a single loop function, checking for final text responses on each iteration. Always define an explicit iteration ceiling max iterations = 10 to protect against infinite loops if a model fails to converge: php def run agent loop user prompt: str, max iterations: int = 10 - str: messages = {"role": "user", "content": user prompt} for in range max iterations : response = llm.generate messages=messages, tools=tools Exit condition: Model produced final text response if not response.tool calls: return response.text Execution state: Run requested functions and update history for tool call in response.tool calls: result = dispatch tool call tool call.name, tool call.args messages.append response.message messages.append { "role": "tool", "tool call id": tool call.id, "content": str result } raise RuntimeError "Agent exceeded maximum iteration limit." Understanding this core loop removes the ambiguity surrounding agent frameworks. Higher-level orchestration layers build on this foundation to manage edge cases, state persistence, and complex routing, but the core mechanism remains simple: structured model decisions guiding local code execution.