LangGraph: Build an Agent with Tool Calling and Bounded Execution for Code Verification
Learn how to build a stateful LangGraph agent featuring tool calling, step bounding, and automated test verification for repository code changes.
On this page
Autonomous code modification requires more than a standard linear prompt chain. When an agent modifies a repository, it must read files, apply edits, execute test suites, inspect outputs, and self-correct when regressions appear. However, cyclical agent patterns—often based on the ReAct (Reasoning and Acting) paradigm—pose operational hazards if unconstrained. An agent encountering broken dependencies or persistent test failures can enter expensive, unbounded execution loops.
Using Google AI's documented ReAct architecture for LangGraph, this guide adapts core state-graph mechanics into a specialized, bounded coding agent. The implementation executes code edits, verifies changes against automated test suites, and enforces strict iteration budgets.
Core Architecture: State, Nodes, and Conditional Edges
LangGraph models agent workflows as directed graphs. Unlike static directed acyclic graphs (DAGs), LangGraph natively accommodates cycles, which are essential for iterative agent reasoning.
[Entrypoint]
│
▼
┌─────────────┐
│ call_model │◄────────────┐
└──────┬──────┘ │
│ │
[should_continue] │
/ \ │
(has tools) (done / max) │
/ \ │
▼ ▼ │
┌───────────┐ ┌─────┐ │
│ call_tool │ │ END │ │
└─────┬─────┘ └─────┘ │
│ │
└─────────────────────────────┘
The graph relies on three architectural elements:
- State: A centralized typed data structure that stores the message history, run metadata, and explicit step counters.
- Nodes: Plain Python functions that receive the current state, run isolated computations (calling the model or executing a tool), and return updated state attributes.
- Conditional Edges: Decision functions that inspect the state (such as the presence of tool calls or exceeding an iteration ceiling) to decide whether to route to the tool runner or terminate at
END.
Prerequisites and Dependencies
Ensure your development environment contains the necessary graph orchestration and model driver packages:
pip install langgraph langchain-core langchain-openai
To connect the agent to inference providers through an OpenAI-compatible gateway, configure your environment variables. Using OneMux simplifies unified key management and spend controls across multiple models:
export ONEMUX_API_KEY="your_onemux_api_key"
export ONEMUX_BASE_URL="https://api.onemux.net/v1"
(Note: Always verify that your selected target model supports structured tool-calling parameters via OpenAI-compatible endpoints).
Step 1: Defining Bounded Agent State
In standard conversational workflows, MessagesState handles appending chat history. For coding tasks, we explicitly define AgentState using Python's TypedDict to track message sequences alongside an explicit counter (step_count) and a maximum threshold (max_steps).
from typing import Annotated, Sequence, TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
step_count: int
max_steps: int
The add_messages reducer ensures incoming message lists are appended cleanly to conversation history rather than overwriting previous observations.
Step 2: Implementing File System and Verification Tools
The agent requires tools to inspect source files, apply updates, and run repository tests. We encapsulate these operations using LangChain's @tool decorator.
import subprocess
from langchain_core.tools import tool
@tool
def read_file(filepath: str) -> str:
"""Reads contents of a file from the repository."""
try:
with open(filepath, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
return f"Error reading {filepath}: {str(e)}"
@tool
def write_file(filepath: str, content: str) -> str:
"""Overwrites a file with modified source code."""
try:
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
return f"Successfully updated {filepath}"
except Exception as e:
return f"Error writing {filepath}: {str(e)}"
@tool
def run_test_suite(test_target: str) -> str:
"""Runs pytest on a specific file or test target."""
try:
result = subprocess.run(
["pytest", test_target],
capture_output=True,
text=True,
timeout=30
)
return f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}\nExit Code: {result.returncode}"
except subprocess.TimeoutExpired:
return "Error: Test execution timed out after 30 seconds."
except Exception as e:
return f"Error executing tests: {str(e)}"
tools = [read_file, write_file, run_test_suite]
tools_by_name = {t.name: t for t in tools}
Step 3: Model Setup via Unified API
We initialize the model using ChatOpenAI, pointing to the unified provider proxy endpoint. Check the OneMux Docs and Quickstart Guide to ensure parameter compatibility for function calling.
import os
from langchain_openai import ChatOpenAI
api_key = os.getenv("ONEMUX_API_KEY")
base_url = os.getenv("ONEMUX_BASE_URL", "https://api.onemux.net/v1")
llm = ChatOpenAI(
model="gpt-4o",
temperature=0.0,
api_key=api_key,
base_url=base_url
)
# Bind tools to the model instance
model = llm.bind_tools(tools)
Step 4: Constructing Graph Nodes and Loop Guards
Nodes execute logic and increment bounds. If an agent loops repeatedly without fixing a broken test, the edge logic detects that step_count >= max_steps and routes directly to graph completion.
from langchain_core.messages import ToolMessage, AIMessage
from langchain_core.runnables import RunnableConfig
def call_model(state: AgentState, config: RunnableConfig):
"""Calls the model and increments the iteration step count."""
response = model.invoke(state["messages"], config)
current_steps = state.get("step_count", 0) + 1
return {
"messages": [response],
"step_count": current_steps
}
def call_tool(state: AgentState):
"""Executes tool calls generated by the most recent model response."""
last_message = state["messages"][-1]
outputs = []
for tool_call in getattr(last_message, "tool_calls", []):
tool_inst = tools_by_name.get(tool_call["name"])
if tool_inst:
result = tool_inst.invoke(tool_call["args"])
else:
result = f"Error: Tool {tool_call['name']} not found."
outputs.append(
ToolMessage(
content=str(result),
name=tool_call["name"],
tool_call_id=tool_call["id"]
)
)
return {"messages": outputs}
def should_continue(state: AgentState):
"""Evaluates tool requests and halts execution if boundaries are crossed."""
messages = state["messages"]
last_message = messages[-1]
step_count = state.get("step_count", 0)
max_steps = state.get("max_steps", 5)
# Boundary check: Halt if loop budget exceeded
if step_count >= max_steps:
return "end"
# Continue if the model requested another tool call
if getattr(last_message, "tool_calls", None):
return "continue"
return "end"
Step 5: Graph Compilation and Assembly
Assemble the components using StateGraph and compile the workflow:
from langgraph.graph import StateGraph, END
workflow = StateGraph(AgentState)
# Register Nodes
workflow.add_node("agent", call_model)
workflow.add_node("tools", call_tool)
# Set Entrypoint
workflow.set_entry_point("agent")
# Define Conditional and Static Edges
workflow.add_conditional_edges(
"agent",
should_continue,
{
"continue": "tools",
"end": END
}
)
workflow.add_edge("tools", "agent")
# Compile Graph
app = workflow.compile()
Step 6: Executing Code Changes with Test Verification
To run the workflow, define a coding task alongside strict execution bounds:
initial_input = {
"messages": [(
"user",
"Fix the bug in utils/math_lib.py where divide_numbers fails on zero values. "
"Verify your fix by running the test suite on tests/test_math_lib.py. "
"Only stop once all tests pass."
)],
"step_count": 0,
"max_steps": 6
}
for event in app.stream(initial_input, stream_mode="values"):
latest = event["messages"][-1]
if isinstance(latest, AIMessage) and latest.tool_calls:
print(f"[Action] Invoking tools: {[t['name'] for t in latest.tool_calls]}")
elif isinstance(latest, ToolMessage):
print(f"[Observation] {latest.name} -> {latest.content[:100]}...")
else:
print(f"[Agent] {latest.content}")
[Action] Invoking tools: ['read_file']
[Observation] read_file -> def divide_numbers(a, b):
return a / b...
[Action] Invoking tools: ['write_file']
[Observation] write_file -> Successfully updated utils/math_lib.py...
[Action] Invoking tools: ['run_test_suite']
[Observation] run_test_suite -> STDOUT:
============================= test session starts ==============================
... 1 passed in 0.02s...
[Agent] The divide_numbers function has been updated to handle zero division safely, and all tests in tests/test_math_lib.py pass.
Limitations and Production Hardening
When deploying code-modifying agents in production environments, consider the following safeguards:
- Sandboxing Execution: Running arbitrary shell commands like
pytestlocally risks environment corruption or unsafe script execution. Isolate test execution within ephemeral Docker containers or secure sandboxes. - State Token Limits: Extended tool outputs—such as multi-page stack traces—can quickly exhaust context windows. Truncate test output messages before writing them into the state history.
- Managing Model Costs: Iterative ReAct loops multiply token consumption. Track usage via the OneMux Pricing dashboard, and evaluate lower-cost reasoning models on OneMux Models to optimize cost-per-task.
Sources
- Google AI for Developers: ReAct agent from scratch with Gemini and LangGraph (Updated Sept 2, 2026).
FAQ
What is the primary benefit of LangGraph over standard linear agent chains?
LangGraph natively supports cyclic graphs, enabling agents to iteratively run tools, inspect output observations, and self-correct their actions until a task passes verification.
Why is bounded execution essential for code-modifying agents?
Without an enforced step counter or ceiling, an agent that fails to fix a broken test could remain stuck in an infinite tool-calling loop, exhausting token budgets and resources.
Can I use LangGraph with models accessed via OneMux?
Yes. By using LangChain's ChatOpenAI client pointed at OneMux's base URL and providing your OneMux API key, you can orchestrate LangGraph agents using any supported model with tool-calling capabilities.
Related articles
Guides
Build a LangGraph Customer Support Agent with Tool Calling, Human Approval, and Execution Limits
A practical LangGraph pattern for customer-support triage: route requests, call lookup tools, pause refunds for human approval, and bound graph execution.
Guides
Build a Bounded LangGraph Agent for Schema-Validated Extraction
Build a LangGraph workflow that calls a tool when needed, stops within a defined execution budget, and returns structured data validated against a Pydantic schema.
Guides
LangGraph Agentic RAG: Tool Calling, Source Attribution, and Bounded Execution
Build a LangGraph document question-answering agent that decides when to retrieve, evaluates context, retries weak searches, and returns source-attributed answers without unbounded execution.
Guides
Gemini 3.6 Flash Benchmarks: What Agentic Claims Mean for Chatbot Backend Architecture
A practical framework for evaluating Gemini 3.6 Flash for AI chatbot backends, with routing, tool-loop, cost-control, and rollout guidance.