Tool Calls and function_tool
codex funnels all model-callable tools onto the Responses API's function calling protocol. The tools/ crate defines the pure protocol layer — ToolSpec, ToolDefinition, ToolCall, the ToolExecutor trait all live here, with no dependency on codex-core. core/src/function_tool.rs is just a re-export shim; the real tool registration, dispatch, and safety checks are spread across core/src/tools/ and core/src/agent/. This split lets the protocol layer be reused independently by app-server, extension, MCP adapter, and so on.
Responsibilities
- Define model-visible tool specs: the
ToolSpecenum coversFunction/Namespace/ToolSearch/WebSearch/Freeform, serializing directly into the Responses API'stoolsfield (codex-rs/tools/src/tool_spec.rs:15-51). - Describe tool metadata:
ToolDefinitioncarries name, description, input_schema, output_schema, defer_loading, for adapters to convert intoToolSpec(codex-rs/tools/src/tool_definition.rs:7-26). - Unified execution contract: the
ToolExecutor<Invocation>trait bundles "tool name → spec → handle" into an object;ToolExposurecontrols four exposure modes — direct / deferred / direct-model-only / hidden (codex-rs/tools/src/tool_executor.rs:14-69). - Carry invocation context:
ToolCallpacks turn_id, call_id, conversation_history, environments, payload into one value handed to the executor (codex-rs/tools/src/tool_call.rs:90-133). - Register sub-agents:
AgentRegistrymanages the multi-agent tree;reserve_spawn_slotuses CAS to gate themax_threadscap;SpawnReservationuses RAII to release on drop (codex-rs/core/src/agent/registry.rs:78-96).
Design motivation
Why is core/src/function_tool.rs a one-line re-export? Early on this logic lived in core; later it was extracted into the codex-tools crate, and core only keeps an alias so old imports don't break. The motivation for the split: app-server, extension runtime, and MCP adapter all need to construct ToolSpec and handle ToolCall, but don't want to pull in all of codex-core's dependencies. With the protocol layer as an independent crate, downstream only depends on codex-tools + codex-protocol.
The four values of ToolExposure reflect a real tension: the longer the tool list, the higher the chance the model picks the wrong tool. Deferred lets a tool stay hidden from the model until it actively tool_searches for it — compressing the initial context of N tools into 1 tool_search call. DirectModelOnly is specifically for code mode: code mode is a nested sub-session, and tools visible to the main session but not to the code-mode sub-session go this route. Hidden is for internal scheduling, like request_plugin_install, which we don't want the model to call directly.
At the heart of AgentRegistry is total_count: AtomicUsize. When multiple agents spawn concurrently, compare_exchange_weak spins to grab a slot; failure returns AgentLimitReached. SpawnReservation holds an Arc<AgentRegistry>; on drop it automatically fetch_sub(1) to release the slot — a Rust idiom that avoids forgetting to release manually.
Key files
codex-rs/core/src/function_tool.rs:1-1 — the entire file is one line: pub use codex_tools::FunctionCallError, core's old import entry.codex-rs/tools/src/lib.rs:1-107 — the codex-tools crate root, listing all public re-exports.codex-rs/tools/src/tool_spec.rs:15-51 — the ToolSpec enum, serde tags for the five tool types.codex-rs/tools/src/tool_definition.rs:7-26 — ToolDefinition, tool metadata, with into_deferred conversion.codex-rs/tools/src/tool_executor.rs:14-69 — ToolExposure and the ToolExecutor trait, the runtime contract.codex-rs/tools/src/tool_call.rs:62-101 — ToolEnvironment and ToolCall, the full context of one invocation.codex-rs/core/src/agent/registry.rs:22-96 — AgentRegistry and reserve_spawn_slot, CAS slot management.codex-rs/core/src/agent/registry.rs:279-316 — SpawnReservation, RAII release of a spawn slot.codex-rs/core/src/agent/builtins/awaiter.toml:1-40 — the built-in awaiter agent's TOML config, defining sub-agent behavior rules.ToolSpec is serde'd directly into the JSON shape the Responses API wants; #[serde(tag = "type")] makes each variant carry a type field when serialized. This is the minimal contract between codex and the 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),
}The ToolExecutor trait ties "tool spec" and "execution logic" together. handle returns a boxed future with a uniform signature; concrete implementations can be a shell command, apply_patch, an MCP call, or an extension callback. exposure() defaults to Direct; deferred tools need to 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's CAS slot-grab is a typical pattern: Ordering::AcqRel for compare_exchange, and on failure re-read the latest value and loop. This is lighter than a Mutex and suits high-frequency spawn scenarios:
// 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,
}
}
}Data flow
Boundaries and failures
function_argumentstype mismatch:ToolCall::function_argumentschecks thatpayloadisToolPayload::Function, otherwise returnsFunctionCallError::Fatalwith "tool invoked with incompatible payload" (codex-rs/tools/src/tool_call.rs:123-133).- Nickname pool exhaustion: when
reserve_agent_nicknameruns out, it clearsused_agent_nicknamesandnickname_reset_count += 1; subsequent nicknames get "the 2nd" / "the 3rd" suffix; once the pool is truly empty it returnsUnsupportedOperation(codex-rs/core/src/agent/registry.rs:187-225). - Agent path conflict:
reserve_agent_pathreturnsUnsupportedOperationon an existing path; callers must check before spawning (codex-rs/core/src/agent/registry.rs:227-244). max_threadscap:reserve_spawn_slotreturnsAgentLimitReached { max_threads }when it cannot grab a slot; extensions must handle this error rather than retry indefinitely (codex-rs/core/src/agent/registry.rs:82-96).DirectModelOnlyand code mode:is_directtreats bothDirectandDirectModelOnlyas direct, but in code-mode nested sub-sessions only the former is allowed as a nested tool; this decides whether the spec enterscollect_code_mode_tool_definitions(codex-rs/tools/src/tool_executor.rs:38-42).
Summary
The protocol layer (codex-tools) and the runtime layer (core/src/tools/, agent/) of tools are cleanly separated: the protocol layer only cares about "how to talk to the model"; the runtime layer cares about "how to execute". Multi-agent concurrency is funneled into the small CAS + RAII pattern of AgentRegistry; any extension that wants to start a new thread has to go this route. To see a concrete tool implementation like apply_patch, continue with apply_patch protocol; to see how tool calls plug into the main loop, continue with Agent main loop.