diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index b77ed5719..338a34e59 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -459,6 +459,9 @@ "skill_mcp_dependency_install": { "type": "boolean" }, + "spawn_csv": { + "type": "boolean" + }, "sqlite": { "type": "boolean" }, @@ -1957,6 +1960,9 @@ "skill_mcp_dependency_install": { "type": "boolean" }, + "spawn_csv": { + "type": "boolean" + }, "sqlite": { "type": "boolean" }, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 4eb66dea6..a21833c8f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -385,6 +385,7 @@ impl Codex { if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = session_source && depth >= config.agent_max_depth { + let _ = config.features.disable(Feature::SpawnCsv); let _ = config.features.disable(Feature::Collab); } diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 107b99ad4..7de8b8c7e 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -138,6 +138,8 @@ pub enum Feature { EnableRequestCompression, /// Enable collab tools. Collab, + /// Enable CSV-backed agent job tools. + SpawnCsv, /// Enable apps. Apps, /// Enable plugins. @@ -414,6 +416,9 @@ impl Features { } pub(crate) fn normalize_dependencies(&mut self) { + if self.enabled(Feature::SpawnCsv) && !self.enabled(Feature::Collab) { + self.enable(Feature::Collab); + } if self.enabled(Feature::JsReplToolsOnly) && !self.enabled(Feature::JsRepl) { tracing::warn!("js_repl_tools_only requires js_repl; disabling js_repl_tools_only"); self.disable(Feature::JsReplToolsOnly); @@ -693,6 +698,12 @@ pub const FEATURES: &[FeatureSpec] = &[ }, default_enabled: false, }, + FeatureSpec { + id: Feature::SpawnCsv, + key: "spawn_csv", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::Apps, key: "apps", @@ -997,6 +1008,27 @@ mod tests { assert_eq!(feature_for_key("collab"), Some(Feature::Collab)); } + #[test] + fn spawn_csv_is_under_development() { + assert_eq!(Feature::SpawnCsv.stage(), Stage::UnderDevelopment); + assert_eq!(Feature::SpawnCsv.default_enabled(), false); + } + + #[test] + fn spawn_csv_normalization_enables_multi_agent_one_way() { + let mut spawn_csv_features = Features::with_defaults(); + spawn_csv_features.enable(Feature::SpawnCsv); + spawn_csv_features.normalize_dependencies(); + assert_eq!(spawn_csv_features.enabled(Feature::SpawnCsv), true); + assert_eq!(spawn_csv_features.enabled(Feature::Collab), true); + + let mut collab_features = Features::with_defaults(); + collab_features.enable(Feature::Collab); + collab_features.normalize_dependencies(); + assert_eq!(collab_features.enabled(Feature::Collab), true); + assert_eq!(collab_features.enabled(Feature::SpawnCsv), false); + } + #[test] fn apps_require_feature_flag_and_chatgpt_auth() { let mut features = Features::with_defaults(); diff --git a/codex-rs/core/src/guardian.rs b/codex-rs/core/src/guardian.rs index d8c5d40e7..8db5af402 100644 --- a/codex-rs/core/src/guardian.rs +++ b/codex-rs/core/src/guardian.rs @@ -687,6 +687,7 @@ fn build_guardian_subagent_config( )?); } for feature in [ + Feature::SpawnCsv, Feature::Collab, Feature::WebSearchRequest, Feature::WebSearchCached, diff --git a/codex-rs/core/src/memories/phase2.rs b/codex-rs/core/src/memories/phase2.rs index 1a31bb335..75b29aeff 100644 --- a/codex-rs/core/src/memories/phase2.rs +++ b/codex-rs/core/src/memories/phase2.rs @@ -270,6 +270,7 @@ mod agent { // Approval policy agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); // Consolidation runs as an internal sub-agent and must not recursively delegate. + let _ = agent_config.features.disable(Feature::SpawnCsv); let _ = agent_config.features.disable(Feature::Collab); // Sandbox policy diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs index 1146be615..0a72355b5 100644 --- a/codex-rs/core/src/tasks/review.rs +++ b/codex-rs/core/src/tasks/review.rs @@ -100,6 +100,7 @@ async fn start_review_conversation( { panic!("by construction Constrained must always support Disabled: {err}"); } + let _ = sub_agent_config.features.disable(Feature::SpawnCsv); let _ = sub_agent_config.features.disable(Feature::Collab); // Set explicit review rubric for the sub-agent diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index 54e146518..a2d4e39b9 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -974,6 +974,7 @@ fn apply_spawn_agent_runtime_overrides( fn apply_spawn_agent_overrides(config: &mut Config, child_depth: i32) { if child_depth >= config.agent_max_depth { + let _ = config.features.disable(Feature::SpawnCsv); let _ = config.features.disable(Feature::Collab); } } diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 8f7a25076..a34ca7315 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -135,6 +135,7 @@ impl ToolsConfig { let include_js_repl_tools_only = include_js_repl && features.enabled(Feature::JsReplToolsOnly); let include_collab_tools = features.enabled(Feature::Collab); + let include_agent_jobs = features.enabled(Feature::SpawnCsv); let include_request_user_input = !matches!(session_source, SessionSource::SubAgent(_)); let include_default_mode_request_user_input = include_request_user_input && features.enabled(Feature::DefaultModeRequestUserInput); @@ -143,7 +144,6 @@ impl ToolsConfig { features.enabled(Feature::Artifact) && codex_artifacts::can_manage_artifact_runtime(); let include_image_gen_tool = features.enabled(Feature::ImageGeneration) && supports_image_generation(model_info); - let include_agent_jobs = include_collab_tools; let request_permission_enabled = features.enabled(Feature::RequestPermissions); let request_permissions_tool_enabled = features.enabled(Feature::RequestPermissionsTool); let shell_command_backend = @@ -2631,6 +2631,28 @@ mod tests { session_source: SessionSource::Cli, }); let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); + assert_contains_tool_names( + &tools, + &["spawn_agent", "send_input", "wait", "close_agent"], + ); + assert_lacks_tool_name(&tools, "spawn_agents_on_csv"); + } + + #[test] + fn test_build_specs_spawn_csv_enables_agent_jobs_and_collab_tools() { + let config = test_config(); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let mut features = Features::with_defaults(); + features.enable(Feature::SpawnCsv); + features.normalize_dependencies(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + }); + let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); assert_contains_tool_names( &tools, &[ @@ -2668,7 +2690,8 @@ mod tests { let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); - features.enable(Feature::Collab); + features.enable(Feature::SpawnCsv); + features.normalize_dependencies(); features.enable(Feature::Sqlite); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, diff --git a/codex-rs/core/tests/suite/agent_jobs.rs b/codex-rs/core/tests/suite/agent_jobs.rs index 190302a3e..443043c6f 100644 --- a/codex-rs/core/tests/suite/agent_jobs.rs +++ b/codex-rs/core/tests/suite/agent_jobs.rs @@ -224,7 +224,7 @@ async fn report_agent_job_result_rejects_wrong_thread() -> Result<()> { let mut builder = test_codex().with_config(|config| { config .features - .enable(Feature::Collab) + .enable(Feature::SpawnCsv) .expect("test config should allow feature update"); config .features @@ -290,7 +290,7 @@ async fn spawn_agents_on_csv_runs_and_exports() -> Result<()> { let mut builder = test_codex().with_config(|config| { config .features - .enable(Feature::Collab) + .enable(Feature::SpawnCsv) .expect("test config should allow feature update"); config .features @@ -333,7 +333,7 @@ async fn spawn_agents_on_csv_dedupes_item_ids() -> Result<()> { let mut builder = test_codex().with_config(|config| { config .features - .enable(Feature::Collab) + .enable(Feature::SpawnCsv) .expect("test config should allow feature update"); config .features @@ -391,7 +391,7 @@ async fn spawn_agents_on_csv_stop_halts_future_items() -> Result<()> { let mut builder = test_codex().with_config(|config| { config .features - .enable(Feature::Collab) + .enable(Feature::SpawnCsv) .expect("test config should allow feature update"); config .features