mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Unify extension response item injection
This commit is contained in:
@@ -45,14 +45,22 @@ struct SessionResponseItemInjector {
|
||||
impl codex_extension_api::ResponseItemInjector for SessionResponseItemInjector {
|
||||
fn inject_response_items<'a>(
|
||||
&'a self,
|
||||
items: Vec<ResponseInputItem>,
|
||||
items: Vec<codex_extension_api::ResponseInjectionItem>,
|
||||
) -> 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IdleTurnCandidate
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if !request.prompt.trim().is_empty() {
|
||||
if is_non_empty_idle_input(&request.item) {
|
||||
return Some(IdleTurnCandidate {
|
||||
contributor_index,
|
||||
request,
|
||||
@@ -160,6 +161,13 @@ async fn next_idle_turn_candidate(session: &Session) -> Option<IdleTurnCandidate
|
||||
None
|
||||
}
|
||||
|
||||
fn is_non_empty_idle_input(item: &ResponseInjectionItem) -> 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("<goal_context>", "</goal_context>"),
|
||||
prompt,
|
||||
);
|
||||
));
|
||||
let TurnInput::ResponseInputItem(ResponseInputItem::Message { content, .. }) =
|
||||
idle_extension_input(request)
|
||||
else {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<HiddenContext> for ResponseInjectionItem {
|
||||
fn from(context: HiddenContext) -> Self {
|
||||
Self::HiddenContext(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResponseInputItem> 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<Box<dyn Future<Output = Result<(), Vec<ResponseInputItem>>> + Send + 'a>>;
|
||||
Pin<Box<dyn Future<Output = Result<(), Vec<ResponseInjectionItem>>> + 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<ResponseInputItem>,
|
||||
items: Vec<ResponseInjectionItem>,
|
||||
) -> ResponseItemInjectionFuture<'a>;
|
||||
}
|
||||
|
||||
@@ -26,7 +59,7 @@ pub struct NoopResponseItemInjector;
|
||||
impl ResponseItemInjector for NoopResponseItemInjector {
|
||||
fn inject_response_items<'a>(
|
||||
&'a self,
|
||||
items: Vec<ResponseInputItem>,
|
||||
items: Vec<ResponseInjectionItem>,
|
||||
) -> ResponseItemInjectionFuture<'a> {
|
||||
Box::pin(std::future::ready(Err(items)))
|
||||
}
|
||||
|
||||
@@ -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<str>) -> 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<String>) -> 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,
|
||||
|
||||
@@ -78,12 +78,13 @@ pub type ThreadIdleTurnStartFuture<'a> = std::pin::Pin<Box<dyn Future<Output = b
|
||||
|
||||
/// Contributor that can request a host-owned turn while a thread is idle.
|
||||
///
|
||||
/// Implementations should return a concise hidden prompt body. The host owns
|
||||
/// the response-item shape and may ask the contributor to confirm the request
|
||||
/// again immediately before the turn starts.
|
||||
/// Implementations should normally return hidden context. Raw items are
|
||||
/// available for extensions that intentionally need unwrapped input. The host
|
||||
/// owns hidden-context wrapping and may ask the contributor to confirm the
|
||||
/// request again immediately before the turn starts.
|
||||
pub trait ThreadIdleTurnContributor: Send + Sync {
|
||||
/// Returns one hidden prompt body to start an idle turn, if the extension
|
||||
/// still has work that should run without user input.
|
||||
/// Returns input to start an idle turn, if the extension still has work
|
||||
/// that should run without user input.
|
||||
fn request_thread_idle_turn<'a>(
|
||||
&'a self,
|
||||
_input: ThreadIdleInput<'a>,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::ExtensionData;
|
||||
use crate::HiddenContextMarker;
|
||||
use crate::ResponseInjectionItem;
|
||||
use crate::ResponseItemInjector;
|
||||
use codex_protocol::protocol::ThreadSettingsSnapshot;
|
||||
|
||||
@@ -40,19 +40,16 @@ pub struct ThreadIdleInput<'a> {
|
||||
|
||||
/// Extension request to start a new idle turn.
|
||||
pub struct ThreadIdleRequest {
|
||||
/// Hidden prompt body that the host wraps as extension-owned context.
|
||||
pub prompt: String,
|
||||
/// Marker pair used to wrap the prompt as hidden extension-owned context.
|
||||
pub context_marker: HiddenContextMarker,
|
||||
/// Extension-owned input that the host injects as model-visible input.
|
||||
pub item: ResponseInjectionItem,
|
||||
/// Opaque extension-owned key used to reject stale requests before start.
|
||||
pub validation_key: Option<String>,
|
||||
}
|
||||
|
||||
impl ThreadIdleRequest {
|
||||
pub fn new(context_marker: HiddenContextMarker, prompt: impl Into<String>) -> Self {
|
||||
pub fn new(item: impl Into<ResponseInjectionItem>) -> Self {
|
||||
Self {
|
||||
prompt: prompt.into(),
|
||||
context_marker,
|
||||
item: item.into(),
|
||||
validation_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ pub use capabilities::ExtensionEventFuture;
|
||||
pub use capabilities::ExtensionEventSink;
|
||||
pub use capabilities::NoopExtensionEventSink;
|
||||
pub use capabilities::NoopResponseItemInjector;
|
||||
pub use capabilities::ResponseInjectionItem;
|
||||
pub use capabilities::ResponseItemInjectionFuture;
|
||||
pub use capabilities::ResponseItemInjector;
|
||||
pub use codex_tools::ConversationHistory;
|
||||
@@ -24,6 +25,7 @@ pub use codex_tools::ToolPayload;
|
||||
pub use codex_tools::ToolSpec;
|
||||
pub use codex_tools::parse_tool_input_schema;
|
||||
pub use codex_tools::parse_tool_input_schema_without_compaction;
|
||||
pub use context::HiddenContext;
|
||||
pub use context::HiddenContextMarker;
|
||||
pub use contributors::ApprovalReviewContributor;
|
||||
pub use contributors::ConfigContributor;
|
||||
|
||||
@@ -39,7 +39,7 @@ use crate::events::GoalEventEmitter;
|
||||
use crate::metrics::GoalMetrics;
|
||||
use crate::runtime::GoalRuntimeHandle;
|
||||
use crate::spec::UPDATE_GOAL_TOOL_NAME;
|
||||
use crate::steering::budget_limit_steering_item;
|
||||
use crate::steering::budget_limit_steering_context;
|
||||
use crate::tool::GoalToolExecutor;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -372,8 +372,8 @@ where
|
||||
{
|
||||
return;
|
||||
}
|
||||
let item = budget_limit_steering_item(&goal);
|
||||
runtime.inject_active_turn_steering(item).await;
|
||||
let context = budget_limit_steering_context(&goal);
|
||||
runtime.inject_active_turn_steering(context).await;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use codex_extension_api::HiddenContext;
|
||||
use codex_extension_api::ResponseItemInjector;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::protocol::ThreadGoal;
|
||||
use codex_protocol::protocol::ThreadSettingsSnapshot;
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::accounting::GoalAccountingState;
|
||||
use crate::events::GoalEventEmitter;
|
||||
use crate::metrics::GoalMetrics;
|
||||
use crate::steering::continuation_steering_request;
|
||||
use crate::steering::objective_updated_steering_item;
|
||||
use crate::steering::objective_updated_steering_context;
|
||||
use crate::tool::protocol_goal_from_state;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -147,8 +147,9 @@ impl GoalRuntimeHandle {
|
||||
.mark_idle_goal_active(goal.goal_id.clone());
|
||||
}
|
||||
if should_steer_active_turn {
|
||||
let item = objective_updated_steering_item(&protocol_goal_from_state(goal));
|
||||
self.inject_active_turn_steering(item).await;
|
||||
let context =
|
||||
objective_updated_steering_context(&protocol_goal_from_state(goal));
|
||||
self.inject_active_turn_steering(context).await;
|
||||
}
|
||||
}
|
||||
codex_state::ThreadGoalStatus::BudgetLimited => {
|
||||
@@ -323,11 +324,11 @@ impl GoalRuntimeHandle {
|
||||
Ok(goal.goal_id == expected_goal_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn inject_active_turn_steering(&self, item: ResponseInputItem) {
|
||||
pub(crate) async fn inject_active_turn_steering(&self, context: HiddenContext) {
|
||||
if self
|
||||
.inner
|
||||
.response_item_injector
|
||||
.inject_response_items(vec![item])
|
||||
.inject_response_items(vec![context.into()])
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use codex_extension_api::HiddenContext;
|
||||
use codex_extension_api::HiddenContextMarker;
|
||||
use codex_extension_api::ThreadIdleRequest;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::protocol::ThreadGoal;
|
||||
use codex_utils_template::Template;
|
||||
use std::sync::LazyLock;
|
||||
@@ -47,20 +47,20 @@ fn render_embedded_template<const N: usize>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn budget_limit_steering_item(goal: &ThreadGoal) -> ResponseInputItem {
|
||||
goal_context_input_item(budget_limit_prompt(goal))
|
||||
pub(crate) fn budget_limit_steering_context(goal: &ThreadGoal) -> HiddenContext {
|
||||
goal_context(budget_limit_prompt(goal))
|
||||
}
|
||||
|
||||
pub(crate) fn continuation_steering_request(goal: &ThreadGoal) -> ThreadIdleRequest {
|
||||
ThreadIdleRequest::new(GOAL_CONTEXT_MARKER, continuation_prompt(goal))
|
||||
ThreadIdleRequest::new(goal_context(continuation_prompt(goal)))
|
||||
}
|
||||
|
||||
pub(crate) fn objective_updated_steering_item(goal: &ThreadGoal) -> ResponseInputItem {
|
||||
goal_context_input_item(objective_updated_prompt(goal))
|
||||
pub(crate) fn objective_updated_steering_context(goal: &ThreadGoal) -> HiddenContext {
|
||||
goal_context(objective_updated_prompt(goal))
|
||||
}
|
||||
|
||||
fn goal_context_input_item(prompt: String) -> ResponseInputItem {
|
||||
GOAL_CONTEXT_MARKER.response_input_item(prompt)
|
||||
fn goal_context(prompt: String) -> HiddenContext {
|
||||
HiddenContext::new(GOAL_CONTEXT_MARKER, prompt)
|
||||
}
|
||||
|
||||
fn continuation_prompt(goal: &ThreadGoal) -> String {
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_extension_api::ExtensionEventSink;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::FunctionCallError;
|
||||
use codex_extension_api::NoopResponseItemInjector;
|
||||
use codex_extension_api::ResponseInjectionItem;
|
||||
use codex_extension_api::ThreadIdleInput;
|
||||
use codex_extension_api::ThreadIdleRequest;
|
||||
use codex_extension_api::ThreadIdleTurnStartInput;
|
||||
@@ -507,12 +508,15 @@ async fn thread_idle_hook_requests_hidden_goal_continuation() -> anyhow::Result<
|
||||
.ok_or_else(|| anyhow::anyhow!("active goal should request idle continuation"))?;
|
||||
|
||||
assert!(request.validation_key.is_some());
|
||||
let ResponseInjectionItem::HiddenContext(context) = &request.item else {
|
||||
panic!("expected hidden context item");
|
||||
};
|
||||
assert!(
|
||||
request
|
||||
.prompt
|
||||
context
|
||||
.body()
|
||||
.contains("Continue working toward the active thread goal.")
|
||||
);
|
||||
assert!(request.prompt.contains("Completion audit:"));
|
||||
assert!(context.body().contains("Completion audit:"));
|
||||
assert!(
|
||||
harness
|
||||
.should_start_idle_turn(ModeKind::Default, &request)
|
||||
|
||||
Reference in New Issue
Block a user