Build an AI Coding Agent That Fixes GitHub Issues Automatically
GitHub Issues are often the starting point for software changes. A developer reports a bug, requests a small feature, or describes something that needs to be improved, and someone from the development team investigates the repository before writing a fix.
A coding agent can automate much of this workflow.
Instead of asking an AI model to simply suggest a code change, we can give it a GitHub issue, let it inspect the repository, identify the relevant files, make a change, run the project's tests, and prepare a pull request for a developer to review.
In this tutorial, we will build an AI coding agent in Python that takes a GitHub issue and turns it into a proposed code change.
The agent will use different LLMs for different stages. A low-cost model can understand and classify the issue, a stronger coding model can implement the change, and another model can review the result before the agent opens a pull request.
We will use Velona's unified AI gateway so the agent can access multiple models through one API.
What Are We Building?
The finished workflow will look like this:
GitHub Issue
↓
AI Agent
↓
Read repository
↓
Understand the issue
↓
Find relevant files
↓
Create a fix
↓
Run tests
↓
Review the changes
↓
Create GitHub Pull Request
The agent will not push changes directly to the default branch.
Instead, it will create a separate branch for the issue, make the changes there, and open a pull request. This keeps a human developer in the review loop and gives the team a chance to inspect the generated code before merging it.
Why Build an AI Coding Agent?
A typical issue fixing workflow contains several repetitive steps.
- Read the GitHub issue.
- Understand what the issue is asking for.
- Search the repository for relevant code.
- Inspect the surrounding implementation.
- Write a change.
- Run tests.
- Fix any failures.
- Review the final diff.
- Create a pull request.
An AI coding agent can assist with each of these steps.
The important part is that the agent should not be treated as an autonomous replacement for code review. Its job is to produce a useful, testable change that a developer can inspect.
Why Use Different AI Models?
A coding agent can make several model calls while fixing one issue.
Using an expensive model for every step can make the workflow unnecessarily expensive.
For example, understanding a short GitHub issue does not require the same model capability as debugging a difficult database query.
We can therefore use a multi-model architecture:
- Issue analysis: Use a low-cost model to understand and classify the issue.
- Repository analysis: Use an affordable model to identify relevant files and code paths.
- Implementation: Use a stronger coding model for the actual code change.
- Review: Use a separate model to inspect the proposed change and test results.
Velona currently lists DeepSeek V4 Flash at ₹8.32 per million input tokens and ₹16.63 per million output tokens. Kimi K2.6 is currently listed at ₹97.66 per million input tokens and ₹411.18 per million output tokens. Grok 4.20 is listed at ₹128.48 per million input tokens and ₹256.96 per million output tokens.
These are live prices and can change, so check the Velona Pricing Index before calculating production costs.
Architecture of the Coding Agent
Our agent will have five main components.
- Issue Reader: Retrieves the issue title, description, labels, and metadata.
- Repository Explorer: Clones the repository and gives the agent access to its files.
- Planner: Determines which files need to be investigated and what change is likely required.
- Coding Agent: Modifies the relevant files and runs tests.
- Reviewer: Checks the resulting diff and test output before the pull request is created.
GitHub Issue
↓
Issue Reader
↓
Planner
↓
Repository Explorer
↓
Coding Agent
↓
Run Tests
↓
Reviewer
↓
Git Branch
↓
Pull Request
What We Need
For this project, you will need:
- A GitHub repository containing a project you can safely test.
- A GitHub token with the permissions required to read the repository and create branches and pull requests.
- A Velona API key.
- Python 3.10 or newer.
- Git installed on the machine running the agent.
For production use, a GitHub App or a fine-grained personal access token is preferable to using a broad classic token. GitHub's current documentation lists repository contents and pull request permissions for the relevant API operations.
Setting Up the Python Project
Create a new project:
mkdir github-coding-agent
cd github-coding-agent
python -m venv .venv
source .venv/bin/activate
pip install httpx python-dotenv
On Windows, activate the environment with:
.venv\Scripts\activate
Create a .env file:
VELONA_API_KEY=YOUR_VELONA_API_KEY
GITHUB_TOKEN=YOUR_GITHUB_TOKEN
Never commit the .env file to the repository.
Connecting to GitHub
GitHub provides REST API endpoints for reading and managing issues, repositories, branches, and pull requests.
We will use the API for repository metadata and Git operations where appropriate, while using a local clone for inspecting and modifying the actual project files.
Start with a small GitHub API helper:
import os
import httpx
from dotenv import load_dotenv
load_dotenv()
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
GITHUB_API = "https://api.github.com"
HEADERS = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {GITHUB_TOKEN}",
"X-GitHub-Api-Version": "2026-03-10"
}
Reading a GitHub Issue
The first thing the agent needs is the actual issue.
We can retrieve it using the GitHub Issues API.
def get_issue(owner, repo, issue_number):
url = (
f"{GITHUB_API}/repos/"
f"{owner}/{repo}/issues/{issue_number}"
)
response = httpx.get(
url,
headers=HEADERS,
timeout=30
)
response.raise_for_status()
return response.json()
Now create a small helper to turn the issue into information that can be passed to the model:
def format_issue(issue):
return f"""
Title:
{issue.get("title", "")}
Description:
{issue.get("body", "")}
Labels:
{", ".join(
label["name"]
for label in issue.get("labels", [])
)}
""".strip()
A typical issue might look like:
Title:
Login API returns 500 when email is missing
Description:
The login endpoint crashes when the email field is not
provided. It should return a validation error instead.
Understanding the Issue with an LLM
The first model does not need to write code.
Its job is to understand what the issue is asking for and produce a small implementation plan.
We can use a low-cost model for this stage.
VELONA_API = "https://velona.in/gateway/v1/inference/run"
def call_velona(model, turns):
response = httpx.post(
VELONA_API,
headers={
"Authorization": f"Bearer {os.environ['VELONA_API_KEY']}",
"Content-Type": "application/json"
},
json={
"model": model,
"turns": turns
},
timeout=120
)
response.raise_for_status()
return response.json()
Now create the issue analysis function:
def analyze_issue(issue_text):
turns = [
{
"role": "system",
"content": (
"You are a software issue analyst. "
"Understand the GitHub issue and produce a concise "
"implementation plan. Identify the likely type of "
"change, relevant areas of the repository, possible "
"risks, and tests that should be added or updated."
)
},
{
"role": "user",
"content": issue_text
}
]
result = call_velona(
"deepseek/deepseek-v4-flash",
turns
)
return result["data"]["output"]
The result might look like:
Issue type:
Bug fix
Likely area:
Authentication API
Plan:
1. Find the login endpoint.
2. Inspect request validation.
3. Add validation for the email field.
4. Return a 400 response for invalid input.
5. Add a regression test for a missing email.
Cloning the Repository
The coding agent needs to inspect the actual source code before making a change.
We can clone the repository into a temporary working directory.
import subprocess
import tempfile
from pathlib import Path
def clone_repository(repo_url):
directory = Path(
tempfile.mkdtemp(prefix="ai-coding-agent-")
)
subprocess.run(
[
"git",
"clone",
"--depth",
"1",
repo_url,
str(directory)
],
check=True
)
return directory
Using a temporary directory keeps the agent's working copy separate from the application itself.
Inspecting the Repository
Do not send the entire repository to the LLM immediately.
Large repositories can contain thousands of files, generated assets, dependencies, binaries, and documentation that are irrelevant to the issue.
Start by collecting a simple file listing.
def list_source_files(root):
files = []
for path in root.rglob("*"):
if not path.is_file():
continue
if ".git" in path.parts:
continue
files.append(
str(path.relative_to(root))
)
return files
You can then provide the agent with the repository structure and ask it to identify which files are relevant to the issue.
Finding Relevant Files
Use a separate model call to narrow the repository down.
def find_relevant_files(issue, file_list):
file_text = "\n".join(file_list)
turns = [
{
"role": "system",
"content": (
"You are a repository navigation assistant. "
"Given a GitHub issue and a file tree, identify "
"the most relevant source files to inspect. "
"Do not invent files."
)
},
{
"role": "user",
"content": (
f"Issue:\n{issue}\n\n"
f"Repository files:\n{file_text}"
)
}
]
result = call_velona(
"deepseek/deepseek-v4-flash",
turns
)
return result["data"]["output"]
The agent can then read only the relevant files instead of filling its context with the entire repository.
Choosing the Coding Model
Once the agent knows which files matter, it can send the relevant source code to a stronger coding model.
Kimi K2.6 is a useful candidate for this stage because Velona currently lists it with a 262,144 token context window and a coding index of 61.8 from Artificial Analysis.
DeepSeek V4 Flash is also an interesting option when cost is the primary concern. Velona currently lists its coding index at 69.1 while its input price is only ₹8.32 per million tokens.
This is exactly why benchmarking matters. The cheapest model is not automatically the best model for every coding task.
Velona's coding model rankings provide current coding benchmarks and INR pricing that can be used when choosing the model for your own workload.
Giving the Agent the Relevant Source Code
Create a helper that reads selected files:
def read_files(root, filenames):
parts = []
for filename in filenames:
path = root / filename
if not path.exists():
continue
content = path.read_text(
encoding="utf-8",
errors="ignore"
)
parts.append(
f"\n--- {filename} ---\n"
f"{content}"
)
return "\n".join(parts)
The agent can now receive the issue, the implementation plan, and the relevant source files.
Generating the Fix
The coding model should be given a very specific instruction.
It should not rewrite unrelated files or make broad changes simply because they are possible.
def generate_fix(issue, plan, source_code):
turns = [
{
"role": "system",
"content": (
"You are a senior software engineer working inside "
"a GitHub repository. Implement the requested issue "
"with the smallest safe change. Preserve existing "
"project conventions. Do not modify unrelated files. "
"Add or update tests when appropriate."
)
},
{
"role": "user",
"content": (
f"Issue:\n{issue}\n\n"
f"Plan:\n{plan}\n\n"
f"Relevant source code:\n{source_code}"
)
}
]
result = call_velona(
"moonshotai/kimi-k2.6",
turns
)
return result["data"]["output"]
At this point the model has generated a proposed solution, but we still should not trust the output blindly.
Why the Agent Should Edit Files Locally
One of the most important parts of a coding agent is verification.
Generating a code block and declaring the issue fixed is not enough.
The agent should apply the change to a temporary working copy and run the project's own tests.
This is where the local Git repository becomes useful.
In a production implementation, the model should return structured file changes rather than unrestricted shell commands. Your Python application can validate those changes, write them to the working directory, and reject unexpected paths.
A safe change format can look like this:
{
"changes": [
{
"file": "src/auth/login.py",
"action": "modify",
"content": "..."
}
]
}
Before writing a file, validate that its path stays inside the repository directory and that the agent is not attempting to modify sensitive files such as .env, SSH keys, or deployment credentials.
Running the Tests
After applying the generated changes, the agent should run the repository's test suite.
For a Python project, the command might be:
pytest
For a Node.js project, it might be:
npm test
Do not assume that every repository uses the same command.
A better implementation reads the project's existing documentation and configuration files to determine the appropriate test command.
For example, a Python project may define its testing configuration in pyproject.toml, while a Node.js project may define test scripts inside package.json.
Giving Test Failures Back to the Coding Agent
If the tests fail, the agent can receive the failure output and attempt another fix.
def build_retry_prompt(issue, changes, test_output):
return f"""
The following GitHub issue is being fixed:
{issue}
The current implementation produced these changes:
{changes}
The tests failed with this output:
{test_output}
Identify the cause of the failure and propose the
smallest correction required to make the tests pass.
""".strip()
This creates a controlled feedback loop:
Generate fix
↓
Run tests
↓
Tests pass?
├── Yes → Continue
└── No
↓
Read failure
↓
Fix problem
↓
Run tests again
Set a maximum number of attempts. An agent should not be allowed to repeatedly modify a repository without a limit.
Adding a Maximum Retry Limit
MAX_ATTEMPTS = 3
for attempt in range(MAX_ATTEMPTS):
apply_changes()
test_output = run_tests()
if test_output.success:
break
send_failure_to_model(test_output.output)
Three attempts is only an example. The correct limit depends on the repository and the type of issue being handled.
Reviewing the Final Diff
Even when the tests pass, the agent should inspect the final Git diff.
Passing tests do not prove that the implementation is correct.
A model can make an unnecessary change, add dead code, weaken validation, or modify a file that was unrelated to the issue while still producing a green test suite.
Get the final diff:
def get_diff(root):
result = subprocess.run(
["git", "diff"],
cwd=root,
capture_output=True,
text=True,
check=True
)
return result.stdout
Then send the diff and test output to a review model.
def review_change(issue, diff, test_output):
turns = [
{
"role": "system",
"content": (
"You are reviewing a code change generated by an "
"AI coding agent. Check correctness, security, "
"unnecessary changes, regressions, and whether "
"the original issue has actually been addressed."
)
},
{
"role": "user",
"content": (
f"Issue:\n{issue}\n\n"
f"Diff:\n{diff}\n\n"
f"Test output:\n{test_output}"
)
}
]
result = call_velona(
"deepseek/deepseek-v4-flash",
turns
)
return result["data"]["output"]
Creating the Git Branch
Once the change passes the validation and review stages, create a dedicated branch.
def create_branch(root, branch_name):
subprocess.run(
["git", "checkout", "-b", branch_name],
cwd=root,
check=True
)
A useful branch name might be:
ai-fix/issue-42-login-validation
The branch should be based on the repository's default branch and should contain only the changes related to the issue.
Committing the Generated Fix
Before committing, inspect the diff one more time.
def commit_changes(root, message):
subprocess.run(
["git", "add", "."],
cwd=root,
check=True
)
subprocess.run(
["git", "commit", "-m", message],
cwd=root,
check=True
)
Do not allow the agent to commit secrets, credentials, or generated files that are unrelated to the issue.
Pushing the Branch
The branch can then be pushed to GitHub.
def push_branch(root, branch_name):
subprocess.run(
["git", "push", "origin", branch_name],
cwd=root,
check=True
)
For production systems, use a dedicated GitHub identity or GitHub App with the minimum repository permissions required for the workflow.
Creating the Pull Request
GitHub provides a REST API endpoint for creating pull requests from one branch into another.
We can create the pull request after the branch has been pushed.
def create_pull_request(
owner,
repo,
title,
body,
head,
base
):
url = (
f"{GITHUB_API}/repos/"
f"{owner}/{repo}/pulls"
)
response = httpx.post(
url,
headers=HEADERS,
json={
"title": title,
"body": body,
"head": head,
"base": base
},
timeout=30
)
response.raise_for_status()
return response.json()
The pull request body should explain what the agent changed and how the change was tested.
body = f"""
## Summary
This pull request was generated by the AI coding agent
from GitHub issue #{issue_number}.
## Changes
{summary}
## Tests
{test_output}
## AI Review
{review}
""".strip()
The resulting pull request becomes the human review point for the generated change.
Linking the Pull Request to the Issue
GitHub can automatically link a pull request to an issue when the pull request body uses a closing keyword such as Closes #42.
For an AI coding agent, it is safer to use this only when the agent is confident that the pull request completely addresses the issue.
Otherwise, use a simple reference such as:
Related to #42
This prevents an AI-generated pull request from automatically closing an issue when the implementation still requires human review.
Running the Complete Workflow
The high-level Python workflow now looks like this:
def process_issue(owner, repo, issue_number):
issue = get_issue(
owner,
repo,
issue_number
)
issue_text = format_issue(issue)
plan = analyze_issue(issue_text)
repo_url = issue["repository_url"]
repository = clone_repository(
get_clone_url(owner, repo)
)
files = list_source_files(repository)
relevant_files = find_relevant_files(
issue_text,
files
)
source_code = read_files(
repository,
extract_filenames(relevant_files)
)
changes = generate_fix(
issue_text,
plan,
source_code
)
apply_agent_changes(
repository,
changes
)
test_output = run_tests(
repository
)
review = review_change(
issue_text,
get_diff(repository),
test_output
)
create_branch(
repository,
f"ai-fix/issue-{issue_number}"
)
commit_changes(
repository,
f"Fix issue #{issue_number}"
)
push_branch(
repository,
f"ai-fix/issue-{issue_number}"
)
return create_pull_request(
owner,
repo,
f"AI fix for issue #{issue_number}",
build_pr_body(
issue,
review,
test_output
),
f"ai-fix/issue-{issue_number}",
get_default_branch(owner, repo)
)
The helper functions in this example are intentionally separated. In a real project, each stage should have its own validation and error handling.
Keeping the Agent Safe
An AI coding agent has access to source code and potentially to the ability to execute commands. That makes safety one of the most important parts of the architecture.
- Use a temporary working directory: Keep generated changes isolated from the machine running the agent.
- Never expose secrets to the model: Do not send
.envfiles, credentials, private keys, or deployment secrets in prompts. - Restrict file writes: Allow the agent to modify only files inside the repository workspace.
- Limit shell commands: Do not allow arbitrary model-generated shell commands to execute without validation.
- Limit retries: Stop the agent after a fixed number of failed attempts.
- Use a separate branch: Never let the agent modify the default branch directly.
- Require human review: Treat the generated pull request as a proposal rather than an automatically trusted change.
GitHub also recommends keeping credentials out of repositories and provides fine-grained permissions for repository operations. Use the smallest set of permissions that your agent actually needs.
Using GitHub Actions Instead of a Permanent Server
You do not necessarily need to run the coding agent on a server all the time.
A GitHub Actions workflow can start the agent when an issue is opened or when a maintainer adds a specific label.
For example, you could use a label such as:
ai-fix
The workflow can then pass the repository and issue information to the Python agent.
name: AI Issue Fixer
on:
issues:
types:
- labeled
jobs:
fix:
if: github.event.label.name == 'ai-fix'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install httpx python-dotenv
- name: Run AI coding agent
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VELONA_API_KEY: ${{ secrets.VELONA_API_KEY }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: python agent.py
This creates a simple workflow where a maintainer controls when the agent is allowed to work on an issue.
Why a Label Is Better Than Automatic Issue Fixing
Not every GitHub issue is suitable for an AI coding agent.
An issue might contain an incomplete bug report, a feature request that requires product decisions, or a security problem that should be handled manually.
A label gives the development team a simple approval mechanism.
New Issue
↓
Developer checks issue
↓
Add "ai-fix"
↓
Agent starts
↓
Generate branch
↓
Run tests
↓
Open PR
↓
Human review
This keeps the automation useful without pretending that every issue should be solved autonomously.
Making the Coding Agent Cheaper
The agent can make several model calls for one issue, so model selection has a direct effect on the cost.
A practical setup could look like this:
| Agent stage | Model | Reason |
|---|---|---|
| Issue analysis | DeepSeek V4 Flash | Low-cost issue classification |
| Repository navigation | DeepSeek V4 Flash | Low-cost file selection |
| Code implementation | Kimi K2.6 | Stronger coding and agentic capabilities |
| Test failure analysis | Kimi K2.6 | Useful for debugging difficult failures |
| Final review | DeepSeek V4 Flash | Low-cost review pass |
For particularly difficult issues, the agent can escalate to a more capable model instead of using that model for every issue.
Velona's current coding model rankings show that model selection is not simply a choice between the cheapest and most expensive model. DeepSeek V4 Flash currently has a 69.1 coding index at a very low price, while Kimi K2.6 has a 61.8 coding index with a higher cost and a larger context window. The right choice depends on the actual workload.
Measuring the Agent
Before calling the agent production-ready, measure how it performs on real issues.
Track:
- Issues attempted
- Pull requests created
- Tests passing on the first attempt
- Average number of repair attempts
- Human review rejection rate
- Total input tokens
- Total output tokens
- Average cost per issue
- Average time per issue
Token usage is especially important when building a coding agent because repository files and test output can create large prompts.
Velona's gateway provides usage information with inference responses, allowing you to track the cost of each stage of the workflow.
Do Not Give the Agent the Entire Repository
One of the easiest ways to make a coding agent expensive is to send the entire repository to the model for every task.
A better workflow is:
Issue
↓
Repository tree
↓
Relevant files
↓
Relevant functions
↓
Focused context
↓
Coding model
This reduces token usage and usually gives the model a clearer view of the actual problem.
It also makes the agent easier to reason about because every model call has a specific purpose.
What We Built
We started with a normal GitHub issue and turned it into an automated development workflow.
GitHub Issue
↓
AI analysis
↓
Repository search
↓
Implementation plan
↓
Code generation
↓
Local tests
↓
AI review
↓
Git branch
↓
Pull request
↓
Human review
The important difference from a basic AI code generator is that the agent operates inside an actual software development workflow.
It does not just produce a code snippet. It works with the repository, modifies files, runs tests, examines the resulting diff, and prepares a pull request.
Conclusion
AI coding agents become much more useful when they are connected to the tools developers already use.
A GitHub issue provides the task, the repository provides the context, Git provides an isolated workspace, the test suite provides a verification step, and the pull request provides a human review point.
In this tutorial, we built the architecture for an AI coding agent that can read a GitHub issue, inspect the repository, generate a fix, run tests, review the resulting changes, and open a pull request.
Using multiple models also makes the workflow more economical. Lightweight tasks can use inexpensive models, while coding and difficult debugging can be routed to models with stronger coding capabilities.
The final step should always remain human review. Passing tests are useful evidence, but they are not proof that an AI-generated change is correct or appropriate for a production codebase.
You can explore the current AI coding model rankings, compare live INR prices on the Velona Pricing Index, or follow the Velona API documentation to start building your own AI coding agent.