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
@@ -10,10 +10,9 @@ use tracing::Instrument;
|
||||
use tracing::debug_span;
|
||||
use tracing::info_span;
|
||||
|
||||
use crate::session::SteerInputError;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::session::SessionSettingsUpdate;
|
||||
use crate::session::thread_settings;
|
||||
use crate::session::turn_input;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::review_prompts::resolve_review_request;
|
||||
@@ -22,10 +21,7 @@ use crate::tasks::CompactTask;
|
||||
use crate::tasks::UserShellCommandMode;
|
||||
use crate::tasks::UserShellCommandTask;
|
||||
use crate::tasks::execute_user_shell_command;
|
||||
use crate::user_message_admission::UserMessageAdmission;
|
||||
use codex_history::RolloutItem;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -43,8 +39,6 @@ use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::ThreadSettingsAppliedEvent;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
@@ -82,231 +76,6 @@ pub async fn realtime_conversation_list_voices(sess: &Session, sub_id: String) {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn user_input_or_turn(
|
||||
sess: &Arc<Session>,
|
||||
sub_id: String,
|
||||
op: Op,
|
||||
client_user_message_id: Option<String>,
|
||||
parent_turn_id: Option<String>,
|
||||
root_turn_id: Option<String>,
|
||||
) {
|
||||
let admission = user_input_or_turn_inner(
|
||||
sess,
|
||||
sub_id.clone(),
|
||||
op,
|
||||
client_user_message_id,
|
||||
parent_turn_id,
|
||||
root_turn_id,
|
||||
)
|
||||
.await;
|
||||
sess.pending_user_message_admissions
|
||||
.complete(&sub_id, admission);
|
||||
}
|
||||
|
||||
pub async fn update_thread_settings(
|
||||
sess: &Arc<Session>,
|
||||
sub_id: String,
|
||||
thread_settings: ThreadSettingsOverrides,
|
||||
) {
|
||||
let updates = thread_settings_update(sess, thread_settings).await;
|
||||
match sess.update_settings(updates).await {
|
||||
Ok(()) => {
|
||||
sess.send_event_raw_without_materializing_rollout(Event {
|
||||
id: sub_id,
|
||||
msg: thread_settings_applied_event(sess).await,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: format!("invalid thread settings override: {err}"),
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn thread_settings_update(
|
||||
sess: &Session,
|
||||
thread_settings: ThreadSettingsOverrides,
|
||||
) -> SessionSettingsUpdate {
|
||||
let ThreadSettingsOverrides {
|
||||
environments,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
windows_sandbox_level,
|
||||
model,
|
||||
effort,
|
||||
summary,
|
||||
service_tier,
|
||||
collaboration_mode,
|
||||
personality,
|
||||
} = thread_settings;
|
||||
let collaboration_mode = match collaboration_mode {
|
||||
Some(collaboration_mode) => collaboration_mode,
|
||||
None => {
|
||||
let state = sess.state.lock().await;
|
||||
// Model and reasoning effort live in CollaborationMode settings today, so
|
||||
// partial thread-settings updates refresh those fields on the active mode.
|
||||
state
|
||||
.session_configuration
|
||||
.collaboration_mode
|
||||
.with_updates(model, effort, /*developer_instructions*/ None)
|
||||
}
|
||||
};
|
||||
SessionSettingsUpdate {
|
||||
environments,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
windows_sandbox_level,
|
||||
collaboration_mode: Some(collaboration_mode),
|
||||
reasoning_summary: summary,
|
||||
service_tier,
|
||||
personality,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
|
||||
let snapshot = {
|
||||
let state = sess.state.lock().await;
|
||||
state.session_configuration.thread_config_snapshot()
|
||||
};
|
||||
EventMsg::ThreadSettingsApplied(ThreadSettingsAppliedEvent {
|
||||
thread_settings: snapshot.into_thread_settings_snapshot(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn user_input_or_turn_inner(
|
||||
sess: &Arc<Session>,
|
||||
sub_id: String,
|
||||
op: Op,
|
||||
client_user_message_id: Option<String>,
|
||||
parent_turn_id: Option<String>,
|
||||
root_turn_id: Option<String>,
|
||||
) -> CodexResult<UserMessageAdmission> {
|
||||
let Op::UserInput {
|
||||
items,
|
||||
final_output_json_schema,
|
||||
responsesapi_client_metadata,
|
||||
additional_context,
|
||||
thread_settings,
|
||||
} = op
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
let emit_thread_settings_applied = thread_settings != ThreadSettingsOverrides::default();
|
||||
let mut updates = if emit_thread_settings_applied {
|
||||
thread_settings_update(sess, thread_settings).await
|
||||
} else {
|
||||
SessionSettingsUpdate::default()
|
||||
};
|
||||
updates.final_output_json_schema = Some(final_output_json_schema);
|
||||
|
||||
// new_turn_with_sub_id already emits an error event when settings are invalid.
|
||||
let current_context = sess.new_turn_with_sub_id(sub_id.clone(), updates).await?;
|
||||
if emit_thread_settings_applied {
|
||||
sess.send_event_raw_without_materializing_rollout(Event {
|
||||
id: sub_id.clone(),
|
||||
msg: thread_settings_applied_event(sess).await,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
sess.maybe_emit_model_warnings_for_turn(current_context.as_ref())
|
||||
.await;
|
||||
if let Some(id) = root_turn_id.as_ref() {
|
||||
current_context
|
||||
.turn_metadata_state
|
||||
.set_root_turn_id(id.clone());
|
||||
}
|
||||
let incoming_turn_metadata = parent_turn_id
|
||||
.as_ref()
|
||||
.map(|_| current_context.turn_metadata_state.as_ref());
|
||||
match sess
|
||||
.steer_input(
|
||||
items.clone(),
|
||||
additional_context.clone(),
|
||||
/*expected_turn_id*/ None,
|
||||
client_user_message_id.clone(),
|
||||
responsesapi_client_metadata.clone(),
|
||||
incoming_turn_metadata,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(turn_id) => {
|
||||
current_context.session_telemetry.user_prompt(&items);
|
||||
Ok(UserMessageAdmission::Steered { turn_id })
|
||||
}
|
||||
Err(SteerInputError::NoActiveTurn(items)) => {
|
||||
if root_turn_id.is_none()
|
||||
&& parent_turn_id.is_none()
|
||||
&& !items.is_empty()
|
||||
&& current_context
|
||||
.turn_metadata_state
|
||||
.can_start_root_turn(¤t_context.session_source)
|
||||
{
|
||||
current_context
|
||||
.turn_metadata_state
|
||||
.set_root_turn_id(sub_id.clone());
|
||||
}
|
||||
if let Some(id) = parent_turn_id {
|
||||
current_context.turn_metadata_state.set_parent_turn_id(id);
|
||||
}
|
||||
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
|
||||
current_context
|
||||
.turn_metadata_state
|
||||
.set_responsesapi_client_metadata(responsesapi_client_metadata);
|
||||
}
|
||||
current_context.session_telemetry.user_prompt(&items);
|
||||
let additional_context_input = {
|
||||
let mut state = sess.state.lock().await;
|
||||
state.additional_context.merge(additional_context)
|
||||
};
|
||||
let mut task_input = additional_context_input
|
||||
.into_iter()
|
||||
.map(ResponseItem::from)
|
||||
.map(|item| sess.annotate_client_response_item(item))
|
||||
.map(TurnInput::ResponseItem)
|
||||
.collect::<Vec<_>>();
|
||||
if !items.is_empty() {
|
||||
task_input.push(TurnInput::UserInput {
|
||||
content: items,
|
||||
client_id: client_user_message_id,
|
||||
});
|
||||
}
|
||||
sess.spawn_task(
|
||||
Arc::clone(¤t_context),
|
||||
task_input,
|
||||
crate::tasks::RegularTask::new(),
|
||||
)
|
||||
.await;
|
||||
Ok(UserMessageAdmission::Started { turn_id: sub_id })
|
||||
}
|
||||
Err(err) => {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id.clone(),
|
||||
msg: EventMsg::Error(err.to_error_event()),
|
||||
})
|
||||
.await;
|
||||
Err(CodexErr::InvalidRequest(format!(
|
||||
"failed to admit user message: {err:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues an inter-agent message, then lets the shared pending-work scheduler
|
||||
/// decide whether an idle session should start a regular turn.
|
||||
pub async fn inter_agent_communication(
|
||||
@@ -787,20 +556,17 @@ pub(super) async fn submission_loop(
|
||||
realtime_conversation_list_voices(&sess, sub.id.clone()).await;
|
||||
false
|
||||
}
|
||||
op @ Op::UserInput { .. } => {
|
||||
user_input_or_turn(
|
||||
&sess,
|
||||
sub.id.clone(),
|
||||
op,
|
||||
sub.client_user_message_id,
|
||||
sub.parent_turn_id,
|
||||
sub.root_turn_id,
|
||||
)
|
||||
.await;
|
||||
Op::TurnInput {
|
||||
request,
|
||||
mode,
|
||||
reply,
|
||||
} => {
|
||||
let result = turn_input::handle(&sess, *request, mode, sub.id.clone()).await;
|
||||
let _ = reply.send(result);
|
||||
false
|
||||
}
|
||||
Op::ThreadSettings { thread_settings } => {
|
||||
update_thread_settings(&sess, sub.id.clone(), thread_settings).await;
|
||||
thread_settings::update(&sess, sub.id.clone(), thread_settings).await;
|
||||
false
|
||||
}
|
||||
Op::InterAgentCommunication { communication } => {
|
||||
|
||||
Reference in new issue
Block a user