mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +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:
@@ -36,4 +36,5 @@ pub mod request_permissions;
|
||||
pub mod request_user_input;
|
||||
pub mod review_format;
|
||||
pub mod shell_environment;
|
||||
pub mod turn_input;
|
||||
pub mod user_input;
|
||||
|
||||
@@ -31,6 +31,7 @@ use crate::dynamic_tools::DynamicToolCallOutputContentItem;
|
||||
use crate::dynamic_tools::DynamicToolCallRequest;
|
||||
use crate::dynamic_tools::DynamicToolResponse;
|
||||
use crate::dynamic_tools::DynamicToolSpec;
|
||||
use crate::error::Result as CodexResult;
|
||||
use crate::items::TurnItem;
|
||||
use crate::mcp::CallToolResult;
|
||||
use crate::mcp::RequestId;
|
||||
@@ -54,6 +55,10 @@ use crate::plan_tool::UpdatePlanArgs;
|
||||
use crate::request_permissions::RequestPermissionsEvent;
|
||||
use crate::request_permissions::RequestPermissionsResponse;
|
||||
use crate::request_user_input::RequestUserInputResponse;
|
||||
use crate::turn_input::TurnInputMode;
|
||||
use crate::turn_input::TurnInputRequest;
|
||||
use crate::turn_input::TurnInputSubmission;
|
||||
#[cfg(test)]
|
||||
use crate::user_input::UserInput;
|
||||
use codex_extension_items::image_generation::ImageGenerationFailure;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -67,6 +72,7 @@ use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
use serde_with::serde_as;
|
||||
use strum_macros::Display;
|
||||
use tokio::sync::oneshot;
|
||||
use tracing::error;
|
||||
use ts_rs::TS;
|
||||
|
||||
@@ -179,11 +185,11 @@ pub struct Submission {
|
||||
pub id: String,
|
||||
/// Payload
|
||||
pub op: Op,
|
||||
/// Client-provided id for the user message represented by `Op::UserInput`.
|
||||
pub client_user_message_id: Option<String>,
|
||||
/// Optional W3C trace carrier propagated across async submission handoffs.
|
||||
pub trace: Option<W3cTraceContext>,
|
||||
/// Core-provided ID of the parent turn that directly initiated this submission.
|
||||
///
|
||||
/// This is only used for inter-agent communication.
|
||||
pub parent_turn_id: Option<String>,
|
||||
/// Core-provided ID of the top-level turn that causally initiated this submission.
|
||||
pub root_turn_id: Option<String>,
|
||||
@@ -558,19 +564,11 @@ pub enum Op {
|
||||
/// Request the list of voices supported by realtime conversation streams.
|
||||
RealtimeConversationListVoices,
|
||||
|
||||
/// User input, optionally with thread-settings overrides applied first.
|
||||
UserInput {
|
||||
/// User input items, see `InputItem`
|
||||
items: Vec<UserInput>,
|
||||
/// Optional JSON Schema used to constrain the final assistant message for this turn.
|
||||
final_output_json_schema: Option<Value>,
|
||||
/// Optional turn-scoped Responses API `client_metadata`.
|
||||
responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
/// Client-supplied context fragments keyed by an opaque source identifier.
|
||||
additional_context: BTreeMap<String, AdditionalContextEntry>,
|
||||
|
||||
/// Persistent thread-settings overrides to apply before the input.
|
||||
thread_settings: ThreadSettingsOverrides,
|
||||
/// Submit turn input using the requested routing behavior.
|
||||
TurnInput {
|
||||
request: Box<TurnInputRequest>,
|
||||
mode: TurnInputMode,
|
||||
reply: oneshot::Sender<CodexResult<TurnInputSubmission>>,
|
||||
},
|
||||
|
||||
/// Apply persistent thread-settings overrides without starting a turn.
|
||||
@@ -727,18 +725,6 @@ impl FromStr for ThreadHistoryMode {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<UserInput>> for Op {
|
||||
fn from(value: Vec<UserInput>) -> Self {
|
||||
Op::UserInput {
|
||||
items: value,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: ThreadSettingsOverrides::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct InterAgentCommunication {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -877,7 +863,7 @@ impl Op {
|
||||
Self::RealtimeConversationSpeech(_) => "realtime_conversation_speech",
|
||||
Self::RealtimeConversationClose => "realtime_conversation_close",
|
||||
Self::RealtimeConversationListVoices => "realtime_conversation_list_voices",
|
||||
Self::UserInput { .. } => "user_input",
|
||||
Self::TurnInput { .. } => "turn_input",
|
||||
Self::ThreadSettings { .. } => "thread_settings",
|
||||
Self::InterAgentCommunication { .. } => "inter_agent_communication",
|
||||
Self::ExecApproval { .. } => "exec_approval",
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
//! Turn-input request and result types shared by Core's submission APIs.
|
||||
|
||||
use crate::models::ResponseItem;
|
||||
use crate::protocol::AdditionalContextEntry;
|
||||
use crate::protocol::InterAgentCommunication;
|
||||
use crate::protocol::NonSteerableTurnKind;
|
||||
use crate::protocol::ThreadSettingsOverrides;
|
||||
use crate::protocol::W3cTraceContext;
|
||||
use crate::user_input::UserInput;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Input consumed by a regular turn.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TurnInput {
|
||||
UserInput {
|
||||
content: Vec<UserInput>,
|
||||
client_id: Option<String>,
|
||||
},
|
||||
ResponseItem(ResponseItem),
|
||||
InterAgentCommunication(InterAgentCommunication),
|
||||
}
|
||||
|
||||
/// One turn input and the context that follows it through submission.
|
||||
///
|
||||
/// Callers choose start-or-steer, idle-start, or steer-only behavior through
|
||||
/// the corresponding `CodexThread` method.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TurnInputRequest {
|
||||
pub input: TurnInput,
|
||||
pub thread_settings: ThreadSettingsOverrides,
|
||||
pub start: TurnStartOptions,
|
||||
pub additional_context: BTreeMap<String, AdditionalContextEntry>,
|
||||
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
pub trace: Option<W3cTraceContext>,
|
||||
}
|
||||
|
||||
impl TurnInputRequest {
|
||||
/// Creates turn input that can be passed to one of the submission methods.
|
||||
pub fn new(input: TurnInput) -> Self {
|
||||
Self {
|
||||
input,
|
||||
thread_settings: ThreadSettingsOverrides::default(),
|
||||
start: TurnStartOptions::default(),
|
||||
additional_context: BTreeMap::new(),
|
||||
responsesapi_client_metadata: None,
|
||||
trace: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates ordinary user input without a client-provided message id.
|
||||
pub fn user_input(content: Vec<UserInput>) -> Self {
|
||||
Self::new(TurnInput::UserInput {
|
||||
content,
|
||||
client_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Persistent thread settings applied when Core accepts this input.
|
||||
///
|
||||
/// Settings are applied for both `Started` and `Steered`. A steered input
|
||||
/// cannot change its already-active turn context, so those settings apply
|
||||
/// to subsequent turns. `NotSubmitted` leaves them unapplied.
|
||||
pub fn with_thread_settings(mut self, thread_settings: ThreadSettingsOverrides) -> Self {
|
||||
self.thread_settings = thread_settings;
|
||||
self
|
||||
}
|
||||
|
||||
/// Options consulted only by start-capable submission methods when this
|
||||
/// request starts a turn.
|
||||
pub fn on_start(mut self, start: TurnStartOptions) -> Self {
|
||||
self.start = start;
|
||||
self
|
||||
}
|
||||
|
||||
/// Context merged whether this request starts or steers.
|
||||
pub fn with_additional_context(
|
||||
mut self,
|
||||
additional_context: BTreeMap<String, AdditionalContextEntry>,
|
||||
) -> Self {
|
||||
self.additional_context = additional_context;
|
||||
self
|
||||
}
|
||||
|
||||
/// Responses metadata attached whether this request starts or steers.
|
||||
pub fn with_responses_metadata(
|
||||
mut self,
|
||||
responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
) -> Self {
|
||||
self.responsesapi_client_metadata = responsesapi_client_metadata;
|
||||
self
|
||||
}
|
||||
|
||||
/// Trace context used when this request crosses the session loop.
|
||||
pub fn with_trace(mut self, trace: Option<W3cTraceContext>) -> Self {
|
||||
self.trace = trace;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// How Core should route submitted turn input.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum TurnInputMode {
|
||||
/// Start a regular turn when idle, otherwise steer the active regular turn.
|
||||
StartOrSteer,
|
||||
/// Start only when the thread is idle.
|
||||
StartIfIdle,
|
||||
/// Steer only if this exact turn is active.
|
||||
Steer { expected_turn_id: String },
|
||||
}
|
||||
|
||||
/// Options for the new-turn branch of a submission.
|
||||
///
|
||||
/// Core only records these options when input starts a turn. When input steers,
|
||||
/// `final_output_json_schema` is a compatibility requirement: Core only
|
||||
/// accepts the steer if the active turn already uses the same schema. For
|
||||
/// child input, Core also compares root lineage to detect ambiguity.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TurnStartOptions {
|
||||
/// Structured-output schema for a new turn. When steering, Core rejects
|
||||
/// the input if the active turn uses a different schema.
|
||||
pub final_output_json_schema: Option<Value>,
|
||||
/// Parent turn lineage recorded if this request starts a new turn.
|
||||
pub parent_turn_id: Option<String>,
|
||||
/// Causal root turn lineage recorded if this request starts a new turn.
|
||||
pub root_turn_id: Option<String>,
|
||||
}
|
||||
|
||||
/// What Core did with input submitted through `start_or_steer_turn`.
|
||||
///
|
||||
/// Started and Steered only mean Core accepted the input for turn processing. They
|
||||
/// do not wait for user-prompt hooks, updating the in-memory model context,
|
||||
/// rollout persistence, or sampling.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum TurnInputSubmission {
|
||||
/// Core started a turn. Persistent thread settings and start options were applied.
|
||||
Started { turn_id: String },
|
||||
/// Core steered an active turn. Persistent thread settings were applied for
|
||||
/// subsequent turns. No new turn was created, so lineage metadata was not
|
||||
/// recorded. If the request included `final_output_json_schema`, the active
|
||||
/// turn already used the same schema.
|
||||
Steered { turn_id: String },
|
||||
/// Core rejected the input without applying settings or start options.
|
||||
NotSubmitted { reason: NotSubmittedReason },
|
||||
}
|
||||
|
||||
/// What Core did with input submitted only for an idle turn start.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum StartIfIdleSubmission {
|
||||
/// Core started a turn. Persistent thread settings and start options were applied.
|
||||
Started { turn_id: String },
|
||||
/// Core rejected the input without applying settings or start options.
|
||||
NotSubmitted { reason: NotSubmittedReason },
|
||||
}
|
||||
|
||||
/// What Core did with input submitted only for steering.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SteerSubmission {
|
||||
/// Core steered an active turn. Persistent thread settings were applied for
|
||||
/// subsequent turns.
|
||||
Steered { turn_id: String },
|
||||
/// Core rejected the input without applying settings.
|
||||
NotSubmitted { reason: NotSubmittedReason },
|
||||
}
|
||||
|
||||
/// Why Core did not accept submitted turn input for turn processing.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum NotSubmittedReason {
|
||||
/// `start_turn_if_idle` found an active turn.
|
||||
NotIdle,
|
||||
|
||||
/// `start_turn_if_idle` yielded to higher-priority trigger-turn mailbox input.
|
||||
PendingTriggerTurn,
|
||||
|
||||
/// `start_turn_if_idle` received automatic non-user input for a turn that
|
||||
/// would run in Plan mode.
|
||||
PlanMode,
|
||||
|
||||
/// `steer_turn` found no active turn.
|
||||
NoActiveTurn,
|
||||
|
||||
/// `steer_turn` found a different active turn.
|
||||
ExpectedTurnMismatch { expected: String, actual: String },
|
||||
|
||||
/// `start_or_steer_turn` or `steer_turn` found an active turn that does not accept steering.
|
||||
ActiveTurnNotSteerable { turn_kind: NonSteerableTurnKind },
|
||||
|
||||
/// `start_or_steer_turn` or `steer_turn` required a final output schema
|
||||
/// that differs from the active turn's schema.
|
||||
ActiveTurnOutputSchemaMismatch,
|
||||
|
||||
/// `start_or_steer_turn` or `steer_turn` reached a steering path with empty user input.
|
||||
EmptyInput,
|
||||
}
|
||||
Reference in New Issue
Block a user