mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Unify turn input submission and routing (#38275)
## 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
This commit is contained in:
1 parent
4b07886d59
commit
cbb7e82a8b
137 files changed
+5217
-6812
No files matched your search
@@ -1,18 +1,10 @@
|
||||
use super::input_queue::TurnInput;
|
||||
use super::TurnInput as PendingTurnInput;
|
||||
use super::session::Session;
|
||||
use super::turn_context::TurnContext;
|
||||
use crate::codex_thread::TryStartTurnIfIdleError;
|
||||
use crate::codex_thread::TryStartTurnIfIdleRejectionReason;
|
||||
use crate::state::ActiveTurn;
|
||||
use crate::state::TurnState;
|
||||
use crate::tasks::MailboxParentProvenance;
|
||||
use crate::tasks::RegularTask;
|
||||
use codex_features::Feature;
|
||||
use codex_history::CodexHarnessMetadata;
|
||||
use codex_history::ResponseItemEnvelope;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use std::sync::Arc;
|
||||
|
||||
impl Session {
|
||||
/// Returns the input if there is no active turn to inject into.
|
||||
@@ -33,7 +25,7 @@ impl Session {
|
||||
input
|
||||
.into_iter()
|
||||
.map(ResponseItemEnvelope::new)
|
||||
.map(TurnInput::ResponseItem)
|
||||
.map(PendingTurnInput::ResponseItem)
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
@@ -62,7 +54,10 @@ impl Session {
|
||||
self.input_queue
|
||||
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
active_turn.turn_state.as_ref(),
|
||||
items.into_iter().map(TurnInput::ResponseItem).collect(),
|
||||
items
|
||||
.into_iter()
|
||||
.map(PendingTurnInput::ResponseItem)
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
@@ -119,136 +114,6 @@ impl Session {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Starts a regular turn with the provided input only if automatic idle work
|
||||
/// is allowed for the current session state.
|
||||
///
|
||||
/// This is the shared gate for extension-initiated idle work. It refuses to
|
||||
/// start a turn when user/client-triggered work is queued or any task is
|
||||
/// still active. Work without user input is also rejected in Plan mode.
|
||||
/// Active Review tasks are covered by the active-task check because Review
|
||||
/// turns are not steerable.
|
||||
pub(crate) async fn try_start_turn_if_idle(
|
||||
self: &Arc<Self>,
|
||||
input: Vec<TurnInput>,
|
||||
) -> Result<(), TryStartTurnIfIdleError> {
|
||||
if input.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let has_user_input = input.iter().any(
|
||||
|item| matches!(item, TurnInput::UserInput { content, .. } if !content.is_empty()),
|
||||
);
|
||||
if self.input_queue.has_trigger_turn_mailbox_items().await {
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::PendingTriggerTurn,
|
||||
input,
|
||||
));
|
||||
}
|
||||
if !has_user_input && self.collaboration_mode().await.mode == ModeKind::Plan {
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::PlanMode,
|
||||
input,
|
||||
));
|
||||
}
|
||||
|
||||
let turn_state = {
|
||||
let mut active_turn = self.active_turn.lock().await;
|
||||
if active_turn.is_some() {
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::Busy,
|
||||
input,
|
||||
));
|
||||
}
|
||||
let active_turn = active_turn.get_or_insert_with(ActiveTurn::default);
|
||||
Arc::clone(&active_turn.turn_state)
|
||||
};
|
||||
|
||||
if self.input_queue.has_trigger_turn_mailbox_items().await {
|
||||
self.clear_reserved_idle_turn(&turn_state).await;
|
||||
self.maybe_start_turn_for_pending_work().await;
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::PendingTriggerTurn,
|
||||
input,
|
||||
));
|
||||
}
|
||||
|
||||
let turn_context = self
|
||||
.new_default_turn_with_sub_id(uuid::Uuid::new_v4().to_string())
|
||||
.await;
|
||||
if !has_user_input && turn_context.mode == ModeKind::Plan {
|
||||
self.clear_reserved_idle_turn(&turn_state).await;
|
||||
self.maybe_start_turn_for_pending_work().await;
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::PlanMode,
|
||||
input,
|
||||
));
|
||||
}
|
||||
self.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
|
||||
.await;
|
||||
if self.input_queue.has_trigger_turn_mailbox_items().await {
|
||||
self.clear_reserved_idle_turn(&turn_state).await;
|
||||
self.maybe_start_turn_for_pending_work().await;
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::PendingTriggerTurn,
|
||||
input,
|
||||
));
|
||||
}
|
||||
let still_reserved = {
|
||||
let active_turn = self.active_turn.lock().await;
|
||||
active_turn.as_ref().is_some_and(|active_turn| {
|
||||
active_turn.task.is_none() && Arc::ptr_eq(&active_turn.turn_state, &turn_state)
|
||||
})
|
||||
};
|
||||
if !still_reserved {
|
||||
self.clear_reserved_idle_turn(&turn_state).await;
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::Busy,
|
||||
input,
|
||||
));
|
||||
}
|
||||
|
||||
if has_user_input
|
||||
&& turn_context
|
||||
.turn_metadata_state
|
||||
.can_start_root_turn(&turn_context.session_source)
|
||||
{
|
||||
turn_context
|
||||
.turn_metadata_state
|
||||
.set_root_turn_id(turn_context.sub_id.clone());
|
||||
}
|
||||
let task_input = if has_user_input {
|
||||
self.clear_connector_selection().await;
|
||||
for item in &input {
|
||||
if let TurnInput::UserInput { content, .. } = item {
|
||||
turn_context.session_telemetry.user_prompt(content);
|
||||
}
|
||||
}
|
||||
input
|
||||
} else {
|
||||
self.input_queue
|
||||
.extend_pending_input_for_turn_state(turn_state.as_ref(), input)
|
||||
.await;
|
||||
Vec::new()
|
||||
};
|
||||
self.start_task(
|
||||
turn_context,
|
||||
task_input,
|
||||
RegularTask::new(),
|
||||
MailboxParentProvenance::Ignore,
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn clear_reserved_idle_turn(&self, turn_state: &Arc<tokio::sync::Mutex<TurnState>>) {
|
||||
let mut active_turn_guard = self.active_turn.lock().await;
|
||||
if let Some(active_turn) = active_turn_guard.as_ref()
|
||||
&& active_turn.task.is_none()
|
||||
&& Arc::ptr_eq(&active_turn.turn_state, turn_state)
|
||||
{
|
||||
*active_turn_guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects items into active work, or records them without starting a turn.
|
||||
pub(crate) async fn inject_no_new_turn(
|
||||
&self,
|
||||
|
||||
Reference in new issue
Block a user