Skip to content

工具呼叫與 function_tool

源码版本rust-v0.145.0

codex 把所有模型可呼叫的工具(tools)都收斂到 Responses API 的 function calling 協議上。tools/ crate 定義純協議層——ToolSpecToolDefinitionToolCallToolExecutor trait 都在這裡,不依賴 codex-corecore/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 自旋搶名額,搶不到就回傳 AgentLimitReachedSpawnReservation 持有 Arc<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 直接 serde 成 Responses API 要的 JSON 形態,#[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_arguments 檢查 payload 必須是 ToolPayload::Function,否則回傳 FunctionCallError::Fatal,提示「tool invoked with incompatible payload」(codex-rs/tools/src/tool_call.rs:123-133)。
  • nickname 池耗盡:reserve_agent_nickname 池子用完時清空 used_agent_nicknamesnickname_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 主迴圈