Skip to content

Appel d'outils et function_tool

源码版本rust-v0.145.0

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

  1. Définir le spec d'outil visible du modèle : l'énum ToolSpec couvre Function / Namespace / ToolSearch / WebSearch / Freeform — cinq formes sérialisables directement dans le champ tools de la Responses API (codex-rs/tools/src/tool_spec.rs:15-51).
  2. Décrire les métadonnées d'outil : ToolDefinition porte name, description, input_schema, output_schema, defer_loading, pour qu'un adapter le convertisse en ToolSpec (codex-rs/tools/src/tool_definition.rs:7-26).
  3. Unifier le contrat d'exécution : le trait ToolExecutor<Invocation> encapsule « nom d'outil -> spec -> handle » ; ToolExposure contrôle les quatre expositions direct / deferred / direct-model-only / hidden (codex-rs/tools/src/tool_executor.rs:14-69).
  4. Porter le contexte d'appel : ToolCall emballe 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).
  5. Enregistrer les sub-agent : AgentRegistry gère l'arbre multi-agent ; reserve_spawn_slot utilise un CAS pour défendre la limite max_threads ; SpawnReservation libè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-26ToolDefinition, métadonnées d'outil, avec conversion into_deferred.codex-rs/tools/src/tool_executor.rs:14-69ToolExposure et le trait ToolExecutor, contrat runtime.codex-rs/tools/src/tool_call.rs:62-101ToolEnvironment et ToolCall, contexte complet d'un appel.codex-rs/core/src/agent/registry.rs:22-96AgentRegistry et reserve_spawn_slot, gestion des slots par CAS.codex-rs/core/src/agent/registry.rs:279-316SpawnReservation, 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 :

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

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 :

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<'_>;
}

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 :

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

Flux de données

Limites et échecs

  • Type function_arguments incompatibles : ToolCall::function_arguments vérifie que payload est bien ToolPayload::Function, sinon renvoie FunctionCallError::Fatal avec « tool invoked with incompatible payload » (codex-rs/tools/src/tool_call.rs:123-133).
  • Épuisement du pool de nickname : reserve_agent_nickname vide used_agent_nicknames quand le pool est épuisé et incrémente nickname_reset_count ; les nicknames suivants reçoivent un suffixe « the 2nd » / « the 3rd » ; si le pool est complètement vide, renvoie UnsupportedOperation (codex-rs/core/src/agent/registry.rs:187-225).
  • Conflit de path agent : reserve_agent_path renvoie UnsupportedOperation si 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_slot renvoie AgentLimitReached { 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).
  • DirectModelOnly et code mode : is_direct considère Direct et DirectModelOnly comme 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 dans collect_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.