Add batched executor capability discovery (#33852)

## Why

Selected capability roots can contribute plugins, MCP servers, connectors, and
skills. Discovering each contribution separately requires repeated access to the
executor filesystem.

## What changed

- Add the `capabilityRoots/discoverV1` exec-server RPC to scan selected roots and
  materialize recognized plugin manifests, configuration files, skill
  instructions, and skill metadata in one bounded request.
- Add the opt-in `executor_capability_discovery` feature, with a thread-scoped
  cache and per-step snapshot shared by MCP and skill discovery.
- Parse MCP, connector, and skill contributions from the materialized snapshot,
  including serving cached skill instructions without another filesystem read.

## Testing

- Cover discovery limits, manifest precedence, root-local failures, cache reuse,
  plugin contributions, and parity with the existing environment skill loader.

GitOrigin-RevId: f98fd2321cafb58c596db02da1f83c09d8eb375d
This commit is contained in:
Bryan Ashley authored and copyberry committed 2026-07-17 15:45:48 +00:00
1 parent caa6e554d6
commit 08e30a2e4e
47 files changed
+1957 -47

No files matched your search

+42
View File
@@ -30,6 +30,7 @@ pub(crate) struct SkillsThreadState {
config: Mutex<SkillsExtensionConfig>,
orchestrator_skills_available: bool,
executor_cache: Mutex<Vec<CachedExecutorCatalog>>,
executor_discovery_cache: Mutex<Option<CachedExecutorDiscoveryCatalog>>,
orchestrator_cache: Mutex<Option<Arc<OrchestratorGenerationCache>>>,
shadow_selection_turn: Mutex<Option<ShadowSelectionTurn>>,
}
@@ -40,6 +41,7 @@ impl SkillsThreadState {
config: Mutex::new(config),
orchestrator_skills_available,
executor_cache: Mutex::new(Vec::new()),
executor_discovery_cache: Mutex::new(None),
orchestrator_cache: Mutex::new(None),
shadow_selection_turn: Mutex::new(None),
}
@@ -107,6 +109,11 @@ impl SkillsThreadState {
providers: &SkillProviders,
mut query: SkillListQuery,
) -> SkillCatalog {
if query.executor_capability_discovery.is_some() {
return self
.executor_discovery_catalog_snapshot(providers, query)
.await;
}
let roots = std::mem::take(&mut query.executor_roots);
let mut catalog = SkillCatalog::default();
for root in roots {
@@ -119,6 +126,36 @@ impl SkillsThreadState {
catalog
}
async fn executor_discovery_catalog_snapshot(
&self,
providers: &SkillProviders,
query: SkillListQuery,
) -> SkillCatalog {
if let Some(cached) = self
.executor_discovery_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.filter(|cached| cached.roots == query.executor_roots)
{
return cached.catalog.clone();
}
let roots = query.executor_roots.clone();
let discovered = providers.list_executor_for_turn(query).await;
let mut cache = self
.executor_discovery_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(cached) = cache.as_ref().filter(|cached| cached.roots == roots) {
return cached.catalog.clone();
}
*cache = Some(CachedExecutorDiscoveryCatalog {
roots,
catalog: discovered.clone(),
});
discovered
}
pub(crate) async fn orchestrator_catalog_snapshot(
&self,
mcp_resources: Option<&McpResourceClient>,
@@ -236,6 +273,11 @@ struct CachedExecutorCatalog {
catalog: SkillCatalog,
}
struct CachedExecutorDiscoveryCatalog {
roots: Vec<SelectedCapabilityRoot>,
catalog: SkillCatalog,
}
struct OrchestratorGenerationCache {
mcp_cache_key: Option<McpResourceClientCacheKey>,
catalog: OnceCell<SkillCatalog>,