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-genaiinstalled
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:
- Termination. The loop ends when the response has no tool calls. If it never converges, cap iterations — otherwise a stuck agent burns tokens forever.
- 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. - Compositional calls. Dependent tools chain:
get_location()first, thenget_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
- Gemini API function calling. https://ai.google.dev/gemini-api/docs/function-calling
- Gemini API tools overview. https://ai.google.dev/gemini-api/docs/tools
- Gemini function calling (generateContent API). https://ai.google.dev/gemini-api/docs/generate-content/function-calling
- Gemini Agents API. https://ai.google.dev/api/agents