Try Chat Free Docs Pricing AI Analysis Repo Insights Free Tools About Blog
Sign in Get started free
< Back to blog
Tutorial Velona Team ·2 September 2026 ·19 min read

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.

  1. Read the GitHub issue.
  2. Understand what the issue is asking for.
  3. Search the repository for relevant code.
  4. Inspect the surrounding implementation.
  5. Write a change.
  6. Run tests.
  7. Fix any failures.
  8. Review the final diff.
  9. 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:

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.

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:

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.

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:

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.