AI Codingツールの分類
最近、生成AIを利用したコーディングツールは色々とあります。私は大別すると2種類あると思っています。
- VSCodeベース
- Plugin
- Github Copilot
- Cline
- RooCode
- 非Plugin
- Windsurf
- Cursor
- Plugin
- 非VSCodeベース
- 非OSS
- Devin
- Manus
- OSS
- OpenHands
- Plandex
- Codex CLI
- 非OSS
初期の生成AIのコーディングにおいては、GithubCopilotがVSCodeのプラグインとして提供され、とても賢いコードの補完といった感じでした。
しかし、2025年ぐらいからVSCodeにプラグインを追加する形のClineやRooCodeにより、コーディング自体を生成AIが自動で行うものが増えてきました。そのような中で、VSCode自体を改造するWindsurf/Cursorというタイプも出てきました。
その後、AI Coding自体をSaaS化してしまい、自然言語で仕様を指示すると、コード自体を生成するDevin/Manusが話題になりました。
そして、それらのSaaS型のAI Coding自体をOSSとして模倣する動きもあり、その1つがOpenHandsである。と認識しています(OpenHandsはもともとOpenDevinという名前でした)。OpenHandsでも割と複雑なコードを扱えるのですが、それ以上に複雑なコードが扱えるプロダクトとして出てきたのがPlandexです。
先述した通りAI CodingはVSCodeのプラグインとして提供されるものが多かったのですが、最近はCLIベースのツールも増えており、Claude CodeやCodex CLIなども出てきています。
OpenHands Resolverとは
OpenHands Resolverとは、GithubのIssueにIssueを作るとOpenHands(AI)が自動でその内容を読み取り、プルリクエストを作成してくれる機能です。
私がコーディングしているように見えるのはGithubのPATトークンを用いて動作する仕様のためです。
実際に作られたPull Reqeustはこちらです。
このOpenHands Issue ResolverはGoogle Geminiの無料枠で完全無料で動作させることができます。
パブリックなリポジトリだけでなく、プライベートなリポジトリでも動作可能です。
OpenHands Resolverの設定方法
OpenHands Resolverの設定は3ステップです。
- Github Actionsにワークフローを追加
- Gemini APIトークンの設定
- Github PATトークンの設定
Github Actionsの設定
自分のリポジトリーに.github/workflows/openhands-resolver.yml
を作成します。
.github/workflows/openhands-resolver.yml
name: Auto-Fix Tagged Issue with OpenHands
on:
workflow_call:
inputs:
max_iterations:
required: false
type: number
default: 50
macro:
required: false
type: string
default: "@openhands-agent"
target_branch:
required: false
type: string
default: "main"
description: "Target branch to pull and create PR against"
pr_type:
required: false
type: string
default: "draft"
description: "The PR type that is going to be created (draft, ready)"
LLM_MODEL:
required: false
type: string
default: "anthropic/claude-sonnet-4-20250514"
LLM_API_VERSION:
required: false
type: string
default: ""
base_container_image:
required: false
type: string
default: ""
description: "Custom sandbox env"
runner:
required: false
type: string
default: "ubuntu-latest"
secrets:
LLM_MODEL:
required: false
LLM_API_KEY:
required: true
LLM_BASE_URL:
required: false
PAT_TOKEN:
required: false
PAT_USERNAME:
required: false
issues:
types: [labeled]
pull_request:
types: [labeled]
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
permissions:
contents: write
pull-requests: write
issues: write
jobs:
auto-fix:
if: |
github.event_name == 'workflow_call' ||
github.event.label.name == 'fix-me' ||
github.event.label.name == 'fix-me-experimental' ||
(
((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
contains(github.event.comment.body, inputs.macro || '@openhands-agent') &&
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER')
) ||
(github.event_name == 'pull_request_review' &&
contains(github.event.review.body, inputs.macro || '@openhands-agent') &&
(github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'COLLABORATOR' || github.event.review.author_association == 'MEMBER')
)
)
runs-on: "${{ inputs.runner || 'ubuntu-latest' }}"
env:
LLM_NUM_RETRIES: 1000
LLM_RETRY_MIN_WAIT: 60
LLM_RETRY_MAX_WAIT: 3000
LLM_TIMEOUT: 300
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Upgrade pip
run: |
python -m pip install --upgrade pip
- name: Get latest versions and create requirements.txt
run: |
python -m pip index versions openhands-ai > openhands_versions.txt
OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()')
# Create a new requirements.txt locally within the workflow, ensuring no reference to the repo's file
echo "openhands-ai==${OPENHANDS_VERSION}" > /tmp/requirements.txt
cat /tmp/requirements.txt
- name: Cache pip dependencies
if: |
!(
github.event.label.name == 'fix-me-experimental' ||
(
(github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
contains(github.event.comment.body, '@openhands-agent-exp')
) ||
(
github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@openhands-agent-exp')
)
)
uses: actions/cache@v4
with:
path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/*
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }}
- name: Check required environment variables
env:
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }}
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
PAT_USERNAME: ${{ secrets.PAT_USERNAME }}
GITHUB_TOKEN: ${{ github.token }}
run: |
required_vars=("LLM_API_KEY")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
echo "Error: Required environment variable $var is not set."
exit 1
fi
done
# Check optional variables and warn about fallbacks
if [ -z "$LLM_BASE_URL" ]; then
echo "Warning: LLM_BASE_URL is not set, will use default API endpoint"
fi
if [ -z "$PAT_TOKEN" ]; then
echo "Warning: PAT_TOKEN is not set, falling back to GITHUB_TOKEN"
fi
if [ -z "$PAT_USERNAME" ]; then
echo "Warning: PAT_USERNAME is not set, will use openhands-agent"
fi
- name: Set environment variables
env:
REVIEW_BODY: ${{ github.event.review.body || '' }}
run: |
# Handle pull request events first
if [ -n "${{ github.event.pull_request.number }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
# Handle pull request review events
elif [ -n "$REVIEW_BODY" ]; then
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
# Handle issue comment events that reference a PR
elif [ -n "${{ github.event.issue.pull_request }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
# Handle regular issue events
else
echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=issue" >> $GITHUB_ENV
fi
if [ -n "$REVIEW_BODY" ]; then
echo "COMMENT_ID=${{ github.event.review.id || 'None' }}" >> $GITHUB_ENV
else
echo "COMMENT_ID=${{ github.event.comment.id || 'None' }}" >> $GITHUB_ENV
fi
echo "MAX_ITERATIONS=${{ inputs.max_iterations || 50 }}" >> $GITHUB_ENV
echo "SANDBOX_ENV_GITHUB_TOKEN=${{ secrets.PAT_TOKEN || github.token }}" >> $GITHUB_ENV
echo "SANDBOX_BASE_CONTAINER_IMAGE=${{ inputs.base_container_image }}" >> $GITHUB_ENV
# Set branch variables
echo "TARGET_BRANCH=${{ inputs.target_branch || 'main' }}" >> $GITHUB_ENV
- name: Comment on issue with start message
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PAT_TOKEN || github.token }}
script: |
const issueType = process.env.ISSUE_TYPE;
github.rest.issues.createComment({
issue_number: ${{ env.ISSUE_NUMBER }},
owner: context.repo.owner,
repo: context.repo.repo,
body: `[OpenHands](https://github.com/All-Hands-AI/OpenHands) started fixing the ${issueType}! You can monitor the progress [here](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`
});
- name: Install OpenHands
id: install_openhands
uses: actions/github-script@v7
env:
COMMENT_BODY: ${{ github.event.comment.body || '' }}
REVIEW_BODY: ${{ github.event.review.body || '' }}
LABEL_NAME: ${{ github.event.label.name || '' }}
EVENT_NAME: ${{ github.event_name }}
with:
script: |
const commentBody = process.env.COMMENT_BODY.trim();
const reviewBody = process.env.REVIEW_BODY.trim();
const labelName = process.env.LABEL_NAME.trim();
const eventName = process.env.EVENT_NAME.trim();
// Check conditions
const isExperimentalLabel = labelName === "fix-me-experimental";
const isIssueCommentExperimental =
(eventName === "issue_comment" || eventName === "pull_request_review_comment") &&
commentBody.includes("@openhands-agent-exp");
const isReviewCommentExperimental =
eventName === "pull_request_review" && reviewBody.includes("@openhands-agent-exp");
// Set output variable
core.setOutput('isExperimental', isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental);
// Perform package installation
if (isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental) {
console.log("Installing experimental OpenHands...");
await exec.exec("pip install git+https://github.com/all-hands-ai/openhands.git");
} else {
console.log("Installing from requirements.txt...");
await exec.exec("pip install -r /tmp/requirements.txt");
}
- name: Attempt to resolve issue
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || github.token }}
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
GIT_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }}
PYTHONPATH: ""
run: |
cd /tmp && python -m openhands.resolver.resolve_issue \
--selected-repo ${{ github.repository }} \
--issue-number ${{ env.ISSUE_NUMBER }} \
--issue-type ${{ env.ISSUE_TYPE }} \
--max-iterations ${{ env.MAX_ITERATIONS }} \
--comment-id ${{ env.COMMENT_ID }} \
--is-experimental ${{ steps.install_openhands.outputs.isExperimental }}
- name: Check resolution result
id: check_result
run: |
if cd /tmp && grep -q '"success":true' output/output.jsonl; then
echo "RESOLUTION_SUCCESS=true" >> $GITHUB_OUTPUT
else
echo "RESOLUTION_SUCCESS=false" >> $GITHUB_OUTPUT
fi
- name: Upload output.jsonl as artifact
uses: actions/upload-artifact@v4
if: always() # Upload even if the previous steps fail
with:
name: resolver-output
path: /tmp/output/output.jsonl
retention-days: 30 # Keep the artifact for 30 days
- name: Create draft PR or push branch
if: always() # Create PR or branch even if the previous steps fail
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || github.token }}
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
GIT_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }}
PYTHONPATH: ""
run: |
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--target-branch ${{ env.TARGET_BRANCH }} \
--pr-type ${{ inputs.pr_type || 'draft' }} \
--reviewer ${{ github.actor }} | tee pr_result.txt && \
grep "PR created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
else
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--pr-type branch \
--send-on-failure | tee branch_result.txt && \
grep "branch created" branch_result.txt | sed 's/.*\///g; s/.expand=1//g' > branch_name.txt
fi
# Step leaves comment for when agent is invoked on PR
- name: Analyze Push Logs (Updated PR or No Changes) # Skip comment if PR update was successful OR leave comment if the agent made no code changes
uses: actions/github-script@v7
if: always()
env:
AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }}
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
with:
github-token: ${{ secrets.PAT_TOKEN || github.token }}
script: |
const fs = require('fs');
const issueNumber = process.env.ISSUE_NUMBER;
let logContent = '';
try {
logContent = fs.readFileSync('/tmp/pr_result.txt', 'utf8').trim();
} catch (error) {
console.error('Error reading pr_result.txt file:', error);
}
const noChangesMessage = `No changes to commit for issue #${issueNumber}. Skipping commit.`;
// Check logs from send_pull_request.py (pushes code to GitHub)
if (logContent.includes("Updated pull request")) {
console.log("Updated pull request found. Skipping comment.");
process.env.AGENT_RESPONDED = 'true';
} else if (logContent.includes(noChangesMessage)) {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `The workflow to fix this issue encountered an error. Openhands failed to create any code changes.`
});
process.env.AGENT_RESPONDED = 'true';
}
# Step leaves comment for when agent is invoked on issue
- name: Comment on issue # Comment link to either PR or branch created by agent
uses: actions/github-script@v7
if: always() # Comment on issue even if the previous steps fail
env:
AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }}
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
RESOLUTION_SUCCESS: ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}
with:
github-token: ${{ secrets.PAT_TOKEN || github.token }}
script: |
const fs = require('fs');
const path = require('path');
const issueNumber = process.env.ISSUE_NUMBER;
const success = process.env.RESOLUTION_SUCCESS === 'true';
let prNumber = '';
let branchName = '';
let resultExplanation = '';
try {
if (success) {
prNumber = fs.readFileSync('/tmp/pr_number.txt', 'utf8').trim();
} else {
branchName = fs.readFileSync('/tmp/branch_name.txt', 'utf8').trim();
}
} catch (error) {
console.error('Error reading file:', error);
}
try {
if (!success){
// Read result_explanation from JSON file for failed resolution
const outputFilePath = path.resolve('/tmp/output/output.jsonl');
if (fs.existsSync(outputFilePath)) {
const outputContent = fs.readFileSync(outputFilePath, 'utf8');
const jsonLines = outputContent.split('\n').filter(line => line.trim() !== '');
if (jsonLines.length > 0) {
// First entry in JSON lines has the key 'result_explanation'
const firstEntry = JSON.parse(jsonLines[0]);
resultExplanation = firstEntry.result_explanation || '';
}
}
}
} catch (error){
console.error('Error reading file:', error);
}
// Check "success" log from resolver output
if (success && prNumber) {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `A potential fix has been generated and a draft PR #${prNumber} has been created. Please review the changes.`
});
process.env.AGENT_RESPONDED = 'true';
} else if (!success && branchName) {
let commentBody = `An attempt was made to automatically fix this issue, but it was unsuccessful. A branch named '${branchName}' has been created with the attempted changes. You can view the branch [here](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${branchName}). Manual intervention may be required.`;
if (resultExplanation) {
commentBody += `\n\nAdditional details about the failure:\n${resultExplanation}`;
}
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
});
process.env.AGENT_RESPONDED = 'true';
}
# Leave error comment when both PR/Issue comment handling fail
- name: Fallback Error Comment
uses: actions/github-script@v7
if: ${{ env.AGENT_RESPONDED == 'false' }} # Only run if no conditions were met in previous steps
env:
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
with:
github-token: ${{ secrets.PAT_TOKEN || github.token }}
script: |
const issueNumber = process.env.ISSUE_NUMBER;
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `The workflow to fix this issue encountered an error. Please check the [workflow logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for more information.`
});
ほぼ内容自体は公式のworkflowと同じものです。
ただし、Geminiの無料枠で動かすために工夫をしています(後述)。
Gemini API トークンの生成・設定
OpenHands Issue Resolverを動かすためには、4つのSecretsを設定する必要があります。
LLM_API_KEY
として、GoogleのGeminiのAPIキーを設定します。
これは以下のURLで発行します。Geminiは無料枠があるので、軽く触るにはこれで十分です。
次に、LLM_MODEL
にモデル名を指定します。オプションで指定できるモデルはGoogleのドキュメントを参考にします。
設定の仕方としては、gemini-2.0-flash
のような値を指定します。
Github PAT トークンの生成・設定
GithubのPATトークンの設定が若干ややこしいです。
以下のURLでGithubのPersonal Access Tokenを生成します。
一番ややこしいのがここです。「Only select repositories」を指定する必要があります。
Publicなリポジトリだからと言って、「Public repositories」を設定すると動作しません。「All Repositories」でも多分動きますが、セキュリティ的にはリポジトリを絞ったほうがいいと思います。
そのあと、「Repository permissions」を設定します。OpenHandsに何をやってもらいたいか。次第ではありますが、結構選択する必要があります。
私は以下のあたりを設定しています。
設定項目 | 権限 |
---|---|
Actions | Read-Write |
Commit statuses | Read-Write |
Contents | Read-Write |
Discussions | Read-Write |
Environments | Read |
Issues | Read-Write |
Pull requests | Read-Write |
Secrets | Read |
Variables | Read |
Webhooks | Read-Write |
Workflows | Read-Write |
気持ちとしては、「Contents」「Actions」「Issues」「Pull Request」「Workflows」あたりの権限を与えれば最小限何とかなると思っていますが、そこまで完全な検証はしていないです。
ここで生成したPATトークンをSecretsのPAT_TOKEN
に設定します。また、PAT_USERNAME
には自分のGithubでのユーザー名を指定します。
OpenHandsにGithub上でIssueを解決させる
使い方は簡単です。GithubでIssueを作成します。そして、fix-me
というラベルを付けると自動でOpenHandsがGithub Actions上でコーディングをし始めます。
実際の動作ログ
OpenHandsにPRを修正させる
OpenHandsのResolverにはPRのコメントをトリガーに修正をする機能があります。
一般的なレビューと同じようにソースコードにコメントを残します。その時に、@openhand-agent
とメンションを付けます。
「Start a review」でも「Add single comment」どちらでも構いません。
そうすることで、OpenHandsがコードの修正を行います。
OpenHands Resolverの設定におけるポイント
ただし、Geminiの無料枠で動かすために工夫をしています(後述)。
と書きました。OpenHands公式のworkflowをコピペで済むのですが、以下のようなコードを追加しています。
env:
LLM_NUM_RETRIES: 1000
LLM_RETRY_MIN_WAIT: 60
LLM_RETRY_MAX_WAIT: 3000
LLM_TIMEOUT: 300
Google Geminiの無料枠は上限があります。そのため、すぐに上限が来てしまいます。その際、リトライするのですが、そのリトライの上限が低いため、自分で上限をあげてやる必要があります。そうすることで、Geminiの無料枠でも利用することが可能になります。そのため、公式のworkflowが変更され、自分のリポジトリに持ってくる際は、これを書く必要があります。
OpenHandsへのコントリビュート
先日、私がOpenHandsのコントリビューターとして名前が掲載されました。
OpenHandsのNew Contributorsとして名前がのったよ!! https://t.co/7qVSHjvNsv pic.twitter.com/spWi4QdOkY
— こたうち さんさん (@kotauchisunsun) May 15, 2025
何をしたか。というと、前節の 「リトライの上限を変更する機能」を私が実装しました。
OpenHandsのドキュメントには、LLM_NUM_RETRIES
など、リトライに関する環境変数を設定する機能の記載があります。
しかし、いくら設定してもResolverでは動作しないと困っていました。結果、ソースコードを読むと、設定読み込みルーチンがOpenHands本体とResolverで共通化されておらず、「リトライの上限が変更できない」という状態でした。
というわけでパワー(やや強引)で設定できるようにPRを作り、マージされたのが先ほどの話でした。
OSSへの貢献というのは初めてなのですが、意外になんとかなりました。
Devinが割とうらやましいなーと思って、色々とOSSを触っていましたが、その折に見つけたのがOpenHandsでした。それで、Resolverの記事を見つけ、自分で設定したのですが、一部動かない。なんか半分ぐらい動かない?と思って追ったところ、リトライ上限が反映されないという挙動でした。
そんなわけでResolverは、最初あまり使えないなーという感想だったのですが、リトライの上限が反映されるようになって、かなり安定的に動くようになりました。RooCodeはパソコンの前に座っている必要がありますが、これはIssue投げるだけで、GithubActions上で勝手に解決してくれるので、とりあえずお願い!といってIssueだけ作って、運試しで投げておく。しかも無料で。ということで、軽微な修正ならこれで十分だったりします。加えてCIなど自動テストを整えるとかなり強くなってきます。
なので、比較的お手軽に無料でセットアップ出来るのでOpenHandsのResolverを試してみるのはおススメです。
Github Sponsor