Skip to content

Werkzeugaufrufe und function_tool

源码版本rust-v0.145.0

codex bündelt alle vom Modell aufrufbaren Werkzeuge (tools) auf dem Function-Calling-Protokoll der Responses API. Der Crate tools/ definiert eine reine Protokollschicht — ToolSpec, ToolDefinition, ToolCall, der ToolExecutor-Trait liegen hier und hängen nicht von codex-core ab. core/src/function_tool.rs ist nur ein Re-Export-Shim; die eigentliche Werkzeugregistrierung, Planung und Sicherheitsprüfung verteilen sich auf core/src/tools/ und core/src/agent/. Diese Trennung erlaubt es, die Protokollschicht von app-server, Extension und MCP-Adapter unabhängig wiederzuverwenden.

Verantwortlichkeiten

  1. Modell-sichtbare Werkzeug-Spezifikation: Die ToolSpec-Enum deckt fünf Typen ab — Function / Namespace / ToolSearch / WebSearch / Freeform — und serialisiert direkt in das tools-Feld der Responses API (codex-rs/tools/src/tool_spec.rs:15-51).
  2. Werkzeug-Metadaten beschreiben: ToolDefinition trägt name, description, input_schema, output_schema, defer_loading; der Adapter wandelt es in ToolSpec um (codex-rs/tools/src/tool_definition.rs:7-26).
  3. Einheitlicher Ausführungsvertrag: Das ToolExecutor<Invocation>-Trait kapselt „Werkzeugname -> spec -> handle" in einem Objekt; ToolExposure steuert vier Expositionsarten — direct / deferred / direct-model-only / hidden (codex-rs/tools/src/tool_executor.rs:14-69).
  4. Aufruf-Kontext mitführen: ToolCall verpackt turn_id, call_id, conversation_history, environments und payload in einem Wert für den Executor (codex-rs/tools/src/tool_call.rs:90-133).
  5. Sub-Agent registrieren: AgentRegistry verwaltet den Multi-Agent-Baum; reserve_spawn_slot sichert das max_threads-Limit per CAS ab; SpawnReservation gibt den Slot über RAII im drop frei (codex-rs/core/src/agent/registry.rs:78-96).

Entwurfsbeweggründe

Warum besteht core/src/function_tool.rs nur aus einer einzigen Re-Export-Zeile? Früh steckte diese Logik in core, wurde dann in den codex-tools-Crate ausgekoppelt; in core bleibt nur ein Alias, damit alte Imports nicht brechen. Motiv der Trennung: app-server, Extension-Runtime und MCP-Adapter müssen alle ToolSpec konstruieren und ToolCall verarbeiten, ohne die gesamten Abhängigkeiten von codex-core hineinzuziehen. Ist die Protokollschicht ein eigener Crate, genügen downstream codex-tools + codex-protocol.

Die vier Werte von ToolExposure spiegeln einen realen Widerspruch: Je länger die Werkzeugliste, desto höher die Wahrscheinlichkeit, dass das Modell das falsche Werkzeug wählt. Deferred blendet ein Werkzeug vor dem Modell aus, bis es aktiv per tool_search gefunden wird — das presst den anfänglichen Kontext von N Werkzeugen auf einen tool_search-Aufruf zusammen. DirectModelOnly ist dem Code-Modus vorbehalten: Der Code-Modus ist eine verschachtelte Sub-Sitzung, und Werkzeuge, die in der Hauptsitzung sichtbar, aber in der Code-Modus-Sub-Sitzung unsichtbar sind, gehen diesen Weg. Hidden dient der internen Steuerung, etwa für Werkzeuge wie request_plugin_install, die das Modell nicht direkt aufrufen soll.

Im Zentrum von AgentRegistry steht total_count: AtomicUsize. Spawnen mehrere Agenten nebeneinander, wird per compare_exchange_weak um den Slot gespint; wer nicht erwischt, erhält AgentLimitReached. SpawnReservation hält Arc<AgentRegistry> und führt im drop automatisch fetch_sub(1) aus — ein Rust-Idiom, damit kein vergessenes manuelles Release zurückbleibt.

Wichtige Dateien

codex-rs/core/src/function_tool.rs:1-1 — Die gesamte Datei ist eine Zeile pub use codex_tools::FunctionCallError; der alte Import-Einstieg von core.codex-rs/tools/src/lib.rs:1-107 — Wurzel des codex-tools-Crates; listet alle öffentlichen Re-Exports.codex-rs/tools/src/tool_spec.rs:15-51ToolSpec-Enum, serde-Tags der fünf Werkzeugtypen.codex-rs/tools/src/tool_definition.rs:7-26ToolDefinition, Werkzeug-Metadaten, mit into_deferred-Umwandlung.codex-rs/tools/src/tool_executor.rs:14-69ToolExposure und ToolExecutor-Trait, Runtime-Vertrag.codex-rs/tools/src/tool_call.rs:62-101ToolEnvironment und ToolCall, vollständiger Kontext eines Aufrufs.codex-rs/core/src/agent/registry.rs:22-96AgentRegistry und reserve_spawn_slot, CAS-Slot-Verwaltung.codex-rs/core/src/agent/registry.rs:279-316SpawnReservation, RAII-Freigabe des Spawn-Slots.codex-rs/core/src/agent/builtins/awaiter.toml:1-40 — TOML-Konfiguration des eingebauten awaiter-Agenten; definiert die Verhaltensregeln des Sub-Agent.

ToolSpec serialisiert direkt in die JSON-Form der Responses API; #[serde(tag = "type")] sorgt dafür, dass jede Variante beim Serialisieren ein type-Feld mitbekommt. Das ist der minimale Vertrag zwischen codex und OpenAI-API:

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

Das ToolExecutor-Trait bindet „Werkzeug-Spec" und „Ausführungslogik" zusammen. handle liefert eine boxed Future mit einheitlicher Signatur; die konkrete Implementierung kann ein Shell-Kommando, apply_patch, ein MCP-Call oder ein Extension-Callback sein. exposure() ist per Default Direct; deferred Werkzeuge überschreiben es:

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

Das CAS-Spint um den Slot in AgentRegistry ist ein typisches Muster: Ordering::AcqRel für compare_exchange, bei Fehlschlag neuesten Wert neu lesen und weiter loopen. Das ist leichter als ein Mutex und passt zu häufigem Spawn:

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

Datenfluss

Grenzen und Fehler

  • function_arguments-Typ passt nicht: ToolCall::function_arguments prüft, dass payload ein ToolPayload::Function ist; andernfalls FunctionCallError::Fatal mit dem Hinweis „tool invoked with incompatible payload" (codex-rs/tools/src/tool_call.rs:123-133).
  • Nickname-Pool erschöpft: Ist reserve_agent_nickname aufgebraucht, wird used_agent_nicknames geleert und nickname_reset_count += 1 inkrementiert; nachfolgende Nicknames bekommen Suffixe wie „the 2nd" / „the 3rd"; ist auch der Pool endgültig leer, liefert er UnsupportedOperation (codex-rs/core/src/agent/registry.rs:187-225).
  • Agent-Pfad-Kollision: reserve_agent_path liefert bei schon vorhandenem Pfad UnsupportedOperation; der Aufrufer muss vor dem Spawn prüfen (codex-rs/core/src/agent/registry.rs:227-244).
  • max_threads-Limit: reserve_spawn_slot liefert bei Misserfolg AgentLimitReached { max_threads }; die Extension muss diesen Fehler behandeln, statt endlos zu wiederholen (codex-rs/core/src/agent/registry.rs:82-96).
  • DirectModelOnly und Code-Modus: is_direct fasst Direct und DirectModelOnly als direct zusammen, aber in der verschachtelten Sub-Sitzung des Code-Modus ist nur Direct als verschachteltes Werkzeug erlaubt; das bestimmt, ob die Spec in collect_code_mode_tool_definitions eingeht (codex-rs/tools/src/tool_executor.rs:38-42).

Zusammenfassung

Die Werkzeug-Schicht trennt sauber zwischen Protokollschicht (codex-tools) und Runtime-Schicht (core/src/tools/, agent/): Die Protokollschicht kümmert sich nur um „wie man mit dem Modell spricht"; die Runtime-Schicht um „wie ausgeführt wird". Die Nebenläufigkeitskontrolle mehrerer Agenten ist in AgentRegistry auf das kleine Muster CAS + RAII verdichtet; jede Extension, die einen neuen Thread öffnen will, muss diesen Weg gehen. Für eine konkrete Werkzeugimplementierung wie apply_patch siehe apply_patch-Protokoll; wie Werkzeugaufrufe in die Hauptschleife eingebaut werden, siehe Agent-Hauptschleife.