Skip to content

Tool Calls and function_tool

源码版本rust-v0.145.0

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

  1. Define model-visible tool specs: the ToolSpec enum covers Function / Namespace / ToolSearch / WebSearch / Freeform, serializing directly into the Responses API's tools field (codex-rs/tools/src/tool_spec.rs:15-51).
  2. Describe tool metadata: ToolDefinition carries name, description, input_schema, output_schema, defer_loading, for adapters to convert into ToolSpec (codex-rs/tools/src/tool_definition.rs:7-26).
  3. Unified execution contract: the ToolExecutor<Invocation> trait bundles "tool name → spec → handle" into an object; ToolExposure controls four exposure modes — direct / deferred / direct-model-only / hidden (codex-rs/tools/src/tool_executor.rs:14-69).
  4. Carry invocation context: ToolCall packs turn_id, call_id, conversation_history, environments, payload into one value handed to the executor (codex-rs/tools/src/tool_call.rs:90-133).
  5. Register sub-agents: AgentRegistry manages the multi-agent tree; reserve_spawn_slot uses CAS to gate the max_threads cap; SpawnReservation uses 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-26ToolDefinition, tool metadata, with into_deferred conversion.codex-rs/tools/src/tool_executor.rs:14-69ToolExposure and the ToolExecutor trait, the runtime contract.codex-rs/tools/src/tool_call.rs:62-101ToolEnvironment and ToolCall, the full context of one invocation.codex-rs/core/src/agent/registry.rs:22-96AgentRegistry and reserve_spawn_slot, CAS slot management.codex-rs/core/src/agent/registry.rs:279-316SpawnReservation, 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:

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),
}

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:

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'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:

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,
        }
    }
}

Data flow

Boundaries and failures

  • function_arguments type mismatch: ToolCall::function_arguments checks that payload is ToolPayload::Function, otherwise returns FunctionCallError::Fatal with "tool invoked with incompatible payload" (codex-rs/tools/src/tool_call.rs:123-133).
  • Nickname pool exhaustion: when reserve_agent_nickname runs out, it clears used_agent_nicknames and nickname_reset_count += 1; subsequent nicknames get "the 2nd" / "the 3rd" suffix; once the pool is truly empty it returns UnsupportedOperation (codex-rs/core/src/agent/registry.rs:187-225).
  • Agent path conflict: reserve_agent_path returns UnsupportedOperation on an existing path; callers must check before spawning (codex-rs/core/src/agent/registry.rs:227-244).
  • max_threads cap: reserve_spawn_slot returns AgentLimitReached { 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).
  • DirectModelOnly and code mode: is_direct treats both Direct and DirectModelOnly as direct, but in code-mode nested sub-sessions only the former is allowed as a nested tool; this decides whether the spec enters collect_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.