feat(extension-api): add global instructions contributor

This commit is contained in:
Adam Perry
2026-06-06 14:54:46 -07:00
parent d5be415078
commit 7809eacb8f
7 changed files with 102 additions and 0 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -2922,6 +2922,7 @@ dependencies = [
"codex-context-fragments",
"codex-protocol",
"codex-tools",
"codex-utils-absolute-path",
]
[[package]]

View File

@@ -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;

View File

@@ -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 }

View File

@@ -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<AbsolutePathBuf>,
}
impl GlobalInstruction {
pub fn new(contents: impl Into<String>, source: Option<AbsolutePathBuf>) -> 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<GlobalInstruction>,
pub warnings: Vec<String>,
}
/// Future returned while resolving global instructions.
pub type GlobalInstructionsFuture<'a> =
Pin<Box<dyn Future<Output = Result<GlobalInstructions, String>> + 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>(

View File

@@ -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;

View File

@@ -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<C: Sync> {
event_sink: Arc<dyn ExtensionEventSink>,
global_instructions_contributor: Option<Arc<dyn GlobalInstructionsContributor>>,
thread_lifecycle_contributors: Vec<Arc<dyn ThreadLifecycleContributor<C>>>,
turn_lifecycle_contributors: Vec<Arc<dyn TurnLifecycleContributor>>,
config_contributors: Vec<Arc<dyn ConfigContributor<C>>>,
@@ -35,6 +37,7 @@ impl<C: Sync> Default for ExtensionRegistryBuilder<C> {
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<C: Sync> ExtensionRegistryBuilder<C> {
Arc::clone(&self.event_sink)
}
/// Registers the contributor that resolves global model instructions.
pub fn global_instructions_contributor(
&mut self,
contributor: Arc<dyn GlobalInstructionsContributor>,
) {
self.global_instructions_contributor = Some(contributor);
}
/// Registers one approval-review contributor.
pub fn approval_review_contributor(&mut self, contributor: Arc<dyn ApprovalReviewContributor>) {
self.approval_review_contributors.push(contributor);
@@ -125,6 +136,7 @@ impl<C: Sync> ExtensionRegistryBuilder<C> {
pub fn build(self) -> ExtensionRegistry<C> {
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<C: Sync> ExtensionRegistryBuilder<C> {
/// Immutable typed registry produced after extensions are installed.
pub struct ExtensionRegistry<C: Sync> {
event_sink: Arc<dyn ExtensionEventSink>,
global_instructions_contributor: Option<Arc<dyn GlobalInstructionsContributor>>,
thread_lifecycle_contributors: Vec<Arc<dyn ThreadLifecycleContributor<C>>>,
turn_lifecycle_contributors: Vec<Arc<dyn TurnLifecycleContributor>>,
config_contributors: Vec<Arc<dyn ConfigContributor<C>>>,
@@ -160,6 +173,13 @@ impl<C: Sync> ExtensionRegistry<C> {
Arc::clone(&self.event_sink)
}
/// Returns the contributor that resolves global model instructions.
pub fn global_instructions_contributor(
&self,
) -> Option<&Arc<dyn GlobalInstructionsContributor>> {
self.global_instructions_contributor.as_ref()
}
/// Returns the registered thread-lifecycle contributors.
pub fn thread_lifecycle_contributors(&self) -> &[Arc<dyn ThreadLifecycleContributor<C>>] {
&self.thread_lifecycle_contributors

View File

@@ -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<dyn GlobalInstructionsContributor> =
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
));
}