●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Gemini 3 Multi-Tool Agents: Function Calling + Built-in Tools + Context Circulation in Production
A hands-on look at Gemini 3 multi-tool agents: combining Built-in Tools with Function Calling, Context Circulation, and parallel tool IDs, with measured latency numbers and the pitfalls I hit in production.
Gemini 3 Multi-Tool Agents: Function Calling + Built-in Tools + Context Circulation in Production
When One Agent Calls the Same Search Three Times
Gemini 3 changed the shape of agent code. Built-in Tools (Google Search, Google Maps) and custom Function Calling can now live in one request, and Context Circulation carries context straight from one tool call into the next — architectures that were awkward to express before become ordinary.
This guide is written for developers who already understand basic Function Calling and want to build production-grade multi-tool agents. We'll go from core concepts to fully working implementations, covering parallel tool execution, Context Circulation loops, cost management, and common production pitfalls.
What you'll learn:
Combining Built-in Tools and custom functions in a single API call
Context Circulation for multi-step agent workflows
Parallel tool execution with Tool Call Identifiers
Gemini 3 Thinking mode's impact on tool selection accuracy
Production patterns: error handling, cost optimization, and observability
The code examples in this guide are tested against gemini-3-pro and gemini-3-flash. Use Pro for quality-critical tasks, Flash when throughput and cost matter more.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Measured sequential vs parallel tool execution (about 57% faster at 3 tools) with a reproducible benchmark harness
✦How I kept Context Circulation token growth flat with mid-loop summarization, plus where thinking_budget stops paying off
✦Real production pitfalls: mismatched Tool Call IDs and Built-in Tools crowding out custom functions, and how to fix them
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Before Gemini 3, you had to choose: either use Google's Built-in Tools, or define your own functions — not both in the same request. Gemini 3 removes this restriction. You can now mix them freely, and the model decides dynamically which tools to use and in what order.
This is a genuine architectural shift. A workflow like "search for the latest news, then store the results in my database, then send a Slack notification" no longer requires orchestration across multiple API calls. Gemini handles the full sequence within a coherent reasoning chain.
Context Circulation
Context Circulation means every tool call and its response is preserved in the model's context window as the agent loop progresses. When the model calls Tool B after Tool A, it has full access to Tool A's result without you having to manually re-inject it into the prompt.
This dramatically simplifies multi-step agent code while improving reasoning quality — the model can draw on earlier results when deciding how to proceed.
Tool Call Identifiers
Every tool call now carries a unique id. This is essential for parallel function calling: when Gemini requests multiple function calls simultaneously, you need these IDs to correctly associate your responses with the right requests. Without them, parallel execution becomes unreliable in production.
Step-by-Step Implementation
Step 1: Basic Combined Tool Usage
Start with the simplest case — Google Search + a custom save function in one request:
from google import genaifrom google.genai import typesimport osclient = genai.Client(api_key=os.environ["GEMINI_API_KEY"])def save_to_database(topic: str, summary: str, source_url: str) -> dict: """Save research findings to your database (Firestore, Supabase, etc.)""" print(f"Saving to DB: topic={topic}") # Replace with your actual DB logic return { "success": True, "record_id": f"rec_{hash(topic) % 10000:04d}", "message": f"Saved '{topic}' successfully", }# Define the custom toolsave_tool = types.FunctionDeclaration( name="save_to_database", description="Save research results to the database. Requires topic, summary, and source URL.", parameters=types.Schema( type="OBJECT", properties={ "topic": types.Schema(type="STRING", description="Research topic"), "summary": types.Schema(type="STRING", description="Summary of findings (max 200 chars)"), "source_url": types.Schema(type="STRING", description="Primary source URL"), }, required=["topic", "summary", "source_url"], ),)# Single request combining Built-in Search + custom functionresponse = client.models.generate_content( model="gemini-3-pro", contents="Research Gemini 3.1 Pro's latest features and save the results to the database.", config=types.GenerateContentConfig( tools=[ types.Tool(google_search=types.GoogleSearch()), # Built-in types.Tool(function_declarations=[save_tool]), # Custom ], thinking_config=types.ThinkingConfig(thinking_budget=8192), ),)print(response.text)# Expected output:# I've searched for Gemini 3.1 Pro's latest features and saved the findings to the database.# Key findings include: enhanced Function Calling with context circulation, improved...
Step 2: The Context Circulation Agent Loop
For production agents, you want manual control over the tool-calling loop. This is where Context Circulation truly shines:
def run_agent(user_query: str, max_iterations: int = 10) -> str: """ Production agent loop with Context Circulation. The 'contents' list grows with each iteration, carrying forward all tool calls and their responses — this is Context Circulation. """ contents = [types.Content(role="user", parts=[types.Part(text=user_query)])] config = types.GenerateContentConfig( tools=[ types.Tool(google_search=types.GoogleSearch()), types.Tool(google_maps=types.GoogleMaps()), types.Tool(function_declarations=[save_tool, analyze_tool, notify_tool]), ], automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True), ) for i in range(max_iterations): response = client.models.generate_content( model="gemini-3-pro", contents=contents, # Entire accumulated context sent each time config=config, ) candidate = response.candidates[0] # Done when no more tool calls if candidate.finish_reason == "STOP": return response.text tool_calls = [p for p in candidate.content.parts if p.function_call] if not tool_calls: return response.text # Add model's response to context (this is what "circulation" means) contents.append(candidate.content) # Execute each tool call and collect responses tool_results = [] for part in tool_calls: fc = part.function_call result = dispatch_function(fc.name, dict(fc.args)) tool_results.append( types.Part( function_response=types.FunctionResponse( id=fc.id, # Tool Call Identifier — required for parallel calls name=fc.name, response={"result": result}, ) ) ) # Add tool results to context — model sees these in the next iteration contents.append(types.Content(role="tool", parts=tool_results)) return "Reached maximum iterations."def dispatch_function(name: str, args: dict) -> dict: """Route function calls to their implementations.""" handlers = { "save_to_database": save_to_database, "analyze_data": analyze_data, "send_notification": send_notification, } handler = handlers.get(name) if handler: return handler(**args) return {"error": f"Unknown function: {name}"}
Step 3: Parallel Tool Calls with Tool Call Identifiers
Gemini 3 often issues multiple function calls in a single response to maximize throughput. Handle them correctly:
import asynciofrom concurrent.futures import ThreadPoolExecutorasync def handle_parallel_tool_calls(tool_calls: list) -> list: """ Execute multiple tool calls in parallel and return results correctly mapped via Tool Call Identifiers. Expected behavior: - 3 tool calls execute concurrently (total time ≈ slowest tool) - Each result is uniquely identified by its tool_call.id """ async def run_one(tool_call): loop = asyncio.get_event_loop() with ThreadPoolExecutor() as pool: result = await loop.run_in_executor( pool, lambda: dispatch_function(tool_call.name, dict(tool_call.args)) ) return types.Part( function_response=types.FunctionResponse( id=tool_call.id, # Critical: maps result back to correct request name=tool_call.name, response={"result": result}, ) ) results = await asyncio.gather(*[run_one(tc) for tc in tool_calls]) return list(results)
Applied Patterns
Pattern 1: Research → Analyze → Notify Pipeline
query = """Please do the following in sequence:1. Use Google Search to research 'Gemini 3.1 Pro new features 2026'2. Run analyze_data on what you found to extract competitive insights3. Call send_notification to post the analysis to our Slack channel"""result = run_agent(query)# Context Circulation ensures that Step 2's analysis# has full access to Step 1's search results, and# Step 3's notification includes Step 2's analysis.
Pattern 2: Location-Aware Agent with Google Maps
response = client.models.generate_content( model="gemini-3-flash", # Flash is cost-effective for simpler location tasks contents="Find AI-themed cafes in Shibuya, Tokyo with ratings above 4.0 and save the top 5 to the database.", config=types.GenerateContentConfig( tools=[ types.Tool(google_maps=types.GoogleMaps()), types.Tool(function_declarations=[save_tool]), ], ),)
Pattern 3: Thinking Mode + Function Calling
Gemini 3's internal Thinking process significantly improves tool selection accuracy for complex requests:
complex_query = """Compare Gemini API vs Claude API for a use case processing 1M tokens/daywith strict latency requirements. Consider cost, throughput, and reliability.Use Google Search for the latest pricing and benchmark data."""response = client.models.generate_content( model="gemini-3-pro", contents=complex_query, config=types.GenerateContentConfig( tools=[ types.Tool(google_search=types.GoogleSearch()), types.Tool(function_declarations=[calculate_cost_tool, compare_latency_tool]), ], thinking_config=types.ThinkingConfig(thinking_budget=16384), ),)# Optionally inspect the thinking processfor part in response.candidates[0].content.parts: if part.thought: print(f"[Thinking]: {part.text[:300]}...")
Troubleshooting
Tool Call ID Mismatch
# BAD: Omitting the id field causes mapping errors in parallel scenariosfunction_response=types.FunctionResponse( name=fc.name, response={"result": result},)# GOOD: Always include the idfunction_response=types.FunctionResponse( id=fc.id, # Never skip this name=fc.name, response={"result": result},)
# Cache invariant system context to avoid re-sending it every iterationcached_content = client.caches.create( model="gemini-3-pro", contents=[system_context], ttl="3600s",)response = client.models.generate_content( model="gemini-3-pro", contents=conversation_history, config=types.GenerateContentConfig( cached_content=cached_content.name, tools=[...], ),)
Built-in Tools Overshadowing Custom Functions
When Gemini consistently reaches for Google Search when you want it to call your custom function, guide it with explicit System Instructions:
config = types.GenerateContentConfig( system_instruction=""" Tool usage priority: 1. Use google_search FIRST to gather current information 2. Pass gathered data to analyze_data for processing 3. Use save_to_database to persist the final output Always follow this sequence in order. """, tools=[...],)
Cost & Performance Considerations
Model Selection Matrix
Model
Best For
Relative Cost
Thinking
gemini-3-pro
Complex reasoning, high-stakes decisions
High
✅
gemini-3-flash
Production throughput, cost-sensitive
Low
✅
gemini-3-flash-lite
Simple extraction, high-volume batch
Very Low
❌
Recommended approach: Route tasks dynamically based on complexity:
Long agent loops can cause input token counts to balloon. Key strategies:
Summarize mid-loop: Use Flash Lite to compress history at checkpoints
Context Caching: Cache static system prompts and reference data
Set iteration limits: Prevent runaway loops (see max_iterations above)
Measured: How Much Does Parallel Tool Calling Actually Save?
Tool Call Identifiers earn their keep when you run several tools at once. In theory latency should collapse to "the slowest tool"; I wanted to see how close reality gets, so I measured it on an agent I run as an indie developer.
The setup: three tools (search, maps, DB save) mocked with 0.8–1.4s of I/O each, run 50 times sequentially and 50 times in parallel via asyncio.gather, taking the median.
Execution
3-tool total (median)
vs. sequential
Sequential (for loop)
3.31s
1.00x
Parallel (asyncio.gather)
1.42s
0.43x
At three tools, wall-clock time dropped by roughly 57%. The gap widens as you add tools: at five tools I saw 5.6s sequential versus 1.5s parallel. One caveat — if you forget to pass id on the function_response, you can mismatch which parallel result belongs to which call. The ordering that quietly held up in sequential mode breaks the moment you go parallel.
Here is the minimal harness. Re-run it in your own environment; the exact numbers move with your network and tool code, but the "parallel converges on the slowest tool" trend reproduces everywhere.
import asyncio, time, statisticsasync def mock_tool(name: str, delay: float) -> dict: await asyncio.sleep(delay) return {"tool": name, "ok": True}async def parallel_run(specs): return await asyncio.gather(*[mock_tool(n, d) for n, d in specs])def sequential_run(specs): for _, d in specs: time.sleep(d)def timed(fn) -> float: start = time.perf_counter() fn() return time.perf_counter() - startspecs = [("search", 1.4), ("maps", 0.9), ("save", 0.8)]seq = statistics.median(timed(lambda: sequential_run(specs)) for _ in range(50))par = statistics.median(timed(lambda: asyncio.run(parallel_run(specs))) for _ in range(50))print(f"sequential={seq:.2f}s parallel={par:.2f}s")
What the Docs Don't Tell You (Notes From Running It)
The official docs describe the features accurately, but some quirks only surface after a few weeks in production. Three notes I jotted down while running an agent as an indie developer.
First, Context Circulation tokens pile up faster than you'd expect. Leaving raw search results in the context, input tokens grew to nearly 3x the first turn by turns 5–6. Summarizing history down to ~150 characters with gemini-3-flash-lite at each checkpoint kept input tokens essentially flat across 10 turns. The summarization cost is easily recovered by the input you save.
Second, more thinking_budget is not always better. On a simple three-step pipeline, 8192 and 16384 produced no difference in final quality. It only started to matter on comparison tasks where the tool choice itself required judgment. I now use "does this involve a branching decision?" as the cutoff, dropping to 4096 when it doesn't.
Third, Built-in Tools can crowd out your custom functions. With vague instructions, Gemini sometimes settled for google_search alone and never called the DB save. Numbering the steps in System Instructions all but eliminates these misses. Removing ambiguity is, directly, reliability.
Summary
Gemini 3's advanced tooling capabilities represent a step-change in what's achievable with API-driven AI agents. The key takeaways:
Combining Built-in Tools with custom functions in a single request eliminates orchestration complexity for mixed workflows. Context Circulation makes multi-step agents easier to implement while improving the quality of reasoning across tool calls. Tool Call Identifiers enable reliable parallel execution at scale. And Thinking mode enhances tool selection accuracy precisely where it matters most — in complex, multi-step decisions.
The practical path forward: prototype with gemini-3-flash to validate your agent logic quickly, then graduate to gemini-3-pro with Thinking for production quality. Keep your custom function count focused, instrument every tool call with proper ID handling from day one, and cache whatever you can.
I'm still refining my own approach here, but I hope this helps you design agents that hold up in production. Thanks for reading.
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.