From 4d55de675f22934da2f9c37884334216869c6ec0 Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Sun, 19 Apr 2026 14:09:27 -0700 Subject: [PATCH 1/2] chore(multiagent) skills instructions toggle Co-authored-by: Codex --- codex-rs/config/src/skills_config.rs | 4 + codex-rs/core/config.schema.json | 4 + codex-rs/core/src/config/config_tests.rs | 12 ++ codex-rs/core/src/config/mod.rs | 9 ++ codex-rs/core/src/session/mod.rs | 44 ++++---- .../tests/suite/subagent_notifications.rs | 103 ++++++++++++++++++ 6 files changed, 155 insertions(+), 21 deletions(-) diff --git a/codex-rs/config/src/skills_config.rs b/codex-rs/config/src/skills_config.rs index 3dbe65a24588..948b9060c48c 100644 --- a/codex-rs/config/src/skills_config.rs +++ b/codex-rs/config/src/skills_config.rs @@ -27,6 +27,10 @@ pub struct SkillsConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub bundled: Option, + /// Whether turns receive the automatic skills instructions block. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub include_instructions: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub config: Vec, } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 240d84fdb402..697b831c2d57 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1902,6 +1902,10 @@ "$ref": "#/definitions/SkillConfig" }, "type": "array" + }, + "include_instructions": { + "description": "Whether turns receive the automatic skills instructions block.", + "type": "boolean" } }, "type": "object" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index edf5b9ebc231..62cecb81ddcc 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -292,6 +292,9 @@ consolidation_model = "gpt-5" fn parses_bundled_skills_config() { let cfg: ConfigToml = toml::from_str( r#" +[skills] +include_instructions = false + [skills.bundled] enabled = false "#, @@ -302,6 +305,7 @@ enabled = false cfg.skills, Some(SkillsConfig { bundled: Some(BundledSkillsConfig { enabled: false }), + include_instructions: Some(false), config: Vec::new(), }) ); @@ -4888,6 +4892,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { guardian_policy_config: None, include_permissions_instructions: true, include_apps_instructions: true, + include_skill_instructions: true, include_environment_context: true, compact_prompt: None, commit_attribution: None, @@ -5038,6 +5043,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { guardian_policy_config: None, include_permissions_instructions: true, include_apps_instructions: true, + include_skill_instructions: true, include_environment_context: true, compact_prompt: None, commit_attribution: None, @@ -5186,6 +5192,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { guardian_policy_config: None, include_permissions_instructions: true, include_apps_instructions: true, + include_skill_instructions: true, include_environment_context: true, compact_prompt: None, commit_attribution: None, @@ -5319,6 +5326,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { guardian_policy_config: None, include_permissions_instructions: true, include_apps_instructions: true, + include_skill_instructions: true, include_environment_context: true, compact_prompt: None, commit_attribution: None, @@ -6270,6 +6278,9 @@ include_apps_instructions = false include_environment_context = false profile = "chatty" +[skills] +include_instructions = false + [profiles.chatty] include_permissions_instructions = true include_environment_context = true @@ -6284,6 +6295,7 @@ include_environment_context = true assert!(config.include_permissions_instructions); assert!(!config.include_apps_instructions); + assert!(!config.include_skill_instructions); assert!(config.include_environment_context); Ok(()) } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 6320bff2e399..7b5373f489be 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -292,6 +292,9 @@ pub struct Config { /// Whether to inject the `` developer block. pub include_apps_instructions: bool, + /// Whether to inject the `` developer block. + pub include_skill_instructions: bool, + /// Whether to inject the `` user block. pub include_environment_context: bool, @@ -1929,6 +1932,11 @@ impl Config { .include_apps_instructions .or(cfg.include_apps_instructions) .unwrap_or(true); + let include_skill_instructions = cfg + .skills + .as_ref() + .and_then(|skills| skills.include_instructions) + .unwrap_or(true); let include_environment_context = config_profile .include_environment_context .or(cfg.include_environment_context) @@ -2143,6 +2151,7 @@ impl Config { commit_attribution, include_permissions_instructions, include_apps_instructions, + include_skill_instructions, include_environment_context, // The config.toml omits "_mode" because it's a config file. However, "_mode" // is important in code to differentiate the mode from the store implementation. diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 5cda5debe688..9d240d9301bf 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -2423,28 +2423,30 @@ impl Session { developer_sections.push(apps_section); } } - let implicit_skills = turn_context - .turn_skills - .outcome - .allowed_skills_for_implicit_invocation(); - let rendered_skills = render_skills_section( - &implicit_skills, - default_skill_metadata_budget(turn_context.model_info.context_window), - SkillRenderSideEffects::ThreadStart { - session_telemetry: &self.services.session_telemetry, - }, - ); - if let Some(rendered_skills) = rendered_skills { - if rendered_skills.emit_warning { - self.send_event_raw(Event { - id: String::new(), - msg: EventMsg::Warning(WarningEvent { - message: THREAD_START_SKILLS_TRIMMED_WARNING_MESSAGE.to_string(), - }), - }) - .await; + if turn_context.config.include_skill_instructions { + let implicit_skills = turn_context + .turn_skills + .outcome + .allowed_skills_for_implicit_invocation(); + let rendered_skills = render_skills_section( + &implicit_skills, + default_skill_metadata_budget(turn_context.model_info.context_window), + SkillRenderSideEffects::ThreadStart { + session_telemetry: &self.services.session_telemetry, + }, + ); + if let Some(rendered_skills) = rendered_skills { + if rendered_skills.emit_warning { + self.send_event_raw(Event { + id: String::new(), + msg: EventMsg::Warning(WarningEvent { + message: THREAD_START_SKILLS_TRIMMED_WARNING_MESSAGE.to_string(), + }), + }) + .await; + } + developer_sections.push(rendered_skills.text); } - developer_sections.push(rendered_skills.text); } let loaded_plugins = self .services diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index a621aa571b58..860da3d708e4 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -19,6 +19,8 @@ use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; use pretty_assertions::assert_eq; use serde_json::json; +use std::fs; +use std::path::Path; use std::time::Duration; use tokio::time::Instant; use tokio::time::sleep; @@ -101,6 +103,14 @@ fn role_block(description: &str, role_name: &str) -> Option { Some(block.join("\n")) } +fn write_home_skill(codex_home: &Path, dir: &str, name: &str, description: &str) -> Result<()> { + let skill_dir = codex_home.join("skills").join(dir); + fs::create_dir_all(&skill_dir)?; + let contents = format!("---\nname: {name}\ndescription: {description}\n---\n\n# Body\n"); + fs::write(skill_dir.join("SKILL.md"), contents)?; + Ok(()) +} + async fn wait_for_spawned_thread_id(test: &TestCodex) -> Result { let deadline = Instant::now() + Duration::from_secs(2); loop { @@ -507,6 +517,99 @@ async fn spawned_multi_agent_v2_child_receives_xml_tagged_developer_context() -> Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn skills_toggle_skips_instructions_for_parent_and_spawned_child() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let spawn_args = serde_json::to_string(&json!({ + "message": CHILD_PROMPT, + "task_name": "worker", + }))?; + let spawn_turn = mount_sse_once_match( + &server, + |req: &wiremock::Request| body_contains(req, TURN_1_PROMPT), + sse(vec![ + ev_response_created("resp-turn1-1"), + ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args), + ev_completed("resp-turn1-1"), + ]), + ) + .await; + + let _child_request_log = mount_sse_once_match( + &server, + |req: &wiremock::Request| { + body_contains(req, CHILD_PROMPT) && !body_contains(req, SPAWN_CALL_ID) + }, + sse(vec![ + ev_response_created("resp-child-1"), + ev_completed("resp-child-1"), + ]), + ) + .await; + + let _turn1_followup = mount_sse_once_match( + &server, + |req: &wiremock::Request| body_contains(req, SPAWN_CALL_ID), + sse(vec![ + ev_response_created("resp-turn1-2"), + ev_assistant_message("msg-turn1-2", "parent done"), + ev_completed("resp-turn1-2"), + ]), + ) + .await; + + let mut builder = test_codex() + .with_pre_build_hook(|home| { + if let Err(err) = write_home_skill(home, "demo", "demo-skill", "demo skill") { + panic!("write home skill: {err}"); + } + }) + .with_config(|config| { + config + .features + .enable(Feature::Collab) + .expect("test config should allow feature update"); + config + .features + .enable(Feature::MultiAgentV2) + .expect("test config should allow feature update"); + config.include_skill_instructions = false; + }); + let test = builder.build(&server).await?; + + test.submit_turn(TURN_1_PROMPT).await?; + let parent_request = spawn_turn.single_request(); + assert!(!parent_request.body_contains_text("")); + assert!(!parent_request.body_contains_text("demo-skill")); + + let deadline = Instant::now() + Duration::from_secs(2); + let child_request = loop { + if let Some(request) = server + .received_requests() + .await + .unwrap_or_default() + .into_iter() + .find(|request| { + body_contains(request, CHILD_PROMPT) + && body_contains(request, "") + && !body_contains(request, SPAWN_CALL_ID) + }) + { + break request; + } + if Instant::now() >= deadline { + anyhow::bail!("timed out waiting for spawned child request"); + } + sleep(Duration::from_millis(10)).await; + }; + assert!(!body_contains(&child_request, "")); + assert!(!body_contains(&child_request, "demo-skill")); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn spawn_agent_role_overrides_requested_model_and_reasoning_settings() -> Result<()> { skip_if_no_network!(Ok(())); From fc42a1469d541ffe44b03691d3591354cf6f1d83 Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Sun, 19 Apr 2026 14:26:37 -0700 Subject: [PATCH 2/2] fix(guardian) disable skills message Co-authored-by: Codex --- codex-rs/core/src/guardian/review_session.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 3721dc8dfe53..d5bc96b71212 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -720,6 +720,7 @@ pub(crate) fn build_guardian_review_session_config( let mut guardian_config = parent_config.clone(); guardian_config.model = Some(active_model.to_string()); guardian_config.model_reasoning_effort = reasoning_effort; + guardian_config.include_skill_instructions = false; guardian_config.developer_instructions = Some( parent_config .guardian_policy_config @@ -874,6 +875,22 @@ mod tests { assert!(!guardian_config.features.enabled(Feature::CodexHooks)); } + #[tokio::test] + async fn guardian_review_session_config_disables_skill_instructions() { + let mut parent_config = crate::config::test_config().await; + parent_config.include_skill_instructions = true; + + let guardian_config = build_guardian_review_session_config( + &parent_config, + /*live_network_config*/ None, + "active-model", + /*reasoning_effort*/ None, + ) + .expect("guardian config"); + + assert!(!guardian_config.include_skill_instructions); + } + #[tokio::test(flavor = "current_thread")] async fn run_before_review_deadline_times_out_before_future_completes() { let outcome = run_before_review_deadline(