OpenCode v2 拡張機構の調査と導入ガイド

調査日:2026-09-13。本記事では、OpenCode v2の汎用的なプラグイン機能を説明します。プラグインの種類、読み込みの仕組み、機能の登録、実行フック、権限の境界、ライフサイクルを取り上げ、プラグイン開発や組み込み用途の参考にします。

コードの基準は、調査対象のOpenCodeリポジトリの v2 ブランチで、コミットは 2308db16387c9b59e88d732a93ec9bac54462b03です。ソースコードの参照には、この固定コミット内のリポジトリ相対パスを使います。記載するインターフェースや動作はこのバージョンを基準としており、他のOpenCodeバージョンや旧プラグインAPIとの互換性を保証するものではありません。

本記事はソースコードの調査と機能の説明です。コード例はインターフェースや呼び出し方を示すためのもので、独立した実行検証は行っていません。

Plugin:拡張の入口

Pluginとは何か

OpenCodeの拡張は、機能の登録と実行フックを通じて、既存の実行フローに接続します。 現在のOpenCode v2の実装では、Extensionは主にPluginの仕組みに相当します。プラグインは、アシスタント、ツール、スキル、モデル、コマンドなどを提供し、指定された実行箇所で振る舞いを調整できます。セッションの実行、モデルリクエスト、ツール結果、実行記録は、引き続きOpenCodeのコアが処理します。

プラグインの読み込み後に機能を登録し、セッションがコンテキストを準備してリクエストフック経由でモデルを呼び出す。ツール呼び出しは実行フック経由で結果を保存してモデルに返し、最終回答で今回の実行を終了する。
Pluginの読み込み、機能登録、セッション実行までの流れ
Mermaidフローチャートのソースを表示
flowchart TD
    A[配置文件、本地插件或 SDK 注册] --> B[插件加载与生命周期管理]
    B --> C[构建助手、工具、技能等能力]
    C --> D[会话选择助手]
    D --> E[准备上下文与工具快照]
    E --> F[请求阶段钩子]
    F --> G[调用模型]
    G --> H{返回内容}
    H -->|工具调用| I[工具执行与钩子]
    I --> J[保存结果与执行事件]
    J --> G
    H -->|最终回答| K[本次执行结束]

Pluginの分類

内部プラグイン、外部プラグイン、SDKプラグインは、接続元によって区別されますが、最終的には同じ実行管理に組み込まれます。 いずれもアシスタント、ツール、実行フックを登録できます。主な違いは、誰が定義を提供するか、どのようにホストへ読み込むか、ホストがコアの内部サービスを追加で注入するかです。ここで扱うのはサーバー側のプラグインです。ターミナルUIのTUIプラグインは、別の拡張の入口です。

比較項目 内部プラグイン 外部プラグイン SDKプラグイン
提供元 OpenCodeのソースコードと配布バージョン プロジェクトや独立したプラグインパッケージの保守担当者 OpenCodeを組み込むアプリケーションのホスト
入口 PluginInternal 内の prepost コレクション ディレクトリの探索、設定で指定したファイルやパッケージ 組み込みホストの opencode.plugin(definition)
読み込みの形式 コアコードが定義を直接インポート モジュールを解決し、デフォルトエクスポートを取得 メモリ上でプラグインオブジェクトを直接渡す
利用できるインターフェース 公開Contextと、ホストが注入する内部サービス 公開Plugin Context 公開Plugin Context。ホストが明示的に渡した依存をクロージャーで利用可能
主な設定方法 ネイティブ設定、内部サービス、組み込みの既定値 plugins[].optionsctx.options ホストコードとクロージャー。登録インターフェースには独立した options 引数がない
更新元 OpenCodeのコードバージョンに追従。一部のプラグインは設定変更を独自に監視 設定変更、対応するローカル入口の変更、パッケージ設定の変更 ホストが再登録し、内部リビジョン番号を増加
インスタンスの範囲 Locationごとに独立して有効化 各Locationの設定に従って個別に有効化 同じホストで定義を共有し、Locationごとに独立して有効化
用途 ネイティブのアシスタント、ツール、プロバイダー、設定処理を実装 既存のOpenCodeサービスにプロジェクト固有や業務固有の機能を追加 自分のJS/TSアプリケーションにOpenCodeを組み込み、管理

EffectとPromiseは、プラグインを書くための二つのインターフェースです。公開インターフェースを使うEffectプラグインオブジェクトは、デフォルトエクスポートして設定から読み込むことも、組み込みSDKへ直接渡して登録することもできます。中核となる機能は再利用できますが、読み込み順序と設定の渡し方は変わります。

内部プラグインは、OpenCode自身の一部の機能をプラグインとして構成したものです。 ユーザーがディレクトリへインストールする特別なファイルではなく、コアのソースコードから静的にインポートされ、 PluginInternal 内のコレクションに明示的に登録されています。PluginInternal.list() は、現在のLocationに必要なサービスを取得し、 Effect.provide(context) で内部プラグインへ注入したうえで、共通のローダーへ渡します。

ここには二層のContextがあります:effect(ctx) の引数は公開Plugin Contextのままです。内部プラグインはEffect環境を通じて、別途 Config.ServicePermission.ServiceShell.ServiceLocation.Service などのサービスを取得します。したがって、内部プラグインがより多くの機能を使えるのは、ホストが依存を明示的に注入しているためであり、プラグインIDが opencode. で始まるからではありません。

内部プラグインの例 所属するコレクション 実際の責務
opencode.agent pre ネイティブアシスタントの基本定義を登録
opencode.tool.shell pre Shellツールを登録し、ネイティブ実行、権限、実行状態のサービスと連携
ネイティブのプロバイダー、検索、その他のツールのプラグイン pre モデル、検索、ツールの基本機能を提供
opencode.config.agent post アシスタント設定と関連Markdownファイルを読み込み、アシスタント一覧へ反映
プロバイダー、スキル、ポリシー、モデルバリアントの設定プラグイン post 先行するプラグインが提供した機能へ設定や後処理を適用

これにより、カスタムアシスタントと内部プラグインの関係もわかります。ユーザーはアシスタント設定を編集し、その設定の読み込み、監視、適用を実際に担当するのは opencode.config.agentです。各カスタムアシスタントは一つのAgent定義であり、それぞれに独立したプラグインが必要なわけではありません。外部プラグインやSDKプラグインもAgentを登録でき、その後で設定プラグインによる調整を受けられます。

組み込みプラグインも、設定による有効化・無効化の対象です。たとえば -opencode.config.agent を指定すると、アシスタント設定を適用するプラグインが現在の有効な集合から外れ、その設定機能にも影響します。組み込みプラグインを残すかどうかは、実際の責務に基づいて判断するべきです。

Coreの非公開サービスに依存するネイティブ機能を追加する場合、通常はOpenCodeのソースコードを変更して、プラグインを組み込みの集合に追加します。新たなサービス依存が必要なら、サービスの組み立ても変更します。こうした変更はOpenCodeとともにビルド、配布、更新されます。内部プラグインはエンジン自身の機能に適しており、一般的な業務ツールは公開インターフェースから接続することを優先します。

根拠:内部プラグインの集合とサービス注入(packages/core/src/plugin/internal.ts)、ネイティブアシスタントのプラグイン(packages/core/src/plugin/agent.ts)、アシスタント設定のプラグイン(packages/core/src/config/plugin/agent.ts)、Shellツールのプラグイン(packages/core/src/tool/plugin/shell.ts)。

外部プラグインは、ファイルやパッケージを通じて既存のOpenCodeサービスに機能を追加します。 「外部」とは、コードの提供元が組み込みの集合に含まれないことを指します。実行時にはOpenCodeのプロセスへ読み込まれます。ローダーは公開Plugin Contextを提供しますが、内部プラグインのようにCoreサービスを追加注入しません。一方、実行環境で許可されるファイルやネットワークの機能は利用できます。独立したプロセスやセキュリティサンドボックスではありません。

外部プラグインの探索経路は二つあります:

  • 自動探索:ネイティブ設定ディレクトリ内の plugin/plugins/を走査します。現在の実装は、 .ts.js のファイルを直接認識し、パッケージディレクトリや条件を満たすシンボリックリンクの解決にも対応します。パッケージディレクトリでは、まず package.json 内の文字列形式の exportsmodulemainを試し、続いて index.tsindex.jsを試します。この単純な探索処理が、複雑なパッケージのエクスポート規則をすべてサポートするわけではありません。
  • 明示的な設定plugins は、相対パス、絶対パス、ファイルURL、解決可能なパッケージを受け付けます。相対パスは設定ファイルのあるディレクトリを基準に解決されます。ローカルパスはモジュールの読み込みへ進み、パッケージはパッケージリゾルバーに渡され、 server サブエントリーまたはパッケージのルートエントリーを試します。

自動探索の結果が先に操作一覧へ入り、その後で明示的な設定が適用されるため、自動で見つかったプラグインを設定から無効にできます。自動探索は .mjs ファイルを直接列挙しません。この形式の入口を使う場合は、パスを明示的に設定し、実行環境にモジュールの読み込みを任せる必要があります。

{
  "plugins": [
    {
      "package": "./plugins/example.ts",
      "options": {
        "serviceUrl": "https://api.example.com"
      }
    }
  ]
}

上のパスは設定形式の例です。プラグインは ctx.options を通じて serviceUrlを読み取り、必須項目、値の範囲、接続先の制約を自分で検証します。オブジェクトが渡されたことは、業務設定が検証済みであることを意味しません。

外部モジュールは、 id + effect または id + setup を持つプラグインオブジェクトをデフォルトエクスポートしなければなりません:

記述方式 初期化の入口 接続方法とクリーンアップ
Effect effect(ctx) ホストがプラグインのScope内で実行。リソースはscopedライフサイクルまたはfinalizerへ紐付ける
Promise setup(ctx) ローダーが fromPromise でEffectプラグインへ変換。cleanup関数を返せる

@opencode-ai/plugin/effect はEffect APIを提供し、パッケージのルート入口 @opencode-ai/plugin はこのブランチのPromise APIを提供します。旧プラグインAPIには独立した v1 入口があります。「同じOpenCodeプラグインだから」という理由だけで、インターフェースに互換性があると考えてはいけません。

外部プラグインの典型的な読み込み経路は、次のとおりです:

配置目录 / plugins 配置
  → ConfigPluginSource 生成有序操作与本地文件时间戳
  → PluginSupervisor 解析路径或包、导入模块、校验默认导出
  → 适配 Promise 定义,并注入该来源的 options
  → Plugin.Service 创建 Location 内的插件实例
  → 注册助手、工具、钩子及需要清理的资源

ファイルや設定の変更は、プラグイン集合の再生成を起動できます。ただし、これは探索と監視がサポートする範囲内での更新であり、任意の依存ファイルがリアルタイムで更新されるわけではありません。特に、ディレクトリを入口として明示的に設定した場合、内部のファイル変更によって再読み込みされることは、現在の実装では保証されません。パッケージのバージョン更新も、明示的なデプロイや設定変更として管理するべきです。

モジュールのインポートやエクスポート形式の検証に失敗すると、読み込み警告を記録し、その提供元をスキップします。有効化段階に入った後の初期化失敗は、共通のプラグイン管理が扱います。診断では「ファイルが見つかった」「モジュールを解決できた」「プラグインが有効化された」「機能が登録された」をそれぞれ確認します。設定項目が見えるだけでは、ツールが使えることの証明にはなりません。

根拠:提供元の探索とローカル監視(packages/core/src/config/plugin/source.ts)、モジュールの読み込みと設定注入(packages/core/src/plugin/supervisor.ts)、公開パッケージの入口(packages/plugin/package.json)、Promiseアダプター(packages/plugin/src/promise/adapter.ts)。

SDKプラグインは、組み込みホストが直接登録するもので、OpenCodeをアプリケーション内部の実行エンジンとして使う場合に適しています。 本記事でいうSDKは、このブランチの @opencode-ai/sdk-next 組み込みホストを指します。OpenCode.create() は、アプリケーションのプロセス内に実行環境とメモリ上のHTTPルーティング呼び出し経路を作ります。この内部経路のためにネットワークで待ち受ける必要はありませんが、ツール、モデル、業務サービスはそれぞれのネットワークリクエストを送信できます。

ホストは opencode.plugin(definition) を通じてEffectプラグインオブジェクトを直接渡すため、外部モジュールの探索、パッケージ解決、デフォルトエクスポートの検証を省略できます。すでに起動しているリモートのOpenCodeサービスにコードをアップロードするものではありません。通常のHTTPクライアントや、プラグインContext内の ctx.plugin.list() も、この組み込み登録の入口とは異なります。

次の例は、組み込みホストでプラグインを登録、照会する方法を示します。調査対象の基準と一致する依存バージョンが必要です:

import { AbsolutePath, Location, OpenCode } from "@opencode-ai/sdk-next"
import { Effect } from "effect"

const program = Effect.gen(function* () {
  const opencode = yield* OpenCode.create()

  yield* opencode.plugin({
    id: "example.reviewer",
    effect: (ctx) =>
      ctx.agent.transform((draft) => {
        draft.update("reviewer", (agent) => {
          agent.description = "检查代码并给出修改建议"
          agent.system = "分析代码质量,并说明建议的依据。"
          agent.mode = "primary"
        })
      }).pipe(Effect.asVoid),
  })

  const location = Location.Ref.make({
    directory: AbsolutePath.make(process.cwd()),
  })
  return yield* opencode.plugin.list({ location })
})

const result = await Effect.runPromise(program.pipe(Effect.scoped))
console.log(result.data)

例のアシスタントは用途とプロンプトだけを定義しており、権限は別途設定する必要があります。例の実行が終わると、ホストを所有するScopeが閉じます。実際の組み込みアプリケーションでは、そのScopeがサービス全体のライフサイクルを覆うようにします。

SDKプラグインの範囲と更新方法は、二層に分けて理解する必要があります:

管理するもの 有効範囲
ホストのレジストリ Map<plugin.id, Versioned>で、定義と増加するリビジョン番号を保持 同じ組み込みホスト内で共有。異なるホストにはそれぞれ独自のレジストリがある
Locationの有効な集合 そのディレクトリコンテキスト内のプラグインインスタンス、Transform、Hook、Scope Locationごとに有効化とクリーンアップを実施

登録のたびに sdk.plugin.updatedが発行されます。起動済みのLocationは更新を受け取るとプラグイン集合を再生成します。その後に新しく起動するLocationや、回収後に再起動するLocationは、ホストのレジストリから現在の定義を読み取ります。したがって、「ホストが一度だけ登録した」ことは、プラグインの初期化が一度だけ実行されることを意味しません。クロージャー内の可変オブジェクトも、同じホストの複数のLocationインスタンスで共有され得るので、一つのセッションだけの状態だと決めつけてはいけません。

同じSDKレジストリ内で同じIDを再登録すると、定義が置き換わり、新しいリビジョン番号が発生します。異なるホスト同士では上書きされません。登録処理が戻ったことは、定義が保存され更新が発行されたことを示すだけで、すべてのLocationが有効化を完了したという意味ではありません。反映を確認するには、対象Locationの実際のプラグイン一覧と機能一覧を確認し、必要なら有効化イベントを待ちます。

現在のSDKレジストリが提供するのは registerallだけで、独立した unregister インターフェースはありません。各Locationの設定では、プラグインIDで有効化を禁止できますが、ホストのレジストリから定義が削除されるわけではありません。ホストを閉じると実行リソースがクリーンアップされます。ホストを作り直した場合は再登録が必要で、メモリ上の登録は永続的なインストール記録ではありません。

SDKの登録入口はEffectプラグインを受け取ります。外部ローダーによるPromiseプラグインの自動変換は、ここでは自動で行われません。Promiseの定義を再利用するには、対応する fromPromise アダプターを明示的に使います。SDK経路では追加の options 注入はなく、公開Contextには既定で空のオブジェクトが入ります。ホストは通常、ファクトリー関数やクロージャーで独自の設定や業務クライアントを渡します。

SDKプラグインも、内部プラグイン向けのCoreサービス環境を自動では取得しません。ホストから明示的に依存を渡すことはできますが、Coreの非公開サービスへ直接依存すると、追加のバージョン結合を引き受けることになります。調査対象の sdk-next は、まだ private: true の移行用パッケージです。この例を、安定した公開npm APIをインストールできる保証とみなしてはいけません。

根拠:組み込みホストの入口(packages/sdk-next/src/opencode.ts)、SDKレジストリ(packages/core/src/plugin/sdk.ts)、SDKパッケージの状態(packages/sdk-next/package.json)、公開プラグインホスト(packages/core/src/plugin/host.ts)。組み込みテストのソースコード(packages/sdk-next/test/embedded.test.ts)には、Locationをまたぐ更新、Location回収後の再有効化、ホスト間の分離のケースが含まれています。本記事ではこれらのケースを確認しましたが、テストを独立して実行してはいません。

三つの提供元は、一つの順序付きの有効化集合へ統合されます。 有効にする項目が決まった後の、現在の順序は次のとおりです:

内部 pre → SDK 注册插件 → 外部文件 / 包插件 → 内部 post

ConfigPluginSource は外部の提供元と設定操作を担当し、PluginSupervisor はそれらを内部定義、SDK定義と統合し、Plugin.Service は最終的なID重複検査、Scope、初期化、置き換えを担当します。異なる提供元を、三つの独立した実行エンジンと考えてはいけません。

この順序のため、外部プラグインやSDKプラグインが提供したアシスタントなどの機能は、後段の設定プラグインによって調整される場合があります。最終的なプロンプト、モデル、権限を知りたい場合は、個々のプラグインの初期定義だけを見るのではなく、最終的な機能一覧を読み取る必要があります。

プラグインID、パッケージ名、入口のパスは、それぞれ別の役割を持ちます。たとえば、ファイルから読み込むプラグインの公開IDが example.reviewer なら、無効化するときには -example.reviewerと書きます。セレクターは完全一致のID、prefix.**をサポートします。設定操作は順番に実行され、後の操作で既存の定義を再び有効にできます。

同名に関する二つの規則は、混同しやすいので注意が必要です:

  • 同じSDKレジストリ内で同じIDを再登録する場合:その定義を更新します。レジストリがサポートする置き換え操作です。
  • 異なる有効な提供元が最終的に同じIDを提供する場合:有効化段階でID重複として、その回の集合全体が拒否されます。提供元の優先順位で黙って上書きされるわけではありません。外部ファイルとSDK登録から、同じプラグイン定義を同時に渡さないでください。

また、現在の Plugin.Service は、順序付き集合全体のIDとバージョンが完全に一致していれば有効化をスキップします。集合が変わると新しい集合を順に処理し、既存のプラグインもクリーンアップして再初期化します。そのため、一つの提供元の更新で、変更されていない他のプラグインも再初期化され得ます。三種類ともリソースを正しく解放し、初期化を一度だけ行う業務操作として扱わないようにします。置き換えに失敗すると旧バージョンの復元を試みますが、すでに発生した外部の業務上の副作用は巻き戻せません。

根拠:提供元の統合と有効化・無効化の順序(packages/core/src/plugin/supervisor.ts)、共通の有効化と置き換え(packages/core/src/plugin.ts)、設定と読み込み順序のテストソース(packages/core/test/config/plugin.test.ts)。

Pluginの中核実装その1:Transform

Transformは機能を宣言し、Hookは個々の実行を処理します。 拡張点を選ぶ際には、まず要件がどの領域に属するかを確認します。

要件 ネイティブの拡張インターフェース
アシスタントの登録、変更、削除 ctx.agent.transform
実行可能なツールの登録 ctx.tool.transform
スキルの説明とリソース入口の提供 ctx.skill.transform
コマンドの追加 ctx.command.transform
プロバイダー、モデルカタログ、既定の選択の調整 ctx.catalog.transform
参考資料の提供元の追加 ctx.reference.transform
認証と接続方法の設定 ctx.integration.transform
検索バックエンドの提供 ctx.websearch.transform
今回のコンテキストと利用可能なツールの変更 ctx.session.hook("context", ...)
ツール入力の検査や結果の整形 ctx.tool.hook(...)
モデルSDKやモデルインスタンスの調整 ctx.aisdk.hook(...)
モデルのHTTPリクエストやレスポンスの調整 ctx.session.hook("http.request" / "http.response", ...)
コマンド作成引数の調整 ctx.shell.hook("create.before", ...)
リアルタイムイベントの監視 ctx.event.subscribe()
セッションの作成、送信、待機、中断 ctx.session.*

Agent、Catalog、Command、Integration、Reference、Skillなどの状態領域は、有効なTransformを保持し、基本状態から順番に再構築します。プラグインを外すと対応するTransformが削除され、結果が再生成されます。Transformは、そのコールバックに渡されたDraftだけを編集し、Draftへの参照を保持してはいけません。再構築時に外部の業務上の副作用を発生させる用途にも向きません。外部データが必要なら、先にデータを読み込んで保存し、それから対応する領域の reload()を起動します。

Toolの内部実装は、Scopeに紐付いたツール登録とリクエストのスナップショットを使います。ライフサイクルの帰属は前述の領域と共通していますが、すべての transform がまったく同じ状態コンテナや戻り値の型を使うと推測してはいけません。

Pluginの中核実装その2:Hook

Hook(実行フック)は、ホストが実行フローの中に用意した拡張点です。 プラグインは拡張点へコールバック関数を登録します。OpenCodeの実行がその箇所に達すると、コールバックが自動で呼ばれ、その箇所のコンテキストデータが渡されます。プラグインはインターフェースの契約に従ってデータを読み、引数を調整し、結果を処理することで、モデルリクエストやツール実行に参加できます。

Hookの利用には、登録と実行の二段階があります。プラグイン初期化時に、 ctx.session.hook(...)ctx.tool.hook(...) などで参加する箇所を宣言し、実際にその箇所へ達したときに、ホストが登録済みのコールバックを実行します。一度登録すれば、対応箇所が再び実行されるたびに、コールバックも何度でも呼ばれます。ホストはコールバックの完了を待ち、各インターフェースの契約に従って処理を続けます。たとえば、 tool.execute.before では Tool.Error を使って、そのツール実行を拒否できます。

Transformは利用可能な機能を構築し、Hookは具体的な実行箇所でその機能の振る舞いを調整します。たとえば、ツールの登録には ctx.tool.transformを使い、あるツール呼び出しの入力を検査するには ctx.tool.hook("execute.before", ...)を使い、モデルへ送る直前のコンテキストを調整するには ctx.session.hook("context", ...)を使います。Hookは現在の呼び出し経路に参加し、 ctx.event.subscribe() はイベントを購読して実行状況を観察するために使います。

実行時のHookは登録順に直列で実行され、後から登録したHookは先の変更を参照できます。複数のプラグインが同じフィールドを変更する場合は、最終的な読み込み順序を確認する必要があります。組み込みプラグインには前段と後段の集合があるため、「外部プラグインが必ず最後に上書きする」とは限りません。

Hook 変更または観察できる内容 注意点
session.context システムプロンプト、メッセージ、今回のツール定義 アシスタントとモデルの識別子はコンテキストの識別情報。メッセージの変更は今回のリクエストだけの調整であり、永続的な履歴への追加ではない
tool.execute.before ツール入力 戻り値に Tool.Error を指定すると実行を阻止できる
tool.execute.after 成功結果またはツールエラー 結果の整形や情報の補足に利用
session.http.request/response モデルのHTTPリクエストとレスポンス 認証情報や入力全体に触れる可能性があるため、記録する内容を制御する
aisdk.sdk/language SDKと実際のモデルインスタンス プロバイダーへの適合に利用
shell.create.before コマンド、ディレクトリ、時間制限、Shell、環境変数 文字列のルールだけでは、システムレベルの隔離を提供できない

現在の公開Hook型では、 tool.execute.before だけが、回復可能な Tool.Error 型の失敗を返す経路を宣言しています。他のHookを、任意の業務エラーを投げられる汎用ミドルウェアとして扱ってはいけません。

根拠:Plugin Context(packages/plugin/src/effect/plugin.ts)、状態の再構築(packages/core/src/state.ts)、Hookの登録と実行(packages/core/src/plugin/hooks.ts)、ツールの登録とスナップショット(packages/core/src/tool.ts)。

ツールの例とライフサイクル

例1:Plugin Transformを使ってToolを定義する

ツールは、モデルから見える定義と、ホストが実行する関数で構成されます。 モデルがツール名と入力を生成すると、OpenCodeはそのリクエストで取得したツール機能を使って呼び出しを処理します。ツール関数は、ホストが渡す sessionIDagentmessageID と呼び出しの idを受け取ります。長い操作では、 context.progress() で進捗を伝えられます。

以下は、Effect APIを使った独立したツールの例です:

import { Plugin } from "@opencode-ai/plugin/effect"
import { Effect, Schema } from "effect"

export default Plugin.define({
  id: "example.echo",
  effect: Effect.fn(function* (ctx) {
    yield* ctx.tool.transform((tools) => {
      tools.add({
        name: "echo",
        description: "返回收到的文字",
        input: Schema.Struct({ text: Schema.String }),
        output: Schema.Struct({ text: Schema.String }),
        options: { codemode: false },
        execute: ({ text }) =>
          Effect.succeed({
            output: { text },
            content: text,
          }),
      })
    })
  }),
})

結果のフィールドは、用途ごとに区別する必要があります:output は、出力Schemaを宣言した場合に、プログラムが利用する構造化された値です。content はモデルへ渡され、セッションの内容に含まれます。metadata は、範囲を限定した補助的な状態を保持します。出力Schemaを宣言せずに outputを返すと、現在の実行実装ではエラーになります。

ソースコードには、インターフェース名だけではわからない二つの具体的な検証上の注意点があります:

  • tool.execute.before は、ツール実行関数内のSchemaによるデコードより先に実行されます。そのため、Hookで入力を読む際には、まだ未知の構造として扱う必要があります。Hookが変更した後の入力が、後続のデコードへ渡されます。
  • Effect Schemaと対応するStandard Schemaは、実行時の入力検証を行います。一方、純粋なJSON Schemaの分岐では入力をそのまま渡します。純粋なJSON Schemaの出力分岐も、JSON値かどうかを確認するだけで、宣言された制約を個別には検証しません。業務ツールでは、「モデルへJSON Schemaを提供した」ことを「サーバー側で引数を検証済み」と考えてはいけません。

現在のツール処理経路では、実行前のHook自体が失敗すると、その後の処理が直ちに中止されます。この種の拒否について、 execute.after が必ず呼ばれるとは限りません。監査では、正常な実行後の監視だけでなく、拒否される経路も扱う必要があります。

予期される回復可能なツールの失敗は、 Tool.Errorへ対応付けられます。実行のキャンセル、未知のプログラム上の欠陥、成功結果は、それぞれ異なる意味を保つ必要があります。すべてを握りつぶし、通常の成功テキストとして返してはいけません。

根拠:ツールの型(packages/schema/src/tool.ts)、ツール実行のラッパー(packages/core/src/tool.ts)、実際の引数検証(packages/core/src/tool/runtime.ts)。

例2:Code Modeでツール呼び出しを組み合わせる

Code Modeは、一部のツールをどのように提示し、組み合わせるかを決めます。 現在のブランチでは、 codemode: false のツールをモデルへ直接公開します。他のツールはCode Modeのカタログに入り、 execute で短いコードを実行して、呼び出しの組み合わせや結果の処理を行えます。Code Mode内の実際のツール呼び出しも、ホストのツール実行経路へ戻ります。

これは、連続した問い合わせ、フィルタリング、集計などに適しています。Code Modeの実行環境は、直接のファイルアクセスやインポートなどを制限しますが、その制限はOpenCodeサービス全体や外部プラグインがシステムのサンドボックス内で動くことを意味しません。前述の echo の例は codemode: falseを設定し、ツールをモデルへ直接公開する方法を示しています。

モデルリクエストは、その時点のツール登録のスナップショットを取得します。プラグインのホット更新が起きても、準備済みのリクエストが新しいツール一覧を使うことにはなりません。後続のリクエストで機能が再準備されます。プラグインが提供するツール実行関数も、すでにクリーンアップされた、所有者のないバックグラウンドリソースに依存しないようにする必要があります。

根拠:ツールの分類とスナップショット(packages/core/src/tool.ts)、Code Mode(packages/core/src/codemode/tool.ts)、今回のツールの利用可否の検査(packages/core/src/session/model-request.ts)。

PluginのLocation

プラグインインスタンスはLocationごとに管理され、永続的な業務状態は別途保存する必要があります。 Locationは、ディレクトリと任意のネイティブWorkspace識別子で構成される実行コンテキストです。一つのOpenCodeプロセスは複数のLocationを同時に扱え、同じプラグインが各Locationで個別に有効化される場合があります。

各プラグインインスタンスはScopeを持ち、Transform、Hook、ツール登録、正しく紐付けられたリソースを所有します。Scopeを閉じると、これらの登録はクリーンアップされます。プラグイン自身のタイマー、ネットワーク購読、ファイル監視にもクリーンアップ処理を紐付ける必要があります。Promiseプラグインは setup からcleanupを返し、Effectプラグインは対応するscopedリソースやfinalizerを使います。

首次加载 → 创建 Scope → 注册能力与钩子 → 服务会话
文件或配置变化 → 替换插件 → 清理旧 Scope → 激活新版本
新版本激活失败 → 尝试恢复旧版本 → 恢复失败则停用

ローカルのプラグイン入口ファイルと設定元には、変更監視があります。ただし、明示的な入口がディレクトリの場合、その内部のファイル変更について同じ自動ホット更新は保証されません。読み込み、更新、停止は、実際のファイル監視や実行状態にも影響されるため、「ホット更新に対応している」だけでは不十分です。ネイティブの機能一覧を読み、反映を確認する必要があります。

プラグインの復元で戻せるのは、コードと登録だけです。ファイル、データベース、リモートサービスへの操作ですでに発生した副作用は巻き戻せません。定期処理、承認状態、再試行回数、冪等性の識別子は、信頼できるストレージで管理するべきです。メモリ上のセッション状態は、少なくとも sessionID ごとに分離します。モジュールレベルのグローバル変数は、プラグインインスタンスをまたいで共有される場合もあるため、特に注意が必要です。

根拠:プラグインのScopeと復元(packages/core/src/plugin.ts)、プラグインのファイル監視(packages/core/src/config/plugin/source.ts)。

Pluginのベストプラクティス

プラグイン開発では、既存の設定と公開インターフェースの再利用を優先します

  • プロンプト、用途、ステップ上限だけを調整する場合は、Agent設定を使います。
  • 手順や知識を提供する場合は、Skillを使います。
  • 実際の操作を追加する場合は、Toolを登録します。
  • 特定の実行箇所で振る舞いを調整する場合は、対応するHookを使います。
  • 特に、業務状態を永続化したり外部サービスを呼び出したりする場合は、ストレージ、認証、トランザクション、冪等性を明示的に設計します。それらの保証をプラグインのメモリ状態に頼ってはいけません。

単一責任のプラグインなら、一つのTS拡張ファイルだけでも構いません。複数の機能に、独立した有効化・無効化、バージョン管理、失敗時の方針が必要なときだけ、複数のプラグインIDに分けます。通常の外部プラグインは、公開プラグイン、クライアント、schemaのインターフェースに依存し、Coreの非公開実装のインポートを避けます。配布時には、互換性のあるホストバージョン、依存バージョン、モジュールの入口、ビルド方法を明示します。

Pluginを更新するときは、モジュールをインポートできるかだけでなく、少なくとも次の動作を確認するべきです:

  • Agent、ツール、スキルが実際の機能一覧に現れ、アンインストール後には提供した機能が消えるか。
  • 複数プラグインの順序、設定の上書き、ホット更新の失敗後の最終状態が、想定どおりか。
  • ツールの入力、出力、キャンセル、実行拒否が、それぞれ正しい意味を保っているか。
  • セッションのモデルやアシスタントの選択が実際に反映され、サブアシスタントが想定どおりの権限とコンテキストを持つか。
  • カスタムツールと、その呼び出し先のサービスが、セッションの帰属、業務上の認可、状態遷移、重複リクエストを正しく検証するか。
  • サービス再起動後に永続状態を復元でき、リアルタイムイベントの欠落が正しく扱われるか。
  • ツール結果とイベントが、進捗、完了、失敗、キャンセルの状態を正確に表すか。