From e4c7bdc387a8880204fc3807c8afa73cd32e6cd7 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 8 Jul 2026 20:59:47 -0700 Subject: [PATCH] progress --- codex-rs/core/src/codex_delegate.rs | 21 +++++ codex-rs/core/src/compact.rs | 24 ++++- codex-rs/core/src/compact_remote.rs | 15 +++- codex-rs/core/src/compact_remote_request.rs | 2 +- codex-rs/core/src/compact_remote_v2.rs | 8 +- .../core/src/compact_remote_v2_attempt.rs | 1 + codex-rs/core/src/compact_token_budget.rs | 25 ++++-- codex-rs/core/src/context_manager/updates.rs | 16 ++-- codex-rs/core/src/guardian/review.rs | 24 ++++- codex-rs/core/src/guardian/review_session.rs | 21 ++++- codex-rs/core/src/mcp_tool_call.rs | 8 +- codex-rs/core/src/session/inject.rs | 2 +- codex-rs/core/src/session/mcp.rs | 6 +- codex-rs/core/src/session/mod.rs | 58 +++++++++--- codex-rs/core/src/session/multi_agents.rs | 8 +- codex-rs/core/src/session/review.rs | 7 +- .../session/rollout_reconstruction_tests.rs | 16 ++-- codex-rs/core/src/session/step_context.rs | 21 +++++ codex-rs/core/src/session/tests.rs | 4 +- codex-rs/core/src/session/turn.rs | 30 +++++-- codex-rs/core/src/session/turn_context.rs | 88 ++++++++++--------- codex-rs/core/src/session_startup_prewarm.rs | 15 ++-- codex-rs/core/src/state/turn.rs | 2 + codex-rs/core/src/stream_events_utils.rs | 4 +- codex-rs/core/src/tasks/compact.rs | 9 +- codex-rs/core/src/tasks/lifecycle.rs | 3 +- codex-rs/core/src/tasks/mod.rs | 8 +- codex-rs/core/src/tasks/regular.rs | 2 +- codex-rs/core/src/tasks/review.rs | 5 ++ codex-rs/core/src/tasks/user_shell.rs | 2 +- .../core/src/tools/handlers/agent_jobs.rs | 4 +- .../agent_jobs/spawn_agents_on_csv.rs | 13 ++- .../handlers/multi_agents/resume_agent.rs | 6 +- .../tools/handlers/multi_agents/send_input.rs | 3 +- .../src/tools/handlers/multi_agents/spawn.rs | 8 +- .../src/tools/handlers/multi_agents_common.rs | 21 +++-- .../handlers/multi_agents_v2/message_tool.rs | 3 +- .../tools/handlers/multi_agents_v2/spawn.rs | 8 +- codex-rs/core/src/tools/handlers/plan.rs | 2 +- .../src/tools/handlers/request_permissions.rs | 1 + codex-rs/core/src/tools/sandboxing.rs | 2 + justfile | 11 ++- 42 files changed, 391 insertions(+), 146 deletions(-) diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index d2911df407..569b54b083 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::StepContext; use crate::session::turn_context::TurnContext; use codex_login::AuthManager; use codex_models_manager::manager::SharedModelsManager; @@ -79,6 +80,7 @@ pub(crate) async fn run_codex_thread_interactive( models_manager: SharedModelsManager, parent_session: Arc, parent_ctx: Arc, + step_context: Arc, cancel_token: CancellationToken, subagent_source: SubAgentSource, initial_history: Option, @@ -157,6 +159,7 @@ pub(crate) async fn run_codex_thread_interactive( // routing them to the parent session for decisions. let parent_session_clone = Arc::clone(&parent_session); let parent_ctx_clone = Arc::clone(&parent_ctx); + let step_context_clone = Arc::clone(&step_context); let codex_for_events = Arc::clone(&codex); // Cache the child call's MCP metadata at begin time. The later legacy // RequestUserInput approval event only carries a call_id and question metadata. @@ -168,6 +171,7 @@ pub(crate) async fn run_codex_thread_interactive( tx_sub, parent_session_clone, parent_ctx_clone, + step_context_clone, pending_mcp_invocations, cancel_token_events, ) @@ -200,6 +204,7 @@ pub(crate) async fn run_codex_thread_one_shot( input: Vec, parent_session: Arc, parent_ctx: Arc, + step_context: Arc, cancel_token: CancellationToken, subagent_source: SubAgentSource, final_output_json_schema: Option, @@ -214,6 +219,7 @@ pub(crate) async fn run_codex_thread_one_shot( models_manager, parent_session, parent_ctx, + step_context, child_cancel.clone(), subagent_source, initial_history, @@ -279,6 +285,7 @@ async fn forward_events( tx_sub: Sender, parent_session: Arc, parent_ctx: Arc, + step_context: Arc, pending_mcp_invocations: Arc>>, cancel_token: CancellationToken, ) { @@ -315,6 +322,7 @@ async fn forward_events( id, &parent_session, &parent_ctx, + Arc::clone(&step_context), event, &cancel_token, ) @@ -329,6 +337,7 @@ async fn forward_events( id, &parent_session, &parent_ctx, + Arc::clone(&step_context), event, &cancel_token, ) @@ -342,6 +351,7 @@ async fn forward_events( &codex, &parent_session, &parent_ctx, + &step_context, event, &cancel_token, ) @@ -356,6 +366,7 @@ async fn forward_events( id, &parent_session, &parent_ctx, + Arc::clone(&step_context), &pending_mcp_invocations, event, &cancel_token, @@ -493,6 +504,7 @@ async fn handle_exec_approval( turn_id: String, parent_session: &Arc, parent_ctx: &Arc, + step_context: Arc, event: ExecApprovalRequestEvent, cancel_token: &CancellationToken, ) { @@ -515,6 +527,7 @@ async fn handle_exec_approval( let review_rx = spawn_approval_request_review( Arc::clone(parent_session), Arc::clone(parent_ctx), + step_context, new_guardian_review_id(), GuardianApprovalRequest::Shell { id: call_id.clone(), @@ -578,6 +591,7 @@ async fn handle_patch_approval( _id: String, parent_session: &Arc, parent_ctx: &Arc, + step_context: Arc, event: ApplyPatchApprovalRequestEvent, cancel_token: &CancellationToken, ) { @@ -628,6 +642,7 @@ async fn handle_patch_approval( let review_rx = spawn_approval_request_review( Arc::clone(parent_session), Arc::clone(parent_ctx), + step_context, new_guardian_review_id(), GuardianApprovalRequest::ApplyPatch { id: approval_id.clone(), @@ -680,6 +695,7 @@ async fn handle_request_user_input( id: String, parent_session: &Arc, parent_ctx: &Arc, + step_context: Arc, pending_mcp_invocations: &Arc>>, event: RequestUserInputEvent, cancel_token: &CancellationToken, @@ -687,6 +703,7 @@ async fn handle_request_user_input( if let Some(response) = maybe_auto_review_mcp_request_user_input( parent_session, parent_ctx, + step_context, pending_mcp_invocations, &event, cancel_token, @@ -723,6 +740,7 @@ async fn handle_request_user_input( async fn maybe_auto_review_mcp_request_user_input( parent_session: &Arc, parent_ctx: &Arc, + step_context: Arc, pending_mcp_invocations: &Arc>>, event: &RequestUserInputEvent, cancel_token: &CancellationToken, @@ -750,6 +768,7 @@ async fn maybe_auto_review_mcp_request_user_input( let review_rx = spawn_approval_request_review( Arc::clone(parent_session), Arc::clone(parent_ctx), + step_context, new_guardian_review_id(), build_guardian_mcp_tool_review_request(&event.call_id, &invocation, metadata.as_ref()), /*retry_reason*/ None, @@ -796,6 +815,7 @@ async fn handle_request_permissions( codex: &Codex, parent_session: &Arc, parent_ctx: &Arc, + step_context: &Arc, event: RequestPermissionsEvent, cancel_token: &CancellationToken, ) { @@ -811,6 +831,7 @@ async fn handle_request_permissions( }); let response_fut = parent_session.request_permissions_for_cwd( parent_ctx, + step_context, call_id.clone(), args, cwd, diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index d2a9fd5222..e8a1cb747f 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -15,6 +15,7 @@ use crate::responses_metadata::CompactionTurnMetadata; #[cfg(test)] use crate::session::PreviousTurnSettings; use crate::session::session::Session; +use crate::session::step_context::StepContext; use crate::session::turn::get_last_assistant_message_from_turn; use crate::session::turn_context::TurnContext; use crate::util::backoff; @@ -70,13 +71,18 @@ 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( + turn_context, + world_state.as_ref(), + step_context, + ) .await; (items, Some(Arc::clone(world_state))) } @@ -91,6 +97,7 @@ pub(crate) fn should_use_remote_compact_task(provider: &ModelProviderInfo) -> bo pub(crate) async fn run_inline_auto_compact_task( sess: Arc, turn_context: Arc, + step_context: Arc, initial_context_injection: InitialContextInjection, reason: CompactionReason, phase: CompactionPhase, @@ -110,6 +117,7 @@ pub(crate) async fn run_inline_auto_compact_task( run_compact_task_inner( sess, turn_context, + step_context, input, initial_context_injection, CompactionTrigger::Auto, @@ -123,6 +131,7 @@ pub(crate) async fn run_inline_auto_compact_task( pub(crate) async fn run_compact_task( sess: Arc, turn_context: Arc, + step_context: Arc, input: Vec, ) -> CodexResult<()> { let start_event = EventMsg::TurnStarted(TurnStartedEvent { @@ -130,12 +139,13 @@ pub(crate) async fn run_compact_task( trace_id: turn_context.trace_id.clone(), started_at: turn_context.turn_timing_state.started_at_unix_secs().await, model_context_window: turn_context.model_context_window(), - collaboration_mode_kind: turn_context.collaboration_mode.mode, + collaboration_mode_kind: turn_context.mode, }); sess.send_event(&turn_context, start_event).await; run_compact_task_inner( sess.clone(), turn_context, + step_context, input, InitialContextInjection::DoNotInject, CompactionTrigger::Manual, @@ -149,6 +159,7 @@ pub(crate) async fn run_compact_task( async fn run_compact_task_inner( sess: Arc, turn_context: Arc, + step_context: Arc, input: Vec, initial_context_injection: InitialContextInjection, trigger: CompactionTrigger, @@ -185,6 +196,7 @@ async fn run_compact_task_inner( let result = run_compact_task_inner_impl( Arc::clone(&sess), Arc::clone(&turn_context), + step_context, input, initial_context_injection, compaction_metadata, @@ -220,6 +232,7 @@ async fn run_compact_task_inner( async fn run_compact_task_inner_impl( sess: Arc, turn_context: Arc, + step_context: Arc, input: Vec, initial_context_injection: InitialContextInjection, compaction_metadata: CompactionTurnMetadata, @@ -262,6 +275,7 @@ async fn run_compact_task_inner_impl( let attempt_result = drain_to_completed( &sess, turn_context.as_ref(), + step_context.as_ref(), &mut client_session, &responses_metadata, &prompt, @@ -336,6 +350,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; @@ -346,7 +361,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(turn_context.to_turn_context_item(&step_context)) } }; let compacted_item = CompactedItem { @@ -661,6 +676,7 @@ fn build_compacted_history_with_limit( async fn drain_to_completed( sess: &Session, turn_context: &TurnContext, + step_context: &StepContext, client_session: &mut ModelClientSession, responses_metadata: &CodexResponsesMetadata, prompt: &Prompt, @@ -670,7 +686,7 @@ async fn drain_to_completed( prompt, &turn_context.model_info, &turn_context.session_telemetry, - turn_context.reasoning_effort.clone(), + step_context.effective_reasoning_effort(), turn_context.reasoning_summary, turn_context.config.service_tier.clone(), responses_metadata, diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 4b1db93555..bd202ab678 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -81,7 +81,7 @@ pub(crate) async fn run_remote_compact_task( trace_id: turn_context.trace_id.clone(), started_at: turn_context.turn_timing_state.started_at_unix_secs().await, model_context_window: turn_context.model_context_window(), - collaboration_mode_kind: turn_context.collaboration_mode.mode, + collaboration_mode_kind: turn_context.mode, }); sess.send_event(&turn_context, start_event).await; @@ -260,6 +260,7 @@ async fn run_remote_compact_task_inner_impl( let (new_history, world_state_baseline) = process_compacted_history( sess.as_ref(), compaction_turn_context.as_ref(), + 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_turn_context.to_turn_context_item(step_context)) } }; let compacted_item = CompactedItem { @@ -304,14 +305,20 @@ 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>) { // Mid-turn compaction is the only path that must inject initial context above the last user // 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; + let (initial_context, world_state_baseline) = build_compaction_initial_context( + sess, + turn_context, + step_context, + initial_context_injection, + ) + .await; compacted_history.retain(should_keep_compacted_history_item); ( diff --git a/codex-rs/core/src/compact_remote_request.rs b/codex-rs/core/src/compact_remote_request.rs index bbf2f04446..356cca6f8d 100644 --- a/codex-rs/core/src/compact_remote_request.rs +++ b/codex-rs/core/src/compact_remote_request.rs @@ -87,7 +87,7 @@ pub(super) async fn run_remote_compact_attempt( &turn_context.model_info, turn_state, CompactConversationRequestSettings { - effort: turn_context.reasoning_effort.clone(), + effort: step_context.effective_reasoning_effort().clone(), summary: turn_context.reasoning_summary, service_tier: if sess.services.auth_manager.auth_mode() == Some(AuthMode::ApiKey) { None diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index 1571602cc1..8bec218882 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -92,7 +92,7 @@ pub(crate) async fn run_remote_compact_task( trace_id: turn_context.trace_id.clone(), started_at: turn_context.turn_timing_state.started_at_unix_secs().await, model_context_window: turn_context.model_context_window(), - collaboration_mode_kind: turn_context.collaboration_mode.mode, + collaboration_mode_kind: turn_context.mode, }); sess.send_event(&turn_context, start_event).await; @@ -284,6 +284,7 @@ async fn run_remote_compact_task_inner_impl( let (new_history, world_state_baseline) = process_compacted_history( sess.as_ref(), compaction_turn_context.as_ref(), + 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_turn_context.to_turn_context_item(step_context)) } }; let compacted_item = CompactedItem { @@ -330,6 +331,7 @@ struct RemoteCompactionV2Output { async fn run_remote_compaction_request_v2( sess: &Session, turn_context: &TurnContext, + step_context: &StepContext, client_session: &mut ModelClientSession, prompt: &Prompt, responses_metadata: &CodexResponsesMetadata, @@ -346,7 +348,7 @@ async fn run_remote_compaction_request_v2( prompt, &turn_context.model_info, &turn_context.session_telemetry, - turn_context.reasoning_effort.clone(), + step_context.effective_reasoning_effort(), turn_context.reasoning_summary, turn_context.config.service_tier.clone(), responses_metadata, diff --git a/codex-rs/core/src/compact_remote_v2_attempt.rs b/codex-rs/core/src/compact_remote_v2_attempt.rs index a145d00ae6..e7ddf5b2f4 100644 --- a/codex-rs/core/src/compact_remote_v2_attempt.rs +++ b/codex-rs/core/src/compact_remote_v2_attempt.rs @@ -102,6 +102,7 @@ pub(super) async fn run_remote_compact_v2_attempt( let compaction_output_result = run_remote_compaction_request_v2( sess, turn_context.as_ref(), + step_context.as_ref(), client_session, &prompt, &responses_metadata, diff --git a/codex-rs/core/src/compact_token_budget.rs b/codex-rs/core/src/compact_token_budget.rs index 710e7cefe5..afe9fa55f5 100644 --- a/codex-rs/core/src/compact_token_budget.rs +++ b/codex-rs/core/src/compact_token_budget.rs @@ -24,6 +24,7 @@ use codex_protocol::protocol::TurnStartedEvent; /// observe the same lifecycle as local or remote compaction. pub(crate) async fn run_manual_compact_task( sess: Arc, + step_context: Arc, turn_context: Arc, ) -> CodexResult<()> { let start_event = EventMsg::TurnStarted(TurnStartedEvent { @@ -31,14 +32,20 @@ pub(crate) async fn run_manual_compact_task( trace_id: turn_context.trace_id.clone(), started_at: turn_context.turn_timing_state.started_at_unix_secs().await, model_context_window: turn_context.model_context_window(), - collaboration_mode_kind: turn_context.collaboration_mode.mode, + collaboration_mode_kind: turn_context.mode, }); sess.send_event(&turn_context, start_event).await; // 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, + &turn_context, + &step_context, + world_state, + CompactionTrigger::Manual, + ) + .await } /// Runs token-budget inline auto-compaction as a normal compaction lifecycle. @@ -58,12 +65,20 @@ pub(crate) async fn run_inline_auto_compact_task( 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, + turn_context, + &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<()> { @@ -76,7 +91,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(turn_context.as_ref(), world_state, step_context.as_ref()) .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..731cf11906 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; @@ -70,13 +71,11 @@ fn build_collaboration_mode_update_item( } 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 } @@ -85,8 +84,10 @@ fn build_collaboration_mode_update_item( fn build_multi_agent_mode_update_item( previous: Option<&TurnContextItem>, next: &TurnContext, + step_context: &StepContext, ) -> Option { - let effective_multi_agent_mode = crate::session::multi_agents::effective_multi_agent_mode(next); + let effective_multi_agent_mode = + crate::session::multi_agents::effective_multi_agent_mode(next, step_context); let previous = previous?; if previous.multi_agent_mode == effective_multi_agent_mode { return None; @@ -241,6 +242,7 @@ pub(crate) fn build_settings_update_items( previous: Option<&TurnContextItem>, previous_turn_settings: Option<&PreviousTurnSettings>, next: &TurnContext, + step_context: &StepContext, exec_policy: &Policy, personality_feature_enabled: bool, ) -> Vec { @@ -254,7 +256,7 @@ pub(crate) fn build_settings_update_items( build_model_instructions_update_item(previous_turn_settings, next), build_permissions_update_item(previous, next, exec_policy), build_collaboration_mode_update_item(previous, next), - build_multi_agent_mode_update_item(previous, next), + build_multi_agent_mode_update_item(previous, next, step_context), build_realtime_update_item(previous, previous_turn_settings, next), build_personality_update_item(previous, next, personality_feature_enabled), ] diff --git a/codex-rs/core/src/guardian/review.rs b/codex-rs/core/src/guardian/review.rs index 4756cc1fc4..9bff2adac2 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::StepContext; use crate::session::turn_context::TurnContext; use crate::turn_timing::now_unix_timestamp_ms; use crate::util::backoff; @@ -275,6 +276,7 @@ pub(crate) async fn record_guardian_denial_for_test( async fn run_guardian_review( session: Arc, turn: Arc, + step_context: Arc, review_id: String, request: GuardianApprovalRequest, retry_reason: Option, @@ -359,6 +361,7 @@ async fn run_guardian_review( let (outcome, analytics_result) = Box::pin(run_guardian_review_session_with_retry( session.clone(), turn.clone(), + step_context.clone(), request, retry_reason.clone(), schema, @@ -594,6 +597,7 @@ async fn run_guardian_review( pub(crate) async fn review_approval_request( session: &Arc, turn: &Arc, + step_context: Arc, review_id: String, request: GuardianApprovalRequest, retry_reason: Option, @@ -603,6 +607,7 @@ pub(crate) async fn review_approval_request( Box::pin(run_guardian_review( Arc::clone(session), Arc::clone(turn), + step_context, review_id, request, retry_reason, @@ -615,6 +620,7 @@ pub(crate) async fn review_approval_request( pub(crate) async fn review_approval_request_with_cancel( session: &Arc, turn: &Arc, + step_context: Arc, review_id: String, request: GuardianApprovalRequest, retry_reason: Option, @@ -624,6 +630,7 @@ pub(crate) async fn review_approval_request_with_cancel( run_guardian_review( Arc::clone(session), Arc::clone(turn), + step_context, review_id, request, retry_reason, @@ -636,6 +643,7 @@ pub(crate) async fn review_approval_request_with_cancel( pub(crate) fn spawn_approval_request_review( session: Arc, turn: Arc, + step_context: Arc, review_id: String, request: GuardianApprovalRequest, retry_reason: Option, @@ -654,6 +662,7 @@ pub(crate) fn spawn_approval_request_review( let decision = runtime.block_on(review_approval_request_with_cancel( &session, &turn, + step_context, review_id, request, retry_reason, @@ -678,6 +687,7 @@ pub(super) struct GuardianReviewSessionConfig { pub(super) async fn guardian_review_session_config( session: &Session, turn: &TurnContext, + step_context: &StepContext, ) -> anyhow::Result { let network_proxy = session.services.network_proxy.load_full(); let live_network_config = match network_proxy.as_ref() { @@ -722,7 +732,8 @@ pub(super) async fn guardian_review_session_config( .supported_reasoning_levels .iter() .any(|preset| preset.effort == codex_protocol::openai_models::ReasoningEffort::Low), - turn.reasoning_effort + step_context + .reasoning_effort .clone() .or_else(|| turn.model_info.default_reasoning_level.clone()), ); @@ -768,13 +779,19 @@ pub(super) async fn guardian_review_session_config( async fn run_guardian_review_session_before_deadline( session: Arc, turn: Arc, + step_context: Arc, 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 + let session_config = match guardian_review_session_config( + session.as_ref(), + turn.as_ref(), + step_context.as_ref(), + ) + .await { Ok(session_config) => session_config, Err(err) => { @@ -790,6 +807,7 @@ async fn run_guardian_review_session_before_deadline( .run_review(GuardianReviewSessionParams { parent_session: Arc::clone(&session), parent_turn: turn.clone(), + step_context, spawn_config: session_config.spawn_config, request, retry_reason, @@ -862,6 +880,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: Arc, request: GuardianApprovalRequest, retry_reason: Option, schema: serde_json::Value, @@ -875,6 +894,7 @@ pub(super) async fn run_guardian_review_session_with_retry( let (outcome, mut analytics_result) = run_guardian_review_session_before_deadline( Arc::clone(&session), Arc::clone(&turn), + Arc::clone(&step_context), 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 05ef58b4dd..822751ac26 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::StepContext; use crate::session::turn_context::TurnContext; use codex_config::types::McpServerConfig; use codex_features::Feature; @@ -75,6 +76,7 @@ pub(crate) enum GuardianReviewSessionOutcome { pub(crate) struct GuardianReviewSessionParams { pub(crate) parent_session: Arc, pub(crate) parent_turn: Arc, + pub(crate) step_context: Arc, pub(crate) spawn_config: Config, pub(crate) request: GuardianApprovalRequest, pub(crate) retry_reason: Option, @@ -296,12 +298,14 @@ impl GuardianReviewSessionManager { &self, parent_session: Arc, parent_turn: Arc, + step_context: Arc, ) -> 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, &parent_turn, &step_context) + .await? + .spawn_config; let reuse_key = GuardianReviewSessionReuseKey::from_spawn_config( &spawn_config, parent_session.user_instructions().await, @@ -311,6 +315,7 @@ impl GuardianReviewSessionManager { let review_session = spawn_guardian_review_session( &parent_session, &parent_turn, + Arc::clone(&step_context), spawn_config, reuse_key, spawn_cancel_token.clone(), @@ -396,6 +401,7 @@ impl GuardianReviewSessionManager { Box::pin(spawn_guardian_review_session( ¶ms.parent_session, ¶ms.parent_turn, + Arc::clone(¶ms.step_context), params.spawn_config.clone(), next_reuse_key.clone(), spawn_cancel_token.clone(), @@ -607,6 +613,7 @@ impl GuardianReviewSessionManager { Box::pin(spawn_guardian_review_session( ¶ms.parent_session, ¶ms.parent_turn, + Arc::clone(¶ms.step_context), fork_config, reuse_key, spawn_cancel_token.clone(), @@ -649,6 +656,7 @@ impl GuardianReviewSessionManager { async fn spawn_guardian_review_session( parent_session: &Arc, parent_turn: &Arc, + step_context: Arc, spawn_config: Config, reuse_key: GuardianReviewSessionReuseKey, cancel_token: CancellationToken, @@ -668,6 +676,7 @@ async fn spawn_guardian_review_session( parent_session.services.models_manager.clone(), Arc::clone(parent_session), Arc::clone(parent_turn), + step_context, cancel_token.clone(), SubAgentSource::Other(GUARDIAN_REVIEWER_NAME.to_string()), initial_history, @@ -1217,9 +1226,13 @@ mod tests { ) .expect("guardian config"); + let turn = Arc::new(turn); + let step_context = StepContext::for_test(Arc::clone(&turn)); + GuardianReviewSessionParams { parent_session: Arc::new(session), - parent_turn: Arc::new(turn), + parent_turn: Arc::clone(&turn), + step_context, spawn_config, request: GuardianApprovalRequest::Shell { id: "shell-1".to_string(), diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 63d21504b9..cb8dbd8e05 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -395,7 +395,7 @@ async fn handle_approved_mcp_tool_call( let result = async { let rewritten_arguments = rewrite?; let request_meta = - build_mcp_tool_call_request_meta(turn_context, &server, call_id, metadata); + build_mcp_tool_call_request_meta(step_context, &server, call_id, metadata); execute_mcp_tool_call( sess, step_context, @@ -1081,18 +1081,19 @@ async fn custom_mcp_tool_approval_mode( } fn build_mcp_tool_call_request_meta( - turn_context: &TurnContext, + step_context: &StepContext, server: &str, call_id: &str, metadata: Option<&McpToolApprovalMetadata>, ) -> Option { + let turn_context = step_context.turn.as_ref(); let mut request_meta = serde_json::Map::new(); if let Some(turn_metadata) = turn_context .turn_metadata_state .current_meta_value_for_mcp_request(McpTurnMetadataContext { model: turn_context.model_info.slug.as_str(), - reasoning_effort: turn_context.effective_reasoning_effort(), + reasoning_effort: step_context.effective_reasoning_effort(), }) { request_meta.insert( @@ -1274,6 +1275,7 @@ async fn maybe_request_mcp_tool_approval( let decision = review_approval_request( sess, turn_context, + Arc::clone(step_context), review_id.clone(), build_guardian_mcp_tool_review_request(call_id, invocation, metadata), /*retry_reason*/ None, diff --git a/codex-rs/core/src/session/inject.rs b/codex-rs/core/src/session/inject.rs index 5c5b7d1feb..b0b9ef7926 100644 --- a/codex-rs/core/src/session/inject.rs +++ b/codex-rs/core/src/session/inject.rs @@ -86,7 +86,7 @@ impl Session { let turn_context = self .new_default_turn_with_sub_id(uuid::Uuid::new_v4().to_string()) .await; - if turn_context.collaboration_mode.mode == ModeKind::Plan { + if turn_context.mode == ModeKind::Plan { self.clear_reserved_idle_turn(&turn_state).await; self.maybe_start_turn_for_pending_work().await; return Err(TryStartTurnIfIdleError::new( diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 3f5e92eca7..ce8b229406 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -588,11 +588,12 @@ async fn review_guardian_mcp_elicitation( session: Arc, request: ElicitationReviewRequest, ) -> anyhow::Result> { - let Some((turn_context, _cancellation_token)) = - session.active_turn_context_and_cancellation_token().await + let Some((step_context, _cancellation_token)) = + session.active_step_context_and_cancellation_token().await else { return Ok(None); }; + let turn_context = Arc::clone(&step_context.turn); let approvals_reviewer = crate::connectors::mcp_approvals_reviewer( turn_context.config.as_ref(), @@ -624,6 +625,7 @@ async fn review_guardian_mcp_elicitation( let decision = crate::guardian::review_approval_request( &session, &turn_context, + step_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 1efdc44189..90ebe99414 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -344,7 +344,6 @@ use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::LocalImagePreparation; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; -use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::CodexErrorInfo; @@ -1730,6 +1729,7 @@ impl Session { &self, reference_context_item: Option<&TurnContextItem>, current_context: &TurnContext, + step_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 @@ -1744,6 +1744,7 @@ impl Session { reference_context_item, previous_turn_settings.as_ref(), current_context, + step_context, exec_policy.as_ref(), self.features.enabled(Feature::Personality), ) @@ -2033,6 +2034,27 @@ impl Session { )) } + pub(crate) async fn set_active_step_context(&self, step_context: Arc) { + let mut active = self.active_turn.lock().await; + let Some(task) = active.as_mut().and_then(|turn| turn.task.as_mut()) else { + return; + }; + if task.turn_context.sub_id == step_context.turn.sub_id { + task.step_context = Some(step_context); + } + } + + async fn active_step_context_and_cancellation_token( + &self, + ) -> Option<(Arc, CancellationToken)> { + let active = self.active_turn.lock().await; + let task = active.as_ref()?.task.as_ref()?; + Some(( + Arc::clone(task.step_context.as_ref()?), + task.cancellation_token.child_token(), + )) + } + pub(crate) async fn record_execpolicy_amendment_message( &self, sub_id: &str, @@ -2268,6 +2290,7 @@ impl Session { pub(crate) async fn request_permissions_for_environment( self: &Arc, turn_context: &Arc, + step_context: &Arc, call_id: String, args: RequestPermissionsArgs, environment: TurnEnvironmentSelection, @@ -2326,6 +2349,7 @@ impl Session { let review_rx = crate::guardian::spawn_approval_request_review( session, turn, + step_context.clone(), review_id, request, /*retry_reason*/ None, @@ -2436,6 +2460,7 @@ impl Session { pub(crate) async fn request_permissions_for_cwd( self: &Arc, turn_context: &Arc, + step_context: &Arc, call_id: String, args: RequestPermissionsArgs, cwd: AbsolutePathBuf, @@ -2460,6 +2485,7 @@ impl Session { environment.cwd = PathUri::from_abs_path(&cwd); self.request_permissions_for_environment( turn_context, + step_context, call_id, args, environment, @@ -3165,9 +3191,9 @@ impl Session { &self, turn_context: &TurnContext, world_state: &WorldState, + step_context: &StepContext, ) -> 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(turn_context, world_state, step_context) .await } @@ -3175,8 +3201,9 @@ impl Session { &self, turn_context: &TurnContext, world_state: &WorldState, - mcp: &McpRuntimeSnapshot, + step_context: &StepContext, ) -> Vec { + let mcp = step_context.mcp.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(); @@ -3447,7 +3474,9 @@ 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(turn_context, step_context) + { items.push(ContextualUserFragment::into( MultiAgentModeInstructions::new(multi_agent_mode), )); @@ -3516,6 +3545,7 @@ impl Session { &self, turn_context: &TurnContext, world_state: Arc, + step_context: &StepContext, ) -> u64 { let window = { let mut state = self.state.lock().await; @@ -3523,9 +3553,13 @@ impl Session { }; 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( + turn_context, + world_state.as_ref(), + step_context, + ) .await; - let turn_context_item = turn_context.to_turn_context_item(); + let turn_context_item = turn_context.to_turn_context_item(step_context); self.replace_compacted_history( turn_context, context_items, @@ -3573,7 +3607,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 = turn_context.to_turn_context_item(step_context); 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 +3617,7 @@ impl Session { .build_initial_context_with_world_state_and_mcp( turn_context, world_state.as_ref(), - step_context.mcp.as_ref(), + step_context, ) .await; let snapshot = world_state.snapshot(); @@ -3600,7 +3634,11 @@ 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(), + turn_context, + 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..d8198a5f68 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,10 @@ 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( + turn_context: &TurnContext, + step_context: &StepContext, +) -> Option { if turn_context.multi_agent_version != MultiAgentVersion::V2 { return None; } @@ -49,7 +53,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/review.rs b/codex-rs/core/src/session/review.rs index 33de46ede0..9dcbc9e0a3 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -70,7 +70,6 @@ pub(super) async fn spawn_review_thread( let auth_manager_for_context = auth_manager.clone(); let provider_for_context = provider.clone(); let session_telemetry_for_context = session_telemetry.clone(); - let reasoning_effort = per_turn_config.model_reasoning_effort.clone(); let reasoning_summary = per_turn_config .model_reasoning_summary .unwrap_or(model_info.default_reasoning_summary); @@ -114,7 +113,6 @@ pub(super) async fn spawn_review_thread( model_info: model_info.clone(), session_telemetry: session_telemetry_for_context, provider: provider_for_context, - reasoning_effort, reasoning_summary, session_source, parent_thread_id: parent_turn_context.parent_thread_id, @@ -126,7 +124,10 @@ pub(super) async fn spawn_review_thread( timezone: parent_turn_context.timezone.clone(), app_server_client_name: parent_turn_context.app_server_client_name.clone(), developer_instructions: None, - collaboration_mode: parent_turn_context.collaboration_mode.clone(), + mode: parent_turn_context.mode, + collaboration_mode_developer_instructions: parent_turn_context + .collaboration_mode_developer_instructions + .clone(), multi_agent_version: MultiAgentVersion::Disabled, personality: parent_turn_context.personality, approval_policy: parent_turn_context.approval_policy.clone(), diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs index a7d2a7efc1..19db475a1f 100644 --- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs @@ -179,7 +179,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, - collaboration_mode: Some(turn_context.collaboration_mode.clone()), + collaboration_mode: Some(turn_context.collaboration_mode()), multi_agent_version: None, multi_agent_mode: None, realtime_active: Some(turn_context.realtime_active), @@ -225,7 +225,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif model: previous_model.to_string(), comp_hash: Some("comp-hash-a".to_string()), personality: turn_context.personality, - collaboration_mode: Some(turn_context.collaboration_mode.clone()), + collaboration_mode: Some(turn_context.collaboration_mode()), multi_agent_version: None, multi_agent_mode: None, realtime_active: Some(turn_context.realtime_active), @@ -1259,7 +1259,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, - collaboration_mode: Some(turn_context.collaboration_mode.clone()), + collaboration_mode: Some(turn_context.collaboration_mode()), multi_agent_version: None, multi_agent_mode: None, realtime_active: Some(turn_context.realtime_active), @@ -1345,7 +1345,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, - collaboration_mode: Some(turn_context.collaboration_mode.clone()), + collaboration_mode: Some(turn_context.collaboration_mode()), multi_agent_version: None, multi_agent_mode: None, realtime_active: Some(turn_context.realtime_active), @@ -1376,7 +1376,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, - collaboration_mode: Some(turn_context.collaboration_mode.clone()), + collaboration_mode: Some(turn_context.collaboration_mode()), multi_agent_version: None, multi_agent_mode: None, realtime_active: Some(turn_context.realtime_active), @@ -1502,7 +1502,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo model: current_model.to_string(), comp_hash: None, personality: turn_context.personality, - collaboration_mode: Some(turn_context.collaboration_mode.clone()), + collaboration_mode: Some(turn_context.collaboration_mode()), multi_agent_version: None, multi_agent_mode: None, realtime_active: Some(turn_context.realtime_active), @@ -1623,7 +1623,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, - collaboration_mode: Some(turn_context.collaboration_mode.clone()), + collaboration_mode: Some(turn_context.collaboration_mode()), multi_agent_version: None, multi_agent_mode: None, realtime_active: Some(turn_context.realtime_active), @@ -1790,7 +1790,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, - collaboration_mode: Some(turn_context.collaboration_mode.clone()), + collaboration_mode: Some(turn_context.collaboration_mode()), multi_agent_version: None, multi_agent_mode: None, realtime_active: Some(turn_context.realtime_active), diff --git a/codex-rs/core/src/session/step_context.rs b/codex-rs/core/src/session/step_context.rs index 1d7316d914..be02bb4371 100644 --- a/codex-rs/core/src/session/step_context.rs +++ b/codex-rs/core/src/session/step_context.rs @@ -12,6 +12,7 @@ use tokio::sync::OnceCell; #[derive(Debug)] pub(crate) struct StepContext { pub(crate) turn: Arc, + pub(crate) reasoning_effort: Option, pub(crate) environments: TurnEnvironmentSnapshot, /// Capability roots bound to ready environments in this exact step. pub(crate) selected_capability_roots: Vec, @@ -24,6 +25,24 @@ pub(crate) struct StepContext { } impl StepContext { + pub(crate) fn effective_reasoning_effort( + &self, + ) -> Option { + if self.turn.model_info.supports_reasoning_summaries { + self.reasoning_effort + .clone() + .or_else(|| self.turn.model_info.default_reasoning_level.clone()) + } else { + None + } + } + + pub(crate) fn effective_reasoning_effort_for_tracing(&self) -> String { + self.effective_reasoning_effort() + .map(|effort| effort.to_string()) + .unwrap_or_else(|| "default".to_string()) + } + pub(crate) fn new( turn: Arc, environments: TurnEnvironmentSnapshot, @@ -31,8 +50,10 @@ impl StepContext { mcp: Arc, loaded_agents_md: Option>, ) -> Self { + let reasoning_effort = turn.config.model_reasoning_effort.clone(); Self { turn, + reasoning_effort, environments, selected_capability_roots, mcp, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 7274d583e8..294ec4683a 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -2523,7 +2523,7 @@ async fn turn_start_lifecycle_exposes_turn_metadata_and_token_baseline() { thread_level_id: session.thread_id.to_string(), turn_level_id: turn_context.sub_id.clone(), turn_id: turn_context.sub_id.clone(), - collaboration_mode: turn_context.collaboration_mode.clone(), + collaboration_mode: turn_context.collaboration_mode(), token_usage_at_turn_start, saw_session_store: true, saw_thread_store: true, @@ -3089,7 +3089,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, - collaboration_mode: Some(turn_context.collaboration_mode.clone()), + collaboration_mode: Some(turn_context.collaboration_mode()), multi_agent_version: None, multi_agent_mode: None, realtime_active: Some(turn_context.realtime_active), diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 41098b2c1e..d58f0d8c47 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -166,6 +166,8 @@ pub(crate) async fn run_turn( // run_turn owns the step used to seed context and make the first sampling request. let first_step_context = sess.capture_step_context(Arc::clone(&turn_context)).await; + sess.set_active_step_context(Arc::clone(&first_step_context)) + .await; // Keep the exact model-visible state used by this turn and its inline compactions. let (mut world_state, display_roots) = tokio::join!( sess.record_context_updates_and_set_reference_context_item(first_step_context.as_ref()), @@ -204,7 +206,8 @@ pub(crate) async fn run_turn( .await; } - track_turn_resolved_config_analytics(&sess, &turn_context, &input).await; + track_turn_resolved_config_analytics(&sess, &turn_context, &input, first_step_context.clone()) + .await; let mut last_agent_message: Option = None; let mut stop_hook_active = false; @@ -247,7 +250,12 @@ pub(crate) async fn run_turn( // Capture once so context, advertised tools, and tool calls share one request view. let step_context = match next_step_context.take() { Some(step_context) => step_context, - None => sess.capture_step_context(Arc::clone(&turn_context)).await, + None => { + let step_context = sess.capture_step_context(Arc::clone(&turn_context)).await; + sess.set_active_step_context(Arc::clone(&step_context)) + .await; + step_context + } }; let sampling_request_result: CodexResult<_> = async { super::time_reminder::maybe_record_current_time_reminder( @@ -744,6 +752,7 @@ async fn track_turn_resolved_config_analytics( sess: &Session, turn_context: &TurnContext, input: &[TurnInput], + first_step_context: Arc, ) { let thread_config = { let state = sess.state.lock().await; @@ -777,7 +786,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.reasoning_effort.clone(), + reasoning_effort: first_step_context.reasoning_effort.clone(), reasoning_summary: Some(turn_context.reasoning_summary), service_tier: turn_context .config @@ -787,7 +796,7 @@ async fn track_turn_resolved_config_analytics( approval_policy: turn_context.approval_policy.value(), approvals_reviewer: turn_context.config.approvals_reviewer, sandbox_network_access: turn_context.network_sandbox_policy().is_enabled(), - collaboration_mode: turn_context.collaboration_mode.mode, + collaboration_mode: turn_context.mode, personality: turn_context.personality, workspace_kind: turn_context.turn_metadata_state.workspace_kind(), is_first_turn, @@ -808,6 +817,8 @@ async fn run_pre_sampling_compact( if token_status.token_limit_reached { // Pre-turn compaction runs before run_turn creates the normal sampling step. let step_context = sess.capture_step_context(Arc::clone(turn_context)).await; + sess.set_active_step_context(Arc::clone(&step_context)) + .await; run_auto_compact( sess, step_context, @@ -1020,6 +1031,7 @@ async fn run_auto_compact( run_inline_auto_compact_task( Arc::clone(sess), Arc::clone(turn_context), + Arc::clone(&step_context), initial_context_injection, reason, phase, @@ -1158,6 +1170,7 @@ async fn run_sampling_request( tool_runtime.clone(), Arc::clone(&sess), Arc::clone(&turn_context), + step_context.clone(), Arc::clone(&turn_store), client_session, responses_metadata, @@ -1928,6 +1941,7 @@ async fn try_run_sampling_request( tool_runtime: ToolCallRuntime, sess: Arc, turn_context: Arc, + step_context: Arc, turn_store: Arc, client_session: &mut ModelClientSession, responses_metadata: &CodexResponsesMetadata, @@ -1939,7 +1953,7 @@ async fn try_run_sampling_request( model = turn_context.model_info.slug.clone(), approval_policy = turn_context.approval_policy.value(), sandbox_policy = &turn_context.sandbox_policy(), - effort = turn_context.reasoning_effort, + effort = step_context.effective_reasoning_effort_for_tracing(), auth_mode = sess.services.auth_manager.auth_mode(), features = sess.features.enabled_features(), ); @@ -1959,7 +1973,7 @@ async fn try_run_sampling_request( prompt, &turn_context.model_info, &turn_context.session_telemetry, - turn_context.reasoning_effort.clone(), + step_context.effective_reasoning_effort(), turn_context.reasoning_summary, turn_context.config.service_tier.clone(), responses_metadata, @@ -1979,8 +1993,8 @@ async fn try_run_sampling_request( )> = None; let mut should_emit_turn_diff = false; let mut should_emit_token_count = false; - let reasoning_effort = turn_context.effective_reasoning_effort_for_tracing(); - let plan_mode = turn_context.collaboration_mode.mode == ModeKind::Plan; + let reasoning_effort = step_context.effective_reasoning_effort_for_tracing(); + let plan_mode = turn_context.mode == ModeKind::Plan; let mut assistant_message_stream_parsers = AssistantMessageStreamParsers::new(plan_mode); let mut plan_mode_state = plan_mode.then(|| PlanModeStreamState::new(&turn_context.sub_id)); let defer_streamed_turn_items_for_contributors = diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index b9bf42073b..a25a75fd3c 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -108,7 +108,6 @@ pub struct TurnContext { pub(crate) model_info: ModelInfo, pub(crate) session_telemetry: SessionTelemetry, pub(crate) provider: SharedModelProvider, - pub(crate) reasoning_effort: Option, pub(crate) reasoning_summary: ReasoningSummaryConfig, pub(crate) session_source: SessionSource, pub(crate) parent_thread_id: Option, @@ -123,7 +122,8 @@ pub struct TurnContext { pub(crate) timezone: Option, pub(crate) app_server_client_name: Option, pub(crate) developer_instructions: Option, - pub(crate) collaboration_mode: CollaborationMode, + pub(crate) mode: ModeKind, + pub(crate) collaboration_mode_developer_instructions: Option, pub(crate) multi_agent_version: MultiAgentVersion, pub(crate) personality: Option, pub(crate) approval_policy: Constrained, @@ -149,6 +149,17 @@ enum TurnMultiAgentRuntime { } impl TurnContext { + pub(crate) fn collaboration_mode(&self) -> CollaborationMode { + CollaborationMode { + mode: self.mode, + settings: Settings { + model: self.model_info.slug.clone(), + reasoning_effort: self.config.model_reasoning_effort.clone(), + developer_instructions: self.collaboration_mode_developer_instructions.clone(), + }, + } + } + pub(crate) fn permission_profile(&self) -> PermissionProfile { self.permission_profile.clone() } @@ -169,22 +180,6 @@ impl TurnContext { ) } - pub(crate) fn effective_reasoning_effort(&self) -> Option { - if self.model_info.supports_reasoning_summaries { - self.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() - .map(|effort| effort.to_string()) - .unwrap_or_else(|| "default".to_string()) - } - pub(crate) fn model_context_window(&self) -> Option { let effective_context_window_percent = self.model_info.effective_context_window_percent; self.model_info @@ -220,29 +215,24 @@ impl TurnContext { .iter() .map(|preset| preset.effort.clone()) .collect::>(); - let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort.clone() - { - if supported_reasoning_levels.contains(¤t_reasoning_effort) { - Some(current_reasoning_effort) + let reasoning_effort = + if let Some(current_reasoning_effort) = self.config.model_reasoning_effort.clone() { + if supported_reasoning_levels.contains(¤t_reasoning_effort) { + Some(current_reasoning_effort) + } else { + supported_reasoning_levels + .get(supported_reasoning_levels.len().saturating_sub(1) / 2) + .cloned() + .or_else(|| model_info.default_reasoning_level.clone()) + } } else { supported_reasoning_levels .get(supported_reasoning_levels.len().saturating_sub(1) / 2) .cloned() .or_else(|| model_info.default_reasoning_level.clone()) - } - } else { - supported_reasoning_levels - .get(supported_reasoning_levels.len().saturating_sub(1) / 2) - .cloned() - .or_else(|| model_info.default_reasoning_level.clone()) - }; + }; config.model_reasoning_effort = reasoning_effort.clone(); - let collaboration_mode = self.collaboration_mode.with_updates( - Some(model.clone()), - Some(reasoning_effort.clone()), - /*developer_instructions*/ None, - ); let available_models = models_manager .list_models(RefreshStrategy::OnlineIfUncached) .await; @@ -259,7 +249,6 @@ impl TurnContext { .clone() .with_model(model.as_str(), model_info.slug.as_str()), provider: self.provider.clone(), - reasoning_effort, reasoning_summary: self.reasoning_summary, session_source: self.session_source.clone(), parent_thread_id: self.parent_thread_id, @@ -271,7 +260,10 @@ impl TurnContext { timezone: self.timezone.clone(), app_server_client_name: self.app_server_client_name.clone(), developer_instructions: self.developer_instructions.clone(), - collaboration_mode, + mode: self.mode, + collaboration_mode_developer_instructions: self + .collaboration_mode_developer_instructions + .clone(), multi_agent_version: self.multi_agent_version, personality: self.personality, approval_policy: self.approval_policy.clone(), @@ -350,7 +342,7 @@ impl TurnContext { .then_some(file_system_sandbox_policy) } - pub(crate) fn to_turn_context_item(&self) -> TurnContextItem { + pub(crate) fn to_turn_context_item(&self, step_context: &StepContext) -> TurnContextItem { let workspace_roots = self.config.effective_workspace_roots(); #[allow(deprecated)] let cwd = self.cwd.clone(); @@ -368,11 +360,18 @@ impl TurnContext { model: self.model_info.slug.clone(), comp_hash: self.model_info.comp_hash.clone(), personality: self.personality, - collaboration_mode: Some(self.collaboration_mode.clone()), + collaboration_mode: Some(CollaborationMode { + mode: self.mode, + settings: Settings { + model: self.model_info.slug.clone(), + reasoning_effort: step_context.reasoning_effort.clone(), + developer_instructions: self.collaboration_mode_developer_instructions.clone(), + }, + }), multi_agent_version: Some(self.multi_agent_version), - multi_agent_mode: super::multi_agents::effective_multi_agent_mode(self), + multi_agent_mode: super::multi_agents::effective_multi_agent_mode(self, step_context), realtime_active: Some(self.realtime_active), - effort: self.reasoning_effort.clone(), + effort: step_context.effective_reasoning_effort(), summary: ReasoningSummaryConfig::Auto, } } @@ -485,7 +484,7 @@ impl Session { sub_id: String, skills_snapshot: HostSkillsSnapshot, ) -> TurnContext { - let reasoning_effort = session_configuration.collaboration_mode.reasoning_effort(); + let collaboration_mode = &session_configuration.collaboration_mode; let reasoning_summary = session_configuration .model_reasoning_summary .unwrap_or(model_info.default_reasoning_summary); @@ -537,7 +536,6 @@ impl Session { model_info, session_telemetry: session_telemetry_for_context, provider: provider_for_context, - reasoning_effort, reasoning_summary, session_source, parent_thread_id: session_configuration.parent_thread_id, @@ -549,7 +547,11 @@ impl Session { timezone: Some(timezone), app_server_client_name: session_configuration.app_server_client_name.clone(), developer_instructions: session_configuration.developer_instructions.clone(), - collaboration_mode: session_configuration.collaboration_mode.clone(), + mode: collaboration_mode.mode, + collaboration_mode_developer_instructions: collaboration_mode + .settings + .developer_instructions + .clone(), multi_agent_version, personality: session_configuration.personality, approval_policy: session_configuration.approval_policy.clone(), diff --git a/codex-rs/core/src/session_startup_prewarm.rs b/codex-rs/core/src/session_startup_prewarm.rs index a5a4c5e437..39b1eb6799 100644 --- a/codex-rs/core/src/session_startup_prewarm.rs +++ b/codex-rs/core/src/session_startup_prewarm.rs @@ -251,13 +251,21 @@ async fn schedule_startup_prewarm_inner( prewarm_started_at.elapsed(), /*status*/ None, ); + let step_context = session + .capture_step_context(Arc::clone(&startup_turn_context)) + .await; if routes_approval_to_guardian(&startup_turn_context) { let guardian_session = Arc::clone(&session); let guardian_parent_turn = Arc::clone(&startup_turn_context); + let guardian_step_context = Arc::clone(&step_context); drop(tokio::spawn(async move { if let Err(err) = guardian_session .guardian_review_session - .initialize(Arc::clone(&guardian_session), guardian_parent_turn) + .initialize( + Arc::clone(&guardian_session), + guardian_parent_turn, + guardian_step_context, + ) .await { warn!("failed to initialize guardian review session: {err:#}"); @@ -267,9 +275,6 @@ async fn schedule_startup_prewarm_inner( let startup_cancellation_token = CancellationToken::new(); let built_tools_started_at = Instant::now(); // Startup prewarm runs before run_turn and needs its own tool-building snapshot. - let step_context = session - .capture_step_context(Arc::clone(&startup_turn_context)) - .await; let startup_router = built_tools( session.as_ref(), step_context.as_ref(), @@ -310,7 +315,7 @@ async fn schedule_startup_prewarm_inner( &startup_prompt, &startup_turn_context.model_info, &startup_turn_context.session_telemetry, - startup_turn_context.reasoning_effort.clone(), + step_context.effective_reasoning_effort(), startup_turn_context.reasoning_summary, startup_turn_context.config.service_tier.clone(), &responses_metadata, diff --git a/codex-rs/core/src/state/turn.rs b/codex-rs/core/src/state/turn.rs index d1e31b7994..cff1fa755c 100644 --- a/codex-rs/core/src/state/turn.rs +++ b/codex-rs/core/src/state/turn.rs @@ -20,6 +20,7 @@ use tokio::sync::oneshot; use crate::agent::control::AgentExecutionGuard; use crate::session::TurnInputQueue; +use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::tasks::AnySessionTask; use codex_protocol::models::AdditionalPermissionProfile; @@ -76,6 +77,7 @@ pub(crate) struct RunningTask { pub(crate) cancellation_token: CancellationToken, pub(crate) handle: AbortOnDropHandle<()>, pub(crate) turn_context: Arc, + pub(crate) step_context: Option>, pub(crate) turn_extension_data: Arc, pub(crate) _agent_execution_guard: Option, // Timer recorded when the task drops to capture the full turn duration. diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index b07c180b16..ac9b7335ef 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -214,7 +214,7 @@ pub(crate) async fn record_completed_response_item_with_finalized_facts( || { completed_item_defers_mailbox_delivery_to_next_turn( item, - turn_context.collaboration_mode.mode == ModeKind::Plan, + turn_context.mode == ModeKind::Plan, ) }, |facts| facts.defers_mailbox_delivery_to_next_turn, @@ -408,7 +408,7 @@ pub(crate) async fn handle_output_item_done( previously_active_item: Option, ) -> Result { let mut output = OutputItemResult::default(); - let plan_mode = ctx.turn_context.collaboration_mode.mode == ModeKind::Plan; + let plan_mode = ctx.turn_context.mode == ModeKind::Plan; match ToolRouter::build_tool_call(item.clone()) { // The model emitted a tool call; log it, persist the item immediately, and queue the tool execution. diff --git a/codex-rs/core/src/tasks/compact.rs b/codex-rs/core/src/tasks/compact.rs index 60cdebb829..5760c96403 100644 --- a/codex-rs/core/src/tasks/compact.rs +++ b/codex-rs/core/src/tasks/compact.rs @@ -32,8 +32,13 @@ impl SessionTask for CompactTask { _cancellation_token: CancellationToken, ) -> SessionTaskResult { let session = session.clone_session(); + let step_context = session.capture_step_context(Arc::clone(&ctx)).await; + session + .set_active_step_context(Arc::clone(&step_context)) + .await; if ctx.config.features.enabled(Feature::TokenBudget) { - crate::compact_token_budget::run_manual_compact_task(session, ctx).await?; + crate::compact_token_budget::run_manual_compact_task(session, step_context, ctx) + .await?; return Ok(None); } @@ -73,7 +78,7 @@ impl SessionTask for CompactTask { // Compaction prompt is synthesized; no UI element ranges to preserve. text_elements: Vec::new(), }]; - crate::compact::run_compact_task(session.clone(), ctx, input).await + crate::compact::run_compact_task(session.clone(), ctx, step_context, input).await }; if let Err(err @ CodexErr::TurnAborted) = result { return Err(err); diff --git a/codex-rs/core/src/tasks/lifecycle.rs b/codex-rs/core/src/tasks/lifecycle.rs index 782cc8c7de..789955df13 100644 --- a/codex-rs/core/src/tasks/lifecycle.rs +++ b/codex-rs/core/src/tasks/lifecycle.rs @@ -12,11 +12,12 @@ impl Session { turn_context: &TurnContext, token_usage_at_turn_start: &TokenUsage, ) { + let collaboration_mode = turn_context.collaboration_mode(); for contributor in self.services.extensions.turn_lifecycle_contributors() { contributor .on_turn_start(codex_extension_api::TurnStartInput { turn_id: turn_context.sub_id.as_str(), - collaboration_mode: &turn_context.collaboration_mode, + collaboration_mode: &collaboration_mode, token_usage_at_turn_start, session_store: &self.services.session_extension_data, thread_store: &self.services.thread_extension_data, diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index c6d9d2990c..d524ad75fc 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -383,7 +383,12 @@ 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 + .config + .model_reasoning_effort + .as_ref() + .map(ToString::to_string) + .unwrap_or_else(|| "default".to_string()); let task_span = info_span!( "turn", otel.name = span_name, @@ -443,6 +448,7 @@ impl Session { task, cancellation_token, turn_context: Arc::clone(&turn_context), + step_context: None, turn_extension_data, _agent_execution_guard: agent_execution_guard, _timer: timer, diff --git a/codex-rs/core/src/tasks/regular.rs b/codex-rs/core/src/tasks/regular.rs index 40837728a2..66c4d8d5c7 100644 --- a/codex-rs/core/src/tasks/regular.rs +++ b/codex-rs/core/src/tasks/regular.rs @@ -52,7 +52,7 @@ impl SessionTask for RegularTask { trace_id: ctx.trace_id.clone(), started_at: ctx.turn_timing_state.started_at_unix_secs().await, model_context_window: ctx.model_context_window(), - collaboration_mode_kind: ctx.collaboration_mode.mode, + collaboration_mode_kind: ctx.mode, }); sess.send_event(ctx.as_ref(), event).await; sess.set_server_reasoning_included(/*included*/ false).await; diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs index 30f8d47d81..e782886171 100644 --- a/codex-rs/core/src/tasks/review.rs +++ b/codex-rs/core/src/tasks/review.rs @@ -22,6 +22,7 @@ use crate::codex_delegate::run_codex_thread_one_shot; use crate::config::Constrained; use crate::session::TurnInput; use crate::session::session::Session; +use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::state::TaskKind; use codex_features::Feature; @@ -70,11 +71,13 @@ impl SessionTask for ReviewTask { } } + let step_context = session.session.capture_step_context(Arc::clone(&ctx)).await; // Start sub-codex conversation and get the receiver for events. let output = match start_review_conversation( session.clone(), ctx.clone(), user_input, + step_context, cancellation_token.clone(), ) .await @@ -97,6 +100,7 @@ async fn start_review_conversation( session: Arc, ctx: Arc, input: Vec, + step_context: Arc, cancellation_token: CancellationToken, ) -> Option> { let config = ctx.config.clone(); @@ -129,6 +133,7 @@ async fn start_review_conversation( input, session.clone_session(), ctx.clone(), + step_context, cancellation_token, SubAgentSource::Review, /*final_output_json_schema*/ None, diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index a15c53e16b..ddf664c2c4 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -120,7 +120,7 @@ pub(crate) async fn execute_user_shell_command( trace_id: turn_context.trace_id.clone(), started_at: turn_context.turn_timing_state.started_at_unix_secs().await, model_context_window: turn_context.model_context_window(), - collaboration_mode_kind: turn_context.collaboration_mode.mode, + collaboration_mode_kind: turn_context.mode, }); session.send_event(turn_context.as_ref(), event).await; } diff --git a/codex-rs/core/src/tools/handlers/agent_jobs.rs b/codex-rs/core/src/tools/handlers/agent_jobs.rs index 6405414731..196481d20e 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; @@ -108,6 +109,7 @@ fn required_state_db( async fn build_runner_options( session: &Arc, turn: &Arc, + step_context: &StepContext, requested_concurrency: Option, ) -> Result { let multi_agent_version = turn.multi_agent_version; @@ -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, turn.as_ref(), 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..bb933d1e1d 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 @@ -36,6 +36,7 @@ impl SpawnAgentsOnCsvHandler { let ToolInvocation { session, turn, + step_context, payload, .. } = invocation; @@ -49,7 +50,7 @@ impl SpawnAgentsOnCsvHandler { } }; - handle(session, turn, arguments) + handle(session, turn, step_context, arguments) .await .map(boxed_tool_output) } @@ -69,6 +70,7 @@ impl CoreToolRuntime for SpawnAgentsOnCsvHandler { pub async fn handle( session: Arc, turn: Arc, + step_context: Arc, arguments: String, ) -> Result { let args: SpawnAgentsOnCsvArgs = parse_arguments(arguments.as_str())?; @@ -182,7 +184,14 @@ 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 { + let options = match build_runner_options( + &session, + &turn, + step_context.as_ref(), + requested_concurrency, + ) + .await + { Ok(options) => options, Err(err) => { let error_message = err.to_string(); 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..aa0d5e15b1 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,6 +1,7 @@ use super::*; use crate::agent::next_thread_spawn_depth; use crate::session::session::Session; +use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::tools::handlers::multi_agents_spec::create_resume_agent_tool; use codex_tools::ToolSpec; @@ -35,6 +36,7 @@ async fn handle_resume_agent( let ToolInvocation { session, turn, + step_context, payload, call_id, .. @@ -88,6 +90,7 @@ async fn handle_resume_agent( match Box::pin(try_resume_closed_agent( &session, &turn, + step_context.as_ref(), receiver_thread_id, child_depth, )) @@ -188,10 +191,11 @@ 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 config = build_agent_resume_config(turn.as_ref(), 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..4e0aadfaf4 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(turn.as_ref(), 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..ac16475583 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,11 @@ 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, + turn.as_ref(), + 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 287e4b30d5..1d42074e0c 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; @@ -161,28 +162,32 @@ pub(crate) fn parse_collab_input( 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(turn, 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( + turn: &TurnContext, + step_context: &StepContext, +) -> Result { + let mut config = build_agent_shared_config(turn, 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( + turn: &TurnContext, + step_context: &StepContext, +) -> Result { 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 - .reasoning_effort - .clone() - .or_else(|| turn.model_info.default_reasoning_level.clone()); + config.model_reasoning_effort = step_context.effective_reasoning_effort().clone(); config.model_reasoning_summary = Some(turn.reasoning_summary); config.developer_instructions = turn.developer_instructions.clone(); apply_spawn_agent_runtime_overrides(&mut config, turn)?; 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..2324f8e5e7 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(turn.as_ref(), 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..6e2f3c84cd 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,11 @@ 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, + turn.as_ref(), + 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/plan.rs b/codex-rs/core/src/tools/handlers/plan.rs index 1b30c281ce..33b91aa512 100644 --- a/codex-rs/core/src/tools/handlers/plan.rs +++ b/codex-rs/core/src/tools/handlers/plan.rs @@ -81,7 +81,7 @@ impl PlanHandler { } }; - if turn.collaboration_mode.mode == ModeKind::Plan { + if turn.mode == ModeKind::Plan { return Err(FunctionCallError::RespondToModel( "update_plan is a TODO/checklist tool and is not allowed in Plan mode".to_string(), )); diff --git a/codex-rs/core/src/tools/handlers/request_permissions.rs b/codex-rs/core/src/tools/handlers/request_permissions.rs index 9fcb1c642c..13d8e728ee 100644 --- a/codex-rs/core/src/tools/handlers/request_permissions.rs +++ b/codex-rs/core/src/tools/handlers/request_permissions.rs @@ -95,6 +95,7 @@ impl RequestPermissionsHandler { let response = session .request_permissions_for_environment( &turn, + &step_context, call_id, args, turn_environment.selection(), diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 571c5ce6df..5c2d6b288a 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; @@ -384,6 +385,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/justfile b/justfile index d4a0e6566c..ecad2a5e6e 100644 --- a/justfile +++ b/justfile @@ -1,10 +1,12 @@ set working-directory := "codex-rs" -set positional-arguments +set positional-arguments := true + export JUST_SHELL := justfile_directory() / "scripts/just-shell.py" + set shell := ["python3", "-c", 'import os, runpy; runpy.run_path(os.environ["JUST_SHELL"], run_name="__main__")'] set windows-shell := ["python", "-c", 'import os, runpy; runpy.run_path(os.environ["JUST_SHELL"], run_name="__main__")'] -rust_min_stack := "8388608" # 8 MiB +rust_min_stack := "8388608" python := if os_family() == "windows" { "python" } else { "python3" } # Display help @@ -12,7 +14,9 @@ help: just -l # `codex` + alias c := codex + codex *args: cargo run --bin codex -- {args} @@ -76,6 +80,7 @@ install: # # Run `cargo install --locked cargo-nextest` if you don't have it installed. # Prefer this for routine local runs. Workspace crate features are banned, so + # there should be no need to add `--all-features`. [unix] test *args: @@ -86,6 +91,7 @@ test *args: $env:RUST_MIN_STACK = "{{ rust_min_stack }}"; $env:NEXTEST_PROFILE = "local"; cargo nextest run --no-fail-fast @($args | Select-Object -Skip 1) # Run from the repository root so scripts that resolve paths from `cwd` see + # the same layout they use in GitHub Actions. [no-cd] test-github-scripts: @@ -101,6 +107,7 @@ bench-smoke: # Build and run Codex from source using Bazel. # On Unix, use `[no-cd]` and `--run_under="cd $PWD &&"` to ensure Bazel runs + # the command in the current working directory. [no-cd] [unix]