diff --git a/codex-rs/ext/skills/src/extension.rs b/codex-rs/ext/skills/src/extension.rs index de9facd8c2..d86cdf10c2 100644 --- a/codex-rs/ext/skills/src/extension.rs +++ b/codex-rs/ext/skills/src/extension.rs @@ -293,12 +293,16 @@ impl SkillsExtension { mut query: SkillListQuery, thread_state: &SkillsThreadState, ) -> SkillCatalog { + let executor_query = query.clone(); let include_orchestrator_skills = query.include_orchestrator_skills; let orchestrator_query = query.clone(); let mcp_resources = orchestrator_query.mcp_resources.clone(); query.include_orchestrator_skills = false; - let mut catalog = self.providers.list_for_turn(query).await; + let executor_catalog = thread_state + .executor_catalog_snapshot(self.providers.list_executor_for_turn(executor_query)) + .await; + let mut catalog = self.providers.list_for_turn(query, executor_catalog).await; if include_orchestrator_skills { let orchestrator_catalog = thread_state .orchestrator_catalog_snapshot( diff --git a/codex-rs/ext/skills/src/sources.rs b/codex-rs/ext/skills/src/sources.rs index d558c13b3b..78369910e4 100644 --- a/codex-rs/ext/skills/src/sources.rs +++ b/codex-rs/ext/skills/src/sources.rs @@ -107,9 +107,40 @@ impl SkillProviders { .any(|source| source.kind == SkillSourceKind::Orchestrator) } - pub(crate) async fn list_for_turn(&self, query: SkillListQuery) -> SkillCatalog { - self.list_matching(&query, |source| source.should_list(&query)) - .await + pub(crate) async fn list_for_turn( + &self, + query: SkillListQuery, + executor_catalog: SkillCatalog, + ) -> SkillCatalog { + let mut catalog = SkillCatalog::default(); + let mut executor_catalog = Some(executor_catalog); + + for source in self + .sources + .iter() + .filter(|source| source.should_list(&query)) + { + if source.kind == SkillSourceKind::Executor { + if let Some(executor_catalog) = executor_catalog.take() { + catalog.extend(executor_catalog); + } + continue; + } + extend_catalog( + &mut catalog, + source.provider.list(query.clone()).await, + source.label.as_str(), + ); + } + + catalog + } + + pub(crate) async fn list_executor_for_turn(&self, query: SkillListQuery) -> SkillCatalog { + self.list_matching(&query, |source| { + source.kind == SkillSourceKind::Executor && source.should_list(&query) + }) + .await } pub(crate) async fn list_orchestrator_for_turn( diff --git a/codex-rs/ext/skills/src/state.rs b/codex-rs/ext/skills/src/state.rs index 0b751ebdc2..3cd1f69ef4 100644 --- a/codex-rs/ext/skills/src/state.rs +++ b/codex-rs/ext/skills/src/state.rs @@ -27,6 +27,7 @@ const MAX_CACHED_ORCHESTRATOR_CONTENT_BYTES: usize = 8 * 1024 * 1024; pub(crate) struct SkillsThreadState { config: Mutex, selected_roots: Vec, + executor_catalog: OnceCell, orchestrator_skills_available: bool, orchestrator_cache: Mutex>>, } @@ -40,6 +41,7 @@ impl SkillsThreadState { Self { config: Mutex::new(config), selected_roots, + executor_catalog: OnceCell::new(), orchestrator_skills_available, orchestrator_cache: Mutex::new(None), } @@ -67,6 +69,16 @@ impl SkillsThreadState { self.orchestrator_skills_available && self.config().orchestrator_skills_enabled } + pub(crate) async fn executor_catalog_snapshot( + &self, + initialize: impl Future + Send, + ) -> SkillCatalog { + self.executor_catalog + .get_or_init(|| initialize) + .await + .clone() + } + pub(crate) async fn orchestrator_catalog_snapshot( &self, mcp_resources: Option<&McpResourceClient>, diff --git a/codex-rs/ext/skills/tests/skills_extension.rs b/codex-rs/ext/skills/tests/skills_extension.rs index 761dc34d71..d3b08bd5cb 100644 --- a/codex-rs/ext/skills/tests/skills_extension.rs +++ b/codex-rs/ext/skills/tests/skills_extension.rs @@ -147,6 +147,7 @@ async fn installed_extension_uses_host_service_snapshot() -> TestResult { async fn selected_executor_catalog_is_context_and_selected_entrypoint_is_turn_input() -> TestResult { let read_requests = Arc::new(Mutex::new(Vec::new())); + let list_calls = Arc::new(AtomicUsize::new(0)); let executor_provider = Arc::new(StaticSkillProvider { catalog: SkillCatalog { entries: vec![test_entry( @@ -158,7 +159,7 @@ async fn selected_executor_catalog_is_context_and_selected_entrypoint_is_turn_in warnings: Vec::new(), }, read_requests: Arc::clone(&read_requests), - list_calls: None, + list_calls: Some(Arc::clone(&list_calls)), fail_first_list: false, }); let providers = SkillProviders::new().with_executor_provider(executor_provider); @@ -257,6 +258,7 @@ async fn selected_executor_catalog_is_context_and_selected_entrypoint_is_turn_in .await; assert!(next_fragments.is_empty()); + assert_eq!(1, list_calls.load(Ordering::Relaxed)); Ok(()) }