GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/Advanced
Advanced/2026-06-13Advanced

A Minimal Autonomous Agent with Gemini — Tool-Loop Design Lessons

Building an autonomous agent from a minimal setup with the google-genai SDK's automatic function calling — plus the step limits, tool allowlists, and retry decisions learned from automating real blog operations.

Gemini API192Agents7Function Calling16Autonomous AI

Once I started running several blogs in parallel alongside my work as an indie developer, the morning routine quietly grew: checking traffic, sorting the publishing queue, triaging image assets. Each task takes minutes, but every new site shaved more time off the morning.

"Let the model make the judgment calls; let the tools do the work." Deciding to hand Gemini a set of tools under that principle was, for me, the real start of agent development.

Let me say this upfront: agents are not magic. Under the hood, an agent is a loop — the model proposes a tool call, your code executes it, the result goes back to the model, repeat. Write that loop yourself once and you gain a clear yardstick for evaluating every framework and managed service afterward. This article covers that minimal setup and the design decisions that only became visible in production.

A minimal agent with automatic function calling alone

The current Python SDK is google-genai (pip install google-genai). Samples for the old google-generativeai package still rank high in search results, but the class layout is different and mixing them breaks. Right after migrating, I burned time hunting for the old SDK's GenerativeModel myself.

The new SDK's big win is that you can pass plain Python functions and automatic function calling takes over. Function declarations are generated from docstrings and type hints; when the model decides a call is needed, the SDK executes the function and returns a final answer that incorporates the result.

from google import genai
from google.genai import types
 
# Reads the GEMINI_API_KEY environment variable automatically
client = genai.Client()
 
def get_article_stats(slug: str) -> dict:
    """Returns views and clicks for the given article slug over the last 7 days."""
    # In production this queries the GA4 / Search Console data store
    return {"slug": slug, "views": 1280, "clicks": 96}
 
def list_pending_drafts() -> list[str]:
    """Returns the list of draft slugs waiting for publication."""
    return ["gemini-batch-api-notes", "image-asset-pipeline"]
 
response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="Check the pending drafts and, based on recent article performance, propose a publishing order.",
    config=types.GenerateContentConfig(
        tools=[get_article_stats, list_pending_drafts],
    ),
)
 
print(response.text)

With just this, the model calls list_pending_drafts, calls get_article_stats as many times as it needs, and returns a synthesized recommendation. Multi-step execution works even though you never wrote a loop — the SDK is running the tool loop internally.

The part that trips people up is the docstring. With automatic function calling, the docstring is the model's only description of your tool, so a vague "what it returns" makes the call decisions wobble. State the meaning of each argument and the shape of the return value in one sentence each. That alone visibly stabilized the calls for me.

When to switch to a manual loop

Automatic execution is convenient, but in my production pipelines I switch to a manual loop. Two reasons: I want to inspect each proposed tool call before it runs, and I want to own the step limit myself.

from google import genai
from google.genai import types
 
client = genai.Client()
 
publish_tool = types.Tool(function_declarations=[
    types.FunctionDeclaration(
        name="publish_article",
        description="Publishes the article with the given slug. Destructive: requires verification.",
        parameters=types.Schema(
            type=types.Type.OBJECT,
            properties={"slug": types.Schema(type=types.Type.STRING)},
            required=["slug"],
        ),
    ),
])
 
config = types.GenerateContentConfig(
    tools=[publish_tool],
    # Disable auto-execution; receive call proposals only
    automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
)
 
contents = [
    types.Content(role="user", parts=[
        types.Part(text="Publish the reviewed article. Target: gemini-batch-api-notes"),
    ]),
]
 
MAX_STEPS = 6  # Runaway guard. Routine ops tasks fit within 6 steps in my experience
 
for step in range(MAX_STEPS):
    response = client.models.generate_content(
        model="gemini-3.5-flash", contents=contents, config=config,
    )
    part = response.candidates[0].content.parts[0]
 
    if not part.function_call:
        print(response.text)  # No more tool proposals — done
        break
 
    call = part.function_call
    # Inspect the proposed call here before executing it
    result = execute_with_allowlist(call.name, dict(call.args))
 
    contents.append(response.candidates[0].content)
    contents.append(types.Content(role="user", parts=[
        types.Part.from_function_response(name=call.name, response={"result": result}),
    ]))

The key line is MAX_STEPS. An early version of mine lacked it; when a tool kept returning an error string, the model kept retrying the same call and the only thing that accumulated was API spend. I now treat the loop cap as a required component, not an insurance policy.

Separate read tools from destructive tools

The single most effective design decision in production was splitting tools into "read" and "destructive."

Fetching stats or listing drafts is harmless no matter how often the model calls it. Publishing, deleting, or posting externally cannot be taken back. In my implementation, destructive tools go through an allowlist, and arguments are validated before execution inside the loop.

READ_TOOLS = {"get_article_stats", "list_pending_drafts"}
WRITE_TOOLS = {"publish_article"}
 
def execute_with_allowlist(name: str, args: dict) -> str:
    if name in READ_TOOLS:
        return run_tool(name, args)
    if name in WRITE_TOOLS:
        # Destructive calls get an existence check first
        if not draft_exists(args.get("slug", "")):
            return f"Aborted: target {args.get('slug')} not found"
        return run_tool(name, args)
    return f"Tool {name} is not permitted"

"Destructive operations on nonexistent targets get swallowed by the tool itself." After adding that one layer, the path from a model misjudgment to a real incident was closed. Rather than trusting the model to be smart, tilt the tool design toward safety. A surprising share of agent quality lives in the tool-side code.

Before multi-agent, try one agent with good tools

Articles about agents love multi-agent orchestration. I tried it once too — an "analyst," a "writer," and a "reviewer." My conclusion: at solo-developer scale, it did not justify the maintenance cost.

Every handoff prompt between roles made debugging harder, and tracing where a judgment went wrong ate the time the setup was supposed to save. The same results came out more reliably from one agent with clearly scoped tools. Multi-agent pays off only when each role truly needs an isolated context — that is my current read.

If you would rather hand the entire loop and its runtime to Google, Managed Agents (public preview) is an option: an isolated sandbox that handles planning through code execution. The trade-off is exactly the setup above — you give up holding destructive-tool verification in your own code. I am taking a staged path: run my own loop first, learn where the sharp edges are, then move only the parts that transfer cleanly.

The numbers I check every month

Whether an agent keeps running is a numbers decision, not a feeling. I check three things at the start of each month.

Tool-call success rate (around 96% lately; when it dips, I suspect a docstring or argument schema first). Average steps per task (2–3 is healthy; drift upward means the goal statement in the prompt has gone fuzzy). And total token consumption summed from usage_metadata. Put that cost next to the hours the automation saves, and retire what no longer pays for itself. With that yardstick, agents stay in production because they are effective — not merely because they are running.

What to try next

Start with a minimal agent holding just two read-only tools under automatic function calling. Once you have felt how a one-sentence docstring change shifts behavior, moving to a manual loop with a step cap is the natural next step.

When you want finer control over tool definitions, Function Calling with the Gemini API is the next read. If the basics still feel shaky, Gemini API Quickstart is a good place to firm them up.

I hope this helps if you are automating your own daily operations, one small piece at a time.

Share

Thank You 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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Advanced2026-07-04
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.
Advanced2026-07-03
Your Tool Results Are Quietly Eating the Conversation — Handle Passing to Keep Gemini Function Calling Contexts Lean
Tool results linger in Function Calling history and compound your input tokens every turn. Two implementations — a token-budgeted compactor and handle passing — cut my measured input by roughly 8x, with the pitfalls I hit along the way.
Advanced2026-07-01
Getting Artifacts Out of a Managed Agents Sandbox Safely — Scoped Credentials and Egress Design
Gemini API Managed Agents run in a Google-hosted isolated sandbox. Here is the short-lived, least-privilege credential and egress-boundary design I use to return generated artifacts to my own repository safely.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →