mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
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:
@@ -46,6 +46,11 @@ impl<T: ContextualUserFragment> FragmentRegistration for FragmentRegistrationPro
|
||||
pub trait ContextualUserFragment {
|
||||
fn role(&self) -> &'static str;
|
||||
|
||||
/// Whether this fragment must be recorded as its own response item.
|
||||
fn requires_separate_message(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str);
|
||||
|
||||
fn body(&self) -> String;
|
||||
|
||||
@@ -18,6 +18,10 @@ impl ContextualUserFragment for ModelSwitchInstructions {
|
||||
"developer"
|
||||
}
|
||||
|
||||
fn requires_separate_message(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ impl ContextualUserFragment for PersonalitySpecInstructions {
|
||||
"developer"
|
||||
}
|
||||
|
||||
fn requires_separate_message(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ mod apps_instructions;
|
||||
mod collaboration_mode;
|
||||
mod environment;
|
||||
mod environments_instructions;
|
||||
mod model;
|
||||
mod multi_agent_mode;
|
||||
mod permissions;
|
||||
mod personality;
|
||||
mod plugins_instructions;
|
||||
mod realtime;
|
||||
#[cfg(test)]
|
||||
@@ -32,8 +34,10 @@ pub(crate) use apps_instructions::AppsInstructionsState;
|
||||
pub(crate) use collaboration_mode::CollaborationModeState;
|
||||
pub(crate) use environment::EnvironmentsState;
|
||||
pub(crate) use environments_instructions::EnvironmentsInstructionsState;
|
||||
pub(crate) use model::ModelInstructionsState;
|
||||
pub(crate) use multi_agent_mode::MultiAgentModeState;
|
||||
pub(crate) use permissions::PermissionsState;
|
||||
pub(crate) use personality::PersonalityState;
|
||||
pub(crate) use plugins_instructions::PluginsInstructionsState;
|
||||
pub(crate) use realtime::RealtimeState;
|
||||
pub(crate) use tools::ToolsState;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
use super::PreviousSectionState;
|
||||
use super::WorldStateSection;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::ModelSwitchInstructions;
|
||||
|
||||
/// Model identity and the instructions needed when that identity changes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ModelInstructionsState {
|
||||
model: String,
|
||||
previous_model: Option<String>,
|
||||
instructions: String,
|
||||
}
|
||||
|
||||
impl ModelInstructionsState {
|
||||
pub(crate) fn new(model: &str, previous_model: Option<&str>, instructions: String) -> Self {
|
||||
Self {
|
||||
model: model.to_string(),
|
||||
previous_model: previous_model.map(str::to_string),
|
||||
instructions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldStateSection for ModelInstructionsState {
|
||||
const ID: &'static str = "model";
|
||||
type Snapshot = String;
|
||||
|
||||
fn snapshot(&self) -> Self::Snapshot {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
fn matches_legacy_fragment(role: &str, text: &str) -> bool {
|
||||
role == "developer" && ModelSwitchInstructions::matches_text(text)
|
||||
}
|
||||
|
||||
fn has_retained_fragment_matcher() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn matches_retained_fragment(role: &str, text: &str) -> bool {
|
||||
Self::matches_legacy_fragment(role, text)
|
||||
}
|
||||
|
||||
fn render_diff(
|
||||
&self,
|
||||
previous: PreviousSectionState<'_, Self::Snapshot>,
|
||||
) -> Option<Box<dyn ContextualUserFragment>> {
|
||||
let model_changed = match previous {
|
||||
PreviousSectionState::Known(previous) => previous != &self.model,
|
||||
PreviousSectionState::Unknown | PreviousSectionState::Absent => self
|
||||
.previous_model
|
||||
.as_deref()
|
||||
.is_some_and(|previous| previous != self.model),
|
||||
};
|
||||
|
||||
(model_changed && !self.instructions.is_empty()).then(|| {
|
||||
Box::new(ModelSwitchInstructions::new(self.instructions.clone()))
|
||||
as Box<dyn ContextualUserFragment>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "model_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,35 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn model_change_renders_when_persisted_or_inferred_from_previous_turn() {
|
||||
let state = ModelInstructionsState::new("gpt-new", Some("gpt-old"), "instructions".into());
|
||||
let previous = "gpt-old".to_string();
|
||||
|
||||
for previous in [
|
||||
PreviousSectionState::Known(&previous),
|
||||
PreviousSectionState::Unknown,
|
||||
PreviousSectionState::Absent,
|
||||
] {
|
||||
assert_eq!(
|
||||
state
|
||||
.render_diff(previous)
|
||||
.expect("model change should render")
|
||||
.markers(),
|
||||
ModelSwitchInstructions::type_markers()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unchanged_model_does_not_render() {
|
||||
let state = ModelInstructionsState::new("gpt-test", Some("gpt-test"), "instructions".into());
|
||||
let previous = "gpt-test".to_string();
|
||||
|
||||
assert!(
|
||||
state
|
||||
.render_diff(PreviousSectionState::Known(&previous))
|
||||
.is_none()
|
||||
);
|
||||
assert!(state.render_diff(PreviousSectionState::Absent).is_none());
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use super::PreviousSectionState;
|
||||
use super::WorldStateSection;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::PersonalitySpecInstructions;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Personality instructions currently visible to the model.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct PersonalityState {
|
||||
snapshot: PersonalitySnapshot,
|
||||
previous: Option<PersonalitySnapshot>,
|
||||
instructions: Option<String>,
|
||||
personality_is_baked: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub(crate) struct PersonalitySnapshot {
|
||||
model: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
personality: Option<Personality>,
|
||||
}
|
||||
|
||||
impl PersonalityState {
|
||||
pub(crate) fn new(
|
||||
model: &str,
|
||||
personality: Option<Personality>,
|
||||
previous_model: Option<&str>,
|
||||
previous_personality: Option<Personality>,
|
||||
instructions: Option<String>,
|
||||
personality_is_baked: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
snapshot: PersonalitySnapshot {
|
||||
model: model.to_string(),
|
||||
personality,
|
||||
},
|
||||
previous: previous_model.map(|model| PersonalitySnapshot {
|
||||
model: model.to_string(),
|
||||
personality: previous_personality,
|
||||
}),
|
||||
instructions,
|
||||
personality_is_baked,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_change(
|
||||
&self,
|
||||
previous: &PersonalitySnapshot,
|
||||
) -> Option<Box<dyn ContextualUserFragment>> {
|
||||
(previous.model == self.snapshot.model && previous.personality != self.snapshot.personality)
|
||||
.then_some(self.instructions.as_ref())
|
||||
.flatten()
|
||||
.map(|instructions| {
|
||||
Box::new(PersonalitySpecInstructions::new(instructions.clone()))
|
||||
as Box<dyn ContextualUserFragment>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldStateSection for PersonalityState {
|
||||
const ID: &'static str = "personality";
|
||||
type Snapshot = PersonalitySnapshot;
|
||||
|
||||
fn snapshot(&self) -> Self::Snapshot {
|
||||
self.snapshot.clone()
|
||||
}
|
||||
|
||||
fn matches_legacy_fragment(role: &str, text: &str) -> bool {
|
||||
role == "developer" && PersonalitySpecInstructions::matches_text(text)
|
||||
}
|
||||
|
||||
fn render_diff(
|
||||
&self,
|
||||
previous: PreviousSectionState<'_, Self::Snapshot>,
|
||||
) -> Option<Box<dyn ContextualUserFragment>> {
|
||||
match previous {
|
||||
PreviousSectionState::Known(previous) => self.render_change(previous),
|
||||
PreviousSectionState::Unknown => self
|
||||
.previous
|
||||
.as_ref()
|
||||
.and_then(|previous| self.render_change(previous)),
|
||||
PreviousSectionState::Absent => (!self.personality_is_baked
|
||||
&& self
|
||||
.previous
|
||||
.as_ref()
|
||||
.is_none_or(|previous| previous.model == self.snapshot.model))
|
||||
.then_some(self.instructions.as_ref())
|
||||
.flatten()
|
||||
.map(|instructions| {
|
||||
Box::new(PersonalitySpecInstructions::new(instructions.clone()))
|
||||
as Box<dyn ContextualUserFragment>
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "personality_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,106 @@
|
||||
use super::*;
|
||||
use crate::context::world_state::WorldState;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn state(
|
||||
model: &str,
|
||||
personality: Option<Personality>,
|
||||
previous: Option<(&str, Option<Personality>)>,
|
||||
personality_is_baked: bool,
|
||||
) -> PersonalityState {
|
||||
PersonalityState::new(
|
||||
model,
|
||||
personality,
|
||||
previous.map(|(model, _)| model),
|
||||
previous.and_then(|(_, personality)| personality),
|
||||
personality.map(|personality| format!("instructions for {personality:?}")),
|
||||
personality_is_baked,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_personality_renders_only_when_missing_from_base_instructions() {
|
||||
let separate = state(
|
||||
"gpt-test",
|
||||
Some(Personality::Friendly),
|
||||
/*previous*/ None,
|
||||
/*personality_is_baked*/ false,
|
||||
);
|
||||
let baked = state(
|
||||
"gpt-test",
|
||||
Some(Personality::Friendly),
|
||||
/*previous*/ None,
|
||||
/*personality_is_baked*/ true,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
separate
|
||||
.render_diff(PreviousSectionState::Absent)
|
||||
.expect("separate personality should render")
|
||||
.markers(),
|
||||
PersonalitySpecInstructions::type_markers()
|
||||
);
|
||||
assert!(baked.render_diff(PreviousSectionState::Absent).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn personality_changes_render_without_repeating_model_changes() {
|
||||
let previous = PersonalitySnapshot {
|
||||
model: "gpt-test".to_string(),
|
||||
personality: Some(Personality::Friendly),
|
||||
};
|
||||
let changed = state(
|
||||
"gpt-test",
|
||||
Some(Personality::Pragmatic),
|
||||
Some(("gpt-test", Some(Personality::Friendly))),
|
||||
/*personality_is_baked*/ true,
|
||||
);
|
||||
let model_changed = state(
|
||||
"gpt-next",
|
||||
Some(Personality::Pragmatic),
|
||||
Some(("gpt-test", Some(Personality::Friendly))),
|
||||
/*personality_is_baked*/ true,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
changed
|
||||
.render_diff(PreviousSectionState::Known(&previous))
|
||||
.expect("changed personality should render")
|
||||
.markers(),
|
||||
PersonalitySpecInstructions::type_markers()
|
||||
);
|
||||
assert!(
|
||||
model_changed
|
||||
.render_diff(PreviousSectionState::Known(&previous))
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
model_changed
|
||||
.render_diff(PreviousSectionState::Unknown)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
model_changed
|
||||
.render_diff(PreviousSectionState::Absent)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_personality_does_not_require_a_retained_update() {
|
||||
let state = state(
|
||||
"gpt-test",
|
||||
Some(Personality::Friendly),
|
||||
Some(("gpt-test", Some(Personality::Friendly))),
|
||||
/*personality_is_baked*/ false,
|
||||
);
|
||||
let mut world_state = WorldState::default();
|
||||
world_state.add_section(state);
|
||||
let snapshot = world_state.snapshot();
|
||||
|
||||
assert!(
|
||||
world_state
|
||||
.render_history_diff(Some(&snapshot), &[])
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
@@ -1,64 +1,11 @@
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::ModelSwitchInstructions;
|
||||
use crate::context::PersonalitySpecInstructions;
|
||||
use crate::session::PreviousTurnSettings;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
|
||||
fn build_personality_update_item(
|
||||
previous: Option<&TurnContextItem>,
|
||||
next: &TurnContext,
|
||||
personality_feature_enabled: bool,
|
||||
) -> Option<String> {
|
||||
if !personality_feature_enabled {
|
||||
return None;
|
||||
}
|
||||
let previous = previous?;
|
||||
if next.model_info.slug != previous.model {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(personality) = next.personality
|
||||
&& next.personality != previous.personality
|
||||
{
|
||||
let model_info = &next.model_info;
|
||||
let personality_message = personality_message_for(model_info, personality);
|
||||
personality_message.map(|message| PersonalitySpecInstructions::new(message).render())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn personality_message_for(
|
||||
model_info: &ModelInfo,
|
||||
personality: Personality,
|
||||
) -> Option<String> {
|
||||
model_info
|
||||
.model_messages
|
||||
.as_ref()
|
||||
.and_then(|spec| spec.get_personality_message(Some(personality)))
|
||||
.filter(|message| !message.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn build_model_instructions_update_item(
|
||||
previous_turn_settings: Option<&PreviousTurnSettings>,
|
||||
next: &TurnContext,
|
||||
) -> Option<String> {
|
||||
let previous_turn_settings = previous_turn_settings?;
|
||||
if previous_turn_settings.model == next.model_info.slug {
|
||||
return None;
|
||||
}
|
||||
|
||||
let model_instructions = next.model_info.get_model_instructions(next.personality);
|
||||
if model_instructions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ModelSwitchInstructions::new(model_instructions).render())
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum MessageGroup {
|
||||
Standalone,
|
||||
Mergeable,
|
||||
}
|
||||
|
||||
pub(crate) fn build_developer_update_item(text_sections: Vec<String>) -> Option<ResponseItem> {
|
||||
@@ -72,20 +19,29 @@ pub(crate) fn build_contextual_user_message(text_sections: Vec<String>) -> Optio
|
||||
pub(crate) fn merge_contextual_fragments(
|
||||
fragments: Vec<Box<dyn ContextualUserFragment>>,
|
||||
) -> Vec<ResponseItem> {
|
||||
let mut messages: Vec<(&str, Vec<String>)> = Vec::with_capacity(fragments.len());
|
||||
let mut messages: Vec<(&str, MessageGroup, Vec<String>)> = Vec::with_capacity(fragments.len());
|
||||
for fragment in fragments {
|
||||
let role = fragment.role();
|
||||
let group = if fragment.requires_separate_message() {
|
||||
MessageGroup::Standalone
|
||||
} else {
|
||||
MessageGroup::Mergeable
|
||||
};
|
||||
let text = fragment.render();
|
||||
match messages.last_mut() {
|
||||
Some((previous_role, text_sections)) if *previous_role == role => {
|
||||
Some((previous_role, previous_group, text_sections))
|
||||
if *previous_role == role
|
||||
&& *previous_group == MessageGroup::Mergeable
|
||||
&& group == MessageGroup::Mergeable =>
|
||||
{
|
||||
text_sections.push(text);
|
||||
}
|
||||
_ => messages.push((role, vec![text])),
|
||||
_ => messages.push((role, group, vec![text])),
|
||||
}
|
||||
}
|
||||
messages
|
||||
.into_iter()
|
||||
.filter_map(|(role, text_sections)| build_text_message(role, text_sections))
|
||||
.filter_map(|(role, _, text_sections)| build_text_message(role, text_sections))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -107,28 +63,3 @@ fn build_text_message(role: &str, text_sections: Vec<String>) -> Option<Response
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_settings_update_items(
|
||||
previous: Option<&TurnContextItem>,
|
||||
previous_turn_settings: Option<&PreviousTurnSettings>,
|
||||
next: &TurnContext,
|
||||
personality_feature_enabled: bool,
|
||||
) -> Vec<ResponseItem> {
|
||||
// TODO(ccunningham): build_settings_update_items still does not cover every
|
||||
// model-visible item emitted by build_initial_context. Persist the remaining
|
||||
// inputs or add explicit replay events so fork/resume can diff everything
|
||||
// deterministically.
|
||||
let developer_update_sections = [
|
||||
// Keep model-switch instructions first so model-specific guidance is read before
|
||||
// any other context diffs on this turn.
|
||||
build_model_instructions_update_item(previous_turn_settings, next),
|
||||
build_personality_update_item(previous, next, personality_feature_enabled),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
build_developer_update_item(developer_update_sections)
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,6 +20,8 @@ use codex_protocol::openai_models::default_input_modalities;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses::ev_completed_with_tokens;
|
||||
use core_test_support::responses::ev_image_generation_call;
|
||||
@@ -179,6 +181,31 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
|
||||
"expected model switch preamble, got: {model_switch_text:?}"
|
||||
);
|
||||
|
||||
test.codex.ensure_rollout_materialized().await;
|
||||
test.codex.flush_rollout().await?;
|
||||
let rollout_path = test.codex.rollout_path().expect("rollout path");
|
||||
let model_states = std::fs::read_to_string(rollout_path)?
|
||||
.lines()
|
||||
.map(serde_json::from_str::<RolloutLine>)
|
||||
.collect::<serde_json::Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.filter_map(|line| match line.item {
|
||||
RolloutItem::WorldState(item) => item
|
||||
.state
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_string),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
model_states,
|
||||
vec![
|
||||
test.session_configured.model.clone(),
|
||||
next_model.to_string()
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user