dev-orchestra に自作のエージェントを繋いでみた(ローカルLLM編) Connecting My Own Agent to dev-orchestra (Local LLM Edition)
はじめに
前回は、dev-orchestra のレビュアーとロールの組み替え方を書きました。 今回は、Claude Code・Codex・Antigravity 以外の AI を、自分で書いたアダプタで繋ぐ方法です。例として、手元で動かしているローカル LLM をレビュアーに加えます。
ローカル LLM を繋ぐ話は、これまでにも 2 本書いています。
- ローカルLLMをdev-orchestraのコードレビュアーに追加してみた: 繋ぐまでの試行錯誤と、0.9.0 で公式の仕組みが入った経緯
- 機密コードをクラウドに出さずにAIでレビューする: ローカル LLM だけでレビューする運用
この記事では経緯は省いて、dev-orchestra 1.0.0 で一からアダプタを書く手順に絞ります。
dev-orchestra は GitHub で公開しています(MIT ライセンス)。役に立ちそうと思ったら、スターを付けてもらえるとうれしいです。
仕組み:アダプタは「コマンドを組み立てる」だけ
dev-orchestra は、AI を直接呼びません。CLI を起動して、プロンプトを標準入力で渡し、答えを標準出力から読むだけです。 その CLI のコマンドを組み立てるのがアダプタ(provider)です。
| 部品 | 役目 |
|---|---|
| dev-orchestra | プロンプトを作り、アダプタが返したコマンドを起動する |
| アダプタ | 「どのコマンドを、どの引数で起動するか」を返す |
| CLI | 標準入力のプロンプトを AI に渡し、答えを標準出力に書く |
つまり、標準入力で受けて標準出力で返す CLI さえあれば、どんな AI でも繋げます。
手順
1. プロンプトを受けて答えを返す CLI を用意する
ローカル LLM(llama.cpp の llama-server など)は、OpenAI 互換の API を出しています。そこにプロンプトを投げて、答えを出力するだけの CLI を書きます。標準ライブラリだけで書けます。
"""Send a prompt from stdin to a local OpenAI-compatible server and print the answer."""
import json
import os
import sys
import urllib.request
URL = os.environ.get("LOCAL_LLM_URL", "http://127.0.0.1:8080") + "/v1/chat/completions"
def main():
# dev-orchestra writes the prompt as UTF-8, whatever the OS locale is
sys.stdin.reconfigure(encoding="utf-8")
prompt = sys.stdin.read()
body = json.dumps({"messages": [{"role": "user", "content": prompt}]}).encode("utf-8")
request = urllib.request.Request(URL, data=body, headers={"Content-Type": "application/json"})
# Ignore proxy settings so the prompt never leaves this machine
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
with opener.open(request, timeout=1800) as response:
answer = json.load(response)["choices"][0]["message"]["content"]
sys.stdout.reconfigure(encoding="utf-8")
sys.stdout.write(answer)
if __name__ == "__main__":
main()
これを local_llm_review.py として、好きな場所に保存しておきます。
この CLI で気をつけたのは 2 つです。
| 書いたこと | 理由 |
|---|---|
sys.stdin.reconfigure(encoding="utf-8") | dev-orchestra はプロンプトを UTF-8 で書き込む。日本語の Windows は既定で cp932 で読むので、日本語のコメントを含む差分が化けたり、読み込みで落ちたりする |
ProxyHandler({}) で作った opener を使う | urlopen は HTTP_PROXY などの環境変数や Windows のプロキシ設定に従う。127.0.0.1 宛てでも、プロキシ経由でコードが外に出ることがある |
ローカル LLM のサーバーは、
127.0.0.1(localhost)だけで待ち受けるようにしておきます。0.0.0.0で待ち受けると、同じネットワークの誰からでも使えてしまいます。
2. アダプタを書く
次にアダプタです。dev-orchestra のドキュメントにある最小の例をもとに、ローカル LLM 用に書きました。
"""Adapter for a local LLM behind an OpenAI-compatible server. Review only."""
from __future__ import annotations
import sys
from typing import Any, Dict, List, Optional, Sequence
from orchestrator.providers.base import (
READ_ONLY_MODES,
ModelCandidate,
ModelResolutionError,
Provider,
ResolvedModel,
)
# The small CLI that sends the prompt to the local server
CLI = r"C:\path\to\local_llm_review.py"
class LocalLLMProvider(Provider):
name = "localllm"
display_name = "Local LLM"
executable = sys.executable
fallback_models = (ModelCandidate("", "default", "server default", "builtin-fallback"),)
fallback_updated = "2026-10-12"
option_keys = ("args",)
def resolve_latest(self, family: str) -> ResolvedModel:
if family in ("", "default"):
return ResolvedModel(self.name, "default", "latest", None, "server default", "cli-default")
raise ModelResolutionError("localllm: only the default model is supported, not %r" % family)
def build_command(
self,
mode: str,
resolved: ResolvedModel,
cwd: str,
extra_args: Sequence[str] = (),
options: Optional[Dict[str, Any]] = None,
) -> List[str]:
# The CLI cannot read or write files, so it can only review a diff it is given.
if mode not in READ_ONLY_MODES:
raise ModelResolutionError("localllm cannot implement: it has no file access")
return [self.executable, CLI] + self.option_args(options)
def build_provider(executable: Optional[str] = None) -> LocalLLMProvider:
return LocalLLMProvider(executable)
ポイントは 4 つです。
| ポイント | 理由 |
|---|---|
from orchestrator.providers.base import ... | このファイルはプラグインのパッケージの外から読み込まれるので、相対 import(from .base)は使えない |
name = "localllm" | 設定の provider: に書く名前。claude・codex・agy・mock は使えない |
resolve_latest は default だけ受け付ける | 確かめられないモデル名では推測せずに止める。dev-orchestra 全体の決まりに合わせる |
| 読み取り専用でない実行(実装)は断る | この CLI はファイルを読み書きできない。実装役に置かれても、何も変えずに成功したふりをしないように |
CLI には、手順 1 で保存したファイルの本当の場所に書き換えます。例の r"..." は、Windows のパスの \ をそのまま書くためのものです。macOS / Linux なら CLI = "/home/you/local_llm_review.py" のように書きます。
executable = sys.executableなので、dev-orchestra がインストールを確かめるときに動かすのはpython --versionです。CLI の側に--versionを用意する必要はありません。
3. 設定ディレクトリの providers/ に置く
アダプタは、設定ディレクトリの providers/ に置きます。
| OS | 置き場所 |
|---|---|
| Windows | %APPDATA%\dev-orchestra\providers\ |
| macOS / Linux | ~/.config/dev-orchestra/providers/ |
ファイル名は localllm.py のように、識別子として使える名前にします。
プラグインの中(
~/.claude/plugins/cache/...)に置かないでください。そこはプラグインを更新すると消えます。設定ディレクトリなら、設定ファイル(config.yaml)と同じ場所なので、更新しても消えません。
Windows で Microsoft Store 版の Python を使っていると、
%APPDATA%に置いたファイルが Python からは見えないことがあります(以前の記事で半日はまりました)。doctorがこれを見つけると、本当の置き場所と対処のしかたを表示してくれます。
4. doctor で読み込まれたか確かめる
アダプタを置いたら、何よりも先に dev-orchestra doctor を実行します(エージェントに頼んでも同じです)。こう出ました(抜粋、パスは省略)。
Local LLM (localllm)
Installed: yes
Source: user module <設定ディレクトリ>\providers\localllm.py
Model selection: supported
Models (builtin-fallback): server default
Read-only runs: not reported by this adapter
Resume: NOT SUPPORTED -- localllm does not resume sessions
User providers
Directory: <設定ディレクトリ>\providers
Code in this directory is imported at startup, from outside the plugin.
Imported: localllm <- <設定ディレクトリ>\providers\localllm.py
どのファイルから読み込んだのかまで表示されます。最後に No problems found. と出れば成功です。
書き間違いがあっても dev-orchestra 全体は止まらず、doctor がそのファイルとエラーを教えてくれます。
5. レビュアーとして登録する
レビュアーに加えます。--model default を忘れずに付けます。
dev-orchestra reviewer add --provider localllm --model default --role general
私は最初
--modelを付け忘れました。すると系列名がrecommended-codingで登録され、レビューのときに自作のアダプタがonly the default model is supported, not 'recommended-coding'と断りました。手順 2 で「確かめられない名前では止める」と書いたとおりの動きです。
6. レビューさせてみる
前回の記事で使った、バグを仕込んだサンプルで試しました。 ただし、この確認で見たいのは仕組みが通るかです。そこで、ローカル LLM の代わりに、決まった答えを返すダミーのサーバーを使いました。
ok localllm-general localllm server default 1 finding(s)
1 successful, 0 failed
自作のアダプタを通して、ダミーのサーバーの答えが返ってきました。確かめられたのは、アダプタの繋ぎ込みと、答えの書式の読み取りまでです。答えが dev-orchestra の決まった書式(## Finding …)になっていれば、ほかのレビュアーの指摘と同じように重複の統合やトリアージにかかります。
実際のローカル LLM のレビューの実力は、以前の記事(商用 2 つと比べて、6 件中 1 件が採用)や機密コードの記事に書いています。
7. 実装を任せようとすると、断ることを確かめる
念のため、実装役をこのアダプタにして実行してみました。
localllm cannot implement: it has no file access
CLI を起動する前に止まりました。うっかり実装役に置いても、「成功したのに何も変わっていない」という気づきにくい失敗にはなりません。
できる役・できない役
| 役 | このアダプタで | 理由 |
|---|---|---|
| レビュー | ○ | 差分はプロンプトで渡されるので、ファイルを読めなくても成り立つ |
| 設計 | △ | 動くが、渡されたプロンプトしか見えない。関連するファイルを自分で調べられない |
| 実装・修正 | × | ファイルを書けないので、アダプタで断っている |
Claude Code や Codex は、関連するファイルを自分で追いかけて調べられます。この CLI は渡されたものしか見えないので、別の視点を足すレビュアーとして使うのが向いています。
注意すること
providers/のファイルは、dev-orchestra を起動するたびに自分の権限で実行されます。サンドボックスではありません。自分で書いたものか、中身を読んで信頼できるものだけを置きます- ローカル LLM のサーバーは localhost だけで待ち受けます(手順 1 の注意)
- CLI の側では、プロキシを通さずに
127.0.0.1へ送ります(手順 1 のProxyHandler({}))。LOCAL_LLM_URLを変えるときも、手元のアドレスにします - コードを外に出さないつもりなら、Claude Code などのクラウドの AI から dev-orchestra を呼ばないようにします。呼び出し元の AI がコードを読むからです。ターミナルから直接実行し、
--onlyでローカル LLM に絞ります(機密コードの記事) - 自作のアダプタは、プリセットには自動で入りません。設定で名前を挙げたときだけ動きます
doctorのRead-only runs: not reported by this adapterは、このアダプタが「読み取り専用をどう守るか」を報告していないという意味です。この CLI はそもそもファイルに触れないので実害はありませんが、ファイルを触れる CLI を繋ぐときは、読み取り専用を本当に守れるかを自分で確かめる必要があります- アダプタが頼る部分(この記事の例で使っているもの)は、1.0.0 の互換性の約束に入っています。それでも、dev-orchestra を更新したら
doctorを一度実行しておくと安心です
まとめ
- dev-orchestra は CLI を起動するだけ。標準入力で受けて標準出力で返す CLI があれば、どんな AI でも繋げる
- アダプタは 50 行ほど。
orchestrator.providers.baseから import し、resolve_latestとbuild_commandを書く - 置き場所は設定ディレクトリの
providers/。プラグインを更新しても消えない - 置いたら
doctorで、どのファイルから読み込んだかを確かめる - 登録は
reviewer add --provider localllm --model default --role general - ファイルに触れない CLI はレビュー専用にして、実装はアダプタで断る
providers/のコードは自分の権限で実行される。信頼できるものだけを置く
細かな決まりは providers のドキュメント にあります。
GitHub: istb16/dev-orchestra(スターを付けてもらえると励みになります)
English version
Introduction
Last time, I wrote about rearranging reviewers and roles in dev-orchestra. This time, I connect an AI other than Claude Code, Codex, or Antigravity with an adapter I wrote myself. As an example, I add the local LLM I run on my own machine as a reviewer.
I’ve written two posts about connecting a local LLM before.
- Adding a local LLM as a dev-orchestra code reviewer (in Japanese): the trial and error of getting it connected, and how the official mechanism came in with 0.9.0
- Reviewing confidential code with AI without sending it to the cloud (in Japanese): running reviews with only a local LLM
This post skips the backstory and focuses on the steps to write an adapter from scratch in dev-orchestra 1.0.0.
dev-orchestra is open source on GitHub (MIT license). If it looks useful, a star would mean a lot.
How it works: the adapter just “builds a command”
dev-orchestra doesn’t call AIs directly. It just starts a CLI, passes the prompt on stdin, and reads the answer from stdout. The adapter (provider) is what builds the command for that CLI.
| Part | Role |
|---|---|
| dev-orchestra | Builds the prompt and starts the command the adapter returns |
| Adapter | Returns “which command to start, with which arguments” |
| CLI | Passes the prompt from stdin to the AI and writes the answer to stdout |
In other words, any AI can be connected as long as you have a CLI that reads stdin and writes stdout.
Steps
1. Prepare a CLI that takes a prompt and returns an answer
Local LLMs (such as llama.cpp’s llama-server) expose an OpenAI-compatible API. I wrote a CLI that just sends the prompt there and prints the answer. The standard library is enough.
"""Send a prompt from stdin to a local OpenAI-compatible server and print the answer."""
import json
import os
import sys
import urllib.request
URL = os.environ.get("LOCAL_LLM_URL", "http://127.0.0.1:8080") + "/v1/chat/completions"
def main():
# dev-orchestra writes the prompt as UTF-8, whatever the OS locale is
sys.stdin.reconfigure(encoding="utf-8")
prompt = sys.stdin.read()
body = json.dumps({"messages": [{"role": "user", "content": prompt}]}).encode("utf-8")
request = urllib.request.Request(URL, data=body, headers={"Content-Type": "application/json"})
# Ignore proxy settings so the prompt never leaves this machine
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
with opener.open(request, timeout=1800) as response:
answer = json.load(response)["choices"][0]["message"]["content"]
sys.stdout.reconfigure(encoding="utf-8")
sys.stdout.write(answer)
if __name__ == "__main__":
main()
Save this as local_llm_review.py wherever you like.
There were two things I was careful about in this CLI.
| What I wrote | Why |
|---|---|
sys.stdin.reconfigure(encoding="utf-8") | dev-orchestra writes the prompt as UTF-8. Japanese Windows reads cp932 by default, so diffs containing Japanese comments get garbled or crash on read |
Use an opener built with ProxyHandler({}) | urlopen follows environment variables like HTTP_PROXY and the Windows proxy settings. Even for 127.0.0.1, your code can leave the machine through a proxy |
Make the local LLM server listen only on
127.0.0.1(localhost). If it listens on0.0.0.0, anyone on the same network can use it.
2. Write the adapter
Next is the adapter. I based it on the minimal example in the dev-orchestra docs and wrote it for the local LLM.
"""Adapter for a local LLM behind an OpenAI-compatible server. Review only."""
from __future__ import annotations
import sys
from typing import Any, Dict, List, Optional, Sequence
from orchestrator.providers.base import (
READ_ONLY_MODES,
ModelCandidate,
ModelResolutionError,
Provider,
ResolvedModel,
)
# The small CLI that sends the prompt to the local server
CLI = r"C:\path\to\local_llm_review.py"
class LocalLLMProvider(Provider):
name = "localllm"
display_name = "Local LLM"
executable = sys.executable
fallback_models = (ModelCandidate("", "default", "server default", "builtin-fallback"),)
fallback_updated = "2026-10-12"
option_keys = ("args",)
def resolve_latest(self, family: str) -> ResolvedModel:
if family in ("", "default"):
return ResolvedModel(self.name, "default", "latest", None, "server default", "cli-default")
raise ModelResolutionError("localllm: only the default model is supported, not %r" % family)
def build_command(
self,
mode: str,
resolved: ResolvedModel,
cwd: str,
extra_args: Sequence[str] = (),
options: Optional[Dict[str, Any]] = None,
) -> List[str]:
# The CLI cannot read or write files, so it can only review a diff it is given.
if mode not in READ_ONLY_MODES:
raise ModelResolutionError("localllm cannot implement: it has no file access")
return [self.executable, CLI] + self.option_args(options)
def build_provider(executable: Optional[str] = None) -> LocalLLMProvider:
return LocalLLMProvider(executable)
There are four key points.
| Point | Why |
|---|---|
from orchestrator.providers.base import ... | This file is loaded from outside the plugin’s package, so relative imports (from .base) don’t work |
name = "localllm" | The name you write in provider: in the config. claude, codex, agy, and mock are not allowed |
resolve_latest accepts only default | For a model name it can’t verify, it stops instead of guessing, in line with dev-orchestra’s rule everywhere |
| Refuse runs that aren’t read-only (implementation) | This CLI can’t read or write files. Even if it’s put in the implementer role, it should never pretend to succeed without changing anything |
Change CLI to the actual location of the file you saved in step 1. The r"..." in the example lets you write the \ in Windows paths as is. On macOS / Linux, write something like CLI = "/home/you/local_llm_review.py".
Since
executable = sys.executable, what dev-orchestra runs to check the installation ispython --version. The CLI doesn’t need its own--version.
3. Put it in providers/ in the config directory
The adapter goes in providers/ in the config directory.
| OS | Location |
|---|---|
| Windows | %APPDATA%\dev-orchestra\providers\ |
| macOS / Linux | ~/.config/dev-orchestra/providers/ |
Give the file a name that works as an identifier, such as localllm.py.
Don’t put it inside the plugin (
~/.claude/plugins/cache/...). Anything there is deleted when the plugin updates. The config directory is where the config file (config.yaml) lives, so it survives updates.
On Windows, if you use the Microsoft Store version of Python, files you put in
%APPDATA%may not be visible to Python (I lost half a day to this in an earlier post (in Japanese)). Ifdoctordetects this, it shows the real location and how to fix it.
4. Check that it loaded with doctor
Once the adapter is in place, run dev-orchestra doctor before anything else (asking the agent to do it works too). Here’s what I got (excerpt, paths omitted).
Local LLM (localllm)
Installed: yes
Source: user module <config dir>\providers\localllm.py
Model selection: supported
Models (builtin-fallback): server default
Read-only runs: not reported by this adapter
Resume: NOT SUPPORTED -- localllm does not resume sessions
User providers
Directory: <config dir>\providers
Code in this directory is imported at startup, from outside the plugin.
Imported: localllm <- <config dir>\providers\localllm.py
It even shows which file it was loaded from. If it ends with No problems found., you’re done.
Even if you make a typo, dev-orchestra as a whole keeps working, and doctor tells you the file and the error.
5. Register it as a reviewer
Add it as a reviewer. Don’t forget --model default.
dev-orchestra reviewer add --provider localllm --model default --role general
At first I forgot
--model. It got registered with the family namerecommended-coding, and at review time my adapter refused withonly the default model is supported, not 'recommended-coding'. That’s exactly the “stop on names it can’t verify” behavior from step 2.
6. Have it review
I tried it on the sample with planted bugs from the previous post. But what I wanted to check here was whether the plumbing works. So instead of the local LLM, I used a dummy server that returns a fixed answer.
ok localllm-general localllm server default 1 finding(s)
1 successful, 0 failed
The dummy server’s answer came back through my adapter. What this confirmed is the adapter wiring and the parsing of the answer format. If the answer follows dev-orchestra’s format (## Finding …), it goes through deduplication and triage just like the other reviewers’ findings.
For how well a real local LLM actually reviews, see an earlier post (in Japanese) (compared with two commercial ones, 1 of 6 findings was adopted) and the confidential-code post (in Japanese).
7. Confirm it refuses when asked to implement
Just to be safe, I set this adapter as the implementer and ran it.
localllm cannot implement: it has no file access
It stopped before starting the CLI. Even if you put it in the implementer role by mistake, you won’t get the hard-to-notice failure of “it succeeded but nothing changed.”
Roles it can and can’t take
| Role | With this adapter | Why |
|---|---|---|
| Review | Yes | The diff is passed in the prompt, so it works without reading files |
| Design | Partly | It runs, but it only sees the prompt it’s given. It can’t look up related files on its own |
| Implementation / fixing | No | It can’t write files, so the adapter refuses |
Claude Code and Codex can follow and investigate related files on their own. This CLI only sees what it’s given, so it works best as a reviewer that adds another perspective.
Cautions
- Files in
providers/run with your permissions every time dev-orchestra starts. There’s no sandbox. Only put in files you wrote yourself, or ones you’ve read and trust - The local LLM server listens only on localhost (the note in step 1)
- On the CLI side, send to
127.0.0.1without going through a proxy (ProxyHandler({})in step 1). If you changeLOCAL_LLM_URL, keep it a local address too - If you mean to keep your code on your machine, don’t call dev-orchestra from a cloud AI like Claude Code, because the calling AI reads the code. Run it directly from the terminal and narrow it to the local LLM with
--only(the confidential-code post (in Japanese)) - Your own adapter is not added to presets automatically. It only runs when you name it in the config
Read-only runs: not reported by this adapterindoctormeans this adapter doesn’t report how it keeps runs read-only. This CLI never touches files, so there’s no real harm, but when you connect a CLI that can touch files, you need to check for yourself that it really stays read-only- The parts adapters rely on (the ones used in this post’s example) are covered by the 1.0.0 compatibility promise. Still, it’s reassuring to run
doctoronce after updating dev-orchestra
Summary
- dev-orchestra just starts a CLI. With a CLI that reads stdin and writes stdout, any AI can be connected
- The adapter is about 50 lines. Import from
orchestrator.providers.baseand writeresolve_latestandbuild_command - It goes in
providers/in the config directory, which survives plugin updates - Once it’s in place, use
doctorto check which file it was loaded from - Register it with
reviewer add --provider localllm --model default --role general - Make a CLI that can’t touch files review-only, and have the adapter refuse implementation
- Code in
providers/runs with your permissions. Only put in what you trust
The detailed rules are in the providers docs.
GitHub: istb16/dev-orchestra (a star is always appreciated)
関連記事 Related Posts
機密コードをクラウドに出さずにAIでレビューする(ローカルLLM × dev-orchestra) 機密コードをクラウドに出さずにAIでレビューする(ローカルLLM × dev-orchestra)
社外に出せないコードを、クラウドのAIに送らずにレビューする方法をまとめました。ローカルLLMをdev-orchestraのレビュアーに登録し、機密のものはローカルだけで、公開してよいものはクラウドのAIと併用します。ローカルLLMのレビューの実力と、呼び出し方ひとつでコードが外に出てしまう落とし穴も書きます。 社外に出せないコードを、クラウドのAIに送らずにレビューする方法をまとめました。ローカルLLMをdev-orchestraのレビュアーに登録し、機密のものはローカルだけで、公開してよいものはクラウドのAIと併用します。ローカルLLMのレビューの実力と、呼び出し方ひとつでコードが外に出てしまう落とし穴も書きます。
自分が書いた1418行を3つのAIにレビューさせたら、実バグが11件出た 自分が書いた1418行を3つのAIにレビューさせたら、実バグが11件出た
Claude・Codex・ローカルQwenの3枚に、自分で書いて自分で動かしていたスクリプトをレビューさせました。3件は自力では気づけないもので、1件は「レビュー結果を集計するスクリプト自体」のバグでした。一方で、3枚とも見逃した欠陥もあります。 Claude・Codex・ローカルQwenの3枚に、自分で書いて自分で動かしていたスクリプトをレビューさせました。3件は自力では気づけないもので、1件は「レビュー結果を集計するスクリプト自体」のバグでした。一方で、3枚とも見逃した欠陥もあります。
ローカルLLMをdev-orchestraのコードレビュアーに追加してみた ローカルLLMをdev-orchestraのコードレビュアーに追加してみた
自宅PCで動かしているQwenを、複数モデルで独立レビューするdev-orchestraの3枚目のレビュアーとして組み込みました。プロバイダがAPIではなくCLIをラップする設計だったので、薄いCLIを1枚挟んで繋いだ話です。その後の公式対応と、Store版Pythonのパス罠についても追記しました。 自宅PCで動かしているQwenを、複数モデルで独立レビューするdev-orchestraの3枚目のレビュアーとして組み込みました。プロバイダがAPIではなくCLIをラップする設計だったので、薄いCLIを1枚挟んで繋いだ話です。その後の公式対応と、Store版Pythonのパス罠についても追記しました。