OpenCode の技術を徹底解剖 - 10 の設計要素 -
はじめに
AI コーディングエージェントの進化が著しい昨今、オープンソースでその内部構造を学べるプロジェクトは貴重です。OpenCode は、GitHub スター数 10 万超を誇るオープンソースの AI コーディングエージェントです。ターミナル(TUI)、Web ブラウザ、デスクトップアプリ、IDE 拡張と、複数のクライアントから利用でき、75 以上の LLM プロバイダーに対応しています。
本記事では、OpenCode のドキュメントとソースコードを深く読み解き、10 の重要技術要素**を解説します。単なる機能紹介ではなく、「なぜそう設計したのか」「自分でエージェントを作るならどう活かせるか」という視点で掘り下げます。
この記事を読むと、以下のことが理解できます。
- コーディングエージェントのコアアーキテクチャとその設計判断
- エージェントループ(LLM 呼び出し → ツール実行 → 応答記録)の具体的な実装パターン
- ツールシステム、パーミッション制御、プロバイダー抽象化の設計手法
- TUI・LSP・MCP・プラグインといった周辺技術の統合方法
前提知識: LLM の基本的な仕組み(プロンプト、トークン、ストリーミング)、TypeScript の基礎、ターミナル操作の経験があることを前提とします。
参考リンク:
- OpenCode 公式ドキュメント
- OpenCode GitHub リポジトリ
- OpenTUI GitHub リポジトリ
- Model Context Protocol 仕様
- AI SDK(Vercel)
1. 全体アーキテクチャ: クライアント・サーバー分離
なぜクライアントとサーバーを分離するのか
コーディングエージェントを開発する際、最初に直面する設計判断が「UI とロジックをどう分離するか」です。OpenCode はクライアント・サーバーアーキテクチャを採用しています。起動すると HTTP サーバーが立ち上がり、TUI はそのサーバーに接続するクライアントとして動作します。
この設計には明確な理由があります。
設計上のポイント:
-
マルチクライアント対応: 1 つのサーバーに複数のクライアントが同時接続できます。
opencode serveでヘッドレス起動し、Web UI や別のターミナルからアタッチすることも可能です -
リアルタイム同期: Server-Sent Events(SSE)エンドポイント
/global/eventを通じて、すべてのクライアントが同じストリーミング出力をリアルタイムに受信します -
API ファースト設計: OpenAPI 3.1 仕様に準拠した REST API を
/docで公開しており、サードパーティの開発者が独自のクライアントを構築できます
プロジェクト単位の分離
OpenCode はプロジェクトごとに独立した SQLite データベースを持ちます。パスは ~/.local/share/opencode/project/<hash>/data.db です。HTTP サーバーの Instance.provide() ミドルウェアが、リクエストごとにプロジェクトディレクトリをスコープします。
この設計により、複数のプロジェクトを同時に扱っても、セッションやパーミッションが混在しません。
コーディングエージェント開発への示唆
自作エージェントでも、早い段階で UI とエージェントロジックを分離することを推奨します。最初は CLI だけで十分でも、後から Web UI や IDE 拡張を追加したくなるのは確実です。HTTP API + SSE というパターンは、実装コストが低く、拡張性が高い選択肢です。
2. エージェントループ: コーディングエージェントの心臓部
ループの全体像
コーディングエージェントの最も重要な部分は、エージェントループです。OpenCode では SessionPrompt.loop() として実装されています。ユーザー入力を受け取り、LLM を呼び出し、ツールを実行し、結果を記録する — このサイクルを繰り返すのがエージェントの本質です。
メッセージ処理パイプライン
LLM を呼び出す前に、内部メッセージを API フォーマットに変換するパイプラインが走ります。SessionProcessor.create() が以下の変換を順に実行します。
| ステップ | 処理内容 | 具体例 |
|---|---|---|
MessageV2.toModelMessages() |
内部メッセージ構造を AI SDK 形式に変換 | ポリモーフィックな Part を統一フォーマットに |
ProviderTransform.message() |
プロバイダー固有の変換 | Anthropic: 空コンテンツ除去、Mistral: ツールID を 9 文字英数字に正規化 |
ProviderTransform.options() |
モデルパラメータ設定 | maxTokens, temperature, topP の調整 |
ProviderTransform.applyCaching() |
プロンプトキャッシュ適用 | Anthropic, Bedrock, OpenRouter 向けキャッシュ制御ヘッダ |
LLM.stream() |
ストリーミング呼び出し | エラーリカバリとリトライロジック付き |
ポリモーフィックな Part システム
OpenCode のメッセージは、型付けされた Part オブジェクトの配列で構成されます。これはコーディングエージェントの拡張性を支える重要な設計です。
// Part の種類(概念的なコード)
type Part =
| { type: "text"; content: string }
| { type: "tool-invocation"; toolName: string; args: Record<string, unknown>; result: ToolResult }
| { type: "reasoning"; content: string }
| { type: "file"; path: string; content: string }
| { type: "image"; data: Buffer; mimeType: string }
| { type: "agent"; agentName: string; sessionID: string }
各 Part は独立したスキーマとレンダリングロジックを持ちます。新しい種類のコンテンツ(例: 音声、動画)を追加する際も、既存の Part に影響を与えません。
2 階層エージェント構成
OpenCode はプライマリエージェントとサブエージェントの 2 階層構成を採用しています。
| 種類 | エージェント | アクセスレベル | 用途 |
|---|---|---|---|
| プライマリ | Build | 全ツールアクセス可 | デフォルトの開発エージェント |
| プライマリ | Plan | 読み取り専用(write, edit, bash 無効) | 安全なコード分析・設計 |
| サブ | General | 全アクセス(todo除く) | 複雑な検索の委譲先 |
| サブ | Explore | 読み取り専用 | コードベース探索 |
| 隠し | Compaction | — | コンテキスト圧縮 |
| 隠し | Title | — | セッション名生成 |
Tab キーでプライマリエージェントを切り替え、サブエージェントは task ツール経由で自動的に呼び出されます。「計画フェーズでは Plan エージェント、実装フェーズでは Build エージェント」と使い分けることで、意図しないファイル変更を防げます。
コーディングエージェント開発への示唆
エージェントループの設計で重要なのは以下の 3 点です。
-
ツール実行結果を次の LLM 呼び出しに確実にフィードバックする:
finish_reasonがtool-callsならループを継続する - プロバイダー固有の差異を吸収する変換レイヤーを設ける: LLM プロバイダーごとの癖(空コンテンツの扱い、ID フォーマットなど)をループ本体から切り離す
- メッセージを構造化された Part の配列として管理する: テキスト、ツール呼び出し、推論過程を分離することで、レンダリングや永続化が柔軟になる
3. ツールシステムとパーミッション制御
14 個のビルトインツール
コーディングエージェントの「手足」にあたるのがツールです。OpenCode は 14 個のビルトインツールを提供しています。
| カテゴリ | ツール | 機能 | 内部実装の特徴 |
|---|---|---|---|
| ファイル操作 | read |
ファイル読み込み | LSP 診断情報を出力に含める |
write |
ファイル作成・上書き | 全文置換 | |
edit |
部分的な編集 | 検索・置換ベースの差分編集 | |
patch |
パッチ適用 | パッチファイル形式で一括変更 | |
| 検索 | grep |
テキスト検索 | 内部で ripgrep を使用、.gitignore を尊重 |
glob |
ファイルパターン検索 | 更新日時順でソート | |
list |
ディレクトリ一覧 | ツリー表示 | |
| 実行 | bash |
シェルコマンド実行 | 疑似端末で実行 |
task |
サブエージェント委譲 | 子エージェントを生成 | |
| 知識 | skill |
スキル読み込み | オンデマンドのコンテキスト注入 |
webfetch |
Web コンテンツ取得 | ページのフェッチとパース | |
websearch |
Web 検索 | Tavily API 経由 | |
lsp |
コードインテリジェンス | 型情報、シンボル、診断 | |
| 対話 | question |
ユーザーへの質問 | ユーザー応答をブロック待ち |
ツール定義のパターン
ツールは Tool.define() を使い、Zod スキーマで引数を定義します。
// ツール定義の概念的なコード
import { z } from "zod"
const readTool = Tool.define({
name: "read",
description: "ファイルの内容を読み込む",
inputSchema: z.object({
file_path: z.string().describe("読み込むファイルのパス"),
offset: z.number().optional().describe("開始行番号"),
limit: z.number().optional().describe("読み込む行数"),
}),
async execute(args, ctx) {
// ファイル読み込み + LSP 診断情報の付加
const content = await readFile(args.file_path, args.offset, args.limit)
const diagnostics = await getLspDiagnostics(args.file_path)
return { content, diagnostics }
},
})
Tool.Context には、セッション ID、メッセージ ID、エージェント名、中断シグナル、パーミッション確認関数が含まれます。これにより、ツール実行時のコンテキストを完全に把握できます。
ツール実行パイプライン
ツール呼び出しは 6 段階のパイプラインを経て実行されます。
ツールの Part は状態遷移を追跡します。pending → executing → completed | error の各遷移が SSE 経由でクライアントにストリーミングされるため、ユーザーはツールの実行状況をリアルタイムに確認できます。
パーミッション制御
コーディングエージェントにとって、何を許可し何を拒否するかは安全性の要です。OpenCode のパーミッションシステムは 3 つのアクション(allow、deny、ask)と、glob パターンによるルールマッチングで構成されます。
{
"permission": {
"read": { ".env": "deny", "**": "allow" },
"bash": { "rm -rf *": "deny", "git *": "allow", "**": "ask" },
"edit": { "**": "allow" },
"external_directory": { "**": "ask" }
}
}
評価の優先順位は以下の通りです。
- セッションレベルのパーミッション(最優先)
- エージェントレベルのパーミッション
- グローバル設定のパーミッション(最低優先)
最後にマッチした glob パターンが適用されます。承認済みのパーミッションはデータベースに永続化されるため、同じ操作を繰り返し承認する必要はありません。
カスタムツール
.opencode/tools/ ディレクトリに TypeScript ファイルを配置するだけで、カスタムツールを追加できます。ファイル名がツール名になります。
// .opencode/tools/deploy.ts
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "アプリケーションをステージング環境にデプロイする",
args: {
environment: tool.schema.string().describe("デプロイ先の環境名"),
version: tool.schema.string().optional().describe("デプロイするバージョン"),
},
async execute(args) {
// デプロイロジック
return `${args.environment} へのデプロイが完了しました`
},
})
同名のビルトインツールをカスタムツールで上書きすることも可能です。この設計により、チーム固有のワークフローをエージェントに組み込めます。
4. プロバイダー抽象化レイヤー
75 以上のプロバイダーを統一的に扱う
コーディングエージェントを開発する際、「どの LLM を使うか」を 1 つに固定するのは現実的ではありません。OpenCode はプロバイダー抽象化レイヤーを設け、75 以上の LLM プロバイダーを統一的に扱います。
プロバイダー解決チェーン
モデルの選択と認証情報の解決は、以下の優先順位で行われます。
認証情報の解決も階層的です。
| 優先度 | ソース | 例 |
|---|---|---|
| 1(最高) | 環境変数 |
ANTHROPIC_API_KEY, OPENAI_API_KEY
|
| 2 | 認証ファイル | ~/.local/share/opencode/auth.json |
| 3 | 設定ファイル |
opencode.json の provider セクション |
| 4 | Models.dev データベース | デフォルト設定 |
ProviderTransform: プロバイダーの癖を吸収する
プロバイダーごとの差異を吸収する ProviderTransform 名前空間は、エージェント開発者にとって参考になる設計パターンです。
// 概念的なコード: プロバイダー固有の変換
namespace ProviderTransform {
// Anthropic: 空のコンテンツを除去(API エラー防止)
// Mistral: ツール呼び出し ID を 9 文字英数字に正規化
// OpenAI: 特に変換不要
function message(provider: string, messages: Message[]): Message[] {
switch (provider) {
case "anthropic":
return messages.filter((m) => m.content.length > 0)
case "mistral":
return messages.map((m) => normalizeToolIds(m, 9))
default:
return messages
}
}
// Anthropic, Bedrock, OpenRouter: プロンプトキャッシュ制御ヘッダを付与
function applyCaching(provider: string, messages: Message[]): Message[] {
if (["anthropic", "bedrock", "openrouter"].includes(provider)) {
return addCacheControlHeaders(messages)
}
return messages
}
}
この設計の要点は、プロバイダーの差異をエージェントループから完全に隔離することです。ループ本体は「メッセージを送り、レスポンスを受け取る」というシンプルなインターフェースだけを扱います。
認証方式の多様性
OpenCode は以下の認証方式をサポートしています。
| 認証方式 | 対応プロバイダー例 |
|---|---|
| API キー | OpenAI, Anthropic, Google, DeepSeek |
| OAuth フロー | GitHub Copilot, GitLab Duo, Anthropic Claude Pro/Max |
| クラウド認証情報 | Amazon Bedrock (AWS), Google Vertex AI |
| ローカルエンドポイント(認証不要) | Ollama, LM Studio, vLLM |
すべてのプロバイダー SDK はバンドル済みで、追加の npm インストールは不要です。これにより、ユーザーは設定ファイルで model: "anthropic/claude-sonnet-4-20250514" のように プロバイダーID/モデルID を指定するだけでモデルを切り替えられます。
5. OpenTUI: Zig × SolidJS によるターミナル UI
なぜ独自の TUI フレームワークを作ったのか
OpenCode の最も独創的な技術的決断の 1 つが、独自の TUI フレームワーク OpenTUI の開発です。既存の TUI ライブラリ(React 向けの Ink、Go 向けの Bubbletea など)を使わず、ゼロからフレームワークを構築しています。
その理由は、60 FPS のターミナルレンダリングを実現するためです。LLM のストリーミング出力をリアルタイムで表示しつつ、スクロール、シンタックスハイライト、差分表示を同時に処理するには、既存のフレームワークでは性能が不足していました。
Zig + TypeScript の二層アーキテクチャ
OpenTUI は Zig と TypeScript の二層構造になっています。
Zig コアはパフォーマンスが最も重要な処理を担います。
- フレーム差分: 前フレームと現フレームのセル配列を比較し、変更があった部分だけを特定します
- ANSI 生成: 同一スタイルのセルをランレングスエンコーディングで圧縮し、出力バイト数を最小化します
- Rope バッファ: 大きなテキストの挿入・削除を O(log n) で処理します
TypeScript バインディングは Bun の dlopen() FFI を使って Zig バイナリを読み込みます。6 つのプラットフォーム変種(macOS/Linux/Windows × arm64/x64)に対応しています。
SolidJS リコンサイラー
OpenTUI は solid-js/universal の createRenderer を使って、SolidJS のリアクティビティをターミナルレンダリングに接続しています。
SolidJS のコンパイル時 JSX 変換が createElement、insertNode、setProperty への直接呼び出しを生成します。仮想 DOM は存在しません。componentCatalogue が JSX タグ名を Renderable コンストラクタにマッピングします。
主要な Renderable コンポーネントは以下の通りです。
| Renderable | 用途 |
|---|---|
BoxRenderable |
ボーダー、背景、タイトル付きのレイアウトコンテナ |
TextRenderable |
スタイル付きテキスト表示 |
EditBufferRenderable |
カーソル、Undo/Redo 対応の複数行エディタ |
CodeRenderable |
Tree-sitter によるシンタックスハイライト |
DiffRenderable |
統合/分割差分表示 |
ScrollBoxRenderable |
カスタムアクセラレーション付きスクロール領域 |
レンダリングパイプライン
1 フレームの描画は以下のパイプラインで処理されます。
リクエスト → Yoga レイアウト計算 → コンポーネント render(buffer) → ヒットグリッドマッピング → Zig フレーム差分 → Zig ANSI 生成 → ターミナル出力 → バッファスワップ
サブミリ秒のフレームタイムを実現しており、ストリーミング出力中でも滑らかなスクロールと表示更新を維持します。
コーディングエージェント開発への示唆
独自の TUI フレームワークを作る必要はありませんが、以下の設計原則は参考になります。
- パフォーマンスクリティカルな部分だけネイティブ言語で書く: OpenCode は全体を TypeScript で書きつつ、フレーム差分と ANSI 生成だけを Zig に切り出しています
- 宣言的 UI フレームワークを活用する: SolidJS のリアクティビティにより、状態変更時に手動で画面を更新する必要がありません
- ターミナルプロトコルを活用する: Kitty キーボードプロトコル、OSC 11(背景色検出)、OSC 52(クリップボード連携)など、モダンなターミナル機能を積極的に利用しています
6. LSP 統合によるコードインテリジェンス
なぜエージェントに LSP が必要なのか
コーディングエージェントがファイルを読むだけでは、コードの全体像を把握するのは困難です。「この関数はどこから呼ばれているか」「この変数の型は何か」「このファイルにエラーはないか」— これらの情報を正確に得るには、Language Server Protocol(LSP)の力が必要です。
OpenCode は 30 以上の言語サーバーをプリコンフィグで搭載しています。ファイル拡張子を検出すると、対応する言語サーバーが自動的に起動します。
LSP ツールが提供する機能
lsp ツール(実験的機能)は、以下の LSP 操作をエージェントに公開します。
| 操作 | 説明 | ユースケース |
|---|---|---|
goToDefinition |
定義へのジャンプ | 関数やクラスの実装場所を特定 |
findReferences |
参照の検索 | 変更の影響範囲を把握 |
hover |
ホバー情報 | 型情報やドキュメントの取得 |
documentSymbol |
ドキュメントシンボル | ファイル内の構造を把握 |
workspaceSymbol |
ワークスペースシンボル | プロジェクト全体からシンボルを検索 |
goToImplementation |
実装へのジャンプ | インターフェースの実装を特定 |
callHierarchy |
呼び出し階層 | 関数の呼び出し元・呼び出し先を追跡 |
read ツールとの統合
特筆すべきは、read ツールが LSP 診断情報を出力に含める点です。エージェントがファイルを読み込むと、そのファイルの警告、エラー、ヒントが自動的に付加されます。これにより、エージェントは「ファイルを読む」という 1 回のツール呼び出しで、コードの内容と問題点を同時に把握できます。
設定例
{
"lsp": {
"typescript-language-server": {
"command": ["typescript-language-server", "--stdio"],
"extensions": [".ts", ".tsx", ".js", ".jsx"]
},
"rust-analyzer": {
"disabled": false,
"extensions": [".rs"]
},
"custom-lsp": {
"command": ["custom-lsp-server", "--stdio"],
"extensions": [".custom"],
"env": { "CUSTOM_VAR": "value" },
"initialization": { "settings": {} }
}
}
}
コーディングエージェント開発への示唆
LSP 統合は、コーディングエージェントの精度を大幅に向上させます。
- ファイル読み込み時に診断情報を付加する: エージェントがコードの問題を「自然に」認識できるようになります
- 定義ジャンプと参照検索を組み合わせる: エージェントがコードベースをナビゲートする際の効率が飛躍的に向上します
- 言語サーバーの自動起動: ユーザーに設定負荷を与えず、プロジェクトに応じたコードインテリジェンスを提供できます
7. MCP 統合による外部ツール拡張
MCP とは
Model Context Protocol(MCP)は、LLM に外部ツールを提供するための標準プロトコルです。OpenCode は @modelcontextprotocol/sdk を MCP クライアントとして使用し、外部サーバーのツールをエージェントにシームレスに統合します。
ローカルサーバーとリモートサーバー
MCP サーバーには 2 種類あります。
{
"mcp": {
"local-server": {
"type": "local",
"command": ["npx", "-y", "@modelcontextprotocol/server-filesystem"],
"environment": { "ROOT_DIR": "/path/to/dir" },
"timeout": 10000
},
"remote-server": {
"type": "remote",
"url": "https://mcp.example.com/sse",
"headers": { "Authorization": "Bearer {env:MCP_TOKEN}" },
"timeout": 5000
}
}
}
-
ローカルサーバー: CLI コマンドとして実行されます。
command配列で起動コマンドを指定し、environmentで環境変数を渡します - リモートサーバー: HTTP 経由で接続します。OAuth 認証もサポートしており、401 レスポンスを検出すると自動的に Dynamic Client Registration(RFC 7591)を開始します
ツールレジストリへの統合
MCP サーバーが接続すると、そのツールは自動的にビルトインツールと同じ ToolRegistry に登録されます。LLM からは、ビルトインツールも MCP ツールも区別なく呼び出せます。
パーミッション制御も同じ allow / deny / ask フレームワークが適用されるため、セキュリティを犠牲にすることなく外部ツールを追加できます。
コーディングエージェント開発への示唆
MCP 対応は、エージェントのエコシステム拡張性を大幅に高めます。
- ビルトインツールと外部ツールを同一のレジストリで管理する: LLM から見た一貫性が保たれます
- パーミッション制御を外部ツールにも適用する: 「このツールは自動実行 OK」「このツールはユーザー確認必要」をツール単位で制御できます
- OAuth 認証のサポート: SaaS ツールとの連携が格段に容易になります
8. コンテキストウィンドウ管理とコンパクション
なぜコンテキスト管理が重要なのか
コーディングエージェントの会話は長くなりがちです。ファイルの読み込み、ツール実行結果、コード差分などが蓄積すると、LLM のコンテキストウィンドウをすぐに圧迫します。OpenCode はこの問題をコンパクションシステムで解決しています。
3 段階のコンテキスト管理
src/session/compaction.ts に実装されたコンパクションシステムは、以下の 3 段階で動作します。
ステップ 1: プルーニング
メッセージ履歴を後方から走査し、直近 40,000 トークン(PRUNE_PROTECT)は保護します。それより古いツール出力のうち、20,000 トークン(PRUNE_MINIMUM)を超えるものだけを削除します。
この「保護ウィンドウ」と「最小プルーニング閾値」の 2 つのパラメータが重要です。
- 保護ウィンドウが小さすぎると: 直近の文脈が失われ、エージェントが同じ操作を繰り返す
- 最小閾値が小さすぎると: 頻繁なプルーニングでスラッシング(振動)が発生する
skill ツールの出力はプルーニングから保護されます。スキルはプロジェクト固有のルールや手順を含むため、会話の途中で失われると品質が低下するためです。
ステップ 2: コンパクション
プルーニングだけでは不十分な場合、隠しの Compaction エージェントが会話全体を要約します。要約には以下が含まれます。
- 会話のゴール
- ユーザーの指示
- 発見した事実
- 完了した作業
- 関連ファイルのリスト
ステップ 3: 置換
要約メッセージが新しいアシスタントメッセージとして挿入され、古いメッセージ群を置き換えます。
コーディングエージェント開発への示唆
コンテキスト管理は、長時間のコーディングセッションでの信頼性に直結します。
- 直近のコンテキストは絶対に保護する: 最新のツール実行結果やユーザーの指示が失われると、エージェントは迷走します
- プルーニングとコンパクションを段階的に適用する: まずツール出力のプルーニング、それでも足りなければ会話全体のコンパクション、と段階的に処理します
- 特定のツール出力をプルーニングから保護する: プロジェクトルールやスキル情報は、コンテキストから消えてはいけません
9. セッション管理と Database.effect パターン
セッション分岐
OpenCode のセッションは親子関係をサポートします。Session.fork() は、指定されたメッセージまでの会話履歴をコピーし、新しいセッション ID を持つ子セッションを作成します。parentID による系譜を維持しつつ、元のセッションに影響を与えません。
この機能により、「この方針でうまくいかなかったら、ここに戻ってやり直す」という探索的なコーディングが可能になります。
Database.effect パターン
OpenCode のデータベース層で最も注目すべきアーキテクチャパターンが Database.effect です。
リアルタイムシステムでよくある問題として、「データベースに書き込む前にイベントが発行されてしまい、クライアントが不整合な状態を観測する」というレースコンディションがあります。OpenCode はこれを Database.effect パターンで解決します。
// 概念的なコード: Database.effect パターン
Database.use(async (tx) => {
// トランザクション内でデータベースを更新
await tx.insert(messages).values({
sessionID,
role: "assistant",
content: responseContent,
})
// イベント発行をトランザクションのコミット後にスケジュール
Database.effect((effect) => {
if (effect.changes.messages) {
EventBus.publish("message.created", {
sessionID,
messageID: newMessage.id,
})
}
})
})
// ← ここでトランザクションがコミットされ、
// その後に effect 内のイベントが発行される
仕組み:
- すべてのデータベース変更は
Database.use()のトランザクション内で実行されます -
Database.effect()でイベント発行をスケジュールします - トランザクションが正常にコミットされた後に初めてイベントが発行されます
この設計により、クライアントが「イベントを受信したのにデータが見つからない」という状態が発生しません。
データベーススキーマ
OpenCode はプロジェクトごとに以下のテーブルを持つ SQLite データベースを使用します。
| テーブル | 目的 | 特徴 |
|---|---|---|
sessions |
セッションメタデータ | CASCADE 削除 |
messages |
メッセージ本体 | セッション外部キー |
parts |
型付きメッセージパーツ | デュアルインデックス |
permissions |
ツール実行権限 | セッション単位 |
mcp_servers |
MCP 接続状態 | — |
ORM は Drizzle ORM を使用しています。SQLite を選択した理由は、プロジェクトごとの分離が容易で、外部のデータベースサーバーが不要なためです。
コーディングエージェント開発への示唆
- イベント発行はデータベースコミット後に行う: リアルタイム性を重視しすぎてイベントを先行発行すると、クライアントが不整合状態を観測します
- セッション分岐をサポートする: コーディング作業は探索的な性質を持つため、「ここまで戻ってやり直す」機能は実用上非常に重要です
- SQLite でプロジェクトごとに分離する: サーバー不要で移植性が高く、プロジェクト間のデータ干渉がありません
10. プラグインシステムとイベント駆動アーキテクチャ
プラグインの読み込み
OpenCode のプラグインは 3 つのソースから読み込まれます。
-
npm パッケージ: 設定ファイルで指定し、Bun が自動インストール。
~/.cache/opencode/node_modules/にキャッシュされます -
ローカルファイル:
.opencode/plugins/または~/.config/opencode/plugins/に配置した.ts/.jsファイル - デフォルトプラグイン: 無効化しない限り自動読み込み
20 以上のフックポイント
プラグインはイベント駆動で動作します。以下は主要なフックポイントの一部です。
| フック | タイミング | ユースケース |
|---|---|---|
chat.params |
LLM 呼び出しパラメータ設定時 | temperature や maxTokens の動的調整 |
chat.messages.transform |
メッセージ履歴変換時 | カスタムプロンプトの注入 |
llm.stream.before / after
|
LLM ストリーミングの前後 | レイテンシ計測、カスタムログ |
tool.execute.before / after
|
ツール実行の前後 | 実行のインターセプト、結果の加工 |
message.updated |
メッセージ更新時 | 外部通知(Slack 連携など) |
session.compaction |
コンパクション時 | カスタム要約ロジック |
shell.env |
シェル環境変数設定時 | 環境変数の動的注入 |
フックはパイプラインパターンで実行されます。各プラグインが前のプラグインの出力を受け取り、変換して次に渡します。
プラグインのコンテキスト
プラグインは以下のコンテキスト情報にアクセスできます。
// プラグインが受け取るコンテキスト(概念的なコード)
interface PluginContext {
project: {
id: string
path: string
}
directory: string
worktree: string
client: OpencodeClient // SDK クライアントインスタンス
$: ShellAPI // シェルコマンド実行 API
}
$ シェル API により、プラグインからシェルコマンドを安全に実行できます。client を通じて OpenCode の全 API にアクセスすることも可能です。
コーディングエージェント開発への示唆
プラグインシステムの設計は、エージェントの拡張性を決定づけます。
- イベント駆動のフックポイントを設計段階から組み込む: 後付けでフックを追加すると、既存コードの大幅な修正が必要になります
- パイプラインパターンでフックを連鎖させる: 複数のプラグインが同じイベントに対して順番に処理を行えます
- コンテキスト情報を十分に提供する: プラグインがプロジェクト情報やシェル API にアクセスできることで、幅広いカスタマイズが可能になります
まとめ
本記事では、OpenCode のソースコードとドキュメントを深く読み解き、コーディングエージェント開発者が学ぶべき 10 の技術要素を解説しました。
10 の要素の振り返り
| # | 要素 | 核心となる設計判断 |
|---|---|---|
| 1 | クライアント・サーバー分離 | HTTP API + SSE でマルチクライアント対応 |
| 2 | エージェントループ | LLM → ツール実行 → 応答のサイクルを finish_reason で制御 |
| 3 | ツールシステム | Zod スキーマ定義 + 6 段階パイプライン + パーミッション制御 |
| 4 | プロバイダー抽象化 | ProviderTransform でプロバイダーの癖を隔離 |
| 5 | OpenTUI | Zig で描画性能、SolidJS で宣言的 UI |
| 6 | LSP 統合 | read ツールに診断情報を自動付加 |
| 7 | MCP 統合 | 外部ツールをビルトインと同一レジストリで管理 |
| 8 | コンテキスト管理 | 保護ウィンドウ + 段階的プルーニング + コンパクション |
| 9 | Database.effect | トランザクションコミット後にイベント発行 |
| 10 | プラグインシステム | パイプラインパターンの 20 以上のフックポイント |
まとめ
OpenCode から学べる最も重要な教訓は、「関心の分離」を徹底することです。
- UI とエージェントロジックを分離する(クライアント・サーバー)
- プロバイダーの差異をループから分離する(ProviderTransform)
- パフォーマンスクリティカルな部分を分離する(Zig コア)
- 拡張ロジックを本体から分離する(プラグイン + MCP)
これらの分離が適切に行われているからこそ、OpenCode は 75 以上のプロバイダー、複数のクライアント、多数のプラグインを同時にサポートしながら、コードベースの一貫性を保っています。