ANTIGRAVITY LABJP
Articles/Agents & Manager
Agents & Manager/2026-04-07Advanced

Antigravity × LangGraph: Complete Masterguide for Stateful AI Agent Development

A comprehensive guide to building stateful AI agents by combining LangGraph with Antigravity. Covers graph-based workflow design, state management, checkpointing, and human-in-the-loop patterns for production-ready systems.

LangGraphagents123statefulworkflow49AI development5Python14LangChain3

Setup and context: Why Stateful AI Agents Matter

As AI agent development matures, simple chain-based processing falls short for increasingly complex scenarios. A code review agent may need to pause and wait for human approval. A data analysis agent may need to branch dynamically based on earlier results. These aren't edge cases — they're core requirements for any production-grade autonomous system.

LangGraph is the answer to these challenges. Developed by LangChain, it lets you define AI agents as graph structures — nodes and edges — with explicit state management that enables complex, branching workflows to be built cleanly and reliably.

In this guide, you'll learn how to leverage Antigravity to implement LangGraph agents efficiently, with practical code examples throughout. This guide assumes familiarity with Python and basic LangChain concepts.

What you'll learn in this article:

  • LangGraph's core concepts: StateGraph, nodes, edges, and checkpointers
  • Using Antigravity to generate and refactor LangGraph code efficiently
  • Production-quality agent design patterns
  • Human-in-the-loop workflow implementation
  • Error handling and rollback strategies

Understanding LangGraph's Core Concepts

StateGraph: The Skeleton of Your Agent

The heart of LangGraph is the StateGraph class. You define your agent's "state" using a TypedDict or Pydantic model, then register nodes (functions) that transform that state.

from typing import TypedDict, Annotated, List
from langgraph.graph import StateGraph, END
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
import operator
 
# Define agent state
class AgentState(TypedDict):
    messages: Annotated[List[BaseMessage], operator.add]  # Message history (append mode)
    current_step: str          # Name of current processing step
    tool_results: dict         # Tool execution results
    iteration_count: int       # Loop counter (prevents infinite loops)
    error: str | None          # Error information
 
# Initialize graph
graph = StateGraph(AgentState)

The Annotated[List[BaseMessage], operator.add] notation is key. This is LangGraph's "Reducer" feature — it specifies how partial states returned from each node should be merged. operator.add means list concatenation, so message history accumulates automatically.

Nodes and Edges: Designing the Flow

Nodes are functions that accept a state and return an updated partial state. Edges define connections between nodes.

from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
 
# Initialize LLM
llm = ChatOpenAI(model="gemini-2.0-flash", temperature=0)
 
# Define tools
@tool
def search_codebase(query: str) -> str:
    """Search the codebase and return relevant code snippets"""
    # In practice, this integrates with Antigravity's MCP tools
    return f"Search results for: {query}"
 
@tool
def run_tests(test_path: str) -> dict:
    """Run tests at the specified path and return results"""
    return {"passed": 10, "failed": 2, "errors": ["test_auth.py:45"]}
 
tools = [search_codebase, run_tests]
llm_with_tools = llm.bind_tools(tools)
 
# Agent node (LLM invocation)
def agent_node(state: AgentState) -> dict:
    """Main LLM agent processing step"""
    response = llm_with_tools.invoke(state["messages"])
    return {
        "messages": [response],
        "current_step": "agent",
        "iteration_count": state.get("iteration_count", 0) + 1
    }
 
# Tool execution node
from langgraph.prebuilt import ToolNode
tool_node = ToolNode(tools)
 
# Conditional edge routing function
def should_continue(state: AgentState) -> str:
    """Determine the next node based on current state"""
    messages = state["messages"]
    last_message = messages[-1]
 
    # Max iteration check (prevents infinite loops)
    if state.get("iteration_count", 0) >= 10:
        return "end"
 
    # Route to tools if there are tool calls
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "tools"
 
    # Otherwise, finish
    return "end"
 
# Build the graph
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)
 
graph.set_entry_point("agent")
graph.add_conditional_edges(
    "agent",
    should_continue,
    {"tools": "tools", "end": END}
)
graph.add_edge("tools", "agent")  # Always return to agent after tool execution
 
app = graph.compile()

Leveraging Antigravity for LangGraph Development

Let Antigravity Handle Project Setup

LangGraph projects have complex dependency trees. With Antigravity, you can generate the right project structure and config files all at once.

Try giving Antigravity the following prompt in chat:

Set up a LangGraph agent project with these requirements:

- Python 3.11+
- LangGraph 0.2+, LangChain 0.3+
- Use Gemini 2.0 Flash as the LLM
- PostgreSQL + pgvector as the checkpoint store
- Dev environment via Docker Compose
- Testing: pytest + pytest-asyncio

Create initial files: pyproject.toml, docker-compose.yml,
.env.example, and src/agents/__init__.py

Antigravity will generate a well-versioned pyproject.toml with proper dependency constraints:

# pyproject.toml (Antigravity-generated example)
[project]
name = "langgraph-agent"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "langgraph>=0.2.0",
    "langchain>=0.3.0",
    "langchain-google-genai>=2.0.0",
    "langchain-community>=0.3.0",
    "psycopg[binary]>=3.1.0",
    "langgraph-checkpoint-postgres>=2.0.0",
    "python-dotenv>=1.0.0",
    "pydantic>=2.0.0",
]
 
[project.optional-dependencies]
dev = [
    "pytest>=8.0.0",
    "pytest-asyncio>=0.23.0",
    "pytest-mock>=3.12.0",
    "ruff>=0.3.0",
]

Refactoring Patterns with Antigravity

Antigravity is a powerful partner for refactoring existing code into LangGraph agents. For example, converting a simple LLM chain into a stateful StateGraph:

  1. Open the existing code in Antigravity's editor
  2. Add a comment like # TODO: Convert to LangGraph StateGraph
  3. Launch Antigravity's inline chat with Cmd+I (or Ctrl+I)
  4. Ask: "Convert this LLM chain to a LangGraph StateGraph. Include messages, current_step, and error fields in the state, and make it compatible with ToolNode."

Antigravity reads the full file context and proposes a conversion that fits your existing code structure.


Checkpointing: Fault Tolerance for Long-Running Tasks

Setting Up PostgreSQL Checkpointing

In production, you need to persist the in-progress state of agents — this is called "checkpointing." LangGraph supports multiple backends: PostgreSQL, SQLite, Redis, and more.

import asyncio
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
 
async def create_agent_with_checkpoints():
    """Create an agent with PostgreSQL checkpointing"""
 
    connection_string = "postgresql://user:password@localhost:5432/langgraph_db"
 
    async with AsyncConnectionPool(
        conninfo=connection_string,
        max_size=20,
        kwargs={"autocommit": True, "prepare_threshold": 0}
    ) as pool:
        # Initialize checkpointer
        checkpointer = AsyncPostgresSaver(pool)
        await checkpointer.setup()  # Auto-creates required tables
 
        # Compile graph with checkpointer
        app = graph.compile(checkpointer=checkpointer)
 
        # Use thread_id to maintain conversation continuity
        config = {
            "configurable": {
                "thread_id": "user-123-task-456",
                "checkpoint_ns": "code_review_agent"
            }
        }
 
        # First invocation
        initial_state = {
            "messages": [HumanMessage(content="Please review this PR")],
            "current_step": "start",
            "tool_results": {},
            "iteration_count": 0,
            "error": None
        }
 
        result = await app.ainvoke(initial_state, config=config)
        print(f"First run result: {result['messages'][-1].content}")
 
        # Resume later using the same thread_id (continues from saved state)
        followup = {
            "messages": [HumanMessage(content="Focus especially on the security section")]
        }
        result2 = await app.ainvoke(followup, config=config)
 
        return result2

Inspecting and Debugging Checkpoint State

Combining Antigravity's debugging capabilities with LangGraph's checkpoint inspection lets you visualize state progression during development.

async def inspect_checkpoints(app, thread_id: str):
    """Inspect the checkpoint history for a given thread"""
    config = {"configurable": {"thread_id": thread_id}}
 
    # Get current state
    current_state = await app.aget_state(config)
    print("Current state values:", current_state.values)
    print("Next nodes:", current_state.next)
    print("Created at:", current_state.created_at)
 
    # Retrieve checkpoint history (most recent 3)
    history = []
    async for state in app.aget_state_history(config, limit=3):
        history.append({
            "checkpoint_id": state.config["configurable"]["checkpoint_id"],
            "step": state.values.get("current_step"),
            "message_count": len(state.values.get("messages", [])),
            "created_at": state.created_at
        })
 
    return history
 
# Roll back to a specific checkpoint
async def rollback_to_checkpoint(app, thread_id: str, checkpoint_id: str):
    """Roll back to a specific checkpoint and re-run from that point"""
    config = {
        "configurable": {
            "thread_id": thread_id,
            "checkpoint_id": checkpoint_id  # Target checkpoint to restore
        }
    }
    result = await app.ainvoke(
        {"messages": [HumanMessage(content="Try a different approach")]},
        config=config
    )
    return result

Human-in-the-Loop: Agents That Wait for Approval

Implementing Approval Flows with interrupt_before

When automating complex tasks, you often need human approval before certain actions — updating a production database, making external API calls, deploying to production. LangGraph's interrupt_before feature makes this clean to implement.

from langgraph.graph import StateGraph, END
 
# High-risk node requiring human approval
def deploy_to_production(state: AgentState) -> dict:
    """Deploy to production (requires approval via interrupt_before)"""
    deploy_result = execute_deployment(state["tool_results"]["build_artifact"])
    return {
        "messages": [AIMessage(content=f"Deployment complete: {deploy_result}")],
        "current_step": "deployed"
    }
 
def prepare_deployment(state: AgentState) -> dict:
    """Prepare deployment (build, test, etc.)"""
    return {
        "tool_results": {"build_artifact": "dist/app.zip", "test_passed": True},
        "current_step": "deploy_ready",
        "messages": [AIMessage(content="Build and tests passed. Ready to deploy to production — please confirm.")]
    }
 
# Build deployment graph
deploy_graph = StateGraph(AgentState)
deploy_graph.add_node("prepare", prepare_deployment)
deploy_graph.add_node("deploy", deploy_to_production)
deploy_graph.set_entry_point("prepare")
deploy_graph.add_edge("prepare", "deploy")
deploy_graph.add_edge("deploy", END)
 
# Pause before the "deploy" node for human review
deploy_app = deploy_graph.compile(
    checkpointer=checkpointer,
    interrupt_before=["deploy"]
)
 
# Execute — agent will pause before "deploy"
config = {"configurable": {"thread_id": "deploy-task-001"}}
state = await deploy_app.ainvoke(initial_state, config=config)
 
# At this point, the agent is paused before the deploy node
current = await deploy_app.aget_state(config)
print("Waiting for approval:", current.next)  # ['deploy']
print("Build result:", current.values["tool_results"])
 
# Human approves — pass None to continue without changing state
await deploy_app.ainvoke(None, config=config)
 
# Human approves with additional instructions
await deploy_app.aupdate_state(
    config,
    {"messages": [HumanMessage(content="Approved. Please also deploy to staging simultaneously.")]}
)
await deploy_app.ainvoke(None, config=config)

Wrapping Human-in-the-Loop in a FastAPI Endpoint

Ask Antigravity to generate a REST API wrapper for this workflow:

Add FastAPI endpoints to wrap this LangGraph agent:

1. POST /tasks — Start a new task, return task_id
2. GET /tasks/{task_id}/status — Return status (include waiting_approval flag)
3. POST /tasks/{task_id}/approve — Resume the agent with approval
4. POST /tasks/{task_id}/reject — Reject and provide alternative instructions

Use asyncio and BackgroundTasks for async processing

Multi-Agent Orchestration

The Supervisor Pattern

For large-scale tasks, a "supervisor agent" that coordinates multiple specialized agents is a powerful architectural pattern.

from langgraph.graph import StateGraph, END
from typing import Literal
 
class SupervisorState(TypedDict):
    messages: Annotated[List[BaseMessage], operator.add]
    next_agent: str
    task_context: dict
    completed_tasks: List[str]
 
# Supervisor node
def supervisor_node(state: SupervisorState) -> dict:
    """Decide which specialized agent to run next"""
 
    system_prompt = """You are an agent orchestrator.
    Coordinate the following specialized agents to complete tasks:
    - researcher: information gathering and research
    - coder: code implementation and fixes
    - reviewer: code review and testing
    - documenter: documentation generation
    - FINISH: all tasks complete
 
    Respond with exactly one word: the name of the next agent to run.
    """
 
    response = llm.invoke([
        {"role": "system", "content": system_prompt},
        *state["messages"]
    ])
 
    return {"next_agent": response.content.strip()}
 
# Specialized agent (researcher example)
def researcher_agent(state: SupervisorState) -> dict:
    """Specialized research agent"""
    research_llm = llm_with_tools  # LLM equipped with web_search, read_doc tools
 
    result = research_llm.invoke([
        {"role": "system", "content": "You are a technical research specialist."},
        *state["messages"]
    ])
 
    return {
        "messages": [result],
        "completed_tasks": state.get("completed_tasks", []) + ["research"]
    }
 
# Routing function
def route_to_agent(state: SupervisorState) -> Literal["researcher", "coder", "reviewer", "documenter", "end"]:
    agent = state["next_agent"].lower()
    if agent == "finish":
        return "end"
    return agent if agent in ["researcher", "coder", "reviewer", "documenter"] else "end"
 
# Build supervisor graph
supervisor_graph = StateGraph(SupervisorState)
supervisor_graph.add_node("supervisor", supervisor_node)
supervisor_graph.add_node("researcher", researcher_agent)
supervisor_graph.add_node("coder", coder_agent)
supervisor_graph.add_node("reviewer", reviewer_agent)
supervisor_graph.add_node("documenter", documenter_agent)
 
supervisor_graph.set_entry_point("supervisor")
supervisor_graph.add_conditional_edges(
    "supervisor",
    route_to_agent,
    {
        "researcher": "researcher",
        "coder": "coder",
        "reviewer": "reviewer",
        "documenter": "documenter",
        "end": END
    }
)
 
# All specialized agents return to supervisor after completion
for agent in ["researcher", "coder", "reviewer", "documenter"]:
    supervisor_graph.add_edge(agent, "supervisor")
 
multi_agent_app = supervisor_graph.compile(checkpointer=checkpointer)

Error Handling and Retry Strategies

Robust Error Handling Patterns

Production environments require graceful handling of LLM rate limits and tool execution failures.

import time
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
 
def agent_node_with_retry(state: AgentState) -> dict:
    """Agent node with retry logic built in"""
 
    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=2, max=10),
        retry=retry_if_exception_type((Exception,)),
        reraise=True
    )
    def _invoke_with_retry():
        return llm_with_tools.invoke(state["messages"])
 
    try:
        response = _invoke_with_retry()
        return {
            "messages": [response],
            "current_step": "agent",
            "error": None,
            "iteration_count": state.get("iteration_count", 0) + 1
        }
    except Exception as e:
        error_msg = f"Agent execution error: {str(e)}"
        return {
            "messages": [AIMessage(content=f"An error occurred: {error_msg}")],
            "current_step": "error",
            "error": error_msg
        }
 
# Error recovery node
def error_handler_node(state: AgentState) -> dict:
    """Handle error state and attempt recovery"""
    error = state.get("error", "Unknown error")
 
    recovery_response = llm.invoke([
        *state["messages"],
        HumanMessage(content=f"An error occurred: {error}\nPlease try an alternative approach to achieve the goal.")
    ])
 
    return {
        "messages": [recovery_response],
        "current_step": "recovery",
        "error": None  # Clear the error
    }
 
# Route on error
def route_on_error(state: AgentState) -> str:
    if state.get("error"):
        return "error_handler"
    return should_continue(state)
 
graph.add_node("error_handler", error_handler_node)
graph.add_conditional_edges("agent", route_on_error, {
    "tools": "tools",
    "end": END,
    "error_handler": "error_handler"
})
graph.add_edge("error_handler", "agent")  # Retry after recovery

Observability with LangSmith

Setting Up Tracing

LangSmith provides powerful tracing for debugging LangGraph agents. Antigravity makes it easy to scaffold the tracing configuration.

import os
from langsmith.run_helpers import traceable
 
# Enable LangSmith via environment variables
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "YOUR_LANGSMITH_API_KEY"
os.environ["LANGCHAIN_PROJECT"] = "antigravitylab-agent"
 
@traceable(name="code_review_workflow", metadata={"version": "1.0", "env": "production"})
async def run_code_review_agent(pr_url: str, thread_id: str) -> dict:
    """Run code review agent with full LangSmith tracing"""
    config = {"configurable": {"thread_id": thread_id}}
 
    result = await app.ainvoke(
        {"messages": [HumanMessage(content=f"Please review this PR: {pr_url}")]},
        config=config
    )
 
    return {
        "review": result["messages"][-1].content,
        "steps_taken": result.get("iteration_count", 0)
    }

The LangSmith dashboard gives you real-time visibility into token usage, latency, and error rates per run. It's invaluable for monthly cost analysis and performance optimization.


Testing Strategy

Unit Testing LangGraph Agents

Use Antigravity to generate well-structured test scaffolding for your agents.

import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from langchain_core.messages import HumanMessage, AIMessage
 
@pytest.mark.asyncio
async def test_agent_node_normal_flow():
    """Test normal execution path of agent node"""
    mock_response = AIMessage(
        content="I'll search the codebase",
        tool_calls=[{
            "name": "search_codebase",
            "args": {"query": "authentication"},
            "id": "call_001"
        }]
    )
 
    with patch.object(llm_with_tools, "invoke", return_value=mock_response):
        state = AgentState(
            messages=[HumanMessage(content="Look into the auth code")],
            current_step="start",
            tool_results={},
            iteration_count=0,
            error=None
        )
 
        result = agent_node(state)
 
        assert result["current_step"] == "agent"
        assert result["iteration_count"] == 1
        assert len(result["messages"]) == 1
        assert result["messages"][0].tool_calls[0]["name"] == "search_codebase"
 
@pytest.mark.asyncio
async def test_infinite_loop_prevention():
    """Test that infinite loop prevention works correctly"""
    state = AgentState(
        messages=[HumanMessage(content="test")],
        current_step="agent",
        tool_results={},
        iteration_count=10,  # Already at max
        error=None
    )
 
    mock_response = AIMessage(
        content="",
        tool_calls=[{"name": "search_codebase", "args": {}, "id": "call_999"}]
    )
 
    with patch.object(llm_with_tools, "invoke", return_value=mock_response):
        next_node = should_continue(state)
        # Should return "end" since iteration_count has reached the limit
        assert next_node == "end"
 
@pytest.mark.asyncio
async def test_checkpoint_persistence():
    """Test checkpoint persistence using in-memory SQLite"""
    from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
 
    async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
        test_app = graph.compile(checkpointer=checkpointer)
        config = {"configurable": {"thread_id": "test-thread-001"}}
 
        # First run
        await test_app.ainvoke(
            {"messages": [HumanMessage(content="Hello")]},
            config=config
        )
 
        # Verify checkpoint was saved
        state = await test_app.aget_state(config)
        assert state is not None
        assert len(state.values["messages"]) > 0

Cost Optimization and Token Management

Efficiently Trimming Message History

The single most impactful lever for reducing LangGraph agent costs is message history management. LangChain's trimMessages function lets you drop older messages while preserving the context that actually matters.

from langchain_core.messages import trim_messages, SystemMessage
 
def trim_agent_messages(state: AgentState) -> dict:
    """Trim message history to reduce token usage"""
 
    trimmed = trim_messages(
        state["messages"],
        max_tokens=4096,          # A good target is ~50% of the model's input limit
        strategy="last",          # Keep the most recent messages
        token_counter=llm,        # Use the model's own tokenizer for counting
        include_system=True,      # Always preserve system messages
        allow_partial=False,      # Never split a message mid-way
        start_on="human"          # Ensure the history starts with a HumanMessage
    )
 
    return {"messages": trimmed}
 
# Add the trimming node to the graph
graph.add_node("trim_history", trim_agent_messages)
graph.add_edge("tools", "trim_history")    # Trim after each tool execution
graph.add_edge("trim_history", "agent")    # Then return to agent

Implementing Tool Call Caching

When the same tool gets called repeatedly with identical arguments, caching can yield significant cost savings.

import hashlib
import json
from datetime import datetime, timedelta
 
class CachedToolWrapper:
    """Tool wrapper with TTL-based caching"""
 
    def __init__(self, tool_func, ttl_minutes: int = 10):
        self.tool_func = tool_func
        self.ttl = timedelta(minutes=ttl_minutes)
        self._cache: dict = {}
 
    def _make_cache_key(self, **kwargs) -> str:
        """Generate a deterministic cache key from arguments"""
        return hashlib.md5(json.dumps(kwargs, sort_keys=True).encode()).hexdigest()
 
    def __call__(self, **kwargs) -> str:
        cache_key = self._make_cache_key(**kwargs)
        now = datetime.now()
 
        # Check cache
        if cache_key in self._cache:
            result, timestamp = self._cache[cache_key]
            if now - timestamp < self.ttl:
                print(f"Cache hit for {self.tool_func.__name__}")
                return result
 
        # Cache miss: execute the tool
        result = self.tool_func(**kwargs)
        self._cache[cache_key] = (result, now)
        return result
 
# Create cached version of expensive tools
cached_search = CachedToolWrapper(search_codebase, ttl_minutes=5)

Adaptive Model Selection to Minimize Cost

For the same task, switching between models based on required precision can dramatically cut costs without sacrificing quality where it counts.

from langchain_google_genai import ChatGoogleGenerativeAI
 
class AdaptiveModelSelector:
    """Dynamically select the optimal model based on task complexity"""
 
    def __init__(self):
        # Cost-efficient (for simpler tasks)
        self.flash_model = ChatGoogleGenerativeAI(
            model="gemini-2.0-flash",
            temperature=0
        )
        # High-precision (for complex tasks)
        self.pro_model = ChatGoogleGenerativeAI(
            model="gemini-2.0-pro",
            temperature=0
        )
 
    def select_model(self, task_complexity: str, requires_reasoning: bool = False):
        """Return the best model for the given task characteristics"""
        if requires_reasoning or task_complexity == "high":
            return self.pro_model
        return self.flash_model
 
# Apply adaptive model selection in the agent node
def adaptive_agent_node(state: AgentState) -> dict:
    """Agent node that switches models based on complexity signals"""
    selector = AdaptiveModelSelector()
 
    iteration = state.get("iteration_count", 0)
    is_complex = iteration > 3 or "architecture" in str(state["messages"]).lower()
 
    model = selector.select_model(
        task_complexity="high" if is_complex else "low",
        requires_reasoning=is_complex
    )
 
    model_with_tools = model.bind_tools(tools)
    response = model_with_tools.invoke(state["messages"])
 
    return {
        "messages": [response],
        "current_step": "agent",
        "iteration_count": iteration + 1
    }

Production Deployment Best Practices

Building an Agent API Server with FastAPI

Here's a production-ready pattern for exposing a LangGraph agent as a REST API.

from fastapi import FastAPI, BackgroundTasks, HTTPException
from pydantic import BaseModel
import uuid
from typing import Optional
 
api_server = FastAPI(title="LangGraph Agent API")
 
# In-memory task registry (use Redis or Postgres in production)
task_registry: dict = {}
 
class TaskCreateRequest(BaseModel):
    task_description: str
    user_id: str
    priority: str = "normal"
 
class TaskStatusResponse(BaseModel):
    task_id: str
    status: str           # running / waiting_approval / completed / failed
    current_step: str
    result: Optional[str] = None
    waiting_for: Optional[str] = None
 
@api_server.post("/tasks", response_model=dict)
async def create_task(
    request: TaskCreateRequest,
    background_tasks: BackgroundTasks
):
    """Start a new agent task"""
    task_id = str(uuid.uuid4())
 
    task_registry[task_id] = {
        "status": "running",
        "current_step": "start",
        "result": None,
        "error": None
    }
 
    background_tasks.add_task(
        run_agent_task,
        task_id=task_id,
        description=request.task_description,
        thread_id=f"{request.user_id}-{task_id}"
    )
 
    return {"task_id": task_id, "status": "started"}
 
async def run_agent_task(task_id: str, description: str, thread_id: str):
    """Execute the agent task in the background"""
    config = {"configurable": {"thread_id": thread_id}}
 
    try:
        result = await app.ainvoke(
            {
                "messages": [HumanMessage(content=description)],
                "current_step": "start",
                "tool_results": {},
                "iteration_count": 0,
                "error": None
            },
            config=config
        )
 
        # Check if agent paused at an interrupt_before node
        state = await app.aget_state(config)
        if state.next:  # Remaining nodes = waiting for approval
            task_registry[task_id].update({
                "status": "waiting_approval",
                "current_step": state.next[0],
                "waiting_for": f"Approval required for: {state.next[0]}"
            })
        else:
            task_registry[task_id].update({
                "status": "completed",
                "result": result["messages"][-1].content
            })
 
    except Exception as e:
        task_registry[task_id].update({
            "status": "failed",
            "error": str(e)
        })
 
@api_server.get("/tasks/{task_id}/status", response_model=TaskStatusResponse)
async def get_task_status(task_id: str):
    """Get the current status of a task"""
    if task_id not in task_registry:
        raise HTTPException(status_code=404, detail="Task not found")
 
    task = task_registry[task_id]
    return TaskStatusResponse(
        task_id=task_id,
        status=task["status"],
        current_step=task.get("current_step", "unknown"),
        result=task.get("result"),
        waiting_for=task.get("waiting_for")
    )
 
@api_server.post("/tasks/{task_id}/approve")
async def approve_task(task_id: str, background_tasks: BackgroundTasks):
    """Resume a task that is waiting for approval"""
    if task_id not in task_registry:
        raise HTTPException(status_code=404, detail="Task not found")
 
    if task_registry[task_id]["status"] != "waiting_approval":
        raise HTTPException(status_code=400, detail="Task is not waiting for approval")
 
    task_registry[task_id]["status"] = "running"
    background_tasks.add_task(resume_agent_task, task_id=task_id)
    return {"message": "Task resumed", "task_id": task_id}

Health Checks and Graceful Shutdown

from contextlib import asynccontextmanager
 
@asynccontextmanager
async def lifespan(app: FastAPI):
    """Manage application lifecycle for safe startup and shutdown"""
    print("🚀 Starting LangGraph Agent API Server...")
    await checkpointer.setup()
    print("✅ Checkpoint store initialized")
 
    yield  # Application runs here
 
    # On shutdown: safely handle in-flight tasks
    print("🔄 Shutting down gracefully...")
    running_tasks = [
        tid for tid, t in task_registry.items()
        if t["status"] == "running"
    ]
    if running_tasks:
        print(f"⚠️ {len(running_tasks)} tasks still running — state is saved in checkpoint store")
        # Tasks can be resumed after restart since state is persisted
 
    print("✅ Shutdown complete")
 
api_server = FastAPI(title="LangGraph Agent API", lifespan=lifespan)
 
@api_server.get("/health")
async def health_check():
    """Health check endpoint for load balancer probes"""
    return {
        "status": "healthy",
        "active_tasks": len([t for t in task_registry.values() if t["status"] == "running"]),
        "pending_approvals": len([t for t in task_registry.values() if t["status"] == "waiting_approval"])
    }

Deployment Checklist

Before shipping a LangGraph agent to production, run through these key checks:

Environment and configuration — all secrets are loaded from environment variables, not hardcoded; LANGCHAIN_TRACING_V2=true is enabled for LangSmith observability; rate limit handling is configured for your chosen LLM provider.

State design — message history trimming is in place to prevent runaway token costs; iteration limits are set on all loops to prevent infinite recursion; the TypedDict or Pydantic state model has well-defined types for all fields.

Persistence — the checkpoint store is backed by a persistent database (not SQLite in production); connection pooling is configured appropriately for expected concurrency; checkpoints are periodically cleaned up to prevent unbounded storage growth.

Error handling — all nodes have try/except with meaningful error messages; an error recovery node allows the agent to attempt alternative approaches; circuit breakers are in place for external API calls.

Testing — unit tests cover all conditional edge routing functions; integration tests use in-memory SQLite to verify checkpoint persistence; load tests validate behavior at expected concurrency levels.


Real-World Architecture Patterns

Event-Driven Agent Triggers

In production systems, LangGraph agents are often triggered by external events rather than direct API calls. A common pattern is consuming messages from a queue and processing them asynchronously.

import asyncio
from typing import AsyncIterator
 
async def event_driven_agent_loop(queue_client, max_concurrent: int = 5):
    """Continuously process tasks from an event queue"""
    semaphore = asyncio.Semaphore(max_concurrent)
 
    async def process_event(event: dict):
        async with semaphore:
            task_id = event.get("task_id", str(uuid.uuid4()))
            thread_id = f"event-{task_id}"
            config = {"configurable": {"thread_id": thread_id}}
 
            print(f"Processing event: {task_id}")
 
            try:
                result = await app.ainvoke(
                    {
                        "messages": [HumanMessage(content=event["payload"])],
                        "current_step": "start",
                        "tool_results": {},
                        "iteration_count": 0,
                        "error": None
                    },
                    config=config
                )
 
                # Publish result to output queue
                await queue_client.publish(
                    topic="agent-results",
                    message={
                        "task_id": task_id,
                        "status": "completed",
                        "result": result["messages"][-1].content
                    }
                )
 
            except Exception as e:
                await queue_client.publish(
                    topic="agent-errors",
                    message={"task_id": task_id, "error": str(e)}
                )
 
    # Main event loop
    async for event in queue_client.consume(topic="agent-tasks"):
        asyncio.create_task(process_event(event))

Combining Antigravity MCP Tools with LangGraph

One of the most powerful patterns when developing with Antigravity is connecting your LangGraph agents directly to Antigravity's built-in MCP tools. This means your agent can perform real code searches, read files, and run terminal commands as part of its workflow.

# Example: Code analysis agent powered by Antigravity MCP tools
 
@tool
def read_file_content(file_path: str) -> str:
    """Read the contents of a file in the current project"""
    # This would connect to Antigravity's file reading capability via MCP
    with open(file_path, 'r') as f:
        return f.read()
 
@tool
def search_in_files(pattern: str, directory: str = ".") -> list:
    """Search for a pattern across project files"""
    # Connects to Antigravity's code search via MCP
    import subprocess
    result = subprocess.run(
        ["grep", "-rn", pattern, directory, "--include=*.py"],
        capture_output=True, text=True
    )
    return result.stdout.split('\n')[:20]  # Return first 20 matches
 
@tool
def run_command(command: str) -> dict:
    """Execute a shell command and return the output"""
    import subprocess
    result = subprocess.run(
        command.split(), capture_output=True, text=True, timeout=30
    )
    return {
        "stdout": result.stdout,
        "stderr": result.stderr,
        "returncode": result.returncode
    }
 
# Build a code analysis agent with these tools
code_analysis_tools = [read_file_content, search_in_files, run_command]
analysis_llm = llm.bind_tools(code_analysis_tools)

When combined with LangGraph's stateful architecture, this approach lets you build agents that can autonomously navigate your codebase, run tests, and make incremental improvements — all within a tracked, resumable workflow that you can inspect and roll back at any point.


Summary

In this guide, we explored how to combine LangGraph with Antigravity to build production-quality stateful AI agents.

The key takeaways: LangGraph's StateGraph and Reducer patterns let you declare complex agent data flows cleanly. PostgreSQL checkpointing gives you fault tolerance and conversation continuity for long-running tasks. The interrupt_before pattern for human-in-the-loop is an essential building block for safely automating high-stakes workflows. The supervisor pattern enables sophisticated orchestration of specialized agents. And throughout all of this, Antigravity — with its deep understanding of your codebase context — significantly accelerates LangGraph code generation, refactoring, and debugging.

LangGraph and Antigravity together form a powerful combination that meaningfully raises the productivity ceiling for AI agent development. Use the code in this guide as a foundation and adapt it to your own projects.

If you'd like to solidify your foundation before or alongside this guide, the Complete Guide to AI Agent System Design covers the architectural principles that make stateful agents reliable at scale. For a practical look at orchestrating multiple LangGraph agents together, Getting Started with Multi-Agent Development in Antigravity AgentKit 2.0 pairs naturally with what we've covered here.

Share

Thank You for Reading

Antigravity 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

Agents & Manager2026-06-18
Three Boundaries I Draw Before Handing Work to an Antigravity 2.0 Agent
What to hand a background agent, and what to keep in your own hands. The three boundaries I actually drew while running solo-dev automation in parallel, and how to encode them so the lines hold.
Agents & Manager2026-05-10
Defining 'Done' with Antigravity Agents: Writing Acceptance Criteria into Your Prompts
When Antigravity returns code that is only halfway working, the usual cause is a missing Definition of Done. Here is the three-layer fix.
Agents & Manager2026-04-28
Stopping an Antigravity Agent Mid-Run: How to Pause and Resume Without Losing Your Work
When an agent starts heading in the wrong direction, hitting Stop without thinking can leave half-edited files and broken checkpoints. Here is the workflow I use to pause an agent and resume cleanly.
📚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 →