diff --git a/codex-rs/core/src/skills.rs b/codex-rs/core/src/skills.rs index 7b5d87b26f..c5182a529c 100644 --- a/codex-rs/core/src/skills.rs +++ b/codex-rs/core/src/skills.rs @@ -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, diff --git a/codex-rs/ext/extension-api/src/contributors.rs b/codex-rs/ext/extension-api/src/contributors.rs index 96f1ee0b48..0ddf5a75d9 100644 --- a/codex-rs/ext/extension-api/src/contributors.rs +++ b/codex-rs/ext/extension-api/src/contributors.rs @@ -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. diff --git a/codex-rs/ext/extension-api/src/contributors/skill_invocation.rs b/codex-rs/ext/extension-api/src/contributors/skill_invocation.rs new file mode 100644 index 0000000000..5479c54177 --- /dev/null +++ b/codex-rs/ext/extension-api/src/contributors/skill_invocation.rs @@ -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, +} diff --git a/codex-rs/ext/extension-api/src/lib.rs b/codex-rs/ext/extension-api/src/lib.rs index c3873a2ed3..7722c74d6e 100644 --- a/codex-rs/ext/extension-api/src/lib.rs +++ b/codex-rs/ext/extension-api/src/lib.rs @@ -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; diff --git a/codex-rs/ext/extension-api/src/registry.rs b/codex-rs/ext/extension-api/src/registry.rs index e8cc989337..842155ea29 100644 --- a/codex-rs/ext/extension-api/src/registry.rs +++ b/codex-rs/ext/extension-api/src/registry.rs @@ -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 { turn_lifecycle_contributors: Vec>, config_contributors: Vec>>, token_usage_contributors: Vec>, + skill_invocation_contributors: Vec>, context_contributors: Vec>, mcp_server_contributors: Vec>>, turn_input_contributors: Vec>, @@ -41,6 +43,7 @@ impl Default for ExtensionRegistryBuilder { 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 ExtensionRegistryBuilder { self.token_usage_contributors.push(contributor); } + /// Registers one skill-invocation contributor. + pub fn skill_invocation_contributor( + &mut self, + contributor: Arc, + ) { + self.skill_invocation_contributors.push(contributor); + } + /// Registers one prompt contributor. pub fn prompt_contributor(&mut self, contributor: Arc) { self.context_contributors.push(contributor); @@ -137,6 +148,7 @@ impl ExtensionRegistryBuilder { 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 { turn_lifecycle_contributors: Vec>, config_contributors: Vec>>, token_usage_contributors: Vec>, + skill_invocation_contributors: Vec>, context_contributors: Vec>, mcp_server_contributors: Vec>>, turn_input_contributors: Vec>, @@ -190,6 +203,11 @@ impl ExtensionRegistry { &self.token_usage_contributors } + /// Returns the registered skill-invocation contributors. + pub fn skill_invocation_contributors(&self) -> &[Arc] { + &self.skill_invocation_contributors + } + /// Claims the first rendered approval-review prompt accepted by an /// installed contributor. pub async fn approval_review( diff --git a/codex-rs/ext/extension-api/tests/registry.rs b/codex-rs/ext/extension-api/tests/registry.rs index 59b4f8c5e4..dac259bf60 100644 --- a/codex-rs/ext/extension-api/tests/registry.rs +++ b/codex-rs/ext/extension-api/tests/registry.rs @@ -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);