Durable AI agent with Gemini and Temporal

This tutorial walks you through building a durable AI agent that uses the Gemini API for reasoning and Temporal for durability. It uses Temporal's built-in Gemini SDK integration.

The agent can call tools, like looking up weather alerts or geolocating an IP address, and will loop until it has enough information to respond.

What makes this different from a typical agent demo is durability. Every LLM call and every tool invocation is persisted by Temporal. If the process crashes, the network drops, or an API times out, Temporal automatically retries and resumes from the last completed step. No conversation history is lost, and no tool calls are incorrectly repeated.

Architecture

The architecture consists of three parts:

  • Workflow: A single generate_content call. The Gemini SDK's automatic function calling (AFC) loop runs inside the Workflow, and Temporal makes every step of it durable.
  • Activities: Individual units of work that Temporal makes durable. The Gemini API calls become Activities automatically.
  • Worker: The process that executes the Workflows and Activities, and the only place your API key lives.

In this example, you will place all three of these pieces in a single file (durable_agent_worker.py). In a real-world implementation, you would separate them to allow for various deployment and scalability advantages. You will supply prompts to the agent with the Temporal CLI, so there is no client code to write.

Prerequisites

To complete this guide, you'll need:

  • A Gemini API key. You can create one for free in Google AI Studio.
  • Python version 3.10 or later.
  • uv for dependency management.
  • The Temporal CLI for running a local development server and starting Workflows.

Setup

Before you begin, ensure you have a Temporal development server running locally:

temporal server start-dev

Next, create a project and install the required dependencies:

uv init durable-gemini-agent
cd durable-gemini-agent
uv add "temporalio[google-genai]" httpx python-dotenv

uv creates and manages the virtual environment for you, so every Python command later in this tutorial runs through uv run.

Create a .env file in your project directory with your Gemini API key. You can get an API key from Google AI Studio.

echo "GOOGLE_API_KEY=your-api-key-here" > .env

Implementation

The rest of this tutorial walks through durable_agent_worker.py from top to bottom, building up the agent piece by piece. Create the file and follow along.

Imports and sandbox setup

Start with the imports that must be defined up-front. The workflow.unsafe.imports_passed_through() block tells Temporal's Workflow sandbox to let httpx pass through without restriction. Importing httpx executes class _CookieCompatRequest(urllib.request.Request), and the sandbox blocks subclassing that stdlib class.

Your tools use httpx, and activity_as_tool() needs the Workflow to import those tool functions so Gemini can derive their schemas from the signatures. So httpx reaches the sandbox no matter how you split the files—moving the tools into their own module doesn't avoid it.

from temporalio import workflow

with workflow.unsafe.imports_passed_through():
    import httpx

You don't need to list google.genai here. The Temporal plugin you configure later adds it—along with pydantic_core and annotated_types—to the sandbox passthrough set for you.

System instructions

Next, define the agent's personality. The system instructions tell the model how to behave. This agent is instructed to respond in haikus when no tools are needed.

SYSTEM_INSTRUCTIONS = """
You are a helpful agent that can use tools to help the user.
You will be given an input from the user and a list of tools to use.
You may or may not need to use the tools to satisfy the user ask.
If no tools are needed, respond in haikus.
"""

Tool definitions

Now define the tools the agent can use. Each tool is an ordinary Temporal Activity: an async function decorated with @activity.defn, with type-annotated parameters and a descriptive docstring. Gemini builds the function declaration from that signature and docstring, so document each parameter in the Args section.

import json

from temporalio import activity

NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"

@activity.defn
async def get_weather_alerts(state: str) -> str:
    """Get weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g. CA, NY)
    """
    headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
    url = f"{NWS_API_BASE}/alerts/active/area/{state}"

    async with httpx.AsyncClient() as client:
        response = await client.get(url, headers=headers, timeout=5.0)
        response.raise_for_status()
        return json.dumps(response.json())

Next, define tools for IP address geolocation:

@activity.defn
async def get_ip_address() -> str:
    """Get the public IP address of the current machine."""
    async with httpx.AsyncClient() as client:
        response = await client.get("https://icanhazip.com")
        response.raise_for_status()
        return response.text.strip()

@activity.defn
async def get_location_info(ipaddress: str) -> str:
    """Get the location information for an IP address including city, state, and country.

    Args:
        ipaddress: An IP address to look up
    """
    async with httpx.AsyncClient() as client:
        response = await client.get(f"http://ip-api.com/json/{ipaddress}"