Skip to content

ツール呼び出しと function_tool

源码版本rust-v0.145.0

codex はモデルが呼べる全ツール(tools)を Responses API の function calling プロトコルに集約する。tools/ crate は純粋なプロトコル層を定義する——ToolSpecToolDefinitionToolCallToolExecutor trait はここにあり、codex-core に依存しない。core/src/function_tool.rs は単なる re-export shim で、本当のツール登録、スケジューリング、安全チェックは core/src/tools/core/src/agent/ の下に分散している。この分割により、プロトコル層を app-server、extension、MCP adapter などが独立に再利用できる。

責務

  1. モデル可視のツール仕様の定義:ToolSpec 列挙型は Function / Namespace / ToolSearch / WebSearch / Freeform の五つをカバーし、Responses API の tools フィールドに直接シリアライズされる (codex-rs/tools/src/tool_spec.rs:15-51)。
  2. ツールメタデータの記述:ToolDefinition は name、description、input_schema、output_schema、defer_loading を持ち、adapter が ToolSpec に変換する (codex-rs/tools/src/tool_definition.rs:7-26)。
  3. 実行契約の統一:ToolExecutor<Invocation> trait が「ツール名 -> spec -> handle」を一つのオブジェクトにまとめ、ToolExposure が direct / deferred / direct-model-only / hidden の四つの露出方法を制御する (codex-rs/tools/src/tool_executor.rs:14-69)。
  4. 呼び出しコンテキストの運搬:ToolCall は turn_id、call_id、conversation_history、environments、payload を一つの値にまとめて実行器に渡す (codex-rs/tools/src/tool_call.rs:90-133)。
  5. sub-agent の登録:AgentRegistry がマルチ agent ツリーを管理し、reserve_spawn_slot が CAS で max_threads 上限を守り、SpawnReservation が RAII で drop 時に解放する (codex-rs/core/src/agent/registry.rs:78-96)。

設計動機

なぜ core/src/function_tool.rs は一行の re-export だけなのか?このロジックは元々 core にあったが、後に引き抜かれて codex-tools crate になり、core は古い import が壊れないよう別名を残した。この分割の動機は:app-server、extension runtime、MCP adapter がどれも ToolSpec を組み立てて ToolCall を処理する必要があるが、codex-core の全依存を引き込みたくないからだ。プロトコル層を独立 crate にすれば、下流は codex-tools + codex-protocol だけに依存すればよい。

ToolExposure の四つの値は現実の矛盾を反映する:ツールリストが長くなるほど、モデルが間違ったツールを選ぶ確率が上がる。Deferred はツールを最初はモデルに晒さず、モデルが自発的に tool_search した時にだけ見つかるようにする——N 個のツールの初期 context を 1 個の tool_search 呼び出しに圧縮する。DirectModelOnly は code mode 専用:code mode はネストされたサブセッションで、メインセッションから見えるが code mode サブセッションからは見えないツールはこの路を使う。Hidden は内部スケジューリング用で、request_plugin_install のようなモデルに直接呼ばせたくないツールに使う。

AgentRegistry の核心は total_count: AtomicUsize だ。マルチ agent が並発 spawn する時、compare_exchange_weak で自旋して枠を奪い合い、取れなければ AgentLimitReached を返す。SpawnReservationArc<AgentRegistry> を持ち、drop 時に自動で fetch_sub(1) して枠を解放する——Rust の定石で、手動 release 忘れを防ぐ。

主要ファイル

codex-rs/core/src/function_tool.rs:1-1 — ファイル全体で pub use codex_tools::FunctionCallError の一行のみ。core の古い import 入口。codex-rs/tools/src/lib.rs:1-107codex-tools crate のルート。公開 re-export をすべて列挙。codex-rs/tools/src/tool_spec.rs:15-51ToolSpec 列挙型。五種のツール型の serde tag。codex-rs/tools/src/tool_definition.rs:7-26ToolDefinition。ツールのメタデータ、into_deferred 変換付き。codex-rs/tools/src/tool_executor.rs:14-69ToolExposureToolExecutor trait。ランタイム契約。codex-rs/tools/src/tool_call.rs:62-101ToolEnvironmentToolCall。一回の呼び出しの完全なコンテキスト。codex-rs/core/src/agent/registry.rs:22-96AgentRegistryreserve_spawn_slot。CAS による枠管理。codex-rs/core/src/agent/registry.rs:279-316SpawnReservation。RAII で spawn 枠を解放。codex-rs/core/src/agent/builtins/awaiter.toml:1-40 — 組み込み awaiter agent の TOML 設定。sub-agent の挙動ルールを定義。

ToolSpec は Responses API が要求する JSON 形態に直接 serde され、#[serde(tag = "type")] で各バリアントがシリアライズ時に type フィールドを持つ。これが codex と OpenAI API の間の最小契約だ:

rust
// tool_spec.rs:15-51 — 五种工具类型,直接序列化成 API JSON
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(tag = "type")]
pub enum ToolSpec {
    #[serde(rename = "function")]
    Function(ResponsesApiTool),
    #[serde(rename = "namespace")]
    Namespace(ResponsesApiNamespace),
    #[serde(rename = "tool_search")]
    ToolSearch { execution: String, description: String, parameters: JsonSchema },
    #[serde(rename = "web_search")]
    WebSearch { /* ... 外部搜索开关 */ },
    #[serde(rename = "custom")]
    Freeform(FreeformTool),
}

ToolExecutor trait が「ツール仕様」と「実行ロジック」を一つに結ぶ。handle は boxed future を返し、シグネチャは統一されている。具体的な実装は shell コマンド、apply_patch、MCP call、extension callback のいずれでもよい。exposure() はデフォルトで Direct で、deferred ツールは override が必要:

rust
// tool_executor.rs:49-69 — 所有工具运行时的统一接口
pub trait ToolExecutor<Invocation>: Send + Sync {
    fn tool_name(&self) -> ToolName;
    fn spec(&self) -> ToolSpec;
    fn exposure(&self) -> ToolExposure { ToolExposure::Direct }
    fn search_info(&self) -> Option<ToolSearchInfo> { /* 从 spec 派生 */ }
    fn supports_parallel_tool_calls(&self) -> bool { false }
    fn handle(&self, invocation: Invocation) -> ToolExecutorFuture<'_>;
}

AgentRegistry の CAS 自旋による枠奪取は典型パターン:Ordering::AcqRel で compare_exchange し、失敗したら最新値を読み直して loop する。Mutex より軽く、高頻度 spawn に向く:

rust
// agent/registry.rs:260-276 — CAS 抢占 spawn 槽位
fn try_increment_spawned(&self, max_threads: usize) -> bool {
    let mut current = self.total_count.load(Ordering::Acquire);
    loop {
        if current >= max_threads {
            return false;
        }
        match self.total_count.compare_exchange_weak(
            current, current + 1, Ordering::AcqRel, Ordering::Acquire,
        ) {
            Ok(_) => return true,
            Err(updated) => current = updated,
        }
    }
}

データフロー

境界と失敗

  • function_arguments の型不一致:ToolCall::function_argumentspayloadToolPayload::Function であることを検査し、違えば FunctionCallError::Fatal を返し、「tool invoked with incompatible payload」を伝える (codex-rs/tools/src/tool_call.rs:123-133)。
  • nickname プール枯渇:reserve_agent_nickname はプールが尽きたら used_agent_nicknames を空にして nickname_reset_count += 1 し、以後の nickname に "the 2nd" / "the 3rd" のサフィックスを付ける。プールが完全に空になったら UnsupportedOperation を返す (codex-rs/core/src/agent/registry.rs:187-225)。
  • agent path 衝突:reserve_agent_path はすでに存在する path に遭遇すると UnsupportedOperation を返す。呼び出し側は spawn 前にチェックする必要がある (codex-rs/core/src/agent/registry.rs:227-244)。
  • max_threads 上限:reserve_spawn_slot が枠を取れない時は AgentLimitReached { max_threads } を返す。extension は無限リトライせずこのエラーを処理しなければならない (codex-rs/core/src/agent/registry.rs:82-96)。
  • DirectModelOnly と code mode:is_directDirectDirectModelOnly をどちらも direct とみなすが、code mode のネストサブセッションでは前者だけがネストツールとして許され、これが spec が collect_code_mode_tool_definitions に入るかを決める (codex-rs/tools/src/tool_executor.rs:38-42)。

まとめ

ツール周りはプロトコル層(codex-tools)とランタイム層(core/src/tools/agent/)がきれいに分かれている:プロトコル層は「どうモデルと話すか」だけを管し、ランタイム層は「どう実行するか」を管する。マルチ agent の並発管理は AgentRegistry が CAS + RAII の小さなパターンに収束し、extension が新 thread を開くには必ずこの路を通る。具体的なツール実装(apply_patch など)を見るには apply_patch プロトコル へ、ツール呼び出しがどうメインループに詰め込まれるかは Agent メインループ へ。