Clarify when to wait for starting environments (#33636)

## Why

An execution environment marked `starting` is not yet usable, but unrelated work can continue while it becomes ready.

## What changed

- Add developer guidance that explains execution-environment isolation and the resources that become available after startup.
- Tell the model to wait only when the current task needs the starting environment.
- Retain the guidance across world-state updates without duplicating it in request history.

## Testing

- Cover world-state transitions, history retention, rollback handling, and deferred-environment request deduplication.

GitOrigin-RevId: f12f9204d4cbddce50a70eb0998beb6b66cd2fa9
This commit is contained in:
sayan-oai
2026-07-16 16:27:22 +00:00
committed by copyberry
parent 41efc6333e
commit aff7c69659
11 changed files with 219 additions and 0 deletions
@@ -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. `<environment_context>` 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()
}
}
+2
View File
@@ -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;
@@ -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<Box<dyn ContextualUserFragment>> {
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;
@@ -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()
);
}
@@ -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 {
@@ -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)
<environments_instructions>
## Execution environments
Execution environments are separate machines or workspaces with their own files, shell, and installed capabilities. `<environment_context>` 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.
</environments_instructions>
false -> true (role - developer)
<environments_instructions>
## Execution environments
Execution environments are separate machines or workspaces with their own files, shell, and installed capabilities. `<environment_context>` 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.
</environments_instructions>
true -> true
None
true -> false
None
Unknown -> false
None
Unknown -> true
None
@@ -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("<collaboration_mode>ROLLED_BACK_DEV_INSTRUCTIONS</collaboration_mode>"),
developer_msg("<multi_agent_mode>ROLLED_BACK_MULTI_AGENT_MODE</multi_agent_mode>"),
user_input_text_msg(
+2
View File
@@ -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,
+9
View File
@@ -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;
+12
View File
@@ -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;
+2
View File
@@ -103,6 +103,8 @@ pub const USER_INSTRUCTIONS_OPEN_TAG: &str = "<user_instructions>";
pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = "</user_instructions>";
pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = "<environment_context>";
pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = "</environment_context>";
pub const ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG: &str = "<environments_instructions>";
pub const ENVIRONMENTS_INSTRUCTIONS_CLOSE_TAG: &str = "</environments_instructions>";
pub const APPS_INSTRUCTIONS_OPEN_TAG: &str = "<apps_instructions>";
pub const APPS_INSTRUCTIONS_CLOSE_TAG: &str = "</apps_instructions>";
pub const SKILLS_INSTRUCTIONS_OPEN_TAG: &str = "<skills_instructions>";