Track model and personality in world state (#35530)

## What changed

- Add model and personality sections to the persisted world-state snapshot.
- Generate model-switch and personality instructions through world-state diffs, including when prior state must be inferred during replay.
- Keep those instruction fragments in standalone developer messages instead of merging them with adjacent context updates.

## Testing

- Cover initial, unchanged, and changed model and personality states.
- Verify that model changes are recorded in rollout world-state items.

GitOrigin-RevId: 497e29cf38cac08e992e546df900f6f8b01120f9
This commit is contained in:
sayan-oai
2026-07-26 23:36:33 +00:00
committed by copyberry
parent 0896bf6fc0
commit 18f50c9e62
13 changed files with 421 additions and 148 deletions
+9 -57
View File
@@ -26,8 +26,8 @@ use crate::config::resolve_tool_suggest_config_from_layer_stack;
use crate::context::ApprovedCommandPrefixSaved;
use crate::context::AvailableSkillsInstructions;
use crate::context::ContextualUserFragment;
use crate::context::ModelSwitchInstructions;
use crate::context::NetworkRuleSaved;
use crate::context::PersonalitySpecInstructions;
use crate::context::RecommendedPluginsInstructions;
use crate::context::world_state::WorldState;
use crate::current_time::TimeProvider;
@@ -1807,27 +1807,6 @@ impl Session {
self.refresh_runtime_config(next_config).await;
}
async fn build_settings_update_items(
&self,
reference_context_item: Option<&TurnContextItem>,
current_context: &TurnContext,
) -> Vec<ResponseItem> {
// TODO: Make context updates a pure diff of persisted previous/current TurnContextItem
// state so replay/backtracking is deterministic. Runtime inputs that affect model-visible
// context (feature gates and the previous-turn bridge) should be persisted
// state or explicit non-state replay events.
let previous_turn_settings = {
let state = self.state.lock().await;
state.previous_turn_settings()
};
crate::context_manager::updates::build_settings_update_items(
reference_context_item,
previous_turn_settings.as_ref(),
current_context,
self.features.enabled(Feature::Personality),
)
}
/// Record a terminal CodexErr before the app-server completion notification is reduced.
pub(crate) fn track_turn_codex_error(&self, turn_context: &TurnContext, error: &CodexErr) {
self.services
@@ -3351,23 +3330,13 @@ impl Session {
let mut developer_sections = Vec::<String>::with_capacity(8);
let mut contextual_user_sections = Vec::<String>::with_capacity(2);
let mut separate_developer_sections = Vec::<String>::new();
let (previous_turn_settings, base_instructions, session_source, auto_compact_window_ids) = {
let (session_source, auto_compact_window_ids) = {
let state = self.state.lock().await;
(
state.previous_turn_settings(),
state.session_configuration.base_instructions.clone(),
state.session_configuration.session_source.clone(),
state.auto_compact_window_ids(),
)
};
if let Some(model_switch_message) =
crate::context_manager::updates::build_model_instructions_update_item(
previous_turn_settings.as_ref(),
turn_context,
)
{
developer_sections.push(model_switch_message);
}
let separate_guardian_developer_message =
crate::guardian::is_guardian_reviewer_source(&session_source);
// Keep the guardian policy prompt out of the aggregated developer bundle so it
@@ -3378,23 +3347,6 @@ impl Session {
{
developer_sections.push(developer_instructions.to_string());
}
if self.features.enabled(Feature::Personality)
&& let Some(personality) = turn_context.personality
{
let model_info = turn_context.model_info.clone();
let has_baked_personality = model_info.supports_personality()
&& base_instructions == model_info.get_model_instructions(Some(personality));
if !has_baked_personality
&& let Some(personality_message) =
crate::context_manager::updates::personality_message_for(
&model_info,
personality,
)
{
developer_sections
.push(PersonalitySpecInstructions::new(personality_message).render());
}
}
if turn_context.config.include_skill_instructions {
let host_catalog_in_world_state = turn_context
.extension_data
@@ -3546,6 +3498,12 @@ impl Session {
let mut initial_multi_agent_mode = None;
for fragment in world_state.render_full() {
match fragment.role() {
"developer"
if fragment.markers().0 == ModelSwitchInstructions::type_markers().0 =>
{
// New-model instructions must precede the rest of the developer context.
developer_sections.insert(0, fragment.render());
}
"developer" if fragment.markers().0 == MULTI_AGENT_MODE_OPEN_TAG => {
initial_multi_agent_mode = Some(fragment);
}
@@ -3720,11 +3678,6 @@ impl Session {
Some(WorldStateItem::full(snapshot.into_value())),
)
} else {
// Steady-state path: append only built-in context diffs here; turn-scoped extension
// context is added below.
let mut context_items = self
.build_settings_update_items(reference_context_item.as_ref(), turn_context)
.await;
let (world_state_items, world_state_item) = {
let mut state = self.state.lock().await;
let (fragments, rollout_item) =
@@ -3734,8 +3687,7 @@ impl Session {
rollout_item,
)
};
context_items.extend(world_state_items);
(context_items, world_state_item)
(world_state_items, world_state_item)
};
if !should_inject_full_context && turn_context_changed {
context_items.extend(
-5
View File
@@ -9359,11 +9359,6 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
}
let rollout_path = attach_thread_persistence(&mut session).await;
let update_items = session
.build_settings_update_items(Some(&previous_context_item), &turn_context)
.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
+44
View File
@@ -7,8 +7,10 @@ use crate::context::world_state::AppsInstructionsState;
use crate::context::world_state::CollaborationModeState;
use crate::context::world_state::EnvironmentsInstructionsState;
use crate::context::world_state::EnvironmentsState;
use crate::context::world_state::ModelInstructionsState;
use crate::context::world_state::MultiAgentModeState;
use crate::context::world_state::PermissionsState;
use crate::context::world_state::PersonalityState;
use crate::context::world_state::PluginsInstructionsState;
use crate::context::world_state::RealtimeState;
use crate::context::world_state::ToolsState;
@@ -27,6 +29,21 @@ impl Session {
selected_capability_root_count = step_context.selected_capability_roots.len(),
"building step world state"
);
let (previous_model, previous_context, base_instructions) = {
let state = self.state.lock().await;
(
state
.previous_turn_settings()
.map(|previous| previous.model),
state.reference_context_item(),
state.session_configuration.base_instructions.clone(),
)
};
let model_instructions = turn_context
.model_info
.get_model_instructions(turn_context.personality);
let personality_is_baked = turn_context.model_info.supports_personality()
&& base_instructions == model_instructions;
let environment_subagents = if turn_context.config.include_environment_context {
self.services
.agent_control
@@ -36,6 +53,33 @@ impl Session {
String::new()
};
let mut world_state = WorldState::default();
world_state.add_section(ModelInstructionsState::new(
&turn_context.model_info.slug,
previous_model.as_deref(),
model_instructions,
));
if self.features.enabled(Feature::Personality) {
let personality_instructions = turn_context.personality.and_then(|personality| {
turn_context
.model_info
.model_messages
.as_ref()
.and_then(|messages| messages.get_personality_message(Some(personality)))
.filter(|message| !message.is_empty())
});
world_state.add_section(PersonalityState::new(
&turn_context.model_info.slug,
turn_context.personality,
previous_context
.as_ref()
.map(|previous| previous.model.as_str()),
previous_context
.as_ref()
.and_then(|previous| previous.personality),
personality_instructions,
personality_is_baked,
));
}
world_state.add_section(RealtimeState::new(
turn_context.realtime_active,
turn_context