🥂

「Codexでサブエージェント」を独自実装した話

に公開

はじめに(注意喚起)

本記事で紹介する方法は、コードからCodexCLIを呼び出す手法です。つまり、Codexセッションの中からさらに別のCodexセッションを起動することになり、当たり前ですがその分Codexの利用枠を消費します

  • サブスクリプションでCodexを利用している方:残りの利用枠にご注意ください
  • API従量課金の方:課金額にご注意ください

今回のサンプルでは意図的に大量の利用枠を消費するような実装にはしていませんが、AIエージェントのトークン消費は事前に予測しづらいため、あらかじめご了承ください。

また、本記事で紹介する手法はベストプラクティスではありません。CodexSDKを利用して擬似的にClaudeCodeのサブエージェント機能を再現してみよう、という実験的な内容です。効率的な方法かどうかはわかりませんが、CodexCLIをより効果的に使うヒントにはなると思いますので、ぜひ読んでいただければと思います。

Codexにサブエージェントが欲しい

Codexはモデルの性能が高く、コーディング支援ツールとして非常に便利です。しかし、ClaudeCodeのようなカスタマイズ性がない点が惜しいところ。特にサブエージェントなどのコンテキストエンジニアリング関連の仕組みが弱いと感じていました。

そこで今回、Codexでもサブエージェント的な仕組みを実装してみようと思います。本記事では最低限のサンプルと仕組みを紹介しますので、これを参考にご自身でより良い設計を目指していただければ幸いです。

今回紹介する手法の概要

そもそもサブエージェントとは?

サブエージェントとは、メインのエージェントから呼び出される、特定のタスクに特化した小さなエージェントのことです。メインエージェントが「この作業は専門家に任せよう」と判断して、サブエージェントに仕事を投げるイメージですね。

サブエージェントがあると何が嬉しいか

最大のメリットはコンテキストの節約です。

例えばエラー調査を考えてみましょう。メインエージェントがエラー調査を行うと、調査の過程(ファイルを読んだ、grepした、推論した…)がすべてメインのコンテキストに蓄積されます。調査が長引くほどコンテキストを圧迫し、本来やりたかった作業に使える余裕が減っていきます。

サブエージェントに調査を任せれば、調査過程はサブエージェント側のコンテキストで消費され、メイン側には調査結果だけが返ってきます。つまり、調査過程で消費するコンテキストをサブエージェント側に「押し付ける」ことができるわけです。

どうやって実現するか

今回は以下のアプローチで実現します:

  1. Codexからコマンド経由で別のCodexを起動する
  2. あらかじめ専用のプロンプトを用意し、Codexを呼び出すコマンドとセットにする

これにより、サブエージェント的な扱いができるようになります。

使用する技術

  • Node.js:JavaScript のランタイム環境
  • CodexSDK(@openai/codex-sdk):CodexCLIをラップして簡単に扱える公式SDK

これらを使って、エラーを調査し調査結果を返すことに特化したサブエージェントを作ります。

処理の流れ

今回実装するサブエージェントの処理フローは以下の通りです:

ポイントは、サブエージェントの調査過程(ファイル読み込み、推論など)はメインのコンテキストに含まれないこと。メインが受け取るのは最終的な調査レポートだけです。

実際のコードを見ていこう

1. サブエージェント本体(error_research_subagent.mjs)

まずはサブエージェントのメイン処理を見ていきます。

#!/usr/bin/env node
// error_research_subagent.mjs
// Codex SDK でエラー調査専用サブエージェントを起動するサンプル
//
// 使い方: node subagents/error_research_subagent.mjs "<エラーの説明>"
// 注意: 実行すると Codex の利用枠を消費します

import { Codex } from '@openai/codex-sdk';
import { preparePrompt } from './error_research_setup.mjs';

const run = async () => {
    // CLI 引数から「エラー状況の説明」を取得
    const errorDesc = process.argv[2];

    // プロンプトを組み立てる
    const { fullPrompt } = preparePrompt(errorDesc);
    
    // Codex インスタンスを作成
    const codex = new Codex();

    // スレッド設定: read-only サンドボックスでファイル変更を禁止
    const thread = codex.startThread({
        model: 'gpt-5-codex-mini',
        sandboxMode: 'read-only',
        workingDirectory: process.cwd(),
        skipGitRepoCheck: true,
        modelReasoningEffort: 'medium',
    });

    // ストリーミングでイベントを受け取る
    const { events } = await thread.runStreamed(fullPrompt);

    console.log("=== エラー調査サブエージェントの応答(Codexを起動します) ===");

    let lastAgentMessage = '';

    for await (const event of events) {
        // reasoning: 思考ログ(デバッグ用)
        if (event.item?.type === 'reasoning' && typeof event.item.text === 'string') {
            console.log(`[推論] ${event.item.text.trim()}`);
        }

        // エラーイベント
        if (event.type === 'error' && event.message) {
            console.error(`[エラー] ${event.message}`);
        }

        // ターン完了時に最終レポートを取得
        if (event.type === 'turn.completed') {
            if (typeof event.finalResponse === 'string') {
                lastAgentMessage = event.finalResponse.trim();
            }
        }

        // agent_message のフォールバック
        if (event.item?.type === 'agent_message' && typeof event.item.text === 'string') {
            lastAgentMessage = event.item.text.trim();
        }
    }

    const finalMessage = lastAgentMessage || '[error-research] Codexから応答を取得できませんでした';
    console.log(finalMessage);
};

// エラーハンドリング
run().catch((err) => {
    console.error(`[error-research] 実行失敗: ${err?.message || err}`);
    process.exit(1);
});

ポイント解説

Codex SDK でセッションを起動

const codex = new Codex();
const thread = codex.startThread({
    model: 'gpt-5-codex-mini',
    sandboxMode: 'read-only',
    workingDirectory: process.cwd(),
    skipGitRepoCheck: true,
    modelReasoningEffort: 'medium',
});

startThread() でプロンプトを渡す Codex セッションを作成できます。ここで設定できる主なオプション:

  • model:使用するモデル(今回は軽量な gpt-5-codex-mini
  • sandboxMode:サンドボックスのモード
  • workingDirectory:Codex がファイルを参照するディレクトリ

なぜ read-only を指定するのか

サブエージェントには sandboxMode: 'read-only' を強く推奨します。理由は再帰的なループを防ぐためです。

もしサブエージェントがコマンド実行可能な状態だと、「サブエージェントの中でさらにサブエージェントを呼び出す」という再帰的なループに陥る可能性があります。read-onlyにしておけば、ファイルの読み取りはできますが書き込みやコマンド実行はできないため、安全です。

推論過程の表示について

if (event.item?.type === 'reasoning' && typeof event.item.text === 'string') {
    console.log(`[推論] ${event.item.text.trim()}`);
}

開発中は推論過程(Codexが何を考えているか)を表示すると参考になります。ただし、本番運用では出力しない方がよいでしょう。なぜなら、この出力がメイン側のセッションにも含まれてしまい、結局コンテキストを消費することになるからです。

2. プロンプト組み立て(error_research_setup.mjs)

次に、サブエージェントに渡すプロンプトを組み立てる部分です。

// error_research_setup.mjs
// サブエージェントに渡すプロンプトを組み立てるヘルパー

export function preparePrompt(errorDesc) {
    const promptHeader = `
あなたはビルドエラー・テストエラー・実行時エラーの「原因調査」に特化したアシスタントです。

【あなたの役割】
- ユーザーから与えられる情報は「ざっくりした状況説明」や「コンソールに出たエラー断片」で十分です。
- ログ全文や完全な再現手順がなくても、一般的な知識や定石に基づいて「ありそうな原因」を絞り込んでください。
- ここでは「具体的な修正パッチ」よりも、「どこが怪しくて何を確認すべきか」を整理することが目的です。

【前提・制約】
- あなたは \`read-only\` サンドボックスで動作しています。
- ファイルの読み込みやディレクトリの確認(\`ls\`, \`cat\`, \`grep\` 等)は可能ですが、書き込みや変更はできません。
- エラーの原因特定のために、必要なファイルは積極的に読んでください。

【出力フォーマット】
次の 3 セクションをこの順番で出力してください。

1. 想定される主な原因(箇条書きで 3 個以内)
2. 追加で確認すべきこと(箇条書きで 3〜7 個程度)
3. 一行の要約
`.trim();

    const fullPrompt = `${promptHeader}

---

【ユーザーから提供されたエラー状況の説明】

${errorDesc}

上記の情報だけを前提に、指定した出力フォーマットで回答してください。
`;

    return { errorDesc, fullPrompt };
}

ここは比較的シンプルです。エラー調査に特化したプロンプトを定義し、ユーザーから渡されたエラー説明と組み合わせて完全なプロンプトを作成しています。

出力フォーマットを明確に指定することで、サブエージェントからの応答が整理された形で返ってくるようにしています。

3. カスタムコマンド用プロンプト

最後に、メインのCodexからサブエージェントを呼び出すためのカスタムコマンド用プロンプトです。

あなたは「ビルド/テスト/実行時エラーの原因調査」を手伝うカスタムコマンド用アシスタントです。

## やること
1. ユーザーが報告したエラー状況を引数に渡す(重要な情報は省略しない)
2. 以下のコマンドを実行してエラー原因を調査する
   `node subagents/error_research_subagent.mjs "<エラー状況の説明>"`
3. 標準出力を「エラー原因の一次調査レポート」としてユーザーに共有する

## 注意
- 必要なら簡潔な補足コメントを付けて構いません
- コマンドが失敗した場合はエラー内容を伝え、手動での調査に切り替えてください

このプロンプトをカスタムコマンドとして登録しておきます。

カスタムコマンドの登録方法(Mac の場合)

  1. ホームディレクトリに .codex という隠しディレクトリがあります
  2. その中に prompts ディレクトリを作成します
  3. マークダウンファイル(例:error-research.md)としてプロンプトを保存します
mkdir -p ~/.codex/prompts
# プロンプトファイルを作成
vim ~/.codex/prompts/error-research.md

CodexCLIでスラッシュ / を入力すると候補に表示されるので、Enterで使えます。

スクリプトの配置方法

上記のスクリプトを自分のプロジェクトで使うための配置方法を説明します。

ディレクトリ構成

プロジェクトのルートに subagents/ ディレクトリを作成し、以下のように配置します:

your-project/
├── subagents/
│   ├── error_research_subagent.mjs  # サブエージェント本体
│   └── error_research_setup.mjs     # プロンプト組み立て
├── package.json
└── (その他のプロジェクトファイル)

package.json の設定

Codex SDK を依存関係に追加します:

{
  "name": "your-project",
  "type": "module",
  "dependencies": {
    "@openai/codex-sdk": "^0.63.0"
  }
}

セットアップ手順

# 1. subagents ディレクトリを作成
mkdir -p subagents

# 2. スクリプトファイルを配置(上記のコードをコピー)

# 3. 依存関係をインストール
npm install @openai/codex-sdk

# 4. 動作確認
node subagents/error_research_subagent.mjs "テスト"

これで、プロジェクト内から node subagents/error_research_subagent.mjs "<エラー説明>" でサブエージェントを呼び出せるようになります。

実際に使ってみる

エラーが起きるサンプルを用意

意図的にエラーが起きるコードを用意しました:

// app.js の一部(エラーを含む)
function filterTodos(todos, filter) {
    const filtered = todos.filter(todo => {
        // ... フィルタ処理
    });
    
    if (filtered.length === 0 {  // ← ここ!閉じカッコが足りない
        console.log('No todos found');
    }
    
    return filtered;
}

Codexを起動してエラーを発生させる

※以下は実行時の出力イメージです。実際の出力とは異なる場合があります

$ codex
> 今回は実験のため、指示以外のことはしないでください。node app.js を実行してください。

# エラー発生がターミナルに表示される

カスタムコマンドでサブエージェントを呼び出す

※以下は実行時の出力イメージです。実際の出力とは異なる場合があります

> /prompts:error-research

=== エラー調査サブエージェントの応答(Codexを起動します) ===

[推論] app.js のエラーを調査します。SyntaxError は構文エラーなので...
[推論] ファイルを読んで該当箇所を確認します...

## 1. 想定される主な原因

- **app.js 94行目付近の構文エラー**: `if (filtered.length === 0 {` で条件式の閉じカッコ `)` が欠落している可能性が高い

## 2. 追加で確認すべきこと

- app.js の 94 行目周辺のコードを目視確認
- エディタの構文ハイライトでエラー箇所を特定
- `node --check app.js` で構文チェックを実行

## 3. 一行の要約

if文の条件式で閉じカッコ `)` が欠落している構文エラーです。

調査レポートが返ってきました!メインのCodexはこのレポートを元に修正を進められます。

おわりに

今回はCodexSDKを使って「サブエージェント的なもの」を実装してみました。

改めて注意ですが、CodexSDKを通して別のCodexを起動しているので、その分利用枠を消費します。このサンプルを試して利用枠がたくさん消費されても自己責任でお願いします。

今回学んだことを応用すれば、以下のようなことも可能です:

  • 複数のCodexを並列で起動して、異なる観点から同時に調査
  • 様々な解析タスクを自動化(セキュリティチェック、パフォーマンス分析など)
  • 特定ドメインに特化したサブエージェント群を作成

AIエージェントはGUIツールかCLIツールか、どちらが良いかという議論をSNS上でも見かけますが、プログラムに組み込んで利用できることはCLIのわかりやすい強みだと思います。今回の記事でその魅力が伝わったら嬉しいです。

ぜひ皆さんもCLIツールを駆使して独自のエージェントを開発して見てください。

参考リンク

こちらの記事もぜひ

https://zenn.dev/aki_think/articles/e8a846e9bb95b0
https://zenn.dev/aki_think/articles/5fb93a15a6257a

Discussion