Appel d'outils et function_tool
codex regroupe tous les outils (tools) que le modèle peut appeler derrière le protocole de function calling de la Responses API. Le crate tools/ définit la couche protocole pure — ToolSpec, ToolDefinition, ToolCall, le trait ToolExecutor s'y trouvent, sans dépendre de codex-core. core/src/function_tool.rs n'est qu'un shim de re-export ; l'enregistrement, le scheduling et les vérifications de sécurité des outils sont dispatchés dans core/src/tools/ et core/src/agent/. Ce découpage permet à la couche protocole d'être réutilisée par app-server, extension runtime, MCP adapter, etc.
Responsabilités
- Définir le spec d'outil visible du modèle : l'énum
ToolSpeccouvreFunction/Namespace/ToolSearch/WebSearch/Freeform— cinq formes sérialisables directement dans le champtoolsde la Responses API (codex-rs/tools/src/tool_spec.rs:15-51). - Décrire les métadonnées d'outil :
ToolDefinitionporte name, description, input_schema, output_schema, defer_loading, pour qu'un adapter le convertisse enToolSpec(codex-rs/tools/src/tool_definition.rs:7-26). - Unifier le contrat d'exécution : le trait
ToolExecutor<Invocation>encapsule « nom d'outil -> spec -> handle » ;ToolExposurecontrôle les quatre expositions direct / deferred / direct-model-only / hidden (codex-rs/tools/src/tool_executor.rs:14-69). - Porter le contexte d'appel :
ToolCallemballe turn_id, call_id, conversation_history, environments, payload dans une seule valeur passée à l'exécuteur (codex-rs/tools/src/tool_call.rs:90-133). - Enregistrer les sub-agent :
AgentRegistrygère l'arbre multi-agent ;reserve_spawn_slotutilise un CAS pour défendre la limitemax_threads;SpawnReservationlibère au drop via RAII (codex-rs/core/src/agent/registry.rs:78-96).
Motivations de conception
Pourquoi core/src/function_tool.rs ne contient-il qu'une ligne de re-export ? À l'origine cette logique était dans core, puis elle a été extraite dans le crate codex-tools ; core ne conserve qu'un alias pour ne pas casser les anciens import. Motivation : app-server, extension runtime et MCP adapter ont tous besoin de construire des ToolSpec et de traiter des ToolCall, sans tirer toute la dépendance codex-core. Une fois la couche protocole extraite en crate, les下游 ne dépendent que de codex-tools + codex-protocol.
Les quatre valeurs de ToolExposure reflètent une tension réelle : plus la liste d'outils est longue, plus le modèle se trompe de choix. Deferred cache l'outil au modèle jusqu'à ce qu'il appelle tool_search — ce qui ramène le contexte initial de N outils à un seul appel tool_search. DirectModelOnly est dédié au code mode : c'est une sous-session imbriquée ; les outils visibles de la session principale mais pas du code mode passent par ici. Hidden sert à l'ordonnancement interne, pour des outils comme request_plugin_install qu'on ne veut pas voir appelés directement par le modèle.
Le cœur de AgentRegistry est total_count: AtomicUsize. Lors d'un spawn multi-agent concurrent, on utilise compare_exchange_weak en spin pour prendre un slot ; si on n'en obtient pas, on renvoie AgentLimitReached. SpawnReservation détient Arc<AgentRegistry> et, à son drop, fait automatiquement fetch_sub(1) pour libérer le slot — idiome Rust pour éviter d'oublier un release manuel.
Fichiers clés
codex-rs/core/src/function_tool.rs:1-1 — tout le fichier tient en une ligne pub use codex_tools::FunctionCallError, l'entrée d'import historique de core.codex-rs/tools/src/lib.rs:1-107 — racine du crate codex-tools, liste tous les re-export publics.codex-rs/tools/src/tool_spec.rs:15-51 — l'énum ToolSpec, tag serde des cinq types d'outil.codex-rs/tools/src/tool_definition.rs:7-26 — ToolDefinition, métadonnées d'outil, avec conversion into_deferred.codex-rs/tools/src/tool_executor.rs:14-69 — ToolExposure et le trait ToolExecutor, contrat runtime.codex-rs/tools/src/tool_call.rs:62-101 — ToolEnvironment et ToolCall, contexte complet d'un appel.codex-rs/core/src/agent/registry.rs:22-96 — AgentRegistry et reserve_spawn_slot, gestion des slots par CAS.codex-rs/core/src/agent/registry.rs:279-316 — SpawnReservation, libération RAII du slot de spawn.codex-rs/core/src/agent/builtins/awaiter.toml:1-40 — configuration TOML de l'agent awaiter intégré, règles de comportement des sub-agent.ToolSpec serde directement dans la forme JSON attendue par la Responses API ; #[serde(tag = "type")] fait que chaque variante est sérialisée avec un champ type. C'est le contrat minimal entre codex et l'API OpenAI :
// 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),
}Le trait ToolExecutor lie « spec d'outil » et « logique d'exécution ». handle retourne un boxed future ; la signature est unifiée ; les impl peuvent être commande shell, apply_patch, appel MCP, callback d'extension. exposure() vaut Direct par défaut ; les outils deferred doivent l'overrider :
// 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<'_>;
}Le spin CAS de AgentRegistry est un pattern classique : compare_exchange en Ordering::AcqRel, en cas d'échec on relit la valeur courante et on boucle. Plus léger qu'un Mutex, adapté à un spawn haute fréquence :
// 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,
}
}
}Flux de données
Limites et échecs
- Type
function_argumentsincompatibles :ToolCall::function_argumentsvérifie quepayloadest bienToolPayload::Function, sinon renvoieFunctionCallError::Fatalavec « tool invoked with incompatible payload » (codex-rs/tools/src/tool_call.rs:123-133). - Épuisement du pool de nickname :
reserve_agent_nicknamevideused_agent_nicknamesquand le pool est épuisé et incrémentenickname_reset_count; les nicknames suivants reçoivent un suffixe « the 2nd » / « the 3rd » ; si le pool est complètement vide, renvoieUnsupportedOperation(codex-rs/core/src/agent/registry.rs:187-225). - Conflit de path agent :
reserve_agent_pathrenvoieUnsupportedOperationsi le path existe déjà ; l'appelant doit vérifier avant spawn (codex-rs/core/src/agent/registry.rs:227-244). - Limite
max_threads:reserve_spawn_slotrenvoieAgentLimitReached { max_threads }s'il n'obtient pas de slot ; l'extension doit gérer cette erreur plutôt que de retryer indéfiniment (codex-rs/core/src/agent/registry.rs:82-96). DirectModelOnlyet code mode :is_directconsidèreDirectetDirectModelOnlycomme direct, mais en sous-session code mode imbriquée seule la première est autorisée comme outil imbriqué ; cela détermine si le spec entre danscollect_code_mode_tool_definitions(codex-rs/tools/src/tool_executor.rs:38-42).
Récapitulatif
La couche protocole (codex-tools) et la couche runtime (core/src/tools/, agent/) des outils sont proprement séparées : la protocole ne gère que « comment parler au modèle » ; le runtime gère « comment exécuter ». Le contrôle de concurrence multi-agent est concentré par AgentRegistry dans un petit motif CAS + RAII ; toute extension qui veut ouvrir un nouveau thread doit passer par là. Pour voir une implémentation concrète comme apply_patch, enchaînez sur Protocole apply_patch ; pour voir comment l'appel d'outil s'insère dans la boucle principale, voir Boucle principale de l'Agent.