Build an Autonomous Coding Agent with GitHub CI/CD and LangGraph
Source: mortalapps.comThis guide provides a comprehensive, end-to-end tutorial on building an advanced autonomous coding agent. This agent accepts a natural language task description, intelligently plans its implementation, writes Python code in a secure sandboxed environment (E2B), automatically generates and runs a test suite, and self-corrects based on test failures. Once the code is validated, the agent proceeds to open a GitHub pull request, demonstrating a complete autonomous coding agent GitHub CI/CD workflow. This system is designed to operate with minimal human intervention, automating significant portions of the development lifecycle.
This project is ideal for AI engineers, software developers, and DevOps practitioners interested in leveraging agentic AI to streamline software development processes. You will gain practical experience in designing complex, stateful agent workflows, integrating external tools for code execution and version control, and implementing robust self-correction mechanisms. The guide will also cover essential production engineering aspects like observability, error handling, and secure deployment.
The business problem it solves is the reduction of manual effort in repetitive coding tasks, acceleration of development cycles, and improvement of code quality through automated testing and iterative refinement. By automating the code generation, testing, and pull request creation process, teams can free up valuable engineering time to focus on more complex architectural challenges.
By the end of this project, you will have a fully functional, deployable autonomous coding agent capable of transforming natural language instructions into tested, version-controlled code. You will understand how to orchestrate sophisticated AI workflows with LangGraph, integrate secure sandbox environments, and manage interactions with external services like GitHub. The architecture emphasizes modularity, resilience, and observability, preparing you for real-world agentic applications.
What You Will Build
You will build a Python-based autonomous coding agent orchestrated by LangGraph. This agent will take a natural language task description as input. Internally, it will leverage a Large Language Model (LLM) to perform several key actions: decompose the task into a detailed plan, generate the required Python code, and then generate a comprehensive suite of unit tests for that code. These generated code and test files will be executed within a secure E2B sandbox environment, providing isolated and reproducible execution.
The agent's core loop involves running tests, analyzing the results, and, if failures occur, reflecting on the issues, refining its plan, and generating corrected code and tests. This self-correction mechanism allows it to iteratively improve its output. Once the agent determines the code is correct and tests pass, it will interact with the GitHub API to create a new branch in a specified repository, commit the generated code and tests, and open a pull request. The system will be equipped with robust logging and observability features, ensuring that every step of this complex, multi-turn process is traceable and debuggable. The final architecture is a stateful graph where nodes represent agentic steps (planning, coding, testing, reflecting, GitHub interaction) and edges define the flow, including conditional routing for self-correction.
Technology Stack
| Component | Technology | Why This Choice |
|---|---|---|
| Large Language Model (LLM) | OpenAI GPT-4o |
Selected for its advanced reasoning capabilities, superior code generation quality, and ability to follow complex instructions, which are critical for robust planning, coding, and self-correction. |
| Orchestration Framework | LangGraph |
Provides a powerful graph-based approach for defining stateful, multi-step agent workflows. Its explicit state management and conditional routing are ideal for implementing iterative coding, testing, and self-correction loops. |
| Memory & State Persistence | LangGraph Checkpointers (SQLite) |
Enables the agent to maintain its state across multiple turns and tool executions, ensuring continuity and recovery from interruptions in long-running development tasks. SQLite is used for simplicity in this guide. |
| Tool Execution Environment | E2B Sandbox |
Offers a secure, isolated, and pre-configured cloud environment for executing generated code and running tests. This prevents malicious code execution on the host and simplifies tool integration. |
| Observability & Debugging | LangSmith |
Provides comprehensive tracing, debugging, and evaluation capabilities for agent runs. It's invaluable for visualizing agent thought processes, tool interactions, and state transitions, aiding development and optimization. |
| Authentication & External Integration | GitHub Personal Access Token, E2B API Key |
Standard and secure methods for authenticating and interacting with the GitHub API for version control operations and the E2B sandbox for code execution. |
| Deployment & Containerization | Docker, Kubernetes |
Docker ensures a consistent and isolated runtime environment, while Kubernetes provides scalable orchestration, self-healing, and robust deployment capabilities for production environments. |
OpenAI GPT-4o is chosen as the primary Large Language Model for this project due to its industry-leading performance in complex reasoning, code generation, and adherence to intricate instructions. Its ability to understand nuanced task descriptions and produce high-quality, executable Python code is crucial for the agent's effectiveness. While other models like Claude 3 Opus or open-source alternatives like Llama 3 offer competitive features, GPT-4o's current capabilities provide the best balance of performance and reliability for an autonomous coding agent that needs to self-correct.
LangGraph serves as the core orchestration framework, a deliberate choice for its explicit state management and graph-based workflow definition. Unlike simpler sequential orchestrators, LangGraph allows us to define complex conditional logic, loops for self-correction, and clear state transitions, which are fundamental to the iterative development process of an autonomous coding agent. This structured approach makes the agent's behavior predictable and debuggable, which is paramount for production systems.
LangGraph Checkpointers, specifically using SQLite for this guide, are essential for maintaining the agent's state across multiple execution steps and potential restarts. This ensures that the agent can resume its work without losing context, which is vital for long-running tasks like software development where an agent might pause, reflect, or be interrupted. For production, a more robust checkpointer like PostgreSQL would be recommended.
E2B Sandbox provides a secure and isolated environment for executing generated code and tests. This is a non-negotiable security measure for an autonomous agent that generates and executes arbitrary code. E2B simplifies the setup of a dynamic execution environment, abstracting away the complexities of container management and ensuring that potential errors or malicious code do not impact the host system. Alternatives might involve custom Docker containers, but E2B offers a managed solution with robust APIs.
LangSmith is integrated for comprehensive observability, tracing every step of the agent's execution. For an advanced agent with multiple LLM calls and tool interactions, understanding the flow, inputs, outputs, and reasoning behind each decision is critical for development, debugging, and performance optimization. LangSmith's visual traces significantly reduce the cognitive load of debugging complex agentic systems. GitHub Personal Access Token and E2B API Key are used for secure authentication with external services, adhering to best practices by using environment variables. Docker and Kubernetes are chosen for their industry-standard capabilities in containerization and scalable deployment, ensuring the agent can be reliably run in production.
Project Structure
autonomous-coding-agent/
├── .env
├── main.py
├── agent.py
├── tools.py
├── github_utils.py
├── tests/
│ └── test_agent.py
├── Dockerfile
├── requirements.txt
└── k8s/
└── deployment.yaml
The .env file is used to store sensitive environment variables such as API keys and configuration settings, keeping them separate from the codebase and ensuring secure deployment. The main.py file serves as the primary entry point for the application, responsible for initializing the agent and handling the overall execution flow based on user input. The agent.py file encapsulates the core LangGraph state machine, defining the various agent nodes, their logic, and the transitions between them, orchestrating the autonomous coding process. Custom tools that the agent can invoke, such as the E2B code interpreter and file system operations, are defined within tools.py.
Interactions with the GitHub API, including creating branches, committing changes, and opening pull requests, are abstracted into github_utils.py, promoting modularity and reusability. The tests/ directory contains test_agent.py, which includes unit and integration tests for the agent's components and an example of an LLM-as-a-Judge evaluation. The Dockerfile provides instructions for building a container image of the agent, ensuring a consistent and portable execution environment across different stages. All Python dependencies are listed in requirements.txt. Finally, the k8s/ directory holds Kubernetes manifest files like deployment.yaml for deploying the agent in a production cluster, enabling scalability and reliability.
Implementation
Phase 1: Environment Setup and Project Scaffold
This phase establishes the foundational project structure, installs necessary dependencies, and configures environment variables. It sets up basic logging and initializes an empty LangGraph workflow to prepare for the core agent logic.
# requirements.txt
# langchain-openai==0.1.7
# langgraph==0.2.0
# python-dotenv==1.0.1
# e2b==0.1.10
# PyGithub==2.2.0
# tenacity==8.2.3
# .env (template)
# OPENAI_API_KEY="your_openai_api_key_here"
# E2B_API_KEY="your_e2b_api_key_here"
# GITHUB_TOKEN="your_github_personal_access_token_here"
# GITHUB_REPO_OWNER="your_github_username_or_org"
# GITHUB_REPO_NAME="your_repo_name"
# Optional: LangSmith for observability
# LANGCHAIN_TRACING_V2="true"
# LANGCHAIN_API_KEY="your_langsmith_api_key_here"
# LANGCHAIN_PROJECT="Autonomous Coding Agent"
# main.py
import os
import logging
from dotenv import load_dotenv
from typing import TypedDict, List, Annotated
from langgraph.graph import StateGraph, END
from agent import create_agent_graph, AgentState
load_dotenv()
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def run_agent(task: str) -> str:
"""
Initializes and runs the autonomous coding agent with a given task.
"""
openai_api_key = os.environ.get("OPENAI_API_KEY")
e2b_api_key = os.environ.get("E2B_API_KEY")
github_token = os.environ.get("GITHUB_TOKEN")
github_repo_owner = os.environ.get("GITHUB_REPO_OWNER")
github_repo_name = os.environ.get("GITHUB_REPO_NAME")
if not all([openai_api_key, e2b_api_key, github_token, github_repo_owner, github_repo_name]):
logger.error("Missing one or more required environment variables.")
raise ValueError("Ensure OPENAI_API_KEY, E2B_API_KEY, GITHUB_TOKEN, GITHUB_REPO_OWNER, GITHUB_REPO_NAME are set.")
workflow = create_agent_graph(openai_api_key)
inputs: AgentState = {
"task": task,
"chat_history": [],
"plan": "",
"code": "",
"test_code": "",
"test_results": "",
"pull_request_url": "",
"iterations": 0,
"max_iterations": 5, # Default, can be overridden by plan_task
"output": "",
"pr_number": 0, # Placeholder
"github_repo_owner": github_repo_owner,
"github_repo_name": github_repo_name
}
logger.info(f"Starting agent for task: {task}")
final_output = None
try:
for s in workflow.stream(inputs, {"configurable": {"thread_id": "1"}}):
logger.info(f"Agent step: {s}")
final_output = s
if final_output:
# final_output is {"node_name": state_dict} from the last streamed step
last_state = next(iter(final_output.values()), {}) if isinstance(final_output, dict) else {}
return last_state.get("output", str(final_output))
else:
return "Agent finished without explicit output."
except Exception as e:
logger.exception(f"An error occurred during agent execution: {e}")
return f"Agent execution failed: {e}"
if __name__ == "__main__":
test_task = "Create a Python function `multiply(a, b)` that returns the product of two numbers. Include a docstring and a simple unit test for it. The function should be in a file named `src/generated_code.py` and tests in `tests/test_generated_code.py`."
print("--- Running autonomous coding agent ---")
result = run_agent(test_task)
print("
--- Agent Finished ---")
print(f"Final Result: {result}")
# agent.py
from typing import TypedDict, List, Annotated
import operator
import logging
from langgraph.graph import StateGraph, END
from langchain_core.messages import BaseMessage
logger = logging.getLogger(__name__)
class AgentState(TypedDict):
"""
Represents the state of our agent.
Attributes:
task: The original task description.
plan: The current high-level plan for execution.
code: The generated code.
test_code: The generated test code.
test_results: Results from running tests.
pull_request_url: URL of the created pull request.
chat_history: A list of messages representing the conversation history.
iterations: Number of self-correction iterations.
max_iterations: Maximum allowed self-correction iterations.
output: The final output message from the agent.
pr_number: The number of the created pull request (for monitoring).
github_repo_owner: Owner of the GitHub repository.
github_repo_name: Name of the GitHub repository.
"""
task: str
plan: str
code: str
test_code: str
test_results: str
pull_request_url: str
chat_history: Annotated[List[BaseMessage], operator.add]
iterations: int
max_iterations: int
output: str
pr_number: int
github_repo_owner: str
github_repo_name: str
def create_agent_graph(openai_api_key: str):
"""
Creates and compiles the LangGraph workflow for the autonomous coding agent.
"""
workflow = StateGraph(AgentState)
# Define nodes here (will be added in subsequent phases)
# Define edges here (will be added in subsequent phases)
logger.info("LangGraph workflow initialized.")
return workflow.compile()
# tools.py
import os
import logging
from e2b import Sandbox
from tenacity import retry, stop_after_attempt, wait_exponential
from typing import Dict, Any
logger = logging.getLogger(__name__)
# Sandbox instance will be initialized once and reused
_sandbox_instance: Sandbox | None = None
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def get_e2b_sandbox() -> Sandbox:
"""
Retrieves or initializes the E2B sandbox instance.
Retries on connection errors.
"""
global _sandbox_instance
if _sandbox_instance is None:
e2b_api_key = os.environ.get("E2B_API_KEY")
if not e2b_api_key:
logger.error("E2B_API_KEY environment variable not set.")
raise ValueError("E2B_API_KEY environment variable not set.")
try:
_sandbox_instance = Sandbox(api_key=e2b_api_key)
logger.info("E2B Sandbox initialized successfully.")
except Exception as e:
logger.error(f"Failed to initialize E2B Sandbox: {e}")
raise
return _sandbox_instance
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def execute_python_code(code: str) -> Dict[str, Any]:
"""
Executes Python code in the E2B sandbox and returns the stdout, stderr, and error status.
Retries on sandbox connection or execution errors.
"""
sandbox = get_e2b_sandbox()
try:
_escaped = code.replace("'", "\'")
proc = sandbox.process.start(
cmd=f"python -c '{_escaped}'", # Escape single quotes for shell
timeout=10
)
proc.wait()
stdout = proc.stdout
stderr = proc.stderr
exit_code = proc.exit_code
logger.info(f"Code execution finished with exit code {exit_code}")
logger.debug(f"Stdout: {stdout}")
logger.debug(f"Stderr: {stderr}")
if exit_code != 0:
logger.warning(f"Code execution failed. Stderr: {stderr}")
return {"status": "error", "stdout": stdout, "stderr": stderr, "exit_code": exit_code}
else:
return {"status": "success", "stdout": stdout, "stderr": stderr, "exit_code": exit_code}
except Exception as e:
logger.exception(f"Error executing code in sandbox: {e}")
return {"status": "runtime_error", "stdout": "", "stderr": str(e), "exit_code": -1}
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def write_file_to_sandbox(file_path: str, content: str) -> Dict[str, Any]:
"""
Writes content to a specified file path in the E2B sandbox.
"""
sandbox = get_e2b_sandbox()
try:
sandbox.filesystem.write(file_path, content)
logger.info(f"File '{file_path}' written to sandbox.")
return {"status": "success", "message": f"File '{file_path}' written."}
except Exception as e:
logger.error(f"Failed to write file '{file_path}' to sandbox: {e}")
return {"status": "error", "message": str(e)}
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def read_file_from_sandbox(file_path: str) -> Dict[str, Any]:
"""
Reads content from a specified file path in the E2B sandbox.
"""
sandbox = get_e2b_sandbox()
try:
content = sandbox.filesystem.read(file_path)
logger.info(f"File '{file_path}' read from sandbox.")
return {"status": "success", "content": content}
except Exception as e:
logger.error(f"Failed to read file '{file_path}' from sandbox: {e}")
return {"status": "error", "message": str(e)}
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def list_files_in_sandbox(path: str = ".") -> Dict[str, Any]:
"""
Lists files in a given directory in the E2B sandbox.
"""
sandbox = get_e2b_sandbox()
try:
files = sandbox.filesystem.ls(path)
file_names = [f.name for f in files]
logger.info(f"Listed files in '{path}': {file_names}")
return {"status": "success", "files": file_names}
except Exception as e:
logger.error(f"Failed to list files in '{path}' in sandbox: {e}")
return {"status": "error", "message": str(e)}
requirements.txt lists all Python packages required, including langgraph for orchestration, langchain-openai for LLM interaction, python-dotenv for environment variable management, e2b for sandbox access, PyGithub for GitHub API interaction, and tenacity for retry logic. The .env template guides users to configure their API keys and repository details securely, preventing hardcoding of sensitive information. main.py serves as the application's entry point, initializing logging and loading environment variables. It defines the run_agent function, which will orchestrate the agent's execution.
agent.py starts with the AgentState TypedDict, which defines the schema for the agent's internal state, crucial for LangGraph's state management. This state will evolve throughout the agent's lifecycle, storing the task, plan, code, test results, and chat history. The create_agent_graph function is a placeholder for now but will be where the LangGraph workflow is defined. tools.py contains helper functions for interacting with the E2B sandbox, such as get_e2b_sandbox to initialize the sandbox connection, execute_python_code to run arbitrary Python code, and write_file_to_sandbox to manage files within the sandbox. Each tool function incorporates tenacity decorators for automatic retries with exponential backoff, making them resilient to transient errors, and robust logging for visibility into operations.
Ensure you've created a .env file from the template and filled in the required API keys and GitHub details. Run pip install -r requirements.txt followed by python main.py. You should see INFO level logs indicating initialization. If any required environment variables are missing, a ValueError should be raised, as designed.
Phase 2: Core Agent Loop (Plan, Code, Test, Reflect)
This phase implements the fundamental iterative reasoning loop of the autonomous coding agent. It includes nodes for planning the task, generating code, generating tests, executing those tests in the E2B sandbox, and reflecting on the test results to self-correct.
# agent.py (updated)
from typing import TypedDict, List, Annotated, Callable
import operator
import logging
import json
import os
from langgraph.graph import StateGraph, END
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
from tenacity import retry, stop_after_attempt, wait_exponential
from tools import execute_python_code, write_file_to_sandbox, read_file_from_sandbox, list_files_in_sandbox
logger = logging.getLogger(__name__)
class AgentState(TypedDict):
"""
Represents the state of our agent.
Attributes:
task: The original task description.
plan: The current high-level plan for execution.
code: The generated code.
test_code: The generated test code.
test_results: Results from running tests.
pull_request_url: URL of the created pull request.
chat_history: A list of messages representing the conversation history.
iterations: Number of self-correction iterations.
max_iterations: Maximum allowed self-correction iterations.
output: The final output message from the agent.
pr_number: The number of the created pull request (for monitoring).
github_repo_owner: Owner of the GitHub repository.
github_repo_name: Name of the GitHub repository.
"""
task: str
plan: str
code: str
test_code: str
test_results: str
pull_request_url: str
chat_history: Annotated[List[BaseMessage], operator.add]
iterations: int
max_iterations: int
output: str
pr_number: int
github_repo_owner: str
github_repo_name: str
class Plan(BaseModel):
"""Plan to write and test code."""
steps: List[str] = Field(description="List of steps to complete the task.")
class CodeResponse(BaseModel):
"""Response containing generated code."""
code: str = Field(description="The generated Python code.")
class TestResponse(BaseModel):
"""Response containing generated test code."""
test_code: str = Field(description="The generated Python test code.")
class Reflection(BaseModel):
"""Reflection on test results and plan for next steps."""
thought: str = Field(description="Detailed thought process on why tests failed or what needs improvement.")
new_plan_steps: List[str] = Field(description="Revised plan steps based on reflection.")
is_complete: bool = Field(description="True if the task is considered complete and ready for PR.")
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def _get_llm(openai_api_key: str):
"""Initializes and returns the OpenAI ChatModel."""
return ChatOpenAI(model="gpt-4o", temperature=0.7, api_key=openai_api_key)
def _get_prompt(system_message: str):
"""Creates a chat prompt template."""
return ChatPromptTemplate.from_messages([
("system", system_message),
("placeholder", "{chat_history}"),
("human", "{task}"),
("human", "{current_state}"), # Optional: pass current state for more context
])
def plan_task(state: AgentState, config: dict) -> AgentState:
"""Agent node to generate a plan for the coding task."""
logger.info("--- Planning Task ---")
llm = _get_llm(config["configurable"]["openai_api_key"])
plan_prompt = _get_prompt(
"You are an expert software engineer. Given a task, create a detailed plan with specific steps to implement the required code and unit tests. "
"Break down complex problems into smaller, manageable steps. Focus on implementation details, testing strategy, and potential edge cases. "
"Output your plan as a JSON object with a 'steps' key containing a list of strings."
).partial(current_state="")
plan_chain = plan_prompt | llm.with_structured_output(Plan)
try:
plan_obj = plan_chain.invoke({
"chat_history": state["chat_history"],
"task": state["task"]
})
plan_str = "
".join(f"- {step}" for step in plan_obj.steps)
logger.info(f"Generated Plan:
{plan_str}")
github_repo_owner = os.environ.get("GITHUB_REPO_OWNER")
github_repo_name = os.environ.get("GITHUB_REPO_NAME")
if not github_repo_owner or not github_repo_name:
logger.error("GITHUB_REPO_OWNER or GITHUB_REPO_NAME not set.")
raise ValueError("GitHub repository details not configured.")
return {
"plan": plan_str,
"chat_history": [HumanMessage(content=state["task"]), AIMessage(content=f"Plan: {plan_str}")],
"iterations": 0,
"max_iterations": 5,
"github_repo_owner": github_repo_owner,
"github_repo_name": github_repo_name
}
except Exception as e:
logger.exception(f"Error in plan_task: {e}")
return {"output": f"Failed to plan task: {e}"}
def generate_code(state: AgentState, config: dict) -> AgentState:
"""Agent node to generate code based on the plan."""
logger.info("--- Generating Code ---")
llm = _get_llm(config["configurable"]["openai_api_key"])
code_prompt = _get_prompt(
"You are an expert Python programmer. Given the task and current plan, write the complete Python code. "
"Focus on writing clean, correct, and robust code. Do not include test code. "
"Output ONLY the Python code, ensuring it's syntactically correct and ready to be executed. "
"Wrap your code in a JSON object with a 'code' key."
)
code_chain = code_prompt | llm.with_structured_output(CodeResponse)
try:
code_response = code_chain.invoke({
"chat_history": state["chat_history"],
"task": state["task"],
"current_state": f"Current Plan:
{state['plan']}"
})
logger.info(f"Generated Code:
{code_response.code}")
return {
"code": code_response.code,
"chat_history": state["chat_history"] + [AIMessage(content=f"Generated Code:
```python
{code_response.code}
```")]
}
except Exception as e:
logger.exception(f"Error in generate_code: {e}")
return {"output": f"Failed to generate code: {e}"}
def generate_tests(state: AgentState, config: dict) -> AgentState:
"""Agent node to generate unit tests for the generated code."""
logger.info("--- Generating Tests ---")
llm = _get_llm(config["configurable"]["openai_api_key"])
test_prompt = _get_prompt(
"You are an expert Python developer focused on test-driven development. "
"Given the task and the generated code, write comprehensive unit tests. "
"Ensure tests cover functionality, edge cases, and error conditions. "
"Use `unittest` or `pytest` if applicable, but for simple functions, direct assertions are fine. "
"Output ONLY the Python test code. It should be executable and import the main code if necessary. "
"Wrap your test code in a JSON object with a 'test_code' key."
)
test_chain = test_prompt | llm.with_structured_output(TestResponse)
try:
test_response = test_chain.invoke({
"chat_history": state["chat_history"],
"task": state["task"],
"current_state": f"Current Plan:
{state['plan']}
Generated Code:
{state['code']}"
})
logger.info(f"Generated Test Code:
{test_response.test_code}")
return {
"test_code": test_response.test_code,
"chat_history": state["chat_history"] + [AIMessage(content=f"Generated Tests:
```python
{test_response.test_code}
```")]
}
except Exception as e:
logger.exception(f"Error in generate_tests: {e}")
return {"output": f"Failed to generate tests: {e}"}
def execute_tests(state: AgentState) -> AgentState:
"""Agent node to execute generated code and tests in the E2B sandbox."""
logger.info("--- Executing Code and Tests ---")
write_code_result = write_file_to_sandbox("main.py", state["code"])
if write_code_result["status"] == "error":
return {"output": f"Failed to write main.py: {write_code_result['message']}"}
write_test_result = write_file_to_sandbox("test_main.py", state["test_code"])
if write_test_result["status"] == "error":
return {"output": f"Failed to write test_main.py: {write_test_result['message']}"}
test_execution_result = execute_python_code("import unittest; from test_main import *; unittest.main(exit=False)")
if test_execution_result["status"] == "runtime_error":
logger.error(f"Sandbox runtime error during test execution: {test_execution_result['stderr']}")
return {
"test_results": f"Sandbox runtime error: {test_execution_result['stderr']}",
"chat_history": state["chat_history"] + [AIMessage(content=f"Test Execution Failed (Sandbox Error):
{test_execution_result['stderr']}")]
}
stdout = test_execution_result["stdout"]
stderr = test_execution_result["stderr"]
exit_code = test_execution_result["exit_code"]
full_output = f"Stdout:
{stdout}
Stderr:
{stderr}
Exit Code: {exit_code}"
logger.info(f"Test Execution Result:
{full_output}")
return {
"test_results": full_output,
"chat_history": state["chat_history"] + [AIMessage(content=f"Test Execution Result:
```
{full_output}
```")]
}
def reflect_on_results(state: AgentState, config: dict) -> AgentState:
"""Agent node to reflect on test results and potentially update the plan for self-correction."""
logger.info("--- Reflecting on Results ---")
llm = _get_llm(config["configurable"]["openai_api_key"])
reflection_prompt = _get_prompt(
"You are an expert debugger and code reviewer. Analyze the provided task, generated code, test code, and test results. "
"Identify issues, propose fixes, and revise the plan if necessary. "
"If the tests pass and the code meets the requirements, indicate that the task is complete. "
"Output your reflection as a JSON object with 'thought', 'new_plan_steps' (list of strings), and 'is_complete' (boolean)."
)
reflection_chain = reflection_prompt | llm.with_structured_output(Reflection)
try:
reflection_response = reflection_chain.invoke({
"chat_history": state["chat_history"],
"task": state["task"],
"current_state": f"Current Plan:
{state['plan']}
Generated Code:
{state['code']}
Test Code:
{state['test_code']}
Test Results:
{state['test_results']}"
})
logger.info(f"Reflection:
Thought: {reflection_response.thought}
New Plan: {reflection_response.new_plan_steps}
Is Complete: {reflection_response.is_complete}")
new_plan_str = "
".join(f"- {step}" for step in reflection_response.new_plan_steps)
return {
"plan": new_plan_str if reflection_response.new_plan_steps else state["plan"],
"iterations": state["iterations"] + 1,
"chat_history": state["chat_history"] + [AIMessage(content=f"Reflection: {reflection_response.thought}
New Plan: {new_plan_str}
Complete: {reflection_response.is_complete}")],
"output": "Code generation complete, ready for PR." if reflection_response.is_complete else None
}
except Exception as e:
logger.exception(f"Error in reflect_on_results: {e}")
return {"output": f"Failed to reflect: {e}"}
def should_continue_coding(state: AgentState) -> str:
"""Conditional edge to decide whether to continue coding or proceed to GitHub."""
if state["chat_history"] and isinstance(state["chat_history"][-1], AIMessage):
try:
reflection_content = state["chat_history"][-1].content
if "Complete: True" in reflection_content:
logger.info("Agent reflection indicates task is complete. Proceeding to GitHub.")
return "github_integration"
except Exception as e:
logger.warning(f"Could not parse last chat history message for completion status: {e}")
if state["iterations"] >= state["max_iterations"]:
logger.warning(f"Max iterations ({state['max_iterations']}) reached. Aborting coding loop.")
return "end_failure"
logger.info(f"Continuing coding loop. Iteration {state['iterations']}/{state['max_iterations']}")
return "continue_coding"
def create_agent_graph(openai_api_key: str):
"""
Creates and compiles the LangGraph workflow for the autonomous coding agent.
"""
workflow = StateGraph(AgentState)
workflow.add_node("plan_task", plan_task)
workflow.add_node("generate_code", generate_code)
workflow.add_node("generate_tests", generate_tests)
workflow.add_node("execute_tests", execute_tests)
workflow.add_node("reflect_on_results", reflect_on_results)
workflow.set_entry_point("plan_task")
workflow.add_edge("plan_task", "generate_code")
workflow.add_edge("generate_code", "generate_tests")
workflow.add_edge("generate_tests", "execute_tests")
workflow.add_edge("execute_tests", "reflect_on_results")
workflow.add_conditional_edges(
"reflect_on_results",
should_continue_coding,
{
"continue_coding": "generate_code",
"github_integration": END,
"end_failure": END
}
)
logger.info("LangGraph workflow initialized and compiled.")
return workflow.compile(config={"configurable": {"openai_api_key": openai_api_key}})
Pydantic models (Plan, CodeResponse, TestResponse, Reflection) to structure the LLM's outputs, ensuring type safety and predictability. The _get_llm helper initializes the ChatOpenAI model with a specified temperature, and _get_prompt creates a reusable chat prompt template.
Five key agent nodes are implemented:
1. plan_task: Takes the initial task and generates a detailed plan using the LLM. This node also initializes iterations and max_iterations for the self-correction loop and fetches GitHub repository details from environment variables.
2. generate_code: Based on the current plan and chat history, the LLM generates the Python code. It's prompted to output only the code, wrapped in a structured response.
3. generate_tests: Following code generation, the LLM generates unit tests for the code. This promotes a test-driven approach within the agent's loop.
4. execute_tests: This node is crucial. It uses the write_file_to_sandbox tool to put the generated code (main.py) and tests (test_main.py) into the E2B sandbox. Then, execute_python_code is invoked to run the tests. The results (stdout, stderr, exit code) are captured and stored in the agent's state.
5. reflect_on_results: The LLM analyzes the test output. If tests fail, it identifies issues and proposes a revised plan (new_plan_steps). If tests pass and the task is deemed complete, it sets is_complete to True. This node drives the self-correction mechanism.
The should_continue_coding function acts as a conditional edge. It checks the is_complete flag from the reflection or if max_iterations has been reached. If not complete and iterations remain, the agent loops back to generate_code to refine its implementation. Otherwise, it proceeds to github_integration (which will be implemented in the next phase) or end_failure if iterations are exhausted.
Run python main.py with the provided test_task. Observe the detailed INFO logs. You should see the agent progress through planning, code generation, test generation, execution, and reflection. If the code initially fails, observe the agent looping back to generate_code after reflection. You can also craft a simple task that is easy to get wrong initially (e.g., 'write a function that subtracts two numbers, but accidentally add them') to explicitly test the self-correction loop.
Phase 3: GitHub Integration and Pull Request Creation
This phase integrates the agent with GitHub, allowing it to create a new branch, commit the successfully developed code and tests, and open a pull request. It completes the automated development workflow by pushing the agent's work to a version control system.
# github_utils.py
import os
import logging
from github import Github, Auth
from github.GithubException import GithubException, UnknownObjectException
from tenacity import retry, stop_after_attempt, wait_exponential
from typing import Dict, Any, List
logger = logging.getLogger(__name__)
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def _get_github_client() -> Github:
"""Initializes and returns a GitHub client."""
github_token = os.environ.get("GITHUB_TOKEN")
if not github_token:
logger.error("GITHUB_TOKEN environment variable not set.")
raise ValueError("GITHUB_TOKEN environment variable not set.")
try:
auth = Auth.Token(github_token)
return Github(auth=auth)
except Exception as e:
logger.error(f"Failed to initialize GitHub client: {e}")
raise
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def create_branch_commit_pr(repo_owner: str, repo_name: str, task_description: str, code_content: str, test_content: str) -> str:
"""
Creates a new branch, commits the code and tests, and opens a pull request.
Returns the PR URL.
"""
g = _get_github_client()
try:
repo = g.get_user(repo_owner).get_repo(repo_name)
except UnknownObjectException:
try:
repo = g.get_organization(repo_owner).get_repo(repo_name)
except UnknownObjectException:
logger.error(f"Repository {repo_owner}/{repo_name} not found.")
raise ValueError(f"Repository {repo_owner}/{repo_name} not found.")
except GithubException as e:
logger.error(f"Error accessing GitHub repository {repo_owner}/{repo_name}: {e}")
raise
branch_name = f"feature/agent-task-{hash(task_description) % 10000}" # Simple unique branch name
commit_message = f"feat: Implement task: {task_description[:50]}..."
pr_title = f"feat: Autonomous Agent Implements: {task_description[:70]}..."
pr_body = (
f"This PR was automatically generated by an autonomous coding agent to address the task:
"
f"**Task Description:** {task_description}
"
f"The agent generated the code and tests, executed them successfully in a sandbox, and is now proposing these changes.
"
f"Please review the changes. If further modifications are needed, the agent can attempt to address comments."
)
try:
default_branch = repo.get_branch(repo.default_branch)
ref = repo.create_git_ref(ref=f"refs/heads/{branch_name}", sha=default_branch.commit.sha)
logger.info(f"Created new branch: {branch_name}")
latest_commit_sha = ref.object.sha
code_blob = repo.create_git_blob(code_content, "utf-8")
code_element = repo.create_input_git_tree_element("src/generated_code.py", "100644", "blob", code_blob.sha)
test_blob = repo.create_git_blob(test_content, "utf-8")
test_element = repo.create_input_git_tree_element("tests/test_generated_code.py", "100644", "blob", test_blob.sha)
base_tree = repo.get_git_tree(latest_commit_sha)
new_tree = repo.create_git_tree([code_element, test_element], base_tree)
logger.info("Created new git tree with code and test files.")
parent_commit = repo.get_git_commit(latest_commit_sha)
new_commit = repo.create_git_commit(commit_message, new_tree, [parent_commit])
logger.info(f"Created new commit: {new_commit.sha}")
ref.edit(sha=new_commit.sha, force=False)
logger.info(f"Branch '{branch_name}' updated to new commit.")
pr = repo.create_pull(
title=pr_title,
body=pr_body,
head=branch_name,
base=repo.default_branch
)
logger.info(f"Created Pull Request: {pr.html_url}")
return pr.html_url
except GithubException as e:
logger.error(f"GitHub API error during PR creation: {e.data}")
raise
except Exception as e:
logger.exception(f"An unexpected error occurred during PR creation: {e}")
raise
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def get_pr_comments(repo_owner: str, repo_name: str, pr_number: int) -> List[Dict[str, Any]]:
"""
Fetches comments for a given pull request.
"""
g = _get_github_client()
try:
repo = g.get_user(repo_owner).get_repo(repo_name)
pr = repo.get_pull(pr_number)
comments = [{"user": c.user.login, "body": c.body, "created_at": c.created_at.isoformat()} for c in pr.get_comments()]
logger.info(f"Fetched {len(comments)} comments for PR #{pr_number}.")
return comments
except GithubException as e:
logger.error(f"GitHub API error fetching PR comments: {e.data}")
raise
except Exception as e:
logger.exception(f"An unexpected error occurred fetching PR comments: {e}")
raise
# agent.py (updated again)
from typing import TypedDict, List, Annotated, Callable
import operator
import logging
import json
import os
from langgraph.graph import StateGraph, END
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
from tenacity import retry, stop_after_attempt, wait_exponential
from tools import execute_python_code, write_file_to_sandbox, read_file_from_sandbox, list_files_in_sandbox
from github_utils import create_branch_commit_pr, get_pr_comments
logger = logging.getLogger(__name__)
class AgentState(TypedDict):
"""
Represents the state of our agent.
Attributes:
task: The original task description.
plan: The current high-level plan for execution.
code: The generated code.
test_code: The generated test code.
test_results: Results from running tests.
pull_request_url: URL of the created pull request.
chat_history: A list of messages representing the conversation history.
iterations: Number of self-correction iterations.
max_iterations: Maximum allowed self-correction iterations.
output: The final output message from the agent.
pr_number: The number of the created pull request (for monitoring).
github_repo_owner: Owner of the GitHub repository.
github_repo_name: Name of the GitHub repository.
"""
task: str
plan: str
code: str
test_code: str
test_results: str
pull_request_url: str
chat_history: Annotated[List[BaseMessage], operator.add]
iterations: int
max_iterations: int
output: str
pr_number: int
github_repo_owner: str
github_repo_name: str
class Plan(BaseModel):
"""Plan to write and test code."""
steps: List[str] = Field(description="List of steps to complete the task.")
class CodeResponse(BaseModel):
"""Response containing generated code."""
code: str = Field(description="The generated Python code.")
class TestResponse(BaseModel):
"""Response containing generated test code."""
test_code: str = Field(description="The generated Python test code.")
class Reflection(BaseModel):
"""Reflection on test results and plan for next steps."""
thought: str = Field(description="Detailed thought process on why tests failed or what needs improvement.")
new_plan_steps: List[str] = Field(description="Revised plan steps based on reflection.")
is_complete: bool = Field(description="True if the task is considered complete and ready for PR.")
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def _get_llm(openai_api_key: str):
"""Initializes and returns the OpenAI ChatModel."""
return ChatOpenAI(model="gpt-4o", temperature=0.7, api_key=openai_api_key)
def _get_prompt(system_message: str):
"""Creates a chat prompt template."""
return ChatPromptTemplate.from_messages([
("system", system_message),
("placeholder", "{chat_history}"),
("human", "{task}"),
("human", "{current_state}"), # Optional: pass current state for more context
])
def plan_task(state: AgentState, config: dict) -> AgentState:
"""Agent node to generate a plan for the coding task."""
logger.info("--- Planning Task ---")
llm = _get_llm(config["configurable"]["openai_api_key"])
plan_prompt = _get_prompt(
"You are an expert software engineer. Given a task, create a detailed plan with specific steps to implement the required code and unit tests. "
"Break down complex problems into smaller, manageable steps. Focus on implementation details, testing strategy, and potential edge cases. "
"Output your plan as a JSON object with a 'steps' key containing a list of strings."
).partial(current_state="")
plan_chain = plan_prompt | llm.with_structured_output(Plan)
try:
plan_obj = plan_chain.invoke({
"chat_history": state["chat_history"],
"task": state["task"]
})
plan_str = "
".join(f"- {step}" for step in plan_obj.steps)
logger.info(f"Generated Plan:
{plan_str}")
github_repo_owner = os.environ.get("GITHUB_REPO_OWNER")
github_repo_name = os.environ.get("GITHUB_REPO_NAME")
if not github_repo_owner or not github_repo_name:
logger.error("GITHUB_REPO_OWNER or GITHUB_REPO_NAME not set.")
raise ValueError("GitHub repository details not configured.")
return {
"plan": plan_str,
"chat_history": [HumanMessage(content=state["task"]), AIMessage(content=f"Plan: {plan_str}")],
"iterations": 0,
"max_iterations": 5,
"github_repo_owner": github_repo_owner,
"github_repo_name": github_repo_name
}
except Exception as e:
logger.exception(f"Error in plan_task: {e}")
return {"output": f"Failed to plan task: {e}"}
def generate_code(state: AgentState, config: dict) -> AgentState:
"""Agent node to generate code based on the plan."""
logger.info("--- Generating Code ---")
llm = _get_llm(config["configurable"]["openai_api_key"])
code_prompt = _get_prompt(
"You are an expert Python programmer. Given the task and current plan, write the complete Python code. "
"Focus on writing clean, correct, and robust code. Do not include test code. "
"Output ONLY the Python code, ensuring it's syntactically correct and ready to be executed. "
"Wrap your code in a JSON object with a 'code' key."
)
code_chain = code_prompt | llm.with_structured_output(CodeResponse)
try:
code_response = code_chain.invoke({
"chat_history": state["chat_history"],
"task": state["task"],
"current_state": f"Current Plan:
{state['plan']}"
})
logger.info(f"Generated Code:
{code_response.code}")
return {
"code": code_response.code,
"chat_history": state["chat_history"] + [AIMessage(content=f"Generated Code:
```python
{code_response.code}
```")]
}
except Exception as e:
logger.exception(f"Error in generate_code: {e}")
return {"output": f"Failed to generate code: {e}"}
def generate_tests(state: AgentState, config: dict) -> AgentState:
"""Agent node to generate unit tests for the generated code."""
logger.info("--- Generating Tests ---")
llm = _get_llm(config["configurable"]["openai_api_key"])
test_prompt = _get_prompt(
"You are an expert Python developer focused on test-driven development. "
"Given the task and the generated code, write comprehensive unit tests. "
"Ensure tests cover functionality, edge cases, and error conditions. "
"Use `unittest` or `pytest` if applicable, but for simple functions, direct assertions are fine. "
"Output ONLY the Python test code. It should be executable and import the main code if necessary. "
"Wrap your test code in a JSON object with a 'test_code' key."
)
test_chain = test_prompt | llm.with_structured_output(TestResponse)
try:
test_response = test_chain.invoke({
"chat_history": state["chat_history"],
"task": state["task"],
"current_state": f"Current Plan:
{state['plan']}
Generated Code:
{state['code']}"
})
logger.info(f"Generated Test Code:
{test_response.test_code}")
return {
"test_code": test_response.test_code,
"chat_history": state["chat_history"] + [AIMessage(content=f"Generated Tests:
```python
{test_response.test_code}
```")]
}
except Exception as e:
logger.exception(f"Error in generate_tests: {e}")
return {"output": f"Failed to generate tests: {e}"}
def execute_tests(state: AgentState) -> AgentState:
"""Agent node to execute generated code and tests in the E2B sandbox."""
logger.info("--- Executing Code and Tests ---")
write_code_result = write_file_to_sandbox("src/generated_code.py", state["code"])
if write_code_result["status"] == "error":
return {"output": f"Failed to write src/generated_code.py: {write_code_result['message']}"}
write_test_result = write_file_to_sandbox("tests/test_generated_code.py", state["test_code"])
if write_test_result["status"] == "error":
return {"output": f"Failed to write tests/test_generated_code.py: {write_test_result['message']}"}
# Ensure the sandbox has access to the main code for testing
# The command assumes 'src' and 'tests' are in the root of the sandbox
test_execution_result = execute_python_code(
"import sys; sys.path.append('src'); import unittest; from tests.test_generated_code import *; unittest.main(exit=False)"
)
if test_execution_result["status"] == "runtime_error":
logger.error(f"Sandbox runtime error during test execution: {test_execution_result['stderr']}")
return {
"test_results": f"Sandbox runtime error: {test_execution_result['stderr']}",
"chat_history": state["chat_history"] + [AIMessage(content=f"Test Execution Failed (Sandbox Error):
{test_execution_result['stderr']}")]
}
stdout = test_execution_result["stdout"]
stderr = test_execution_result["stderr"]
exit_code = test_execution_result["exit_code"]
full_output = f"Stdout:
{stdout}
Stderr:
{stderr}
Exit Code: {exit_code}"
logger.info(f"Test Execution Result:
{full_output}")
return {
"test_results": full_output,
"chat_history": state["chat_history"] + [AIMessage(content=f"Test Execution Result:
```
{full_output}
```")]
}
def reflect_on_results(state: AgentState, config: dict) -> AgentState:
"""Agent node to reflect on test results and potentially update the plan for self-correction."""
logger.info("--- Reflecting on Results ---")
llm = _get_llm(config["configurable"]["openai_api_key"])
reflection_prompt = _get_prompt(
"You are an expert debugger and code reviewer. Analyze the provided task, generated code, test code, and test results. "
"Identify issues, propose fixes, and revise the plan if necessary. "
"If the tests pass and the code meets the requirements, indicate that the task is complete. "
"Output your reflection as a JSON object with 'thought', 'new_plan_steps' (list of strings), and 'is_complete' (boolean)."
)
reflection_chain = reflection_prompt | llm.with_structured_output(Reflection)
try:
reflection_response = reflection_chain.invoke({
"chat_history": state["chat_history"],
"task": state["task"],
"current_state": f"Current Plan:
{state['plan']}
Generated Code:
{state['code']}
Test Code:
{state['test_code']}
Test Results:
{state['test_results']}"
})
logger.info(f"Reflection:
Thought: {reflection_response.thought}
New Plan: {reflection_response.new_plan_steps}
Is Complete: {reflection_response.is_complete}")
new_plan_str = "
".join(f"- {step}" for step in reflection_response.new_plan_steps)
return {
"plan": new_plan_str if reflection_response.new_plan_steps else state["plan"],
"iterations": state["iterations"] + 1,
"chat_history": state["chat_history"] + [AIMessage(content=f"Reflection: {reflection_response.thought}
New Plan: {new_plan_str}
Complete: {reflection_response.is_complete}")],
"output": "Code generation complete, ready for PR." if reflection_response.is_complete else None
}
except Exception as e:
logger.exception(f"Error in reflect_on_results: {e}")
return {"output": f"Failed to reflect: {e}"}
def create_github_pr_node(state: AgentState) -> AgentState:
"""Agent node to create a GitHub Pull Request."""
logger.info("--- Creating GitHub Pull Request ---")
try:
pr_url = create_branch_commit_pr(
repo_owner=state["github_repo_owner"],
repo_name=state["github_repo_name"],
task_description=state["task"],
code_content=state["code"],
test_content=state["test_code"]
)
logger.info(f"Pull Request created: {pr_url}")
return {
"pull_request_url": pr_url,
"chat_history": state["chat_history"] + [AIMessage(content=f"GitHub PR created: {pr_url}")],
"output": f"Task completed. PR created: {pr_url}"
}
except Exception as e:
logger.exception(f"Error creating GitHub PR: {e}")
return {"output": f"Failed to create GitHub PR: {e}"}
def should_continue_coding(state: AgentState) -> str:
"""Conditional edge to decide whether to continue coding or proceed to GitHub."""
if state["chat_history"] and isinstance(state["chat_history"][-1], AIMessage):
try:
reflection_content = state["chat_history"][-1].content
if "Complete: True" in reflection_content:
logger.info("Agent reflection indicates task is complete. Proceeding to GitHub.")
return "create_github_pr"
except Exception as e:
logger.warning(f"Could not parse last chat history message for completion status: {e}")
if state["iterations"] >= state["max_iterations"]:
logger.warning(f"Max iterations ({state['max_iterations']}) reached. Aborting coding loop.")
return "end_failure"
logger.info(f"Continuing coding loop. Iteration {state['iterations']}/{state['max_iterations']}")
return "continue_coding"
def create_agent_graph(openai_api_key: str):
"""
Creates and compiles the LangGraph workflow for the autonomous coding agent.
"""
workflow = StateGraph(AgentState)
workflow.add_node("plan_task", plan_task)
workflow.add_node("generate_code", generate_code)
workflow.add_node("generate_tests", generate_tests)
workflow.add_node("execute_tests", execute_tests)
workflow.add_node("reflect_on_results", reflect_on_results)
workflow.add_node("create_github_pr", create_github_pr_node)
workflow.set_entry_point("plan_task")
workflow.add_edge("plan_task", "generate_code")
workflow.add_edge("generate_code", "generate_tests")
workflow.add_edge("generate_tests", "execute_tests")
workflow.add_edge("execute_tests", "reflect_on_results")
workflow.add_conditional_edges(
"reflect_on_results",
should_continue_coding,
{
"continue_coding": "generate_code",
"create_github_pr": "create_github_pr",
"end_failure": END
}
)
workflow.add_edge("create_github_pr", END)
logger.info("LangGraph workflow initialized and compiled.")
return workflow.compile(config={"configurable": {"openai_api_key": openai_api_key}})
github_utils.py, is created to encapsulate all GitHub API interactions using the PyGithub library. The _get_github_client function securely initializes the GitHub client using a Personal Access Token (PAT) retrieved from environment variables, complete with retry logic for robustness. The central function here is create_branch_commit_pr, which takes the repository details, task description, generated code, and tests. It then performs the following steps:
1. Repository Access: Retrieves the specified GitHub repository.
2. Branch Creation: Creates a new feature branch with a unique name based on the task description.
3. File Staging: Creates Git blobs and tree elements for the generated code (src/generated_code.py) and tests (tests/test_generated_code.py). Note the update to execute_tests to use these paths and sys.path.append('src') for imports.
4. Commit Creation: Creates a new commit on the feature branch, including a descriptive commit message.
5. Branch Update: Updates the branch reference to point to the new commit.
6. Pull Request Creation: Finally, it opens a new pull request from the feature branch to the default branch, including a title and a detailed body explaining the agent's work.
In agent.py, a new node create_github_pr_node is added to the LangGraph workflow. This node invokes the create_branch_commit_pr utility function. The should_continue_coding conditional edge is updated to route to create_github_pr once the reflect_on_results node indicates that the task is is_complete: True. After the PR is successfully created, the agent transitions to the END state, signifying the completion of its primary task.
Before running, ensure your GITHUB_TOKEN, GITHUB_REPO_OWNER, and GITHUB_REPO_NAME are correctly set in .env. The GITHUB_TOKEN must have at least repo scope. Create an empty repository on GitHub for testing. Run python main.py with the test_task. After the agent completes, verify that a new branch and pull request appear in your GitHub repository, containing the generated code and tests.
Phase 4: Production Hardening: Observability and Error Handling
This phase focuses on enhancing the agent's resilience and debuggability for production environments. It formalizes error handling, integrates robust retry mechanisms, and sets up comprehensive observability using LangSmith.
# main.py (updated for LangSmith note)
import os
import logging
from dotenv import load_dotenv
from typing import TypedDict, List, Annotated
from langgraph.graph import StateGraph, END
from agent import create_agent_graph, AgentState
load_dotenv()
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Optional: Configure LangSmith for observability
# Set LANGCHAIN_TRACING_V2="true" and LANGCHAIN_API_KEY="your_langsmith_api_key" in your .env
# LANGCHAIN_PROJECT should also be set, e.g., "Autonomous Coding Agent"
# For local development, you might not need to set these explicitly if you're not using LangSmith.
# However, for production, these are crucial.
def run_agent(task: str) -> str:
"""
Initializes and runs the autonomous coding agent with a given task.
"""
openai_api_key = os.environ.get("OPENAI_API_KEY")
e2b_api_key = os.environ.get("E2B_API_KEY")
github_token = os.environ.get("GITHUB_TOKEN")
github_repo_owner = os.environ.get("GITHUB_REPO_OWNER")
github_repo_name = os.environ.get("GITHUB_REPO_NAME")
if not all([openai_api_key, e2b_api_key, github_token, github_repo_owner, github_repo_name]):
logger.error("Missing one or more required environment variables.")
raise ValueError("Ensure OPENAI_API_KEY, E2B_API_KEY, GITHUB_TOKEN, GITHUB_REPO_OWNER, GITHUB_REPO_NAME are set.")
workflow = create_agent_graph(openai_api_key)
inputs: AgentState = {
"task": task,
"chat_history": [],
"plan": "",
"code": "",
"test_code": "",
"test_results": "",
"pull_request_url": "",
"iterations": 0,
"max_iterations": 5, # Default, can be overridden by plan_task
"output": "",
"pr_number": 0, # Placeholder
"github_repo_owner": github_repo_owner,
"github_repo_name": github_repo_name
}
logger.info(f"Starting agent for task: {task}")
final_output = None
try:
# The LangGraph stream method automatically integrates with LangSmith if environment variables are set.
for s in workflow.stream(inputs, {"configurable": {"thread_id": "1"}}):
logger.info(f"Agent step: {s}")
final_output = s
if final_output:
# final_output is {"node_name": state_dict} from the last streamed step
last_state = next(iter(final_output.values()), {}) if isinstance(final_output, dict) else {}
return last_state.get("output", str(final_output))
else:
return "Agent finished without explicit output."
except Exception as e:
logger.exception(f"An error occurred during agent execution: {e}")
return f"Agent execution failed: {e}"
if __name__ == "__main__":
test_task = "Create a Python function `multiply(a, b)` that returns the product of two numbers. Include a docstring and a simple unit test for it. The function should be in a file named `src/generated_code.py` and tests in `tests/test_generated_code.py`."
print("--- Running autonomous coding agent ---")
result = run_agent(test_task)
print("
--- Agent Finished ---")
print(f"Final Result: {result}")
tenacity for robust retry logic with exponential backoff, which was already integrated into our tools.py and github_utils.py functions in Phase 1 and 3, respectively. This helps the agent gracefully handle transient network issues or temporary API rate limits from services like E2B or GitHub, preventing premature failures and improving overall reliability. For instance, if an API call experiences a momentary glitch, the agent will automatically retry the operation, enhancing its resilience.
Crucially, we integrate LangSmith for comprehensive observability. By setting specific environment variables (LANGCHAIN_TRACING_V2, LANGCHAIN_API_KEY, LANGCHAIN_PROJECT in the .env file), LangChain and LangGraph automatically instrument all LLM calls, tool executions, and state transitions. This provides a detailed "flight recorder" of every agent run, allowing developers to visualize the agent's decision-making process, examine intermediate states, and pinpoint exactly where and why issues occur. This is invaluable for debugging complex agentic workflows, understanding LLM reasoning, and evaluating performance. Without such tracing, debugging an autonomous agent that might run for many iterations and interact with external systems would be extremely challenging. We've also ensured that our AgentState captures sufficient detail, including iteration counts and outputs, to provide a clear narrative of the agent's progress, which is then visible in LangSmith traces.
To verify, ensure LANGCHAIN_TRACING_V2="true", LANGCHAIN_API_KEY="your_langsmith_api_key_here", and LANGCHAIN_PROJECT="Autonomous Coding Agent" are set in your .env file. Run python main.py with a task. Then, navigate to your LangSmith project dashboard to observe the detailed traces of the agent's execution. You can also temporarily invalidate an API key (e.g., E2B_API_KEY) to see the retry logic activate in the console logs before ultimately failing, and observe how this failure is recorded in LangSmith.
Testing & Evaluation
Testing and evaluating an autonomous coding agent, especially one that interacts with external systems and self-corrects, requires a multi-faceted approach. Due to the inherent non-determinism of LLMs and the complexity of iterative workflows, traditional unit tests alone are insufficient. We combine functional testing with LLM-as-a-Judge evaluation, failure scenario analysis, and performance validation.
Functional Testing
Functional testing ensures the agent performs its core duties correctly. This involves defining a diverse set of coding tasks, ranging from simple functions to more complex modules, and verifying the agent's output against expected behavior:
- Code Correctness: Does the generated
src/generated_code.pyaccurately implement the task requirements and handle specified inputs/outputs? - Test Suite Quality: Are the generated
tests/test_generated_code.pycomprehensive? Do they cover positive cases, edge cases, and potential error conditions? - Self-Correction Efficacy: When tests initially fail, does the agent successfully identify the root cause, propose a fix in its reflection, and generate corrected code that passes the tests?
- GitHub Integration Accuracy: Is the pull request created correctly? Does it target the right branch, include the correct files, and have a meaningful title and description?
LLM-as-a-Judge Evaluation
For scalable and automated assessment of the agent's output, we employ an LLM-as-a-Judge. A separate, often more powerful or specifically instructed, LLM evaluates the agent's performance against a predefined rubric. This allows for automated quality checks in CI/CD pipelines.
Automated Evaluation Code Example
# tests/test_agent.py
import os
import json
import logging
from typing import Dict, Any, List
from main import run_agent # Import the agent's entry point
# from github_utils import get_pr_comments # Uncomment if you need to fetch PR comments for evaluation
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class AgentPerformanceRubric(BaseModel):
"""Rubric for evaluating the autonomous coding agent's performance."""
code_correctness_score: int = Field(..., description="Score (1-5) for code correctness. 5=perfect.")
test_coverage_score: int = Field(..., description="Score (1-5) for test coverage. 5=comprehensive.")
plan_quality_score: int = Field(..., description="Score (1-5) for initial plan quality. 5=well-structured.")
self_correction_efficiency_score: int = Field(..., description="Score (1-5) for how efficiently the agent self-corrected. 5=fast, accurate fixes.")
overall_assessment: str = Field(..., description="Overall assessment of the agent's performance.")
passed: bool = Field(..., description="True if the agent successfully completed the task to a high standard.")
def evaluate_agent_output_with_llm(task: str, agent_final_state: Dict[str, Any]) -> AgentPerformanceRubric:
"""
Uses an LLM as a judge to evaluate the agent's performance based on its final state.
"""
openai_api_key = os.environ.get("OPENAI_API_KEY")
if not openai_api_key:
raise ValueError("OPENAI_API_KEY not set for LLM-as-a-Judge. Please configure in .env.")
llm = ChatOpenAI(model="gpt-4o", temperature=0.0, api_key=openai_api_key) # Use lower temperature for evaluation
evaluation_prompt = ChatPromptTemplate.from_messages([
("system",
"You are an impartial judge evaluating an autonomous coding agent's work. "
"You will be given the original task, and the agent's final state including generated code, tests, and PR URL. "
"Rate the agent's performance based on the following rubric (1-5, 5 being excellent): "
"1. Code Correctness: Is the generated code functional, bug-free, and adheres to the task? "
"2. Test Coverage: Are the generated unit tests comprehensive, covering functionality and edge cases? "
"3. Plan Quality: Was the initial plan logical and effective for addressing the task? "
"4. Self-Correction Efficiency: How well did the agent identify and fix issues during its iterative process? "
"Provide an overall assessment and a boolean indicating if the task was successfully completed to a high standard. "
"Ensure the Python code provided in the agent_final_state is syntactically correct and runnable."
),
("human",
"Original Task:
{task}
" # Pass the original task directly
"Agent's Final Code:{generated_code}
" # Directly pass generated code
"Agent's Final Tests:{generated_tests}
" # Directly pass generated tests
"Agent's Final Output Message: {agent_output_message}
" # Final completion message
"Pull Request URL: {pr_url}
" # PR URL
"Please provide your evaluation according to the rubric, ensuring the generated code is valid Python."
)
])
evaluation_chain = evaluation_prompt | llm.with_structured_output(AgentPerformanceRubric)
try:
evaluation = evaluation_chain.invoke({
"task": task,
"generated_code": agent_final_state.get("code", "No code generated."),
"generated_tests": agent_final_state.get("test_code", "No tests generated."),
"agent_output_message": agent_final_state.get("output", "Agent finished without explicit message."),
"pr_url": agent_final_state.get("pull_request_url", "N/A")
})
logger.info(f"LLM-as-a-Judge Evaluation:
{evaluation.json(indent=2)}")
return evaluation
except Exception as e:
logger.error(f"Error during LLM-as-a-Judge evaluation: {e}")
return AgentPerformanceRubric(
code_correctness_score=1,
test_coverage_score=1,
plan_quality_score=1,
self_correction_efficiency_score=1,
overall_assessment=f"Evaluation failed: {e}",
passed=False
)
# To make run_agent return the final state for evaluation, you'd modify main.py:
# def run_agent_and_get_final_state(task: str) -> AgentState:
# # ... existing setup ...
# final_state = None
# for s in workflow.stream(inputs, {"configurable": {"thread_id": "1"}}):
# final_state = s
# return final_state[END] if END in final_state else final_state # Or just the entire state if END is not reached
if __name__ == "__main__":
# This part requires modifying main.py to return the final AgentState object
# For this example, we will simulate a final state.
test_task_for_eval = "Create a Python function `power(base, exponent)` that calculates base to the power of exponent. Include docstrings and unit tests covering positive, negative, and zero exponents."
print("--- Simulating agent run for LLM-as-a-Judge evaluation ---
")
# Simulate a final AgentState object (in reality, this comes from run_agent_and_get_final_state)
simulated_final_state = {
"task": test_task_for_eval,
"plan": "- Plan to implement power function
- Generate tests for exponents: positive, negative, zero
- Write code, run tests, self-correct.",
"code": "def power(base: float, exponent: int) -> float:
\"\"\"Calculates base to the power of exponent.
Args:
base: The base number.
exponent: The exponent (integer).
Returns:
The result of base raised to the power of exponent.
\"\"\"
return base ** exponent",
"test_code": "import unittest
import sys; sys.path.append('src')
from generated_code import power
class TestPowerFunction(unittest.TestCase):
def test_positive_exponent(self):
self.assertEqual(power(2, 3), 8)
self.assertEqual(power(5, 2), 25)
def test_zero_exponent(self):
self.assertEqual(power(10, 0), 1) # Any non-zero base to power 0 is 1
self.assertEqual(power(-7, 0), 1)
def test_negative_exponent(self):
self.assertAlmostEqual(power(2, -1), 0.5)
self.assertAlmostEqual(power(10, -2), 0.01)
def test_base_zero(self):
self.assertEqual(power(0, 5), 0)
with self.assertRaises(ZeroDivisionError): # 0**-ve is undefined / error
power(0, -2)
if __name__ == '__main__':
unittest.main()",
"test_results": "Stdout:
.
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
Stderr:
Exit Code: 0",
"pull_request_url": "https://github.com/mortalapps/autonomous-agent-test/pull/42",
"iterations": 2,
"max_iterations": 5,
"output": "Task completed. PR created: https://github.com/mortalapps/autonomous-agent-test/pull/42",
"github_repo_owner": "mortalapps",
"github_repo_name": "autonomous-agent-test"
}
evaluation_result = evaluate_agent_output_with_llm(
task=test_task_for_eval,
agent_final_state=simulated_final_state
)
print("
--- LLM-as-a-Judge Evaluation Complete ---")
print(f"Overall Passed: {evaluation_result.passed}")
print(f"Overall Assessment: {evaluation_result.overall_assessment}")Failure Scenario Testing
An autonomous agent must be resilient to various failures. We rigorously test by:
- Invalid API Keys: Intentionally misconfigure
OPENAI_API_KEY,E2B_API_KEY, orGITHUB_TOKENto ensure the agent logs appropriate errors, retries where applicable, and terminates gracefully without exposing sensitive information. - Sandbox Runtime Errors: Provide tasks that lead to code with runtime errors (e.g., division by zero, invalid list indexing). Verify the agent detects these via
stderrandexit_codefrom the sandbox, reflects on the error message, and attempts to self-correct. - GitHub API Failures: Simulate network outages or permission issues when creating branches or PRs. Confirm that
tenacityretry logic activates and, if persistent, leads to a clear error message in the agent's output. - Max Iterations Reached: Design tasks that are deliberately complex or ambiguous, ensuring the agent's
max_iterationslimit is respected, preventing infinite loops and ensuring a graceful exit even if the task cannot be fully completed.
Edge Case Coverage
Testing with edge cases helps uncover subtle flaws:
- Trivial Tasks: Provide very simple tasks (e.g., a function that returns a constant) to ensure the agent doesn't over-engineer or fail on simplicity.
- Complex Logic: Tasks requiring multiple functions, class definitions, or interactions with external (mocked) APIs if the sandbox environment supports it.
- Empty or Malformed Input: How does the agent handle an empty task description or a task that is syntactically invalid for programming?
- Security Vulnerabilities: While not fully implemented in this guide, in a real-world scenario, you'd test if the agent can generate or identify insecure code patterns (e.g.,
os.systemcalls, SQL injection if a database tool were present).
Performance Validation
Performance metrics are crucial for production deployment:
- Time-to-PR: Measure the total time from task submission to a successful GitHub PR. This is a key indicator of development cycle acceleration.
- Token Usage: Monitor LLM token consumption per task using LangSmith. Identify opportunities to optimize prompts for conciseness and reduce operational costs.
- Iteration Count: Track the average and maximum number of self-correction iterations per task. High iteration counts may indicate issues with initial planning, reflection quality, or prompt effectiveness.
Regression Testing
As the agent evolves, a robust regression test suite prevents new changes from breaking existing functionality. Integrate the LLM-as-a-Judge evaluation into a CI/CD pipeline. Any code changes to the agent should trigger this automated evaluation against a baseline set of tasks. This ensures that new features or optimizations do not degrade performance or introduce bugs in previously working scenarios. LangSmith's prompt versioning features can also track prompt changes and their impact on agent performance over time, helping to maintain evaluation consistency.
Deployment
Deploying an autonomous coding agent to production requires careful consideration of security, scalability, and operational robustness. This section outlines a production-ready deployment strategy leveraging Docker for containerization and Kubernetes for orchestration. For local development, you can simply run python main.py after setting up your .env file.
Dockerfile
The Dockerfile defines the agent's container image, ensuring a consistent and isolated runtime environment. This approach eliminates 'it works on my machine' issues and provides a portable artifact for deployment.
# Use a slim Python base image for smaller image size and reduced attack surface
FROM python:3.10-slim-buster
# Set working directory inside the container
WORKDIR /app
# Install system dependencies if any (e.g., git, if PyGithub needed it directly)
# For PyGithub, python-git is usually enough, but sometimes git CLI might be useful in sandbox
# RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
# Copy requirements.txt and install Python dependencies
# Use --no-cache-dir to prevent pip from storing cache, reducing image size
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code
COPY . .
# Create a non-root user for security best practices
RUN adduser --system --group appuser
USER appuser
# Define entrypoint to run the main script
# In a real-world scenario, you might want to wrap this in a shell script
# that handles environment variable validation or other pre-start logic.
# For an agent that processes a single task and exits, this is suitable.
# For a long-running service, you might use a web framework like FastAPI.
ENTRYPOINT ["python", "main.py"]Kubernetes Manifest
For scalable and resilient deployment, Kubernetes is an excellent choice. This deployment.yaml defines a basic deployment for our autonomous coding agent.
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: autonomous-coding-agent
labels:
app: autonomous-coding-agent
spec:
replicas: 1 # Start with 1 replica, scale as needed based on workload
selector:
matchLabels:
app: autonomous-coding-agent
template:
metadata:
labels:
app: autonomous-coding-agent
spec:
containers:
- name: agent
image: your-docker-registry/autonomous-coding-agent:latest # IMPORTANT: Replace with your actual image path and tag
env:
# Securely inject environment variables using Kubernetes Secrets
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: agent-secrets # Name of the Kubernetes Secret
key: OPENAI_API_KEY
- name: E2B_API_KEY
valueFrom:
secretKeyRef:
name: agent-secrets
key: E2B_API_KEY
- name: GITHUB_TOKEN
valueFrom:
secretKeyRef:
name: agent-secrets
key: GITHUB_TOKEN
- name: GITHUB_REPO_OWNER
valueFrom:
secretKeyRef:
name: agent-secrets
key: GITHUB_REPO_OWNER
- name: GITHUB_REPO_NAME
valueFrom:
secretKeyRef:
name: agent-secrets
key: GITHUB_REPO_NAME
- name: LANGCHAIN_TRACING_V2
value: "true" # Enable LangSmith tracing
- name: LANGCHAIN_API_KEY
valueFrom:
secretKeyRef:
name: agent-secrets
key: LANGCHAIN_API_KEY
- name: LANGCHAIN_PROJECT
value: "Autonomous Coding Agent Production" # Specific project for production traces
# Resource requests and limits for proper scheduling and stability
resources:
requests:
memory: "1Gi" # Request 1GB of memory
cpu: "1000m" # Request 1 CPU core
limits:
memory: "2Gi" # Limit to 2GB of memory
cpu: "2000m" # Limit to 2 CPU cores
# If the agent were a web service, you'd add liveness and readiness probes here.
# For a script that runs and exits, Kubernetes' restart policy acts as a basic liveness check.
restartPolicy: OnFailure # Restart the container if the script exits with an errorSecrets Management
All sensitive information (API keys, tokens) must be managed securely. In the deployment.yaml, we reference Kubernetes Secrets. First, create the secret in your Kubernetes cluster:
# Ensure these environment variables are set in your shell before running this command
export OPENAI_API_KEY="your_openai_api_key"
export E2B_API_KEY="your_e2b_api_key"
export GITHUB_TOKEN="your_github_token"
export GITHUB_REPO_OWNER="your_github_username_or_org"
export GITHUB_REPO_NAME="your_repo_name"
export LANGCHAIN_API_KEY="your_langsmith_api_key"
kubectl create secret generic agent-secrets \
--from-literal=OPENAI_API_KEY=$OPENAI_API_KEY \
--from-literal=E2B_API_KEY=$E2B_API_KEY \
--from-literal=GITHUB_TOKEN=$GITHUB_TOKEN \
--from-literal=GITHUB_REPO_OWNER=$GITHUB_REPO_OWNER \
--from-literal=GITHUB_REPO_NAME=$GITHUB_REPO_NAME \
--from-literal=LANGCHAIN_API_KEY=$LANGCHAIN_API_KEYThese secrets are then mounted as environment variables into the container, ensuring they are not hardcoded or exposed in the image. For more advanced setups, consider external secret managers like HashiCorp Vault or cloud-native solutions (AWS Secrets Manager, Azure Key Vault).
Health Check and Readiness Probe
For an agent designed as a one-shot job (running a task and exiting), traditional HTTP liveness and readiness probes are not directly applicable to the main.py script itself. Kubernetes' restartPolicy: OnFailure acts as a basic health check, restarting the pod if the script exits with a non-zero status. If the agent were exposed as a web service (e.g., using FastAPI to trigger tasks), then standard HTTP probes would be essential to ensure the service is responsive and ready to handle requests.
Scaling Considerations
The replicas: 1 in the deployment means only one instance of the agent will run. For handling multiple concurrent tasks or achieving high availability:
- Horizontal Pod Autoscaler (HPA): Configure HPA to scale the number of replicas based on CPU utilization or custom metrics (e.g., the depth of a task queue). This is crucial if the agent is part of a larger, high-throughput system.
- Task Queue Integration: For asynchronous task processing, integrate the agent with a message queue (e.g., RabbitMQ, SQS, Kafka). Agent replicas would consume tasks from the queue, allowing for parallel processing, decoupling task submission from execution, and providing backpressure.
- Resource Limits: Properly setting
requestsandlimitsin the deployment ensures fair resource allocation, prevents resource starvation, and improves cluster stability.
Rollback Strategy
A robust rollback strategy is essential for minimizing downtime and recovering from faulty deployments:
- Immutable Deployments: Always build new Docker images for each version of your agent. Use semantic versioning (e.g.,
v1.0.0,v1.0.1) for Docker image tags instead oflatest. - Kubernetes Rollback: If a new deployment introduces issues, Kubernetes allows you to easily roll back to a previous stable revision:
``bash kubectl rollout undo deployment/autonomous-coding-agent `` This command reverts the deployment to its previous configuration, pulling the older, stable image. Ensure your CI/CD pipeline includes automated tests and evaluations before promoting new versions to production.
Production Hardening
Hardening an autonomous coding agent for production involves addressing critical aspects beyond its core functionality, focusing on security, reliability, observability, and operational efficiency. Given its ability to generate and execute code, these considerations are paramount.
Security
- Least Privilege (OWASP ASI03): The GitHub Personal Access Token (PAT) must be scoped to the absolute minimum permissions required (e.g.,
reposcope for creating branches and PRs). Avoid granting broader access than necessary. Similarly, if E2B offered fine-grained permissions, those should be applied. The Kubernetes service account running the pod should also adhere to least privilege, only having permissions needed for logging, secrets access, etc. - Secrets Management: As detailed in deployment, use Kubernetes Secrets or a dedicated secrets manager (e.g., HashiCorp Vault, AWS Secrets Manager) for all API keys and tokens. Implement regular key rotation policies to mitigate the risk of compromised credentials.
- Sandbox Security (OWASP ASI05): While E2B provides a secure, isolated sandbox, it's crucial to understand its security model. Never execute untrusted or unknown code directly on the host system. Ensure the sandbox environment itself is regularly updated and monitored for vulnerabilities.
- Input Validation & Sanitization: While LLMs process natural language, any user inputs that could eventually influence code generation or tool arguments should be validated and sanitized to prevent prompt injection attacks or unintended commands.
- Dependency Scanning: Regularly scan your project dependencies (
requirements.txt) for known vulnerabilities using tools like Snyk, Dependabot, or Trivy. Promptly update vulnerable libraries.
Observability
- Comprehensive Structured Logging: Implement structured logging (e.g., JSON format) with varying severity levels (DEBUG, INFO, WARNING, ERROR, CRITICAL). Logs should capture key agent decisions, LLM inputs/outputs (carefully redacting sensitive info), tool invocations, and detailed error messages. Centralize these logs using a system like Elasticsearch, Splunk, or cloud-native logging services.
- Distributed Tracing (LangSmith): Leverage LangSmith (as integrated) for end-to-end tracing of agent runs. This allows visualization of the entire workflow, including LLM calls, tool executions, and state transitions, which is indispensable for debugging complex, multi-turn agentic systems and understanding LLM reasoning.
- Metrics and Alerting: Collect key operational metrics such as 'tasks completed per hour,' 'average iterations per task,' 'failed tasks rate,' 'LLM token usage per task,' and 'E2B sandbox execution time.' Set up alerts for deviations from baselines (e.g., sudden spikes in failure rates, increased latency, unexpected cost increases).
- Time-Travel Debugging: Utilize LangSmith's ability to replay agent runs, inspect the agent's state at each step, and analyze its decision-making process. This is invaluable for post-incident analysis and continuous improvement.
Reliability
- Retry Mechanisms: The
tenacitylibrary provides robust retry logic with exponential backoff for all external API calls (LLM, E2B, GitHub). This is vital for handling transient network issues, temporary API rate limits, and improving overall system resilience. - Circuit Breakers: Consider implementing circuit breakers for critical external services to prevent cascading failures if a dependency becomes unhealthy. This can prevent your agent from repeatedly hammering a failing service.
- Idempotency: Design agent actions to be idempotent where possible. For GitHub operations, this means ensuring that re-running a step (e.g., attempting to create a PR for an already completed task) does not lead to unintended duplicate PRs or errors. Our branch naming strategy helps, but a more robust check for existing PRs could be added.
- Fallback LLM Providers: For critical operations, implement a fallback strategy to a secondary, potentially less performant but reliable, LLM provider if the primary one is unavailable.
Rate Limiting
- External API Rate Limits: Be acutely aware of and respect the rate limits imposed by OpenAI, E2B, and GitHub. Implement client-side rate limiting or use libraries that automatically handle rate limit headers and backoff. Exceeding limits can lead to service interruptions.
- Internal Rate Limiting: If the agent processes tasks from a queue, ensure it doesn't overwhelm downstream systems (e.g., a build server triggered by GitHub webhooks). Implement internal rate limits if necessary to control throughput.
Authentication
- Service Principals/Managed Identities: For cloud deployments, prioritize using cloud-native identity solutions (e.g., AWS IAM Roles, Azure Managed Identities) instead of long-lived Personal Access Tokens. These provide more secure, automatically rotated, and fine-grained access control.
- OAuth/OIDC: Explore OAuth or OpenID Connect for more robust and granular access control to GitHub if the PAT model proves too broad or difficult to manage at scale.
Governance
- Human-in-the-Loop (HITL): For tasks that involve critical code changes or sensitive systems, implement an explicit human review and approval step before the agent can merge a PR. This can be achieved with LangGraph's interrupt nodes.
- Auditing: Maintain a clear, immutable audit trail of all agent actions, decisions, LLM prompts/responses, and external interactions. This includes who initiated a task, when, what code was generated, and when a PR was opened. LangSmith traces contribute significantly to this.
- Version Control for Agent Logic: Treat the agent's code, prompts, and configuration as first-class code artifacts. Store them in version control and apply standard CI/CD practices for testing and deployment.
Cost Optimisation
- Token Usage Monitoring: Continuously monitor LLM token usage via LangSmith or custom metrics. Identify and optimize prompts for conciseness and efficiency without sacrificing performance. This is crucial for managing operational costs.
- Model Selection: Evaluate if a less expensive LLM (e.g., gpt-4o-mini for simpler planning steps) can achieve acceptable performance for certain sub-tasks, reserving the most powerful models for critical reasoning.
- Caching: Implement caching for LLM responses to common queries or tool outputs to reduce redundant calls and associated costs.
- Sandbox Resource Management: Ensure E2B sandbox instances are spun down when not in use to avoid unnecessary charges. Optimize sandbox usage patterns to minimize idle time.
- By the end of this project, you will be able to implement a state machine for complex agent workflows using LangGraph.
- By the end of this project, you will be able to integrate secure code execution sandboxes for safe tool use.
- By the end of this project, you will be able to design and implement agent reflection and self-correction mechanisms based on external feedback.
- By the end of this project, you will be able to configure persistent state for long-running agent graphs using LangGraph checkpointers.
- By the end of this project, you will be able to orchestrate multi-step development and deployment processes, including GitHub integration.
- By the end of this project, you will be able to apply observability tools like LangSmith for debugging and monitoring agent execution.