Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions codex-rs/config/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema {
if feature.id == codex_features::Feature::Artifact {
continue;
}
if feature.id == codex_features::Feature::CodeMode {
validation.properties.insert(
feature.key.to_string(),
schema_gen.subschema_for::<codex_features::FeatureToml<
codex_features::CodeModeConfigToml,
>>(),
);
continue;
}
if feature.id == codex_features::Feature::MultiAgentV2 {
validation.properties.insert(
feature.key.to_string(),
Expand Down
30 changes: 28 additions & 2 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,22 @@
},
"type": "object"
},
"CodeModeConfigToml": {
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"excluded_tool_namespaces": {
"description": "Exact tool namespaces to omit from the code-mode nested tool surface.",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"ConfigProfile": {
"additionalProperties": false,
"description": "Collection of common configuration options that a user can define as a unit in `config.toml`.",
Expand Down Expand Up @@ -410,7 +426,7 @@
"type": "boolean"
},
"code_mode": {
"type": "boolean"
"$ref": "#/definitions/FeatureToml_for_CodeModeConfigToml"
},
"code_mode_only": {
"type": "boolean"
Expand Down Expand Up @@ -830,6 +846,16 @@
}
]
},
"FeatureToml_for_CodeModeConfigToml": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#/definitions/CodeModeConfigToml"
}
]
},
"FeatureToml_for_MultiAgentV2ConfigToml": {
"anyOf": [
{
Expand Down Expand Up @@ -4533,7 +4559,7 @@
"type": "boolean"
},
"code_mode": {
"type": "boolean"
"$ref": "#/definitions/FeatureToml_for_CodeModeConfigToml"
},
"code_mode_only": {
"type": "boolean"
Expand Down
26 changes: 26 additions & 0 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,32 @@ async fn load_config_resolves_experimental_request_user_input_enabled() -> std::
Ok(())
}

#[tokio::test]
async fn load_config_resolves_code_mode_config() -> std::io::Result<()> {
let codex_home = tempdir()?;
let config_toml: ConfigToml = toml::from_str(
r#"
[features.code_mode]
enabled = true
excluded_tool_namespaces = ["mcp__codex_apps", "multi_agent_v1"]
"#,
)
.expect("TOML deserialization should succeed");
let config = Config::load_from_base_config_with_overrides(
config_toml,
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;

assert_eq!(
config.code_mode.excluded_tool_namespaces,
vec!["mcp__codex_apps".to_string(), "multi_agent_v1".to_string()]
);
assert!(config.features.enabled(Feature::CodeMode));
Ok(())
}

#[test]
fn rejects_provider_auth_with_env_key() {
let err = toml::from_str::<ConfigToml>(
Expand Down
29 changes: 29 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ use codex_core_plugins::PluginsConfigInput;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::LOCAL_FS;
use codex_features::AppsMcpPathOverrideConfigToml;
use codex_features::CodeModeConfigToml;
use codex_features::Feature;
use codex_features::FeatureConfigSource;
use codex_features::FeatureOverrides;
Expand Down Expand Up @@ -975,6 +976,9 @@ pub struct Config {
/// Whether to register the experimental request_user_input tool.
pub experimental_request_user_input_enabled: bool,

/// Configuration for the experimental code-mode tool surface.
pub code_mode: CodeModeConfig,

/// If set to `true`, used only the experimental unified exec tool.
pub use_experimental_unified_exec_tool: bool,

Expand Down Expand Up @@ -1027,6 +1031,11 @@ pub struct Config {
pub otel: codex_config::types::OtelConfig,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct CodeModeConfig {
pub excluded_tool_namespaces: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MultiAgentV2Config {
pub max_concurrent_threads_per_session: usize,
Expand Down Expand Up @@ -2288,6 +2297,17 @@ fn resolve_experimental_request_user_input_enabled(config_toml: &ConfigToml) ->
.is_none_or(|config| config.enabled)
}

fn resolve_code_mode_config(config_toml: &ConfigToml) -> CodeModeConfig {
let base = code_mode_toml_config(config_toml.features.as_ref());

CodeModeConfig {
excluded_tool_namespaces: base
.and_then(|config| config.excluded_tool_namespaces.as_ref())
.cloned()
.unwrap_or_default(),
}
}

fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config {
let base = multi_agent_v2_toml_config(config_toml.features.as_ref());
let default = MultiAgentV2Config::default();
Expand Down Expand Up @@ -2370,6 +2390,13 @@ fn resolve_optional_prompt_text(
}
}

fn code_mode_toml_config(features: Option<&FeaturesToml>) -> Option<&CodeModeConfigToml> {
match features?.code_mode.as_ref()? {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => Some(config),
}
}

fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiAgentV2ConfigToml> {
match features?.multi_agent_v2.as_ref()? {
FeatureToml::Enabled(_) => None,
Expand Down Expand Up @@ -3011,6 +3038,7 @@ impl Config {
let web_search_config = resolve_web_search_config(&cfg);
let experimental_request_user_input_enabled =
resolve_experimental_request_user_input_enabled(&cfg);
let code_mode = resolve_code_mode_config(&cfg);
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg);
let apps_mcp_path_override = if features.enabled(Feature::AppsMcpPathOverride) {
let base = apps_mcp_path_override_toml_config(cfg.features.as_ref());
Expand Down Expand Up @@ -3552,6 +3580,7 @@ impl Config {
web_search_mode: constrained_web_search_mode.value,
web_search_config,
experimental_request_user_input_enabled,
code_mode,
use_experimental_unified_exec_tool,
background_terminal_max_timeout,
ghost_snapshot,
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/core/src/session/config_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ mod tests {
spec.key
);
}
assert_eq!(
features.code_mode,
Some(FeatureToml::Enabled(
sc.original_config_do_not_use
.features
.enabled(Feature::CodeMode)
))
);

let multi_agent_v2 = features
.multi_agent_v2
Expand Down
45 changes: 32 additions & 13 deletions codex-rs/core/src/tools/spec_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,12 @@ fn build_model_visible_specs_and_registry(
if exposure.is_direct() && !is_hidden_by_code_mode_only(turn_context, &tool_name, exposure)
{
let spec = runtime.spec();
specs.push(spec_for_model_request(turn_context, exposure, spec));
specs.push(spec_for_model_request(
turn_context,
exposure,
&tool_name,
spec,
));
}
}
specs.extend(hosted_specs);
Expand All @@ -226,12 +231,14 @@ fn build_model_visible_specs_and_registry(
fn spec_for_model_request(
turn_context: &TurnContext,
exposure: ToolExposure,
tool_name: &ToolName,
spec: ToolSpec,
) -> ToolSpec {
if matches!(
turn_context.tool_mode,
ToolMode::CodeMode | ToolMode::CodeModeOnly
) && exposure != ToolExposure::DirectModelOnly
&& !is_excluded_from_code_mode(turn_context, tool_name)
&& codex_code_mode::is_code_mode_nested_tool(spec.name())
{
codex_tools::augment_tool_spec_for_code_mode(spec)
Expand Down Expand Up @@ -405,10 +412,19 @@ fn is_hidden_by_code_mode_only(
))
}

fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName) -> bool {
tool_name.namespace.as_ref().is_some_and(|namespace| {
turn_context
.config
.code_mode
.excluded_tool_namespaces
.contains(namespace)
})
}

fn build_code_mode_executors(
turn_context: &TurnContext,
executors: &[Arc<dyn CoreToolRuntime>],
deferred_tools_available: bool,
) -> Vec<Arc<dyn CoreToolRuntime>> {
if !matches!(
turn_context.tool_mode,
Expand All @@ -419,6 +435,8 @@ fn build_code_mode_executors(

let mut code_mode_nested_tool_specs = Vec::new();
let mut exec_prompt_tool_specs = Vec::new();
let mut deferred_tools_available = false;
let deferred_tools_guidance_enabled = search_tool_enabled(turn_context);
for executor in executors {
let exposure = executor.exposure();
if exposure == ToolExposure::DirectModelOnly {
Expand All @@ -428,9 +446,19 @@ fn build_code_mode_executors(
if exposure == ToolExposure::Hidden {
continue;
}

if is_excluded_from_code_mode(turn_context, &executor.tool_name()) {
continue;
}

let spec = executor.spec();

if exposure != ToolExposure::Deferred {
if exposure == ToolExposure::Deferred {
// Only show deferred-tool guidance when supported and an included spec is usable by code mode.
deferred_tools_available |= deferred_tools_guidance_enabled
&& !collect_code_mode_exec_prompt_tool_definitions(std::iter::once(&spec))
.is_empty();
} else {
exec_prompt_tool_specs.push(spec.clone());
}
code_mode_nested_tool_specs.push(spec);
Expand Down Expand Up @@ -839,16 +867,7 @@ fn prepend_code_mode_executors(
planned_tools: &mut PlannedTools,
) {
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(
turn_context,
planned_tools.runtimes(),
deferred_tools_available,
);
let code_mode_executors = build_code_mode_executors(turn_context, planned_tools.runtimes());
planned_tools.runtimes.splice(0..0, code_mode_executors);
}

Expand Down
36 changes: 36 additions & 0 deletions codex-rs/core/src/tools/spec_plan_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,42 @@ async fn code_mode_only_exposes_code_executor_and_hides_nested_tools() {
);
}

#[tokio::test]
async fn excluded_deferred_namespaces_do_not_enable_nested_tool_guidance() {
let plan = probe_with(
|turn| {
set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]);
set_feature(turn, Feature::Collab, /*enabled*/ false);
turn.model_info.supports_search_tool = true;
update_config(turn, |config| {
config.code_mode.excluded_tool_namespaces = vec!["excluded".to_string()];
});
},
ToolPlanInputs {
dynamic_tools: vec![dynamic_tool(
Some("excluded"),
"lookup",
/*defer_loading*/ true,
)],
..ToolPlanInputs::default()
},
)
.await;

let ToolSpec::Freeform(exec) = plan.visible_spec(codex_code_mode::PUBLIC_TOOL_NAME) else {
panic!("expected code mode exec tool");
};
assert!(
!exec
.description
.contains("Some deferred nested tools may be omitted")
);
plan.assert_registered_contains(&[
&ToolName::namespaced("excluded", "lookup").to_string(),
"tool_search",
]);
}

#[tokio::test]
async fn multi_agent_feature_selects_one_agent_tool_family() {
let v1 = probe(|turn| {
Expand Down
Loading
Loading