diff --git a/codex-rs/app-server/src/extensions.rs b/codex-rs/app-server/src/extensions.rs index 72eb267cbb..ad859af123 100644 --- a/codex-rs/app-server/src/extensions.rs +++ b/codex-rs/app-server/src/extensions.rs @@ -127,6 +127,9 @@ pub(crate) fn thread_extensions( catalog_deltas_enabled: config .features .enabled(codex_features::Feature::SkillCatalogDeltas), + omit_descriptions: config + .features + .enabled(codex_features::Feature::OmitSkillDescriptions), max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: config.bundled_skills_enabled(), cloud_skill_enabled: config.cloud_skill_enabled, diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 7b167742ad..591d056387 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -2170,6 +2170,9 @@ async fn run_debug_prompt_input_command( catalog_deltas_enabled: config .features .enabled(codex_features::Feature::SkillCatalogDeltas), + omit_descriptions: config + .features + .enabled(codex_features::Feature::OmitSkillDescriptions), max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: config.bundled_skills_enabled(), cloud_skill_enabled: config.cloud_skill_enabled, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index ae31c7e87a..c285c5e35a 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1194,6 +1194,9 @@ "omit_app_server_notification_media": { "type": "boolean" }, + "omit_skill_descriptions": { + "type": "boolean" + }, "output_token_replay": { "type": "boolean" }, @@ -7839,6 +7842,9 @@ "omit_app_server_notification_media": { "type": "boolean" }, + "omit_skill_descriptions": { + "type": "boolean" + }, "output_token_replay": { "type": "boolean" }, diff --git a/codex-rs/core/tests/suite/executor_discovery_v2.rs b/codex-rs/core/tests/suite/executor_discovery_v2.rs index ab62dfd944..24757e8bad 100644 --- a/codex-rs/core/tests/suite/executor_discovery_v2.rs +++ b/codex-rs/core/tests/suite/executor_discovery_v2.rs @@ -586,6 +586,7 @@ async fn check_acquisition(enabled: bool, version: &'static str) -> Result<()> { |config: &Config| codex_skills_extension::SkillsExtensionConfig { include_instructions: true, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, @@ -1102,6 +1103,7 @@ async fn auth_rotation_refreshes_plugin_acceptance_before_merged_skills() -> Res |_config: &Config| SkillsExtensionConfig { include_instructions: true, catalog_deltas_enabled: false, + omit_descriptions: false, max_context_tokens: None, bundled_skills_enabled: false, cloud_skill_enabled: true, diff --git a/codex-rs/core/tests/suite/guardian_environments_tests.rs b/codex-rs/core/tests/suite/guardian_environments_tests.rs index 39b05bc54d..3c7edea82a 100644 --- a/codex-rs/core/tests/suite/guardian_environments_tests.rs +++ b/codex-rs/core/tests/suite/guardian_environments_tests.rs @@ -350,6 +350,7 @@ async fn guardian_reviews_with_offline_primary_executor() -> Result<()> { catalog_deltas_enabled: config .features .enabled(codex_features::Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 1619257cdc..f993f2fb22 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -89,6 +89,7 @@ fn skills_extensions() -> Arc> { install(&mut extensions, |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: config.bundled_skills_enabled(), cloud_skill_enabled: config.cloud_skill_enabled, diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 38c36200f0..e60d60eb49 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -94,6 +94,7 @@ fn skills_extensions() -> Arc> { install(&mut extensions, |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: config.bundled_skills_enabled(), cloud_skill_enabled: config.cloud_skill_enabled, diff --git a/codex-rs/core/tests/suite/remote_env_required_skills_tests.rs b/codex-rs/core/tests/suite/remote_env_required_skills_tests.rs index 82708a7eda..20b31aee6b 100644 --- a/codex-rs/core/tests/suite/remote_env_required_skills_tests.rs +++ b/codex-rs/core/tests/suite/remote_env_required_skills_tests.rs @@ -76,6 +76,7 @@ async fn required_environment_skill_gates_inference(availability: Availability) |_: &Config| SkillsExtensionConfig { include_instructions: false, catalog_deltas_enabled: false, + omit_descriptions: false, max_context_tokens: None, bundled_skills_enabled: false, cloud_skill_enabled: false, diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index c87bcedf5f..54e8a4b461 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -2249,6 +2249,7 @@ async fn local_stdio_server_uses_runtime_fallback_cwd_when_config_omits_cwd() -> codex_skills_extension::SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: config.bundled_skills_enabled(), cloud_skill_enabled: config.cloud_skill_enabled, diff --git a/codex-rs/core/tests/suite/scenarios.rs b/codex-rs/core/tests/suite/scenarios.rs index a31a92e5a6..e0fe6da767 100644 --- a/codex-rs/core/tests/suite/scenarios.rs +++ b/codex-rs/core/tests/suite/scenarios.rs @@ -207,6 +207,9 @@ fn skills_extensions() -> Arc> { install(&mut extensions, |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: config + .features + .enabled(codex_features::Feature::OmitSkillDescriptions), max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: config.bundled_skills_enabled(), cloud_skill_enabled: config.cloud_skill_enabled, @@ -937,8 +940,10 @@ async fn astra_kickoff_with_skills_plugins_and_remote_compaction() -> Result<()> Ok(()) } +#[test_case::test_case(false; "descriptions")] +#[test_case::test_case(true; "omit_descriptions")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn astra_plugin_policy_limits_visible_skills() -> Result<()> { +async fn astra_plugin_policy_limits_visible_skills(omit_descriptions: bool) -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -996,6 +1001,10 @@ enabled = true .with_extensions(skills_extensions()) .with_config(move |config| { configure_scenario_catalog(config); + config + .features + .set_enabled(Feature::OmitSkillDescriptions, omit_descriptions) + .expect("set omit_skill_descriptions feature"); config.chatgpt_base_url = chatgpt_base_url; config.workspace_roots = vec![config.cwd.clone()]; }) @@ -1011,7 +1020,11 @@ enabled = true let requests = mock.requests(); insta::assert_snapshot!( - "astra_plugin_policy_limits_visible_skills", + if omit_descriptions { + "astra_plugin_policy_without_skill_descriptions" + } else { + "astra_plugin_policy_limits_visible_skills" + }, context_snapshot::format_request_history_snapshot( "Astra sees the permitted calendar plugin and standalone skills, but not the unlisted notes plugin.", &requests, @@ -1122,6 +1135,9 @@ async fn astra_omits_disabled_executor_and_plugin_skills_from_model_context( |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: config + .features + .enabled(codex_features::Feature::OmitSkillDescriptions), max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, @@ -1610,6 +1626,9 @@ async fn skill_read_recovers_omitted_installation_and_reads_selected_environment |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: config + .features + .enabled(codex_features::Feature::OmitSkillDescriptions), max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: true, diff --git a/codex-rs/core/tests/suite/scenarios_frozen_cloud_skills.rs b/codex-rs/core/tests/suite/scenarios_frozen_cloud_skills.rs index 31b8432fc0..25d094c767 100644 --- a/codex-rs/core/tests/suite/scenarios_frozen_cloud_skills.rs +++ b/codex-rs/core/tests/suite/scenarios_frozen_cloud_skills.rs @@ -35,6 +35,7 @@ async fn frozen_cloud_skills_with_named_executor_request_history() -> Result<()> )), |config: &Config| SkillsExtensionConfig { include_instructions: true, + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: true, diff --git a/codex-rs/core/tests/suite/scenarios_skill_catalog_dedup.rs b/codex-rs/core/tests/suite/scenarios_skill_catalog_dedup.rs index 3ba2753a74..4b939e5187 100644 --- a/codex-rs/core/tests/suite/scenarios_skill_catalog_dedup.rs +++ b/codex-rs/core/tests/suite/scenarios_skill_catalog_dedup.rs @@ -32,8 +32,12 @@ use serde_json::json; use super::super::skills_extension::CatalogSkillProvider; +#[test_case::test_case(false; "descriptions")] +#[test_case::test_case(true; "omit_descriptions")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn cloud_preference_preserves_executor_aliases_and_description_budget() -> Result<()> { +async fn cloud_preference_preserves_executor_aliases_and_description_budget( + omit_descriptions: bool, +) -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -86,6 +90,9 @@ async fn cloud_preference_preserves_executor_aliases_and_description_budget() -> .with_cloud_provider(provider(cloud_entries)), |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, + omit_descriptions: config + .features + .enabled(codex_features::Feature::OmitSkillDescriptions), max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: true, @@ -111,12 +118,11 @@ async fn cloud_preference_preserves_executor_aliases_and_description_budget() -> let test = test_codex() .with_exec_server_url("none") .with_extensions(Arc::new(extensions.build())) - .with_model_info_override("gpt-6-astra", |model| { - model.context_window = Some(70_000); - model.max_context_window = None; - model.include_skills_usage_instructions = false; - }) - .with_config(|config| { + .with_config(move |config| { + config + .features + .set_enabled(Feature::OmitSkillDescriptions, omit_descriptions) + .expect("set omit_skill_descriptions feature"); super::configure_scenario_catalog(config); config.cloud_skill_enabled = true; config @@ -124,6 +130,7 @@ async fn cloud_preference_preserves_executor_aliases_and_description_budget() -> .enable(Feature::ExecutorCapabilityDiscovery) .expect("enable executor capability discovery"); }) + .with_model("gpt-6-astra") .build_with_auto_env(&server) .await?; test.submit_turn("Which skills are available?").await?; @@ -150,12 +157,23 @@ async fn cloud_preference_preserves_executor_aliases_and_description_budget() -> index == 1 ); } - assert_eq!(body.matches("UNIQUE_DESCRIPTION_END").count(), 3); + assert_eq!( + body.matches("UNIQUE_DESCRIPTION_END").count(), + if omit_descriptions { 0 } else { 3 } + ); } insta::assert_snapshot!( - "dedup_before_budgeting", + if omit_descriptions { + "dedup_without_descriptions" + } else { + "dedup_before_budgeting" + }, context_snapshot::format_request_history_snapshot( - "Cloud skills replace duplicate executor listings before budgeting; unique executor skills retain their descriptions and e1 aliases across turns.", + if omit_descriptions { + "Cloud and executor skill catalogs retain names and aliases across turns with descriptions omitted." + } else { + "Cloud skills replace duplicate executor listings before budgeting; unique executor skills retain their descriptions and e1 aliases across turns." + }, &requests, &ContextSnapshotOptions::default().include_request_settings(), ) @@ -193,6 +211,7 @@ async fn skill_catalog_deltas_survive_resume_and_rebuild_after_compaction() -> R SkillProviders::new().with_host_provider(Arc::new(HostSkillProvider::new())), |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, + omit_descriptions: config.features.enabled(Feature::OmitSkillDescriptions), max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index 7e2a5c6caa..1c610164b7 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -328,6 +328,7 @@ async fn user_turn_selects_symlinked_skill_by_advertised_discovery_path() -> Res catalog_deltas_enabled: config .features .enabled(codex_features::Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, diff --git a/codex-rs/core/tests/suite/skills_extension.rs b/codex-rs/core/tests/suite/skills_extension.rs index 7ac7a579e9..460b836c97 100644 --- a/codex-rs/core/tests/suite/skills_extension.rs +++ b/codex-rs/core/tests/suite/skills_extension.rs @@ -367,6 +367,7 @@ fn catalog_extensions( SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, @@ -645,6 +646,7 @@ async fn capability_sections_render_in_order_with_host_repo_and_plugin_skills() install(&mut extensions, |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: config.bundled_skills_enabled(), cloud_skill_enabled: config.cloud_skill_enabled, @@ -1057,6 +1059,7 @@ async fn opted_in_executor_provider_skips_host_discovery_but_injects_discovered_ |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, @@ -1287,6 +1290,7 @@ async fn executor_only_provider_preserves_structured_repo_skill_without_discover |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, @@ -1437,6 +1441,7 @@ async fn executor_skill_tool_reads_references_under_current_permissions( |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, @@ -1879,6 +1884,7 @@ async fn explicit_executor_skill_prompt_rejects_oversized_resource() -> Result<( |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, @@ -2112,6 +2118,7 @@ async fn production_turn_aliases_catalogs_with_separate_cloud_budget() -> Result |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: true, @@ -2252,6 +2259,7 @@ async fn assert_catalog_model_switch(max_context_tokens: Option) -> Resul |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, @@ -2692,6 +2700,7 @@ async fn production_turn_uses_provider_host_catalog_and_core_snapshot_injection( |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, @@ -2814,6 +2823,7 @@ async fn production_turn_suppresses_only_the_superseded_host_skill_prompt() -> R |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, @@ -3069,6 +3079,7 @@ async fn production_turn_keeps_rebalanced_catalogs_stable_after_compaction_and_r |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: config.cloud_skill_enabled, @@ -3581,6 +3592,7 @@ async fn production_turn_fairly_shortens_extension_catalog_descriptions() -> Res |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, diff --git a/codex-rs/core/tests/suite/skills_extension/cloud_lifecycle_tests.rs b/codex-rs/core/tests/suite/skills_extension/cloud_lifecycle_tests.rs index fd062218a5..aba192e5b0 100644 --- a/codex-rs/core/tests/suite/skills_extension/cloud_lifecycle_tests.rs +++ b/codex-rs/core/tests/suite/skills_extension/cloud_lifecycle_tests.rs @@ -113,6 +113,7 @@ async fn cloud_skills_reuse_cache_and_invalidate_on_connection_or_auth_change() |_config: &Config| SkillsExtensionConfig { include_instructions: true, catalog_deltas_enabled: false, + omit_descriptions: false, max_context_tokens: None, bundled_skills_enabled: false, cloud_skill_enabled: true, diff --git a/codex-rs/core/tests/suite/skills_extension/cloud_skill_tests.rs b/codex-rs/core/tests/suite/skills_extension/cloud_skill_tests.rs index 74a14c0aff..bf56badb52 100644 --- a/codex-rs/core/tests/suite/skills_extension/cloud_skill_tests.rs +++ b/codex-rs/core/tests/suite/skills_extension/cloud_skill_tests.rs @@ -164,6 +164,7 @@ text({ names: result.skills.map(skill => skill.name), warnings: result.warnings, |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: true, @@ -508,6 +509,7 @@ async fn production_turn_aliases_discovered_singleton_cloud_locator() -> Result< |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: true, @@ -598,6 +600,7 @@ async fn cloud_skill_reads_run_in_parallel() -> Result<()> { |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: true, @@ -722,6 +725,7 @@ async fn cloud_skill_can_read_referenced_resource_without_an_executor() -> Resul |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: true, diff --git a/codex-rs/core/tests/suite/skills_extension/yielded_skill_tests.rs b/codex-rs/core/tests/suite/skills_extension/yielded_skill_tests.rs index 8966dd5143..a276697fa3 100644 --- a/codex-rs/core/tests/suite/skills_extension/yielded_skill_tests.rs +++ b/codex-rs/core/tests/suite/skills_extension/yielded_skill_tests.rs @@ -37,6 +37,7 @@ async fn yielded_skill_read_keeps_originating_turn_metadata() -> Result<()> { |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: true, diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__astra_plugin_policy_without_skill_descriptions.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__astra_plugin_policy_without_skill_descriptions.snap new file mode 100644 index 0000000000..8a81d583ff --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__astra_plugin_policy_without_skill_descriptions.snap @@ -0,0 +1,114 @@ +--- +source: core/tests/suite/scenarios.rs +expression: "context_snapshot::format_request_history_snapshot(\"Astra sees the permitted calendar plugin and standalone skills, but not the unlisted notes plugin.\",\n&requests, &ContextSnapshotOptions::default().include_request_settings(),)" +--- +Scenario: Astra sees the permitted calendar plugin and standalone skills, but not the unlisted notes plugin. + +## Window 1 +Settings: + include: ["reasoning.encrypted_content"] + model: "gpt-6-astra" + parallel_tool_calls: false + prompt_cache_key: "" + reasoning: {"context":"all_turns","effort":"low"} + store: false + stream: true + text: {"verbosity":"low"} + tool_choice: "auto" +-- request 1 (turn) -- +00:additional_tools/developer (3; hash=bf2e30605ff5dc17): + - namespace/functions; hash=fada4a476a3754dd + - custom/exec + - function/wait + - function/request_user_input + - function/request_user_input_async + - namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317 + - function/sleep + - namespace/collaboration: Tools for spawning and managing sub-agents.; hash=2c8d6295978dbe63 + - function/followup_task + - function/interrupt_agent + - function/list_agents + - function/send_message + - function/spawn_agent + - function/wait_agent +01:message/developer: + You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803] + + # When to ask the user for permission + + Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388] + + User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c] + + + + - Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list. + - MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from. + - Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn. + - Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task. + - Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0] + - Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e] + +02:message/developer[3]: + [01] + ## Skills + A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=d516c00c9246c269] + ### Skill roots + - `r0` = `` + - `r1` = `/openai-curated-remote` + ### Available skills + - calendar:agenda: (file: r1/calendar/local/skills/agenda/SKILL.md) + - final-check: (file: r0/final-check/SKILL.md) + - outline: (file: r0/outline/SKILL.md) + + [02] + Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] + Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + + [03] # Collaboration Mode: Default + + You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. + + Your active mode changes only when new developer instructions with a different `...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812] + + ## request_user_input availability + + Use the `request_user_input` tool only when it is listed in the available tools for this turn. + + Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work. + + If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked. + + Never use the `request_user_input` tool for permission requests or permission-related escalations. + +03:message/developer: + You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals. + + At the start of your turn, you are the active agent. + You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. + All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools. + + You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202] + `send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers. + + - All agents use the same current working directory. + - As a result, edits made by one agent are immediately visible to all other agents. + + When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling. + + There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you. + + Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d... so, set `fork_turns` to `"none"`. [hash=d5928243dc643bcc] +04:message/developer: + Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work. [hash=2b6e037067b4390c] +05:message/user: + + + + + + <...e="unrestricted" /> [hash=d40b2f45cb028888] + +06:message/user: + Plan a team kickoff. Which planning skills are available? +07:configuration_update:reasoning={"effort":"low"} diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__skill_catalog_dedup__dedup_without_descriptions.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__skill_catalog_dedup__dedup_without_descriptions.snap new file mode 100644 index 0000000000..9bb1897a43 --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__skill_catalog_dedup__dedup_without_descriptions.snap @@ -0,0 +1,125 @@ +--- +source: core/tests/suite/scenarios_skill_catalog_dedup.rs +expression: "context_snapshot::format_request_history_snapshot(if omit_descriptions\n{\n \"Cloud and executor skill catalogs retain names and aliases across turns with descriptions omitted.\"\n} else\n{\n \"Cloud skills replace duplicate executor listings before budgeting; unique executor skills retain their descriptions and e1 aliases across turns.\"\n}, &requests, &ContextSnapshotOptions::default().include_request_settings(),)" +--- +Scenario: Cloud and executor skill catalogs retain names and aliases across turns with descriptions omitted. + +## Window 1 +Settings: + include: ["reasoning.encrypted_content"] + model: "gpt-6-astra" + parallel_tool_calls: false + prompt_cache_key: "" + reasoning: {"context":"all_turns","effort":"low"} + store: false + stream: true + text: {"verbosity":"low"} + tool_choice: "auto" +-- request 1 (turn) -- +00:additional_tools/developer (3; hash=5e8370540461db70): + - namespace/functions; hash=7d26e086c1dbba28 + - custom/exec + - function/wait + - function/request_user_input + - function/request_user_input_async + - namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317 + - function/sleep + - namespace/collaboration: Tools for spawning and managing sub-agents.; hash=2c8d6295978dbe63 + - function/followup_task + - function/interrupt_agent + - function/list_agents + - function/send_message + - function/spawn_agent + - function/wait_agent +01:message/developer: + You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803] + + # When to ask the user for permission + + Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388] + + User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c] + + + + - Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list. + - MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from. + - Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn. + - Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task. + - Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0] + - Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e] + +02:message/developer[4]: + [01] + Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] + Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + + [02] # Collaboration Mode: Default + + You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. + + Your active mode changes only when new developer instructions with a different `...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812] + + ## request_user_input availability + + Use the `request_user_input` tool only when it is listed in the available tools for this turn. + + Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work. + + If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked. + + Never use the `request_user_input` tool for permission requests or permission-related escalations. + + [03] + ## Skills + A skill is a set of instructions provided through a `SKILL.md` source. Below is the list of skills that ...locators can be expanded using the skill roots table. [hash=18b83353757fba24] + ### Skill roots + - `e1` = `skill://executor-b/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb...bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb` [hash=aec7776011a3143f] + Read a skill package directly with `skills.read({"package":""})` to read its `SKILL.md`; root a...ackage is not provided, use `skills.list` to find it. [hash=25bf84ea2123d647] + ### Available skills + - other:s0: (executor package: e1/s0) + - other:s1: (executor package: e1/s1) + - other:s2: (executor package: e1/s2) + + [04] + ## Skills + A skill is a set of instructions provided through a `SKILL.md` source. Below is the list of skills that ...urce` locators use their provider's access mechanism. [hash=97229b4ea8d5a55c] + Read a skill package directly with `skills.read({"package":""})` to read its `SKILL.md`; root a...ackage is not provided, use `skills.list` to find it. [hash=25bf84ea2123d647] + ### Available skills + - demo:s0: (cloud package: skill://cloud/s0) + - demo:s1: (cloud package: skill://cloud/s1) + - demo:s2: (cloud package: skill://cloud/s2) + +03:message/developer: + You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals. + + At the start of your turn, you are the active agent. + You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. + All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools. + + You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202] + `send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers. + + - All agents use the same current working directory. + - As a result, edits made by one agent are immediately visible to all other agents. + + When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling. + + There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you. + + Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d... so, set `fork_turns` to `"none"`. [hash=d5928243dc643bcc] +04:message/developer: + Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work. [hash=2b6e037067b4390c] +05:message/user: + + + + +06:message/user: + Which skills are available? +07:configuration_update:reasoning={"effort":"low"} +-- request 2 (turn) -- +08:message/assistant: + Skills are available. +09:message/user: + Are those same skills still available? diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 47629f5cb8..c75dd859e1 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -3327,6 +3327,7 @@ async fn skill_instructions_in_parent_and_spawned_child(include_instructions: bo codex_skills_extension::install(&mut extensions, |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: false, cloud_skill_enabled: false, diff --git a/codex-rs/core/tests/suite/token_budget.rs b/codex-rs/core/tests/suite/token_budget.rs index 65e92e0865..041d2c6b3e 100644 --- a/codex-rs/core/tests/suite/token_budget.rs +++ b/codex-rs/core/tests/suite/token_budget.rs @@ -1695,6 +1695,7 @@ async fn new_context_tool_skips_auto_compact_fallback() -> Result<()> { install(&mut extensions, |config: &Config| SkillsExtensionConfig { include_instructions: config.include_skill_instructions, catalog_deltas_enabled: config.features.enabled(Feature::SkillCatalogDeltas), + omit_descriptions: false, max_context_tokens: config.skill_max_context_tokens, bundled_skills_enabled: config.bundled_skills_enabled(), cloud_skill_enabled: config.cloud_skill_enabled, diff --git a/codex-rs/ext/skills/src/catalog_delta.rs b/codex-rs/ext/skills/src/catalog_delta.rs index 1ed5b7cb47..6d0ab57d28 100644 --- a/codex-rs/ext/skills/src/catalog_delta.rs +++ b/codex-rs/ext/skills/src/catalog_delta.rs @@ -18,6 +18,7 @@ use serde::Serialize; use serde_json::json; use crate::aliases::AliasRoot; +use crate::catalog_prompt::SkillPrompt; use crate::catalog_prompt::SkillPromptKind; use crate::fragments::AvailableSkillsInstructions; use crate::render::AvailableSkillsRender; @@ -75,6 +76,8 @@ struct CatalogSnapshot { roots: BTreeMap, usage: CatalogUsage, instructions_sent: bool, + #[serde(default)] + omit_descriptions: bool, allocation: Option, } @@ -142,6 +145,7 @@ fn build_initial_catalog(source: &str, catalog: CatalogDelta) -> CatalogResult { let usage = CatalogUsage::measure(|budget| rendered.metadata_cost(budget, catalog.include_usage)); let entries = std::mem::take(&mut rendered.entries); + snapshot.omit_descriptions = rendered.prompt.omit_descriptions; let fragment = rendered.into_fragment(catalog.include_usage); for (id, skill) in entries { if fragment.is_some() @@ -169,16 +173,24 @@ fn build_catalog_update( mut previous: CatalogSnapshot, remaining: &mut SkillMetadataBudget, ) -> CatalogResult { + let usage_update = catalog.rendered.as_ref().filter(|rendered| { + previous.instructions_sent + && catalog.include_usage + && previous.omit_descriptions != rendered.prompt.omit_descriptions + }); let message = if previous.instructions_sent { CatalogMessage::Update { heading: format!("\n## {source} skills update\n"), } } else { CatalogMessage::FirstListing { - prompt_kind: catalog - .rendered - .as_ref() - .map_or(SkillPromptKind::Unaliased, |r| r.prompt_kind), + prompt: catalog.rendered.as_ref().map_or( + SkillPrompt { + kind: SkillPromptKind::Unaliased, + omit_descriptions: false, + }, + |r| r.prompt, + ), include_usage: catalog.include_usage, } }; @@ -209,7 +221,15 @@ fn build_catalog_update( }, ); } - let body = draft.into_body(source); + let mut body = draft.into_body(source); + if let Some(rendered) = usage_update { + let instructions = rendered.prompt.usage_instructions(); + body.get_or_insert_with(|| format!("\n## {source} skills update\n")) + .push_str(&format!("\n### How to use skills\n{instructions}\n")); + } + if let Some(rendered) = &catalog.rendered { + previous.omit_descriptions = rendered.prompt.omit_descriptions; + } previous.entries = entries; previous.instructions_sent |= body.is_some(); previous.allocation = catalog.allocation; @@ -229,7 +249,7 @@ fn listing_body(source: &str, instructions: AvailableSkillsInstructions) -> Stri enum CatalogMessage { FirstListing { - prompt_kind: SkillPromptKind, + prompt: SkillPrompt, include_usage: bool, }, Update { @@ -267,11 +287,11 @@ impl UpdateDraft { if let Some((_, line)) = &new_mapping { cost += match &self.message { CatalogMessage::FirstListing { - prompt_kind, + prompt, include_usage, } if self.root_lines.is_empty() => aliased_metadata_overhead_cost( budget, - *prompt_kind, + *prompt, std::slice::from_ref(line), *include_usage, ), @@ -308,18 +328,16 @@ impl UpdateDraft { } Some(match self.message { CatalogMessage::FirstListing { - prompt_kind, + mut prompt, include_usage, } => { - let kind = if self.root_lines.is_empty() { - SkillPromptKind::Unaliased - } else { - prompt_kind - }; + if self.root_lines.is_empty() { + prompt.kind = SkillPromptKind::Unaliased; + } listing_body( source, AvailableSkillsInstructions::from_skill_lines( - kind, + prompt, self.root_lines, self.skill_lines, include_usage, diff --git a/codex-rs/ext/skills/src/catalog_prompt.rs b/codex-rs/ext/skills/src/catalog_prompt.rs index ec9f7c5063..0f60373707 100644 --- a/codex-rs/ext/skills/src/catalog_prompt.rs +++ b/codex-rs/ext/skills/src/catalog_prompt.rs @@ -64,15 +64,6 @@ impl SkillPromptKind { } } - pub(crate) fn usage_instructions(self) -> &'static str { - match self { - Self::Unaliased | Self::ResourceAliases | Self::ResourceAliasesV2 => { - SKILLS_HOW_TO_USE_WITH_SOURCE_LOCATORS - } - Self::HostAliases => SKILLS_HOW_TO_USE_WITH_HOST_ALIASES, - } - } - pub(crate) fn alias_instructions(self) -> Option<&'static str> { match self { Self::ResourceAliases | Self::ResourceAliasesV2 => Some(RESOURCE_ALIAS_INSTRUCTIONS), @@ -81,17 +72,47 @@ impl SkillPromptKind { } } +/// Prompt layout and description policy shared by rendering and budget accounting. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct SkillPrompt { + pub(crate) kind: SkillPromptKind, + pub(crate) omit_descriptions: bool, +} + +impl SkillPrompt { + pub(crate) fn usage_instructions(self) -> String { + let instructions = match self.kind { + SkillPromptKind::Unaliased + | SkillPromptKind::ResourceAliases + | SkillPromptKind::ResourceAliasesV2 => SKILLS_HOW_TO_USE_WITH_SOURCE_LOCATORS, + SkillPromptKind::HostAliases => SKILLS_HOW_TO_USE_WITH_HOST_ALIASES, + }; + if self.omit_descriptions { + instructions + .replace("name + description +", "name +") + .replace("description shown above", "name shown above") + } else { + instructions.to_owned() + } + } +} + pub(crate) fn render_available_skills_body( - prompt_kind: SkillPromptKind, + prompt: SkillPrompt, skill_root_lines: &[String], skill_lines: &[String], ) -> String { let mut lines: Vec = Vec::new(); lines.push("## Skills".to_string()); - lines.push(prompt_kind.intro().to_string()); + let intro = prompt.kind.intro(); + lines.push(if prompt.omit_descriptions { + intro.replace("name, description, and", "name and") + } else { + intro.to_string() + }); if !skill_root_lines.is_empty() { lines.push("### Skill roots".to_string()); - if prompt_kind == SkillPromptKind::ResourceAliasesV2 { + if prompt.kind == SkillPromptKind::ResourceAliasesV2 { lines.push("For each alias listed below, this mapping replaces any earlier mapping for that alias. Use it for subsequent skill calls.".to_string()); } lines.extend(skill_root_lines.iter().cloned()); diff --git a/codex-rs/ext/skills/src/cloud_cache_tests.rs b/codex-rs/ext/skills/src/cloud_cache_tests.rs index d7d407124c..8639255126 100644 --- a/codex-rs/ext/skills/src/cloud_cache_tests.rs +++ b/codex-rs/ext/skills/src/cloud_cache_tests.rs @@ -95,6 +95,7 @@ fn state() -> SkillsThreadState { SkillsExtensionConfig { include_instructions: true, catalog_deltas_enabled: false, + omit_descriptions: false, max_context_tokens: None, bundled_skills_enabled: false, cloud_skill_enabled: true, diff --git a/codex-rs/ext/skills/src/config.rs b/codex-rs/ext/skills/src/config.rs index 98ce1f5f42..042174fcbd 100644 --- a/codex-rs/ext/skills/src/config.rs +++ b/codex-rs/ext/skills/src/config.rs @@ -7,6 +7,8 @@ pub struct SkillsExtensionConfig { pub include_instructions: bool, /// Whether skill additions and edits share a cumulative metadata budget until compaction. pub catalog_deltas_enabled: bool, + /// Whether descriptions are omitted from rendered catalogs only. + pub omit_descriptions: bool, /// Optional token budget override for the available-skills catalog. pub max_context_tokens: Option, /// Whether bundled skills are eligible for discovery. diff --git a/codex-rs/ext/skills/src/extension.rs b/codex-rs/ext/skills/src/extension.rs index faf7d927af..f0fdee286d 100644 --- a/codex-rs/ext/skills/src/extension.rs +++ b/codex-rs/ext/skills/src/extension.rs @@ -239,7 +239,8 @@ where CatalogSurface::ThreadContext, &catalog, include_usage, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE + .with_omitted_descriptions(config.omit_descriptions), skill_metadata_budget(/*context_window*/ None, config.max_context_tokens), ); if let Some(message) = rendered.warning_message { @@ -418,7 +419,8 @@ where CatalogSurface::TurnInput, &turn_catalog, include_usage, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE + .with_omitted_descriptions(config.omit_descriptions), metadata_budget, ); if let Some(message) = rendered.warning_message { diff --git a/codex-rs/ext/skills/src/extension_tests.rs b/codex-rs/ext/skills/src/extension_tests.rs index 118812bc97..32b470045a 100644 --- a/codex-rs/ext/skills/src/extension_tests.rs +++ b/codex-rs/ext/skills/src/extension_tests.rs @@ -46,7 +46,7 @@ fn empty_catalog_records_zero_metrics_without_a_fragment() { CatalogSurface::ThreadContext, &SkillCatalog::default(), /*include_skills_usage_instructions*/ false, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, SkillMetadataBudget::Characters(8_000), ); diff --git a/codex-rs/ext/skills/src/fragments.rs b/codex-rs/ext/skills/src/fragments.rs index 9bb8874f63..d4a0342a7e 100644 --- a/codex-rs/ext/skills/src/fragments.rs +++ b/codex-rs/ext/skills/src/fragments.rs @@ -6,33 +6,33 @@ use codex_protocol::models::input_text; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG; -use crate::catalog_prompt::SkillPromptKind; +use crate::catalog_prompt::SkillPrompt; use crate::catalog_prompt::render_available_skills_body; use crate::tools::SkillToolAuthority; #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct AvailableSkillsInstructions { - prompt_kind: SkillPromptKind, + prompt: SkillPrompt, skill_root_lines: Vec, skill_lines: Vec, } impl AvailableSkillsInstructions { pub(crate) fn from_skill_lines( - prompt_kind: SkillPromptKind, + prompt: SkillPrompt, skill_root_lines: Vec, mut skill_lines: Vec, include_skills_usage_instructions: bool, ) -> Self { if include_skills_usage_instructions { skill_lines.push("### How to use skills".to_string()); - if let Some(instructions) = prompt_kind.alias_instructions() { + if let Some(instructions) = prompt.kind.alias_instructions() { skill_lines.push(instructions.to_string()); } - skill_lines.push(prompt_kind.usage_instructions().to_string()); + skill_lines.push(prompt.usage_instructions()); } Self { - prompt_kind, + prompt, skill_root_lines, skill_lines, } @@ -62,7 +62,7 @@ impl ContextualUserFragment for AvailableSkillsInstructions { fn body(&self) -> ContentItem { input_text(render_available_skills_body( - self.prompt_kind, + self.prompt, &self.skill_root_lines, &self.skill_lines, )) diff --git a/codex-rs/ext/skills/src/invocation_tests.rs b/codex-rs/ext/skills/src/invocation_tests.rs index c38e404821..ddd629516a 100644 --- a/codex-rs/ext/skills/src/invocation_tests.rs +++ b/codex-rs/ext/skills/src/invocation_tests.rs @@ -46,6 +46,7 @@ fn detect_executor_skill(command: &str, environments: &[&str]) -> Option &str { - match self { - Self::CoreCompatible => entry.description.as_str(), - Self::ExtensionCompatible | Self::DiscoveryV2 => entry - .short_description - .as_deref() - .unwrap_or(entry.description.as_str()), + pub(crate) const CORE_COMPATIBLE: Self = Self { + style: SkillCatalogRenderStyle::CoreCompatible, + omit_descriptions: false, + }; + pub(crate) const EXTENSION_COMPATIBLE: Self = Self { + style: SkillCatalogRenderStyle::ExtensionCompatible, + omit_descriptions: false, + }; + pub(crate) const DISCOVERY_V2: Self = Self { + style: SkillCatalogRenderStyle::DiscoveryV2, + omit_descriptions: false, + }; + + pub(crate) fn with_omitted_descriptions(self, omit_descriptions: bool) -> Self { + Self { + omit_descriptions, + ..self + } + } + + fn prompt(self, kind: SkillPromptKind) -> SkillPrompt { + SkillPrompt { + kind, + omit_descriptions: self.omit_descriptions, + } + } + + pub(crate) fn description(self, entry: &SkillCatalogEntry) -> &str { + if self.omit_descriptions { + return ""; + } + match self.style { + SkillCatalogRenderStyle::CoreCompatible => entry.description.as_str(), + SkillCatalogRenderStyle::ExtensionCompatible | SkillCatalogRenderStyle::DiscoveryV2 => { + entry + .short_description + .as_deref() + .unwrap_or(entry.description.as_str()) + } } } fn order_entries(self, entries: &mut [&SkillCatalogEntry]) { - match self { - Self::CoreCompatible => { + match self.style { + SkillCatalogRenderStyle::CoreCompatible => { let scope_rank = |entry: &SkillCatalogEntry| match entry.prompt_scope() { Some(SkillScope::System) => 0, Some(SkillScope::Admin) => 1, @@ -66,14 +105,17 @@ impl SkillCatalogRenderPolicy { .then_with(|| a.main_prompt.as_str().cmp(b.main_prompt.as_str())) }); } - Self::ExtensionCompatible | Self::DiscoveryV2 => {} + SkillCatalogRenderStyle::ExtensionCompatible | SkillCatalogRenderStyle::DiscoveryV2 => { + } } } fn includes_omission_notice(self) -> bool { - match self { - Self::CoreCompatible => false, - Self::ExtensionCompatible | Self::DiscoveryV2 => true, + match self.style { + SkillCatalogRenderStyle::CoreCompatible => false, + SkillCatalogRenderStyle::ExtensionCompatible | SkillCatalogRenderStyle::DiscoveryV2 => { + true + } } } } @@ -466,7 +508,7 @@ fn allocate_description_chars( } struct RenderedCatalog { - prompt_kind: SkillPromptKind, + prompt: SkillPrompt, skill_root_lines: Vec, skill_lines: Vec, included_ids: Option>, @@ -474,7 +516,7 @@ struct RenderedCatalog { } pub(crate) struct AvailableSkillsRender { - pub(crate) prompt_kind: SkillPromptKind, + pub(crate) prompt: SkillPrompt, pub(crate) skill_root_lines: Vec, skill_lines: Vec, included_ids: Option>, @@ -508,7 +550,7 @@ impl AvailableSkillsRender { } else { aliased_metadata_overhead_cost( budget, - self.prompt_kind, + self.prompt, &self.skill_root_lines, include_skills_usage_instructions, ) @@ -524,7 +566,7 @@ impl AvailableSkillsRender { ) -> Option { (self.preserve_empty_fragment || !self.skill_lines.is_empty()).then(|| { AvailableSkillsInstructions::from_skill_lines( - self.prompt_kind, + self.prompt, self.skill_root_lines, self.skill_lines, include_skills_usage_instructions, @@ -642,7 +684,7 @@ struct CombinedAvailableSkillsRender { } struct CatalogLines<'a> { - prompt_kind: SkillPromptKind, + prompt: SkillPrompt, skills: Vec>, root_lines: Vec, } @@ -650,7 +692,7 @@ struct CatalogLines<'a> { impl<'a> CatalogLines<'a> { fn unaliased(entries: &[&'a SkillCatalogEntry], policy: SkillCatalogRenderPolicy) -> Self { Self { - prompt_kind: SkillPromptKind::Unaliased, + prompt: policy.prompt(SkillPromptKind::Unaliased), skills: entries .iter() .map(|entry| SkillLine::new(entry, policy)) @@ -680,19 +722,21 @@ impl<'a> CatalogLines<'a> { }) .collect(); Self { - prompt_kind: entries - .first() - .map(|entry| { - let kind = SkillPromptKind::for_aliased_source(&entry.authority.kind); - if policy == SkillCatalogRenderPolicy::DiscoveryV2 - && kind == SkillPromptKind::ResourceAliases - { - SkillPromptKind::ResourceAliasesV2 - } else { - kind - } - }) - .unwrap_or(SkillPromptKind::Unaliased), + prompt: policy.prompt( + entries + .first() + .map(|entry| { + let kind = SkillPromptKind::for_aliased_source(&entry.authority.kind); + if policy.style == SkillCatalogRenderStyle::DiscoveryV2 + && kind == SkillPromptKind::ResourceAliases + { + SkillPromptKind::ResourceAliasesV2 + } else { + kind + } + }) + .unwrap_or(SkillPromptKind::Unaliased), + ), skills, root_lines: match plan { Cow::Borrowed(plan) => plan.root_lines_for(&used_roots), @@ -726,21 +770,21 @@ fn render_combined_lines( executor: render_combined_group( &lines[..executor_end], &allocations[..executor_end], - executor.prompt_kind, + executor.prompt, executor.root_lines, executor_omission_marker, ), cloud: render_combined_group( &lines[executor_end..cloud_end], &allocations[executor_end..cloud_end], - cloud.prompt_kind, + cloud.prompt, cloud.root_lines, cloud_omission_marker, ), host: render_combined_group( &lines[cloud_end..], &allocations[cloud_end..], - host.prompt_kind, + host.prompt, host.root_lines, /*omission_marker*/ None, ), @@ -750,7 +794,7 @@ fn render_combined_lines( fn render_combined_group( skill_lines: &[SkillLine<'_>], allocations: &[SkillLineAllocation], - prompt_kind: SkillPromptKind, + prompt: SkillPrompt, skill_root_lines: Vec, omission_marker: Option, ) -> AvailableSkillsRender { @@ -765,7 +809,7 @@ fn render_combined_group( lines.push(marker); } AvailableSkillsRender { - prompt_kind, + prompt, skill_root_lines, entries: BTreeMap::new(), skill_lines: lines, @@ -801,7 +845,7 @@ fn build_aliased_combined_catalog( .map(|catalog| { aliased_metadata_overhead_cost( budget, - catalog.prompt_kind, + catalog.prompt, &catalog.root_lines, include_skills_usage_instructions, ) @@ -927,7 +971,7 @@ fn render_catalog( skill_lines: Vec>, budget: SkillMetadataBudget, skill_root_lines: Vec, - prompt_kind: SkillPromptKind, + prompt: SkillPrompt, policy: SkillCatalogRenderPolicy, ) -> RenderedCatalog { let total_count = skill_lines.len(); @@ -961,7 +1005,7 @@ fn render_catalog( } RenderedCatalog { - prompt_kind, + prompt, skill_root_lines, skill_lines: rendered_lines, included_ids, @@ -1003,7 +1047,7 @@ fn build_aliased_catalog( } let table_cost = aliased_metadata_overhead_cost( budget, - catalog.prompt_kind, + catalog.prompt, &catalog.root_lines, include_skills_usage_instructions, ); @@ -1020,7 +1064,7 @@ fn build_aliased_catalog( catalog.skills, adjusted_budget, catalog.root_lines, - catalog.prompt_kind, + catalog.prompt, policy, )) } @@ -1066,17 +1110,23 @@ fn render_skill_locator_with_aliases( pub(crate) fn aliased_metadata_overhead_cost( budget: SkillMetadataBudget, - prompt_kind: SkillPromptKind, + prompt: SkillPrompt, skill_root_lines: &[String], include_skills_usage_instructions: bool, ) -> usize { let empty_skill_lines: &[String] = &[]; - let absolute_body = - render_available_skills_body(SkillPromptKind::Unaliased, &[], empty_skill_lines); - let aliased_body = - render_available_skills_body(prompt_kind, skill_root_lines, empty_skill_lines); + let absolute_body = render_available_skills_body( + SkillPrompt { + kind: SkillPromptKind::Unaliased, + ..prompt + }, + &[], + empty_skill_lines, + ); + let aliased_body = render_available_skills_body(prompt, skill_root_lines, empty_skill_lines); let alias_instruction_cost = if include_skills_usage_instructions { - prompt_kind + prompt + .kind .alias_instructions() .map_or(0, |instructions| metadata_line_cost(budget, instructions)) } else { @@ -1115,7 +1165,7 @@ fn rendered_catalog_cost( } else { aliased_metadata_overhead_cost( budget, - rendered.prompt_kind, + rendered.prompt, &rendered.skill_root_lines, include_skills_usage_instructions, ) diff --git a/codex-rs/ext/skills/src/render_dedup.rs b/codex-rs/ext/skills/src/render_dedup.rs index f5c95cda76..3fe7721d12 100644 --- a/codex-rs/ext/skills/src/render_dedup.rs +++ b/codex-rs/ext/skills/src/render_dedup.rs @@ -8,6 +8,7 @@ use std::collections::HashSet; use super::AvailableSkillsRender; use super::CatalogLines; use super::SkillCatalogRenderPolicy; +use super::SkillCatalogRenderStyle; use super::SkillMetadataBudget; use super::aliased_render_is_better; use super::build_alias_plan; @@ -141,7 +142,7 @@ impl<'a> PreparedSkillCatalog<'a> { self.unaliased().skills, budget, Vec::new(), - SkillPromptKind::Unaliased, + self.policy.prompt(SkillPromptKind::Unaliased), self.policy, ); let selected = if let Some(aliased) = build_aliased_catalog( @@ -161,12 +162,12 @@ impl<'a> PreparedSkillCatalog<'a> { }; Some(AvailableSkillsRender { - prompt_kind: selected.prompt_kind, + prompt: selected.prompt, skill_root_lines: selected.skill_root_lines, skill_lines: selected.skill_lines, included_ids: selected.included_ids, entries: BTreeMap::new(), - preserve_empty_fragment: self.policy == SkillCatalogRenderPolicy::CoreCompatible, + preserve_empty_fragment: self.policy.style == SkillCatalogRenderStyle::CoreCompatible, report: selected.report, }) } diff --git a/codex-rs/ext/skills/src/render_tests.rs b/codex-rs/ext/skills/src/render_tests.rs index 754be97c6b..7813f5c0a1 100644 --- a/codex-rs/ext/skills/src/render_tests.rs +++ b/codex-rs/ext/skills/src/render_tests.rs @@ -31,10 +31,13 @@ fn render_combined_available_skills( render_prepared_skill_catalogs( &PreparedSkillCatalog::new( executor_catalog, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, ), - &PreparedSkillCatalog::new(cloud_catalog, SkillCatalogRenderPolicy::ExtensionCompatible), - &PreparedSkillCatalog::new(host_catalog, SkillCatalogRenderPolicy::CoreCompatible), + &PreparedSkillCatalog::new( + cloud_catalog, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, + ), + &PreparedSkillCatalog::new(host_catalog, SkillCatalogRenderPolicy::CORE_COMPATIBLE), budget, include_skills_usage_instructions, ) @@ -53,7 +56,7 @@ fn edits_beyond_the_catalog_description_cap_keep_the_same_fingerprint() { ..SkillCatalog::default() }; let prepared = - PreparedSkillCatalog::new(&catalog, SkillCatalogRenderPolicy::CoreCompatible); + PreparedSkillCatalog::new(&catalog, SkillCatalogRenderPolicy::CORE_COMPATIBLE); let rendered = prepared .render( SkillMetadataBudget::Characters(8_000), @@ -112,7 +115,7 @@ fn prebudget_dedup_can_choose_full_locators_over_preserved_aliases() { warnings: Vec::new(), }; let mut prepared = - PreparedSkillCatalog::new(&executor, SkillCatalogRenderPolicy::ExtensionCompatible); + PreparedSkillCatalog::new(&executor, SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE); prepared.prefer_cloud_skills(&cloud); let rendered = prepared .render( @@ -217,9 +220,9 @@ fn ordering_follows_render_policy() { }; assert_eq!( - render(SkillCatalogRenderPolicy::CoreCompatible), + render(SkillCatalogRenderPolicy::CORE_COMPATIBLE), render_available_skills_body( - SkillPromptKind::Unaliased, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE.prompt(SkillPromptKind::Unaliased), &[], &[ "- system-zeta: Description. (file: /skills/system-zeta/SKILL.md)".to_string(), @@ -232,9 +235,9 @@ fn ordering_follows_render_policy() { ) ); assert_eq!( - render(SkillCatalogRenderPolicy::ExtensionCompatible), + render(SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE), render_available_skills_body( - SkillPromptKind::Unaliased, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE.prompt(SkillPromptKind::Unaliased), &[], &[ "- repo-zeta: Description. (file: /skills/repo-zeta/SKILL.md)".to_string(), @@ -266,14 +269,14 @@ fn description_selection_follows_render_policy() { let core = available_skills_fragment( &catalog, /*include_skills_usage_instructions*/ false, - SkillCatalogRenderPolicy::CoreCompatible, + SkillCatalogRenderPolicy::CORE_COMPATIBLE, SkillMetadataBudget::Characters(8_000), ) .expect("catalog should render"); let extension = available_skills_fragment( &catalog, /*include_skills_usage_instructions*/ false, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, SkillMetadataBudget::Characters(8_000), ) .expect("catalog should render"); @@ -281,7 +284,7 @@ fn description_selection_follows_render_policy() { assert_eq!( core.body().expect_text(), render_available_skills_body( - SkillPromptKind::Unaliased, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE.prompt(SkillPromptKind::Unaliased), &[], &[ "- fallback: fallback description (file: /skills/fallback/SKILL.md)".to_string(), @@ -292,7 +295,7 @@ fn description_selection_follows_render_policy() { assert_eq!( extension.body().expect_text(), render_available_skills_body( - SkillPromptKind::Unaliased, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE.prompt(SkillPromptKind::Unaliased), &[], &[ "- shortened: short description (file: /skills/shortened/SKILL.md)".to_string(), @@ -366,7 +369,7 @@ fn host_only_prompts_preserve_existing_behavior_with_and_without_aliases() { let unaliased = available_skills_fragment( &unaliased_catalog, /*include_skills_usage_instructions*/ true, - SkillCatalogRenderPolicy::CoreCompatible, + SkillCatalogRenderPolicy::CORE_COMPATIBLE, SkillMetadataBudget::Characters(usize::MAX), ) .expect("unaliased host catalog should render"); @@ -401,7 +404,7 @@ fn host_only_prompts_preserve_existing_behavior_with_and_without_aliases() { let aliased = available_skills_fragment( &catalog, /*include_skills_usage_instructions*/ true, - SkillCatalogRenderPolicy::CoreCompatible, + SkillCatalogRenderPolicy::CORE_COMPATIBLE, SkillMetadataBudget::Characters(200), ) .expect("aliased host catalog should render"); @@ -455,7 +458,7 @@ fn path_aliases_are_used_without_budget_pressure_when_they_reduce_prompt_size() let fragment = available_skills_fragment( &catalog, /*include_skills_usage_instructions*/ false, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, SkillMetadataBudget::Characters(usize::MAX), ) .expect("catalog should render"); @@ -489,7 +492,7 @@ fn path_aliases_retain_every_skill_under_budget_pressure() { let plan = build_alias_plan(&visible_entries).expect("alias plan should build"); let table_cost = aliased_metadata_overhead_cost( SkillMetadataBudget::Characters(usize::MAX), - SkillPromptKind::HostAliases, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE.prompt(SkillPromptKind::HostAliases), &plan.root_lines(), /*include_skills_usage_instructions*/ true, ); @@ -497,7 +500,7 @@ fn path_aliases_retain_every_skill_under_budget_pressure() { cost.saturating_add( SkillLine::with_locator( entry, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, render_skill_locator_with_aliases(entry, &plan, &mut HashSet::new()), ) .minimum_cost(SkillMetadataBudget::Characters(usize::MAX)), @@ -505,7 +508,7 @@ fn path_aliases_retain_every_skill_under_budget_pressure() { }); let absolute_minimum = visible_entries.iter().fold(0usize, |cost, entry| { cost.saturating_add( - SkillLine::new(entry, SkillCatalogRenderPolicy::ExtensionCompatible) + SkillLine::new(entry, SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE) .minimum_cost(SkillMetadataBudget::Characters(usize::MAX)), ) }); @@ -514,7 +517,7 @@ fn path_aliases_retain_every_skill_under_budget_pressure() { let fragment = available_skills_fragment( &catalog, /*include_skills_usage_instructions*/ true, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, SkillMetadataBudget::Characters(alias_minimum), ) .expect("catalog should render"); @@ -525,6 +528,54 @@ fn path_aliases_retain_every_skill_under_budget_pressure() { assert!(body.contains("(file: r0/skill-11/SKILL.md)")); assert!(body.contains("Skill bodies live on disk at the listed paths after expanding")); assert!(!body.contains("additional skills omitted")); + + // With descriptions omitted, alias overhead is 30 tokens and each row is 8. + // Charging the description-bearing intro rounds overhead up to 31 and drops both rows. + let root = format!("/{}", "a".repeat(250)); + let catalog = SkillCatalog { + entries: ["one", "two"] + .into_iter() + .map(|name| { + entry_with_path( + name, + "Full description.", + Some("Short description."), + &format!("{root}/{name}/SKILL.md"), + ) + .with_alias_root(root.clone()) + }) + .collect(), + ..SkillCatalog::default() + }; + let budget = SkillMetadataBudget::Tokens(46); + let rendered = render_available_skills( + &catalog, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE + .with_omitted_descriptions(/*omit_descriptions*/ true), + budget, + /*include_skills_usage_instructions*/ true, + /*alias_plan*/ None, + ) + .expect("both skills should fit using aliases"); + assert_eq!( + ( + rendered.metadata_cost(budget, /*include_skills_usage_instructions*/ true), + rendered.skill_lines, + rendered.report + ), + ( + 46, + vec![ + "- one: (file: r0/one/SKILL.md)".to_string(), + "- two: (file: r0/two/SKILL.md)".to_string(), + ], + SkillRenderReport { + total_count: 2, + included_count: 2, + ..SkillRenderReport::default() + } + ) + ); } #[test] @@ -600,7 +651,7 @@ async fn host_alias_roots_follow_core_discovery_order() -> Result<(), Box>(); - SkillCatalogRenderPolicy::CoreCompatible.order_entries(&mut entries); + SkillCatalogRenderPolicy::CORE_COMPATIBLE.order_entries(&mut entries); let actual = build_alias_plan(&entries) .expect("alias plan should build") .root_lines(); @@ -651,7 +702,7 @@ fn mixed_catalogs_keep_absolute_authority_aware_rendering_under_budget_pressure( let visible_entries = catalog.entries.iter().collect::>(); let absolute_minimum = visible_entries.iter().fold(0usize, |cost, entry| { cost.saturating_add( - SkillLine::new(entry, SkillCatalogRenderPolicy::ExtensionCompatible) + SkillLine::new(entry, SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE) .minimum_cost(SkillMetadataBudget::Characters(usize::MAX)), ) }); @@ -661,7 +712,7 @@ fn mixed_catalogs_keep_absolute_authority_aware_rendering_under_budget_pressure( let fragment = available_skills_fragment( &catalog, /*include_skills_usage_instructions*/ true, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, SkillMetadataBudget::Characters(absolute_minimum), ) .expect("catalog should render"); @@ -1021,14 +1072,14 @@ fn omission_notice_follows_render_policy_and_is_charged_to_catalog_budget() { let core_fragment = available_skills_fragment( &catalog, /*include_skills_usage_instructions*/ false, - SkillCatalogRenderPolicy::CoreCompatible, + SkillCatalogRenderPolicy::CORE_COMPATIBLE, SkillMetadataBudget::Tokens(100), ) .expect("core-compatible catalog should render"); let fragment = available_skills_fragment( &catalog, /*include_skills_usage_instructions*/ false, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, SkillMetadataBudget::Tokens(100), ) .expect("catalog should render"); @@ -1078,7 +1129,7 @@ fn character_fallback_counts_multibyte_metadata_by_characters() { let fragment = available_skills_fragment( &catalog, /*include_skills_usage_instructions*/ false, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, SkillMetadataBudget::Characters(8_000), ) .expect("catalog should render"); @@ -1112,7 +1163,7 @@ fn catalog_report_counts_partial_description_truncation() { let render = render_available_skills( &catalog, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, budget, /*include_skills_usage_instructions*/ false, /*alias_plan*/ None, @@ -1157,7 +1208,7 @@ fn catalog_emits_omission_marker_when_every_minimum_skill_line_exceeds_budget() }; let core_render = render_available_skills( &catalog, - SkillCatalogRenderPolicy::CoreCompatible, + SkillCatalogRenderPolicy::CORE_COMPATIBLE, SkillMetadataBudget::Tokens(100), /*include_skills_usage_instructions*/ false, /*alias_plan*/ None, @@ -1189,7 +1240,7 @@ fn catalog_emits_omission_marker_when_every_minimum_skill_line_exceeds_budget() assert!(!core_fragment.body().expect_text().contains("- oversized:")); let render = render_available_skills( &catalog, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, SkillMetadataBudget::Tokens(100), /*include_skills_usage_instructions*/ false, /*alias_plan*/ None, @@ -1225,7 +1276,7 @@ fn catalog_preserves_report_when_no_fragment_fits_budget() { let render = render_available_skills( &catalog, - SkillCatalogRenderPolicy::ExtensionCompatible, + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE, SkillMetadataBudget::Tokens(1), /*include_skills_usage_instructions*/ false, /*alias_plan*/ None, diff --git a/codex-rs/ext/skills/src/shadow_selection_experiment/experiment_tests.rs b/codex-rs/ext/skills/src/shadow_selection_experiment/experiment_tests.rs index c40c73e6a8..9668e2b163 100644 --- a/codex-rs/ext/skills/src/shadow_selection_experiment/experiment_tests.rs +++ b/codex-rs/ext/skills/src/shadow_selection_experiment/experiment_tests.rs @@ -79,6 +79,7 @@ async fn turn_input_and_history_advance_while_ranking_is_blocked() let config = SkillsExtensionConfig { include_instructions: false, catalog_deltas_enabled: false, + omit_descriptions: false, max_context_tokens: None, bundled_skills_enabled: false, cloud_skill_enabled: false, diff --git a/codex-rs/ext/skills/src/world_state_catalogs.rs b/codex-rs/ext/skills/src/world_state_catalogs.rs index 06f6d67fd7..7e3b07eeb5 100644 --- a/codex-rs/ext/skills/src/world_state_catalogs.rs +++ b/codex-rs/ext/skills/src/world_state_catalogs.rs @@ -130,10 +130,11 @@ impl<'a> CatalogRenderingContext<'a> { }); let render_policy = if step_state.discovery_v2 { - SkillCatalogRenderPolicy::DiscoveryV2 + SkillCatalogRenderPolicy::DISCOVERY_V2 } else { - SkillCatalogRenderPolicy::ExtensionCompatible - }; + SkillCatalogRenderPolicy::EXTENSION_COMPATIBLE + } + .with_omitted_descriptions(step_state.config.omit_descriptions); Some(Self { input, step_state, @@ -195,10 +196,7 @@ impl<'a> CatalogRenderingContext<'a> { &entry.id.0, entry.authority.kind.to_string(), &entry.name, - entry - .short_description - .as_ref() - .unwrap_or(&entry.description), + self.render_policy.description(entry), entry.rendered_path(), entry.alias_root(), entry.alias_root_order(), @@ -250,7 +248,8 @@ impl<'a> CatalogRenderingContext<'a> { ); let host = PreparedSkillCatalog::new( &catalogs.host.catalog, - SkillCatalogRenderPolicy::CoreCompatible, + SkillCatalogRenderPolicy::CORE_COMPATIBLE + .with_omitted_descriptions(self.step_state.config.omit_descriptions), ); for (catalog, rendered) in [ (&executor, &mut rendered.executor), @@ -314,7 +313,8 @@ impl<'a> CatalogRenderingContext<'a> { ), &PreparedSkillCatalog::new( &catalogs.host.catalog, - SkillCatalogRenderPolicy::CoreCompatible, + SkillCatalogRenderPolicy::CORE_COMPATIBLE + .with_omitted_descriptions(self.step_state.config.omit_descriptions), ), self.metadata_budget, self.include_usage, @@ -371,7 +371,8 @@ impl<'a> CatalogRenderingContext<'a> { &PreparedSkillCatalog::new(&SkillCatalog::default(), self.render_policy), &PreparedSkillCatalog::new( &catalogs.host.catalog, - SkillCatalogRenderPolicy::CoreCompatible, + SkillCatalogRenderPolicy::CORE_COMPATIBLE + .with_omitted_descriptions(self.step_state.config.omit_descriptions), ), filesystem_budget, self.include_usage, diff --git a/codex-rs/ext/skills/tests/skills_extension.rs b/codex-rs/ext/skills/tests/skills_extension.rs index 6f13258acd..90f2dae8cd 100644 --- a/codex-rs/ext/skills/tests/skills_extension.rs +++ b/codex-rs/ext/skills/tests/skills_extension.rs @@ -3614,6 +3614,7 @@ fn test_entry( struct TestConfig { include_instructions: bool, catalog_deltas_enabled: bool, + omit_descriptions: bool, bundled_skills_enabled: bool, cloud_skill_enabled: bool, shadow_selection_enabled: bool, @@ -3623,6 +3624,7 @@ fn default_config() -> TestConfig { TestConfig { include_instructions: true, catalog_deltas_enabled: false, + omit_descriptions: false, bundled_skills_enabled: true, cloud_skill_enabled: true, shadow_selection_enabled: false, @@ -3633,6 +3635,7 @@ fn skills_extension_config(config: &TestConfig) -> SkillsExtensionConfig { SkillsExtensionConfig { include_instructions: config.include_instructions, catalog_deltas_enabled: config.catalog_deltas_enabled, + omit_descriptions: config.omit_descriptions, max_context_tokens: None, bundled_skills_enabled: config.bundled_skills_enabled, cloud_skill_enabled: config.cloud_skill_enabled, diff --git a/codex-rs/ext/skills/tests/skills_extension/catalog_delta_tests.rs b/codex-rs/ext/skills/tests/skills_extension/catalog_delta_tests.rs index fbc7919f0b..44f7689949 100644 --- a/codex-rs/ext/skills/tests/skills_extension/catalog_delta_tests.rs +++ b/codex-rs/ext/skills/tests/skills_extension/catalog_delta_tests.rs @@ -44,6 +44,45 @@ async fn skill_sources_spend_the_same_remaining_budget() -> TestResult { let empty = render_catalogs(&[], &disabled, &mut previous).await?; assert!(empty.contains("skills are currently available"), "{empty}"); assert_eq!(render_catalogs(&[], &disabled, &mut previous).await?, ""); + let omitted = TestConfig { + omit_descriptions: true, + ..enabled + }; + previous.clear(); + let emitted = + render_catalogs(&[("notes", "Hidden description.")], &enabled, &mut previous).await?; + assert!(emitted.contains("description shown above"), "{emitted}"); + assert!( + emitted.contains("- host-notes: Hidden description."), + "{emitted}" + ); + // Resume a snapshot written before the description policy was persisted. + for snapshot in previous.values_mut() { + snapshot.as_object_mut().unwrap().remove("omitDescriptions"); + } + let emitted = + render_catalogs(&[("notes", "Hidden description.")], &omitted, &mut previous).await?; + assert!( + emitted.contains("OR the task clearly matches a skill's name shown above, you must use that skill for that turn."), + "{emitted}" + ); + assert!(emitted.contains("- executor-notes: "), "{emitted}"); + assert!(!emitted.contains("Hidden description."), "{emitted}"); + assert_eq!( + render_catalogs(&[("notes", "Edited description.")], &omitted, &mut previous).await?, + "" + ); + // A policy change still refreshes instructions when no more rows fit the budget. + let emitted = + render_catalogs(&[("notes", "Edited description.")], &enabled, &mut previous).await?; + assert!(emitted.contains("description shown above"), "{emitted}"); + assert!(!emitted.contains("- executor-notes:"), "{emitted}"); + assert!(!emitted.contains("- cloud-notes:"), "{emitted}"); + assert!(!emitted.contains("- host-notes:"), "{emitted}"); + assert_eq!( + render_catalogs(&[("notes", "Edited description.")], &enabled, &mut previous).await?, + "" + ); Ok(()) } @@ -116,6 +155,7 @@ async fn render_catalogs( step_store: &ExtensionData::new("step"), model_info: &ModelInfo { context_window: Some(5_000), + include_skills_usage_instructions: true, ..catalog_model_info() }, thread_id: codex_protocol::ThreadId::new(), diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 88b77b7d0b..129b09668c 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -380,6 +380,8 @@ pub enum Feature { OutputTokenReplay, /// Allow prompting and installing missing MCP dependencies. SkillMcpDependencyInstall, + /// Omit descriptions from the model-visible available-skills catalog. + OmitSkillDescriptions, /// Run cheap skill-search methods in shadow mode and emit experiment metrics. SkillSearch, /// Append skill additions and edits within a cumulative context budget. @@ -1911,6 +1913,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::OmitSkillDescriptions, + key: "omit_skill_descriptions", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::SkillSearch, key: "skill_search",