Remove ToolsConfig from tool planning (#22835)

## 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`
This commit is contained in:
jif-oai authored and GitHub committed 2026-05-19 11:24:09 +02:00
1 parent ba57aab13a
commit 05e171094d
25 files changed
+1426 -5232

No files matched your search

+2 -3
View File
@@ -3,7 +3,6 @@ use std::collections::HashSet;
use codex_features::Feature;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::ToolInfo as McpToolInfo;
use codex_tools::ToolsConfig;
use crate::config::Config;
use crate::connectors;
@@ -19,7 +18,7 @@ pub(crate) fn build_mcp_tool_exposure(
all_mcp_tools: &[McpToolInfo],
connectors: Option<&[connectors::AppInfo]>,
config: &Config,
tools_config: &ToolsConfig,
search_tool_enabled: bool,
) -> McpToolExposure {
let mut deferred_tools = filter_non_codex_apps_mcp_tools_only(all_mcp_tools);
if let Some(connectors) = connectors {
@@ -30,7 +29,7 @@ pub(crate) fn build_mcp_tool_exposure(
));
}
let should_defer = tools_config.search_tool
let should_defer = search_tool_enabled
&& (config
.features
.enabled(Feature::ToolSearchAlwaysDeferMcpTools)
+7 -36
View File
@@ -2,17 +2,9 @@ use std::collections::HashSet;
use std::sync::Arc;
use codex_features::Feature;
use codex_features::Features;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::ToolInfo;
use codex_models_manager::test_support::construct_model_info_offline_for_tests;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::SessionSource;
use codex_tools::ToolName;
use codex_tools::ToolsConfig;
use codex_tools::ToolsConfigParams;
use pretty_assertions::assert_eq;
use rmcp::model::JsonObject;
use rmcp::model::Tool;
@@ -94,34 +86,14 @@ fn tool_names(tools: &[ToolInfo]) -> HashSet<ToolName> {
.collect()
}
async fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig {
let config = test_config().await;
let model_info =
construct_model_info_offline_for_tests("gpt-5.4", &config.to_models_manager_config());
let features = Features::with_defaults();
let available_models = Vec::new();
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
tools_config.search_tool = search_tool;
tools_config
}
#[tokio::test]
async fn directly_exposes_small_effective_tool_sets() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
let exposure =
build_mcp_tool_exposure(&mcp_tools, /*connectors*/ None, &config, &tools_config);
let exposure = build_mcp_tool_exposure(
&mcp_tools, /*connectors*/ None, &config, /*search_tool_enabled*/ true,
);
assert_eq!(tool_names(&exposure.direct_tools), tool_names(&mcp_tools));
assert!(exposure.deferred_tools.is_none());
@@ -130,11 +102,11 @@ async fn directly_exposes_small_effective_tool_sets() {
#[tokio::test]
async fn searches_large_effective_tool_sets() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD);
let exposure =
build_mcp_tool_exposure(&mcp_tools, /*connectors*/ None, &config, &tools_config);
let exposure = build_mcp_tool_exposure(
&mcp_tools, /*connectors*/ None, &config, /*search_tool_enabled*/ true,
);
assert!(exposure.direct_tools.is_empty());
let deferred_tools = exposure
@@ -151,7 +123,6 @@ async fn always_defer_feature_defers_apps_too() {
.features
.enable(Feature::ToolSearchAlwaysDeferMcpTools)
.expect("test config should allow feature update");
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mcp_tools = vec![
make_mcp_tool(
"rmcp",
@@ -176,7 +147,7 @@ async fn always_defer_feature_defers_apps_too() {
&mcp_tools,
Some(connectors.as_slice()),
&config,
&tools_config,
/*search_tool_enabled*/ true,
);
assert!(exposure.direct_tools.is_empty());
+6 -4
View File
@@ -43,14 +43,16 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec<Stri
(None, "shell_command") => serde_json::from_str::<ShellCommandToolCallParams>(arguments)
.ok()
.map(|params| {
if !invocation.turn.tools_config.allow_login_shell && params.login == Some(true) {
if !invocation.turn.config.permissions.allow_login_shell
&& params.login == Some(true)
{
#[allow(deprecated)]
let cwd = invocation.turn.resolve_path(params.workdir).to_path_buf();
return (Vec::new(), cwd);
}
let use_login_shell = params
.login
.unwrap_or(invocation.turn.tools_config.allow_login_shell);
.unwrap_or(invocation.turn.config.permissions.allow_login_shell);
let command = invocation
.session
.user_shell()
@@ -65,8 +67,8 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec<Stri
let command = crate::tools::handlers::unified_exec::get_command(
&params,
invocation.session.user_shell(),
&invocation.turn.tools_config.unified_exec_shell_mode,
invocation.turn.tools_config.allow_login_shell,
&invocation.turn.unified_exec_shell_mode,
invocation.turn.config.permissions.allow_login_shell,
)
.ok()?;
#[allow(deprecated)]
+3 -2
View File
@@ -93,6 +93,7 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::models::format_allow_prefixes;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::FileChange;
@@ -355,8 +356,8 @@ use codex_protocol::protocol::TokenUsageInfo;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::user_input::UserInput;
use codex_tools::ToolEnvironmentMode;
use codex_tools::ToolsConfig;
use codex_tools::ToolsConfigParams;
use codex_tools::UnifiedExecShellMode;
use codex_tools::shell_command_backend_for_features;
use codex_utils_absolute_path::AbsolutePathBuf;
#[cfg(test)]
use codex_utils_stream_parser::ProposedPlanSegment;
+15 -54
View File
@@ -1,4 +1,3 @@
use super::turn_context::image_generation_tool_auth_allowed;
use super::*;
use std::sync::atomic::AtomicBool;
@@ -23,61 +22,21 @@ pub(super) async fn spawn_review_thread(
let mut review_features = sess.features.clone();
let _ = review_features.disable(Feature::WebSearchRequest);
let _ = review_features.disable(Feature::WebSearchCached);
let _ = review_features.disable(Feature::Goals);
let review_web_search_mode = WebSearchMode::Disabled;
let goal_tools_supported = !config.ephemeral && parent_turn_context.tools_config.goal_tools;
let provider_capabilities = parent_turn_context.provider.capabilities();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &review_model_info,
available_models: &sess
.services
.models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await,
features: &review_features,
image_generation_tool_auth_allowed: image_generation_tool_auth_allowed(Some(
sess.services.auth_manager.as_ref(),
)),
web_search_mode: Some(review_web_search_mode),
session_source: parent_turn_context.session_source.clone(),
permission_profile: &parent_turn_context.permission_profile,
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
})
.with_namespace_tools_capability(provider_capabilities.namespace_tools)
.with_image_generation_capability(provider_capabilities.image_generation)
.with_web_search_capability(provider_capabilities.web_search)
.with_unified_exec_shell_mode_for_session(
let goal_tools_supported = !config.ephemeral && parent_turn_context.goal_tools_enabled();
let available_models = sess
.services
.models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await;
let shell_command_backend = shell_command_backend_for_features(review_features.get());
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
shell_command_backend,
crate::tools::tool_user_shell_type(sess.services.user_shell.as_ref()),
sess.services.shell_zsh_path.as_ref(),
sess.services.main_execve_wrapper_exe.as_ref(),
)
.with_web_search_config(/*web_search_config*/ None)
.with_allow_login_shell(config.permissions.allow_login_shell)
.with_environment_mode(parent_turn_context.tools_config.environment_mode)
.with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled)
.with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone())
.with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata)
.with_multi_agent_v2_tool_namespace(config.multi_agent_v2.tool_namespace.clone())
.with_multi_agent_v2_non_code_mode_only(config.multi_agent_v2.non_code_mode_only)
.with_goal_tools_allowed(goal_tools_supported)
.with_max_concurrent_threads_per_session(config.agent_max_threads)
.with_wait_agent_min_timeout_ms(
review_features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.min_wait_timeout_ms),
)
.with_wait_agent_max_timeout_ms(
review_features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.max_wait_timeout_ms),
)
.with_wait_agent_default_timeout_ms(
review_features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.default_wait_timeout_ms),
)
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
&config.agent_roles,
));
);
let review_prompt = resolved.prompt.clone();
let provider = parent_turn_context.provider.clone();
@@ -139,8 +98,10 @@ pub(super) async fn spawn_review_thread(
session_source,
thread_source: parent_turn_context.thread_source,
environments: parent_turn_context.environments.clone(),
tools_config,
features: parent_turn_context.features.clone(),
available_models,
unified_exec_shell_mode,
goal_tools_supported,
features: review_features,
ghost_snapshot: parent_turn_context.ghost_snapshot.clone(),
current_date: parent_turn_context.current_date.clone(),
timezone: parent_turn_context.timezone.clone(),
+8 -7
View File
@@ -536,8 +536,8 @@ async fn preview_session_start_hooks(
}
fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> ToolCallRuntime {
let router = Arc::new(ToolRouter::from_config(
&turn_context.tools_config,
let router = Arc::new(ToolRouter::from_turn_context(
&turn_context,
crate::tools::router::ToolRouterParams {
mcp_tools: None,
deferred_mcp_tools: None,
@@ -9259,8 +9259,8 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
.await
};
let deferred_mcp_tools = Some(tools.clone());
let router = ToolRouter::from_config(
&turn_context.tools_config,
let router = ToolRouter::from_turn_context(
&turn_context,
crate::tools::router::ToolRouterParams {
deferred_mcp_tools,
mcp_tools: Some(tools),
@@ -9828,9 +9828,10 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
turn_context_mut.permission_profile = PermissionProfile::Disabled;
let file_system_sandbox_policy = turn_context.file_system_sandbox_policy();
let command = session
.user_shell()
.derive_exec_args(command_script, turn_context.tools_config.allow_login_shell);
let command = session.user_shell().derive_exec_args(
command_script,
turn_context.config.permissions.allow_login_shell,
);
let exec_approval_requirement = session
.services
.exec_policy
+6 -4
View File
@@ -55,6 +55,8 @@ use crate::tools::parallel::ToolCallRuntime;
use crate::tools::registry::ToolArgumentDiffConsumer;
use crate::tools::router::ToolRouterParams;
use crate::tools::router::extension_tool_executors;
use crate::tools::spec_plan::search_tool_enabled;
use crate::tools::spec_plan::tool_suggest_enabled;
use crate::turn_diff_tracker::TurnDiffTracker;
use crate::turn_timing::record_turn_ttft_metric;
use crate::util::backoff;
@@ -1120,7 +1122,7 @@ pub(crate) async fn built_tools(
None
};
let auth = sess.services.auth_manager.auth().await;
let discoverable_tools = if apps_enabled && turn_context.tools_config.tool_suggest {
let discoverable_tools = if apps_enabled && tool_suggest_enabled(turn_context) {
if let Some(accessible_connectors) = accessible_connectors_with_enabled_state.as_ref() {
match connectors::list_tool_suggest_discoverable_tools_with_auth(
&turn_context.config,
@@ -1152,12 +1154,12 @@ pub(crate) async fn built_tools(
&all_mcp_tools,
connectors.as_deref(),
&turn_context.config,
&turn_context.tools_config,
search_tool_enabled(turn_context),
);
let mcp_tools = has_mcp_servers.then_some(mcp_tool_exposure.direct_tools);
let deferred_mcp_tools = mcp_tool_exposure.deferred_tools;
Ok(Arc::new(ToolRouter::from_config(
&turn_context.tools_config,
Ok(Arc::new(ToolRouter::from_turn_context(
turn_context,
ToolRouterParams {
mcp_tools,
deferred_mcp_tools,
+26 -122
View File
@@ -14,10 +14,6 @@ use codex_sandboxing::policy_transforms::effective_network_sandbox_policy;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
pub(super) fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManager>) -> bool {
auth_manager.is_some_and(AuthManager::current_auth_uses_codex_backend)
}
#[derive(Clone, Debug)]
pub(crate) struct TurnSkillsContext {
pub(crate) outcome: Arc<SkillLoadOutcome>,
@@ -84,7 +80,9 @@ pub struct TurnContext {
pub(crate) network: Option<NetworkProxy>,
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
pub(crate) shell_environment_policy: ShellEnvironmentPolicy,
pub(crate) tools_config: ToolsConfig,
pub(crate) available_models: Vec<ModelPreset>,
pub(crate) unified_exec_shell_mode: UnifiedExecShellMode,
pub(crate) goal_tools_supported: bool,
pub features: ManagedFeatures,
pub(crate) ghost_snapshot: GhostSnapshotConfig,
pub(crate) final_output_json_schema: Option<Value>,
@@ -156,6 +154,14 @@ impl TurnContext {
self.features.apps_enabled_for_auth(uses_codex_backend)
}
pub(crate) fn tool_environment_mode(&self) -> ToolEnvironmentMode {
ToolEnvironmentMode::from_count(self.environments.turn_environments.len())
}
pub(crate) fn goal_tools_enabled(&self) -> bool {
self.goal_tools_supported && self.features.get().enabled(Feature::Goals)
}
pub(crate) async fn with_model(
&self,
model: String,
@@ -195,61 +201,9 @@ impl TurnContext {
/*developer_instructions*/ None,
);
let features = self.features.clone();
let provider_capabilities = self.provider.capabilities();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await,
features: &features,
image_generation_tool_auth_allowed: image_generation_tool_auth_allowed(
self.auth_manager.as_deref(),
),
web_search_mode: self.tools_config.web_search_mode,
session_source: self.session_source.clone(),
permission_profile: &self.permission_profile,
windows_sandbox_level: self.windows_sandbox_level,
})
.with_namespace_tools_capability(provider_capabilities.namespace_tools)
.with_image_generation_capability(provider_capabilities.image_generation)
.with_web_search_capability(provider_capabilities.web_search)
.with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone())
.with_web_search_config(self.tools_config.web_search_config.clone())
.with_allow_login_shell(self.tools_config.allow_login_shell)
.with_environment_mode(self.tools_config.environment_mode)
.with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled)
.with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone())
.with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata)
.with_multi_agent_v2_tool_namespace(config.multi_agent_v2.tool_namespace.clone())
.with_multi_agent_v2_non_code_mode_only(config.multi_agent_v2.non_code_mode_only)
.with_goal_tools_allowed(self.tools_config.goal_tools)
.with_max_concurrent_threads_per_session(
config
.features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.max_concurrent_threads_per_session),
)
.with_wait_agent_min_timeout_ms(
config
.features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.min_wait_timeout_ms),
)
.with_wait_agent_max_timeout_ms(
config
.features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.max_wait_timeout_ms),
)
.with_wait_agent_default_timeout_ms(
config
.features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.default_wait_timeout_ms),
)
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
&config.agent_roles,
));
let available_models = models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await;
Self {
sub_id: self.sub_id.clone(),
@@ -283,7 +237,9 @@ impl TurnContext {
network: self.network.clone(),
windows_sandbox_level: self.windows_sandbox_level,
shell_environment_policy: self.shell_environment_policy.clone(),
tools_config,
available_models,
unified_exec_shell_mode: self.unified_exec_shell_mode.clone(),
goal_tools_supported: self.goal_tools_supported,
features,
ghost_snapshot: self.ghost_snapshot.clone(),
final_output_json_schema: self.final_output_json_schema.clone(),
@@ -503,72 +459,18 @@ impl Session {
model_info.slug.as_str(),
);
let session_source = session_configuration.session_source.clone();
let image_generation_tool_auth_allowed =
image_generation_tool_auth_allowed(auth_manager.as_deref());
let auth_manager_for_context = auth_manager.clone();
let provider_for_context = create_model_provider(provider, auth_manager);
let provider_capabilities = provider_for_context.capabilities();
let session_telemetry_for_context = session_telemetry;
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &models_manager.try_list_models().unwrap_or_default(),
features: &per_turn_config.features,
image_generation_tool_auth_allowed,
web_search_mode: Some(per_turn_config.web_search_mode.value()),
session_source: session_source.clone(),
permission_profile: &session_configuration.permission_profile(),
windows_sandbox_level: session_configuration.windows_sandbox_level,
})
.with_namespace_tools_capability(provider_capabilities.namespace_tools)
.with_image_generation_capability(provider_capabilities.image_generation)
.with_web_search_capability(provider_capabilities.web_search)
.with_unified_exec_shell_mode_for_session(
let available_models = models_manager.try_list_models().unwrap_or_default();
let shell_command_backend =
shell_command_backend_for_features(per_turn_config.features.get());
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
shell_command_backend,
crate::tools::tool_user_shell_type(user_shell),
shell_zsh_path,
main_execve_wrapper_exe,
)
.with_web_search_config(per_turn_config.web_search_config.clone())
.with_allow_login_shell(per_turn_config.permissions.allow_login_shell)
.with_environment_mode(ToolEnvironmentMode::from_count(
environments.turn_environments.len(),
))
.with_spawn_agent_usage_hint(per_turn_config.multi_agent_v2.usage_hint_enabled)
.with_spawn_agent_usage_hint_text(per_turn_config.multi_agent_v2.usage_hint_text.clone())
.with_hide_spawn_agent_metadata(per_turn_config.multi_agent_v2.hide_spawn_agent_metadata)
.with_multi_agent_v2_tool_namespace(per_turn_config.multi_agent_v2.tool_namespace.clone())
.with_multi_agent_v2_non_code_mode_only(per_turn_config.multi_agent_v2.non_code_mode_only)
.with_goal_tools_allowed(goal_tools_supported)
.with_max_concurrent_threads_per_session(
per_turn_config
.features
.enabled(Feature::MultiAgentV2)
.then_some(
per_turn_config
.multi_agent_v2
.max_concurrent_threads_per_session,
),
)
.with_wait_agent_min_timeout_ms(
per_turn_config
.features
.enabled(Feature::MultiAgentV2)
.then_some(per_turn_config.multi_agent_v2.min_wait_timeout_ms),
)
.with_wait_agent_max_timeout_ms(
per_turn_config
.features
.enabled(Feature::MultiAgentV2)
.then_some(per_turn_config.multi_agent_v2.max_wait_timeout_ms),
)
.with_wait_agent_default_timeout_ms(
per_turn_config
.features
.enabled(Feature::MultiAgentV2)
.then_some(per_turn_config.multi_agent_v2.default_wait_timeout_ms),
)
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
&per_turn_config.agent_roles,
));
);
let mut per_turn_config = per_turn_config;
per_turn_config.service_tier = per_turn_config
@@ -616,7 +518,9 @@ impl Session {
network,
windows_sandbox_level: session_configuration.windows_sandbox_level,
shell_environment_policy: per_turn_config.permissions.shell_environment_policy.clone(),
tools_config,
available_models,
unified_exec_shell_mode,
goal_tools_supported,
features: per_turn_config.features.clone(),
ghost_snapshot: per_turn_config.ghost_snapshot.clone(),
final_output_json_schema: None,
@@ -268,8 +268,8 @@ async fn handle_output_item_done_returns_contributed_last_agent_message() {
session.services.extensions = Arc::new(builder.build());
let session = Arc::new(session);
let turn_context = Arc::new(turn_context);
let router = Arc::new(ToolRouter::from_config(
&turn_context.tools_config,
let router = Arc::new(ToolRouter::from_turn_context(
&turn_context,
crate::tools::router::ToolRouterParams {
mcp_tools: None,
deferred_mcp_tools: None,
@@ -183,7 +183,7 @@ impl ToolExecutor<ToolInvocation> for ShellCommandHandler {
session.as_ref(),
turn.as_ref(),
session.conversation_id,
turn.tools_config.allow_login_shell,
turn.config.permissions.allow_login_shell,
)?;
let shell_type = Some(session.user_shell().shell_type.clone());
run_exec_like(RunExecLikeArgs {
@@ -144,8 +144,8 @@ impl ToolExecutor<ToolInvocation> for ExecCommandHandler {
let resolved_command = get_command(
&args,
session.user_shell(),
&turn.tools_config.unified_exec_shell_mode,
turn.tools_config.allow_login_shell,
&turn.unified_exec_shell_mode,
turn.config.permissions.allow_login_shell,
)
.map_err(FunctionCallError::RespondToModel)?;
let command = resolved_command.command;
+1
View File
@@ -14,6 +14,7 @@ pub(crate) mod runtimes;
pub(crate) mod sandboxing;
pub(crate) mod spec_plan;
pub(crate) mod tool_dispatch_trace;
pub(crate) mod tool_family;
pub(crate) mod tool_search_entry;
use std::borrow::Cow;
+9 -2
View File
@@ -287,8 +287,15 @@ impl ToolRegistry {
}
#[cfg(test)]
pub(crate) fn has_tool(&self, name: &ToolName) -> bool {
self.tool(name).is_some()
pub(crate) fn tool_names_for_test(&self) -> Vec<ToolName> {
let mut names = self.tools.keys().cloned().collect::<Vec<_>>();
names.sort();
names
}
#[cfg(test)]
pub(crate) fn tool_exposure(&self, name: &ToolName) -> Option<ToolExposure> {
self.tools.get(name).map(|tool| tool.exposure())
}
pub(crate) fn create_diff_consumer(
+15 -3
View File
@@ -17,7 +17,6 @@ use codex_tools::ToolCall as ExtensionToolCall;
use codex_tools::ToolExecutor;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_tools::ToolsConfig;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use tokio_util::sync::CancellationToken;
@@ -46,8 +45,8 @@ pub(crate) struct ToolRouterParams<'a> {
}
impl ToolRouter {
pub fn from_config(config: &ToolsConfig, params: ToolRouterParams<'_>) -> Self {
build_tool_router(config, params)
pub fn from_turn_context(turn_context: &TurnContext, params: ToolRouterParams<'_>) -> Self {
build_tool_router(turn_context, params)
}
pub(crate) fn from_parts(registry: ToolRegistry, model_visible_specs: Vec<ToolSpec>) -> Self {
@@ -61,6 +60,19 @@ impl ToolRouter {
self.model_visible_specs.clone()
}
#[cfg(test)]
pub(crate) fn registered_tool_names_for_test(&self) -> Vec<ToolName> {
self.registry.tool_names_for_test()
}
#[cfg(test)]
pub(crate) fn tool_exposure_for_test(
&self,
name: &ToolName,
) -> Option<crate::tools::registry::ToolExposure> {
self.registry.tool_exposure(name)
}
pub(crate) fn create_diff_consumer(
&self,
tool_name: &ToolName,
+10 -10
View File
@@ -106,8 +106,8 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
.await
.list_all_tools()
.await;
let router = ToolRouter::from_config(
&turn.tools_config,
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: Some(mcp_tools),
@@ -172,8 +172,8 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
#[tokio::test]
async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
let (_, turn) = make_session_and_context().await;
let router = ToolRouter::from_config(
&turn.tools_config,
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: Some(vec![
@@ -220,8 +220,8 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
#[tokio::test]
async fn tools_without_handlers_do_not_support_parallel() -> anyhow::Result<()> {
let (_, turn) = make_session_and_context().await;
let router = ToolRouter::from_config(
&turn.tools_config,
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: None,
@@ -272,8 +272,8 @@ async fn specs_filter_deferred_dynamic_tools() -> anyhow::Result<()> {
},
];
let router = ToolRouter::from_config(
&turn.tools_config,
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: None,
@@ -328,8 +328,8 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
let (mut session, turn) = make_session_and_context().await;
session.services.extensions = extension_tool_test_registry();
let router = ToolRouter::from_config(
&turn.tools_config,
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: None,
+356 -222
View File
@@ -1,6 +1,4 @@
use crate::config::DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS;
use crate::config::DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS;
use crate::config::DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS;
use crate::session::turn_context::TurnContext;
use crate::tools::code_mode::execute_spec::create_code_mode_tool;
use crate::tools::context::ToolInvocation;
use crate::tools::handlers::ApplyPatchHandler;
@@ -8,8 +6,6 @@ use crate::tools::handlers::CodeModeExecuteHandler;
use crate::tools::handlers::CodeModeWaitHandler;
use crate::tools::handlers::CreateGoalHandler;
use crate::tools::handlers::DynamicToolHandler;
use crate::tools::handlers::ExecCommandHandler;
use crate::tools::handlers::ExecCommandHandlerOptions;
use crate::tools::handlers::GetGoalHandler;
use crate::tools::handlers::ListMcpResourceTemplatesHandler;
use crate::tools::handlers::ListMcpResourcesHandler;
@@ -19,13 +15,10 @@ use crate::tools::handlers::ReadMcpResourceHandler;
use crate::tools::handlers::RequestPermissionsHandler;
use crate::tools::handlers::RequestPluginInstallHandler;
use crate::tools::handlers::RequestUserInputHandler;
use crate::tools::handlers::ShellCommandHandler;
use crate::tools::handlers::ShellCommandHandlerOptions;
use crate::tools::handlers::TestSyncHandler;
use crate::tools::handlers::ToolSearchHandler;
use crate::tools::handlers::UpdateGoalHandler;
use crate::tools::handlers::ViewImageHandler;
use crate::tools::handlers::WriteStdinHandler;
use crate::tools::handlers::agent_jobs::ReportAgentJobResultHandler;
use crate::tools::handlers::agent_jobs::SpawnAgentsOnCsvHandler;
use crate::tools::handlers::extension_tools::ExtensionToolAdapter;
@@ -55,9 +48,15 @@ use crate::tools::registry::ToolRegistry;
use crate::tools::registry::override_tool_exposure;
use crate::tools::router::ToolRouter;
use crate::tools::router::ToolRouterParams;
use crate::tools::tool_family::shell::ShellToolsOptions;
use crate::tools::tool_family::shell::build_shell_tools;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_tools::DiscoverableTool;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
@@ -68,9 +67,12 @@ use codex_tools::ToolExecutor;
use codex_tools::ToolName;
use codex_tools::ToolOutput;
use codex_tools::ToolSpec;
use codex_tools::ToolsConfig;
use codex_tools::can_request_original_image_detail;
use codex_tools::collect_code_mode_exec_prompt_tool_definitions;
use codex_tools::default_namespace_description;
use codex_tools::request_user_input_available_modes;
use codex_tools::shell_command_backend_for_features;
use codex_tools::shell_type_for_model_and_features;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::sync::Arc;
@@ -78,8 +80,34 @@ use tracing::warn;
const MULTI_AGENT_V2_NAMESPACE_DESCRIPTION: &str = "Tools for spawning and managing sub-agents.";
type PlannedRuntime = Arc<dyn CoreToolRuntime>;
#[derive(Default)]
struct PlannedTools {
runtimes: Vec<PlannedRuntime>,
hosted_specs: Vec<ToolSpec>,
}
impl PlannedTools {
fn add_runtime<T>(&mut self, runtime: T)
where
T: CoreToolRuntime + 'static,
{
self.runtimes.push(Arc::new(runtime));
}
fn add_runtime_arc(&mut self, runtime: PlannedRuntime) {
self.runtimes.push(runtime);
}
fn runtimes(&self) -> &[PlannedRuntime] {
&self.runtimes
}
}
#[derive(Clone, Copy)]
struct ToolRegistryBuildParams<'a> {
struct CoreToolPlanContext<'a> {
turn_context: &'a TurnContext,
mcp_tools: Option<&'a [ToolInfo]>,
deferred_mcp_tools: Option<&'a [ToolInfo]>,
discoverable_tools: Option<&'a [DiscoverableTool]>,
@@ -89,13 +117,16 @@ struct ToolRegistryBuildParams<'a> {
wait_agent_timeouts: WaitAgentTimeoutOptions,
}
pub(crate) fn build_tool_router(config: &ToolsConfig, params: ToolRouterParams<'_>) -> ToolRouter {
let (model_visible_specs, registry) = build_tool_specs_and_registry(config, params);
pub(crate) fn build_tool_router(
turn_context: &TurnContext,
params: ToolRouterParams<'_>,
) -> ToolRouter {
let (model_visible_specs, registry) = build_tool_specs_and_registry(turn_context, params);
ToolRouter::from_parts(registry, model_visible_specs)
}
fn build_tool_specs_and_registry(
config: &ToolsConfig,
turn_context: &TurnContext,
params: ToolRouterParams<'_>,
) -> (Vec<ToolSpec>, ToolRegistry) {
let ToolRouterParams {
@@ -107,65 +138,73 @@ fn build_tool_specs_and_registry(
} = params;
let default_agent_type_description =
crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new());
let mut executors = collect_tool_executors(
config,
ToolRegistryBuildParams {
mcp_tools: mcp_tools.as_deref(),
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
discoverable_tools: discoverable_tools.as_deref(),
extension_tool_executors: &extension_tool_executors,
dynamic_tools,
default_agent_type_description: &default_agent_type_description,
wait_agent_timeouts: wait_agent_timeout_options(config),
},
);
append_tool_search_executor(config, &mut executors);
prepend_code_mode_executors(config, &mut executors);
build_model_visible_specs_and_registry(config, executors, hosted_model_tool_specs(config))
let context = CoreToolPlanContext {
turn_context,
mcp_tools: mcp_tools.as_deref(),
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
discoverable_tools: discoverable_tools.as_deref(),
extension_tool_executors: &extension_tool_executors,
dynamic_tools,
default_agent_type_description: &default_agent_type_description,
wait_agent_timeouts: wait_agent_timeout_options(turn_context),
};
let mut planned_tools = PlannedTools::default();
add_tool_sources(&context, &mut planned_tools);
append_tool_search_executor(&context, &mut planned_tools);
prepend_code_mode_executors(&context, &mut planned_tools);
build_model_visible_specs_and_registry(turn_context, planned_tools)
}
fn build_model_visible_specs_and_registry(
config: &ToolsConfig,
executors: Vec<Arc<dyn CoreToolRuntime>>,
hosted_specs: Vec<ToolSpec>,
turn_context: &TurnContext,
planned_tools: PlannedTools,
) -> (Vec<ToolSpec>, ToolRegistry) {
let PlannedTools {
runtimes,
hosted_specs,
} = planned_tools;
let mut specs = Vec::new();
let mut seen_tool_names = HashSet::new();
for executor in &executors {
let tool_name = executor.tool_name();
for runtime in &runtimes {
let tool_name = runtime.tool_name();
if !seen_tool_names.insert(tool_name.clone()) {
continue;
}
let exposure = executor.exposure();
let exposure = runtime.exposure();
if exposure.is_direct()
&& !is_hidden_by_code_mode_only(config, &tool_name, exposure)
&& let Some(spec) = executor.spec()
&& !is_hidden_by_code_mode_only(turn_context, &tool_name, exposure)
&& let Some(spec) = runtime.spec()
{
specs.push(spec_for_model_request(config, exposure, spec));
specs.push(spec_for_model_request(turn_context, exposure, spec));
}
}
for spec in hosted_specs {
if !is_hidden_by_code_mode_only(config, &ToolName::plain(spec.name()), ToolExposure::Direct)
{
if !is_hidden_by_code_mode_only(
turn_context,
&ToolName::plain(spec.name()),
ToolExposure::Direct,
) {
specs.push(spec);
}
}
let registry = ToolRegistry::from_tools(executors);
let registry = ToolRegistry::from_tools(runtimes);
let model_visible_specs = merge_into_namespaces(specs)
.into_iter()
.filter(|spec| config.namespace_tools || !matches!(spec, ToolSpec::Namespace(_)))
.filter(|spec| {
namespace_tools_enabled(turn_context) || !matches!(spec, ToolSpec::Namespace(_))
})
.collect();
(model_visible_specs, registry)
}
fn spec_for_model_request(
config: &ToolsConfig,
turn_context: &TurnContext,
exposure: ToolExposure,
spec: ToolSpec,
) -> ToolSpec {
if config.code_mode_enabled
if code_mode_enabled(turn_context)
&& exposure != ToolExposure::DirectModelOnly
&& codex_code_mode::is_code_mode_nested_tool(spec.name())
{
@@ -175,33 +214,104 @@ fn spec_for_model_request(
}
}
pub(crate) fn hosted_model_tool_specs(config: &ToolsConfig) -> Vec<ToolSpec> {
pub(crate) fn hosted_model_tool_specs(turn_context: &TurnContext) -> Vec<ToolSpec> {
let mut specs = Vec::new();
let provider_capabilities = turn_context.provider.capabilities();
let web_search_mode = provider_capabilities
.web_search
.then_some(turn_context.config.web_search_mode.value());
let web_search_config = if provider_capabilities.web_search {
turn_context.config.web_search_config.as_ref()
} else {
None
};
if let Some(web_search_tool) = create_web_search_tool(WebSearchToolOptions {
web_search_mode: config.web_search_mode,
web_search_config: config.web_search_config.as_ref(),
web_search_tool_type: config.web_search_tool_type,
web_search_mode,
web_search_config,
web_search_tool_type: turn_context.model_info.web_search_tool_type,
}) {
specs.push(web_search_tool);
}
if config.image_gen_tool {
if image_generation_tool_enabled(turn_context) {
specs.push(create_image_generation_tool("png"));
}
specs
}
fn wait_agent_timeout_options(config: &ToolsConfig) -> WaitAgentTimeoutOptions {
if config.multi_agent_v2 {
pub(crate) fn search_tool_enabled(turn_context: &TurnContext) -> bool {
turn_context.model_info.supports_search_tool
}
pub(crate) fn tool_suggest_enabled(turn_context: &TurnContext) -> bool {
let features = turn_context.features.get();
features.enabled(Feature::ToolSuggest)
&& features.enabled(Feature::Apps)
&& features.enabled(Feature::Plugins)
}
fn namespace_tools_enabled(turn_context: &TurnContext) -> bool {
turn_context.provider.capabilities().namespace_tools
}
fn code_mode_enabled(turn_context: &TurnContext) -> bool {
turn_context.features.get().enabled(Feature::CodeMode)
}
fn code_mode_only_enabled(turn_context: &TurnContext) -> bool {
code_mode_enabled(turn_context) && turn_context.features.get().enabled(Feature::CodeModeOnly)
}
fn multi_agent_v2_enabled(turn_context: &TurnContext) -> bool {
turn_context.features.get().enabled(Feature::MultiAgentV2)
}
fn collab_tools_enabled(turn_context: &TurnContext) -> bool {
multi_agent_v2_enabled(turn_context) || turn_context.features.get().enabled(Feature::Collab)
}
fn goal_tools_enabled(turn_context: &TurnContext) -> bool {
turn_context.goal_tools_enabled()
&& !matches!(
turn_context.session_source,
SessionSource::SubAgent(SubAgentSource::Review)
)
}
fn agent_jobs_tools_enabled(turn_context: &TurnContext) -> bool {
turn_context.features.get().enabled(Feature::SpawnCsv)
}
fn agent_jobs_worker_tools_enabled(turn_context: &TurnContext) -> bool {
agent_jobs_tools_enabled(turn_context)
&& matches!(
&turn_context.session_source,
SessionSource::SubAgent(SubAgentSource::Other(label))
if label.starts_with("agent_job:")
)
}
fn image_generation_tool_enabled(turn_context: &TurnContext) -> bool {
turn_context
.auth_manager
.as_deref()
.is_some_and(AuthManager::current_auth_uses_codex_backend)
&& turn_context.provider.capabilities().image_generation
&& turn_context
.features
.get()
.enabled(Feature::ImageGeneration)
&& turn_context
.model_info
.input_modalities
.contains(&InputModality::Image)
}
fn wait_agent_timeout_options(turn_context: &TurnContext) -> WaitAgentTimeoutOptions {
if multi_agent_v2_enabled(turn_context) {
return WaitAgentTimeoutOptions {
default_timeout_ms: config
.wait_agent_default_timeout_ms
.unwrap_or(DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS),
min_timeout_ms: config
.wait_agent_min_timeout_ms
.unwrap_or(DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS),
max_timeout_ms: config
.wait_agent_max_timeout_ms
.unwrap_or(DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS),
default_timeout_ms: turn_context.config.multi_agent_v2.default_wait_timeout_ms,
min_timeout_ms: turn_context.config.multi_agent_v2.min_wait_timeout_ms,
max_timeout_ms: turn_context.config.multi_agent_v2.max_wait_timeout_ms,
};
}
@@ -212,20 +322,34 @@ fn wait_agent_timeout_options(config: &ToolsConfig) -> WaitAgentTimeoutOptions {
}
}
fn agent_type_description(config: &ToolsConfig, default_agent_type_description: &str) -> String {
if config.agent_type_description.is_empty() {
fn max_concurrent_threads_per_session(turn_context: &TurnContext) -> Option<usize> {
multi_agent_v2_enabled(turn_context).then_some(
turn_context
.config
.multi_agent_v2
.max_concurrent_threads_per_session,
)
}
fn agent_type_description(
turn_context: &TurnContext,
default_agent_type_description: &str,
) -> String {
let agent_type_description =
crate::agent::role::spawn_tool_spec::build(&turn_context.config.agent_roles);
if agent_type_description.is_empty() {
default_agent_type_description.to_string()
} else {
config.agent_type_description.clone()
agent_type_description
}
}
fn is_hidden_by_code_mode_only(
config: &ToolsConfig,
turn_context: &TurnContext,
tool_name: &ToolName,
exposure: ToolExposure,
) -> bool {
config.code_mode_only_enabled
code_mode_only_enabled(turn_context)
&& exposure != ToolExposure::DirectModelOnly
&& codex_code_mode::is_code_mode_nested_tool(&codex_tools::code_mode_name_for_tool_name(
tool_name,
@@ -233,11 +357,11 @@ fn is_hidden_by_code_mode_only(
}
fn build_code_mode_executors(
config: &ToolsConfig,
turn_context: &TurnContext,
executors: &[Arc<dyn CoreToolRuntime>],
deferred_tools_available: bool,
) -> Vec<Arc<dyn CoreToolRuntime>> {
if !config.code_mode_enabled {
if !code_mode_enabled(turn_context) {
return vec![];
}
@@ -262,7 +386,7 @@ fn build_code_mode_executors(
create_code_mode_tool(
&enabled_tools,
&namespace_descriptions,
config.code_mode_only_enabled,
code_mode_only_enabled(turn_context),
deferred_tools_available,
),
code_mode_nested_tool_specs,
@@ -339,203 +463,201 @@ fn code_mode_namespace_descriptions(
namespace_descriptions
}
fn collect_tool_executors(
config: &ToolsConfig,
params: ToolRegistryBuildParams<'_>,
) -> Vec<Arc<dyn CoreToolRuntime>> {
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
let mut executors = Vec::<Arc<dyn CoreToolRuntime>>::new();
fn add_tool_sources(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
add_shell_tools(context, planned_tools);
add_mcp_resource_tools(context, planned_tools);
add_core_utility_tools(context, planned_tools);
add_collaboration_tools(context, planned_tools);
add_mcp_runtime_tools(context, planned_tools);
add_dynamic_tools(context, planned_tools);
add_extension_tools(context, planned_tools);
planned_tools
.hosted_specs
.extend(hosted_model_tool_specs(context.turn_context));
}
if config.environment_mode.has_environment() {
let include_environment_id =
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
match &config.shell_type {
ConfigShellToolType::UnifiedExec => {
executors.push(Arc::new(ExecCommandHandler::new(
ExecCommandHandlerOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
include_environment_id,
},
)));
executors.push(Arc::new(WriteStdinHandler));
}
ConfigShellToolType::Disabled => {}
ConfigShellToolType::Default
| ConfigShellToolType::Local
| ConfigShellToolType::ShellCommand => {
executors.push(Arc::new(ShellCommandHandler::new(
ShellCommandHandlerOptions {
backend_config: config.shell_command_backend,
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
},
)));
}
}
fn add_shell_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
let turn_context = context.turn_context;
let features = turn_context.features.get();
planned_tools
.runtimes
.extend(build_shell_tools(ShellToolsOptions {
shell_type: shell_type_for_model_and_features(&turn_context.model_info, features),
shell_command_backend: shell_command_backend_for_features(features),
environment_mode: turn_context.tool_environment_mode(),
allow_login_shell: turn_context.config.permissions.allow_login_shell,
exec_permission_approvals_enabled: features.enabled(Feature::ExecPermissionApprovals),
}));
}
fn add_mcp_resource_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
if context.mcp_tools.is_some() {
planned_tools.add_runtime(ListMcpResourcesHandler);
planned_tools.add_runtime(ListMcpResourceTemplatesHandler);
planned_tools.add_runtime(ReadMcpResourceHandler);
}
}
fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
let turn_context = context.turn_context;
let features = turn_context.features.get();
let environment_mode = turn_context.tool_environment_mode();
planned_tools.add_runtime(PlanHandler);
if goal_tools_enabled(turn_context) {
planned_tools.add_runtime(GetGoalHandler);
planned_tools.add_runtime(CreateGoalHandler);
planned_tools.add_runtime(UpdateGoalHandler);
}
if config.environment_mode.has_environment()
&& config.shell_type != ConfigShellToolType::Disabled
{
match &config.shell_type {
ConfigShellToolType::UnifiedExec => {
executors.push(Arc::new(ShellCommandHandler::from(
config.shell_command_backend,
)));
}
ConfigShellToolType::Default
| ConfigShellToolType::Local
| ConfigShellToolType::ShellCommand
| ConfigShellToolType::Disabled => {}
}
planned_tools.add_runtime(RequestUserInputHandler {
available_modes: request_user_input_available_modes(features),
});
if features.enabled(Feature::RequestPermissionsTool) {
planned_tools.add_runtime(RequestPermissionsHandler);
}
if params.mcp_tools.is_some() {
executors.push(Arc::new(ListMcpResourcesHandler));
executors.push(Arc::new(ListMcpResourceTemplatesHandler));
executors.push(Arc::new(ReadMcpResourceHandler));
}
executors.push(Arc::new(PlanHandler));
if config.goal_tools {
executors.push(Arc::new(GetGoalHandler));
executors.push(Arc::new(CreateGoalHandler));
executors.push(Arc::new(UpdateGoalHandler));
}
executors.push(Arc::new(RequestUserInputHandler {
available_modes: config.request_user_input_available_modes.clone(),
}));
if config.request_permissions_tool_enabled {
executors.push(Arc::new(RequestPermissionsHandler));
}
if config.tool_suggest
if tool_suggest_enabled(turn_context)
&& let Some(discoverable_tools) =
params.discoverable_tools.filter(|tools| !tools.is_empty())
context.discoverable_tools.filter(|tools| !tools.is_empty())
{
executors.push(Arc::new(RequestPluginInstallHandler::new(
discoverable_tools,
)));
planned_tools.add_runtime(RequestPluginInstallHandler::new(discoverable_tools));
}
if config.environment_mode.has_environment() && config.apply_patch_tool_type.is_some() {
let include_environment_id =
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
executors.push(Arc::new(ApplyPatchHandler::new(include_environment_id)));
if environment_mode.has_environment() && turn_context.model_info.apply_patch_tool_type.is_some()
{
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
planned_tools.add_runtime(ApplyPatchHandler::new(include_environment_id));
}
if config
if turn_context
.model_info
.experimental_supported_tools
.iter()
.any(|tool| tool == "test_sync_tool")
{
executors.push(Arc::new(TestSyncHandler));
planned_tools.add_runtime(TestSyncHandler);
}
if config.environment_mode.has_environment() {
let include_environment_id =
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
executors.push(Arc::new(ViewImageHandler::new(ViewImageToolOptions {
can_request_original_image_detail: config.can_request_original_image_detail,
if environment_mode.has_environment() {
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
planned_tools.add_runtime(ViewImageHandler::new(ViewImageToolOptions {
can_request_original_image_detail: can_request_original_image_detail(
&turn_context.model_info,
),
include_environment_id,
})));
}));
}
}
if config.collab_tools {
if config.multi_agent_v2 {
let exposure = if config.multi_agent_v2_non_code_mode_only {
fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
let turn_context = context.turn_context;
if collab_tools_enabled(turn_context) {
if multi_agent_v2_enabled(turn_context) {
let exposure = if turn_context.config.multi_agent_v2.non_code_mode_only {
ToolExposure::DirectModelOnly
} else {
ToolExposure::Direct
};
let tool_namespace = config
.namespace_tools
.then_some(config.multi_agent_v2_tool_namespace.as_deref())
let tool_namespace = namespace_tools_enabled(turn_context)
.then_some(turn_context.config.multi_agent_v2.tool_namespace.as_deref())
.flatten();
let agent_type_description =
agent_type_description(config, params.default_agent_type_description);
executors.push(multi_agent_v2_handler(
agent_type_description(turn_context, context.default_agent_type_description);
planned_tools.add_runtime_arc(multi_agent_v2_handler(
SpawnAgentHandlerV2::new(SpawnAgentToolOptions {
available_models: config.available_models.clone(),
available_models: turn_context.available_models.clone(),
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
hide_agent_type_model_reasoning: turn_context
.config
.multi_agent_v2
.hide_spawn_agent_metadata,
include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
max_concurrent_threads_per_session: max_concurrent_threads_per_session(
turn_context,
),
}),
exposure,
tool_namespace,
));
executors.push(multi_agent_v2_handler(
planned_tools.add_runtime_arc(multi_agent_v2_handler(
SendMessageHandlerV2,
exposure,
tool_namespace,
));
executors.push(multi_agent_v2_handler(
planned_tools.add_runtime_arc(multi_agent_v2_handler(
FollowupTaskHandlerV2,
exposure,
tool_namespace,
));
executors.push(multi_agent_v2_handler(
WaitAgentHandlerV2::new(params.wait_agent_timeouts),
planned_tools.add_runtime_arc(multi_agent_v2_handler(
WaitAgentHandlerV2::new(context.wait_agent_timeouts),
exposure,
tool_namespace,
));
executors.push(multi_agent_v2_handler(
planned_tools.add_runtime_arc(multi_agent_v2_handler(
CloseAgentHandlerV2,
exposure,
tool_namespace,
));
executors.push(multi_agent_v2_handler(
planned_tools.add_runtime_arc(multi_agent_v2_handler(
ListAgentsHandlerV2,
exposure,
tool_namespace,
));
} else {
let agent_type_description =
agent_type_description(config, params.default_agent_type_description);
executors.push(Arc::new(SpawnAgentHandler::new(SpawnAgentToolOptions {
available_models: config.available_models.clone(),
agent_type_description(turn_context, context.default_agent_type_description);
planned_tools.add_runtime(SpawnAgentHandler::new(SpawnAgentToolOptions {
available_models: turn_context.available_models.clone(),
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
})));
executors.push(Arc::new(SendInputHandler));
executors.push(Arc::new(ResumeAgentHandler));
executors.push(Arc::new(WaitAgentHandler::new(params.wait_agent_timeouts)));
executors.push(Arc::new(CloseAgentHandler));
hide_agent_type_model_reasoning: turn_context
.config
.multi_agent_v2
.hide_spawn_agent_metadata,
include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
max_concurrent_threads_per_session: max_concurrent_threads_per_session(
turn_context,
),
}));
planned_tools.add_runtime(SendInputHandler);
planned_tools.add_runtime(ResumeAgentHandler);
planned_tools.add_runtime(WaitAgentHandler::new(context.wait_agent_timeouts));
planned_tools.add_runtime(CloseAgentHandler);
}
}
if config.agent_jobs_tools {
executors.push(Arc::new(SpawnAgentsOnCsvHandler));
if config.agent_jobs_worker_tools {
executors.push(Arc::new(ReportAgentJobResultHandler));
if agent_jobs_tools_enabled(turn_context) {
planned_tools.add_runtime(SpawnAgentsOnCsvHandler);
if agent_jobs_worker_tools_enabled(turn_context) {
planned_tools.add_runtime(ReportAgentJobResultHandler);
}
}
}
if let Some(mcp_tools) = params.mcp_tools {
fn add_mcp_runtime_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
if let Some(mcp_tools) = context.mcp_tools {
for tool in mcp_tools {
executors.push(Arc::new(McpHandler::new(tool.clone())));
planned_tools.add_runtime(McpHandler::new(tool.clone()));
}
}
if let Some(deferred_mcp_tools) = params.deferred_mcp_tools {
if let Some(deferred_mcp_tools) = context.deferred_mcp_tools {
for tool in deferred_mcp_tools {
executors.push(Arc::new(McpHandler::with_exposure(
planned_tools.add_runtime(McpHandler::with_exposure(
tool.clone(),
ToolExposure::Deferred,
)));
));
}
}
}
for tool in params.dynamic_tools {
let Some(handler) = DynamicToolHandler::new(tool).map(Arc::new) else {
fn add_dynamic_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
for tool in context.dynamic_tools {
let Some(handler) = DynamicToolHandler::new(tool) else {
tracing::error!(
"Failed to convert dynamic tool {:?} to OpenAI tool",
tool.name
@@ -543,23 +665,31 @@ fn collect_tool_executors(
continue;
};
executors.push(handler);
planned_tools.add_runtime(handler);
}
}
append_extension_tool_executors(config, params.extension_tool_executors, &mut executors);
executors
fn add_extension_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
// Extension ToolContributor implementations are resolved into executors
// before planning. Core only adapts those executors into its runtime set.
append_extension_tool_executors(
context.turn_context,
context.extension_tool_executors,
planned_tools,
);
}
fn append_tool_search_executor(
config: &ToolsConfig,
executors: &mut Vec<Arc<dyn CoreToolRuntime>>,
context: &CoreToolPlanContext<'_>,
planned_tools: &mut PlannedTools,
) {
if !(config.search_tool && config.namespace_tools) {
let turn_context = context.turn_context;
if !(search_tool_enabled(turn_context) && namespace_tools_enabled(turn_context)) {
return;
}
let search_infos = executors
let search_infos = planned_tools
.runtimes()
.iter()
.filter(|executor| executor.exposure() == ToolExposure::Deferred)
.filter_map(|executor| executor.search_info())
@@ -568,42 +698,49 @@ fn append_tool_search_executor(
return;
}
executors.push(Arc::new(ToolSearchHandler::new(search_infos)));
planned_tools.add_runtime(ToolSearchHandler::new(search_infos));
}
fn prepend_code_mode_executors(
config: &ToolsConfig,
executors: &mut Vec<Arc<dyn CoreToolRuntime>>,
context: &CoreToolPlanContext<'_>,
planned_tools: &mut PlannedTools,
) {
let deferred_tools_available = config.search_tool
&& executors
let turn_context = context.turn_context;
let deferred_tools_available = search_tool_enabled(turn_context)
&& planned_tools
.runtimes()
.iter()
.any(|executor| executor.exposure() == ToolExposure::Deferred);
let code_mode_executors =
build_code_mode_executors(config, executors, deferred_tools_available);
executors.splice(0..0, code_mode_executors);
let code_mode_executors = build_code_mode_executors(
turn_context,
planned_tools.runtimes(),
deferred_tools_available,
);
planned_tools.runtimes.splice(0..0, code_mode_executors);
}
fn append_extension_tool_executors(
config: &ToolsConfig,
turn_context: &TurnContext,
executors: &[Arc<dyn ToolExecutor<ExtensionToolCall>>],
registered_executors: &mut Vec<Arc<dyn CoreToolRuntime>>,
planned_tools: &mut PlannedTools,
) {
if executors.is_empty() {
return;
}
let mut reserved_tool_names = registered_executors
let mut reserved_tool_names = planned_tools
.runtimes()
.iter()
.map(|executor| executor.tool_name())
.collect::<HashSet<_>>();
if config.code_mode_enabled {
if code_mode_enabled(turn_context) {
reserved_tool_names.insert(ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME));
reserved_tool_names.insert(ToolName::plain(codex_code_mode::WAIT_TOOL_NAME));
}
if config.search_tool
&& config.namespace_tools
&& registered_executors
if search_tool_enabled(turn_context)
&& namespace_tools_enabled(turn_context)
&& planned_tools
.runtimes()
.iter()
.any(|executor| executor.exposure() == ToolExposure::Deferred)
{
@@ -616,7 +753,7 @@ fn append_extension_tool_executors(
warn!("Skipping extension tool `{tool_name}`: tool already registered");
continue;
}
registered_executors.push(Arc::new(ExtensionToolAdapter::new(executor)));
planned_tools.add_runtime(ExtensionToolAdapter::new(executor));
}
}
@@ -714,9 +851,6 @@ fn code_mode_namespace_name<'a>(
.map(|namespace_description| namespace_description.name.as_str())
}
#[cfg(test)]
#[path = "spec_plan_model_tests.rs"]
mod model_tests;
#[cfg(test)]
#[path = "spec_plan_tests.rs"]
mod tests;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
pub(crate) mod shell;
@@ -0,0 +1,72 @@
use std::sync::Arc;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_tools::ShellCommandBackendConfig;
use codex_tools::ToolEnvironmentMode;
use crate::tools::handlers::ExecCommandHandler;
use crate::tools::handlers::ExecCommandHandlerOptions;
use crate::tools::handlers::ShellCommandHandler;
use crate::tools::handlers::ShellCommandHandlerOptions;
use crate::tools::handlers::WriteStdinHandler;
use crate::tools::registry::CoreToolRuntime;
#[derive(Clone, Copy, Debug)]
pub(crate) struct ShellToolsOptions {
pub(crate) shell_type: ConfigShellToolType,
pub(crate) shell_command_backend: ShellCommandBackendConfig,
pub(crate) environment_mode: ToolEnvironmentMode,
pub(crate) allow_login_shell: bool,
pub(crate) exec_permission_approvals_enabled: bool,
}
pub(crate) fn build_shell_tools(options: ShellToolsOptions) -> Vec<Arc<dyn CoreToolRuntime>> {
let mut runtimes = Vec::new();
if !options.environment_mode.has_environment() {
return runtimes;
}
let include_environment_id = matches!(options.environment_mode, ToolEnvironmentMode::Multiple);
match options.shell_type {
ConfigShellToolType::UnifiedExec => {
add_runtime(
&mut runtimes,
ExecCommandHandler::new(ExecCommandHandlerOptions {
allow_login_shell: options.allow_login_shell,
exec_permission_approvals_enabled: options.exec_permission_approvals_enabled,
include_environment_id,
}),
);
add_runtime(&mut runtimes, WriteStdinHandler);
// Keep the legacy shell tool registered as a hidden runtime while
// unified exec is model-visible.
add_runtime(
&mut runtimes,
ShellCommandHandler::from(options.shell_command_backend),
);
}
ConfigShellToolType::Disabled => {}
ConfigShellToolType::Default
| ConfigShellToolType::Local
| ConfigShellToolType::ShellCommand => {
add_runtime(
&mut runtimes,
ShellCommandHandler::new(ShellCommandHandlerOptions {
backend_config: options.shell_command_backend,
allow_login_shell: options.allow_login_shell,
exec_permission_approvals_enabled: options.exec_permission_approvals_enabled,
}),
);
}
}
runtimes
}
fn add_runtime<T>(runtimes: &mut Vec<Arc<dyn CoreToolRuntime>>, runtime: T)
where
T: CoreToolRuntime + 'static,
{
runtimes.push(Arc::new(runtime));
}
@@ -1014,10 +1014,8 @@ impl UnifiedExecProcessManager {
local_policy_env,
};
let mut orchestrator = ToolOrchestrator::new();
let mut runtime = UnifiedExecRuntime::new(
self,
context.turn.tools_config.unified_exec_shell_mode.clone(),
);
let mut runtime =
UnifiedExecRuntime::new(self, context.turn.unified_exec_shell_mode.clone());
let file_system_sandbox_policy = context.turn.file_system_sandbox_policy();
let exec_approval_requirement = context
.session