Add a skill invocation extension contributor (#32200)

## What changed

- Add `SkillInvocationContributor` and registration support to the extension API.
- Provide invocation callbacks with session-, thread-, and turn-scoped extension data, the turn ID, skill resource, and explicit or implicit invocation kind.
- Notify registered contributors when Codex observes a deduplicated implicit skill invocation.

## Testing

- Extend the extension registry round-trip test to cover skill invocation contributors.

GitOrigin-RevId: f428336728889d9bf4fba41a7dd8d3a6f9b9728c
This commit is contained in:
jif
2026-07-10 15:52:04 +00:00
committed by copyberry
parent 601b61a83f
commit 2b9c050460
6 changed files with 87 additions and 0 deletions
+15
View File
@@ -4,6 +4,8 @@ use crate::session::turn_context::TurnContext;
use codex_analytics::InvocationType;
use codex_analytics::SkillInvocation;
use codex_analytics::build_track_events_context;
use codex_extension_api::SkillInvocationInput;
use codex_extension_api::SkillInvocationKind;
use codex_protocol::protocol::SkillScope;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_plugins::PluginSkillRoot;
@@ -86,6 +88,19 @@ pub(crate) async fn maybe_emit_implicit_skill_invocation(
return;
}
for contributor in sess.services.extensions.skill_invocation_contributors() {
contributor
.on_skill_invocation(SkillInvocationInput {
session_store: &sess.services.session_extension_data,
thread_store: &sess.services.thread_extension_data,
turn_store: turn_context.extension_data.as_ref(),
turn_id: turn_context.sub_id.as_str(),
skill_resource: skill_path.as_ref(),
kind: SkillInvocationKind::Implicit,
})
.await;
}
turn_context.session_telemetry.counter(
"codex.skill.injected",
/*inc*/ 1,
@@ -14,6 +14,7 @@ use crate::ExtensionData;
mod context;
mod mcp;
mod prompt;
mod skill_invocation;
mod thread_lifecycle;
mod tool_lifecycle;
mod turn_input;
@@ -25,6 +26,8 @@ pub use mcp::McpServerContribution;
pub use mcp::McpServerContributionContext;
pub use prompt::PromptFragment;
pub use prompt::PromptSlot;
pub use skill_invocation::SkillInvocationInput;
pub use skill_invocation::SkillInvocationKind;
pub use thread_lifecycle::ThreadIdleInput;
pub use thread_lifecycle::ThreadResumeInput;
pub use thread_lifecycle::ThreadStartInput;
@@ -247,6 +250,23 @@ pub trait TokenUsageContributor: Send + Sync {
}
}
/// Contributor for skill invocations observed by the host or an owning extension.
///
/// Implementations should treat the skill resource as an opaque identity and keep this callback
/// cheap because it runs inline with skill loading or command dispatch.
pub trait SkillInvocationContributor: Send + Sync {
/// Called after one explicit skill load or deduplicated implicit skill invocation is observed.
fn on_skill_invocation<'a>(
&'a self,
_input: SkillInvocationInput<'a>,
) -> ExtensionFuture<'a, ()> {
Box::pin(async move {
let _self = self;
let _input = _input;
})
}
}
/// Extension contribution that exposes native tools owned by a feature.
pub trait ToolContributor: Send + Sync {
/// Returns the native tools visible for the supplied extension stores.
@@ -0,0 +1,26 @@
use crate::ExtensionData;
/// Input supplied when the host or an extension observes one skill invocation.
pub struct SkillInvocationInput<'a> {
/// Store scoped to the host session runtime.
pub session_store: &'a ExtensionData,
/// Store scoped to this thread runtime.
pub thread_store: &'a ExtensionData,
/// Store scoped to this turn runtime.
pub turn_store: &'a ExtensionData,
/// Current turn submission id.
pub turn_id: &'a str,
/// Main prompt path or opaque resource id for the invoked skill.
pub skill_resource: &'a str,
/// How the skill invocation was initiated.
pub kind: SkillInvocationKind,
}
/// How an observed skill invocation was initiated.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SkillInvocationKind {
/// The user explicitly mentioned the skill.
Explicit,
/// The model read the skill instructions or ran one of its scripts.
Implicit,
}
+3
View File
@@ -42,6 +42,9 @@ pub use contributors::PreviousWorldStateSection;
pub use contributors::PromptFragment;
pub use contributors::PromptSlot;
pub use contributors::RenderedWorldStateFragment;
pub use contributors::SkillInvocationContributor;
pub use contributors::SkillInvocationInput;
pub use contributors::SkillInvocationKind;
pub use contributors::ThreadIdleInput;
pub use contributors::ThreadLifecycleContributor;
pub use contributors::ThreadResumeInput;
@@ -9,6 +9,7 @@ use crate::ExtensionData;
use crate::ExtensionEventSink;
use crate::McpServerContributor;
use crate::NoopExtensionEventSink;
use crate::SkillInvocationContributor;
use crate::ThreadLifecycleContributor;
use crate::TokenUsageContributor;
use crate::ToolContributor;
@@ -24,6 +25,7 @@ pub struct ExtensionRegistryBuilder<C: Sync> {
turn_lifecycle_contributors: Vec<Arc<dyn TurnLifecycleContributor>>,
config_contributors: Vec<Arc<dyn ConfigContributor<C>>>,
token_usage_contributors: Vec<Arc<dyn TokenUsageContributor>>,
skill_invocation_contributors: Vec<Arc<dyn SkillInvocationContributor>>,
context_contributors: Vec<Arc<dyn ContextContributor>>,
mcp_server_contributors: Vec<Arc<dyn McpServerContributor<C>>>,
turn_input_contributors: Vec<Arc<dyn TurnInputContributor>>,
@@ -41,6 +43,7 @@ impl<C: Sync> Default for ExtensionRegistryBuilder<C> {
turn_lifecycle_contributors: Vec::new(),
config_contributors: Vec::new(),
token_usage_contributors: Vec::new(),
skill_invocation_contributors: Vec::new(),
approval_review_contributors: Vec::new(),
context_contributors: Vec::new(),
mcp_server_contributors: Vec::new(),
@@ -99,6 +102,14 @@ impl<C: Sync> ExtensionRegistryBuilder<C> {
self.token_usage_contributors.push(contributor);
}
/// Registers one skill-invocation contributor.
pub fn skill_invocation_contributor(
&mut self,
contributor: Arc<dyn SkillInvocationContributor>,
) {
self.skill_invocation_contributors.push(contributor);
}
/// Registers one prompt contributor.
pub fn prompt_contributor(&mut self, contributor: Arc<dyn ContextContributor>) {
self.context_contributors.push(contributor);
@@ -137,6 +148,7 @@ impl<C: Sync> ExtensionRegistryBuilder<C> {
turn_lifecycle_contributors: self.turn_lifecycle_contributors,
config_contributors: self.config_contributors,
token_usage_contributors: self.token_usage_contributors,
skill_invocation_contributors: self.skill_invocation_contributors,
approval_review_contributors: self.approval_review_contributors,
context_contributors: self.context_contributors,
mcp_server_contributors: self.mcp_server_contributors,
@@ -155,6 +167,7 @@ pub struct ExtensionRegistry<C: Sync> {
turn_lifecycle_contributors: Vec<Arc<dyn TurnLifecycleContributor>>,
config_contributors: Vec<Arc<dyn ConfigContributor<C>>>,
token_usage_contributors: Vec<Arc<dyn TokenUsageContributor>>,
skill_invocation_contributors: Vec<Arc<dyn SkillInvocationContributor>>,
context_contributors: Vec<Arc<dyn ContextContributor>>,
mcp_server_contributors: Vec<Arc<dyn McpServerContributor<C>>>,
turn_input_contributors: Vec<Arc<dyn TurnInputContributor>>,
@@ -190,6 +203,11 @@ impl<C: Sync> ExtensionRegistry<C> {
&self.token_usage_contributors
}
/// Returns the registered skill-invocation contributors.
pub fn skill_invocation_contributors(&self) -> &[Arc<dyn SkillInvocationContributor>] {
&self.skill_invocation_contributors
}
/// Claims the first rendered approval-review prompt accepted by an
/// installed contributor.
pub async fn approval_review(
@@ -13,6 +13,7 @@ use codex_extension_api::ExtensionFuture;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::PromptFragment;
use codex_extension_api::PromptSlot;
use codex_extension_api::SkillInvocationContributor;
use codex_extension_api::ThreadLifecycleContributor;
use codex_extension_api::TokenUsageContributor;
use codex_extension_api::ToolCall;
@@ -53,6 +54,8 @@ impl ConfigContributor<()> for AllContributors {}
impl TokenUsageContributor for AllContributors {}
impl SkillInvocationContributor for AllContributors {}
impl TurnInputContributor for AllContributors {
fn contribute<'a>(
&'a self,
@@ -117,6 +120,7 @@ async fn build_round_trips_every_contributor_category() {
builder.turn_lifecycle_contributor(contributor.clone());
builder.config_contributor(contributor.clone());
builder.token_usage_contributor(contributor.clone());
builder.skill_invocation_contributor(contributor.clone());
builder.prompt_contributor(contributor.clone());
builder.turn_input_contributor(contributor.clone());
builder.tool_contributor(contributor.clone());
@@ -129,6 +133,7 @@ async fn build_round_trips_every_contributor_category() {
assert_eq!(registry.turn_lifecycle_contributors().len(), 1);
assert_eq!(registry.config_contributors().len(), 1);
assert_eq!(registry.token_usage_contributors().len(), 1);
assert_eq!(registry.skill_invocation_contributors().len(), 1);
assert_eq!(registry.context_contributors().len(), 1);
assert_eq!(registry.turn_input_contributors().len(), 1);
assert_eq!(registry.tool_contributors().len(), 1);