Llamada a herramientas y function_tool
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
- Definir las especificaciones de herramienta visibles para el modelo: el enum
ToolSpeccubre cinco tipos —Function/Namespace/ToolSearch/WebSearch/Freeform—, que se serializan directamente al campotoolsde Responses API (codex-rs/tools/src/tool_spec.rs:15-51). - Describir los metadatos de herramienta:
ToolDefinitionlleva name, description, input_schema, output_schema, defer_loading, para que el adapter lo convierta aToolSpec(codex-rs/tools/src/tool_definition.rs:7-26). - Contrato unificado de ejecución: el trait
ToolExecutor<Invocation>envuelve «nombre de herramienta -> spec -> handle» en un objeto;ToolExposurecontrola cuatro formas de exposición — direct / deferred / direct-model-only / hidden — (codex-rs/tools/src/tool_executor.rs:14-69). - Acarrear el contexto de invocación:
ToolCallempaqueta 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). - Registrar sub-agents:
AgentRegistrygestiona el árbol multi-agent;reserve_spawn_slotusa CAS para imponer el límitemax_threads;SpawnReservationlibera 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-26 — ToolDefinition, metadatos de herramienta, con conversión into_deferred.codex-rs/tools/src/tool_executor.rs:14-69 — ToolExposure y el trait ToolExecutor, el contrato de runtime.codex-rs/tools/src/tool_call.rs:62-101 — ToolEnvironment y ToolCall, el contexto completo de una invocación.codex-rs/core/src/agent/registry.rs:22-96 — AgentRegistry y reserve_spawn_slot, gestión de plazas por CAS.codex-rs/core/src/agent/registry.rs:279-316 — SpawnReservation, 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:
// 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:
// 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:
// 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_argumentscon tipo incorrecto:ToolCall::function_argumentscomprueba quepayloadseaToolPayload::Function; en caso contrario devuelveFunctionCallError::Fatalcon 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 hacenickname_reset_count += 1; los nicknames siguientes llevan sufijos «the 2nd» / «the 3rd»; si el pool se agota del todo, devuelveUnsupportedOperation(codex-rs/core/src/agent/registry.rs:187-225). - Conflicto de agent path:
reserve_agent_path, si el path ya existe, devuelveUnsupportedOperation; el llamador debe comprobar antes de hacer spawn (codex-rs/core/src/agent/registry.rs:227-244). - Límite superior
max_threads: sireserve_spawn_slotno consigue plaza, devuelveAgentLimitReached { max_threads }; la extensión debe gestionar este error y no reintentar infinitamente (codex-rs/core/src/agent/registry.rs:82-96). DirectModelOnlyy code mode:is_directcuenta tantoDirectcomoDirectModelOnlycomo direct, pero en la subsesión anidada de code mode solo se permite la primera como herramienta anidada; eso determina si el spec entra encollect_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.