Skip to content

Llamada a herramientas y function_tool

源码版本rust-v0.145.0

codex reúne todas las herramientas invocables por el modelo (tools) en el protocolo de function calling de Responses API. El crate tools/ define la capa pura de protocolo — ToolSpec, ToolDefinition, ToolCall, el trait ToolExecutor viven aquí, sin depender de codex-core. core/src/function_tool.rs es solo un shim de re-export; el registro, dispatch y chequeo de seguridad de herramientas se reparten entre core/src/tools/ y core/src/agent/. Esta separación permite que la capa de protocolo se reutilice de forma independiente en app-server, extensiones, el adapter de MCP, etc.

Responsabilidades

  1. Definir las especificaciones de herramienta visibles para el modelo: el enum ToolSpec cubre cinco tipos — Function / Namespace / ToolSearch / WebSearch / Freeform —, que se serializan directamente al campo tools de Responses API (codex-rs/tools/src/tool_spec.rs:15-51).
  2. Describir los metadatos de herramienta: ToolDefinition lleva name, description, input_schema, output_schema, defer_loading, para que el adapter lo convierta a ToolSpec (codex-rs/tools/src/tool_definition.rs:7-26).
  3. Contrato unificado de ejecución: el trait ToolExecutor<Invocation> envuelve «nombre de herramienta -> spec -> handle» en un objeto; ToolExposure controla cuatro formas de exposición — direct / deferred / direct-model-only / hidden — (codex-rs/tools/src/tool_executor.rs:14-69).
  4. Acarrear el contexto de invocación: ToolCall empaqueta turn_id, call_id, conversation_history, environments y payload en un solo valor para pasárselo al ejecutor (codex-rs/tools/src/tool_call.rs:90-133).
  5. Registrar sub-agents: AgentRegistry gestiona el árbol multi-agent; reserve_spawn_slot usa CAS para imponer el límite max_threads; SpawnReservation libera el slot en drop vía RAII (codex-rs/core/src/agent/registry.rs:78-96).

Motivación de diseño

¿Por qué core/src/function_tool.rs es un re-export de una sola línea? Al principio esta lógica estaba en core; luego se sacó al crate codex-tools, y core conserva solo un alias para no romper los imports viejos. La motivación de la separación: app-server, el runtime de extensiones y el adapter de MCP necesitan construir ToolSpec y procesar ToolCall, pero sin arrastrar toda la dependencia de codex-core. Al independizar la capa de protocolo en su propio crate, los consumidores de aguas abajo solo dependen de codex-tools + codex-protocol.

Los cuatro valores de ToolExposure reflejan una tensión real: cuantas más herramientas haya en la lista, mayor es la probabilidad de que el modelo elija la equivocada. Deferred hace que la herramienta no se exponga al modelo al principio; solo se descubre cuando el modelo invoca tool_search activamente — esto comprime el contexto inicial de N herramientas a 1 llamada tool_search. DirectModelOnly se reserva para code mode: code mode es una subsesión anidada, y las herramientas visibles en la sesión principal pero no en la subsesión de code mode van por aquí. Hidden es para la orquestación interna, herramientas como request_plugin_install que no se quiere que el modelo llame directamente.

El núcleo de AgentRegistry es total_count: AtomicUsize. Cuando varios agents hacen spawn concurrente, se usa compare_exchange_weak en spin para tomar plaza; si no la consigue, devuelve AgentLimitReached. SpawnReservation sostiene un Arc<AgentRegistry> y al hacer drop ejecuta fetch_sub(1) automáticamente para liberar plaza — es el idiom de Rust, para evitar olvidos de release manual.

Archivos clave

codex-rs/core/src/function_tool.rs:1-1 — todo el archivo es una sola línea pub use codex_tools::FunctionCallError, la entrada vieja de imports de core.codex-rs/tools/src/lib.rs:1-107 — raíz del crate codex-tools, lista todos los re-exports públicos.codex-rs/tools/src/tool_spec.rs:15-51 — enum ToolSpec, tag serde para los cinco tipos de herramienta.codex-rs/tools/src/tool_definition.rs:7-26ToolDefinition, metadatos de herramienta, con conversión into_deferred.codex-rs/tools/src/tool_executor.rs:14-69ToolExposure y el trait ToolExecutor, el contrato de runtime.codex-rs/tools/src/tool_call.rs:62-101ToolEnvironment y ToolCall, el contexto completo de una invocación.codex-rs/core/src/agent/registry.rs:22-96AgentRegistry y reserve_spawn_slot, gestión de plazas por CAS.codex-rs/core/src/agent/registry.rs:279-316SpawnReservation, libera el slot de spawn por RAII.codex-rs/core/src/agent/builtins/awaiter.toml:1-40 — configuración TOML del agent awaiter integrado, define las reglas de comportamiento del sub-agent.

ToolSpec se serializa directamente a la forma JSON que pide Responses API; #[serde(tag = "type")] hace que cada variante lleve el campo type al serializar. Es el contrato mínimo entre codex y la API de OpenAI:

rust
// tool_spec.rs:15-51 — cinco tipos de herramienta, se serializan directo al JSON de la API
#[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 { /* ... interruptor de búsqueda externa */ },
    #[serde(rename = "custom")]
    Freeform(FreeformTool),
}

El trait ToolExecutor une «especificación de herramienta» y «lógica de ejecución». handle devuelve un boxed future con firma uniforme; la implementación concreta puede ser un comando shell, apply_patch, una llamada MCP o un callback de extensión. exposure() por defecto es Direct; las herramientas deferred deben sobrescribirlo:

rust
// tool_executor.rs:49-69 — interfaz unificada de runtime para todas las herramientas
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> { /* se deriva del spec */ }
    fn supports_parallel_tool_calls(&self) -> bool { false }
    fn handle(&self, invocation: Invocation) -> ToolExecutorFuture<'_>;
}

El spin CAS de AgentRegistry para tomar plaza es un patrón típico: usa Ordering::AcqRel para compare_exchange, y si falla releee el último valor y sigue en bucle. Es más ligero que un Mutex, adecuado para escenarios de spawn de alta frecuencia:

rust
// agent/registry.rs:260-276 — CAS para tomar el slot de 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,
        }
    }
}

Flujo de datos

Bordes y fallos

  • function_arguments con tipo incorrecto: ToolCall::function_arguments comprueba que payload sea ToolPayload::Function; en caso contrario devuelve FunctionCallError::Fatal con el mensaje «tool invoked with incompatible payload» (codex-rs/tools/src/tool_call.rs:123-133).
  • Pool de nicknames agotado: reserve_agent_nickname, al vaciarse el pool, lo limpia y hace nickname_reset_count += 1; los nicknames siguientes llevan sufijos «the 2nd» / «the 3rd»; si el pool se agota del todo, devuelve UnsupportedOperation (codex-rs/core/src/agent/registry.rs:187-225).
  • Conflicto de agent path: reserve_agent_path, si el path ya existe, devuelve UnsupportedOperation; el llamador debe comprobar antes de hacer spawn (codex-rs/core/src/agent/registry.rs:227-244).
  • Límite superior max_threads: si reserve_spawn_slot no consigue plaza, devuelve AgentLimitReached { max_threads }; la extensión debe gestionar este error y no reintentar infinitamente (codex-rs/core/src/agent/registry.rs:82-96).
  • DirectModelOnly y code mode: is_direct cuenta tanto Direct como DirectModelOnly como direct, pero en la subsesión anidada de code mode solo se permite la primera como herramienta anidada; eso determina si el spec entra en collect_code_mode_tool_definitions (codex-rs/tools/src/tool_executor.rs:38-42).

Resumen

La capa de protocolo (codex-tools) y la de runtime (core/src/tools/, agent/) están claramente separadas: la de protocolo solo se ocupa de «cómo hablar con el modelo»; la de runtime, de «cómo ejecutar». El control de concurrencia multi-agent se concentra en AgentRegistry con un patrón pequeño de CAS + RAII; cualquier extensión que quiera abrir un thread nuevo pasa por ahí. Para ver implementaciones concretas de herramientas como apply_patch, sigue por Protocolo apply_patch; para ver cómo se injerta la llamada a herramienta en el bucle principal, sigue por Bucle principal del Agent.