Use the host skill provider catalog in world state (#36309)

## What changed

- Load and cache the host skill provider catalog during world-state contribution.
- Render host skill listings from that catalog with Core-compatible budgeting, while preserving Core's full prompt injection for selected host skills.
- Reuse the cached host catalog for shadow selection, including when skill listings are disabled.
- Preserve an empty Core-compatible skills fragment when every entry exceeds the metadata budget, and make budget warnings independent of the configured percentage.

## Testing

- Cover provider-backed host listings, selected-skill prompt injection, shadow selection, disabled listings, oversized prompts, empty fragments, metrics, and warnings.

GitOrigin-RevId: b6b2d8ea14386356094cff5783d121d74657e9e3
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-07-31 11:01:47 +00:00
1 parent bf4d3f51ea
commit 5e8b22488f
11 files changed
+545 -166

No files matched your search

+78 -68
View File
@@ -64,6 +64,7 @@ use crate::shadow_selection_experiment::ShadowSelectionExperiment;
use crate::sources::SkillProviders;
use crate::state::EmittedCatalogBudgetWarnings;
use crate::state::ExecutorSkillsStepState;
use crate::state::HostSkillsStepState;
use crate::state::SkillsSessionState;
use crate::state::SkillsThreadState;
use crate::state::SkillsTurnState;
@@ -72,7 +73,6 @@ use crate::warnings::bounded_warnings;
use crate::world_state::HostSkillsWarningEmitter;
use crate::world_state::executor_skills_world_state_section;
use crate::world_state::host_skills_world_state_section;
use crate::world_state::rendered_host_skills_world_state_section;
struct SkillsExtension<C> {
providers: SkillProviders,
@@ -283,42 +283,50 @@ where
let metadata_budget = skill_metadata_budget(context_window);
let extension_metrics = input.extension_metrics.clone();
let host_snapshot = input.turn_store.get::<HostSkillsSnapshot>();
let shared_rendered = if config.include_instructions
let needs_host_catalog = config.include_instructions || config.shadow_selection_enabled;
let host_catalog = if needs_host_catalog
&& let Some(host_snapshot) = host_snapshot.as_ref()
&& self.providers.has_host_provider()
{
let host_catalog = self
.providers
.list_host_for_turn(SkillListQuery {
turn_id: input.turn_id.to_string(),
executor_roots: Vec::new(),
resolved_executor_roots: Vec::new(),
host_snapshot: Some(Arc::clone(host_snapshot)),
include_host_skills: true,
include_bundled_skills: false,
include_orchestrator_skills: false,
mcp_resources: None,
executor_capability_discovery: None,
})
.await;
let (host_rendered, executor_rendered) =
render_combined_available_skills(&host_catalog, &catalog, metadata_budget);
if host_catalog
Some(
self.providers
.list_host_for_turn(SkillListQuery {
turn_id: input.turn_id.to_string(),
executor_roots: Vec::new(),
resolved_executor_roots: Vec::new(),
host_snapshot: Some(Arc::clone(host_snapshot)),
include_host_skills: true,
include_bundled_skills: false,
include_orchestrator_skills: false,
mcp_resources: None,
executor_capability_discovery: None,
})
.await,
)
} else {
None
};
if let Some(host_catalog) = host_catalog.as_ref() {
input
.turn_store
.insert(HostSkillsStepState(host_catalog.clone()));
}
let host_catalog_for_rendering = if config.include_instructions {
host_catalog.as_ref()
} else {
None
};
let shared_rendered = host_catalog_for_rendering.and_then(|host_catalog| {
(host_catalog
.entries
.iter()
.any(SkillCatalogEntry::is_model_visible)
&& catalog
.entries
.iter()
.any(SkillCatalogEntry::is_model_visible)
{
Some((host_rendered, executor_rendered))
} else {
None
}
} else {
None
};
.any(SkillCatalogEntry::is_model_visible))
.then(|| render_combined_available_skills(host_catalog, &catalog, metadata_budget))
});
let (rendered, rendered_host) =
if let Some((host_rendered, executor_rendered)) = shared_rendered {
if let Some(host_rendered) = host_rendered.as_ref() {
@@ -335,6 +343,16 @@ where
host_rendered,
)
} else {
let rendered_host = host_catalog_for_rendering.and_then(|host_catalog| {
render_available_skills(
host_catalog,
SkillCatalogRenderPolicy::CoreCompatible,
metadata_budget,
)
});
if let Some(rendered_host) = rendered_host.as_ref() {
trace_catalog_budget_pressure(metadata_budget, &rendered_host.report);
}
(
if config.include_instructions {
render_catalog(
@@ -348,12 +366,29 @@ where
} else {
RenderedCatalog::default()
},
None,
rendered_host,
)
};
let host_warning_message = rendered_host
.as_ref()
.and_then(|rendered_host| rendered_host.report.warning_message());
let host_render_metrics = config.include_instructions.then(|| {
rendered_host
.as_ref()
.map(|rendered_host| {
let report = &rendered_host.report;
(
report.total_count,
report.included_count,
report.omitted_count,
report.truncated_description_chars,
)
})
.unwrap_or_default()
});
let host_body = rendered_host
.and_then(|rendered_host| rendered_host.into_fragment(include_usage))
.map(|fragment| fragment.body());
let executor_body = rendered.fragment.map(|fragment| fragment.body());
let mut sections = vec![executor_skills_world_state_section(
executor_body,
@@ -362,9 +397,7 @@ where
let emitted_warnings = input
.turn_store
.get_or_init(EmittedCatalogBudgetWarnings::default);
if let Some(host_snapshot) = host_snapshot
&& self.providers.has_host_provider()
{
if host_snapshot.is_some() && self.providers.has_host_provider() {
input.turn_store.insert(HostSkillsCatalogInWorldState);
let event_sink = Arc::clone(&self.event_sink);
let emitted_warnings = Arc::clone(&emitted_warnings);
@@ -379,35 +412,14 @@ where
});
}
});
if let Some(rendered_host) = rendered_host {
let report = &rendered_host.report;
let render_metrics = Some((
report.total_count,
report.included_count,
report.omitted_count,
report.truncated_description_chars,
));
let body = rendered_host
.into_fragment(include_usage)
.map(|fragment| fragment.body());
sections.push(rendered_host_skills_world_state_section(
body,
config.include_instructions,
render_metrics,
extension_metrics,
host_warning_message,
warning_emitter,
));
} else {
sections.push(host_skills_world_state_section(
&host_snapshot,
config.include_instructions,
include_usage,
metadata_budget,
extension_metrics,
warning_emitter,
));
}
sections.push(host_skills_world_state_section(
host_body,
config.include_instructions,
host_render_metrics,
extension_metrics,
host_warning_message,
warning_emitter,
));
}
if let Some(message) = rendered.warning_message
&& emitted_warnings.insert(&message)
@@ -521,18 +533,18 @@ where
let host_snapshot = turn_store.get::<HostSkillsSnapshot>();
let host_catalog_in_world_state =
turn_store.get::<HostSkillsCatalogInWorldState>().is_some();
let host_skills = turn_store.get::<HostSkillsStepState>();
let query = SkillListQuery {
turn_id: input.turn_id.clone(),
executor_roots: Vec::new(),
resolved_executor_roots: Vec::new(),
host_snapshot: host_snapshot.clone(),
include_host_skills: !host_catalog_in_world_state,
include_host_skills: host_skills.is_none() && !host_catalog_in_world_state,
include_bundled_skills: config.bundled_skills_enabled,
include_orchestrator_skills: thread_state.orchestrator_skills_enabled(),
mcp_resources: mcp_resources.clone(),
executor_capability_discovery: None,
};
let host_query = query.clone();
let mut catalog = turn_store
.get::<ExecutorSkillsStepState>()
.map(|executor_skills| executor_skills.0.clone())
@@ -545,8 +557,8 @@ where
let selected_entries = collect_explicit_skill_mentions(&input.user_input, &catalog);
let shadow_selection_turn = if config.shadow_selection_enabled {
let mut shadow_catalog = catalog.clone();
if host_catalog_in_world_state && host_snapshot.is_some() {
shadow_catalog.extend(self.providers.list_host_for_turn(host_query).await);
if let Some(host_skills) = host_skills {
shadow_catalog.extend(host_skills.0.clone());
}
let shadow_selected_entries =
collect_explicit_skill_mentions(&input.user_input, &shadow_catalog);
@@ -562,9 +574,7 @@ where
thread_state
.replace_shadow_selection_turn(input.turn_id.clone(), shadow_selection_turn);
let mut fragments: Vec<Box<dyn ContextualUserFragment + Send>> = Vec::new();
if config.include_instructions
&& turn_store.get::<HostSkillsCatalogInWorldState>().is_none()
{
if config.include_instructions && !host_catalog_in_world_state {
let mut turn_catalog = catalog.clone();
turn_catalog.entries.retain(|entry| {
entry.authority.kind != SkillSourceKind::Executor
-21
View File
@@ -1,4 +1,3 @@
use codex_core_skills::AvailableSkills;
use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS;
use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ALIASES;
use codex_core_skills::render_available_skills_body;
@@ -32,26 +31,6 @@ impl AvailableSkillsInstructions {
skill_lines,
}
}
pub(crate) fn from_available_skills(
available: AvailableSkills,
include_skills_usage_instructions: bool,
) -> Self {
let mut skill_lines = available.skill_lines;
if include_skills_usage_instructions {
skill_lines.push("### How to use skills".to_string());
let instructions = if available.skill_root_lines.is_empty() {
SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS
} else {
SKILLS_HOW_TO_USE_WITH_ALIASES
};
skill_lines.push(instructions.to_string());
}
Self {
skill_root_lines: available.skill_root_lines,
skill_lines,
}
}
}
impl ContextualUserFragment for AvailableSkillsInstructions {
+4 -1
View File
@@ -440,6 +440,7 @@ struct RenderedCatalog {
pub(crate) struct AvailableSkillsRender {
skill_root_lines: Vec<String>,
skill_lines: Vec<String>,
preserve_empty_fragment: bool,
pub(crate) report: SkillRenderReport,
}
@@ -448,7 +449,7 @@ impl AvailableSkillsRender {
self,
include_skills_usage_instructions: bool,
) -> Option<AvailableSkillsInstructions> {
(!self.skill_lines.is_empty()).then(|| {
(self.preserve_empty_fragment || !self.skill_lines.is_empty()).then(|| {
AvailableSkillsInstructions::from_skill_lines(
self.skill_root_lines,
self.skill_lines,
@@ -501,6 +502,7 @@ pub(crate) fn render_available_skills(
Some(AvailableSkillsRender {
skill_root_lines: selected.skill_root_lines,
skill_lines: selected.skill_lines,
preserve_empty_fragment: policy == SkillCatalogRenderPolicy::CoreCompatible,
report: selected.report,
})
}
@@ -615,6 +617,7 @@ fn render_combined_group(
AvailableSkillsRender {
skill_root_lines,
skill_lines: lines.into_iter().map(|rendered| rendered.line).collect(),
preserve_empty_fragment: false,
report: SkillRenderReport {
total_count: skill_lines.len(),
included_count: skill_lines.len().saturating_sub(omitted_count),
+9 -4
View File
@@ -387,6 +387,10 @@ fn mixed_catalog_reserves_executor_omission_marker_by_omitting_host_first() {
"- 1 additional skill omitted from this bounded skills list.".to_string(),
]
);
assert!(
host.into_fragment(/*include_skills_usage_instructions*/ false)
.is_none()
);
}
#[test]
@@ -630,10 +634,11 @@ fn catalog_emits_omission_marker_when_every_minimum_skill_line_exceeds_budget()
)
.expect("core-compatible report should render");
assert_eq!(core_render.report, expected_report);
assert_eq!(
core_render.into_fragment(/*include_skills_usage_instructions*/ false),
None
);
let core_fragment = core_render
.into_fragment(/*include_skills_usage_instructions*/ false)
.expect("core-compatible rendering should preserve an empty skills fragment");
assert!(core_fragment.body().contains("## Skills"));
assert!(!core_fragment.body().contains("- oversized:"));
let render = render_available_skills(
&catalog,
SkillCatalogRenderPolicy::ExtensionCompatible,
+3
View File
@@ -375,3 +375,6 @@ pub(crate) struct SkillsTurnState {
#[derive(Clone, Debug, Default)]
pub(crate) struct ExecutorSkillsStepState(pub(crate) SkillCatalog);
#[derive(Clone, Debug, Default)]
pub(crate) struct HostSkillsStepState(pub(crate) SkillCatalog);
-60
View File
@@ -1,9 +1,5 @@
use std::sync::Arc;
use codex_core_skills::HostSkillsSnapshot;
use codex_core_skills::build_available_skills;
use codex_core_skills::render::SkillRenderSideEffects;
use codex_extension_api::ContextualUserFragment;
use codex_extension_api::ExtensionMetrics;
use codex_extension_api::PreviousWorldStateSection;
use codex_extension_api::RenderedWorldStateFragment;
@@ -12,8 +8,6 @@ use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
use serde_json::json;
use crate::fragments::AvailableSkillsInstructions;
use crate::render::SkillMetadataBudget;
use crate::render_observability::CatalogSurface;
use crate::render_observability::record_catalog_metrics;
@@ -82,60 +76,6 @@ pub(crate) fn executor_skills_world_state_section(
}
pub(crate) fn host_skills_world_state_section(
host_snapshot: &HostSkillsSnapshot,
include_instructions: bool,
include_skills_usage_instructions: bool,
metadata_budget: SkillMetadataBudget,
extension_metrics: Option<Arc<dyn ExtensionMetrics>>,
warning_emitter: HostSkillsWarningEmitter,
) -> WorldStateSectionContribution {
let outcome = host_snapshot.outcome();
let metadata_budget = match metadata_budget {
SkillMetadataBudget::Tokens(limit) => codex_core_skills::SkillMetadataBudget::Tokens(limit),
SkillMetadataBudget::Characters(limit) => {
codex_core_skills::SkillMetadataBudget::Characters(limit)
}
};
let available = if include_instructions {
build_available_skills(outcome, metadata_budget, SkillRenderSideEffects::None)
} else {
None
};
let warning_message = available
.as_ref()
.and_then(|available| available.warning_message.clone());
let render_metrics = include_instructions.then(|| {
available
.as_ref()
.map(|available| {
let report = &available.report;
(
report.total_count,
report.included_count,
report.omitted_count,
report.truncated_description_chars,
)
})
.unwrap_or_default()
});
let body = available.map(|available| {
AvailableSkillsInstructions::from_available_skills(
available,
include_skills_usage_instructions,
)
.body()
});
rendered_host_skills_world_state_section(
body,
include_instructions,
render_metrics,
extension_metrics,
warning_message,
warning_emitter,
)
}
pub(crate) fn rendered_host_skills_world_state_section(
body: Option<String>,
include_instructions: bool,
render_metrics: Option<(usize, usize, usize, usize)>,