mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
## What changed - Add `TurnInputRequest` and typed submission results for atomically starting a turn, steering the active turn, or declining input with a specific reason. - Expose `start_or_steer_turn`, `start_turn_if_idle`, and `steer_turn` on `CodexThread`, and migrate Core consumers to these APIs. - Make app-server `turn/start` steer an active regular turn and return that turn's ID. Reject incompatible output schemas and non-steerable turns without applying settings or enqueueing input. ## Testing - Cover concurrent start-or-steer submissions, accepted and rejected settings updates, output-schema compatibility, idle-start rejection, and app-server steering. GitOrigin-RevId: dd9b5528d76ec650c019e97af420bc13190ea86a
137 lines
4.8 KiB
Rust
137 lines
4.8 KiB
Rust
use super::TurnInput as PendingTurnInput;
|
|
use super::session::Session;
|
|
use super::turn_context::TurnContext;
|
|
use codex_features::Feature;
|
|
use codex_history::CodexHarnessMetadata;
|
|
use codex_history::ResponseItemEnvelope;
|
|
use codex_protocol::models::ResponseItem;
|
|
|
|
impl Session {
|
|
/// Returns the input if there is no active turn to inject into.
|
|
#[expect(
|
|
clippy::await_holding_invalid_type,
|
|
reason = "active turn checks and turn state updates must remain atomic"
|
|
)]
|
|
pub async fn inject_if_running(
|
|
&self,
|
|
input: Vec<ResponseItem>,
|
|
) -> Result<(), Vec<ResponseItem>> {
|
|
let mut active = self.active_turn.lock().await;
|
|
match active.as_mut() {
|
|
Some(active_turn) => {
|
|
self.input_queue
|
|
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
|
active_turn.turn_state.as_ref(),
|
|
input
|
|
.into_iter()
|
|
.map(ResponseItemEnvelope::new)
|
|
.map(PendingTurnInput::ResponseItem)
|
|
.collect(),
|
|
)
|
|
.await;
|
|
Ok(())
|
|
}
|
|
None => Err(input),
|
|
}
|
|
}
|
|
|
|
/// Preserves trusted client provenance while items wait for an active turn.
|
|
#[expect(
|
|
clippy::await_holding_invalid_type,
|
|
reason = "active turn checks and turn state updates must remain atomic"
|
|
)]
|
|
pub(crate) async fn inject_client_response_items(
|
|
&self,
|
|
items: Vec<ResponseItem>,
|
|
turn_context: &TurnContext,
|
|
) {
|
|
let items = items
|
|
.into_iter()
|
|
.map(|item| self.annotate_client_response_item(item))
|
|
.collect::<Vec<_>>();
|
|
let mut active = self.active_turn.lock().await;
|
|
if let Some(active_turn) = active.as_mut() {
|
|
self.input_queue
|
|
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
|
active_turn.turn_state.as_ref(),
|
|
items
|
|
.into_iter()
|
|
.map(PendingTurnInput::ResponseItem)
|
|
.collect(),
|
|
)
|
|
.await;
|
|
return;
|
|
}
|
|
drop(active);
|
|
self.record_annotated_conversation_items(turn_context, items)
|
|
.await;
|
|
}
|
|
|
|
pub(crate) fn annotate_client_response_item(&self, item: ResponseItem) -> ResponseItemEnvelope {
|
|
let metadata = (self.enabled(Feature::RetainClientDeveloperMessages)
|
|
&& matches!(&item, ResponseItem::Message { role, .. } if role == "developer"))
|
|
.then_some(CodexHarnessMetadata {
|
|
client_authored: true,
|
|
});
|
|
|
|
ResponseItemEnvelope { item, metadata }
|
|
}
|
|
|
|
pub(crate) async fn record_annotated_conversation_items(
|
|
&self,
|
|
turn_context: &TurnContext,
|
|
items: Vec<ResponseItemEnvelope>,
|
|
) {
|
|
if !self.enabled(Feature::RetainClientDeveloperMessages)
|
|
|| items.iter().all(|item| item.metadata.is_none())
|
|
{
|
|
let items = items
|
|
.into_iter()
|
|
.map(ResponseItemEnvelope::into_item)
|
|
.collect::<Vec<_>>();
|
|
self.record_conversation_items(turn_context, &items).await;
|
|
return;
|
|
}
|
|
|
|
let mut annotated_items = Vec::with_capacity(items.len());
|
|
let mut image_preparations = Vec::new();
|
|
for envelope in items {
|
|
let (prepared_items, prepared_images) = self.prepare_conversation_items_for_history(
|
|
turn_context,
|
|
std::slice::from_ref(&envelope.item),
|
|
);
|
|
image_preparations.extend(prepared_images);
|
|
|
|
let mut metadata = envelope.metadata;
|
|
annotated_items.extend(prepared_items.into_owned().into_iter().map(|item| {
|
|
ResponseItemEnvelope {
|
|
item,
|
|
metadata: metadata.take(),
|
|
}
|
|
}));
|
|
}
|
|
self.record_prepared_conversation_items(turn_context, annotated_items, image_preparations)
|
|
.await;
|
|
}
|
|
|
|
/// Injects items into active work, or records them without starting a turn.
|
|
pub(crate) async fn inject_no_new_turn(
|
|
&self,
|
|
items: Vec<ResponseItem>,
|
|
current_turn_context: Option<&TurnContext>,
|
|
) {
|
|
let Err(items) = self.inject_if_running(items).await else {
|
|
return;
|
|
};
|
|
let default_turn_context;
|
|
let turn_context = match current_turn_context {
|
|
Some(turn_context) => turn_context,
|
|
None => {
|
|
default_turn_context = self.new_default_turn().await;
|
|
default_turn_context.as_ref()
|
|
}
|
|
};
|
|
self.record_conversation_items(turn_context, &items).await;
|
|
}
|
|
}
|