diff --git a/codex-rs/core/src/context/environments_instructions.rs b/codex-rs/core/src/context/environments_instructions.rs new file mode 100644 index 0000000000..7a4cfe676e --- /dev/null +++ b/codex-rs/core/src/context/environments_instructions.rs @@ -0,0 +1,33 @@ +use codex_protocol::protocol::ENVIRONMENTS_INSTRUCTIONS_CLOSE_TAG; +use codex_protocol::protocol::ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG; + +use super::ContextualUserFragment; + +pub(crate) struct EnvironmentsInstructions; + +impl ContextualUserFragment for EnvironmentsInstructions { + fn role(&self) -> &'static str { + "developer" + } + + fn markers(&self) -> (&'static str, &'static str) { + Self::type_markers() + } + + fn type_markers() -> (&'static str, &'static str) { + ( + ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG, + ENVIRONMENTS_INSTRUCTIONS_CLOSE_TAG, + ) + } + + fn body(&self) -> String { + "\n## Execution environments\n\ +Execution environments are separate machines or workspaces with their own files, shell, and installed capabilities. `` lists the environments selected for this task.\n\ +\n\ +An environment marked `starting` is not yet usable. Its files, commands, AGENTS.md instructions, skills, plugins, and MCP tools may become available when startup completes.\n\ +\n\ +Wait only when the current task needs that environment. Continue using tools that are already available for unrelated work.\n" + .to_string() + } +} diff --git a/codex-rs/core/src/context/mod.rs b/codex-rs/core/src/context/mod.rs index 8987f90e3c..4d8f1817ad 100644 --- a/codex-rs/core/src/context/mod.rs +++ b/codex-rs/core/src/context/mod.rs @@ -8,6 +8,7 @@ mod collaboration_mode_instructions; mod contextual_user_message; mod current_time_reminder; mod environment_context; +mod environments_instructions; mod guardian_followup_review_reminder; mod hook_additional_context; mod image_generation_instructions; @@ -48,6 +49,7 @@ pub(crate) use collaboration_mode_instructions::CollaborationModeInstructions; pub(crate) use contextual_user_message::is_contextual_user_fragment; pub(crate) use contextual_user_message::parse_visible_hook_prompt_message; pub(crate) use current_time_reminder::CurrentTimeReminder; +pub(crate) use environments_instructions::EnvironmentsInstructions; pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder; pub(crate) use hook_additional_context::HookAdditionalContext; pub use image_generation_instructions::extension_image_generation_output_hint; diff --git a/codex-rs/core/src/context/world_state/environments_instructions.rs b/codex-rs/core/src/context/world_state/environments_instructions.rs new file mode 100644 index 0000000000..47576ed53c --- /dev/null +++ b/codex-rs/core/src/context/world_state/environments_instructions.rs @@ -0,0 +1,55 @@ +use super::PreviousSectionState; +use super::WorldStateSection; +use crate::context::ContextualUserFragment; +use crate::context::EnvironmentsInstructions; + +/// Whether generic execution-environment guidance should be visible to the model. +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct EnvironmentsInstructionsState { + enabled: bool, +} + +impl EnvironmentsInstructionsState { + pub(crate) fn new(enabled: bool) -> Self { + Self { enabled } + } +} + +impl WorldStateSection for EnvironmentsInstructionsState { + const ID: &'static str = "environments_instructions"; + type Snapshot = bool; + + fn snapshot(&self) -> Self::Snapshot { + self.enabled + } + + fn matches_legacy_fragment(role: &str, text: &str) -> bool { + role == "developer" && EnvironmentsInstructions::matches_text(text) + } + + fn has_retained_fragment_matcher() -> bool { + true + } + + fn matches_retained_fragment(role: &str, text: &str) -> bool { + Self::matches_legacy_fragment(role, text) + } + + fn render_diff( + &self, + previous: PreviousSectionState<'_, Self::Snapshot>, + ) -> Option> { + if !self.enabled + || matches!(previous, PreviousSectionState::Known(previous) if *previous) + || matches!(previous, PreviousSectionState::Unknown) + { + return None; + } + + Some(Box::new(EnvironmentsInstructions)) + } +} + +#[cfg(test)] +#[path = "environments_instructions_tests.rs"] +mod tests; diff --git a/codex-rs/core/src/context/world_state/environments_instructions_tests.rs b/codex-rs/core/src/context/world_state/environments_instructions_tests.rs new file mode 100644 index 0000000000..c3013494e4 --- /dev/null +++ b/codex-rs/core/src/context/world_state/environments_instructions_tests.rs @@ -0,0 +1,57 @@ +use super::*; +use crate::context::ContextualUserFragment; +use crate::context::world_state::test_support::render_section_cases; +use codex_protocol::models::ResponseItem; +use pretty_assertions::assert_eq; + +#[test] +fn snapshots() { + use PreviousSectionState::Absent; + use PreviousSectionState::Known; + use PreviousSectionState::Unknown; + + let disabled = EnvironmentsInstructionsState::new(/*enabled*/ false); + let enabled = EnvironmentsInstructionsState::new(/*enabled*/ true); + + insta::assert_snapshot!(render_section_cases(&[ + (Absent, Absent), + (Absent, Known(&disabled)), + (Absent, Known(&enabled)), + (Known(&disabled), Known(&enabled)), + (Known(&enabled), Known(&enabled)), + (Known(&enabled), Known(&disabled)), + (Unknown, Known(&disabled)), + (Unknown, Known(&enabled)), + ])); +} + +#[test] +fn legacy_guidance_is_not_injected_again() { + let mut world_state = super::super::WorldState::default(); + world_state.add_section(EnvironmentsInstructionsState::new(/*enabled*/ true)); + let legacy: ResponseItem = ContextualUserFragment::into(EnvironmentsInstructions); + + assert!( + world_state + .render_history_diff(/*previous*/ None, &[legacy]) + .is_empty() + ); +} + +#[test] +fn persisted_guidance_is_restored_only_when_missing_from_history() { + let mut world_state = super::super::WorldState::default(); + world_state.add_section(EnvironmentsInstructionsState::new(/*enabled*/ true)); + let snapshot = world_state.snapshot(); + let retained: ResponseItem = ContextualUserFragment::into(EnvironmentsInstructions); + + assert_eq!( + world_state.render_history_diff(Some(&snapshot), &[]).len(), + 1 + ); + assert!( + world_state + .render_history_diff(Some(&snapshot), &[retained]) + .is_empty() + ); +} diff --git a/codex-rs/core/src/context/world_state/mod.rs b/codex-rs/core/src/context/world_state/mod.rs index bb77c6f3da..fe90af711f 100644 --- a/codex-rs/core/src/context/world_state/mod.rs +++ b/codex-rs/core/src/context/world_state/mod.rs @@ -1,6 +1,7 @@ mod agents_md; mod apps_instructions; mod environment; +mod environments_instructions; mod plugins_instructions; #[cfg(test)] mod test_support; @@ -22,6 +23,7 @@ use std::fmt; pub(crate) use agents_md::AgentsMdState; pub(crate) use apps_instructions::AppsInstructionsState; pub(crate) use environment::EnvironmentsState; +pub(crate) use environments_instructions::EnvironmentsInstructionsState; pub(crate) use plugins_instructions::PluginsInstructionsState; trait ErasedWorldStateSection: Send + Sync { diff --git a/codex-rs/core/src/context/world_state/snapshots/codex_core__context__world_state__environments_instructions__tests__snapshots.snap b/codex-rs/core/src/context/world_state/snapshots/codex_core__context__world_state__environments_instructions__tests__snapshots.snap new file mode 100644 index 0000000000..d6fee85db9 --- /dev/null +++ b/codex-rs/core/src/context/world_state/snapshots/codex_core__context__world_state__environments_instructions__tests__snapshots.snap @@ -0,0 +1,41 @@ +--- +source: core/src/context/world_state/environments_instructions_tests.rs +expression: "render_section_cases(&[(Absent, Absent), (Absent, Known(&disabled)),\n(Absent, Known(&enabled)), (Known(&disabled), Known(&enabled)),\n(Known(&enabled), Known(&enabled)), (Known(&enabled), Known(&disabled)),\n(Unknown, Known(&disabled)), (Unknown, Known(&enabled)),])" +--- +Absent -> Absent +None + +Absent -> false +None + +Absent -> true (role - developer) + +## Execution environments +Execution environments are separate machines or workspaces with their own files, shell, and installed capabilities. `` lists the environments selected for this task. + +An environment marked `starting` is not yet usable. Its files, commands, AGENTS.md instructions, skills, plugins, and MCP tools may become available when startup completes. + +Wait only when the current task needs that environment. Continue using tools that are already available for unrelated work. + + +false -> true (role - developer) + +## Execution environments +Execution environments are separate machines or workspaces with their own files, shell, and installed capabilities. `` lists the environments selected for this task. + +An environment marked `starting` is not yet usable. Its files, commands, AGENTS.md instructions, skills, plugins, and MCP tools may become available when startup completes. + +Wait only when the current task needs that environment. Continue using tools that are already available for unrelated work. + + +true -> true +None + +true -> false +None + +Unknown -> false +None + +Unknown -> true +None diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index ccd32bb0e3..8a21b6b775 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -23,6 +23,7 @@ use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::default_input_modalities; use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::SandboxPolicy; @@ -1031,6 +1032,9 @@ fn drop_last_n_user_turns_trims_context_updates_above_rolled_back_turn() { developer_msg(&format!( "{PLUGINS_INSTRUCTIONS_OPEN_TAG}\nROLLED_BACK_PLUGIN_INSTRUCTIONS" )), + developer_msg(&format!( + "{ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG}\nROLLED_BACK_ENVIRONMENT_INSTRUCTIONS" + )), developer_msg("ROLLED_BACK_DEV_INSTRUCTIONS"), developer_msg("ROLLED_BACK_MULTI_AGENT_MODE"), user_input_text_msg( diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index 1339eea03b..6a474567b3 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -18,6 +18,7 @@ use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG; +use codex_protocol::protocol::ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG; use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG; @@ -36,6 +37,7 @@ const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[ APPS_INSTRUCTIONS_OPEN_TAG, COLLABORATION_MODE_OPEN_TAG, MULTI_AGENT_MODE_OPEN_TAG, + ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG, PLUGINS_INSTRUCTIONS_OPEN_TAG, REALTIME_CONVERSATION_OPEN_TAG, SKILLS_INSTRUCTIONS_OPEN_TAG, diff --git a/codex-rs/core/src/session/world_state.rs b/codex-rs/core/src/session/world_state.rs index c7743c3fe1..e4632544f4 100644 --- a/codex-rs/core/src/session/world_state.rs +++ b/codex-rs/core/src/session/world_state.rs @@ -3,10 +3,12 @@ use super::step_context::StepContext; use crate::connectors; use crate::context::world_state::AgentsMdState; use crate::context::world_state::AppsInstructionsState; +use crate::context::world_state::EnvironmentsInstructionsState; use crate::context::world_state::EnvironmentsState; use crate::context::world_state::PluginsInstructionsState; use crate::context::world_state::WorldState; use codex_extension_api::WorldStateContributionInput; +use codex_features::Feature; impl Session { #[tracing::instrument(name = "world_state.build", level = "info", skip_all)] @@ -39,6 +41,13 @@ impl Session { .with_subagents(environment_subagents), ); } + world_state.add_section(EnvironmentsInstructionsState::new( + turn_context.config.include_environment_context + && turn_context + .config + .features + .enabled(Feature::DeferredExecutor), + )); let apps_available = if turn_context.config.include_apps_instructions && turn_context.apps_enabled() { let tools = step_context.mcp_tools().await; diff --git a/codex-rs/core/tests/suite/remote_env.rs b/codex-rs/core/tests/suite/remote_env.rs index e19bab7eac..cdeaef1832 100644 --- a/codex-rs/core/tests/suite/remote_env.rs +++ b/codex-rs/core/tests/suite/remote_env.rs @@ -28,6 +28,7 @@ use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; @@ -690,6 +691,9 @@ async fn deferred_executor_loads_agents_md_when_environment_becomes_ready() -> R assert_eq!(agents_md_occurrences(&requests[0], AGENTS_CONTENT), 0); assert_eq!(agents_md_occurrences(&requests[1], AGENTS_CONTENT), 1); assert_eq!(agents_md_occurrences(&requests[2], AGENTS_CONTENT), 1); + assert_eq!(environment_instructions_occurrences(&requests[0]), 1); + assert_eq!(environment_instructions_occurrences(&requests[1]), 1); + assert_eq!(environment_instructions_occurrences(&requests[2]), 1); assert_eq!(test.codex.instruction_sources().await, vec![agents_path]); Ok(()) @@ -703,6 +707,14 @@ fn agents_md_occurrences(request: &ResponsesRequest, contents: &str) -> usize { .count() } +fn environment_instructions_occurrences(request: &ResponsesRequest) -> usize { + request + .message_input_texts("developer") + .iter() + .filter(|text| text.contains(ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG)) + .count() +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn deferred_executor_wait_reports_startup_failure() -> Result<()> { let server = start_mock_server().await; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index d5bc2d5c08..d64b4bb96e 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -103,6 +103,8 @@ pub const USER_INSTRUCTIONS_OPEN_TAG: &str = ""; pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = ""; pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = ""; pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = ""; +pub const ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG: &str = ""; +pub const ENVIRONMENTS_INSTRUCTIONS_CLOSE_TAG: &str = ""; pub const APPS_INSTRUCTIONS_OPEN_TAG: &str = ""; pub const APPS_INSTRUCTIONS_CLOSE_TAG: &str = ""; pub const SKILLS_INSTRUCTIONS_OPEN_TAG: &str = "";