From eceb3ddbe67ad53c54f759852b4f0d39296d437c Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 8 Jul 2026 18:49:27 -0700 Subject: [PATCH] core: route downstream reasoning effort through steps --- codex-rs/core/src/agent/control_tests.rs | 11 ++- codex-rs/core/src/codex_delegate.rs | 10 +- codex-rs/core/src/compact.rs | 8 +- codex-rs/core/src/compact_remote.rs | 17 ++-- codex-rs/core/src/compact_remote_v2.rs | 13 +-- codex-rs/core/src/compact_tests.rs | 6 +- codex-rs/core/src/compact_token_budget.rs | 10 +- codex-rs/core/src/context_manager/updates.rs | 26 +++--- codex-rs/core/src/guardian/review.rs | 57 ++++++------ codex-rs/core/src/guardian/review_session.rs | 10 +- codex-rs/core/src/guardian/tests.rs | 17 ++-- codex-rs/core/src/mcp_tool_call.rs | 3 +- codex-rs/core/src/mcp_tool_call_tests.rs | 22 +++-- .../src/session/elicitation_holders_tests.rs | 3 +- codex-rs/core/src/session/mcp.rs | 3 +- codex-rs/core/src/session/mod.rs | 39 ++++---- codex-rs/core/src/session/multi_agents.rs | 6 +- .../session/rollout_reconstruction_tests.rs | 42 ++++++--- codex-rs/core/src/session/step_context.rs | 64 +++++++++++++ codex-rs/core/src/session/tests.rs | 93 +++++++++++++------ .../core/src/session/tests/guardian_tests.rs | 8 +- codex-rs/core/src/session/turn.rs | 9 +- codex-rs/core/src/session/turn_context.rs | 43 ++------- codex-rs/core/src/tasks/mod.rs | 2 +- codex-rs/core/src/thread_manager_tests.rs | 4 +- .../src/thread_rollout_truncation_tests.rs | 4 +- .../core/src/tools/handlers/agent_jobs.rs | 6 +- .../agent_jobs/spawn_agents_on_csv.rs | 28 +++--- .../core/src/tools/handlers/apply_patch.rs | 4 + .../handlers/multi_agents/resume_agent.rs | 10 +- .../tools/handlers/multi_agents/send_input.rs | 3 +- .../src/tools/handlers/multi_agents/spawn.rs | 7 +- .../src/tools/handlers/multi_agents_common.rs | 21 +++-- .../src/tools/handlers/multi_agents_tests.rs | 22 ++++- .../handlers/multi_agents_v2/message_tool.rs | 3 +- .../tools/handlers/multi_agents_v2/spawn.rs | 7 +- .../src/tools/handlers/request_permissions.rs | 4 +- codex-rs/core/src/tools/handlers/shell.rs | 5 + .../src/tools/handlers/shell/shell_command.rs | 1 + .../handlers/unified_exec/exec_command.rs | 8 +- codex-rs/core/src/tools/network_approval.rs | 20 +++- .../core/src/tools/network_approval_tests.rs | 2 + codex-rs/core/src/tools/orchestrator.rs | 9 +- .../tools/runtimes/shell/unix_escalation.rs | 11 ++- .../runtimes/shell/unix_escalation_tests.rs | 4 + codex-rs/core/src/tools/sandboxing.rs | 3 + codex-rs/core/src/unified_exec/mod.rs | 10 +- codex-rs/core/src/unified_exec/mod_tests.rs | 8 +- .../core/src/unified_exec/process_manager.rs | 1 + .../src/unified_exec/process_manager_tests.rs | 1 + 50 files changed, 474 insertions(+), 254 deletions(-) diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 4741c3c101..dfb004497a 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -11,6 +11,7 @@ use crate::config::ConfigBuilder; use crate::context::ContextualUserFragment; use crate::context::SubagentNotification; use crate::init_state_db; +use crate::session::tests::turn_context_item_for_test; use crate::thread_manager::StartThreadOptions; use assert_matches::assert_matches; use codex_extension_api::ExtensionDataInit; @@ -983,7 +984,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { ], ) .await; - let parent_reference_context_item = turn_context.to_turn_context_item(); + let parent_reference_context_item = turn_context_item_for_test(&turn_context); parent_thread .codex .session @@ -1187,7 +1188,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() { previous_window_id: None, window_id: None, }), - RolloutItem::TurnContext(turn_context.to_turn_context_item()), + RolloutItem::TurnContext(turn_context_item_for_test(&turn_context)), RolloutItem::ResponseItem(spawn_agent_call(&parent_spawn_call_id)), ]) .await; @@ -1374,9 +1375,9 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() { parent_thread .codex .session - .persist_rollout_items(&[RolloutItem::TurnContext( - spawn_turn_context.to_turn_context_item(), - )]) + .persist_rollout_items(&[RolloutItem::TurnContext(turn_context_item_for_test( + &spawn_turn_context, + ))]) .await; parent_thread .codex diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index d2911df407..a39817a547 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -51,6 +51,7 @@ use crate::session::CodexSpawnOk; use crate::session::SUBMISSION_CHANNEL_CAPACITY; use crate::session::emit_subagent_session_started; use crate::session::session::Session; +use crate::session::step_context::StepContextSeed; use crate::session::turn_context::TurnContext; use codex_login::AuthManager; use codex_models_manager::manager::SharedModelsManager; @@ -514,7 +515,7 @@ async fn handle_exec_approval( let review_cancel = cancel_token.child_token(); let review_rx = spawn_approval_request_review( Arc::clone(parent_session), - Arc::clone(parent_ctx), + StepContextSeed::from_turn(Arc::clone(parent_ctx)), new_guardian_review_id(), GuardianApprovalRequest::Shell { id: call_id.clone(), @@ -627,7 +628,7 @@ async fn handle_patch_approval( .join("\n"); let review_rx = spawn_approval_request_review( Arc::clone(parent_session), - Arc::clone(parent_ctx), + StepContextSeed::from_turn(Arc::clone(parent_ctx)), new_guardian_review_id(), GuardianApprovalRequest::ApplyPatch { id: approval_id.clone(), @@ -749,7 +750,7 @@ async fn maybe_auto_review_mcp_request_user_input( let review_cancel = cancel_token.child_token(); let review_rx = spawn_approval_request_review( Arc::clone(parent_session), - Arc::clone(parent_ctx), + StepContextSeed::from_turn(Arc::clone(parent_ctx)), new_guardian_review_id(), build_guardian_mcp_tool_review_request(&event.call_id, &invocation, metadata.as_ref()), /*retry_reason*/ None, @@ -809,8 +810,9 @@ async fn handle_request_permissions( #[allow(deprecated)] parent_ctx.cwd.clone() }); + let step_context_seed = StepContextSeed::from_turn(Arc::clone(parent_ctx)); let response_fut = parent_session.request_permissions_for_cwd( - parent_ctx, + &step_context_seed, call_id.clone(), args, cwd, diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index e335792ac3..4076ad2e0b 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -70,14 +70,14 @@ pub(crate) enum InitialContextInjection { pub(crate) async fn build_compaction_initial_context( sess: &Session, - turn_context: &TurnContext, + step_context: &StepContext, initial_context_injection: &InitialContextInjection, ) -> (Vec, Option>) { // Return the rendered state with its items so history and its baseline stay identical. match initial_context_injection { InitialContextInjection::BeforeLastUserMessage(world_state) => { let items = sess - .build_initial_context_with_world_state(turn_context, world_state.as_ref()) + .build_initial_context_with_world_state(step_context, world_state.as_ref()) .await; (items, Some(Arc::clone(world_state))) } @@ -337,7 +337,7 @@ async fn run_compact_task_inner_impl( let (initial_context, world_state_baseline) = build_compaction_initial_context( sess.as_ref(), - turn_context.as_ref(), + step_context.as_ref(), &initial_context_injection, ) .await; @@ -348,7 +348,7 @@ async fn run_compact_task_inner_impl( let reference_context_item = match initial_context_injection { InitialContextInjection::DoNotInject => None, InitialContextInjection::BeforeLastUserMessage(_) => { - Some(turn_context.to_turn_context_item()) + Some(step_context.to_turn_context_item()) } }; let compacted_item = CompactedItem { diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 4b1db93555..aa2b66c6cd 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -212,8 +212,8 @@ async fn run_remote_compact_task_inner_impl( analytics_details, ) .await; - let (attempt, compaction_turn_context) = match attempt { - Ok(attempt) => (attempt, turn_context), + let (attempt, compaction_step_context) = match attempt { + Ok(attempt) => (attempt, step_context), Err(error) => { let Some(fallback_step_context) = fallback_step_context else { return Err(error); @@ -247,11 +247,12 @@ async fn run_remote_compact_task_inner_impl( fallback_result.as_ref().err(), ); match fallback_result { - Ok(attempt) => (attempt, fallback_turn_context), + Ok(attempt) => (attempt, fallback_step_context), Err(_) => return Err(error), } } }; + let compaction_turn_context = compaction_step_context.turn.as_ref(); let RemoteCompactAttempt { new_history, trace_input_history, @@ -259,7 +260,7 @@ async fn run_remote_compact_task_inner_impl( let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await; let (new_history, world_state_baseline) = process_compacted_history( sess.as_ref(), - compaction_turn_context.as_ref(), + compaction_step_context.as_ref(), new_history, &initial_context_injection, ) @@ -268,7 +269,7 @@ async fn run_remote_compact_task_inner_impl( let reference_context_item = match initial_context_injection { InitialContextInjection::DoNotInject => None, InitialContextInjection::BeforeLastUserMessage(_) => { - Some(compaction_turn_context.to_turn_context_item()) + Some(compaction_step_context.to_turn_context_item()) } }; let compacted_item = CompactedItem { @@ -287,7 +288,7 @@ async fn run_remote_compact_task_inner_impl( replacement_history: &new_history, }); sess.replace_compacted_history( - compaction_turn_context.as_ref(), + compaction_turn_context, new_history, reference_context_item, world_state_baseline, @@ -303,7 +304,7 @@ async fn run_remote_compact_task_inner_impl( pub(crate) async fn process_compacted_history( sess: &Session, - turn_context: &TurnContext, + step_context: &StepContext, mut compacted_history: Vec, initial_context_injection: &InitialContextInjection, ) -> (Vec, Option>) { @@ -311,7 +312,7 @@ pub(crate) async fn process_compacted_history( // message in the replacement history. Pre-turn compaction instead injects context after the // compaction item, but mid-turn compaction keeps the compaction item last for model training. let (initial_context, world_state_baseline) = - build_compaction_initial_context(sess, turn_context, initial_context_injection).await; + build_compaction_initial_context(sess, step_context, initial_context_injection).await; compacted_history.retain(should_keep_compacted_history_item); ( diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index d3ffdcbd65..5b1f6b5a53 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -224,8 +224,8 @@ async fn run_remote_compact_task_inner_impl( analytics_details, ) .await; - let (attempt, compaction_turn_context) = match attempt { - Ok(attempt) => (attempt, turn_context), + let (attempt, compaction_step_context) = match attempt { + Ok(attempt) => (attempt, step_context), Err(error) => { let Some(fallback_step_context) = fallback_step_context else { return Err(error); @@ -259,11 +259,12 @@ async fn run_remote_compact_task_inner_impl( fallback_result.as_ref().err(), ); match fallback_result { - Ok(attempt) => (attempt, fallback_turn_context), + Ok(attempt) => (attempt, fallback_step_context), Err(_) => return Err(error), } } }; + let compaction_turn_context = compaction_step_context.turn.as_ref(); let RemoteCompactV2Attempt { trace_input_history, prompt_input, @@ -283,7 +284,7 @@ async fn run_remote_compact_task_inner_impl( let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await; let (new_history, world_state_baseline) = process_compacted_history( sess.as_ref(), - compaction_turn_context.as_ref(), + compaction_step_context.as_ref(), compacted_history, &initial_context_injection, ) @@ -292,7 +293,7 @@ async fn run_remote_compact_task_inner_impl( let reference_context_item = match initial_context_injection { InitialContextInjection::DoNotInject => None, InitialContextInjection::BeforeLastUserMessage(_) => { - Some(compaction_turn_context.to_turn_context_item()) + Some(compaction_step_context.to_turn_context_item()) } }; let compacted_item = CompactedItem { @@ -308,7 +309,7 @@ async fn run_remote_compact_task_inner_impl( replacement_history: &new_history, }); sess.replace_compacted_history( - compaction_turn_context.as_ref(), + compaction_turn_context, new_history, reference_context_item, world_state_baseline, diff --git a/codex-rs/core/src/compact_tests.rs b/codex-rs/core/src/compact_tests.rs index 16b9ae16f6..776de45cf7 100644 --- a/codex-rs/core/src/compact_tests.rs +++ b/codex-rs/core/src/compact_tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::session::step_context::StepContext; use crate::session::tests::build_world_state_from_turn_context; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::WireApi; @@ -13,17 +14,18 @@ async fn process_compacted_history_with_test_session( ) -> (Vec, Vec) { let (session, turn_context) = crate::session::tests::make_session_and_context().await; let turn_context = Arc::new(turn_context); + let step_context = StepContext::for_test(Arc::clone(&turn_context)); session .set_previous_turn_settings(previous_turn_settings.cloned()) .await; let world_state = Arc::new(build_world_state_from_turn_context(&session, &turn_context).await); let initial_context = session - .build_initial_context_with_world_state(&turn_context, world_state.as_ref()) + .build_initial_context_with_world_state(&step_context, world_state.as_ref()) .await; let initial_context_injection = InitialContextInjection::BeforeLastUserMessage(world_state); let (refreshed, _) = crate::compact_remote::process_compacted_history( &session, - &turn_context, + &step_context, compacted_history, &initial_context_injection, ) diff --git a/codex-rs/core/src/compact_token_budget.rs b/codex-rs/core/src/compact_token_budget.rs index 710e7cefe5..ae212a613a 100644 --- a/codex-rs/core/src/compact_token_budget.rs +++ b/codex-rs/core/src/compact_token_budget.rs @@ -38,7 +38,7 @@ pub(crate) async fn run_manual_compact_task( // Manual compaction runs outside run_turn, so it captures its own current step. let step_context = sess.capture_step_context(Arc::clone(&turn_context)).await; let world_state = Arc::new(sess.build_world_state_for_step(&step_context).await); - run_compact_task_inner(&sess, &turn_context, world_state, CompactionTrigger::Manual).await + run_compact_task_inner(&sess, &step_context, world_state, CompactionTrigger::Manual).await } /// Runs token-budget inline auto-compaction as a normal compaction lifecycle. @@ -51,22 +51,22 @@ pub(crate) async fn run_inline_auto_compact_task( step_context: Arc, initial_context_injection: InitialContextInjection, ) -> CodexResult<()> { - let turn_context = &step_context.turn; let world_state = match initial_context_injection { InitialContextInjection::BeforeLastUserMessage(world_state) => world_state, InitialContextInjection::DoNotInject => { Arc::new(sess.build_world_state_for_step(&step_context).await) } }; - run_compact_task_inner(&sess, turn_context, world_state, CompactionTrigger::Auto).await + run_compact_task_inner(&sess, &step_context, world_state, CompactionTrigger::Auto).await } async fn run_compact_task_inner( sess: &Arc, - turn_context: &Arc, + step_context: &Arc, world_state: Arc, trigger: CompactionTrigger, ) -> CodexResult<()> { + let turn_context = &step_context.turn; let pre_compact_outcome = run_pre_compact_hooks(sess, turn_context, trigger).await; match pre_compact_outcome { PreCompactHookOutcome::Continue => {} @@ -76,7 +76,7 @@ async fn run_compact_task_inner( let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new()); sess.emit_turn_item_started(turn_context, &compaction_item) .await; - sess.start_new_context_window(turn_context.as_ref(), world_state) + sess.start_new_context_window(step_context.as_ref(), world_state) .await; sess.emit_turn_item_completed(turn_context, compaction_item) .await; diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index d4cf65e86d..a3c61d6d26 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -9,6 +9,7 @@ use crate::context::RealtimeEndInstructions; use crate::context::RealtimeStartInstructions; use crate::context::RealtimeStartWithInstructions; use crate::session::PreviousTurnSettings; +use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use codex_execpolicy::Policy; use codex_features::Feature; @@ -63,20 +64,18 @@ fn build_permissions_update_item( fn build_collaboration_mode_update_item( previous: Option<&TurnContextItem>, - next: &TurnContext, + next: &StepContext, ) -> Option { - if !next.config.include_collaboration_mode_instructions { + if !next.turn.config.include_collaboration_mode_instructions { return None; } let prev = previous?; - if prev.collaboration_mode.as_ref() != Some(&next.collaboration_mode) { + let collaboration_mode = next.collaboration_mode(); + if prev.collaboration_mode.as_ref() != Some(&collaboration_mode) { // If the next mode has empty developer instructions, this returns None and we emit no // update, so prior collaboration instructions remain in the prompt history. - Some( - CollaborationModeInstructions::from_collaboration_mode(&next.collaboration_mode)? - .render(), - ) + Some(CollaborationModeInstructions::from_collaboration_mode(&collaboration_mode)?.render()) } else { None } @@ -84,7 +83,7 @@ fn build_collaboration_mode_update_item( fn build_multi_agent_mode_update_item( previous: Option<&TurnContextItem>, - next: &TurnContext, + next: &StepContext, ) -> Option { let effective_multi_agent_mode = crate::session::multi_agents::effective_multi_agent_mode(next); let previous = previous?; @@ -240,10 +239,11 @@ fn build_text_message(role: &str, text_sections: Vec) -> Option, previous_turn_settings: Option<&PreviousTurnSettings>, - next: &TurnContext, + next: &StepContext, exec_policy: &Policy, personality_feature_enabled: bool, ) -> Vec { + let turn_context = next.turn.as_ref(); // TODO(ccunningham): build_settings_update_items still does not cover every // model-visible item emitted by build_initial_context. Persist the remaining // inputs or add explicit replay events so fork/resume can diff everything @@ -251,12 +251,12 @@ pub(crate) fn build_settings_update_items( let developer_update_sections = [ // Keep model-switch instructions first so model-specific guidance is read before // any other context diffs on this turn. - build_model_instructions_update_item(previous_turn_settings, next), - build_permissions_update_item(previous, next, exec_policy), + build_model_instructions_update_item(previous_turn_settings, turn_context), + build_permissions_update_item(previous, turn_context, exec_policy), build_collaboration_mode_update_item(previous, next), build_multi_agent_mode_update_item(previous, next), - build_realtime_update_item(previous, previous_turn_settings, next), - build_personality_update_item(previous, next, personality_feature_enabled), + build_realtime_update_item(previous, previous_turn_settings, turn_context), + build_personality_update_item(previous, turn_context, personality_feature_enabled), ] .into_iter() .flatten() diff --git a/codex-rs/core/src/guardian/review.rs b/codex-rs/core/src/guardian/review.rs index 2e93194fde..04f094f2f8 100644 --- a/codex-rs/core/src/guardian/review.rs +++ b/codex-rs/core/src/guardian/review.rs @@ -25,6 +25,7 @@ use tokio::time::sleep_until; use tokio_util::sync::CancellationToken; use crate::session::session::Session; +use crate::session::step_context::StepContextSeed; use crate::session::turn_context::TurnContext; use crate::turn_timing::now_unix_timestamp_ms; use crate::util::backoff; @@ -274,13 +275,14 @@ pub(crate) async fn record_guardian_denial_for_test( /// caller as distinct from explicit guardian denials. async fn run_guardian_review( session: Arc, - turn: Arc, + step_context_seed: StepContextSeed, review_id: String, request: GuardianApprovalRequest, retry_reason: Option, approval_request_source: GuardianApprovalRequestSource, external_cancel: Option, ) -> ReviewDecision { + let turn = Arc::clone(&step_context_seed.turn); let target_item_id = guardian_request_target_item_id(&request).map(str::to_string); let assessment_turn_id = guardian_request_turn_id(&request, &turn.sub_id).to_string(); let action_summary = guardian_assessment_action(&request); @@ -358,7 +360,7 @@ async fn run_guardian_review( let terminal_action = action_summary.clone(); let (outcome, analytics_result) = Box::pin(run_guardian_review_session_with_retry( session.clone(), - turn.clone(), + step_context_seed.clone(), request, retry_reason.clone(), schema, @@ -593,7 +595,7 @@ async fn run_guardian_review( /// Public entrypoint for approval requests that should be reviewed by guardian. pub(crate) async fn review_approval_request( session: &Arc, - turn: &Arc, + step_context_seed: &StepContextSeed, review_id: String, request: GuardianApprovalRequest, retry_reason: Option, @@ -602,7 +604,7 @@ pub(crate) async fn review_approval_request( // guardian session state machine into their own async stack. Box::pin(run_guardian_review( Arc::clone(session), - Arc::clone(turn), + step_context_seed.clone(), review_id, request, retry_reason, @@ -614,7 +616,7 @@ pub(crate) async fn review_approval_request( pub(crate) async fn review_approval_request_with_cancel( session: &Arc, - turn: &Arc, + step_context_seed: &StepContextSeed, review_id: String, request: GuardianApprovalRequest, retry_reason: Option, @@ -623,7 +625,7 @@ pub(crate) async fn review_approval_request_with_cancel( ) -> ReviewDecision { run_guardian_review( Arc::clone(session), - Arc::clone(turn), + step_context_seed.clone(), review_id, request, retry_reason, @@ -635,7 +637,7 @@ pub(crate) async fn review_approval_request_with_cancel( pub(crate) fn spawn_approval_request_review( session: Arc, - turn: Arc, + step_context_seed: StepContextSeed, review_id: String, request: GuardianApprovalRequest, retry_reason: Option, @@ -653,7 +655,7 @@ pub(crate) fn spawn_approval_request_review( }; let decision = runtime.block_on(review_approval_request_with_cancel( &session, - &turn, + &step_context_seed, review_id, request, retry_reason, @@ -677,8 +679,9 @@ pub(super) struct GuardianReviewSessionConfig { pub(super) async fn guardian_review_session_config( session: &Session, - turn: &TurnContext, + step_context_seed: &StepContextSeed, ) -> anyhow::Result { + let turn = step_context_seed.turn.as_ref(); let network_proxy = session.services.network_proxy.load_full(); let live_network_config = match network_proxy.as_ref() { Some(network_proxy) => Some(network_proxy.proxy().current_cfg().await?), @@ -722,8 +725,8 @@ pub(super) async fn guardian_review_session_config( .supported_reasoning_levels .iter() .any(|preset| preset.effort == codex_protocol::openai_models::ReasoningEffort::Low), - turn.config - .model_reasoning_effort + step_context_seed + .reasoning_effort .clone() .or_else(|| turn.model_info.default_reasoning_level.clone()), ); @@ -768,29 +771,29 @@ pub(super) async fn guardian_review_session_config( /// rules. async fn run_guardian_review_session_before_deadline( session: Arc, - turn: Arc, + step_context_seed: StepContextSeed, request: GuardianApprovalRequest, retry_reason: Option, schema: serde_json::Value, external_cancel: Option, deadline: Instant, ) -> (GuardianReviewOutcome, GuardianReviewAnalyticsResult) { - let session_config = match guardian_review_session_config(session.as_ref(), turn.as_ref()).await - { - Ok(session_config) => session_config, - Err(err) => { - return ( - GuardianReviewOutcome::Error(GuardianReviewError::prompt_build(err)), - GuardianReviewAnalyticsResult::without_session(), - ); - } - }; + let session_config = + match guardian_review_session_config(session.as_ref(), &step_context_seed).await { + Ok(session_config) => session_config, + Err(err) => { + return ( + GuardianReviewOutcome::Error(GuardianReviewError::prompt_build(err)), + GuardianReviewAnalyticsResult::without_session(), + ); + } + }; let (session_outcome, session_analytics_result) = Box::pin( session .guardian_review_session .run_review(GuardianReviewSessionParams { parent_session: Arc::clone(&session), - parent_turn: turn.clone(), + parent_turn: Arc::clone(&step_context_seed.turn), spawn_config: session_config.spawn_config, request, retry_reason, @@ -801,8 +804,8 @@ async fn run_guardian_review_session_before_deadline( guardian_catalog_contains_auto_review: session_config.catalog_contains_auto_review, guardian_review_model_overridden: session_config.model_overridden, guardian_review_model_override: session_config.model_override, - reasoning_summary: turn.reasoning_summary, - personality: turn.personality, + reasoning_summary: step_context_seed.turn.reasoning_summary, + personality: step_context_seed.turn.personality, external_cancel, deadline, }), @@ -862,7 +865,7 @@ async fn run_guardian_review_session_before_deadline( pub(super) async fn run_guardian_review_session_with_retry( session: Arc, - turn: Arc, + step_context_seed: StepContextSeed, request: GuardianApprovalRequest, retry_reason: Option, schema: serde_json::Value, @@ -875,7 +878,7 @@ pub(super) async fn run_guardian_review_session_with_retry( loop { let (outcome, mut analytics_result) = run_guardian_review_session_before_deadline( Arc::clone(&session), - Arc::clone(&turn), + step_context_seed.clone(), request.clone(), retry_reason.clone(), schema.clone(), diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 4662990c5b..17693bb765 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -44,6 +44,7 @@ use crate::context::ContextualUserFragment; use crate::context::GuardianFollowupReviewReminder; use crate::session::Codex; use crate::session::session::Session; +use crate::session::step_context::StepContextSeed; use crate::session::turn_context::TurnContext; use codex_config::types::McpServerConfig; use codex_features::Feature; @@ -299,9 +300,12 @@ impl GuardianReviewSessionManager { ) -> BoxFuture<'_, anyhow::Result<()>> { // Boxing breaks the Session::new -> Guardian -> Session::new future recursion. Box::pin(async move { - let spawn_config = guardian_review_session_config(&parent_session, &parent_turn) - .await? - .spawn_config; + let spawn_config = guardian_review_session_config( + &parent_session, + &StepContextSeed::from_turn(Arc::clone(&parent_turn)), + ) + .await? + .spawn_config; let reuse_key = GuardianReviewSessionReuseKey::from_spawn_config( &spawn_config, parent_session.user_instructions().await, diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index e23475d30e..7d413d4977 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -7,6 +7,7 @@ use crate::config::NetworkProxySpec; use crate::config::test_config; use crate::guardian::approval_request::guardian_request_target_item_id; use crate::session::session::Session; +use crate::session::step_context::StepContextSeed; use crate::session::turn_context::TurnContext; use crate::test_support; use codex_analytics::GuardianApprovalRequestSource; @@ -1151,7 +1152,7 @@ async fn cancelled_guardian_review_emits_terminal_abort_without_warning() { let decision = review_approval_request_with_cancel( &session, - &turn, + &StepContextSeed::from_turn(Arc::clone(&turn)), "review-cancelled-guardian".to_string(), GuardianApprovalRequest::ApplyPatch { id: "patch-1".to_string(), @@ -2321,7 +2322,7 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() -> let decision = review_approval_request( &session, - &turn, + &StepContextSeed::from_turn(Arc::clone(&turn)), "review-shell-guardian-error".to_string(), GuardianApprovalRequest::Shell { id: "shell-guardian-error".to_string(), @@ -2461,7 +2462,7 @@ async fn guardian_review_does_not_retry_missing_assessment_payload() -> anyhow:: let decision = review_approval_request( &session, - &turn, + &StepContextSeed::from_turn(Arc::clone(&turn)), "review-missing-assessment".to_string(), guardian_shell_request("shell-missing-assessment"), /*retry_reason*/ None, @@ -2565,7 +2566,7 @@ async fn guardian_review_exhausts_three_failures_with_one_terminal_event() -> an let decision = review_approval_request( &session, - &turn, + &StepContextSeed::from_turn(Arc::clone(&turn)), "review-exhausted-retry".to_string(), guardian_shell_request("shell-exhausted-retry"), /*retry_reason*/ None, @@ -2616,7 +2617,7 @@ async fn guardian_review_does_not_retry_valid_denial() -> anyhow::Result<()> { let decision = review_approval_request( &session, - &turn, + &StepContextSeed::from_turn(Arc::clone(&turn)), "review-valid-denial".to_string(), guardian_shell_request("shell-valid-denial"), /*retry_reason*/ None, @@ -2719,7 +2720,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() -> assert_eq!( review_approval_request( &session, - &turn, + &StepContextSeed::from_turn(Arc::clone(&turn)), "review-shell-guardian-1".to_string(), initial_request, /*retry_reason*/ None @@ -2773,7 +2774,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() -> let mut second_review = tokio::spawn(async move { review_approval_request( &session_for_second, - &turn_for_second, + &StepContextSeed::from_turn(Arc::clone(&turn_for_second)), "review-shell-guardian-2".to_string(), second_request, Some("trunk follow-up".to_string()), @@ -2820,7 +2821,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() -> let third_decision = review_approval_request( &session, - &turn, + &StepContextSeed::from_turn(Arc::clone(&turn)), "review-shell-guardian-3".to_string(), third_request, Some("parallel follow-up".to_string()), diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 1926a1b896..4a33d43a87 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -19,6 +19,7 @@ use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam; use crate::mcp_tool_approval_templates::render_mcp_tool_approval_template; use crate::session::session::Session; use crate::session::step_context::StepContext; +use crate::session::step_context::StepContextSeed; use crate::session::turn_context::TurnContext; use crate::tools::hook_names::HookToolName; use crate::tools::sandboxing::PermissionRequestPayload; @@ -1274,7 +1275,7 @@ async fn maybe_request_mcp_tool_approval( let review_id = new_guardian_review_id(); let decision = review_approval_request( sess, - turn_context, + &StepContextSeed::from(step_context.as_ref()), review_id.clone(), build_guardian_mcp_tool_review_request(call_id, invocation, metadata), /*retry_reason*/ None, diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index eb820a0307..5b56dfad95 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -93,10 +93,11 @@ fn approval_metadata( } } -fn mcp_turn_metadata_context(turn_context: &TurnContext) -> McpTurnMetadataContext<'_> { +fn mcp_turn_metadata_context(step_context: &StepContext) -> McpTurnMetadataContext<'_> { + let turn_context = step_context.turn.as_ref(); McpTurnMetadataContext { model: turn_context.model_info.slug.as_str(), - reasoning_effort: turn_context.effective_reasoning_effort(), + reasoning_effort: step_context.effective_reasoning_effort(), } } @@ -1091,12 +1092,17 @@ fn truncate_mcp_tool_result_for_event_bounds_large_error() { #[tokio::test] async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() { - let (_, turn_context) = make_session_and_context().await; + let (_, mut turn_context) = make_session_and_context().await; + Arc::make_mut(&mut turn_context.config).model_reasoning_effort = + Some(codex_protocol::openai_models::ReasoningEffort::High); let turn_context = Arc::new(turn_context); - let step_context = StepContext::for_test(Arc::clone(&turn_context)); + let mut step_context = StepContext::for_test(Arc::clone(&turn_context)); + Arc::get_mut(&mut step_context) + .expect("step context should be uniquely owned") + .reasoning_effort = Some(codex_protocol::openai_models::ReasoningEffort::Low); let expected_turn_metadata = turn_context .turn_metadata_state - .current_meta_value_for_mcp_request(mcp_turn_metadata_context(&turn_context)) + .current_meta_value_for_mcp_request(mcp_turn_metadata_context(&step_context)) .expect("turn metadata"); let meta = build_mcp_tool_call_request_meta( @@ -1204,7 +1210,7 @@ async fn plugin_mcp_tool_call_request_meta_includes_plugin_id() { let step_context = StepContext::for_test(Arc::clone(&turn_context)); let expected_turn_metadata = turn_context .turn_metadata_state - .current_meta_value_for_mcp_request(mcp_turn_metadata_context(&turn_context)) + .current_meta_value_for_mcp_request(mcp_turn_metadata_context(&step_context)) .expect("turn metadata"); let mut metadata = approval_metadata( /*connector_id*/ None, /*connector_name*/ None, @@ -1325,7 +1331,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps let step_context = StepContext::for_test(Arc::clone(&turn_context)); let expected_turn_metadata = turn_context .turn_metadata_state - .current_meta_value_for_mcp_request(mcp_turn_metadata_context(&turn_context)) + .current_meta_value_for_mcp_request(mcp_turn_metadata_context(&step_context)) .expect("turn metadata"); let metadata = McpToolApprovalMetadata { annotations: None, @@ -1378,7 +1384,7 @@ async fn codex_apps_tool_call_request_meta_includes_call_id_without_existing_cod let step_context = StepContext::for_test(Arc::clone(&turn_context)); let expected_turn_metadata = turn_context .turn_metadata_state - .current_meta_value_for_mcp_request(mcp_turn_metadata_context(&turn_context)) + .current_meta_value_for_mcp_request(mcp_turn_metadata_context(&step_context)) .expect("turn metadata"); assert_eq!( diff --git a/codex-rs/core/src/session/elicitation_holders_tests.rs b/codex-rs/core/src/session/elicitation_holders_tests.rs index df03aeb291..a121d1c81e 100644 --- a/codex-rs/core/src/session/elicitation_holders_tests.rs +++ b/codex-rs/core/src/session/elicitation_holders_tests.rs @@ -11,6 +11,7 @@ use tokio::sync::watch; use tokio_util::sync::CancellationToken; use super::tests::make_session_and_context_with_rx; +use crate::session::step_context::StepContextSeed; use crate::state::ActiveTurn; async fn wait_until_held(pause_state: &mut watch::Receiver) { @@ -114,7 +115,7 @@ async fn permission_request_holds_an_elicitation_until_response() { .selection(); session .request_permissions_for_environment( - &turn_context, + &StepContextSeed::from_turn(Arc::clone(&turn_context)), "call-1".to_string(), RequestPermissionsArgs { environment_id: None, diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 3f5e92eca7..d7c94c0c7d 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -1,3 +1,4 @@ +use super::step_context::StepContextSeed; use super::*; use crate::mcp::McpRuntimeProjection; use codex_exec_server::ResolvedSelectedCapabilityRoot; @@ -623,7 +624,7 @@ async fn review_guardian_mcp_elicitation( let review_id = crate::guardian::new_guardian_review_id(); let decision = crate::guardian::review_approval_request( &session, - &turn_context, + &StepContextSeed::from_turn(Arc::clone(&turn_context)), review_id.clone(), guardian_request, /*retry_reason*/ None, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index e0eb2ec756..10f9a03c3f 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -40,6 +40,7 @@ use crate::image_preparation::prepare_response_items; use crate::parse_turn_item; use crate::realtime_conversation::RealtimeConversationManager; use crate::session::step_context::StepContext; +use crate::session::step_context::StepContextSeed; use crate::session::turn_context::TurnEnvironment; use crate::session_prefix::format_inter_agent_completion_message; use crate::skills::SkillRenderSideEffects; @@ -1729,7 +1730,7 @@ impl Session { async fn build_settings_update_items( &self, reference_context_item: Option<&TurnContextItem>, - current_context: &TurnContext, + current_context: &StepContext, ) -> Vec { // TODO: Make context updates a pure diff of persisted previous/current TurnContextItem // state so replay/backtracking is deterministic. Runtime inputs that affect model-visible @@ -2267,12 +2268,13 @@ impl Session { )] pub(crate) async fn request_permissions_for_environment( self: &Arc, - turn_context: &Arc, + step_context_seed: &StepContextSeed, call_id: String, args: RequestPermissionsArgs, environment: TurnEnvironmentSelection, cancellation_token: CancellationToken, ) -> Option { + let turn_context = &step_context_seed.turn; match turn_context.as_ref().approval_policy.value() { AskForApproval::Never => { return Some(RequestPermissionsResponse { @@ -2316,7 +2318,6 @@ impl Session { }; let review_id = crate::guardian::new_guardian_review_id(); let session = Arc::clone(self); - let turn = Arc::clone(turn_context); let request = crate::guardian::GuardianApprovalRequest::RequestPermissions { id: call_id, turn_id: turn_context.sub_id.clone(), @@ -2325,7 +2326,7 @@ impl Session { }; let review_rx = crate::guardian::spawn_approval_request_review( session, - turn, + step_context_seed.clone(), review_id, request, /*retry_reason*/ None, @@ -2435,12 +2436,13 @@ impl Session { pub(crate) async fn request_permissions_for_cwd( self: &Arc, - turn_context: &Arc, + step_context_seed: &StepContextSeed, call_id: String, args: RequestPermissionsArgs, cwd: AbsolutePathBuf, cancellation_token: CancellationToken, ) -> Option { + let turn_context = &step_context_seed.turn; let turn_environment = match args.environment_id.as_deref() { Some(environment_id) => turn_context .environments @@ -2459,7 +2461,7 @@ impl Session { let mut environment = turn_environment.selection(); environment.cwd = PathUri::from_abs_path(&cwd); self.request_permissions_for_environment( - turn_context, + step_context_seed, call_id, args, environment, @@ -3165,27 +3167,27 @@ impl Session { pub(crate) async fn build_initial_context_with_world_state( &self, - turn_context: &TurnContext, + step_context: &StepContext, world_state: &WorldState, ) -> Vec { let mcp = self.services.latest_mcp_runtime(); - self.build_initial_context_with_world_state_and_mcp(turn_context, world_state, &mcp) + self.build_initial_context_with_world_state_and_mcp(step_context, world_state, &mcp) .await } async fn build_initial_context_with_world_state_and_mcp( &self, - turn_context: &TurnContext, + step_context: &StepContext, world_state: &WorldState, mcp: &McpRuntimeSnapshot, ) -> Vec { + let turn_context = step_context.turn.as_ref(); let mut developer_sections = Vec::::with_capacity(8); let mut contextual_user_sections = Vec::::with_capacity(2); let mut separate_developer_sections = Vec::::new(); let ( reference_context_item, previous_turn_settings, - collaboration_mode, base_instructions, session_source, auto_compact_window_ids, @@ -3194,12 +3196,12 @@ impl Session { ( state.reference_context_item(), state.previous_turn_settings(), - state.session_configuration.collaboration_mode.clone(), state.session_configuration.base_instructions.clone(), state.session_configuration.session_source.clone(), state.auto_compact_window_ids(), ) }; + let collaboration_mode = step_context.collaboration_mode(); if let Some(model_switch_message) = crate::context_manager::updates::build_model_instructions_update_item( previous_turn_settings.as_ref(), @@ -3449,7 +3451,7 @@ impl Session { { items.push(usage_hint_message); } - if let Some(multi_agent_mode) = multi_agents::effective_multi_agent_mode(turn_context) { + if let Some(multi_agent_mode) = multi_agents::effective_multi_agent_mode(step_context) { items.push(ContextualUserFragment::into( MultiAgentModeInstructions::new(multi_agent_mode), )); @@ -3516,18 +3518,19 @@ impl Session { pub(crate) async fn start_new_context_window( &self, - turn_context: &TurnContext, + step_context: &StepContext, world_state: Arc, ) -> u64 { + let turn_context = step_context.turn.as_ref(); let window = { let mut state = self.state.lock().await; state.start_new_context_window() }; let (window_number, window_ids) = window; let context_items = self - .build_initial_context_with_world_state(turn_context, world_state.as_ref()) + .build_initial_context_with_world_state(step_context, world_state.as_ref()) .await; - let turn_context_item = turn_context.to_turn_context_item(); + let turn_context_item = step_context.to_turn_context_item(); self.replace_compacted_history( turn_context, context_items, @@ -3575,7 +3578,7 @@ impl Session { let state = self.state.lock().await; state.reference_context_item() }; - let turn_context_item = turn_context.to_turn_context_item(); + let turn_context_item = step_context.to_turn_context_item(); let turn_context_changed = reference_context_item.as_ref() != Some(&turn_context_item); let should_inject_full_context = reference_context_item.is_none(); let world_state = Arc::new(self.build_world_state_for_step(step_context).await); @@ -3583,7 +3586,7 @@ impl Session { let (mut context_items, world_state_item) = if should_inject_full_context { let context_items = self .build_initial_context_with_world_state_and_mcp( - turn_context, + step_context, world_state.as_ref(), step_context.mcp.as_ref(), ) @@ -3602,7 +3605,7 @@ impl Session { // Steady-state path: append only built-in context diffs here; turn-scoped extension // context is added below. let mut context_items = self - .build_settings_update_items(reference_context_item.as_ref(), turn_context) + .build_settings_update_items(reference_context_item.as_ref(), step_context) .await; let (world_state_items, world_state_item) = { let mut state = self.state.lock().await; diff --git a/codex-rs/core/src/session/multi_agents.rs b/codex-rs/core/src/session/multi_agents.rs index c2f139e68b..d5e91beed2 100644 --- a/codex-rs/core/src/session/multi_agents.rs +++ b/codex-rs/core/src/session/multi_agents.rs @@ -1,4 +1,5 @@ use crate::config::MultiAgentV2Config; +use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use codex_protocol::config_types::MultiAgentMode; use codex_protocol::openai_models::ReasoningEffort; @@ -36,7 +37,8 @@ fn configured_usage_hint_text_for_source<'a>( } } -pub(crate) fn effective_multi_agent_mode(turn_context: &TurnContext) -> Option { +pub(crate) fn effective_multi_agent_mode(step_context: &StepContext) -> Option { + let turn_context = step_context.turn.as_ref(); if turn_context.multi_agent_version != MultiAgentVersion::V2 { return None; } @@ -49,7 +51,7 @@ pub(crate) fn effective_multi_agent_mode(turn_context: &TurnContext) -> Option MultiAgentMode::Custom(hint_text.clone()), - None => match turn_context.effective_reasoning_effort() { + None => match step_context.effective_reasoning_effort() { Some(ReasoningEffort::Ultra) => MultiAgentMode::Proactive, _ => MultiAgentMode::ExplicitRequestOnly, }, diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs index b72d19b679..660543b518 100644 --- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs @@ -2,6 +2,7 @@ use super::*; use super::tests::build_world_state_from_turn_context; use super::tests::make_session_and_context; +use super::tests::turn_context_item_for_test; use codex_protocol::AgentPath; use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; @@ -138,7 +139,7 @@ async fn record_initial_history_restores_world_state_baseline() { let turn_context = Arc::new(turn_context); let world_state = build_world_state_from_turn_context(&session, &turn_context).await; let rollout_items = completed_user_turn_rollout( - turn_context.to_turn_context_item(), + turn_context_item_for_test(&turn_context), vec![RolloutItem::WorldState(WorldStateItem::full( world_state.snapshot().into_value(), ))], @@ -291,7 +292,8 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif #[tokio::test] async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_completed_turns() { let (session, turn_context) = make_session_and_context().await; - let first_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let first_context_item = turn_context_item_for_test(&turn_context); let first_turn_id = first_context_item .turn_id .clone() @@ -414,7 +416,8 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com #[tokio::test] async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_incomplete_turn() { let (session, turn_context) = make_session_and_context().await; - let first_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let first_context_item = turn_context_item_for_test(&turn_context); let first_turn_id = first_context_item .turn_id .clone() @@ -508,7 +511,8 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc #[tokio::test] async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metadata() { let (session, turn_context) = make_session_and_context().await; - let first_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let first_context_item = turn_context_item_for_test(&turn_context); let first_turn_id = first_context_item .turn_id .clone() @@ -634,7 +638,8 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad #[tokio::test] async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() { let (session, turn_context) = make_session_and_context().await; - let first_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let first_context_item = turn_context_item_for_test(&turn_context); let first_turn_id = first_context_item .turn_id .clone() @@ -735,7 +740,8 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() { #[tokio::test] async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding_user_turns() { let (session, turn_context) = make_session_and_context().await; - let only_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let only_context_item = turn_context_item_for_test(&turn_context); let only_turn_id = only_context_item .turn_id .clone() @@ -789,7 +795,8 @@ async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding #[tokio::test] async fn record_initial_history_resumed_rollback_skips_only_user_turns() { let (session, turn_context) = make_session_and_context().await; - let previous_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let previous_context_item = turn_context_item_for_test(&turn_context); let user_turn_id = previous_context_item .turn_id .clone() @@ -864,7 +871,8 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() { #[tokio::test] async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_compaction_metadata() { let (session, turn_context) = make_session_and_context().await; - let previous_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let previous_context_item = turn_context_item_for_test(&turn_context); let previous_turn_id = previous_context_item .turn_id .clone() @@ -960,7 +968,8 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp #[tokio::test] async fn record_initial_history_resumed_bare_turn_context_does_not_seed_reference_context_item() { let (session, turn_context) = make_session_and_context().await; - let previous_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let previous_context_item = turn_context_item_for_test(&turn_context); let rollout_items = vec![RolloutItem::TurnContext(previous_context_item.clone())]; session @@ -977,7 +986,8 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_seed_referenc #[tokio::test] async fn record_initial_history_resumed_does_not_seed_reference_context_item_after_compaction() { let (session, turn_context) = make_session_and_context().await; - let previous_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let previous_context_item = turn_context_item_for_test(&turn_context); let rollout_items = vec![ RolloutItem::TurnContext(previous_context_item), RolloutItem::Compacted(CompactedItem { @@ -1078,8 +1088,9 @@ async fn reconstruct_history_prefers_compacted_window_over_session_meta() { #[tokio::test] async fn reconstruct_history_replays_world_state_from_latest_compaction_window() { let (session, turn_context) = make_session_and_context().await; + let turn_context = Arc::new(turn_context); let rollout_items = completed_user_turn_rollout( - turn_context.to_turn_context_item(), + turn_context_item_for_test(&turn_context), vec![ RolloutItem::WorldState(WorldStateItem::full(json!({ "environment": {"status": "old"} @@ -1186,7 +1197,8 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_does_ async fn reconstruct_history_legacy_compaction_without_replacement_history_clears_later_reference_context_item() { let (session, turn_context) = make_session_and_context().await; - let current_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let current_context_item = turn_context_item_for_test(&turn_context); let current_turn_id = current_context_item .turn_id .clone() @@ -1479,7 +1491,8 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_for_later_turn_context() { let (session, turn_context) = make_session_and_context().await; - let previous_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let previous_context_item = turn_context_item_for_test(&turn_context); let previous_turn_id = previous_context_item .turn_id .clone() @@ -1717,7 +1730,8 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea #[tokio::test] async fn record_initial_history_resumed_trailing_incomplete_turn_preserves_turn_context_item() { let (session, turn_context) = make_session_and_context().await; - let current_context_item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let current_context_item = turn_context_item_for_test(&turn_context); let current_turn_id = current_context_item .turn_id .clone() diff --git a/codex-rs/core/src/session/step_context.rs b/codex-rs/core/src/session/step_context.rs index 14caa2ea43..fbd2e9961f 100644 --- a/codex-rs/core/src/session/step_context.rs +++ b/codex-rs/core/src/session/step_context.rs @@ -6,7 +6,10 @@ use crate::session::McpRuntimeSnapshot; use crate::session::turn_context::TurnContext; use codex_exec_server::ResolvedSelectedCapabilityRoot; use codex_mcp::ToolInfo; +use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; +use codex_protocol::protocol::TurnContextItem; use tokio::sync::OnceCell; /// Request-scoped state that may change between model sampling requests. @@ -66,4 +69,65 @@ impl StepContext { .map(|effort| effort.to_string()) .unwrap_or_else(|| "default".to_string()) } + + pub(crate) fn collaboration_mode(&self) -> CollaborationMode { + self.turn.collaboration_mode.with_updates( + /*model*/ None, + Some(self.reasoning_effort.clone()), + /*developer_instructions*/ None, + ) + } + + pub(crate) fn to_turn_context_item(&self) -> TurnContextItem { + let workspace_roots = self.turn.config.effective_workspace_roots(); + #[allow(deprecated)] + let cwd = self.turn.cwd.clone(); + TurnContextItem { + turn_id: Some(self.turn.sub_id.clone()), + cwd, + workspace_roots: (!workspace_roots.is_empty()).then_some(workspace_roots), + current_date: self.turn.current_date.clone(), + timezone: self.turn.timezone.clone(), + approval_policy: self.turn.approval_policy.value(), + sandbox_policy: self.turn.sandbox_policy(), + permission_profile: Some(self.turn.permission_profile()), + network: self.turn.turn_context_network_item(), + file_system_sandbox_policy: self.turn.non_legacy_file_system_sandbox_policy(), + model: self.turn.model_info.slug.clone(), + comp_hash: self.turn.model_info.comp_hash.clone(), + personality: self.turn.personality, + collaboration_mode: Some(self.collaboration_mode()), + multi_agent_version: Some(self.turn.multi_agent_version), + multi_agent_mode: super::multi_agents::effective_multi_agent_mode(self), + realtime_active: Some(self.turn.realtime_active), + effort: self.reasoning_effort.clone(), + summary: ReasoningSummaryConfig::Auto, + } + } +} + +/// Frozen step inputs needed by downstream work that can outlive a sampling request. +#[derive(Clone, Debug)] +pub(crate) struct StepContextSeed { + pub(crate) turn: Arc, + pub(crate) reasoning_effort: Option, +} + +impl StepContextSeed { + pub(crate) fn from_turn(turn: Arc) -> Self { + let reasoning_effort = turn.config.model_reasoning_effort.clone(); + Self { + turn, + reasoning_effort, + } + } +} + +impl From<&StepContext> for StepContextSeed { + fn from(step_context: &StepContext) -> Self { + Self { + turn: Arc::clone(&step_context.turn), + reasoning_effort: step_context.reasoning_effort.clone(), + } + } } diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 2495ccdb1b..2fde6d4b0c 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -205,6 +205,10 @@ impl StepContext { } } +pub(crate) fn turn_context_item_for_test(turn: &Arc) -> TurnContextItem { + StepContext::for_test(Arc::clone(turn)).to_turn_context_item() +} + mod guardian_tests; struct InstructionsTestCase { @@ -1134,6 +1138,7 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an let tool_ctx = crate::tools::sandboxing::ToolCtx { session: Arc::clone(&session), turn: Arc::clone(&turn), + step_context: StepContext::for_test(Arc::clone(&turn)), call_id: "probe-call".to_string(), tool_name: codex_tools::ToolName::plain("probe"), }; @@ -2790,9 +2795,10 @@ async fn start_new_context_window_assigns_and_persists_item_ids() { attach_thread_persistence(Arc::get_mut(&mut session).expect("unique session")).await; let world_state = Arc::new(build_world_state_from_turn_context(session.as_ref(), &turn_context).await); + let step_context = StepContext::for_test(Arc::clone(&turn_context)); session - .start_new_context_window(turn_context.as_ref(), world_state) + .start_new_context_window(step_context.as_ref(), world_state) .await; let live_history = session.clone_history().await; @@ -3176,7 +3182,7 @@ async fn thread_rollback_drops_last_turn_from_history() { full_history.extend(initial_context.clone()); full_history.extend(turn_1.clone()); full_history.extend(turn_2); - sess.replace_history(full_history.clone(), Some(tc.to_turn_context_item())) + sess.replace_history(full_history.clone(), Some(turn_context_item_for_test(&tc))) .await; let rollout_items: Vec = full_history .into_iter() @@ -3191,7 +3197,7 @@ async fn thread_rollback_drops_last_turn_from_history() { .await; { let mut state = sess.state.lock().await; - state.set_reference_context_item(Some(tc.to_turn_context_item())); + state.set_reference_context_item(Some(turn_context_item_for_test(&tc))); } handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await; @@ -3236,7 +3242,7 @@ async fn thread_rollback_clears_history_when_num_turns_exceeds_existing_turns() let mut full_history = Vec::new(); full_history.extend(initial_context.clone()); full_history.extend(turn_1); - sess.replace_history(full_history.clone(), Some(tc.to_turn_context_item())) + sess.replace_history(full_history.clone(), Some(turn_context_item_for_test(&tc))) .await; let rollout_items: Vec = full_history .into_iter() @@ -3283,7 +3289,7 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context ) .await; - let first_context_item = tc.to_turn_context_item(); + let first_context_item = turn_context_item_for_test(&tc); let first_turn_id = first_context_item .turn_id .clone() @@ -3405,7 +3411,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio ) .await; - let first_context_item = tc.to_turn_context_item(); + let first_context_item = turn_context_item_for_test(&tc); let first_turn_id = first_context_item .turn_id .clone() @@ -3547,7 +3553,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() { Arc::get_mut(&mut sess).expect("session should not have additional references"), ) .await; - let turn_context_item = tc.to_turn_context_item(); + let turn_context_item = turn_context_item_for_test(&tc); sess.persist_rollout_items(&[ RolloutItem::EventMsg(EventMsg::TurnStarted( @@ -5320,8 +5326,9 @@ async fn build_initial_context( turn_context: &Arc, ) -> Vec { let world_state = build_world_state_from_turn_context(session, turn_context).await; + let step_context = StepContext::for_test(Arc::clone(turn_context)); session - .build_initial_context_with_world_state(turn_context.as_ref(), &world_state) + .build_initial_context_with_world_state(step_context.as_ref(), &world_state) .await } @@ -6118,7 +6125,7 @@ async fn request_permissions_emits_event_when_granular_policy_allows_requests() .selection(); session .request_permissions_for_environment( - &turn_context, + &StepContextSeed::from_turn(Arc::clone(&turn_context)), call_id, codex_protocol::request_permissions::RequestPermissionsArgs { environment_id: None, @@ -6357,7 +6364,7 @@ async fn request_permissions_response_materializes_session_cwd_grants_before_rec .selection(); session .request_permissions_for_environment( - &turn_context, + &StepContextSeed::from_turn(Arc::clone(&turn_context)), call_id, codex_protocol::request_permissions::RequestPermissionsArgs { environment_id: None, @@ -6448,7 +6455,7 @@ async fn request_permissions_is_auto_denied_when_granular_policy_blocks_tool_req .selection(); let response = session .request_permissions_for_environment( - &turn_context, + &StepContextSeed::from_turn(Arc::clone(&turn_context)), call_id, codex_protocol::request_permissions::RequestPermissionsArgs { environment_id: None, @@ -8147,10 +8154,11 @@ async fn build_settings_update_items_emits_realtime_start_when_session_becomes_l ) .await; current_context.realtime_active = true; + let current_context = StepContext::for_test(Arc::new(current_context)); let update_items = session .build_settings_update_items( - Some(&previous_context.to_turn_context_item()), + Some(&turn_context_item_for_test(&previous_context)), ¤t_context, ) .await; @@ -8168,6 +8176,7 @@ async fn build_settings_update_items_emits_realtime_start_when_session_becomes_l async fn build_settings_update_items_emits_realtime_end_when_session_stops_being_live() { let (session, mut previous_context) = make_session_and_context().await; previous_context.realtime_active = true; + let previous_context = Arc::new(previous_context); let mut current_context = previous_context .with_model( previous_context.model_info.slug.clone(), @@ -8175,10 +8184,11 @@ async fn build_settings_update_items_emits_realtime_end_when_session_stops_being ) .await; current_context.realtime_active = false; + let current_context = StepContext::for_test(Arc::new(current_context)); let update_items = session .build_settings_update_items( - Some(&previous_context.to_turn_context_item()), + Some(&turn_context_item_for_test(&previous_context)), ¤t_context, ) .await; @@ -8195,7 +8205,8 @@ async fn build_settings_update_items_emits_realtime_end_when_session_stops_being #[tokio::test] async fn build_settings_update_items_uses_previous_turn_settings_for_realtime_end() { let (session, previous_context) = make_session_and_context().await; - let mut previous_context_item = previous_context.to_turn_context_item(); + let previous_context = Arc::new(previous_context); + let mut previous_context_item = turn_context_item_for_test(&previous_context); previous_context_item.realtime_active = None; let previous_turn_settings = PreviousTurnSettings { model: previous_context.model_info.slug.clone(), @@ -8209,6 +8220,7 @@ async fn build_settings_update_items_uses_previous_turn_settings_for_realtime_en ) .await; current_context.realtime_active = false; + let current_context = StepContext::for_test(Arc::new(current_context)); session .set_previous_turn_settings(Some(previous_turn_settings)) @@ -8241,7 +8253,7 @@ async fn build_initial_context_uses_previous_realtime_state() { "expected initial context to describe active realtime state, got {developer_texts:?}" ); - let previous_context_item = turn_context.to_turn_context_item(); + let previous_context_item = turn_context_item_for_test(&turn_context); { let mut state = session.state.lock().await; state.set_reference_context_item(Some(previous_context_item)); @@ -8398,9 +8410,9 @@ async fn record_context_updates_includes_turn_context_fragments_on_steady_state_ .insert(TurnContextExtensionTestState { expected_model_context_window: Some(50), }); - let mut previous_context_item = turn_context.to_turn_context_item(); - previous_context_item.turn_id = Some("previous-turn-id".to_string()); let turn_context = Arc::new(turn_context); + let mut previous_context_item = turn_context_item_for_test(&turn_context); + previous_context_item.turn_id = Some("previous-turn-id".to_string()); let world_state = build_world_state_from_turn_context(&session, &turn_context).await; { let mut state = session.state.lock().await; @@ -8974,14 +8986,16 @@ async fn turn_context_item_stores_local_cwd() { #[allow(deprecated)] let local_cwd = turn_context.cwd.clone(); - assert_eq!(turn_context.to_turn_context_item().cwd, local_cwd); + let turn_context = Arc::new(turn_context); + assert_eq!(turn_context_item_for_test(&turn_context).cwd, local_cwd); } #[tokio::test] async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() { let (_session, turn_context) = make_session_and_context().await; + let turn_context = Arc::new(turn_context); - let item = turn_context.to_turn_context_item(); + let item = turn_context_item_for_test(&turn_context); assert_eq!(item.file_system_sandbox_policy, None); assert_eq!( @@ -8990,6 +9004,27 @@ async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() ); } +#[tokio::test] +async fn turn_context_item_uses_step_reasoning_effort() { + let (_session, mut turn_context) = make_session_and_context().await; + Arc::make_mut(&mut turn_context.config).model_reasoning_effort = + Some(ReasoningEffortConfig::High); + let turn_context = Arc::new(turn_context); + let mut step_context = StepContext::for_test(Arc::clone(&turn_context)); + Arc::get_mut(&mut step_context) + .expect("step context should be uniquely owned") + .reasoning_effort = Some(ReasoningEffortConfig::Low); + + let item = step_context.to_turn_context_item(); + assert_eq!(item.effort, Some(ReasoningEffortConfig::Low)); + assert_eq!( + item.collaboration_mode + .expect("turn context should have collaboration mode") + .reasoning_effort(), + Some(ReasoningEffortConfig::Low) + ); +} + #[tokio::test] async fn turn_context_item_stores_split_file_system_sandbox_policy_when_different() { let (_session, mut turn_context) = make_session_and_context().await; @@ -9000,7 +9035,8 @@ async fn turn_context_item_stores_split_file_system_sandbox_policy_when_differen turn_context.network_sandbox_policy(), ); - let item = turn_context.to_turn_context_item(); + let turn_context = Arc::new(turn_context); + let item = turn_context_item_for_test(&turn_context); assert_eq!( item.file_system_sandbox_policy, @@ -9028,7 +9064,7 @@ async fn record_context_updates_and_set_reference_context_item_injects_full_cont let current_context = session.reference_context_item().await; assert_eq!( serde_json::to_value(current_context).expect("serialize current context item"), - serde_json::to_value(Some(turn_context.to_turn_context_item())) + serde_json::to_value(Some(step_context.to_turn_context_item())) .expect("serialize expected context item") ); } @@ -9080,8 +9116,8 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co async fn record_context_updates_and_set_reference_context_item_persists_baseline_without_emitting_diffs() { let (mut session, turn_context) = make_session_and_context().await; - let previous_context_item = turn_context.to_turn_context_item(); let previous_context = Arc::new(turn_context); + let previous_context_item = turn_context_item_for_test(&previous_context); let world_state = build_world_state_from_turn_context(&session, &previous_context).await; let mut turn_context = Arc::try_unwrap(previous_context) .unwrap_or_else(|_| panic!("previous turn context should have no remaining references")); @@ -9095,13 +9131,14 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline } let rollout_path = attach_thread_persistence(&mut session).await; + let turn_context = Arc::new(turn_context); + let step_context = StepContext::for_test(Arc::clone(&turn_context)); + let update_items = session - .build_settings_update_items(Some(&previous_context_item), &turn_context) + .build_settings_update_items(Some(&previous_context_item), &step_context) .await; assert_eq!(update_items, Vec::new()); - let turn_context = Arc::new(turn_context); - let step_context = StepContext::for_test(Arc::clone(&turn_context)); session .record_context_updates_and_set_reference_context_item(&step_context) .await; @@ -9113,7 +9150,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline assert_eq!( serde_json::to_value(session.reference_context_item().await) .expect("serialize current context item"), - serde_json::to_value(Some(turn_context.to_turn_context_item())) + serde_json::to_value(Some(step_context.to_turn_context_item())) .expect("serialize expected context item") ); session.ensure_rollout_materialized().await; @@ -9132,7 +9169,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline assert_eq!( serde_json::to_value(persisted_turn_context) .expect("serialize persisted turn context item"), - serde_json::to_value(Some(turn_context.to_turn_context_item())) + serde_json::to_value(Some(step_context.to_turn_context_item())) .expect("serialize expected turn context item") ); } @@ -9258,7 +9295,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei assert_eq!( serde_json::to_value(persisted_turn_context) .expect("serialize persisted turn context item"), - serde_json::to_value(Some(turn_context.to_turn_context_item())) + serde_json::to_value(Some(step_context.to_turn_context_item())) .expect("serialize expected turn context item") ); } diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index d13239d810..9d9472e89a 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -4,6 +4,7 @@ use crate::exec_policy::ExecPolicyManager; use crate::guardian::GUARDIAN_REVIEWER_NAME; use crate::sandboxing::SandboxPermissions; use crate::session::step_context::StepContext; +use crate::session::step_context::StepContextSeed; use crate::test_support::models_manager_with_provider; use crate::tools::context::ToolCallSource; use crate::tools::context::ToolOutput; @@ -132,7 +133,7 @@ async fn request_permissions_routes_to_guardian_when_reviewer_is_enabled() { let response = tokio::time::timeout( Duration::from_secs(45), session.request_permissions_for_environment( - &turn_context, + &StepContextSeed::from_turn(Arc::clone(&turn_context)), "perm-call-1".to_string(), RequestPermissionsArgs { environment_id: None, @@ -227,7 +228,7 @@ async fn request_permissions_guardian_review_stops_when_cancelled() { .selection(); session .request_permissions_for_environment( - &turn_context, + &StepContextSeed::from_turn(Arc::clone(&turn_context)), "perm-call-cancelled".to_string(), RequestPermissionsArgs { environment_id: None, @@ -531,12 +532,13 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message turn_context.session_source = guardian_source; turn_context.developer_instructions = Some(guardian_policy.clone()); let turn_context = Arc::new(turn_context); + let step_context = StepContext::for_test(Arc::clone(&turn_context)); let world_state = Arc::new(build_world_state_from_turn_context(&session, &turn_context).await); let initial_context_injection = InitialContextInjection::BeforeLastUserMessage(world_state); let (refreshed, _) = crate::compact_remote::process_compacted_history( &session, - &turn_context, + &step_context, vec![ ResponseItem::Message { id: None, diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index f2f0174d86..e87157c683 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -204,7 +204,7 @@ pub(crate) async fn run_turn( .await; } - track_turn_resolved_config_analytics(&sess, &turn_context, &input).await; + track_turn_resolved_config_analytics(&sess, first_step_context.as_ref(), &input).await; let mut last_agent_message: Option = None; let mut stop_hook_active = false; @@ -742,9 +742,10 @@ async fn build_extension_turn_input_items( )] async fn track_turn_resolved_config_analytics( sess: &Session, - turn_context: &TurnContext, + step_context: &StepContext, input: &[TurnInput], ) { + let turn_context = step_context.turn.as_ref(); let thread_config = { let state = sess.state.lock().await; state.session_configuration.thread_config_snapshot() @@ -777,7 +778,7 @@ async fn track_turn_resolved_config_analytics( permission_profile: turn_context.permission_profile(), #[allow(deprecated)] permission_profile_cwd: turn_context.cwd.to_path_buf(), - reasoning_effort: turn_context.config.model_reasoning_effort.clone(), + reasoning_effort: step_context.reasoning_effort.clone(), reasoning_summary: Some(turn_context.reasoning_summary), service_tier: turn_context .config @@ -1935,7 +1936,7 @@ async fn try_run_sampling_request( prompt: &Prompt, cancellation_token: CancellationToken, ) -> CodexResult { - let turn_context = &step_context.turn; + let turn_context = Arc::clone(&step_context.turn); feedback_tags!( model = turn_context.model_info.slug.clone(), approval_policy = turn_context.approval_policy.value(), diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index a90c3b4534..4cc970d3aa 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -168,19 +168,17 @@ impl TurnContext { ) } - pub(crate) fn effective_reasoning_effort(&self) -> Option { - if self.model_info.supports_reasoning_summaries { + /// Resolves effort for the turn span, which is created before the first step is captured. + pub(crate) fn turn_start_reasoning_effort_for_tracing(&self) -> String { + let reasoning_effort = if self.model_info.supports_reasoning_summaries { self.config .model_reasoning_effort .clone() .or_else(|| self.model_info.default_reasoning_level.clone()) } else { None - } - } - - pub(crate) fn effective_reasoning_effort_for_tracing(&self) -> String { - self.effective_reasoning_effort() + }; + reasoning_effort .map(|effort| effort.to_string()) .unwrap_or_else(|| "default".to_string()) } @@ -333,7 +331,7 @@ impl TurnContext { } } - fn non_legacy_file_system_sandbox_policy(&self) -> Option { + pub(super) fn non_legacy_file_system_sandbox_policy(&self) -> Option { // Omit the derived split filesystem policy when it is equivalent to // the legacy sandbox policy. This keeps turn-context payloads stable // while both fields exist; once callers consume only the split policy, @@ -349,34 +347,7 @@ impl TurnContext { .then_some(file_system_sandbox_policy) } - pub(crate) fn to_turn_context_item(&self) -> TurnContextItem { - let workspace_roots = self.config.effective_workspace_roots(); - #[allow(deprecated)] - let cwd = self.cwd.clone(); - TurnContextItem { - turn_id: Some(self.sub_id.clone()), - cwd, - workspace_roots: (!workspace_roots.is_empty()).then_some(workspace_roots), - current_date: self.current_date.clone(), - timezone: self.timezone.clone(), - approval_policy: self.approval_policy.value(), - sandbox_policy: self.sandbox_policy(), - permission_profile: Some(self.permission_profile()), - network: self.turn_context_network_item(), - file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(), - model: self.model_info.slug.clone(), - comp_hash: self.model_info.comp_hash.clone(), - personality: self.personality, - collaboration_mode: Some(self.collaboration_mode.clone()), - multi_agent_version: Some(self.multi_agent_version), - multi_agent_mode: super::multi_agents::effective_multi_agent_mode(self), - realtime_active: Some(self.realtime_active), - effort: self.config.model_reasoning_effort.clone(), - summary: ReasoningSummaryConfig::Auto, - } - } - - fn turn_context_network_item(&self) -> Option { + pub(super) fn turn_context_network_item(&self) -> Option { let network = self .config .config_layer_stack diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index c6d9d2990c..0bffb19b15 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -383,7 +383,7 @@ impl Session { let task_cancellation_token = cancellation_token.child_token(); // Task-owned turn spans keep a core-owned span open for the // full task lifecycle after the submission dispatch span ends. - let reasoning_effort = turn_context.effective_reasoning_effort_for_tracing(); + let reasoning_effort = turn_context.turn_start_reasoning_effort_for_tracing(); let task_span = info_span!( "turn", otel.name = span_name, diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 3c1d0146e7..655e5748f5 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -4,6 +4,7 @@ use crate::init_state_db; use crate::installation_id::INSTALLATION_ID_FILENAME; use crate::rollout::RolloutRecorder; use crate::session::session::SessionSettingsUpdate; +use crate::session::step_context::StepContext; use crate::session::tests::build_world_state_from_turn_context; use crate::session::tests::make_session_and_context; use crate::tasks::InterruptedTurnHistoryMarker; @@ -319,9 +320,10 @@ fn out_of_range_truncation_drops_pre_user_active_turn_prefix() { async fn ignores_session_prefix_messages_when_truncating() { let (session, turn_context) = make_session_and_context().await; let turn_context = Arc::new(turn_context); + let step_context = StepContext::for_test(Arc::clone(&turn_context)); let world_state = build_world_state_from_turn_context(&session, &turn_context).await; let mut items = session - .build_initial_context_with_world_state(&turn_context, &world_state) + .build_initial_context_with_world_state(&step_context, &world_state) .await; items.push(user_msg("feature request")); items.push(assistant_msg("ack")); diff --git a/codex-rs/core/src/thread_rollout_truncation_tests.rs b/codex-rs/core/src/thread_rollout_truncation_tests.rs index 16d10a8277..94e0ce01d4 100644 --- a/codex-rs/core/src/thread_rollout_truncation_tests.rs +++ b/codex-rs/core/src/thread_rollout_truncation_tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::session::step_context::StepContext; use crate::session::tests::build_world_state_from_turn_context; use crate::session::tests::make_session_and_context; use codex_protocol::AgentPath; @@ -270,9 +271,10 @@ fn truncates_rollout_from_start_applies_thread_rollback_markers() { async fn ignores_session_prefix_messages_when_truncating_rollout_from_start() { let (session, turn_context) = make_session_and_context().await; let turn_context = Arc::new(turn_context); + let step_context = StepContext::for_test(Arc::clone(&turn_context)); let world_state = build_world_state_from_turn_context(&session, &turn_context).await; let mut items = session - .build_initial_context_with_world_state(&turn_context, &world_state) + .build_initial_context_with_world_state(&step_context, &world_state) .await; items.push(user_msg("feature request")); items.push(assistant_msg("ack")); diff --git a/codex-rs/core/src/tools/handlers/agent_jobs.rs b/codex-rs/core/src/tools/handlers/agent_jobs.rs index 6405414731..3fa2ace0e3 100644 --- a/codex-rs/core/src/tools/handlers/agent_jobs.rs +++ b/codex-rs/core/src/tools/handlers/agent_jobs.rs @@ -3,6 +3,7 @@ use crate::agent::status::is_final; use crate::config::Config; use crate::function_tool::FunctionCallError; use crate::session::session::Session; +use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::tools::handlers::multi_agents::build_agent_spawn_config; use crate::tools::handlers::parse_arguments; @@ -107,9 +108,10 @@ fn required_state_db( async fn build_runner_options( session: &Arc, - turn: &Arc, + step_context: &StepContext, requested_concurrency: Option, ) -> Result { + let turn = &step_context.turn; let multi_agent_version = turn.multi_agent_version; if multi_agent_version == MultiAgentVersion::Disabled { return Err(FunctionCallError::RespondToModel( @@ -124,7 +126,7 @@ async fn build_runner_options( } let max_concurrency = normalize_concurrency(requested_concurrency, agent_max_threads); let base_instructions = session.get_base_instructions().await; - let spawn_config = build_agent_spawn_config(&base_instructions, turn.as_ref())?; + let spawn_config = build_agent_spawn_config(&base_instructions, step_context)?; Ok(JobRunnerOptions { max_concurrency, spawn_config, diff --git a/codex-rs/core/src/tools/handlers/agent_jobs/spawn_agents_on_csv.rs b/codex-rs/core/src/tools/handlers/agent_jobs/spawn_agents_on_csv.rs index a519e5db1b..ed665e2449 100644 --- a/codex-rs/core/src/tools/handlers/agent_jobs/spawn_agents_on_csv.rs +++ b/codex-rs/core/src/tools/handlers/agent_jobs/spawn_agents_on_csv.rs @@ -35,7 +35,7 @@ impl SpawnAgentsOnCsvHandler { ) -> Result, FunctionCallError> { let ToolInvocation { session, - turn, + step_context, payload, .. } = invocation; @@ -49,7 +49,7 @@ impl SpawnAgentsOnCsvHandler { } }; - handle(session, turn, arguments) + handle(session, step_context, arguments) .await .map(boxed_tool_output) } @@ -68,9 +68,10 @@ impl CoreToolRuntime for SpawnAgentsOnCsvHandler { /// `report_agent_job_result`, then exported to CSV on completion. pub async fn handle( session: Arc, - turn: Arc, + step_context: Arc, arguments: String, ) -> Result { + let turn = Arc::clone(&step_context.turn); let args: SpawnAgentsOnCsvArgs = parse_arguments(arguments.as_str())?; if args.instruction.trim().is_empty() { return Err(FunctionCallError::RespondToModel( @@ -182,16 +183,17 @@ pub async fn handle( })?; let requested_concurrency = args.max_concurrency.or(args.max_workers); - let options = match build_runner_options(&session, &turn, requested_concurrency).await { - Ok(options) => options, - Err(err) => { - let error_message = err.to_string(); - let _ = db - .mark_agent_job_failed(job_id.as_str(), error_message.as_str()) - .await; - return Err(err); - } - }; + let options = + match build_runner_options(&session, step_context.as_ref(), requested_concurrency).await { + Ok(options) => options, + Err(err) => { + let error_message = err.to_string(); + let _ = db + .mark_agent_job_failed(job_id.as_str(), error_message.as_str()) + .await; + return Err(err); + } + }; db.mark_agent_job_running(job_id.as_str()) .await .map_err(|err| { diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 6326dbc1ad..0b21d6bad1 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -11,6 +11,7 @@ use crate::apply_patch::InternalApplyPatchInvocation; use crate::apply_patch::convert_apply_patch_to_protocol; use crate::function_tool::FunctionCallError; use crate::session::session::Session; +use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::session::turn_context::TurnEnvironment; use crate::tools::context::ApplyPatchToolOutput; @@ -445,6 +446,7 @@ impl ApplyPatchHandler { let tool_ctx = ToolCtx { session: session.clone(), turn: turn.clone(), + step_context: step_context.clone(), call_id: call_id.clone(), tool_name: tool_name.clone(), }; @@ -550,6 +552,7 @@ pub(crate) async fn intercept_apply_patch( turn_environment: TurnEnvironment, session: Arc, turn: Arc, + step_context: Arc, tracker: Option<&SharedTurnDiffTracker>, call_id: &str, tool_name: &str, @@ -608,6 +611,7 @@ pub(crate) async fn intercept_apply_patch( let tool_ctx = ToolCtx { session: session.clone(), turn: turn.clone(), + step_context, call_id: call_id.to_string(), tool_name: ToolName::plain(tool_name), }; diff --git a/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs b/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs index 8e97b2afbd..572003de87 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs @@ -1,7 +1,7 @@ use super::*; use crate::agent::next_thread_spawn_depth; use crate::session::session::Session; -use crate::session::turn_context::TurnContext; +use crate::session::step_context::StepContext; use crate::tools::handlers::multi_agents_spec::create_resume_agent_tool; use codex_tools::ToolSpec; use std::sync::Arc; @@ -35,6 +35,7 @@ async fn handle_resume_agent( let ToolInvocation { session, turn, + step_context, payload, call_id, .. @@ -87,7 +88,7 @@ async fn handle_resume_agent( let (receiver_agent, error) = if matches!(status, AgentStatus::NotFound) { match Box::pin(try_resume_closed_agent( &session, - &turn, + step_context.as_ref(), receiver_thread_id, child_depth, )) @@ -187,11 +188,12 @@ impl ToolOutput for ResumeAgentResult { async fn try_resume_closed_agent( session: &Arc, - turn: &Arc, + step_context: &StepContext, receiver_thread_id: ThreadId, child_depth: i32, ) -> Result<(), FunctionCallError> { - let config = build_agent_resume_config(turn.as_ref())?; + let turn = step_context.turn.as_ref(); + let config = build_agent_resume_config(step_context)?; Box::pin(session.services.agent_control.resume_agent_from_rollout( config, receiver_thread_id, diff --git a/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs b/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs index 649eaf1ce0..0c657ea490 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs @@ -34,6 +34,7 @@ impl Handler { let ToolInvocation { session, turn, + step_context, payload, call_id, .. @@ -48,7 +49,7 @@ impl Handler { .agent_control .get_agent_metadata(receiver_thread_id); if receiver_agent.is_some() { - let resume_config = build_agent_resume_config(turn.as_ref())?; + let resume_config = build_agent_resume_config(step_context.as_ref())?; session .services .agent_control diff --git a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs index e5923bab79..b28d767fc5 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -48,6 +48,7 @@ async fn handle_spawn_agent( let ToolInvocation { session, turn, + step_context, payload, call_id, .. @@ -86,8 +87,10 @@ async fn handle_spawn_agent( }), ) .await; - let mut config = - build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?; + let mut config = build_agent_spawn_config( + &session.get_base_instructions().await, + step_context.as_ref(), + )?; if let Some(service_tier) = args.service_tier.as_ref() { config.service_tier = Some(service_tier.clone()); } diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index 792cfd4090..1d3ec6810a 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -3,6 +3,7 @@ use crate::config::DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS; use crate::config::HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS; use crate::function_tool::FunctionCallError; use crate::session::session::Session; +use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolOutput; @@ -154,34 +155,36 @@ pub(crate) fn parse_collab_input( /// Builds the base config snapshot for a newly spawned sub-agent. /// /// The returned config starts from the parent's effective config and then refreshes the -/// runtime-owned fields carried on `turn`, including model selection, reasoning settings, +/// runtime-owned fields carried on the turn and step contexts, including model selection, reasoning settings, /// approval policy, sandbox, and cwd. Role-specific overrides are layered after this step; /// skipping this helper and cloning stale config state directly can send the child agent out with /// the wrong provider or runtime policy. pub(crate) fn build_agent_spawn_config( base_instructions: &BaseInstructions, - turn: &TurnContext, + step_context: &StepContext, ) -> Result { - let mut config = build_agent_shared_config(turn)?; + let mut config = build_agent_shared_config(step_context)?; config.base_instructions = Some(base_instructions.text.clone()); Ok(config) } -pub(crate) fn build_agent_resume_config(turn: &TurnContext) -> Result { - let mut config = build_agent_shared_config(turn)?; +pub(crate) fn build_agent_resume_config( + step_context: &StepContext, +) -> Result { + let mut config = build_agent_shared_config(step_context)?; // For resume, keep base instructions sourced from rollout/session metadata. config.base_instructions = None; Ok(config) } -fn build_agent_shared_config(turn: &TurnContext) -> Result { +fn build_agent_shared_config(step_context: &StepContext) -> Result { + let turn = step_context.turn.as_ref(); let base_config = turn.config.clone(); let mut config = (*base_config).clone(); config.model = Some(turn.model_info.slug.clone()); config.model_provider = turn.provider.info().clone(); - config.model_reasoning_effort = turn - .config - .model_reasoning_effort + config.model_reasoning_effort = step_context + .reasoning_effort .clone() .or_else(|| turn.model_info.default_reasoning_level.clone()); config.model_reasoning_summary = Some(turn.reasoning_summary); diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 6060e9e7d4..56804e5022 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -4487,6 +4487,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() { ..ShellEnvironmentPolicy::default() }; config.codex_linux_sandbox_exe = Some(PathBuf::from("/bin/echo")); + config.model_reasoning_effort = Some(ReasoningEffort::High); turn.config = Arc::new(config); let temp_dir = tempfile::tempdir().expect("temp dir"); #[allow(deprecated)] @@ -4513,12 +4514,18 @@ async fn build_agent_spawn_config_uses_turn_context_values() { .set(AskForApproval::OnRequest) .expect("approval policy set"); - let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config"); + let turn = Arc::new(turn); + let mut step_context = StepContext::for_test(Arc::clone(&turn)); + Arc::get_mut(&mut step_context) + .expect("step context should be uniquely owned") + .reasoning_effort = Some(ReasoningEffort::Low); + + let config = build_agent_spawn_config(&base_instructions, &step_context).expect("spawn config"); let mut expected = (*turn.config).clone(); expected.base_instructions = Some(base_instructions.text); expected.model = Some(turn.model_info.slug.clone()); expected.model_provider = turn.provider.info().clone(); - expected.model_reasoning_effort = turn.config.model_reasoning_effort.clone(); + expected.model_reasoning_effort = Some(ReasoningEffort::Low); expected.model_reasoning_summary = Some(turn.reasoning_summary); expected.developer_instructions = turn.developer_instructions.clone(); #[allow(deprecated)] @@ -4542,18 +4549,25 @@ async fn build_agent_resume_config_clears_base_instructions() { let (_session, mut turn) = make_session_and_context().await; let mut base_config = (*turn.config).clone(); base_config.base_instructions = Some("caller-base".to_string()); + base_config.model_reasoning_effort = Some(ReasoningEffort::High); turn.config = Arc::new(base_config); turn.approval_policy .set(AskForApproval::OnRequest) .expect("approval policy set"); - let config = build_agent_resume_config(&turn).expect("resume config"); + let turn = Arc::new(turn); + let mut step_context = StepContext::for_test(Arc::clone(&turn)); + Arc::get_mut(&mut step_context) + .expect("step context should be uniquely owned") + .reasoning_effort = Some(ReasoningEffort::Low); + + let config = build_agent_resume_config(&step_context).expect("resume config"); let mut expected = (*turn.config).clone(); expected.base_instructions = None; expected.model = Some(turn.model_info.slug.clone()); expected.model_provider = turn.provider.info().clone(); - expected.model_reasoning_effort = turn.config.model_reasoning_effort.clone(); + expected.model_reasoning_effort = Some(ReasoningEffort::Low); expected.model_reasoning_summary = Some(turn.reasoning_summary); expected.developer_instructions = turn.developer_instructions.clone(); #[allow(deprecated)] diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs index 62defe65c4..14515872b2 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs @@ -67,6 +67,7 @@ pub(crate) async fn handle_message_string_tool( let ToolInvocation { session, turn, + step_context, call_id, .. } = invocation; @@ -89,7 +90,7 @@ pub(crate) async fn handle_message_string_tool( let receiver_agent_path = receiver_agent.agent_path.clone().ok_or_else(|| { FunctionCallError::RespondToModel("target agent is missing an agent_path".to_string()) })?; - let resume_config = build_agent_resume_config(turn.as_ref())?; + let resume_config = build_agent_resume_config(step_context.as_ref())?; session .services .agent_control diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index 1c68849c5e..23a7934f08 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -43,6 +43,7 @@ async fn handle_spawn_agent( let ToolInvocation { session, turn, + step_context, payload, call_id, .. @@ -59,8 +60,10 @@ async fn handle_spawn_agent( let message = message_content(args.message)?; let session_source = turn.session_source.clone(); let child_depth = next_thread_spawn_depth(&session_source); - let mut config = - build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?; + let mut config = build_agent_spawn_config( + &session.get_base_instructions().await, + step_context.as_ref(), + )?; if let Some(service_tier) = args.service_tier.as_ref() { config.service_tier = Some(service_tier.clone()); } diff --git a/codex-rs/core/src/tools/handlers/request_permissions.rs b/codex-rs/core/src/tools/handlers/request_permissions.rs index 9fcb1c642c..9a6fac3d28 100644 --- a/codex-rs/core/src/tools/handlers/request_permissions.rs +++ b/codex-rs/core/src/tools/handlers/request_permissions.rs @@ -2,6 +2,7 @@ use codex_protocol::request_permissions::RequestPermissionsArgs; use codex_sandboxing::policy_transforms::normalize_additional_permissions; use crate::function_tool::FunctionCallError; +use crate::session::step_context::StepContextSeed; use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; @@ -46,7 +47,6 @@ impl RequestPermissionsHandler { ) -> Result, FunctionCallError> { let ToolInvocation { session, - turn, step_context, cancellation_token, call_id, @@ -94,7 +94,7 @@ impl RequestPermissionsHandler { let response = session .request_permissions_for_environment( - &turn, + &StepContextSeed::from(step_context.as_ref()), call_id, args, turn_environment.selection(), diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 87a9301093..2e79401a74 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -7,6 +7,7 @@ use tokio_util::sync::CancellationToken; use crate::exec::ExecParams; use crate::exec_policy::ExecApprovalRequest; use crate::function_tool::FunctionCallError; +use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::session::turn_context::TurnEnvironment; use crate::shell::ShellType; @@ -54,6 +55,7 @@ struct RunExecLikeArgs { prefix_rule: Option>, session: Arc, turn: Arc, + step_context: Arc, turn_environment: TurnEnvironment, tracker: crate::tools::context::SharedTurnDiffTracker, call_id: String, @@ -71,6 +73,7 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result Result Result, } enum ActiveNetworkApprovalAttribution { @@ -298,6 +302,7 @@ impl NetworkApprovalService { command: String, environment_id: String, cancellation_token: CancellationToken, + reasoning_effort: Option, ) { let mut calls = self.calls.lock().await; let key = registration_id.clone(); @@ -310,6 +315,7 @@ impl NetworkApprovalService { command, environment_id, cancellation_token, + reasoning_effort, }), ); } @@ -632,9 +638,16 @@ impl NetworkApprovalService { let use_guardian = routes_approval_to_guardian(&turn_context); let guardian_review_id = use_guardian.then(new_guardian_review_id); let approval_decision = if let Some(review_id) = guardian_review_id.clone() { + let step_context_seed = owner_call.as_ref().map_or_else( + || StepContextSeed::from_turn(Arc::clone(&turn_context)), + |call| StepContextSeed { + turn: Arc::clone(&turn_context), + reasoning_effort: call.reasoning_effort.clone(), + }, + ); review_approval_request( &session, - &turn_context, + &step_context_seed, review_id, GuardianApprovalRequest::NetworkAccess { id: guardian_approval_id.clone(), @@ -849,7 +862,7 @@ pub(crate) fn build_network_policy_decider( pub(crate) async fn begin_network_approval( session: &Session, - turn_id: &str, + step_context: &StepContext, managed_network_active: bool, selected_sandbox: SandboxType, spec: Option, @@ -890,11 +903,12 @@ pub(crate) async fn begin_network_approval( .network_approval .register_call( registration_id.clone(), - turn_id.to_string(), + step_context.turn.sub_id.clone(), trigger, command, environment_id, cancellation_token.clone(), + step_context.reasoning_effort.clone(), ) .await; diff --git a/codex-rs/core/src/tools/network_approval_tests.rs b/codex-rs/core/src/tools/network_approval_tests.rs index 4fdeb50ed7..5190283374 100644 --- a/codex-rs/core/src/tools/network_approval_tests.rs +++ b/codex-rs/core/src/tools/network_approval_tests.rs @@ -313,6 +313,7 @@ async fn register_call_with_default_shell_trigger( "curl https://example.com".to_string(), "local".to_string(), cancellation_token.clone(), + /*reasoning_effort*/ None, ) .await; cancellation_token @@ -340,6 +341,7 @@ async fn active_call_preserves_triggering_command_context() { "curl https://example.com".to_string(), "remote".to_string(), CancellationToken::new(), + /*reasoning_effort*/ None, ) .await; diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index f82ad5bb70..2555cc9714 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -13,6 +13,7 @@ use crate::guardian::review_approval_request; use crate::guardian::routes_approval_to_guardian; use crate::hook_runtime::run_permission_request_hooks; use crate::network_policy_decision::network_approval_context_from_payload; +use crate::session::step_context::StepContextSeed; use crate::tools::flat_tool_name; use crate::tools::network_approval::ActiveNetworkApproval; use crate::tools::network_approval::DeferredNetworkApproval; @@ -71,7 +72,7 @@ impl ToolOrchestrator { { let network_approval = match begin_network_approval( &tool_ctx.session, - &tool_ctx.turn.sub_id, + tool_ctx.step_context.as_ref(), managed_network_active, attempt.sandbox, tool.network_approval_spec(req, tool_ctx), @@ -85,6 +86,7 @@ impl ToolOrchestrator { let attempt_tool_ctx = ToolCtx { session: tool_ctx.session.clone(), turn: tool_ctx.turn.clone(), + step_context: tool_ctx.step_context.clone(), call_id: tool_ctx.call_id.clone(), tool_name: tool_ctx.tool_name.clone(), }; @@ -172,6 +174,7 @@ impl ToolOrchestrator { let approval_ctx = ApprovalCtx { session: &tool_ctx.session, turn: &tool_ctx.turn, + step_context: &tool_ctx.step_context, call_id: &tool_ctx.call_id, guardian_review_id: guardian_review_id.clone(), retry_reason: None, @@ -207,6 +210,7 @@ impl ToolOrchestrator { let approval_ctx = ApprovalCtx { session: &tool_ctx.session, turn: &tool_ctx.turn, + step_context: &tool_ctx.step_context, call_id: &tool_ctx.call_id, guardian_review_id: guardian_review_id.clone(), retry_reason: reason.clone(), @@ -404,6 +408,7 @@ impl ToolOrchestrator { let approval_ctx = ApprovalCtx { session: &tool_ctx.session, turn: &tool_ctx.turn, + step_context: &tool_ctx.step_context, call_id: &tool_ctx.call_id, guardian_review_id: guardian_review_id.clone(), retry_reason: Some(retry_reason), @@ -579,7 +584,7 @@ impl ToolOrchestrator { Ok(action) => { review_approval_request( approval_ctx.session, - approval_ctx.turn, + &StepContextSeed::from(approval_ctx.step_context.as_ref()), review_id, action, approval_ctx.retry_reason.clone(), diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 10cb460205..b8c7d95a90 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -13,6 +13,7 @@ use crate::hook_runtime::run_permission_request_hooks; use crate::sandboxing::ExecOptions; use crate::sandboxing::ExecRequest; use crate::sandboxing::SandboxPermissions; +use crate::session::step_context::StepContextSeed; use crate::shell::ShellType; use crate::tools::runtimes::build_sandbox_command; use crate::tools::runtimes::exec_env_for_sandbox_permissions; @@ -38,6 +39,7 @@ use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::PermissionProfile; +use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; @@ -236,6 +238,7 @@ pub(super) async fn try_run_zsh_fork( policy: Arc::clone(&exec_policy), session: Arc::clone(&ctx.session), turn: Arc::clone(&ctx.turn), + reasoning_effort: ctx.step_context.reasoning_effort.clone(), call_id: ctx.call_id.clone(), environment_id: req.turn_environment.environment_id.clone(), tool_name: GuardianCommandSource::Shell, @@ -320,6 +323,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( policy: Arc::clone(&exec_policy), session: Arc::clone(&ctx.session), turn: Arc::clone(&ctx.turn), + reasoning_effort: ctx.step_context.reasoning_effort.clone(), call_id: ctx.call_id.clone(), environment_id: req.turn_environment.environment_id.clone(), tool_name: GuardianCommandSource::UnifiedExec, @@ -355,6 +359,7 @@ struct CoreShellActionProvider { policy: Arc>, session: Arc, turn: Arc, + reasoning_effort: Option, call_id: String, environment_id: String, tool_name: GuardianCommandSource, @@ -451,6 +456,10 @@ impl CoreShellActionProvider { let workdir = workdir.clone(); let session = self.session.clone(); let turn = self.turn.clone(); + let step_context_seed = StepContextSeed { + turn: Arc::clone(&turn), + reasoning_effort: self.reasoning_effort.clone(), + }; let call_id = self.call_id.clone(); let approval_id = Some(Uuid::new_v4().to_string()); let environment_id = Some(self.environment_id.clone()); @@ -493,7 +502,7 @@ impl CoreShellActionProvider { if let Some(review_id) = guardian_review_id.clone() { let decision = review_approval_request( &session, - &turn, + &step_context_seed, review_id.clone(), GuardianApprovalRequest::Execve { id: call_id.clone(), diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index 5478c1d946..5afb5d6a7e 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -425,6 +425,7 @@ async fn preapproved_additional_permissions_escalate_intercepted_exec() -> anyho policy: Arc::new(RwLock::new(codex_execpolicy::Policy::empty())), session: Arc::new(session), turn: Arc::new(turn_context), + reasoning_effort: None, call_id: "preapproved-additional-permissions".to_string(), environment_id: "local".to_string(), tool_name: GuardianCommandSource::Shell, @@ -561,6 +562,7 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul policy: std::sync::Arc::new(RwLock::new(codex_execpolicy::Policy::empty())), session: std::sync::Arc::new(session), turn: std::sync::Arc::new(turn_context), + reasoning_effort: None, call_id: "execve-hook-call".to_string(), environment_id: "local".to_string(), tool_name: GuardianCommandSource::Shell, @@ -772,6 +774,7 @@ prefix_rule(pattern = ["{cat_path_literal}"], decision = "allow") policy: Arc::new(RwLock::new(policy)), session: Arc::new(session), turn: Arc::new(turn_context), + reasoning_effort: None, call_id: "deny-read-prefix-allow".to_string(), environment_id: "local".to_string(), tool_name: GuardianCommandSource::Shell, @@ -809,6 +812,7 @@ async fn denied_reads_keep_granular_sandbox_rejection_for_escalation() -> anyhow policy: Arc::new(RwLock::new(PolicyParser::new().build())), session: Arc::new(session), turn: Arc::new(turn_context), + reasoning_effort: None, call_id: "deny-read-granular-sandbox-reject".to_string(), environment_id: "local".to_string(), tool_name: GuardianCommandSource::Shell, diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 571c5ce6df..61be4cf75a 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -7,6 +7,7 @@ use crate::sandboxing::ExecOptions; use crate::sandboxing::SandboxPermissions; use crate::session::session::Session; +use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::state::SessionServices; use crate::tools::hook_names::HookToolName; @@ -121,6 +122,7 @@ where pub(crate) struct ApprovalCtx<'a> { pub session: &'a Arc, pub turn: &'a Arc, + pub step_context: &'a Arc, pub call_id: &'a str, /// Guardian review lifecycle ID for this approval, when guardian is reviewing it. /// @@ -384,6 +386,7 @@ pub(crate) trait Sandboxable { pub(crate) struct ToolCtx { pub session: Arc, pub turn: Arc, + pub step_context: Arc, pub call_id: String, pub tool_name: ToolName, } diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 34d88c8149..cfc10739f7 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -38,6 +38,7 @@ use tokio::sync::Mutex; use crate::sandboxing::SandboxPermissions; use crate::session::session::Session; +use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::session::turn_context::TurnEnvironment; use crate::shell::ShellType; @@ -75,14 +76,21 @@ pub(crate) const MAX_UNIFIED_EXEC_PROCESSES: usize = 64; pub(crate) struct UnifiedExecContext { pub session: Arc, pub turn: Arc, + pub step_context: Arc, pub call_id: String, } impl UnifiedExecContext { - pub fn new(session: Arc, turn: Arc, call_id: String) -> Self { + pub fn new( + session: Arc, + turn: Arc, + step_context: Arc, + call_id: String, + ) -> Self { Self { session, turn, + step_context, call_id, } } diff --git a/codex-rs/core/src/unified_exec/mod_tests.rs b/codex-rs/core/src/unified_exec/mod_tests.rs index 81403c8e52..e06a88751c 100644 --- a/codex-rs/core/src/unified_exec/mod_tests.rs +++ b/codex-rs/core/src/unified_exec/mod_tests.rs @@ -121,8 +121,12 @@ async fn exec_command_with_tty( ) .await?, ); - let context = - UnifiedExecContext::new(Arc::clone(session), Arc::clone(turn), "call".to_string()); + let context = UnifiedExecContext::new( + Arc::clone(session), + Arc::clone(turn), + StepContext::for_test(Arc::clone(turn)), + "call".to_string(), + ); let started_at = Instant::now(); let process_started_alive = !process.has_exited() && process.exit_code().is_none(); if process_started_alive { diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index fe3ec45759..0088d956a9 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -1171,6 +1171,7 @@ impl UnifiedExecProcessManager { let tool_ctx = ToolCtx { session: context.session.clone(), turn: context.turn.clone(), + step_context: context.step_context.clone(), call_id: context.call_id.clone(), tool_name: ToolName::plain("exec_command"), }; diff --git a/codex-rs/core/src/unified_exec/process_manager_tests.rs b/codex-rs/core/src/unified_exec/process_manager_tests.rs index 20a930b552..2e5346ece9 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -262,6 +262,7 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() { let context = UnifiedExecContext::new( Arc::clone(&session), Arc::clone(&turn), + StepContext::for_test(Arc::clone(&turn)), "call-unified-denied".to_string(), ); let request = ExecCommandRequest {