From 6a8173588c15a519f3bb4e63f0cf5ab392f18053 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 18 May 2026 14:08:56 +0200 Subject: [PATCH] feat: add extension event sink capability (#23293) ## Why Extensions can already expose typed contributions and receive host capabilities such as `AgentSpawner`, but they do not have a typed way to send protocol events back through the host. Extensions that need to surface progress or status should not have to own persistence, ordering, transport fanout, or logging decisions themselves. ## What - Add `ExtensionEventSink`, a host-provided fire-and-forget sink for `codex_protocol::protocol::Event`. - Add `NoopExtensionEventSink` so hosts that do not expose extension event emission keep the existing empty-registry behavior. - Store the sink on `ExtensionRegistryBuilder` / `ExtensionRegistry`, with `with_event_sink(...)` and `event_sink()` accessors, and re-export the new capability from `codex-extension-api`. ## Testing - Not run locally; PR metadata/body update only. --- .../extension-api/src/capabilities/events.rs | 19 +++++++++++++++ .../ext/extension-api/src/capabilities/mod.rs | 3 +++ codex-rs/ext/extension-api/src/lib.rs | 2 ++ codex-rs/ext/extension-api/src/registry.rs | 24 +++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 codex-rs/ext/extension-api/src/capabilities/events.rs diff --git a/codex-rs/ext/extension-api/src/capabilities/events.rs b/codex-rs/ext/extension-api/src/capabilities/events.rs new file mode 100644 index 000000000..d8fda1aea --- /dev/null +++ b/codex-rs/ext/extension-api/src/capabilities/events.rs @@ -0,0 +1,19 @@ +use codex_protocol::protocol::Event; + +/// Host-provided fire-and-forget sink for extension-generated events. +/// +/// Extensions construct protocol events with the correlation id appropriate for +/// the callback they are handling, then leave persistence, ordering, transport +/// fanout, and logging decisions to the host. +pub trait ExtensionEventSink: Send + Sync { + /// Queue one protocol event for host-owned delivery. + fn emit(&self, event: Event); +} + +/// Event sink used when the host does not expose extension event emission. +#[derive(Debug, Default, Clone, Copy)] +pub struct NoopExtensionEventSink; + +impl ExtensionEventSink for NoopExtensionEventSink { + fn emit(&self, _event: Event) {} +} diff --git a/codex-rs/ext/extension-api/src/capabilities/mod.rs b/codex-rs/ext/extension-api/src/capabilities/mod.rs index bcc33da97..cd2314654 100644 --- a/codex-rs/ext/extension-api/src/capabilities/mod.rs +++ b/codex-rs/ext/extension-api/src/capabilities/mod.rs @@ -1,4 +1,7 @@ mod agent; +mod events; pub use agent::AgentSpawnFuture; pub use agent::AgentSpawner; +pub use events::ExtensionEventSink; +pub use events::NoopExtensionEventSink; diff --git a/codex-rs/ext/extension-api/src/lib.rs b/codex-rs/ext/extension-api/src/lib.rs index e8be7e030..fe33d4212 100644 --- a/codex-rs/ext/extension-api/src/lib.rs +++ b/codex-rs/ext/extension-api/src/lib.rs @@ -5,6 +5,8 @@ mod state; pub use capabilities::AgentSpawnFuture; pub use capabilities::AgentSpawner; +pub use capabilities::ExtensionEventSink; +pub use capabilities::NoopExtensionEventSink; pub use codex_tools::FunctionCallError; pub use codex_tools::JsonToolOutput; pub use codex_tools::ResponsesApiTool; diff --git a/codex-rs/ext/extension-api/src/registry.rs b/codex-rs/ext/extension-api/src/registry.rs index f00e99631..41d096712 100644 --- a/codex-rs/ext/extension-api/src/registry.rs +++ b/codex-rs/ext/extension-api/src/registry.rs @@ -5,6 +5,8 @@ use crate::ApprovalReviewFuture; use crate::ConfigContributor; use crate::ContextContributor; use crate::ExtensionData; +use crate::ExtensionEventSink; +use crate::NoopExtensionEventSink; use crate::ThreadLifecycleContributor; use crate::TokenUsageContributor; use crate::ToolContributor; @@ -13,6 +15,7 @@ use crate::TurnLifecycleContributor; /// Mutable registry used while hosts register typed runtime contributions. pub struct ExtensionRegistryBuilder { + event_sink: Arc, thread_lifecycle_contributors: Vec>>, turn_lifecycle_contributors: Vec>, config_contributors: Vec>>, @@ -26,6 +29,7 @@ pub struct ExtensionRegistryBuilder { impl Default for ExtensionRegistryBuilder { fn default() -> Self { Self { + event_sink: Arc::new(NoopExtensionEventSink), thread_lifecycle_contributors: Vec::new(), turn_lifecycle_contributors: Vec::new(), config_contributors: Vec::new(), @@ -44,6 +48,19 @@ impl ExtensionRegistryBuilder { Self::default() } + /// Creates an empty registry builder with a host-provided event sink. + pub fn with_event_sink(event_sink: Arc) -> Self { + Self { + event_sink, + ..Self::default() + } + } + + /// Returns the host event sink to pass into extension constructors. + pub fn event_sink(&self) -> Arc { + Arc::clone(&self.event_sink) + } + /// Registers one approval-review contributor. pub fn approval_review_contributor(&mut self, contributor: Arc) { self.approval_review_contributors.push(contributor); @@ -90,6 +107,7 @@ impl ExtensionRegistryBuilder { /// Finishes construction and returns the immutable registry. pub fn build(self) -> ExtensionRegistry { ExtensionRegistry { + event_sink: self.event_sink, thread_lifecycle_contributors: self.thread_lifecycle_contributors, turn_lifecycle_contributors: self.turn_lifecycle_contributors, config_contributors: self.config_contributors, @@ -104,6 +122,7 @@ impl ExtensionRegistryBuilder { /// Immutable typed registry produced after extensions are installed. pub struct ExtensionRegistry { + event_sink: Arc, thread_lifecycle_contributors: Vec>>, turn_lifecycle_contributors: Vec>, config_contributors: Vec>>, @@ -115,6 +134,11 @@ pub struct ExtensionRegistry { } impl ExtensionRegistry { + /// Returns the host event sink retained by this registry. + pub fn event_sink(&self) -> Arc { + Arc::clone(&self.event_sink) + } + /// Returns the registered thread-lifecycle contributors. pub fn thread_lifecycle_contributors(&self) -> &[Arc>] { &self.thread_lifecycle_contributors