You will build a production-grade Python AI code review agent that intercepts destructive pull request modifications, pauses execution for explicit authorization, and safely resumes using stateful checkpoints. By mastering agentic workflows human in the loop, you can automate repository audits without risking unauthorized code pushes.
- Architecting stateful LLM code reviewers using LangGraph and Python
- Implementing a LangGraph human approval step to pause and resume workflows
- Connecting external code repositories via MCP GitHub integration Python
- Enforcing a secure agentic deployment workflow for automated PR remediation
Introduction
Most senior engineers would rather debug a memory leak in a C extension on a Friday afternoon than hand an LLM unrestricted merge access to their production repository. When we started building agentic workflows human in the loop into our CI/CD pipelines, our autonomous reviewer cheerfully deleted three weeks of logging logic and tried to push the commit straight to main. Unchecked AI agency in production is not a productivity boost; it is a fast track to an emergency incident review.
As autonomous agents increasingly execute high-stakes write operations in production environments, developers in October 2026 urgently need reliable human-in-the-loop patterns to intercept and approve destructive steps before execution. Building robust safeguard gates separates a dangerous toy script from a reliable engineering assistant.
In this comprehensive tutorial, we will build a stateful Python AI code review agent from scratch. You will learn how to intercept code changes, serialize agent state while awaiting human review via a webhook, and resume execution without losing context.
Understanding Stateful Agentic Architectures
Traditional script-based linters run blindly, spitting out warnings that developers often ignore. A modern python ai code review agent goes further by analyzing diffs, proposing targeted refactors, and writing patches directly to the codebase.
However, granting an LLM write access requires a shift in how we manage state. We cannot rely on stateless request-response cycles when an agent needs to wait hours for a reviewer to click "Approve" on a dashboard.
This is where building stateful LLM code reviewers changes the equation. By leveraging graph-based orchestration frameworks, we can persist memory to disk or a database, interrupt the execution graph mid-flight, and safely wait for external authorization.
State persistence is what separates interactive agents from simple CLI scripts. If your server restarts while waiting for a review, a stateful graph ensures zero context loss.
Project Architecture and Directory Structure
To keep our codebase modular, testable, and production-ready, we will structure our project using a clean separation of concerns. Our agent relies on LangGraph for state management, the Model Context Protocol (MCP) for GitHub interactions, and FastAPI to handle our webhook approval signals.
Before writing any code, set up your local project directory with the following structure. This layout isolates our graph definition, our GitHub integration tools, and our API routing layer.
# Create directory structure for our review agent
mkdir -p ai-code-reviewer/{agent,mcp_client,api,tests}
cd ai-code-reviewer
touch agent/__init__.py agent/graph.py agent/state.py
touch mcp_client/__init__.py mcp_client/github_tools.py
touch api/__init__.py api/server.py
touch requirements.txt main.py .envThis layout ensures that our LangGraph definitions reside independently from our external API endpoints. Keeping the MCP GitHub integration Python logic self-contained also allows us to unit test our repository tools without booting up the full web server.
Always keep your tool definitions decoupled from your graph state nodes. This makes swapping out GitHub for GitLab or Bitbucket trivial later on.
Configuring the Dependencies and State Schema
We need to install our core libraries, including LangGraph for workflow orchestration, Pydantic for data validation, and the official GitHub MCP client wrappers. Pin your dependencies to ensure reproducible builds across environments.
Open your requirements.txt file and add the required production packages with exact version constraints.
langgraph>=0.2.0
langchain-openai>=0.2.0
fastapi>=0.115.0
uvicorn>=0.32.0
pydantic>=2.9.0
httpx>=0.27.0
python-dotenv>=1.0.1
mcp>=1.0.0Next, we define our graph state schema in agent/state.py. This TypedDict tracks the pull request diff, the AI's proposed refactor, and our explicit approval status.
# agent/state.py
from typing import Annotated, TypedDict, List, Optional
from langgraph.graph.message import add_messages
class ReviewState(TypedDict):
pr_id: str
repository: str
diff_content: str
ai_review_comments: List[str]
proposed_patch: Optional[str]
approval_status: str # "pending", "approved", "rejected"
reviewer_notes: Optional[str]The ReviewState dictionary acts as the single source of truth passed between every node in our graph. By tracking approval_status explicitly, our conditional routing logic can inspect this field to decide whether to push changes or halt execution.
Never store raw API tokens inside your LangGraph state dictionaries. State is often serialized to disk or database tables, which can leak secrets if logged improperly.
Implementing the MCP GitHub Integration
To allow our agent to fetch pull request diffs and post review comments securely, we use the Model Context Protocol. MCP standardizes how LLMs interact with external resources like file systems and Git repositories.
Create mcp_client/github_tools.py to interface with GitHub using authenticated HTTP requests wrapped in clean utility functions.
# mcp_client/github_tools.py
import os
import httpx
from dotenv import load_dotenv
load_dotenv()
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
HEADERS = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "vnd.github+json"
}
async def fetch_pr_diff(repo: str, pr_number: int) -> str:
url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
async with httpx.AsyncClient() as client:
response = await client.get(url, headers={**HEADERS, "Accept": "application/vnd.github.diff"})
response.raise_for_status()
return response.text
async def post_review_comment(repo: str, pr_number: int, body: str) -> None:
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=HEADERS, json={"body": body})
response.raise_for_status()These asynchronous utility functions provide our agent with hands and eyes inside the target repository. Using httpx ensures our graph nodes remain fully non-blocking during network calls.
Always request specific media types like vnd.github.diff when fetching pull request changes to avoid parsing raw JSON payloads manually.
Building the LangGraph Workflow with Human Approval
Now we construct the core graph in agent/graph.py. We need nodes for analyzing the code, generating a patch, waiting for human input, and executing the write operation upon approval.
We configure LangGraph with a memory saver checkpointer so execution pauses cleanly at the approval gate without crashing.
# agent/graph.py
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from agent.state import ReviewState
from mcp_client.github_tools import post_review_comment
def analyze_code_node(state: ReviewState) -> ReviewState:
# Simulate LLM reviewing the diff
diff = state["diff_content"]
comments = ["Consider adding input validation on line 42."]
patch = "diff --git a/app.py b/app.py\n+ # validated input"
return {
**state,
"ai_review_comments": comments,
"proposed_patch": patch,
"approval_status": "pending"
}
def human_approval_node(state: ReviewState) -> ReviewState:
# This node acts as an interrupt boundary in LangGraph
return state
def execute_deployment_node(state: ReviewState) -> ReviewState:
# Post approved changes back to GitHub
repo = state["repository"]
pr_id = int(state["pr_id"])
patch = state["proposed_patch"]
post_review_comment(repo, pr_id, f"Approved patch applied:\n```diff\n{patch}\n```")
return {**state, "approval_status": "executed"}
def route_after_approval(state: ReviewState) -> str:
status = state.get("approval_status")
if status == "approved":
return "execute_deployment"
return END
# Build the workflow graph
workflow = StateGraph(ReviewState)
workflow.add_node("analyze_code", analyze_code_node)
workflow.add_node("human_approval", human_approval_node)
workflow.add_node("execute_deployment", execute_deployment_node)
workflow.set_entry_point("analyze_code")
workflow.add_edge("analyze_code", "human_approval")
workflow.add_conditional_edges(
"human_approval",
route_after_approval,
{
"execute_deployment": "execute_deployment",
END: END
}
)
memory = MemorySaver()
# Compile graph with an explicit interrupt before the approval node
app = workflow.compile(checkpointer=memory, interrupt_before=["human_approval"])In this script, compiling the graph with interrupt_before=["human_approval"] tells LangGraph to execute analyze_code_node, save the state to our checkpointer, and halt right before entering the approval step. This fulfills our secure agentic deployment workflow requirement by ensuring zero automated writes occur without an external trigger.
In a production setup, replace MemorySaver with a persistent PostgreSQL checkpointer using langgraph-checkpoint-postgres so state survives server reboots.
Exposing the Webhook and Resume Endpoints
To let reviewers approve or reject patches from a Slack notification or web dashboard, we need a FastAPI server that can inspect paused threads and resume them with updated state.
Create api/server.py to manage graph execution threads and handle incoming webhook decisions.
# api/server.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from agent.graph import app, memory
from mcp_client.github_tools import fetch_pr_diff
api = FastAPI(title="AI Code Reviewer Agent")
class ReviewInitiateRequest(BaseModel):
repository: str
pr_number: int
class DecisionRequest(BaseModel):
thread_id: str
decision: str # "approved" or "rejected"
notes: str = ""
@api.post("/review/start")
async def start_review(req: ReviewInitiateRequest):
diff = await fetch_pr_diff(req.repository, req.pr_number)
thread_id = f"{req.repository}-{req.pr_number}"
config = {"configurable": {"thread_id": thread_id}}
initial_state = {
"pr_id": str(req.pr_number),
"repository": req.repository,
"diff_content": diff,
"ai_review_comments": [],
"proposed_patch": None,
"approval_status": "pending",
"reviewer_notes": None
}
# Run until the first interrupt (human_approval)
result = app.invoke(initial_state, config)
return {"thread_id": thread_id, "status": "paused_for_approval", "state": result}
@api.post("/review/decide")
async def make_decision(req: DecisionRequest):
config = {"configurable": {"thread_id": req.thread_id}}
# Fetch current checkpoint state
current_state = app.get_state(config)
if not current_state.values:
raise HTTPException(status_code=404, detail="Review thread not found.")
# Update state with human decision
updated_values = {
**current_state.values,
"approval_status": req.decision,
"reviewer_notes": req.notes
}
app.update_state(config, updated_values)
# Resume graph execution from the interrupt point
final_result = app.invoke(None, config)
return {"status": "resumed", "result": final_result}This API bridges the gap between asynchronous human intervention and synchronous graph execution. When a reviewer submits their choice to /review/decide, our server updates the persisted checkpointer state and invokes the graph with None, causing LangGraph to resume right where it paused.
Using app.update_state() allows you to inject human feedback, corrected code snippets, or reviewer notes directly into the state before resuming execution.
Best Practices and Common Pitfalls
Enforcing Token Budgets and Diff Limits
Pull requests can span thousands of lines of legacy code, easily blowing out LLM context windows and incurring massive API costs. Always truncate diffs or chunk large files into logical components before passing them to your review agent.
Common Pitfall: Blindly Trusting Automated Patches
A frequent mistake is letting agents execute git pushes without validating patch syntax first. Always run automated unit tests and syntax checks inside an ephemeral sandbox container before applying any AI-generated diff to a live branch.
Allowing agents to operate without rate limits can trigger infinite loops of self-correction comments on GitHub pull requests. Always cap agent retry attempts.
Real-World Case Study: Financial Tech Security Audits
At a mid-sized fintech firm processing thousands of daily transactions, engineering teams faced compliance bottlenecks during routine pull request reviews. Junior developers frequently introduced subtle SQL injection vectors in migration scripts that standard linters missed.
By implementing a secure agentic deployment workflow powered by LangGraph and MCP, the platform team automated initial security audits. When the agent detects database alterations, it generates a cryptographically signed migration patch, halts execution, and pings the lead security engineer via Slack.
The engineer reviews the diff directly in their browser, clicks "Approve," and the agent safely merges the remediation. This pattern reduced security review turnaround times from 36 hours to under 15 minutes while maintaining strict SOX compliance.
Future Outlook and What's Coming Next
As we look further into 2027, agentic workflows are evolving beyond simple review bots into multi-agent collaborative networks. Upcoming specifications in the Model Context Protocol ecosystem will allow agents to securely negotiate dependency upgrades across independent microservices.
We will soon see standard cryptographic attestation protocols embedded directly into stateful checkpointers, ensuring that every human approval is cryptographically bound to the exact code diff reviewed, eliminating man-in-the-middle tampering risks in automated pipelines.
Conclusion
Building autonomous agents that touch production code requires a disciplined approach to state management and safety boundaries. By combining LangGraph's interruption capabilities with webhook-driven authorization, you gain the productivity of AI automation without sacrificing human control.
Take the code we built today, wire it up to your team's staging repository, and set up your first human-in-the-approval gate. Secure agentic automation is no longer a futuristic concept—it is the engineering standard of modern software development.
- Stateful checkpointers allow LangGraph workflows to pause execution indefinitely without losing context.
- Never grant LLMs unrestricted write access; always enforce an explicit human approval gate before deployment.
- Model Context Protocol (MCP) standardizes how agents interact with external APIs like GitHub safely.
- Always validate and test AI-generated patches in sandboxed environments before merging to production branches.