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
parent 51864b0b4b
commit f2f80ef442
30 changed files with 639 additions and 219 deletions
+1 -9
View File
@@ -16,7 +16,6 @@
//! 3. We do **not** walk past the project root.
use crate::config::Config;
use crate::context::ContextualUserFragment;
use crate::context::UserInstructions as ContextUserInstructions;
use crate::environment_selection::TurnEnvironmentSnapshot;
use codex_config::ConfigLayerSource;
@@ -378,8 +377,7 @@ impl LoadedAgentsMd {
output
}
/// Returns the complete model-visible contextual user fragment.
pub(crate) fn render(&self) -> String {
pub(crate) fn contextual_user_fragment(&self) -> ContextUserInstructions {
// One contributing project environment retains the legacy cwd wrapper. With two or more,
// the body labels every contributing environment itself, so the outer cwd is omitted.
let directory = if self.has_multiple_project_environments() {
@@ -392,12 +390,6 @@ impl LoadedAgentsMd {
directory,
text: self.text(),
}
.render()
}
/// Returns the host-provided user instructions.
pub(crate) fn user_instructions(&self) -> Option<&UserInstructions> {
self.user_instructions.as_ref()
}
/// Returns the AGENTS.md files that supplied instruction entries.
+53
View File
@@ -0,0 +1,53 @@
use crate::agents_md::LoadedAgentsMd;
use crate::agents_md::load_project_instructions;
use crate::config::Config;
use crate::environment_selection::TurnEnvironmentSnapshot;
use codex_extension_api::UserInstructions;
use codex_protocol::protocol::TurnEnvironmentSelection;
use std::sync::Arc;
use tokio::sync::Mutex;
/// Owns the inputs and cached result of AGENTS.md discovery for a session.
pub(crate) struct AgentsMdManager {
user_instructions: Option<UserInstructions>,
cache: Mutex<AgentsMdCache>,
}
#[derive(Default)]
struct AgentsMdCache {
selections: Option<Vec<TurnEnvironmentSelection>>,
loaded: Option<Arc<LoadedAgentsMd>>,
}
impl AgentsMdManager {
pub(crate) fn new(user_instructions: Option<UserInstructions>) -> Self {
Self {
user_instructions: user_instructions
.filter(|instructions| !instructions.text.trim().is_empty()),
cache: Mutex::new(AgentsMdCache::default()),
}
}
pub(crate) async fn refresh(&self, config: &Config, environments: &TurnEnvironmentSnapshot) {
let selections = environments.to_selections();
if self.cache.lock().await.selections.as_ref() == Some(&selections) {
return;
}
let loaded =
load_project_instructions(config, self.user_instructions.clone(), environments)
.await
.map(Arc::new);
let mut cache = self.cache.lock().await;
cache.selections = Some(selections);
cache.loaded = loaded;
}
pub(crate) async fn get_loaded(&self) -> Option<Arc<LoadedAgentsMd>> {
self.cache.lock().await.loaded.clone()
}
pub(crate) fn user_instructions(&self) -> Option<UserInstructions> {
self.user_instructions.clone()
}
}
+15 -6
View File
@@ -1,5 +1,6 @@
use super::*;
use crate::config::ConfigBuilder;
use crate::context::ContextualUserFragment;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::session::turn_context::TurnEnvironment;
use codex_config::ConfigLayerEntry;
@@ -345,7 +346,7 @@ fn foreign_agents_md_uses_environment_native_paths() {
};
assert_eq!(
loaded.render(),
loaded.contextual_user_fragment().render(),
format!(
"# AGENTS.md instructions for {rendered_cwd}
@@ -388,7 +389,7 @@ fn multi_environment_agents_md_renders_mixed_path_conventions() {
};
assert_eq!(
loaded.render(),
loaded.contextual_user_fragment().render(),
r#"# AGENTS.md instructions
<INSTRUCTIONS>
@@ -931,7 +932,10 @@ secondary doc"#,
{inner}
</INSTRUCTIONS>"#
);
assert_eq!(loaded.render(), expected_fragment);
assert_eq!(
loaded.contextual_user_fragment().render(),
expected_fragment
);
assert_eq!(
loaded.sources().collect::<Vec<_>>(),
vec![
@@ -972,7 +976,10 @@ async fn secondary_only_project_doc_uses_single_contributor_layout() {
"# AGENTS.md instructions for {}\n\n<INSTRUCTIONS>\n{inner}\n</INSTRUCTIONS>",
secondary.path().display()
);
assert_eq!(loaded.render(), expected_fragment);
assert_eq!(
loaded.contextual_user_fragment().render(),
expected_fragment
);
}
#[tokio::test]
@@ -998,7 +1005,10 @@ async fn primary_only_project_doc_preserves_legacy_layout_with_multiple_bound_en
"# AGENTS.md instructions for {}\n\n<INSTRUCTIONS>\n{inner}\n</INSTRUCTIONS>",
primary.path().display()
);
assert_eq!(loaded.render(), expected_fragment);
assert_eq!(
loaded.contextual_user_fragment().render(),
expected_fragment
);
}
#[tokio::test]
@@ -1276,7 +1286,6 @@ async fn instruction_sources_include_global_before_agents_md_docs() {
}],
};
assert_eq!(loaded, expected);
assert_eq!(loaded.user_instructions(), cfg.user_instructions.as_ref());
assert_eq!(
loaded.sources().collect::<Vec<_>>(),
vec![
+7 -1
View File
@@ -466,9 +466,15 @@ impl CodexThread {
let turn_context = self.codex.session.new_default_turn().await;
if self.codex.session.reference_context_item().await.is_none() {
// This history-only API runs without run_turn, so it owns its initial step.
let step_context = self
.codex
.session
.capture_step_context(Arc::clone(&turn_context))
.await;
self.codex
.session
.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
.record_context_updates_and_set_reference_context_item(step_context.as_ref())
.await;
}
self.codex
+1
View File
@@ -12,6 +12,7 @@ async fn process_compacted_history_with_test_session(
previous_turn_settings: Option<&PreviousTurnSettings>,
) -> (Vec<ResponseItem>, Vec<ResponseItem>) {
let (session, turn_context) = crate::session::tests::make_session_and_context().await;
let turn_context = Arc::new(turn_context);
session
.set_previous_turn_settings(previous_turn_settings.cloned())
.await;
+10 -10
View File
@@ -7,6 +7,7 @@ use crate::hook_runtime::PreCompactHookOutcome;
use crate::hook_runtime::run_post_compact_hooks;
use crate::hook_runtime::run_pre_compact_hooks;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnContext;
use codex_analytics::CompactionTrigger;
use codex_protocol::error::CodexErr;
@@ -34,10 +35,9 @@ pub(crate) async fn run_manual_compact_task(
});
sess.send_event(&turn_context, start_event).await;
let world_state = Arc::new(
sess.build_world_state_for_environments(&turn_context, &turn_context.environments)
.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
}
@@ -48,17 +48,17 @@ pub(crate) async fn run_manual_compact_task(
/// observe the same lifecycle as local or remote compaction.
pub(crate) async fn run_inline_auto_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
step_context: Arc<StepContext>,
initial_context_injection: InitialContextInjection,
) -> CodexResult<()> {
let turn_context = &step_context.turn;
let world_state = match initial_context_injection {
InitialContextInjection::BeforeLastUserMessage(world_state) => world_state,
InitialContextInjection::DoNotInject => Arc::new(
sess.build_world_state_for_environments(&turn_context, &turn_context.environments)
.await,
),
InitialContextInjection::DoNotInject => {
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, world_state, CompactionTrigger::Auto).await
}
async fn run_compact_task_inner(
@@ -0,0 +1,75 @@
use super::WorldStateSection;
use crate::agents_md::LoadedAgentsMd;
use crate::context::ContextualUserFragment;
use crate::context::UserInstructions;
use serde::Deserialize;
use serde::Serialize;
const REPLACEMENT_NOTICE: &str =
"These AGENTS.md instructions replace all previously provided AGENTS.md instructions.";
const REMOVAL_NOTICE: &str = "The previously provided AGENTS.md instructions no longer apply.";
/// The AGENTS.md instructions currently visible to the model.
#[derive(Clone, Debug, Default)]
pub(crate) struct AgentsMdState {
instructions: Option<UserInstructions>,
}
/// Persisted model-visible AGENTS.md state, without filesystem provenance.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
pub(crate) struct AgentsMdSnapshot {
directory: Option<String>,
text: Option<String>,
}
impl AgentsMdState {
pub(crate) fn new(loaded: Option<&LoadedAgentsMd>) -> Self {
Self {
instructions: loaded.map(LoadedAgentsMd::contextual_user_fragment),
}
}
}
impl WorldStateSection for AgentsMdState {
const ID: &'static str = "agents_md";
type Snapshot = AgentsMdSnapshot;
fn snapshot(&self) -> Self::Snapshot {
match &self.instructions {
Some(instructions) => AgentsMdSnapshot {
directory: instructions.directory.clone(),
text: Some(instructions.text.clone()),
},
None => AgentsMdSnapshot::default(),
}
}
fn render_diff(
&self,
previous: Option<&Self::Snapshot>,
) -> Option<Box<dyn ContextualUserFragment>> {
let current = self.snapshot();
if previous == Some(&current) {
return None;
}
let previous_instructions = previous.and_then(|state| state.text.as_ref());
let instructions = match (&self.instructions, previous_instructions) {
(Some(instructions), Some(_)) => UserInstructions {
directory: instructions.directory.clone(),
text: format!("{REPLACEMENT_NOTICE}\n\n{}", instructions.text),
},
(Some(instructions), None) => instructions.clone(),
(None, Some(_)) => UserInstructions {
directory: None,
text: REMOVAL_NOTICE.to_string(),
},
(None, None) => return None,
};
Some(Box::new(instructions))
}
}
#[cfg(test)]
#[path = "agents_md_tests.rs"]
mod tests;
@@ -0,0 +1,73 @@
use super::*;
use crate::context::world_state::WorldState;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn renders_full_state_and_omits_unchanged_state() {
let loaded = LoadedAgentsMd::from_text_for_testing("use the project formatter");
let mut state = WorldState::default();
state.add_section(AgentsMdState::new(Some(&loaded)));
assert_eq!(
vec![user_message(
"# AGENTS.md instructions\n\n<INSTRUCTIONS>\nuse the project formatter\n</INSTRUCTIONS>",
)],
render_fragments(state.render_full()),
);
assert_eq!(
Vec::<ResponseItem>::new(),
render_fragments(state.render_diff(&state.snapshot()))
);
assert_eq!(
state.snapshot().into_value(),
json!({"agents_md": {"text": "use the project formatter"}}),
);
}
#[test]
fn changed_and_removed_state_supersedes_previous_instructions() {
let previous_loaded = LoadedAgentsMd::from_text_for_testing("old instructions");
let mut previous = WorldState::default();
previous.add_section(AgentsMdState::new(Some(&previous_loaded)));
let current_loaded = LoadedAgentsMd::from_text_for_testing("new instructions");
let mut current = WorldState::default();
current.add_section(AgentsMdState::new(Some(&current_loaded)));
assert_eq!(
vec![user_message(
"# AGENTS.md instructions\n\n<INSTRUCTIONS>\nThese AGENTS.md instructions replace all previously provided AGENTS.md instructions.\n\nnew instructions\n</INSTRUCTIONS>",
)],
render_fragments(current.render_diff(&previous.snapshot())),
);
let mut removed = WorldState::default();
removed.add_section(AgentsMdState::default());
assert_eq!(
vec![user_message(
"# AGENTS.md instructions\n\n<INSTRUCTIONS>\nThe previously provided AGENTS.md instructions no longer apply.\n</INSTRUCTIONS>",
)],
render_fragments(removed.render_diff(&current.snapshot())),
);
}
fn render_fragments(fragments: Vec<Box<dyn ContextualUserFragment>>) -> Vec<ResponseItem> {
fragments
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect()
}
fn user_message(text: &str) -> ResponseItem {
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: text.to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -1,3 +1,4 @@
mod agents_md;
mod environment;
use crate::context::ContextualUserFragment;
@@ -9,6 +10,7 @@ use serde_json::Value;
use std::collections::BTreeMap;
use std::fmt;
pub(crate) use agents_md::AgentsMdState;
pub(crate) use environment::EnvironmentsState;
trait ErasedWorldStateSection: Send + Sync {
-3
View File
@@ -248,7 +248,6 @@ async fn guardian_test_session_turn_and_rx(
turn_mut.config = Arc::clone(&config);
turn_mut.provider =
create_model_provider(config.model_provider.clone(), turn_mut.auth_manager.clone());
turn_mut.user_instructions = None;
(session, turn, rx)
}
@@ -280,7 +279,6 @@ async fn guardian_test_session_and_turn_with_base_url(
session.services.models_manager = models_manager;
turn.config = Arc::clone(&config);
turn.provider = create_model_provider(config.model_provider.clone(), turn.auth_manager.clone());
turn.user_instructions = None;
(Arc::new(session), Arc::new(turn))
}
@@ -2305,7 +2303,6 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() ->
turn_mut.config = Arc::clone(&config);
turn_mut.provider =
create_model_provider(config.model_provider.clone(), turn_mut.auth_manager.clone());
turn_mut.user_instructions = None;
seed_guardian_parent_history(&session, &turn).await;
+1
View File
@@ -130,6 +130,7 @@ pub type NewConversation = NewThread;
#[deprecated(note = "use CodexThread")]
pub type CodexConversation = CodexThread;
pub(crate) mod agents_md;
mod agents_md_manager;
pub use agents_md::DEFAULT_AGENTS_MD_FILENAME;
pub use agents_md::LOCAL_AGENTS_MD_FILENAME;
pub use agents_md::LoadedAgentsMd;
+3 -13
View File
@@ -3,7 +3,6 @@ use std::sync::Arc;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecServerRuntimePaths;
use codex_extension_api::UserInstructionsProvider;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
@@ -78,8 +77,9 @@ pub(crate) async fn build_prompt_input_from_session(
input: Vec<UserInput>,
) -> CodexResult<Vec<ResponseItem>> {
let turn_context = sess.new_default_turn().await;
let world_state = sess
.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
// Prompt debugging builds a standalone request without entering run_turn.
let step_context = sess.capture_step_context(Arc::clone(&turn_context)).await;
sess.record_context_updates_and_set_reference_context_item(step_context.as_ref())
.await;
if !input.is_empty() {
@@ -88,16 +88,6 @@ pub(crate) async fn build_prompt_input_from_session(
.await;
}
let step_context = sess.capture_step_context(Arc::clone(&turn_context)).await;
if turn_context
.config
.features
.enabled(Feature::DeferredExecutor)
{
sess.record_step_environment_context_if_changed(&world_state, step_context.as_ref())
.await;
}
let prompt_input = sess
.clone_history()
.await
+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
}
@@ -266,6 +266,7 @@ 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;
+2
View File
@@ -4,6 +4,7 @@ use std::sync::atomic::AtomicBool;
use crate::SkillsService;
use crate::agent::AgentControl;
use crate::agents_md_manager::AgentsMdManager;
use crate::attestation::AttestationProvider;
use crate::client::ModelClient;
use crate::config::NetworkProxyAuditMetadata;
@@ -64,6 +65,7 @@ pub(crate) struct SessionServices {
pub(crate) guardian_rejection_circuit_breaker: Mutex<GuardianRejectionCircuitBreaker>,
pub(crate) runtime_handle: Handle,
pub(crate) skills_service: Arc<SkillsService>,
pub(crate) agents_md_manager: Arc<AgentsMdManager>,
pub(crate) plugins_manager: Arc<PluginsManager>,
pub(crate) mcp_manager: Arc<McpManager>,
pub(crate) extensions: Arc<ExtensionRegistry<crate::config::Config>>,
@@ -304,6 +304,7 @@ fn out_of_range_truncation_drops_pre_user_active_turn_prefix() {
#[tokio::test]
async fn ignores_session_prefix_messages_when_truncating() {
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 mut items = session
.build_initial_context_with_world_state(&turn_context, &world_state)
@@ -7,6 +7,7 @@ use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::protocol::InterAgentCommunication;
use codex_protocol::protocol::ThreadRolledBackEvent;
use pretty_assertions::assert_eq;
use std::sync::Arc;
fn user_msg(text: &str) -> ResponseItem {
ResponseItem::Message {
@@ -167,6 +168,7 @@ fn truncates_rollout_from_start_applies_thread_rollback_markers() {
#[tokio::test]
async fn ignores_session_prefix_messages_when_truncating_rollout_from_start() {
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 mut items = session
.build_initial_context_with_world_state(&turn_context, &world_state)
+5 -1
View File
@@ -686,7 +686,11 @@ async fn environment_tools_follow_the_step_context() {
let environments = turn.environments.clone();
turn.environments.turn_environments.clear();
let step_context = Arc::new(StepContext::new(Arc::new(turn), environments));
let step_context = Arc::new(StepContext::new(
Arc::new(turn),
environments,
/*loaded_agents_md*/ None,
));
let plan = ToolPlanProbe::from_router(ToolRouter::from_context(
step_context.as_ref(),