工具调用与 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 主循环。