Skip to content
Prev Previous commit
Next Next commit
Project executor skills through World State
  • Loading branch information
jif-oai committed Jun 25, 2026
commit 04a50f49c5b0cad7dd3e35c3ad8ca43bf90dc0ef
59 changes: 56 additions & 3 deletions codex-rs/ext/skills/src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ use codex_extension_api::ToolContributor;
use codex_extension_api::ToolExecutor;
use codex_extension_api::TurnInputContext;
use codex_extension_api::TurnInputContributor;
use codex_extension_api::WorldStateContributionInput;
use codex_extension_api::WorldStateSectionContribution;
use codex_mcp::McpResourceClient;
use codex_protocol::capabilities::CapabilityRootLocation;
use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
Expand All @@ -40,9 +43,11 @@ use crate::render::truncate_main_prompt_contents;
use crate::render::truncate_utf8_to_bytes;
use crate::selection::collect_explicit_skill_mentions;
use crate::sources::SkillProviders;
use crate::state::ExecutorSkillsStepState;
use crate::state::SkillsThreadState;
use crate::state::SkillsTurnState;
use crate::tools::skill_tools;
use crate::world_state::executor_skills_world_state_section;

struct SkillsExtension<C> {
providers: SkillProviders,
Expand Down Expand Up @@ -120,7 +125,7 @@ where
.list_skills(
SkillListQuery {
turn_id: thread_store.level_id().to_string(),
executor_roots: thread_state.selected_roots().to_vec(),
executor_roots: Vec::new(),
host_snapshot: None,
include_host_skills: false,
include_bundled_skills: config.bundled_skills_enabled,
Expand All @@ -139,6 +144,51 @@ where
.collect()
})
}

fn contribute_world_state<'a>(
&'a self,
input: WorldStateContributionInput<'a>,
) -> ExtensionFuture<'a, Vec<WorldStateSectionContribution>> {
Box::pin(async move {
let Some(thread_state) = input.thread_store.get::<SkillsThreadState>() else {
return Vec::new();
};
let config = thread_state.config();
let ready_roots = thread_state
.selected_roots()
.iter()
.filter(|root| {
let CapabilityRootLocation::Environment { environment_id, .. } = &root.location;
input
.environments
Comment thread
jif-oai marked this conversation as resolved.
Outdated
.iter()
.any(|environment| environment.environment_id == *environment_id)
})
.cloned()
.collect();
let catalog = thread_state
.executor_catalog_snapshot(
&self.providers,
SkillListQuery {
turn_id: input.turn_id.to_string(),
executor_roots: ready_roots,
host_snapshot: None,
include_host_skills: false,
include_bundled_skills: config.bundled_skills_enabled,
include_orchestrator_skills: false,
mcp_resources: input.session_store.get::<McpResourceClient>(),
},
)
.await;
input
.turn_store
.insert(ExecutorSkillsStepState(catalog.clone()));
vec![executor_skills_world_state_section(
&catalog,
config.include_instructions,
)]
})
}
}

impl<C> ToolContributor for SkillsExtension<C>
Expand Down Expand Up @@ -187,14 +237,17 @@ where
let host_snapshot = turn_store.get::<HostSkillsSnapshot>();
let query = SkillListQuery {
turn_id: input.turn_id.clone(),
executor_roots: thread_state.selected_roots().to_vec(),
executor_roots: Vec::new(),
host_snapshot: host_snapshot.clone(),
include_host_skills: true,
include_bundled_skills: config.bundled_skills_enabled,
include_orchestrator_skills: thread_state.orchestrator_skills_enabled(),
mcp_resources: session_store.get::<McpResourceClient>(),
};
let catalog = self.list_skills(query, &thread_state).await;
let mut catalog = self.list_skills(query, &thread_state).await;
if let Some(executor_skills) = turn_store.get::<ExecutorSkillsStepState>() {
catalog.extend(executor_skills.0.clone());
Comment thread
jif-oai marked this conversation as resolved.
}
for warning in &catalog.warnings {
self.emit_warning(&input.turn_id, warning.clone());
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/ext/skills/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod selection;
mod sources;
mod state;
mod tools;
mod world_state;

pub use config::SkillsExtensionConfig;
pub use extension::install;
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/ext/skills/src/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ impl SkillProviders {
Ok(catalog)
}

pub(crate) async fn list_executor_for_turn(&self, query: SkillListQuery) -> SkillCatalog {
self.list_matching(&query, |source| source.kind == SkillSourceKind::Executor)
.await
}

async fn list_matching(
&self,
query: &SkillListQuery,
Expand Down
65 changes: 65 additions & 0 deletions codex-rs/ext/skills/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::catalog::SkillProviderResult;
use crate::catalog::SkillReadResult;
use crate::catalog::SkillResourceId;
use crate::catalog::SkillSourceKind;
use crate::provider::SkillListQuery;
use crate::provider::SkillReadRequest;
use crate::sources::SkillProviders;

Expand All @@ -28,6 +29,7 @@ pub(crate) struct SkillsThreadState {
config: Mutex<SkillsExtensionConfig>,
selected_roots: Vec<SelectedCapabilityRoot>,
orchestrator_skills_available: bool,
executor_cache: Mutex<Vec<CachedExecutorCatalog>>,
orchestrator_cache: Mutex<Option<Arc<OrchestratorGenerationCache>>>,
}

Expand All @@ -41,6 +43,7 @@ impl SkillsThreadState {
config: Mutex::new(config),
selected_roots,
orchestrator_skills_available,
executor_cache: Mutex::new(Vec::new()),
orchestrator_cache: Mutex::new(None),
}
}
Expand All @@ -67,6 +70,29 @@ impl SkillsThreadState {
self.orchestrator_skills_available && self.config().orchestrator_skills_enabled
}

/// Returns catalogs for stable selected roots.
///
/// The first catalog returned for a root remains cached until this thread state is dropped.
/// Environment availability only controls whether the root is projected into the current
/// step; it never invalidates the cache. There is intentionally no filesystem watcher or
/// content-based invalidation because selected environment roots are treated as stable.
pub(crate) async fn executor_catalog_snapshot(
&self,
providers: &SkillProviders,
mut query: SkillListQuery,
) -> SkillCatalog {
let roots = std::mem::take(&mut query.executor_roots);
let mut catalog = SkillCatalog::default();
for root in roots {
query.executor_roots = vec![root.clone()];
catalog.extend(
self.executor_root_catalog(providers, root, query.clone())
.await,
);
}
catalog
}

pub(crate) async fn orchestrator_catalog_snapshot(
&self,
mcp_resources: Option<&McpResourceClient>,
Expand Down Expand Up @@ -140,6 +166,42 @@ impl SkillsThreadState {
*cache = Some(Arc::clone(&next_cache));
next_cache
}

async fn executor_root_catalog(
&self,
providers: &SkillProviders,
root: SelectedCapabilityRoot,
query: SkillListQuery,
) -> SkillCatalog {
if let Some(cached) = self
.executor_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.find(|cached| cached.root == root)
{
return cached.catalog.clone();
}

let discovered = providers.list_executor_for_turn(query).await;
let mut cache = self
.executor_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(cached) = cache.iter().find(|cached| cached.root == root) {
return cached.catalog.clone();
}
cache.push(CachedExecutorCatalog {
Comment thread
jif-oai marked this conversation as resolved.
root,
catalog: discovered.clone(),
});
discovered
}
}

struct CachedExecutorCatalog {
root: SelectedCapabilityRoot,
catalog: SkillCatalog,
}

struct OrchestratorGenerationCache {
Expand Down Expand Up @@ -204,3 +266,6 @@ pub(crate) struct SkillsTurnState {
pub(crate) warnings: Vec<String>,
pub(crate) main_prompts_injected: bool,
}

#[derive(Clone, Debug, Default)]
pub(crate) struct ExecutorSkillsStepState(pub(crate) SkillCatalog);
56 changes: 56 additions & 0 deletions codex-rs/ext/skills/src/world_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use codex_extension_api::ContextualUserFragment;
use codex_extension_api::PreviousWorldStateSection;
use codex_extension_api::RenderedWorldStateFragment;
use codex_extension_api::WorldStateSectionContribution;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
use serde_json::json;

use crate::catalog::SkillCatalog;
use crate::render::available_skills_fragment;

pub(crate) const SKILLS_WORLD_STATE_ID: &str = "skills";
const NO_EXECUTOR_SKILLS_BODY: &str =
"\n## Skills update\nNo selected-environment skills are currently available.\n";

pub(crate) fn executor_skills_world_state_section(
catalog: &SkillCatalog,
include_instructions: bool,
) -> WorldStateSectionContribution {
let body = if include_instructions {
available_skills_fragment(catalog).map(|fragment| fragment.body())
Comment thread
jif-oai marked this conversation as resolved.
} else {
None
};
let snapshot = json!({"body": body});

WorldStateSectionContribution::new(SKILLS_WORLD_STATE_ID, snapshot, move |previous| {
let previous_is_absent = matches!(&previous, PreviousWorldStateSection::Absent);
let previous_is_known = matches!(&previous, PreviousWorldStateSection::Known(_));
let previous_body = match &previous {
PreviousWorldStateSection::Known(previous) => {
previous.get("body").and_then(serde_json::Value::as_str)
}
PreviousWorldStateSection::Absent | PreviousWorldStateSection::Unknown => None,
};
if previous_is_known && previous_body == body.as_deref() {
return None;
}

let body = match body.as_deref() {
Some(body) => body,
None if previous_is_absent => return None,
None => NO_EXECUTOR_SKILLS_BODY,
};
Some(RenderedWorldStateFragment::new(
"developer",
(SKILLS_INSTRUCTIONS_OPEN_TAG, SKILLS_INSTRUCTIONS_CLOSE_TAG),
body,
))
})
.with_legacy_matcher(|role, text| {
role == "developer"
&& text.trim_start().starts_with(SKILLS_INSTRUCTIONS_OPEN_TAG)
&& text.trim_end().ends_with(SKILLS_INSTRUCTIONS_CLOSE_TAG)
Comment thread
jif-oai marked this conversation as resolved.
})
}
Loading
Loading