From 7809eacb8f36aea1dd1b6ca807eacb7732cb1b4a Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Sat, 6 Jun 2026 14:54:46 -0700 Subject: [PATCH] feat(extension-api): add global instructions contributor --- codex-rs/Cargo.lock | 1 + codex-rs/core-api/src/lib.rs | 5 +++ codex-rs/ext/extension-api/Cargo.toml | 1 + .../ext/extension-api/src/contributors.rs | 38 +++++++++++++++++++ codex-rs/ext/extension-api/src/lib.rs | 4 ++ codex-rs/ext/extension-api/src/registry.rs | 20 ++++++++++ codex-rs/ext/extension-api/tests/registry.rs | 33 ++++++++++++++++ 7 files changed, 102 insertions(+) create mode 100644 codex-rs/ext/extension-api/tests/registry.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ec5bfebf7b..7af7180607 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2922,6 +2922,7 @@ dependencies = [ "codex-context-fragments", "codex-protocol", "codex-tools", + "codex-utils-absolute-path", ] [[package]] diff --git a/codex-rs/core-api/src/lib.rs b/codex-rs/core-api/src/lib.rs index d1c7c7a18b..fa9169df13 100644 --- a/codex-rs/core-api/src/lib.rs +++ b/codex-rs/core-api/src/lib.rs @@ -47,6 +47,11 @@ pub use codex_core::skills::SkillsManager; pub use codex_core::thread_store_from_config; pub use codex_exec_server::EnvironmentManager; pub use codex_exec_server::ExecServerRuntimePaths; +pub use codex_extension_api::ExtensionRegistryBuilder; +pub use codex_extension_api::GlobalInstruction; +pub use codex_extension_api::GlobalInstructions; +pub use codex_extension_api::GlobalInstructionsContributor; +pub use codex_extension_api::GlobalInstructionsFuture; pub use codex_extension_api::empty_extension_registry; pub use codex_features::Feature; pub use codex_features::Features; diff --git a/codex-rs/ext/extension-api/Cargo.toml b/codex-rs/ext/extension-api/Cargo.toml index 85c7d8f98e..91099e08f1 100644 --- a/codex-rs/ext/extension-api/Cargo.toml +++ b/codex-rs/ext/extension-api/Cargo.toml @@ -18,3 +18,4 @@ async-trait = { workspace = true } codex-context-fragments = { workspace = true } codex-protocol = { workspace = true } codex-tools = { workspace = true } +codex-utils-absolute-path = { workspace = true } diff --git a/codex-rs/ext/extension-api/src/contributors.rs b/codex-rs/ext/extension-api/src/contributors.rs index 8706e8ee7a..6315a3ba46 100644 --- a/codex-rs/ext/extension-api/src/contributors.rs +++ b/codex-rs/ext/extension-api/src/contributors.rs @@ -1,4 +1,5 @@ use std::future::Future; +use std::pin::Pin; use std::sync::Arc; use codex_context_fragments::ContextualUserFragment; @@ -7,6 +8,7 @@ use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::TokenUsageInfo; use codex_tools::ToolCall; use codex_tools::ToolExecutor; +use codex_utils_absolute_path::AbsolutePathBuf; use crate::ExtensionData; @@ -34,6 +36,42 @@ pub use turn_lifecycle::TurnErrorInput; pub use turn_lifecycle::TurnStartInput; pub use turn_lifecycle::TurnStopInput; +/// One model-visible global instruction and its optional source path. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GlobalInstruction { + pub contents: String, + pub source: Option, +} + +impl GlobalInstruction { + pub fn new(contents: impl Into, source: Option) -> Self { + Self { + contents: contents.into(), + source, + } + } +} + +/// Global instructions resolved by a host-installed contributor. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct GlobalInstructions { + pub instructions: Vec, + pub warnings: Vec, +} + +/// Future returned while resolving global instructions. +pub type GlobalInstructionsFuture<'a> = + Pin> + Send + 'a>>; + +/// Resolves model-visible instructions that apply globally to a thread. +/// +/// Implementations should resolve their backing source only when this method +/// is called. Recoverable source issues may be returned in `warnings`; an +/// error indicates that the requested resolution could not be completed. +pub trait GlobalInstructionsContributor: Send + Sync { + fn contribute(&self) -> GlobalInstructionsFuture<'_>; +} + /// Extension contribution that adds prompt fragments during prompt assembly. pub trait ContextContributor: Send + Sync { fn contribute<'a>( diff --git a/codex-rs/ext/extension-api/src/lib.rs b/codex-rs/ext/extension-api/src/lib.rs index 7fa60c0fe7..f85c66903a 100644 --- a/codex-rs/ext/extension-api/src/lib.rs +++ b/codex-rs/ext/extension-api/src/lib.rs @@ -31,6 +31,10 @@ pub use codex_tools::parse_tool_input_schema_without_compaction; pub use contributors::ApprovalReviewContributor; pub use contributors::ConfigContributor; pub use contributors::ContextContributor; +pub use contributors::GlobalInstruction; +pub use contributors::GlobalInstructions; +pub use contributors::GlobalInstructionsContributor; +pub use contributors::GlobalInstructionsFuture; pub use contributors::PromptFragment; pub use contributors::PromptSlot; pub use contributors::ThreadIdleInput; diff --git a/codex-rs/ext/extension-api/src/registry.rs b/codex-rs/ext/extension-api/src/registry.rs index 0849387110..49e5b4507d 100644 --- a/codex-rs/ext/extension-api/src/registry.rs +++ b/codex-rs/ext/extension-api/src/registry.rs @@ -7,6 +7,7 @@ use crate::ConfigContributor; use crate::ContextContributor; use crate::ExtensionData; use crate::ExtensionEventSink; +use crate::GlobalInstructionsContributor; use crate::NoopExtensionEventSink; use crate::ThreadLifecycleContributor; use crate::TokenUsageContributor; @@ -19,6 +20,7 @@ use crate::TurnLifecycleContributor; /// Mutable registry used while hosts register typed runtime contributions. pub struct ExtensionRegistryBuilder { event_sink: Arc, + global_instructions_contributor: Option>, thread_lifecycle_contributors: Vec>>, turn_lifecycle_contributors: Vec>, config_contributors: Vec>>, @@ -35,6 +37,7 @@ impl Default for ExtensionRegistryBuilder { fn default() -> Self { Self { event_sink: Arc::new(NoopExtensionEventSink), + global_instructions_contributor: None, thread_lifecycle_contributors: Vec::new(), turn_lifecycle_contributors: Vec::new(), config_contributors: Vec::new(), @@ -68,6 +71,14 @@ impl ExtensionRegistryBuilder { Arc::clone(&self.event_sink) } + /// Registers the contributor that resolves global model instructions. + pub fn global_instructions_contributor( + &mut self, + contributor: Arc, + ) { + self.global_instructions_contributor = Some(contributor); + } + /// Registers one approval-review contributor. pub fn approval_review_contributor(&mut self, contributor: Arc) { self.approval_review_contributors.push(contributor); @@ -125,6 +136,7 @@ impl ExtensionRegistryBuilder { pub fn build(self) -> ExtensionRegistry { ExtensionRegistry { event_sink: self.event_sink, + global_instructions_contributor: self.global_instructions_contributor, thread_lifecycle_contributors: self.thread_lifecycle_contributors, turn_lifecycle_contributors: self.turn_lifecycle_contributors, config_contributors: self.config_contributors, @@ -142,6 +154,7 @@ impl ExtensionRegistryBuilder { /// Immutable typed registry produced after extensions are installed. pub struct ExtensionRegistry { event_sink: Arc, + global_instructions_contributor: Option>, thread_lifecycle_contributors: Vec>>, turn_lifecycle_contributors: Vec>, config_contributors: Vec>>, @@ -160,6 +173,13 @@ impl ExtensionRegistry { Arc::clone(&self.event_sink) } + /// Returns the contributor that resolves global model instructions. + pub fn global_instructions_contributor( + &self, + ) -> Option<&Arc> { + self.global_instructions_contributor.as_ref() + } + /// Returns the registered thread-lifecycle contributors. pub fn thread_lifecycle_contributors(&self) -> &[Arc>] { &self.thread_lifecycle_contributors diff --git a/codex-rs/ext/extension-api/tests/registry.rs b/codex-rs/ext/extension-api/tests/registry.rs new file mode 100644 index 0000000000..d104b421c1 --- /dev/null +++ b/codex-rs/ext/extension-api/tests/registry.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use codex_extension_api::ExtensionRegistryBuilder; +use codex_extension_api::GlobalInstructions; +use codex_extension_api::GlobalInstructionsContributor; +use codex_extension_api::GlobalInstructionsFuture; + +struct StaticGlobalInstructionsContributor; + +impl GlobalInstructionsContributor for StaticGlobalInstructionsContributor { + fn contribute(&self) -> GlobalInstructionsFuture<'_> { + Box::pin(std::future::ready(Ok(GlobalInstructions::default()))) + } +} + +#[test] +fn global_instructions_contributor_is_optional_and_singular() { + let empty = ExtensionRegistryBuilder::<()>::new().build(); + assert!(empty.global_instructions_contributor().is_none()); + + let contributor: Arc = + Arc::new(StaticGlobalInstructionsContributor); + let mut builder = ExtensionRegistryBuilder::<()>::new(); + builder.global_instructions_contributor(Arc::clone(&contributor)); + let registry = builder.build(); + + assert!(Arc::ptr_eq( + registry + .global_instructions_contributor() + .expect("registered contributor"), + &contributor + )); +}