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 主循环