工具呼叫與 function_tool
codex 把所有模型可呼叫的工具(tools)都收斂到 Responses API 的 function calling 協議上。tools/ crate 定義純協議層——ToolSpec、ToolDefinition、ToolCall、ToolExecutor trait 都在這裡,不依賴 codex-core。core/src/function_tool.rs 只是個 re-export shim,真正的工具註冊、排程、安全檢查分散在 core/src/tools/ 和 core/src/agent/ 下。這種拆分讓協議層可以被 app-server、extension、MCP adapter 等獨立重用。
職責
- 定義模型可見的工具規格:
ToolSpec列舉覆蓋Function/Namespace/ToolSearch/WebSearch/Freeform五種,直接序列化成 Responses API 的tools欄位 (codex-rs/tools/src/tool_spec.rs:15-51)。 - 描述工具元資料:
ToolDefinition攜帶 name、description、input_schema、output_schema、defer_loading,供 adapter 轉成ToolSpec(codex-rs/tools/src/tool_definition.rs:7-26)。 - 統一執行契約:
ToolExecutor<Invocation>trait 把「工具名 -> spec -> handle」封成物件,ToolExposure控制 direct / deferred / direct-model-only / hidden 四種暴露方式 (codex-rs/tools/src/tool_executor.rs:14-69)。 - 攜帶呼叫上下文:
ToolCall把 turn_id、call_id、conversation_history、environments、payload 打包成一個值,傳給執行器 (codex-rs/tools/src/tool_call.rs:90-133)。 - 註冊 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。SpawnReservation 持有 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-107 — codex-tools crate 根,列出所有公開的 re-export。codex-rs/tools/src/tool_spec.rs:15-51 — ToolSpec 列舉,五種工具型別的 serde tag。codex-rs/tools/src/tool_definition.rs:7-26 — ToolDefinition,工具元資料,帶 into_deferred 轉換。codex-rs/tools/src/tool_executor.rs:14-69 — ToolExposure 和 ToolExecutor trait,執行時契約。codex-rs/tools/src/tool_call.rs:62-101 — ToolEnvironment 和 ToolCall,一次呼叫的完整上下文。codex-rs/core/src/agent/registry.rs:22-96 — AgentRegistry 和 reserve_spawn_slot,CAS 名額管理。codex-rs/core/src/agent/registry.rs:279-316 — SpawnReservation,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 之間的最小契約:
// 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:
// 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 場景:
// 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_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_direct把Direct和DirectModelOnly都算 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 主迴圈。