Skip to content
Blog

Tools, Function Calling, and the Agent Execution Loop

The agent loop is a model deciding which tools to call, observing results, and iterating. Learn function declarations, tool_choice modes, and parallel calling.

Published on August 6, 2026

AI Assistant

An agent is not a special model — it is a model in a loop. The model decides which tool to call, your application executes it, the result is fed back, and it repeats until it has a final answer. Function calling is the mechanism that makes that loop possible, and understanding it precisely is what separates a demo from a reliable system.

Prerequisites

  • Python 3.10+
  • A Gemini API key (GEMINI_API_KEY)
  • google-genai installed

The core idea: declarations, not execution

The model never runs your code. It receives a function declaration — the name, parameters, and a description of purpose — and responds with a structured request describing which function to call and with what arguments:

{"name": "get_weather", "args": {"location": "Bangkok"}, "id": "abc123"}

Your application owns the execution. The id lets you return results even out of order (important for parallel calls).

Declaring a function

from google import genai
from google.genai import types

client = genai.Client()

def get_weather(location: str) -> str:
    # ...call a real weather API...
    return f"{location}: 31C, clear"

get_weather_tool = types.Tool(function_declarations=[
    types.FunctionDeclaration(
        name="get_weather",
        description="Get current weather for a city",
        parameters=types.Schema(
            type=types.Type.OBJECT,
            properties={"location": types.Schema(type=types.Type.STRING)},
        ),
    )
])

The declaration shapes which calls the model will make. A blurry description produces blurry arguments — write the description as if explaining the tool to a careful assistant.

The agent execution loop

def run_agent(prompt: str, tools, tool_functions, max_iterations: int = 5) -> str:
    messages = [{"role": "user", "contents": prompt}]
    for _ in range(max_iterations):
        resp = client.models.generate_content(
            model="gemini-2.5-pro",
            contents=messages,
            config=types.GenerateContentConfig(tools=tools),
        )
        calls = resp.function_calls or []
        if not calls:
            return resp.text  # final answer

        for call in calls:
            fn = tool_functions.get(call.name)
            result = fn(**call.args) if fn else "unknown tool"
            messages.append({"role": "model", "parts": [call.__original__]})
            messages.append({
                "role": "user",
                "parts": [types.FunctionResponse(name=call.name, response={"result": result})],
            })
    return resp.text  # hit iteration cap — degrade gracefully

Three properties matter in production:

  1. Termination. The loop ends when the response has no tool calls. If it never converges, cap iterations — otherwise a stuck agent burns tokens forever.
  2. Parallel calls. The model can request several independent functions in one turn (e.g., weather for Bangkok and Singapore). Execute them concurrently and return results mapped by their id.
  3. Compositional calls. Dependent tools chain: get_location() first, then get_weather(location) for that location. Each round-trip is one model call plus your execution.

Controlling the loop with tool_choice

You and not leave tool usage to chance. Gemini’s tool_choice (or function_calling_config) has these modes:

  • auto (default) — the model decides whether to call a tool or reply directly.
  • any — force a function call every turn (useful for routing / pipelines).
  • none — forbid tool calls.
  • validated — the model must adhere to the schema, reducing malformed calls.
  • allowed_tools — restrict which declared functions the model may select.
config = types.GenerateContentConfig(
    tools=[get_weather_tool],
    function_calling_config=types.FunctionCallingConfig(
        mode=types.FunctionCallingMode.ANY,
        allowed_function_names=["get_weather"],
    ),
)

Streaming tool calls

When streaming, the model emits tool calls as incremental step.delta events with arguments arriving in pieces. You must aggregate the deltas before executing — failing to reconstruct partial args is a classic bug in streaming agent loops.

Putting It All Together

A minimal but production-shaped agent over two tools — one to search, one to answer:

tools = [search_tool, answer_tool]
functions = {"search": search_site, "answer": summarize_articles}
for _ in range(5):
    resp = client.models.generate_content(
        model="gemini-2.5-pro",
        contents=messages, tools=tools,
        config=types.GenerateContentConfig(tool_choice="auto"),
    )
    if not (calls := resp.function_calls):
        print("FINAL:", resp.text); break
    for call in calls:
        messages.append({"role": "assistant", "parts": [call]})
        messages.append({"role": "user", "parts": [
            types.FunctionResponse(name=call.name, arguments={"result": functions[call.name](**call.args)})
        ]})

Conclusion & Next Steps

The agent loop is the model deciding, you executing, and the result feeding back — terminated by “no more tool calls” and bounded by an iteration cap. Next: add timeouts and retry-with-backoff around tool execution, enforce validated mode for stricter schema, and decide deliberately whether to hold state in your own loop or delegate to a hosted agent service.

References / Sources