diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index cc85f8b912..9e49d5a79f 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -45,14 +45,22 @@ struct SessionResponseItemInjector { impl codex_extension_api::ResponseItemInjector for SessionResponseItemInjector { fn inject_response_items<'a>( &'a self, - items: Vec, + items: Vec, ) -> codex_extension_api::ResponseItemInjectionFuture<'a> { let session = self.session.clone(); Box::pin(async move { let Some(session) = session.upgrade() else { return Err(items); }; - session.inject_response_items(items).await + let response_items = items + .clone() + .into_iter() + .map(codex_extension_api::ResponseInjectionItem::into_response_input_item) + .collect(); + session + .inject_response_items(response_items) + .await + .map_err(|_| items) }) } } diff --git a/codex-rs/core/src/tasks/idle_extension.rs b/codex-rs/core/src/tasks/idle_extension.rs index b6aeac059c..bb58b384da 100644 --- a/codex-rs/core/src/tasks/idle_extension.rs +++ b/codex-rs/core/src/tasks/idle_extension.rs @@ -2,6 +2,7 @@ use std::sync::Arc; +use codex_extension_api::ResponseInjectionItem; use codex_extension_api::ThreadIdleRequest; use codex_protocol::protocol::ThreadSettingsSnapshot; @@ -149,7 +150,7 @@ async fn next_idle_turn_candidate(session: &Session) -> Option Option bool { + match item { + ResponseInjectionItem::HiddenContext(context) => !context.body().trim().is_empty(), + ResponseInjectionItem::Raw(_) => true, + } +} + async fn should_start_idle_turn(session: &Session, candidate: &IdleTurnCandidate) -> bool { let Some(contributor) = session .services @@ -204,16 +212,25 @@ async fn clear_reserved_idle_turn(session: &Session) { } fn idle_extension_input(request: ThreadIdleRequest) -> TurnInput { - let prompt = codex_utils_string::truncate_middle_with_token_budget( - &request.prompt, - MAX_IDLE_EXTENSION_PROMPT_TOKENS, - ) - .0; - TurnInput::ResponseInputItem(request.context_marker.response_input_item(prompt)) + match request.item { + ResponseInjectionItem::HiddenContext(context) => { + let (marker, body) = context.into_parts(); + let prompt = codex_utils_string::truncate_middle_with_token_budget( + &body, + MAX_IDLE_EXTENSION_PROMPT_TOKENS, + ) + .0; + TurnInput::ResponseInputItem( + codex_extension_api::HiddenContext::new(marker, prompt).into_response_input_item(), + ) + } + ResponseInjectionItem::Raw(item) => TurnInput::ResponseInputItem(item), + } } #[cfg(test)] mod tests { + use codex_extension_api::HiddenContext; use codex_extension_api::HiddenContextMarker; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseInputItem; @@ -228,10 +245,10 @@ mod tests { ); let original_len = prompt.len(); - let request = ThreadIdleRequest::new( + let request = ThreadIdleRequest::new(HiddenContext::new( HiddenContextMarker::new("", ""), prompt, - ); + )); let TurnInput::ResponseInputItem(ResponseInputItem::Message { content, .. }) = idle_extension_input(request) else { diff --git a/codex-rs/ext/extension-api/src/capabilities/mod.rs b/codex-rs/ext/extension-api/src/capabilities/mod.rs index 0fabed3929..a7ee0c84aa 100644 --- a/codex-rs/ext/extension-api/src/capabilities/mod.rs +++ b/codex-rs/ext/extension-api/src/capabilities/mod.rs @@ -8,5 +8,6 @@ pub use events::ExtensionEventFuture; pub use events::ExtensionEventSink; pub use events::NoopExtensionEventSink; pub use response_items::NoopResponseItemInjector; +pub use response_items::ResponseInjectionItem; pub use response_items::ResponseItemInjectionFuture; pub use response_items::ResponseItemInjector; diff --git a/codex-rs/ext/extension-api/src/capabilities/response_items.rs b/codex-rs/ext/extension-api/src/capabilities/response_items.rs index c336d6564e..71a62ce6e7 100644 --- a/codex-rs/ext/extension-api/src/capabilities/response_items.rs +++ b/codex-rs/ext/extension-api/src/capabilities/response_items.rs @@ -1,21 +1,54 @@ +//! Host capability for injecting extension-owned model input into an active turn. + use std::future::Future; use std::pin::Pin; +use crate::HiddenContext; use codex_protocol::models::ResponseInputItem; -/// Future returned when an extension asks the host to inject model-visible input. +/// Extension-owned input to inject into an active model turn. +#[derive(Debug, Clone)] +pub enum ResponseInjectionItem { + /// Hidden context whose marker wrapping is owned by the extension API. + HiddenContext(HiddenContext), + /// Raw response item for extensions that intentionally need unwrapped input. + Raw(ResponseInputItem), +} + +impl ResponseInjectionItem { + pub fn into_response_input_item(self) -> ResponseInputItem { + match self { + Self::HiddenContext(context) => context.into_response_input_item(), + Self::Raw(item) => item, + } + } +} + +impl From for ResponseInjectionItem { + fn from(context: HiddenContext) -> Self { + Self::HiddenContext(context) + } +} + +impl From for ResponseInjectionItem { + fn from(item: ResponseInputItem) -> Self { + Self::Raw(item) + } +} + +/// Future returned when an extension asks the host to inject model input. pub type ResponseItemInjectionFuture<'a> = - Pin>> + Send + 'a>>; + Pin>> + Send + 'a>>; /// Thread-scoped host helper for extensions that need to steer the active model turn. /// -/// Implementations should inject the supplied response items into the active turn -/// for the current thread when it can accept same-turn model input. If injection -/// is unavailable, they return the unchanged items to the caller. +/// Implementations should inject the supplied items into the active turn for +/// the current thread when it can accept same-turn model input. If injection is +/// unavailable, they return the unchanged items to the caller. pub trait ResponseItemInjector: Send + Sync { fn inject_response_items<'a>( &'a self, - items: Vec, + items: Vec, ) -> ResponseItemInjectionFuture<'a>; } @@ -26,7 +59,7 @@ pub struct NoopResponseItemInjector; impl ResponseItemInjector for NoopResponseItemInjector { fn inject_response_items<'a>( &'a self, - items: Vec, + items: Vec, ) -> ResponseItemInjectionFuture<'a> { Box::pin(std::future::ready(Err(items))) } diff --git a/codex-rs/ext/extension-api/src/context.rs b/codex-rs/ext/extension-api/src/context.rs index 6996ecf4e7..88dd5004f2 100644 --- a/codex-rs/ext/extension-api/src/context.rs +++ b/codex-rs/ext/extension-api/src/context.rs @@ -18,17 +18,42 @@ impl HiddenContextMarker { end_marker, } } +} - /// Renders this marker pair as a user-role response input item. - pub fn response_input_item(&self, body: impl AsRef) -> ResponseInputItem { +/// Extension-owned hidden context body with the marker pair used to wrap it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HiddenContext { + marker: HiddenContextMarker, + body: String, +} + +impl HiddenContext { + /// Creates one hidden context body with its wrapping marker pair. + pub fn new(marker: HiddenContextMarker, body: impl Into) -> Self { + Self { + marker, + body: body.into(), + } + } + + /// Returns the unwrapped hidden context body. + pub fn body(&self) -> &str { + &self.body + } + + /// Splits this context into its marker pair and unwrapped body. + pub fn into_parts(self) -> (HiddenContextMarker, String) { + (self.marker, self.body) + } + + /// Renders this hidden context as a user-role response input item. + pub fn into_response_input_item(self) -> ResponseInputItem { ResponseInputItem::Message { role: "user".to_string(), content: vec![ContentItem::InputText { text: format!( "{}\n{}\n{}", - self.start_marker, - body.as_ref(), - self.end_marker + self.marker.start_marker, self.body, self.marker.end_marker ), }], phase: None, diff --git a/codex-rs/ext/extension-api/src/contributors.rs b/codex-rs/ext/extension-api/src/contributors.rs index 2eea9d6d4d..8cdc24e7ae 100644 --- a/codex-rs/ext/extension-api/src/contributors.rs +++ b/codex-rs/ext/extension-api/src/contributors.rs @@ -78,12 +78,13 @@ pub type ThreadIdleTurnStartFuture<'a> = std::pin::Pin