diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 3a8faac441..0036cd2563 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2570,11 +2570,13 @@ impl Session { .await } + /// `maybe_user_input` is only consulted for explicit plugin mentions on this turn. + /// Pass `None` when no raw current-turn user input is available. async fn build_settings_update_items( &self, reference_context_item: Option<&TurnContextItem>, current_context: &TurnContext, - additional_developer_sections: Vec, + maybe_user_input: Option<&[UserInput]>, ) -> 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 @@ -2586,6 +2588,9 @@ impl Session { }; let shell = self.user_shell(); let exec_policy = self.services.exec_policy.current(); + let additional_developer_sections = self + .build_explicit_plugin_developer_sections(current_context, maybe_user_input) + .await; crate::context_manager::updates::build_settings_update_items( reference_context_item, previous_turn_settings.as_ref(), @@ -2597,6 +2602,47 @@ impl Session { ) } + /// `maybe_user_input` is only used to derive explicit plugin-mention guidance for this turn. + /// When the raw user input is unavailable, pass `None` and no turn-local plugin guidance will + /// be added to the canonical developer envelope. + async fn build_explicit_plugin_developer_sections( + &self, + turn_context: &TurnContext, + maybe_user_input: Option<&[UserInput]>, + ) -> Vec { + let Some(user_input) = maybe_user_input else { + return Vec::new(); + }; + + let loaded_plugins = self + .services + .plugins_manager + .plugins_for_config(&turn_context.config); + let mentioned_plugins = + collect_explicit_plugin_mentions(user_input, loaded_plugins.capability_summaries()); + if mentioned_plugins.is_empty() { + return Vec::new(); + } + + let mcp_tools = self + .services + .mcp_connection_manager + .read() + .await + .list_all_tools() + .await; + let available_connectors = if turn_context.apps_enabled() { + let connectors = connectors::merge_plugin_apps_with_accessible( + loaded_plugins.effective_apps(), + connectors::accessible_connectors_from_mcp_tools(&mcp_tools), + ); + connectors::with_app_enabled_state(connectors, &turn_context.config) + } else { + Vec::new() + }; + build_plugin_developer_sections(&mentioned_plugins, &mcp_tools, &available_connectors) + } + /// Persist the event to rollout and send it to clients. pub(crate) async fn send_event(&self, turn_context: &TurnContext, msg: EventMsg) { let legacy_source = msg.clone(); @@ -3433,10 +3479,12 @@ impl Session { } } + /// `maybe_user_input` is only consulted for explicit plugin mentions on this turn. + /// Pass `None` when no raw current-turn user input is available. pub(crate) async fn build_initial_context( &self, turn_context: &TurnContext, - additional_developer_sections: Vec, + maybe_user_input: Option<&[UserInput]>, ) -> Vec { let mut developer_sections = Vec::::with_capacity(8); let mut contextual_user_sections = Vec::::with_capacity(2); @@ -3557,7 +3605,10 @@ impl Session { { developer_sections.push(plugin_section); } - developer_sections.extend(additional_developer_sections); + developer_sections.extend( + self.build_explicit_plugin_developer_sections(turn_context, maybe_user_input) + .await, + ); if turn_context.features.enabled(Feature::CodexGitCommit) && let Some(commit_message_instruction) = commit_message_trailer_instruction( turn_context.config.commit_attribution.as_deref(), @@ -3645,39 +3696,27 @@ impl Session { /// Mid-turn compaction is the other path that can re-establish that baseline when it /// reinjects full initial context into replacement history. Other non-regular tasks /// intentionally do not update the baseline. + /// + /// `maybe_user_input` is only used to derive explicit plugin-mention guidance for the + /// canonical developer envelope on this turn. pub(crate) async fn record_context_updates_and_set_reference_context_item( &self, turn_context: &TurnContext, - ) { - self.record_context_updates_with_additional_developer_sections( - turn_context, - /*additional_developer_sections*/ Vec::new(), - ) - .await; - } - - // Some turn-local guidance, such as explicit plugin mention hints, belongs in the canonical - // pre-user developer message for the current turn but should not become durable - // `TurnContextItem` state. Thread those sections into the context builders here so they share - // the standard developer envelope without affecting the persisted diff baseline. - async fn record_context_updates_with_additional_developer_sections( - &self, - turn_context: &TurnContext, - additional_developer_sections: Vec, + maybe_user_input: Option<&[UserInput]>, ) { let reference_context_item = { let state = self.state.lock().await; state.reference_context_item() }; let context_items = if reference_context_item.is_none() { - self.build_initial_context(turn_context, additional_developer_sections) + self.build_initial_context(turn_context, maybe_user_input) .await } else { // Steady-state path: append only context diffs to minimize token overhead. self.build_settings_update_items( reference_context_item.as_ref(), turn_context, - additional_developer_sections, + maybe_user_input, ) .await }; @@ -5557,7 +5596,8 @@ pub(crate) async fn run_turn( .plugins_manager .plugins_for_config(&turn_context.config); // Structured plugin:// mentions are resolved from the current session's - // enabled plugins, then converted into turn-scoped guidance below. + // enabled plugins here for telemetry/tool selection; the canonical context + // builders derive any explicit plugin guidance from `maybe_user_input`. let mentioned_plugins = collect_explicit_plugin_mentions(&input, loaded_plugins.capability_summaries()); let mcp_tools = if turn_context.apps_enabled() || !mentioned_plugins.is_empty() { @@ -5643,23 +5683,16 @@ pub(crate) async fn run_turn( .await; } - let plugin_developer_sections = - build_plugin_developer_sections(&mentioned_plugins, &mcp_tools, &available_connectors); let mentioned_plugin_metadata = mentioned_plugins .iter() .filter_map(crate::plugins::PluginCapabilitySummary::telemetry_metadata) .collect::>(); - if plugin_developer_sections.is_empty() { - sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref()) - .await; - } else { - sess.record_context_updates_with_additional_developer_sections( - turn_context.as_ref(), - plugin_developer_sections, - ) - .await; - } + sess.record_context_updates_and_set_reference_context_item( + turn_context.as_ref(), + /*maybe_user_input*/ Some(&input), + ) + .await; let mut explicitly_enabled_connectors = collect_explicit_app_ids(&input); explicitly_enabled_connectors.extend(collect_explicit_app_ids_from_skill_items( diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index c7d29ea9f7..a588f5ddb1 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -957,21 +957,24 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only() assert_eq!(expected, history_before_seed.raw_items()); session - .record_context_updates_and_set_reference_context_item(&turn_context) + .record_context_updates_and_set_reference_context_item( + &turn_context, + /*maybe_user_input*/ None, + ) .await; expected.extend( session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await, ); let history_after_seed = session.clone_history().await; assert_eq!(expected, history_after_seed.raw_items()); session - .record_context_updates_and_set_reference_context_item(&turn_context) + .record_context_updates_and_set_reference_context_item( + &turn_context, + /*maybe_user_input*/ None, + ) .await; let history_after_second_seed = session.clone_history().await; assert_eq!( @@ -1346,10 +1349,7 @@ async fn thread_rollback_drops_last_turn_from_history() { let rollout_path = attach_rollout_recorder(&sess).await; let initial_context = sess - .build_initial_context( - tc.as_ref(), - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(tc.as_ref(), /*maybe_user_input*/ None) .await; let turn_1 = vec![ user_message("turn 1 user"), @@ -1415,10 +1415,7 @@ async fn thread_rollback_clears_history_when_num_turns_exceeds_existing_turns() attach_rollout_recorder(&sess).await; let initial_context = sess - .build_initial_context( - tc.as_ref(), - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(tc.as_ref(), /*maybe_user_input*/ None) .await; let turn_1 = vec![user_message("turn 1 user")]; let mut full_history = Vec::new(); @@ -1446,10 +1443,7 @@ async fn thread_rollback_fails_without_persisted_rollout_path() { let (sess, tc, rx) = make_session_and_context_with_rx().await; let initial_context = sess - .build_initial_context( - tc.as_ref(), - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(tc.as_ref(), /*maybe_user_input*/ None) .await; sess.record_into_history(&initial_context, tc.as_ref()) .await; @@ -1768,10 +1762,7 @@ async fn thread_rollback_fails_when_turn_in_progress() { let (sess, tc, rx) = make_session_and_context_with_rx().await; let initial_context = sess - .build_initial_context( - tc.as_ref(), - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(tc.as_ref(), /*maybe_user_input*/ None) .await; sess.record_into_history(&initial_context, tc.as_ref()) .await; @@ -1794,10 +1785,7 @@ async fn thread_rollback_fails_when_num_turns_is_zero() { let (sess, tc, rx) = make_session_and_context_with_rx().await; let initial_context = sess - .build_initial_context( - tc.as_ref(), - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(tc.as_ref(), /*maybe_user_input*/ None) .await; sess.record_into_history(&initial_context, tc.as_ref()) .await; @@ -3734,7 +3722,7 @@ async fn build_settings_update_items_emits_environment_item_for_network_changes( .build_settings_update_items( Some(&reference_context_item), ¤t_context, - /*additional_developer_sections*/ Vec::new(), + /*maybe_user_input*/ None, ) .await; @@ -3773,7 +3761,7 @@ async fn build_settings_update_items_emits_environment_item_for_time_changes() { .build_settings_update_items( Some(&reference_context_item), ¤t_context, - /*additional_developer_sections*/ Vec::new(), + /*maybe_user_input*/ None, ) .await; @@ -3809,7 +3797,7 @@ async fn build_settings_update_items_emits_realtime_start_when_session_becomes_l .build_settings_update_items( Some(&previous_context.to_turn_context_item()), ¤t_context, - /*additional_developer_sections*/ Vec::new(), + /*maybe_user_input*/ None, ) .await; @@ -3838,7 +3826,7 @@ async fn build_settings_update_items_emits_realtime_end_when_session_stops_being .build_settings_update_items( Some(&previous_context.to_turn_context_item()), ¤t_context, - /*additional_developer_sections*/ Vec::new(), + /*maybe_user_input*/ None, ) .await; @@ -3875,7 +3863,7 @@ async fn build_settings_update_items_uses_previous_turn_settings_for_realtime_en .build_settings_update_items( Some(&previous_context_item), ¤t_context, - /*additional_developer_sections*/ Vec::new(), + /*maybe_user_input*/ None, ) .await; @@ -3894,10 +3882,7 @@ async fn build_initial_context_uses_previous_realtime_state() { turn_context.realtime_active = true; let initial_context = session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await; let developer_texts = developer_input_texts(&initial_context); assert!( @@ -3913,10 +3898,7 @@ async fn build_initial_context_uses_previous_realtime_state() { state.set_reference_context_item(Some(previous_context_item)); } let resumed_context = session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await; let resumed_developer_texts = developer_input_texts(&resumed_context); assert!( @@ -3943,10 +3925,7 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist .await; let initial_context = session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await; let developer_texts = developer_input_texts(&initial_context); assert!( @@ -3962,10 +3941,7 @@ async fn build_initial_context_omits_default_image_save_location_without_image_h let (session, turn_context) = make_session_and_context().await; let initial_context = session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await; let developer_texts = developer_input_texts(&initial_context); @@ -4080,10 +4056,7 @@ async fn build_initial_context_uses_previous_turn_settings_for_realtime_end() { .set_previous_turn_settings(Some(previous_turn_settings)) .await; let initial_context = session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await; let developer_texts = developer_input_texts(&initial_context); assert!( @@ -4107,10 +4080,7 @@ async fn build_initial_context_restates_realtime_start_when_reference_context_is .set_previous_turn_settings(Some(previous_turn_settings)) .await; let initial_context = session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await; let developer_texts = developer_input_texts(&initial_context); assert!( @@ -4126,14 +4096,14 @@ async fn record_context_updates_and_set_reference_context_item_injects_full_cont { let (session, turn_context) = make_session_and_context().await; session - .record_context_updates_and_set_reference_context_item(&turn_context) + .record_context_updates_and_set_reference_context_item( + &turn_context, + /*maybe_user_input*/ None, + ) .await; let history = session.clone_history().await; let initial_context = session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await; assert_eq!(history.raw_items().to_vec(), initial_context); @@ -4162,7 +4132,10 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co .record_into_history(std::slice::from_ref(&compacted_summary), &turn_context) .await; session - .record_context_updates_and_set_reference_context_item(&turn_context) + .record_context_updates_and_set_reference_context_item( + &turn_context, + /*maybe_user_input*/ None, + ) .await; { let mut state = session.state.lock().await; @@ -4173,17 +4146,17 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co .await; session - .record_context_updates_and_set_reference_context_item(&turn_context) + .record_context_updates_and_set_reference_context_item( + &turn_context, + /*maybe_user_input*/ None, + ) .await; let history = session.clone_history().await; let mut expected_history = vec![compacted_summary]; expected_history.extend( session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await, ); assert_eq!(history.raw_items().to_vec(), expected_history); @@ -4232,13 +4205,16 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline .build_settings_update_items( Some(&previous_context_item), &turn_context, - /*additional_developer_sections*/ Vec::new(), + /*maybe_user_input*/ None, ) .await; assert_eq!(update_items, Vec::new()); session - .record_context_updates_and_set_reference_context_item(&turn_context) + .record_context_updates_and_set_reference_context_item( + &turn_context, + /*maybe_user_input*/ None, + ) .await; assert_eq!( @@ -4284,10 +4260,7 @@ async fn build_initial_context_prepends_model_switch_message() { .set_previous_turn_settings(Some(previous_turn_settings)) .await; let initial_context = session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await; let ResponseItem::Message { role, content, .. } = &initial_context[0] else { @@ -4356,7 +4329,10 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei })) .await; session - .record_context_updates_and_set_reference_context_item(&turn_context) + .record_context_updates_and_set_reference_context_item( + &turn_context, + /*maybe_user_input*/ None, + ) .await; session.ensure_rollout_materialized().await; session.flush_rollout().await; @@ -4969,10 +4945,7 @@ async fn sample_rollout( // personality_spec) matches reconstruction. let reconstruction_turn = session.new_default_turn().await; let mut initial_context = session - .build_initial_context( - reconstruction_turn.as_ref(), - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(reconstruction_turn.as_ref(), /*maybe_user_input*/ None) .await; // Ensure personality_spec is present when Personality is enabled, so expected matches // what reconstruction produces (build_initial_context may omit it when baked into model). diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 70958ef659..f03455e341 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -201,10 +201,7 @@ async fn run_compact_task_inner( InitialContextInjection::BeforeLastUserMessage ) { let initial_context = sess - .build_initial_context( - turn_context.as_ref(), - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(turn_context.as_ref(), /*maybe_user_input*/ None) .await; new_history = insert_initial_context_before_last_real_user_or_summary(new_history, initial_context); diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index c24331292f..255f163874 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -178,11 +178,8 @@ pub(crate) async fn process_compacted_history( initial_context_injection, InitialContextInjection::BeforeLastUserMessage ) { - sess.build_initial_context( - turn_context, - /*additional_developer_sections*/ Vec::new(), - ) - .await + sess.build_initial_context(turn_context, /*maybe_user_input*/ None) + .await } else { Vec::new() }; diff --git a/codex-rs/core/src/compact_tests.rs b/codex-rs/core/src/compact_tests.rs index 1af4bc4991..628e52989a 100644 --- a/codex-rs/core/src/compact_tests.rs +++ b/codex-rs/core/src/compact_tests.rs @@ -10,10 +10,7 @@ async fn process_compacted_history_with_test_session( .set_previous_turn_settings(previous_turn_settings.cloned()) .await; let initial_context = session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await; let refreshed = crate::compact_remote::process_compacted_history( &session, diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 09b99b852d..d124119b98 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -196,10 +196,7 @@ 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 mut items = session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .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 5bb14cb2f8..9e3f09d7e3 100644 --- a/codex-rs/core/src/thread_rollout_truncation_tests.rs +++ b/codex-rs/core/src/thread_rollout_truncation_tests.rs @@ -125,10 +125,7 @@ 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 mut items = session - .build_initial_context( - &turn_context, - /*additional_developer_sections*/ Vec::new(), - ) + .build_initial_context(&turn_context, /*maybe_user_input*/ None) .await; items.push(user_msg("feature request")); items.push(assistant_msg("ack"));