mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Add an option to omit descriptions from skill catalogs (#57500)
## What changed Add the disabled-by-default `omit_skill_descriptions` feature flag to omit descriptions from model-visible host, executor, and cloud skill catalogs while retaining names and locators. Adapt catalog introductions, usage instructions, and alias budget accounting to the selected policy. Persist the description policy in catalog snapshots and refresh usage instructions when it changes. Ignore description-only edits in catalog deltas while descriptions are omitted. ## Testing Add coverage for alias token budgets, policy changes with older snapshots, and suppressed description-only updates. Extend scenario snapshots to cover plugin visibility and catalog deduplication with descriptions omitted. GitOrigin-RevId: 5bf1dcee8c04059c7a78998affc1efcdef8ec883
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -89,6 +89,7 @@ fn skills_extensions() -> Arc<ExtensionRegistry<Config>> {
|
||||
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,
|
||||
|
||||
@@ -94,6 +94,7 @@ fn skills_extensions() -> Arc<ExtensionRegistry<Config>> {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -207,6 +207,9 @@ fn skills_extensions() -> Arc<ExtensionRegistry<Config>> {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<usize>) -> 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+114
@@ -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: "<PROMPT_CACHE_KEY 1>"
|
||||
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]
|
||||
|
||||
<OMITTED 155 LINES; ~4935 TOKENS; hash=5573869716192298>
|
||||
|
||||
- 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_instructions>
|
||||
## 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` = `<SKILLS_ROOT>`
|
||||
- `r1` = `<PLUGINS_CACHE>/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)
|
||||
</skills_instructions>
|
||||
[02] <permissions instructions>
|
||||
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.
|
||||
</permissions instructions>
|
||||
[03] <collaboration_mode># 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 `<collaboration_mode>...<...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.
|
||||
</collaboration_mode>
|
||||
03:message/developer:
|
||||
<multi_agent_role>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.
|
||||
<OMITTED 17 LINES; ~246 TOKENS; hash=780031e2df907944>
|
||||
- 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"`.</multi_agent_role> [hash=d5928243dc643bcc]
|
||||
04:message/developer:
|
||||
<multi_agent_mode>Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work.</multi_agent_mode> [hash=2b6e037067b4390c]
|
||||
05:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="disabled"><...e="unrestricted" /></permission_profile></filesystem> [hash=d40b2f45cb028888]
|
||||
</environment_context>
|
||||
06:message/user:
|
||||
Plan a team kickoff. Which planning skills are available?
|
||||
07:configuration_update:reasoning={"effort":"low"}
|
||||
+125
@@ -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: "<PROMPT_CACHE_KEY 1>"
|
||||
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]
|
||||
|
||||
<OMITTED 155 LINES; ~4935 TOKENS; hash=5573869716192298>
|
||||
|
||||
- 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] <permissions instructions>
|
||||
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.
|
||||
</permissions instructions>
|
||||
[02] <collaboration_mode># 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 `<collaboration_mode>...<...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.
|
||||
</collaboration_mode>
|
||||
[03] <skills_instructions>
|
||||
## 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":"<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)
|
||||
</skills_instructions>
|
||||
[04] <skills_instructions>
|
||||
## 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":"<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)
|
||||
</skills_instructions>
|
||||
03:message/developer:
|
||||
<multi_agent_role>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.
|
||||
<OMITTED 17 LINES; ~246 TOKENS; hash=780031e2df907944>
|
||||
- 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"`.</multi_agent_role> [hash=d5928243dc643bcc]
|
||||
04:message/developer:
|
||||
<multi_agent_mode>Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work.</multi_agent_mode> [hash=2b6e037067b4390c]
|
||||
05:message/user:
|
||||
<environment_context>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
</environment_context>
|
||||
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?
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String, String>,
|
||||
usage: CatalogUsage,
|
||||
instructions_sent: bool,
|
||||
#[serde(default)]
|
||||
omit_descriptions: bool,
|
||||
allocation: Option<CatalogBudgetAllocation>,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String> = 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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<NonZeroUsize>,
|
||||
/// Whether bundled skills are eligible for discovery.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
|
||||
@@ -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<String>,
|
||||
skill_lines: Vec<String>,
|
||||
}
|
||||
|
||||
impl AvailableSkillsInstructions {
|
||||
pub(crate) fn from_skill_lines(
|
||||
prompt_kind: SkillPromptKind,
|
||||
prompt: SkillPrompt,
|
||||
skill_root_lines: Vec<String>,
|
||||
mut skill_lines: Vec<String>,
|
||||
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,
|
||||
))
|
||||
|
||||
@@ -46,6 +46,7 @@ fn detect_executor_skill(command: &str, environments: &[&str]) -> Option<SkillIn
|
||||
config: crate::SkillsExtensionConfig {
|
||||
include_instructions: true,
|
||||
catalog_deltas_enabled: false,
|
||||
omit_descriptions: false,
|
||||
max_context_tokens: None,
|
||||
bundled_skills_enabled: false,
|
||||
cloud_skill_enabled: false,
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::catalog::SkillCatalog;
|
||||
use crate::catalog::SkillCatalogEntry;
|
||||
use crate::catalog::SkillSourceKind;
|
||||
use crate::catalog_delta::CatalogSkill;
|
||||
use crate::catalog_prompt::SkillPrompt;
|
||||
use crate::catalog_prompt::SkillPromptKind;
|
||||
use crate::catalog_prompt::render_available_skills_body;
|
||||
use crate::fragments::AvailableSkillsInstructions;
|
||||
@@ -32,26 +33,64 @@ pub(crate) const MAX_SKILL_NAME_BYTES: usize = 256;
|
||||
pub(crate) const MAX_SKILL_PATH_BYTES: usize = 1_024;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SkillCatalogRenderPolicy {
|
||||
enum SkillCatalogRenderStyle {
|
||||
CoreCompatible,
|
||||
ExtensionCompatible,
|
||||
DiscoveryV2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct SkillCatalogRenderPolicy {
|
||||
style: SkillCatalogRenderStyle,
|
||||
omit_descriptions: bool,
|
||||
}
|
||||
|
||||
impl SkillCatalogRenderPolicy {
|
||||
fn description(self, entry: &SkillCatalogEntry) -> &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<String>,
|
||||
skill_lines: Vec<String>,
|
||||
included_ids: Option<Vec<String>>,
|
||||
@@ -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<String>,
|
||||
skill_lines: Vec<String>,
|
||||
included_ids: Option<Vec<String>>,
|
||||
@@ -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<AvailableSkillsInstructions> {
|
||||
(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<SkillLine<'a>>,
|
||||
root_lines: Vec<String>,
|
||||
}
|
||||
@@ -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<String>,
|
||||
omission_marker: Option<String>,
|
||||
) -> 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<SkillLine<'_>>,
|
||||
budget: SkillMetadataBudget,
|
||||
skill_root_lines: Vec<String>,
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<dyn st
|
||||
})
|
||||
.await?;
|
||||
let mut entries = catalog.entries.iter().collect::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user