core: make AGENTS.md react to environment changes (#29810)

## Why

With deferred executors, a turn can begin before a remote environment
attaches. AGENTS.md discovery previously ran only during session setup,
so instructions from a later environment never reached the model or the
session instruction sources.

WorldState persistence has now landed, so this uses the durable
model-visible baseline directly instead of carrying a temporary
resume/fork compatibility path.

## What

- Add an `AgentsMdManager` in `SessionServices` to own host
instructions, loaded state, and refresh caching.
- When `DeferredExecutor` is enabled, refresh AGENTS.md when attached
environment selections change and freeze the result in the corresponding
`StepContext`.
- Represent AGENTS.md as a persisted WorldState section for every
session, with bounded initial, replacement, and removal updates.
- Remove duplicate AGENTS.md state and rendering from
`SessionConfiguration` and `TurnContext`.
- Build initial context, per-request updates, and compaction context
from the same step-scoped value.
- On resume and fork, compare current instructions with the restored
WorldState baseline and inject a replacement exactly once when they
differ.

Builds on #29833, #29835, and #29837.

## Tests

- Covers a remote environment becoming ready mid-turn, with AGENTS.md
appearing on the next request exactly once and updating canonical
instruction sources.
- Covers full, unchanged, replaced, and removed AGENTS.md WorldState
rendering.
- Covers changed instructions across cold resume and fork without
duplicate reinjection.
- Covers remote-v2 compaction retaining creation-time instructions in
the live session and cold resume appending one replacement when the
source changed.
- Ran focused `codex-core` AGENTS.md, WorldState, and context-update
test suites.
This commit is contained in:
sayan-oai
2026-06-24 22:57:42 -07:00
committed by GitHub
Unverified
parent 51864b0b4b
commit f2f80ef442
30 changed files with 639 additions and 219 deletions
+32 -53
View File
@@ -14,7 +14,6 @@ use crate::agent::AgentControl;
use crate::agent::AgentStatus;
use crate::agent::agent_status_from_event;
use crate::agent::status::is_final;
use crate::agents_md::LoadedAgentsMd;
use crate::attestation::AttestationProvider;
use crate::build_available_skills;
use crate::compact;
@@ -248,7 +247,6 @@ use self::turn::collect_explicit_app_ids_from_skill_items;
use self::turn::realtime_text_for_event;
use self::turn_context::TurnContext;
use self::turn_context::TurnSkillsContext;
use self::world_state::build_world_state_from_environment_snapshot;
#[cfg(test)]
mod rollout_reconstruction_tests;
@@ -308,7 +306,6 @@ pub(crate) struct PreviousTurnSettings {
#[cfg(test)]
use crate::SkillMetadata;
use crate::SkillsService;
use crate::agents_md::load_project_instructions;
use crate::exec_policy::ExecPolicyUpdateError;
use crate::guardian::GuardianReviewSessionManager;
use crate::mcp::McpManager;
@@ -627,7 +624,6 @@ impl Codex {
model_reasoning_summary: config.model_reasoning_summary,
service_tier,
developer_instructions: config.developer_instructions.clone(),
loaded_agents_md: None,
personality: config.personality,
base_instructions,
compact_prompt: config.compact_prompt.clone(),
@@ -842,10 +838,11 @@ impl Codex {
}
pub(crate) async fn instruction_sources(&self) -> Vec<PathUri> {
let state = self.session.state.lock().await;
state
.session_configuration
.loaded_agents_md
self.session
.services
.agents_md_manager
.get_loaded()
.await
.as_ref()
.map_or_else(Vec::new, |instructions| instructions.sources().collect())
}
@@ -1542,13 +1539,7 @@ impl Session {
}
pub(crate) async fn user_instructions(&self) -> Option<codex_extension_api::UserInstructions> {
let state = self.state.lock().await;
state
.session_configuration
.loaded_agents_md
.as_ref()
.and_then(LoadedAgentsMd::user_instructions)
.cloned()
self.services.agents_md_manager.user_instructions()
}
pub(crate) async fn provider(&self) -> ModelProviderInfo {
@@ -2776,17 +2767,14 @@ impl Session {
self.send_raw_response_items(turn_context, items).await;
}
pub(crate) async fn record_step_environment_context_if_changed(
pub(crate) async fn record_step_world_state_if_changed(
&self,
previous_world_state: &Arc<WorldState>,
step_context: &step_context::StepContext,
) -> Arc<WorldState> {
let turn_context = step_context.turn.as_ref();
// Render model-visible state from the same step used to build and run tools.
let world_state = Arc::new(
self.build_world_state_for_environments(turn_context, &step_context.environments)
.await,
);
let world_state = Arc::new(self.build_world_state_for_step(step_context).await);
// Derive the model update and persisted patch from the same two snapshots.
let previous_snapshot = previous_world_state.snapshot();
let world_state_snapshot = world_state.snapshot();
@@ -2814,21 +2802,37 @@ impl Session {
world_state
}
/// Captures one request-scoped view of dynamic state.
///
/// This may refresh filesystem-derived state. Normal turns should call it only from
/// `run_turn` and pass the result down; standalone request or history boundaries may capture
/// their own step.
pub(crate) async fn capture_step_context(
&self,
turn_context: Arc<TurnContext>,
) -> Arc<StepContext> {
// Keep the old turn-frozen view unless deferred executors are explicitly enabled.
let environments = if turn_context
let deferred_executor_enabled = turn_context
.config
.features
.enabled(Feature::DeferredExecutor)
{
.enabled(Feature::DeferredExecutor);
// Keep the old turn-frozen environment view unless deferred executors are enabled.
let environments = if deferred_executor_enabled {
self.services.turn_environments.snapshot().await
} else {
turn_context.environments.clone()
};
Arc::new(StepContext::new(turn_context, environments))
if deferred_executor_enabled {
self.services
.agents_md_manager
.refresh(&turn_context.config, &environments)
.await;
}
let loaded_agents_md = self.services.agents_md_manager.get_loaded().await;
Arc::new(StepContext::new(
turn_context,
environments,
loaded_agents_md,
))
}
pub(crate) async fn record_inter_agent_communication(
@@ -3088,26 +3092,6 @@ impl Session {
items
}
pub(crate) async fn build_world_state_for_environments(
&self,
turn_context: &TurnContext,
environments: &TurnEnvironmentSnapshot,
) -> WorldState {
let environment_subagents = if turn_context.config.include_environment_context {
self.services
.agent_control
.format_environment_context_subagents(self.thread_id)
.await
} else {
String::new()
};
build_world_state_from_environment_snapshot(
turn_context,
environments,
&environment_subagents,
)
}
pub(crate) async fn build_initial_context_with_world_state(
&self,
turn_context: &TurnContext,
@@ -3311,9 +3295,6 @@ impl Session {
);
}
}
if let Some(user_instructions) = turn_context.user_instructions.as_deref() {
contextual_user_sections.push(user_instructions.to_string());
}
// This is full-context metadata. Steady-state context diffs should not re-emit it.
if turn_context.config.features.enabled(Feature::TokenBudget)
&& turn_context.model_context_window().is_some()
@@ -3519,8 +3500,9 @@ impl Session {
#[instrument(level = "trace", skip_all)]
pub(crate) async fn record_context_updates_and_set_reference_context_item(
&self,
turn_context: &TurnContext,
step_context: &StepContext,
) -> Arc<WorldState> {
let turn_context = step_context.turn.as_ref();
let reference_context_item = {
let state = self.state.lock().await;
state.reference_context_item()
@@ -3528,10 +3510,7 @@ impl Session {
let turn_context_item = turn_context.to_turn_context_item();
let turn_context_changed = reference_context_item.as_ref() != Some(&turn_context_item);
let should_inject_full_context = reference_context_item.is_none();
let world_state = Arc::new(
self.build_world_state_for_environments(turn_context, &turn_context.environments)
.await,
);
let world_state = Arc::new(self.build_world_state_for_step(step_context).await);
// Full initial context resets the baseline; later turns persist only its changes.
let (mut context_items, world_state_item) = if should_inject_full_context {
let context_items = self
-1
View File
@@ -126,7 +126,6 @@ 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,
user_instructions: None,
collaboration_mode: parent_turn_context.collaboration_mode.clone(),
multi_agent_version: MultiAgentVersion::Disabled,
personality: parent_turn_context.personality,
@@ -135,6 +135,7 @@ async fn record_initial_history_reconstructs_typed_inter_agent_message() {
#[tokio::test]
async fn record_initial_history_restores_world_state_baseline() {
let (session, turn_context) = make_session_and_context().await;
let turn_context = Arc::new(turn_context);
let world_state = build_world_state_from_turn_context(&session, &turn_context).await;
let rollout_items = completed_user_turn_rollout(
turn_context.to_turn_context_item(),
@@ -150,8 +151,9 @@ async fn record_initial_history_restores_world_state_baseline() {
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
let step_context = StepContext::for_test(Arc::clone(&turn_context));
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.record_context_updates_and_set_reference_context_item(&step_context)
.await;
assert_eq!(session.clone_history().await.raw_items(), &[]);
+6 -12
View File
@@ -1,6 +1,6 @@
use super::input_queue::InputQueue;
use super::*;
use crate::agents_md::LoadedAgentsMd;
use crate::agents_md_manager::AgentsMdManager;
use crate::config::ConstraintError;
use crate::environment_selection::ThreadEnvironments;
use crate::environment_selection::TurnEnvironmentSnapshot;
@@ -16,7 +16,6 @@ use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::ThreadSource;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections;
use std::sync::OnceLock;
use tokio::sync::Semaphore;
@@ -59,10 +58,6 @@ pub(crate) struct SessionConfiguration {
/// Developer instructions that supplement the base instructions.
pub(super) developer_instructions: Option<String>,
/// Model instructions assembled from provider instructions and discovered
/// AGENTS.md files.
pub(super) loaded_agents_md: Option<LoadedAgentsMd>,
/// Personality preference for the model.
pub(super) personality: Option<Personality>,
@@ -863,12 +858,10 @@ impl Session {
));
turn_environments.update_selections(session_configuration.environment_selections());
let resolved_environments = turn_environments.snapshot().await;
session_configuration.loaded_agents_md = load_project_instructions(
config.as_ref(),
user_instructions,
&resolved_environments,
)
.await;
let agents_md_manager = Arc::new(AgentsMdManager::new(user_instructions));
agents_md_manager
.refresh(config.as_ref(), &resolved_environments)
.await;
let plugin_skill_errors = warm_plugins_and_skills_for_session_init(
Arc::clone(&config),
Arc::clone(&plugins_manager),
@@ -1036,6 +1029,7 @@ impl Session {
guardian_rejection_circuit_breaker: Mutex::new(Default::default()),
runtime_handle: tokio::runtime::Handle::current(),
skills_service,
agents_md_manager,
plugins_manager: Arc::clone(&plugins_manager),
mcp_manager: Arc::clone(&mcp_manager),
extensions,
+13 -2
View File
@@ -1,5 +1,6 @@
use std::sync::Arc;
use crate::agents_md::LoadedAgentsMd;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::session::turn_context::TurnContext;
@@ -8,10 +9,20 @@ use crate::session::turn_context::TurnContext;
pub(crate) struct StepContext {
pub(crate) turn: Arc<TurnContext>,
pub(crate) environments: TurnEnvironmentSnapshot,
/// The canonical AGENTS.md value observed with this environment snapshot.
pub(crate) loaded_agents_md: Option<Arc<LoadedAgentsMd>>,
}
impl StepContext {
pub(crate) fn new(turn: Arc<TurnContext>, environments: TurnEnvironmentSnapshot) -> Self {
Self { turn, environments }
pub(crate) fn new(
turn: Arc<TurnContext>,
environments: TurnEnvironmentSnapshot,
loaded_agents_md: Option<Arc<LoadedAgentsMd>>,
) -> Self {
Self {
turn,
environments,
loaded_agents_md,
}
}
}
+73 -47
View File
@@ -1,5 +1,6 @@
use super::turn_context::TurnEnvironment;
use super::*;
use crate::agents_md_manager::AgentsMdManager;
use crate::codex_thread::TryStartTurnIfIdleRejectionReason;
use crate::config::ConfigBuilder;
use crate::config::ConfigOverrides;
@@ -186,7 +187,11 @@ use std::time::Duration as StdDuration;
impl StepContext {
pub(crate) fn for_test(turn: Arc<TurnContext>) -> Arc<Self> {
let environments = turn.environments.clone();
Arc::new(Self::new(turn, environments))
Arc::new(Self::new(
turn,
environments,
/*loaded_agents_md*/ None,
))
}
}
@@ -2082,6 +2087,7 @@ fn session_meta_item(
#[tokio::test]
async fn resumed_history_injects_initial_context_on_first_context_update_only() {
let (session, turn_context) = make_session_and_context().await;
let turn_context = Arc::new(turn_context);
let (rollout_items, mut expected) = sample_rollout(&session, &turn_context).await;
session
@@ -2095,8 +2101,9 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
let history_before_seed = session.state.lock().await.clone_history();
assert_eq!(expected, history_before_seed.raw_items());
let step_context = StepContext::for_test(Arc::clone(&turn_context));
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.record_context_updates_and_set_reference_context_item(&step_context)
.await;
let initial_context = build_initial_context(&session, &turn_context).await;
expected.extend(initial_context);
@@ -2104,7 +2111,7 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
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(&step_context)
.await;
let history_after_second_seed = session.clone_history().await;
assert_eq!(
@@ -2715,9 +2722,8 @@ async fn start_new_context_window_assigns_and_persists_item_ids() {
.await;
let rollout_path =
attach_thread_persistence(Arc::get_mut(&mut session).expect("unique session")).await;
let world_state = Arc::new(
build_world_state_from_turn_context(session.as_ref(), turn_context.as_ref()).await,
);
let world_state =
Arc::new(build_world_state_from_turn_context(session.as_ref(), &turn_context).await);
session
.start_new_context_window(turn_context.as_ref(), world_state)
@@ -3091,7 +3097,7 @@ async fn thread_rollback_drops_last_turn_from_history() {
)
.await;
let initial_context = build_initial_context(&sess, tc.as_ref()).await;
let initial_context = build_initial_context(&sess, &tc).await;
let turn_1 = vec![
user_message("turn 1 user"),
assistant_message("turn 1 assistant"),
@@ -3159,7 +3165,7 @@ async fn thread_rollback_clears_history_when_num_turns_exceeds_existing_turns()
)
.await;
let initial_context = build_initial_context(&sess, tc.as_ref()).await;
let initial_context = build_initial_context(&sess, &tc).await;
let turn_1 = vec![user_message("turn 1 user")];
let mut full_history = Vec::new();
full_history.extend(initial_context.clone());
@@ -3185,7 +3191,7 @@ async fn thread_rollback_clears_history_when_num_turns_exceeds_existing_turns()
async fn thread_rollback_fails_without_persisted_thread_history() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let initial_context = build_initial_context(&sess, tc.as_ref()).await;
let initial_context = build_initial_context(&sess, &tc).await;
sess.record_conversation_items(tc.as_ref(), &initial_context)
.await;
@@ -3595,7 +3601,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
async fn thread_rollback_fails_when_turn_in_progress() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let initial_context = build_initial_context(&sess, tc.as_ref()).await;
let initial_context = build_initial_context(&sess, &tc).await;
sess.record_conversation_items(tc.as_ref(), &initial_context)
.await;
@@ -3616,7 +3622,7 @@ async fn thread_rollback_fails_when_turn_in_progress() {
async fn thread_rollback_fails_when_num_turns_is_zero() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let initial_context = build_initial_context(&sess, tc.as_ref()).await;
let initial_context = build_initial_context(&sess, &tc).await;
sess.record_conversation_items(tc.as_ref(), &initial_context)
.await;
@@ -3655,7 +3661,6 @@ async fn set_rate_limits_retains_previous_credits() {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -3762,7 +3767,6 @@ async fn set_rate_limits_updates_plan_type_when_present() {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -4293,7 +4297,6 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -5164,7 +5167,6 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -5243,20 +5245,22 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
assert!(msg.contains("zsh fork feature enabled, but no packaged zsh fork is available"));
}
async fn build_initial_context(session: &Session, turn_context: &TurnContext) -> Vec<ResponseItem> {
async fn build_initial_context(
session: &Session,
turn_context: &Arc<TurnContext>,
) -> Vec<ResponseItem> {
let world_state = build_world_state_from_turn_context(session, turn_context).await;
session
.build_initial_context_with_world_state(turn_context, &world_state)
.build_initial_context_with_world_state(turn_context.as_ref(), &world_state)
.await
}
pub(crate) async fn build_world_state_from_turn_context(
session: &Session,
turn_context: &TurnContext,
turn_context: &Arc<TurnContext>,
) -> WorldState {
session
.build_world_state_for_environments(turn_context, &turn_context.environments)
.await
let step_context = StepContext::for_test(Arc::clone(turn_context));
session.build_world_state_for_step(&step_context).await
}
// todo: use online model info
@@ -5293,7 +5297,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -5393,6 +5396,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
guardian_rejection_circuit_breaker: Mutex::new(Default::default()),
runtime_handle: tokio::runtime::Handle::current(),
skills_service,
agents_md_manager: Arc::new(AgentsMdManager::new(/*user_instructions*/ None)),
plugins_manager,
mcp_manager,
extensions: Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
@@ -5471,7 +5475,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
"turn_id".to_string(),
skills_snapshot,
);
let session = Session {
thread_id,
installation_id: "11111111-1111-4111-8111-111111111111".to_string(),
@@ -5541,7 +5544,6 @@ async fn make_session_with_config_and_rx(
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -5648,7 +5650,6 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -7372,7 +7373,6 @@ where
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -7471,6 +7471,7 @@ where
guardian_rejection_circuit_breaker: Mutex::new(Default::default()),
runtime_handle: tokio::runtime::Handle::current(),
skills_service,
agents_md_manager: Arc::new(AgentsMdManager::new(/*user_instructions*/ None)),
plugins_manager,
mcp_manager,
extensions: Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
@@ -7549,7 +7550,6 @@ where
"turn_id".to_string(),
skills_snapshot,
));
let session = Arc::new(Session {
thread_id,
installation_id: "11111111-1111-4111-8111-111111111111".to_string(),
@@ -7718,7 +7718,7 @@ async fn record_context_updates_emits_environment_item_for_network_changes() {
current_context.config = Arc::new(config);
let update_items =
record_context_update_items(&session, &previous_context, &current_context).await;
record_context_update_items(&session, previous_context, current_context).await;
let environment_update = user_input_texts(&update_items)
.into_iter()
@@ -7749,7 +7749,7 @@ async fn record_context_updates_emits_environment_item_for_cwd_changes() {
);
let update_items =
record_context_update_items(&session, &previous_context, &current_context).await;
record_context_update_items(&session, previous_context, current_context).await;
let environment_update = user_input_texts(&update_items)
.into_iter()
@@ -7776,7 +7776,7 @@ async fn record_context_updates_emits_environment_item_for_time_changes() {
current_context.timezone = Some("Europe/Berlin".to_string());
let update_items =
record_context_update_items(&session, &previous_context, &current_context).await;
record_context_update_items(&session, previous_context, current_context).await;
let environment_update = user_input_texts(&update_items)
.into_iter()
@@ -7808,7 +7808,7 @@ async fn record_context_updates_omits_environment_item_when_disabled() {
);
let update_items =
record_context_update_items(&session, &previous_context, &current_context).await;
record_context_update_items(&session, previous_context, current_context).await;
let user_texts = user_input_texts(&update_items);
assert!(
@@ -7821,16 +7821,18 @@ async fn record_context_updates_omits_environment_item_when_disabled() {
async fn record_context_update_items(
session: &Session,
previous_context: &TurnContext,
current_context: &TurnContext,
previous_context: Arc<TurnContext>,
current_context: TurnContext,
) -> Vec<ResponseItem> {
let previous_step = StepContext::for_test(previous_context);
session
.record_context_updates_and_set_reference_context_item(previous_context)
.record_context_updates_and_set_reference_context_item(&previous_step)
.await;
let previous_len = session.clone_history().await.raw_items().len();
let current_step = StepContext::for_test(Arc::new(current_context));
session
.record_context_updates_and_set_reference_context_item(current_context)
.record_context_updates_and_set_reference_context_item(&current_step)
.await;
let history = session.clone_history().await;
history.raw_items()[previous_len..].to_vec()
@@ -7930,6 +7932,7 @@ async fn build_settings_update_items_uses_previous_turn_settings_for_realtime_en
async fn build_initial_context_uses_previous_realtime_state() {
let (session, mut turn_context) = make_session_and_context().await;
turn_context.realtime_active = true;
let turn_context = Arc::new(turn_context);
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_texts = developer_input_texts(&initial_context);
@@ -8043,6 +8046,7 @@ async fn build_initial_context_includes_prompt_fragments_from_extensions() {
.services
.thread_extension_data
.insert(PromptExtensionTestState);
let turn_context = Arc::new(turn_context);
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_messages = developer_message_texts(&initial_context);
@@ -8069,6 +8073,7 @@ async fn build_initial_context_includes_turn_context_fragments_from_extensions()
.insert(TurnContextExtensionTestState {
expected_model_context_window: Some(50),
});
let turn_context = Arc::new(turn_context);
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_messages = developer_message_texts(&initial_context);
@@ -8097,6 +8102,7 @@ async fn record_context_updates_includes_turn_context_fragments_on_steady_state_
});
let mut previous_context_item = turn_context.to_turn_context_item();
previous_context_item.turn_id = Some("previous-turn-id".to_string());
let turn_context = Arc::new(turn_context);
let world_state = build_world_state_from_turn_context(&session, &turn_context).await;
{
let mut state = session.state.lock().await;
@@ -8106,8 +8112,9 @@ async fn record_context_updates_includes_turn_context_fragments_on_steady_state_
.set_world_state_baseline(world_state.snapshot());
}
let step_context = StepContext::for_test(Arc::clone(&turn_context));
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.record_context_updates_and_set_reference_context_item(&step_context)
.await;
let history = session.clone_history().await;
@@ -8125,6 +8132,7 @@ async fn record_context_updates_includes_turn_context_fragments_on_steady_state_
async fn build_initial_context_omits_prompt_fragments_without_extension_state() {
let (mut session, turn_context) = make_session_and_context().await;
session.services.extensions = prompt_extension_test_registry();
let turn_context = Arc::new(turn_context);
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_messages = developer_message_texts(&initial_context);
@@ -8143,7 +8151,7 @@ async fn build_initial_context_adds_multi_agent_v2_root_usage_hint_as_developer_
let (session, turn_context) =
make_multi_agent_v2_usage_hint_test_session(/*enable_multi_agent_v2*/ true).await;
let initial_context = build_initial_context(&session, turn_context.as_ref()).await;
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_messages = developer_message_texts(&initial_context);
assert!(
@@ -8181,7 +8189,7 @@ async fn build_initial_context_adds_multi_agent_v2_subagent_usage_hint_as_develo
.expect("thread settings should not be shared")
.session_source = session_source;
let initial_context = build_initial_context(&session, turn_context.as_ref()).await;
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_messages = developer_message_texts(&initial_context);
assert!(
@@ -8203,7 +8211,7 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_feature_dis
let (session, turn_context) =
make_multi_agent_v2_usage_hint_test_session(/*enable_multi_agent_v2*/ false).await;
let initial_context = build_initial_context(&session, turn_context.as_ref()).await;
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_messages = developer_message_texts(&initial_context);
assert!(
@@ -8230,7 +8238,7 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_hint_is_emp
)
.await;
let initial_context = build_initial_context(&session, turn_context.as_ref()).await;
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_messages = developer_message_texts(&initial_context);
assert!(
@@ -8259,6 +8267,7 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist
/*reference_context_item*/ None,
)
.await;
let turn_context = Arc::new(turn_context);
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_texts = developer_input_texts(&initial_context);
@@ -8273,6 +8282,7 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist
#[tokio::test]
async fn build_initial_context_omits_default_image_save_location_without_image_history() {
let (session, turn_context) = make_session_and_context().await;
let turn_context = Arc::new(turn_context);
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_texts = developer_input_texts(&initial_context);
@@ -8315,6 +8325,7 @@ async fn build_initial_context_trims_skill_metadata_from_context_window_budget()
];
turn_context.model_info.context_window = Some(100);
turn_context.turn_skills = TurnSkillsContext::new(HostSkillsSnapshot::new(Arc::new(outcome)));
let turn_context = Arc::new(turn_context);
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_texts = developer_input_texts(&initial_context);
@@ -8466,6 +8477,7 @@ async fn build_initial_context_emits_thread_start_skill_warning_on_repeated_buil
];
turn_context.model_info.context_window = Some(100);
turn_context.turn_skills = TurnSkillsContext::new(HostSkillsSnapshot::new(Arc::new(outcome)));
let turn_context = Arc::new(turn_context);
let _ = build_initial_context(&session, &turn_context).await;
let warning_event = timeout(Duration::from_secs(1), rx.recv())
@@ -8598,6 +8610,7 @@ async fn build_initial_context_uses_previous_turn_settings_for_realtime_end() {
session
.set_previous_turn_settings(Some(previous_turn_settings))
.await;
let turn_context = Arc::new(turn_context);
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_texts = developer_input_texts(&initial_context);
assert!(
@@ -8621,6 +8634,7 @@ async fn build_initial_context_restates_realtime_start_when_reference_context_is
session
.set_previous_turn_settings(Some(previous_turn_settings))
.await;
let turn_context = Arc::new(turn_context);
let initial_context = build_initial_context(&session, &turn_context).await;
let developer_texts = developer_input_texts(&initial_context);
assert!(
@@ -8704,8 +8718,10 @@ async fn turn_context_item_stores_split_file_system_sandbox_policy_when_differen
async fn record_context_updates_and_set_reference_context_item_injects_full_context_when_baseline_missing()
{
let (session, turn_context) = make_session_and_context().await;
let turn_context = Arc::new(turn_context);
let step_context = StepContext::for_test(Arc::clone(&turn_context));
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.record_context_updates_and_set_reference_context_item(&step_context)
.await;
let history = session.clone_history().await;
let initial_context = build_initial_context(&session, &turn_context).await;
@@ -8723,6 +8739,8 @@ async fn record_context_updates_and_set_reference_context_item_injects_full_cont
async fn record_context_updates_and_set_reference_context_item_reinjects_full_context_after_clear()
{
let (session, turn_context) = make_session_and_context().await;
let turn_context = Arc::new(turn_context);
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let compacted_summary = ResponseItem::Message {
id: None,
role: "user".to_string(),
@@ -8736,7 +8754,7 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
.record_conversation_items(&turn_context, std::slice::from_ref(&compacted_summary))
.await;
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.record_context_updates_and_set_reference_context_item(&step_context)
.await;
{
let mut state = session.state.lock().await;
@@ -8750,7 +8768,7 @@ 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(&step_context)
.await;
let history = session.clone_history().await;
@@ -8773,6 +8791,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
.with_model(next_model.to_string(), &session.services.models_manager)
.await;
let previous_context_item = previous_context.to_turn_context_item();
let previous_context = Arc::new(previous_context);
let world_state = build_world_state_from_turn_context(&session, &previous_context).await;
{
let mut state = session.state.lock().await;
@@ -8788,8 +8807,10 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
.await;
assert_eq!(update_items, Vec::new());
let turn_context = Arc::new(turn_context);
let step_context = StepContext::for_test(Arc::clone(&turn_context));
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.record_context_updates_and_set_reference_context_item(&step_context)
.await;
assert_eq!(
@@ -8835,8 +8856,10 @@ async fn record_context_updates_and_set_reference_context_item_persists_split_fi
);
let rollout_path = attach_thread_persistence(&mut session).await;
let turn_context = Arc::new(turn_context);
let step_context = StepContext::for_test(Arc::clone(&turn_context));
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.record_context_updates_and_set_reference_context_item(&step_context)
.await;
session.ensure_rollout_materialized().await;
session.flush_rollout().await.expect("rollout should flush");
@@ -8869,6 +8892,7 @@ async fn build_initial_context_prepends_model_switch_message() {
session
.set_previous_turn_settings(Some(previous_turn_settings))
.await;
let turn_context = Arc::new(turn_context);
let initial_context = build_initial_context(&session, &turn_context).await;
let ResponseItem::Message { role, content, .. } = &initial_context[0] else {
@@ -8919,8 +8943,10 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei
realtime_active: Some(previous_context.realtime_active),
}))
.await;
let turn_context = Arc::new(turn_context);
let step_context = StepContext::for_test(Arc::clone(&turn_context));
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.record_context_updates_and_set_reference_context_item(&step_context)
.await;
session.ensure_rollout_materialized().await;
session.flush_rollout().await.expect("rollout should flush");
@@ -10195,7 +10221,7 @@ async fn sample_rollout(
// Use the same turn_context source as record_initial_history so model_info (and thus
// personality_spec) matches reconstruction.
let reconstruction_turn = session.new_default_turn().await;
let mut initial_context = build_initial_context(session, reconstruction_turn.as_ref()).await;
let mut initial_context = build_initial_context(session, &reconstruction_turn).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).
if !initial_context.iter().any(|m| {
@@ -530,6 +530,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
}
turn_context.session_source = guardian_source;
turn_context.developer_instructions = Some(guardian_policy.clone());
let turn_context = Arc::new(turn_context);
let world_state = Arc::new(build_world_state_from_turn_context(&session, &turn_context).await);
let initial_context_injection = InitialContextInjection::BeforeLastUserMessage(world_state);
+13 -4
View File
@@ -164,9 +164,11 @@ pub(crate) async fn run_turn(
return Ok(None);
}
// 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;
// Keep the exact model-visible state used by this turn and its inline compactions.
let mut world_state = sess
.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
.record_context_updates_and_set_reference_context_item(first_step_context.as_ref())
.await;
let Some((injection_items, explicitly_enabled_connectors)) =
@@ -214,6 +216,7 @@ pub(crate) async fn run_turn(
// 1. At the start of a turn, so the fresh turn input in `input` gets sampled first.
// 2. After auto-compact, when model/tool continuation needs to resume before any steer.
let mut next_step_context = Some(first_step_context);
loop {
// Note that pending_input would be something like a message the user
// submitted through the UI while the model was running. Though the UI
@@ -237,7 +240,10 @@ pub(crate) async fn run_turn(
.await;
// Capture once so context, advertised tools, and tool calls share one request view.
let step_context = sess.capture_step_context(Arc::clone(&turn_context)).await;
let step_context = match next_step_context.take() {
Some(step_context) => step_context,
None => sess.capture_step_context(Arc::clone(&turn_context)).await,
};
let sampling_request_result: CodexResult<_> = async {
super::time_reminder::maybe_record_current_time_reminder(
sess.as_ref(),
@@ -252,7 +258,7 @@ pub(crate) async fn run_turn(
.enabled(Feature::DeferredExecutor)
{
world_state = sess
.record_step_environment_context_if_changed(&world_state, step_context.as_ref())
.record_step_world_state_if_changed(&world_state, step_context.as_ref())
.await;
}
@@ -796,6 +802,7 @@ async fn run_pre_sampling_compact(
.await;
// Compact if the configured auto-compaction budget or usable context window is exhausted.
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;
run_auto_compact(
sess,
@@ -841,6 +848,7 @@ async fn maybe_run_previous_model_inline_compact(
);
if should_compact_for_comp_hash_change {
// This pre-turn request needs a step built from the previous model's turn context.
let step_context = sess
.capture_step_context(Arc::clone(&previous_model_turn_context))
.await;
@@ -881,6 +889,7 @@ async fn maybe_run_previous_model_inline_compact(
&& previous_model_turn_context.model_info.slug != turn_context.model_info.slug
&& old_context_window > new_context_window;
if should_run {
// This pre-turn request needs a step built from the previous model's turn context.
let step_context = sess
.capture_step_context(Arc::clone(&previous_model_turn_context))
.await;
@@ -916,7 +925,7 @@ async fn run_auto_compact(
// instead of consuming a pending `new_context` tool request.
crate::compact_token_budget::run_inline_auto_compact_task(
Arc::clone(sess),
Arc::clone(turn_context),
step_context,
initial_context_injection,
)
.await?;
@@ -1,5 +1,4 @@
use super::*;
use crate::agents_md::LoadedAgentsMd;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::shell_snapshot::ShellSnapshotFile;
use codex_core_skills::HostSkillsSnapshot;
@@ -124,7 +123,6 @@ pub struct TurnContext {
pub(crate) timezone: Option<String>,
pub(crate) app_server_client_name: Option<String>,
pub(crate) developer_instructions: Option<String>,
pub(crate) user_instructions: Option<String>,
pub(crate) collaboration_mode: CollaborationMode,
pub(crate) multi_agent_version: MultiAgentVersion,
pub(crate) personality: Option<Personality>,
@@ -273,7 +271,6 @@ impl TurnContext {
timezone: self.timezone.clone(),
app_server_client_name: self.app_server_client_name.clone(),
developer_instructions: self.developer_instructions.clone(),
user_instructions: self.user_instructions.clone(),
collaboration_mode,
multi_agent_version: self.multi_agent_version,
personality: self.personality,
@@ -552,10 +549,6 @@ impl Session {
timezone: Some(timezone),
app_server_client_name: session_configuration.app_server_client_name.clone(),
developer_instructions: session_configuration.developer_instructions.clone(),
user_instructions: session_configuration
.loaded_agents_md
.as_ref()
.map(LoadedAgentsMd::render),
collaboration_mode: session_configuration.collaboration_mode.clone(),
multi_agent_version,
personality: session_configuration.personality,
+30 -14
View File
@@ -1,19 +1,35 @@
use super::turn_context::TurnContext;
use super::session::Session;
use super::step_context::StepContext;
use crate::context::world_state::AgentsMdState;
use crate::context::world_state::EnvironmentsState;
use crate::context::world_state::WorldState;
use crate::environment_selection::TurnEnvironmentSnapshot;
pub(super) fn build_world_state_from_environment_snapshot(
turn_context: &TurnContext,
environments: &TurnEnvironmentSnapshot,
environment_subagents: &str,
) -> WorldState {
let mut world_state = WorldState::default();
if turn_context.config.include_environment_context {
world_state.add_section(
EnvironmentsState::from_turn_context_with_environments(turn_context, environments)
.with_subagents(environment_subagents.to_string()),
);
impl Session {
pub(crate) async fn build_world_state_for_step(
&self,
step_context: &StepContext,
) -> WorldState {
let turn_context = step_context.turn.as_ref();
let environment_subagents = if turn_context.config.include_environment_context {
self.services
.agent_control
.format_environment_context_subagents(self.thread_id)
.await
} else {
String::new()
};
let mut world_state = WorldState::default();
world_state.add_section(AgentsMdState::new(step_context.loaded_agents_md.as_deref()));
if turn_context.config.include_environment_context {
world_state.add_section(
EnvironmentsState::from_turn_context_with_environments(
turn_context,
&step_context.environments,
)
.with_subagents(environment_subagents),
);
}
world_state
}
world_state
}