mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
## Why `codex-tools` is meant to hold reusable tool primitives, but `ToolsConfig` had become a second copy of core runtime decisions instead of a small shared contract. It carried provider capabilities, auth/model gates, permission and environment state, web/search/image feature gates, multi-agent settings, and goal availability from core into `codex-tools` ([definition](https://github.com/openai/codex/blob/22dd9ad3929253ed24d7ee4f10f238e95ab25f37/codex-rs/tools/src/tool_config.rs#L97), [stored on each `TurnContext`](https://github.com/openai/codex/blob/22dd9ad3929253ed24d7ee4f10f238e95ab25f37/codex-rs/core/src/session/turn_context.rs#L87)). Every session/context variant then had to build and mutate that snapshot before assembling tools. This PR removes that master object instead of renaming it. Tool planning now reads the live `TurnContext`, where `codex-core` already owns those decisions, while `codex-tools` keeps only reusable primitives and a generic `ToolSetBuilder`/`ToolSet` accumulator. ## What Changed - Removed `ToolsConfig` / `ToolsConfigParams` from `codex-tools`; the crate keeps the shared helpers that still belong there, including request-user-input mode selection, shell backend/type resolution, `UnifiedExecShellMode`, and `ToolEnvironmentMode`. - Replaced config-snapshot planning with `ToolRouter::from_turn_context` and a `spec_plan` pipeline over `CoreToolPlanContext`, deriving provider capabilities, auth gates, model support, feature gates, environment count, goal support, multi-agent options, web search, and image generation from the authoritative turn state. - Added generic `codex_tools::ToolSetBuilder` / `ToolSet`, plus the small core adapter needed to accumulate `CoreToolRuntime` values and hosted model specs. - Added the `tool_family::shell` registration module and moved shell/unified-exec/memory accounting call sites to read the narrow per-turn fields directly. - Narrowed `TurnContext` to the remaining explicit per-turn fields needed by planning: `available_models`, `unified_exec_shell_mode`, and `goal_tools_supported`. - Reworked MCP exposure and tool-search setup so deferred/direct MCP behavior is driven by the current turn rather than a precomputed config snapshot. - Replaced the large expected-spec fixture tests with focused behavior-level coverage for shell tools, environments, goal and agent-job gates, MCP direct/deferred exposure, tool search, request-plugin-install, code mode, multi-agent mode, hosted tools, and extension executor dispatch. ## Verification - `cargo check -p codex-tools` - `cargo check -p codex-core --lib` - `cargo test -p codex-tools` - `cargo test -p codex-core spec_plan --lib` - `cargo test -p codex-core router --lib`
154 lines
4.7 KiB
Rust
154 lines
4.7 KiB
Rust
use codex_features::Feature;
|
|
use codex_features::Features;
|
|
use codex_protocol::config_types::ModeKind;
|
|
use codex_protocol::openai_models::ConfigShellToolType;
|
|
use codex_protocol::openai_models::ModelInfo;
|
|
use codex_protocol::openai_models::ModelVisibility;
|
|
use codex_protocol::openai_models::TruncationPolicyConfig;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo {
|
|
ModelInfo {
|
|
slug: "test-model".to_string(),
|
|
display_name: "Test Model".to_string(),
|
|
description: None,
|
|
default_reasoning_level: None,
|
|
supported_reasoning_levels: Vec::new(),
|
|
shell_type,
|
|
visibility: ModelVisibility::List,
|
|
supported_in_api: true,
|
|
priority: 0,
|
|
additional_speed_tiers: Vec::new(),
|
|
service_tiers: Vec::new(),
|
|
availability_nux: None,
|
|
upgrade: None,
|
|
base_instructions: String::new(),
|
|
model_messages: None,
|
|
supports_reasoning_summaries: false,
|
|
default_reasoning_summary: Default::default(),
|
|
support_verbosity: false,
|
|
default_verbosity: None,
|
|
apply_patch_tool_type: None,
|
|
web_search_tool_type: Default::default(),
|
|
truncation_policy: TruncationPolicyConfig::tokens(/*limit*/ 1024),
|
|
supports_parallel_tool_calls: true,
|
|
supports_image_detail_original: false,
|
|
context_window: None,
|
|
max_context_window: None,
|
|
auto_compact_token_limit: None,
|
|
effective_context_window_percent: 95,
|
|
experimental_supported_tools: Vec::new(),
|
|
input_modalities: codex_protocol::openai_models::default_input_modalities(),
|
|
used_fallback_model_metadata: false,
|
|
supports_search_tool: false,
|
|
}
|
|
}
|
|
|
|
fn shell_features() -> Features {
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ShellTool);
|
|
features.disable(Feature::ShellZshFork);
|
|
features.disable(Feature::UnifiedExec);
|
|
features
|
|
}
|
|
|
|
#[test]
|
|
fn shell_type_is_derived_from_model_and_feature_gates() {
|
|
let model = model_with_shell_type(ConfigShellToolType::UnifiedExec);
|
|
let mut features = shell_features();
|
|
assert_eq!(
|
|
shell_type_for_model_and_features(&model, &features),
|
|
ConfigShellToolType::ShellCommand
|
|
);
|
|
|
|
features.enable(Feature::UnifiedExec);
|
|
let expected_unified_exec = if codex_utils_pty::conpty_supported() {
|
|
ConfigShellToolType::UnifiedExec
|
|
} else {
|
|
ConfigShellToolType::ShellCommand
|
|
};
|
|
assert_eq!(
|
|
shell_type_for_model_and_features(&model, &features),
|
|
expected_unified_exec
|
|
);
|
|
|
|
features.enable(Feature::ShellZshFork);
|
|
assert_eq!(
|
|
shell_type_for_model_and_features(&model, &features),
|
|
ConfigShellToolType::ShellCommand
|
|
);
|
|
|
|
features.disable(Feature::ShellTool);
|
|
assert_eq!(
|
|
shell_type_for_model_and_features(&model, &features),
|
|
ConfigShellToolType::Disabled
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn shell_command_backend_requires_both_shell_tool_and_zsh_fork() {
|
|
let mut features = shell_features();
|
|
assert_eq!(
|
|
shell_command_backend_for_features(&features),
|
|
ShellCommandBackendConfig::Classic
|
|
);
|
|
|
|
features.enable(Feature::ShellZshFork);
|
|
assert_eq!(
|
|
shell_command_backend_for_features(&features),
|
|
ShellCommandBackendConfig::ZshFork
|
|
);
|
|
|
|
features.disable(Feature::ShellTool);
|
|
assert_eq!(
|
|
shell_command_backend_for_features(&features),
|
|
ShellCommandBackendConfig::Classic
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn request_user_input_modes_follow_default_mode_feature() {
|
|
let mut features = Features::with_defaults();
|
|
features.disable(Feature::DefaultModeRequestUserInput);
|
|
assert_eq!(
|
|
request_user_input_available_modes(&features),
|
|
vec![ModeKind::Plan]
|
|
);
|
|
|
|
features.enable(Feature::DefaultModeRequestUserInput);
|
|
assert_eq!(
|
|
request_user_input_available_modes(&features),
|
|
vec![ModeKind::Default, ModeKind::Plan]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unified_exec_shell_mode_uses_zsh_fork_only_when_all_inputs_match() {
|
|
let exe = std::env::current_exe().expect("current exe path");
|
|
let shell = exe.clone();
|
|
|
|
let mode = UnifiedExecShellMode::for_session(
|
|
ShellCommandBackendConfig::ZshFork,
|
|
ToolUserShellType::Zsh,
|
|
Some(&shell),
|
|
Some(&exe),
|
|
);
|
|
if cfg!(unix) {
|
|
assert!(matches!(mode, UnifiedExecShellMode::ZshFork(_)));
|
|
} else {
|
|
assert_eq!(mode, UnifiedExecShellMode::Direct);
|
|
}
|
|
|
|
assert_eq!(
|
|
UnifiedExecShellMode::for_session(
|
|
ShellCommandBackendConfig::Classic,
|
|
ToolUserShellType::Zsh,
|
|
Some(&shell),
|
|
Some(&exe),
|
|
),
|
|
UnifiedExecShellMode::Direct
|
|
);
|
|
}
|