mirror of
https://github.com/openai/codex.git
synced 2026-11-04 00:24:43 +00:00
Use captured step settings when building model context (#44200)
## Why Model changes during an active turn could leave context using the turn's initial model and settings, even after requests switched to the new model. ## What changed - Build model, personality, approval, collaboration, app, plugin, and multi-agent instructions from captured step settings. - Resolve multi-agent policy from the step's effective reasoning effort. - Use the captured step's context window for full-context metadata and retain that step when rebuilding initial context for compaction. ## Testing Add regression coverage for model switches updating context without duplicating updates, delegation policy following selected or model-default reasoning effort, and compaction preserving retained context after a model change. GitOrigin-RevId: 3615c7a3f13f15fdcce9ad13d23311340e0aea88
This commit is contained in:
1 parent
ccf470c060
commit
b4507997e0
11 files changed
+373
-69
No files matched your search
@@ -103,10 +103,7 @@ pub(crate) async fn build_compaction_initial_context(
|
|||||||
step_context,
|
step_context,
|
||||||
} => {
|
} => {
|
||||||
let items = sess
|
let items = sess
|
||||||
.build_initial_context_with_world_state(
|
.build_initial_context_with_world_state(step_context, world_state.as_ref())
|
||||||
step_context.turn.as_ref(),
|
|
||||||
world_state.as_ref(),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
(
|
(
|
||||||
items.into_iter().map(ResponseItemEnvelope::new).collect(),
|
items.into_iter().map(ResponseItemEnvelope::new).collect(),
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ async fn process_compacted_history_with_test_session(
|
|||||||
.expect("world state should build"),
|
.expect("world state should build"),
|
||||||
);
|
);
|
||||||
let initial_context = session
|
let initial_context = session
|
||||||
.build_initial_context_with_world_state(&turn_context, world_state.as_ref())
|
.build_initial_context_with_world_state(&step_context, world_state.as_ref())
|
||||||
.await;
|
.await;
|
||||||
let initial_context_injection = InitialContextInjection::BeforeLastUserMessage {
|
let initial_context_injection = InitialContextInjection::BeforeLastUserMessage {
|
||||||
world_state,
|
world_state,
|
||||||
|
|||||||
@@ -3977,11 +3977,15 @@ impl Session {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `step_context` and `world_state` must come from the same captured step.
|
||||||
|
/// If more callers need this pair, bundle them into a captured-context struct
|
||||||
|
/// so callers cannot mix settings and WorldState from different steps.
|
||||||
pub(crate) async fn build_initial_context_with_world_state(
|
pub(crate) async fn build_initial_context_with_world_state(
|
||||||
&self,
|
&self,
|
||||||
turn_context: &TurnContext,
|
step_context: &StepContext,
|
||||||
world_state: &WorldState,
|
world_state: &WorldState,
|
||||||
) -> Vec<ResponseItem> {
|
) -> Vec<ResponseItem> {
|
||||||
|
let turn_context = step_context.turn.as_ref();
|
||||||
let mut developer_sections = Vec::<RenderedFragment>::with_capacity(8);
|
let mut developer_sections = Vec::<RenderedFragment>::with_capacity(8);
|
||||||
let mut contextual_user_sections = Vec::<RenderedFragment>::with_capacity(2);
|
let mut contextual_user_sections = Vec::<RenderedFragment>::with_capacity(2);
|
||||||
let mut separate_developer_sections = Vec::<RenderedFragment>::new();
|
let mut separate_developer_sections = Vec::<RenderedFragment>::new();
|
||||||
@@ -4071,7 +4075,11 @@ impl Session {
|
|||||||
}
|
}
|
||||||
// This is full-context metadata. Steady-state context diffs should not re-emit it.
|
// This is full-context metadata. Steady-state context diffs should not re-emit it.
|
||||||
if turn_context.config.features.enabled(Feature::TokenBudget)
|
if turn_context.config.features.enabled(Feature::TokenBudget)
|
||||||
&& turn_context.model_context_window().is_some()
|
&& step_context
|
||||||
|
.settings
|
||||||
|
.model_info
|
||||||
|
.resolved_context_window()
|
||||||
|
.is_some()
|
||||||
{
|
{
|
||||||
// Keep the legacy bridge hint when native Notes is disabled. A failed
|
// Keep the legacy bridge hint when native Notes is disabled. A failed
|
||||||
// native request must not fall back to the bridge.
|
// native request must not fall back to the bridge.
|
||||||
@@ -4291,7 +4299,7 @@ impl Session {
|
|||||||
};
|
};
|
||||||
let (window_number, window_ids) = window;
|
let (window_number, window_ids) = window;
|
||||||
let context_items = self
|
let context_items = self
|
||||||
.build_initial_context_with_world_state(turn_context, world_state.as_ref())
|
.build_initial_context_with_world_state(step_context, world_state.as_ref())
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ResponseItemEnvelope::new)
|
.map(ResponseItemEnvelope::new)
|
||||||
@@ -4350,7 +4358,7 @@ impl Session {
|
|||||||
// Full initial context resets the baseline; later turns persist only its changes.
|
// 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 (mut context_items, world_state_item) = if should_inject_full_context {
|
||||||
let context_items = self
|
let context_items = self
|
||||||
.build_initial_context_with_world_state(turn_context, world_state.as_ref())
|
.build_initial_context_with_world_state(step_context, world_state.as_ref())
|
||||||
.await;
|
.await;
|
||||||
let snapshot = world_state.snapshot();
|
let snapshot = world_state.snapshot();
|
||||||
self.state
|
self.state
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::config::MultiAgentV2Config;
|
use crate::config::MultiAgentV2Config;
|
||||||
use crate::context::MultiAgentRoleInstructions;
|
use crate::context::MultiAgentRoleInstructions;
|
||||||
use crate::session::turn_context::TurnContext;
|
use crate::session::step_context::StepContext;
|
||||||
use codex_protocol::config_types::MultiAgentMode;
|
use codex_protocol::config_types::MultiAgentMode;
|
||||||
use codex_protocol::openai_models::MultiAgentRoleMessages;
|
use codex_protocol::openai_models::MultiAgentRoleMessages;
|
||||||
use codex_protocol::openai_models::ReasoningEffort;
|
use codex_protocol::openai_models::ReasoningEffort;
|
||||||
@@ -65,15 +65,17 @@ pub(crate) struct ResolvedMultiAgentV2UsageHints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn usage_hint_text(
|
pub(super) fn usage_hint_text(
|
||||||
turn_context: &TurnContext,
|
step_context: &StepContext,
|
||||||
session_source: &SessionSource,
|
session_source: &SessionSource,
|
||||||
) -> Option<MultiAgentRoleInstructions> {
|
) -> Option<MultiAgentRoleInstructions> {
|
||||||
|
let turn_context = step_context.turn.as_ref();
|
||||||
if turn_context.multi_agent_version != MultiAgentVersion::V2 {
|
if turn_context.multi_agent_version != MultiAgentVersion::V2 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let catalog = turn_context
|
let catalog = step_context
|
||||||
.model_info()
|
.settings
|
||||||
|
.model_info
|
||||||
.model_messages
|
.model_messages
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|messages| messages.multi_agent.as_ref())
|
.and_then(|messages| messages.multi_agent.as_ref())
|
||||||
@@ -152,13 +154,15 @@ pub(crate) fn resolve_usage_hints(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn effective_multi_agent_mode(turn_context: &TurnContext) -> Option<MultiAgentMode> {
|
pub(crate) fn effective_multi_agent_mode(step_context: &StepContext) -> Option<MultiAgentMode> {
|
||||||
|
let turn_context = step_context.turn.as_ref();
|
||||||
|
let settings = &step_context.settings;
|
||||||
if turn_context.multi_agent_version != MultiAgentVersion::V2 {
|
if turn_context.multi_agent_version != MultiAgentVersion::V2 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let catalog_mode = turn_context
|
let catalog_mode = settings
|
||||||
.model_info()
|
.model_info
|
||||||
.model_messages
|
.model_messages
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|messages| messages.multi_agent.as_ref())
|
.and_then(|messages| messages.multi_agent.as_ref())
|
||||||
@@ -174,7 +178,7 @@ pub(crate) fn effective_multi_agent_mode(turn_context: &TurnContext) -> Option<M
|
|||||||
// of an effort-derived built-in policy.
|
// of an effort-derived built-in policy.
|
||||||
let multi_agent_mode = match mode_hint_text {
|
let multi_agent_mode = match mode_hint_text {
|
||||||
Some(hint_text) => MultiAgentMode::Custom(hint_text.to_string()),
|
Some(hint_text) => MultiAgentMode::Custom(hint_text.to_string()),
|
||||||
None => match turn_context.effective_reasoning_effort() {
|
None => match settings.effective_reasoning_effort() {
|
||||||
Some(ReasoningEffort::Ultra) => catalog_mode
|
Some(ReasoningEffort::Ultra) => catalog_mode
|
||||||
.and_then(|messages| messages.proactive.clone())
|
.and_then(|messages| messages.proactive.clone())
|
||||||
.map(MultiAgentMode::Custom)
|
.map(MultiAgentMode::Custom)
|
||||||
|
|||||||
@@ -90,6 +90,12 @@ impl ResolvedStepSettings {
|
|||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn effective_collaboration_mode(&self) -> CollaborationMode {
|
||||||
|
let mut collaboration_mode = self.selected.collaboration_mode.clone();
|
||||||
|
collaboration_mode.settings.model = self.model_info.slug.clone();
|
||||||
|
collaboration_mode
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn approval_policy(&self) -> AskForApproval {
|
pub(crate) fn approval_policy(&self) -> AskForApproval {
|
||||||
self.selected.approval_policy.value()
|
self.selected.approval_policy.value()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ use codex_protocol::models::FunctionCallOutputPayload;
|
|||||||
use codex_protocol::models::ImageDetail;
|
use codex_protocol::models::ImageDetail;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::models::SandboxEnforcement;
|
use codex_protocol::models::SandboxEnforcement;
|
||||||
|
use codex_protocol::openai_models::ModelInstructionsVariables;
|
||||||
use codex_protocol::openai_models::ModelServiceTier;
|
use codex_protocol::openai_models::ModelServiceTier;
|
||||||
use codex_protocol::openai_models::ToolMode;
|
use codex_protocol::openai_models::ToolMode;
|
||||||
use codex_protocol::permissions::FileSystemAccessMode;
|
use codex_protocol::permissions::FileSystemAccessMode;
|
||||||
@@ -6416,9 +6417,13 @@ async fn build_initial_context(
|
|||||||
session: &Session,
|
session: &Session,
|
||||||
turn_context: &Arc<TurnContext>,
|
turn_context: &Arc<TurnContext>,
|
||||||
) -> Vec<ResponseItem> {
|
) -> Vec<ResponseItem> {
|
||||||
let world_state = build_world_state_from_turn_context(session, turn_context).await;
|
let step_context = StepContext::for_test(Arc::clone(turn_context));
|
||||||
|
let world_state = session
|
||||||
|
.build_world_state_for_step(&step_context)
|
||||||
|
.await
|
||||||
|
.expect("world state should build");
|
||||||
session
|
session
|
||||||
.build_initial_context_with_world_state(turn_context.as_ref(), &world_state)
|
.build_initial_context_with_world_state(&step_context, &world_state)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10123,8 +10128,9 @@ async fn build_initial_context_reuses_in_flight_recommendation_prewarm() {
|
|||||||
// Keep the OnceCell initializer unpolled while first-thread context joins its in-flight fetch.
|
// Keep the OnceCell initializer unpolled while first-thread context joins its in-flight fetch.
|
||||||
// This does not depend on how quickly the HTTP server returns its response.
|
// This does not depend on how quickly the HTTP server returns its response.
|
||||||
let world_state = WorldState::default();
|
let world_state = WorldState::default();
|
||||||
|
let step_context = StepContext::for_test(Arc::clone(&turn_context));
|
||||||
let initial_context =
|
let initial_context =
|
||||||
session.build_initial_context_with_world_state(&turn_context, &world_state);
|
session.build_initial_context_with_world_state(&step_context, &world_state);
|
||||||
tokio::pin!(initial_context);
|
tokio::pin!(initial_context);
|
||||||
assert!(futures::poll!(initial_context.as_mut()).is_pending());
|
assert!(futures::poll!(initial_context.as_mut()).is_pending());
|
||||||
|
|
||||||
@@ -10802,6 +10808,103 @@ async fn record_context_updates_and_set_reference_context_item_persists_split_fi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn build_initial_context_uses_retained_step_after_model_change() {
|
||||||
|
let (session, mut turn_context, _rx_event) =
|
||||||
|
make_session_and_context_with_auth_and_config_and_rx(
|
||||||
|
CodexAuth::from_api_key("Test API Key"),
|
||||||
|
Vec::new(),
|
||||||
|
|config| {
|
||||||
|
config.features.enable(Feature::Personality).unwrap();
|
||||||
|
config.features.enable(Feature::TokenBudget).unwrap();
|
||||||
|
config.personality = Some(Personality::Friendly);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
update_turn_settings_for_test(Arc::get_mut(&mut turn_context).unwrap(), |settings| {
|
||||||
|
let model_info = Arc::make_mut(&mut settings.model_info);
|
||||||
|
model_info.slug = "model-a".to_string();
|
||||||
|
model_info.context_window = None;
|
||||||
|
model_info.max_context_window = None;
|
||||||
|
let messages = model_info.model_messages.as_mut().unwrap();
|
||||||
|
messages.instructions_template = Some("A instructions: {{ personality }}".to_string());
|
||||||
|
messages.instructions_variables = Some(ModelInstructionsVariables {
|
||||||
|
personality_default: Some("default".to_string()),
|
||||||
|
personality_friendly: Some("friendly".to_string()),
|
||||||
|
personality_pragmatic: Some("pragmatic".to_string()),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
session
|
||||||
|
.set_previous_turn_settings(Some(PreviousTurnSettings {
|
||||||
|
model: "base-model".to_string(),
|
||||||
|
comp_hash: None,
|
||||||
|
realtime_active: None,
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
let step_a = session
|
||||||
|
.capture_step_context(Arc::clone(&turn_context), &CancellationToken::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let world_a = Arc::new(session.build_world_state_for_step(&step_a).await.unwrap());
|
||||||
|
let retained = crate::compact::InitialContextInjection::BeforeLastUserMessage {
|
||||||
|
world_state: Arc::clone(&world_a),
|
||||||
|
step_context: Arc::clone(&step_a),
|
||||||
|
};
|
||||||
|
let (initial_a, _) =
|
||||||
|
crate::compact::build_compaction_initial_context(&session, &retained).await;
|
||||||
|
|
||||||
|
let mut selected_b = step_a.settings.selected().clone();
|
||||||
|
selected_b.collaboration_mode.settings.model = "model-b".to_string();
|
||||||
|
selected_b.personality = Some(Personality::Pragmatic);
|
||||||
|
let mut model_b = step_a.settings.model_info.as_ref().clone();
|
||||||
|
model_b.slug = "model-b".to_string();
|
||||||
|
model_b.context_window = Some(128_000);
|
||||||
|
model_b
|
||||||
|
.model_messages
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.instructions_template = Some("B instructions: {{ personality }}".to_string());
|
||||||
|
turn_context
|
||||||
|
.current_settings
|
||||||
|
.store(Arc::new(ResolvedStepSettings::new(
|
||||||
|
Arc::new(selected_b),
|
||||||
|
Arc::new(model_b),
|
||||||
|
/*fast_mode_enabled*/ false,
|
||||||
|
)));
|
||||||
|
let step_b = session
|
||||||
|
.capture_step_context(Arc::clone(&turn_context), &CancellationToken::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let world_b = session.build_world_state_for_step(&step_b).await.unwrap();
|
||||||
|
let initial_b = session
|
||||||
|
.build_initial_context_with_world_state(&step_b, &world_b)
|
||||||
|
.await;
|
||||||
|
let (restored_a, restored_world) =
|
||||||
|
crate::compact::build_compaction_initial_context(&session, &retained).await;
|
||||||
|
|
||||||
|
assert_eq!(restored_a, initial_a);
|
||||||
|
assert!(Arc::ptr_eq(restored_world.as_ref().unwrap(), &world_a));
|
||||||
|
let initial_a = initial_a
|
||||||
|
.into_iter()
|
||||||
|
.map(ResponseItemEnvelope::into_item)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let a_text = developer_input_texts(&initial_a).join("\n");
|
||||||
|
let b_text = developer_input_texts(&initial_b).join("\n");
|
||||||
|
assert!(a_text.contains("A instructions: friendly"));
|
||||||
|
assert!(!a_text.contains("<context_window>"));
|
||||||
|
assert!(b_text.contains("B instructions: pragmatic"));
|
||||||
|
assert!(!b_text.contains("A instructions:"));
|
||||||
|
assert!(
|
||||||
|
b_text.contains("<context_window>"),
|
||||||
|
"full-context metadata must use B's window even though the turn started without one"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
step_b.environments.to_selections(),
|
||||||
|
step_a.environments.to_selections()
|
||||||
|
);
|
||||||
|
assert!(Arc::ptr_eq(&step_b.turn.config, &step_a.turn.config));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn build_initial_context_prepends_model_switch_message() {
|
async fn build_initial_context_prepends_model_switch_message() {
|
||||||
let (session, turn_context) = make_session_and_context().await;
|
let (session, turn_context) = make_session_and_context().await;
|
||||||
|
|||||||
@@ -369,16 +369,6 @@ impl TurnContext {
|
|||||||
self.initial_settings.personality()
|
self.initial_settings.personality()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Legacy: returns the frozen initial-turn collaboration-mode developer instructions.
|
|
||||||
/// Step-scoped consumers should use their captured `StepContext::settings`.
|
|
||||||
pub(crate) fn collaboration_mode_developer_instructions(&self) -> &Option<String> {
|
|
||||||
&self
|
|
||||||
.initial_settings
|
|
||||||
.selected_collaboration_mode()
|
|
||||||
.settings
|
|
||||||
.developer_instructions
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn skills_snapshot(&self) -> Arc<HostSkillsSnapshot> {
|
pub(crate) fn skills_snapshot(&self) -> Arc<HostSkillsSnapshot> {
|
||||||
let Some(snapshot) = self.extension_data.get::<HostSkillsSnapshot>() else {
|
let Some(snapshot) = self.extension_data.get::<HostSkillsSnapshot>() else {
|
||||||
unreachable!("every turn has a host skills snapshot");
|
unreachable!("every turn has a host skills snapshot");
|
||||||
@@ -389,14 +379,7 @@ impl TurnContext {
|
|||||||
/// Legacy: returns the frozen initial-turn collaboration mode with the resolved model slug.
|
/// Legacy: returns the frozen initial-turn collaboration mode with the resolved model slug.
|
||||||
/// Step-scoped consumers should use their captured `StepContext::settings`.
|
/// Step-scoped consumers should use their captured `StepContext::settings`.
|
||||||
pub(crate) fn collaboration_mode(&self) -> CollaborationMode {
|
pub(crate) fn collaboration_mode(&self) -> CollaborationMode {
|
||||||
CollaborationMode {
|
self.initial_settings.effective_collaboration_mode()
|
||||||
mode: self.mode(),
|
|
||||||
settings: Settings {
|
|
||||||
model: self.model_info().slug.clone(),
|
|
||||||
reasoning_effort: self.reasoning_effort().cloned(),
|
|
||||||
developer_instructions: self.collaboration_mode_developer_instructions().clone(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn plugin_attribution_for_command(
|
pub(crate) fn plugin_attribution_for_command(
|
||||||
|
|||||||
@@ -37,13 +37,14 @@ impl Session {
|
|||||||
step_context: &StepContext,
|
step_context: &StepContext,
|
||||||
) -> CodexResult<WorldState> {
|
) -> CodexResult<WorldState> {
|
||||||
let turn_context = step_context.turn.as_ref();
|
let turn_context = step_context.turn.as_ref();
|
||||||
|
let settings = &step_context.settings;
|
||||||
|
let model_info = settings.model_info.as_ref();
|
||||||
|
let personality = settings.personality();
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
selected_capability_root_count = step_context.selected_capability_roots.len(),
|
selected_capability_root_count = step_context.selected_capability_roots.len(),
|
||||||
"building step world state"
|
"building step world state"
|
||||||
);
|
);
|
||||||
let model_instructions = turn_context
|
let model_instructions = model_info.get_model_instructions(personality);
|
||||||
.model_info()
|
|
||||||
.get_model_instructions(turn_context.personality());
|
|
||||||
let model_instructions = if !turn_context.config.update_plan_enabled
|
let model_instructions = if !turn_context.config.update_plan_enabled
|
||||||
&& turn_context.config.model_catalog.is_none()
|
&& turn_context.config.model_catalog.is_none()
|
||||||
&& (turn_context.config.base_instructions.is_none()
|
&& (turn_context.config.base_instructions.is_none()
|
||||||
@@ -77,8 +78,8 @@ impl Session {
|
|||||||
base_instructions,
|
base_instructions,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let personality_is_baked = turn_context.model_info().supports_personality()
|
let personality_is_baked =
|
||||||
&& base_instructions == model_instructions;
|
model_info.supports_personality() && base_instructions == model_instructions;
|
||||||
let environment_subagents = if turn_context.config.include_environment_context {
|
let environment_subagents = if turn_context.config.include_environment_context {
|
||||||
self.services
|
self.services
|
||||||
.agent_control
|
.agent_control
|
||||||
@@ -92,22 +93,21 @@ impl Session {
|
|||||||
};
|
};
|
||||||
let mut world_state = WorldState::default();
|
let mut world_state = WorldState::default();
|
||||||
world_state.add_section(ModelInstructionsState::new(
|
world_state.add_section(ModelInstructionsState::new(
|
||||||
&turn_context.model_info().slug,
|
&model_info.slug,
|
||||||
previous_model.as_deref(),
|
previous_model.as_deref(),
|
||||||
model_instructions,
|
model_instructions,
|
||||||
));
|
));
|
||||||
if self.features.enabled(Feature::Personality) {
|
if self.features.enabled(Feature::Personality) {
|
||||||
let personality_instructions = turn_context.personality().and_then(|personality| {
|
let personality_instructions = personality.and_then(|personality| {
|
||||||
turn_context
|
model_info
|
||||||
.model_info()
|
|
||||||
.model_messages
|
.model_messages
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|messages| messages.get_personality_message(Some(personality)))
|
.and_then(|messages| messages.get_personality_message(Some(personality)))
|
||||||
.filter(|message| !message.is_empty())
|
.filter(|message| !message.is_empty())
|
||||||
});
|
});
|
||||||
world_state.add_section(PersonalityState::new(
|
world_state.add_section(PersonalityState::new(
|
||||||
&turn_context.model_info().slug,
|
&model_info.slug,
|
||||||
turn_context.personality(),
|
personality,
|
||||||
previous_context
|
previous_context
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|previous| previous.model.as_str())
|
.map(|previous| previous.model.as_str())
|
||||||
@@ -172,12 +172,12 @@ impl Session {
|
|||||||
let cwd = environment
|
let cwd = environment
|
||||||
.and_then(|environment| environment.cwd().to_abs_path().ok())
|
.and_then(|environment| environment.cwd().to_abs_path().ok())
|
||||||
.unwrap_or_else(|| turn_context.cwd.clone());
|
.unwrap_or_else(|| turn_context.cwd.clone());
|
||||||
let model_messages = turn_context.model_info().model_messages.as_ref();
|
let model_messages = model_info.model_messages.as_ref();
|
||||||
world_state.add_section(PermissionsState::new(
|
world_state.add_section(PermissionsState::new(
|
||||||
&permission_profile,
|
&permission_profile,
|
||||||
step_context.settings.approval_policy(),
|
settings.approval_policy(),
|
||||||
ApprovalPromptContext::new(
|
ApprovalPromptContext::new(
|
||||||
step_context.settings.approvals_reviewer(),
|
settings.approvals_reviewer(),
|
||||||
model_messages.and_then(|messages| messages.approvals.as_ref()),
|
model_messages.and_then(|messages| messages.approvals.as_ref()),
|
||||||
model_messages.and_then(|messages| messages.permissions.as_ref()),
|
model_messages.and_then(|messages| messages.permissions.as_ref()),
|
||||||
),
|
),
|
||||||
@@ -197,9 +197,8 @@ impl Session {
|
|||||||
}
|
}
|
||||||
if turn_context.config.include_collaboration_mode_instructions {
|
if turn_context.config.include_collaboration_mode_instructions {
|
||||||
world_state.add_section(CollaborationModeState::from_collaboration_mode(
|
world_state.add_section(CollaborationModeState::from_collaboration_mode(
|
||||||
&turn_context.collaboration_mode(),
|
&settings.effective_collaboration_mode(),
|
||||||
turn_context
|
model_info
|
||||||
.model_info()
|
|
||||||
.model_messages
|
.model_messages
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|messages| messages.collaboration_modes.as_ref()),
|
.and_then(|messages| messages.collaboration_modes.as_ref()),
|
||||||
@@ -266,12 +265,12 @@ impl Session {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
let apps_usage_instructions_available =
|
let apps_usage_instructions_available =
|
||||||
apps_available && turn_context.model_info().include_apps_usage_instructions;
|
apps_available && model_info.include_apps_usage_instructions;
|
||||||
world_state.add_section(AppsInstructionsState::new(
|
world_state.add_section(AppsInstructionsState::new(
|
||||||
apps_usage_instructions_available,
|
apps_usage_instructions_available,
|
||||||
));
|
));
|
||||||
let plugins_usage_instructions_available = step_context.mcp.plugins_available()
|
let plugins_usage_instructions_available =
|
||||||
&& turn_context.model_info().include_plugin_usage_instructions;
|
step_context.mcp.plugins_available() && model_info.include_plugin_usage_instructions;
|
||||||
world_state.add_section(PluginsInstructionsState::new(
|
world_state.add_section(PluginsInstructionsState::new(
|
||||||
plugins_usage_instructions_available,
|
plugins_usage_instructions_available,
|
||||||
));
|
));
|
||||||
@@ -314,10 +313,10 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut multi_agent_mode = MultiAgentModeState::new(
|
let mut multi_agent_mode = MultiAgentModeState::new(
|
||||||
super::multi_agents::effective_multi_agent_mode(turn_context),
|
super::multi_agents::effective_multi_agent_mode(step_context),
|
||||||
);
|
);
|
||||||
if let Some(usage_hint_text) =
|
if let Some(usage_hint_text) =
|
||||||
super::multi_agents::usage_hint_text(turn_context, &turn_context.session_source)
|
super::multi_agents::usage_hint_text(step_context, &turn_context.session_source)
|
||||||
{
|
{
|
||||||
let usage_hint = MultiAgentUsageHintState::new(usage_hint_text);
|
let usage_hint = MultiAgentUsageHintState::new(usage_hint_text);
|
||||||
multi_agent_mode = multi_agent_mode.with_usage_hint(&usage_hint);
|
multi_agent_mode = multi_agent_mode.with_usage_hint(&usage_hint);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::mcp::McpEnvironmentScope;
|
|||||||
use crate::mcp::McpThreadIdentity;
|
use crate::mcp::McpThreadIdentity;
|
||||||
use crate::rollout::RolloutRecorder;
|
use crate::rollout::RolloutRecorder;
|
||||||
use crate::session::session::SessionSettingsUpdate;
|
use crate::session::session::SessionSettingsUpdate;
|
||||||
|
use crate::session::step_context::StepContext;
|
||||||
use crate::session::tests::build_world_state_from_turn_context;
|
use crate::session::tests::build_world_state_from_turn_context;
|
||||||
use crate::session::tests::make_session_and_context;
|
use crate::session::tests::make_session_and_context;
|
||||||
use crate::tasks::InterruptedTurnHistoryMarker;
|
use crate::tasks::InterruptedTurnHistoryMarker;
|
||||||
@@ -598,8 +599,9 @@ async fn ignores_session_prefix_messages_when_truncating() {
|
|||||||
let (session, turn_context) = make_session_and_context().await;
|
let (session, turn_context) = make_session_and_context().await;
|
||||||
let turn_context = Arc::new(turn_context);
|
let turn_context = Arc::new(turn_context);
|
||||||
let world_state = build_world_state_from_turn_context(&session, &turn_context).await;
|
let world_state = build_world_state_from_turn_context(&session, &turn_context).await;
|
||||||
|
let step_context = StepContext::for_test(turn_context);
|
||||||
let mut items = session
|
let mut items = session
|
||||||
.build_initial_context_with_world_state(&turn_context, &world_state)
|
.build_initial_context_with_world_state(&step_context, &world_state)
|
||||||
.await;
|
.await;
|
||||||
items.push(user_msg("feature request"));
|
items.push(user_msg("feature request"));
|
||||||
items.push(assistant_msg("ack"));
|
items.push(assistant_msg("ack"));
|
||||||
@@ -1201,10 +1203,11 @@ async fn spawn_internal_session_preserves_parent_lineage_without_forking_history
|
|||||||
let reviewer_turn = reviewer.thread.session.new_default_turn().await;
|
let reviewer_turn = reviewer.thread.session.new_default_turn().await;
|
||||||
let reviewer_world_state =
|
let reviewer_world_state =
|
||||||
build_world_state_from_turn_context(&reviewer.thread.session, &reviewer_turn).await;
|
build_world_state_from_turn_context(&reviewer.thread.session, &reviewer_turn).await;
|
||||||
|
let reviewer_step = StepContext::for_test(Arc::clone(&reviewer_turn));
|
||||||
let reviewer_context = reviewer
|
let reviewer_context = reviewer
|
||||||
.thread
|
.thread
|
||||||
.session
|
.session
|
||||||
.build_initial_context_with_world_state(&reviewer_turn, &reviewer_world_state)
|
.build_initial_context_with_world_state(&reviewer_step, &reviewer_world_state)
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(
|
||||||
!serde_json::to_string(&reviewer_context)
|
!serde_json::to_string(&reviewer_context)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::session::step_context::StepContext;
|
||||||
use crate::session::tests::build_world_state_from_turn_context;
|
use crate::session::tests::build_world_state_from_turn_context;
|
||||||
use crate::session::tests::make_session_and_context;
|
use crate::session::tests::make_session_and_context;
|
||||||
use codex_protocol::AgentPath;
|
use codex_protocol::AgentPath;
|
||||||
@@ -364,8 +365,9 @@ async fn ignores_session_prefix_messages_when_truncating_rollout_from_start() {
|
|||||||
let (session, turn_context) = make_session_and_context().await;
|
let (session, turn_context) = make_session_and_context().await;
|
||||||
let turn_context = Arc::new(turn_context);
|
let turn_context = Arc::new(turn_context);
|
||||||
let world_state = build_world_state_from_turn_context(&session, &turn_context).await;
|
let world_state = build_world_state_from_turn_context(&session, &turn_context).await;
|
||||||
|
let step_context = StepContext::for_test(turn_context);
|
||||||
let mut items = session
|
let mut items = session
|
||||||
.build_initial_context_with_world_state(&turn_context, &world_state)
|
.build_initial_context_with_world_state(&step_context, &world_state)
|
||||||
.await;
|
.await;
|
||||||
items.push(user_msg("feature request"));
|
items.push(user_msg("feature request"));
|
||||||
items.push(assistant_msg("ack"));
|
items.push(assistant_msg("ack"));
|
||||||
|
|||||||
@@ -9,12 +9,19 @@ use codex_login::CodexAuth;
|
|||||||
use codex_models_manager::bundled_models_response;
|
use codex_models_manager::bundled_models_response;
|
||||||
use codex_models_manager::manager::RefreshStrategy;
|
use codex_models_manager::manager::RefreshStrategy;
|
||||||
use codex_protocol::config_types::ApprovalsReviewer;
|
use codex_protocol::config_types::ApprovalsReviewer;
|
||||||
|
use codex_protocol::config_types::Personality;
|
||||||
use codex_protocol::config_types::ReasoningSummary;
|
use codex_protocol::config_types::ReasoningSummary;
|
||||||
use codex_protocol::config_types::ServiceTier;
|
use codex_protocol::config_types::ServiceTier;
|
||||||
|
use codex_protocol::openai_models::ApprovalMessages;
|
||||||
|
use codex_protocol::openai_models::CollaborationModeMessages;
|
||||||
use codex_protocol::openai_models::ConfirmationPolicies;
|
use codex_protocol::openai_models::ConfirmationPolicies;
|
||||||
use codex_protocol::openai_models::ModelInfo;
|
use codex_protocol::openai_models::ModelInfo;
|
||||||
|
use codex_protocol::openai_models::ModelInstructionsVariables;
|
||||||
use codex_protocol::openai_models::ModelTokenBudgetConfig;
|
use codex_protocol::openai_models::ModelTokenBudgetConfig;
|
||||||
use codex_protocol::openai_models::ModelsResponse;
|
use codex_protocol::openai_models::ModelsResponse;
|
||||||
|
use codex_protocol::openai_models::MultiAgentMessages;
|
||||||
|
use codex_protocol::openai_models::MultiAgentModeMessages;
|
||||||
|
use codex_protocol::openai_models::MultiAgentRoleMessages;
|
||||||
use codex_protocol::openai_models::ReasoningEffort;
|
use codex_protocol::openai_models::ReasoningEffort;
|
||||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||||
use codex_protocol::openai_models::ToolMessage;
|
use codex_protocol::openai_models::ToolMessage;
|
||||||
@@ -344,6 +351,21 @@ async fn settings_updates_preserve_turn_identity_and_target(target: SettingsTarg
|
|||||||
Some("original-turn-state".to_string())
|
Some("original-turn-state".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(requests[2].header(TURN_STATE_HEADER), None);
|
assert_eq!(requests[2].header(TURN_STATE_HEADER), None);
|
||||||
|
let expected_switch_counts = match target {
|
||||||
|
SettingsTarget::Thread => vec![0, 0, 1],
|
||||||
|
SettingsTarget::Turn => vec![0, 1, 2],
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
requests
|
||||||
|
.iter()
|
||||||
|
.map(|request| request
|
||||||
|
.message_input_texts("developer")
|
||||||
|
.iter()
|
||||||
|
.filter(|text| text.contains("<model_switch>"))
|
||||||
|
.count())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
expected_switch_counts
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -365,7 +387,7 @@ enum TokenBudgetScenario {
|
|||||||
#[test_case(TokenBudgetScenario::InitialWindowOnly)]
|
#[test_case(TokenBudgetScenario::InitialWindowOnly)]
|
||||||
#[test_case(TokenBudgetScenario::DestinationWithoutGuidance)]
|
#[test_case(TokenBudgetScenario::DestinationWithoutGuidance)]
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn active_model_switch_resolves_token_budget_from_original_preferences(
|
async fn active_model_switch_updates_core_context_from_captured_settings(
|
||||||
scenario: TokenBudgetScenario,
|
scenario: TokenBudgetScenario,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
@@ -411,6 +433,15 @@ async fn active_model_switch_resolves_token_budget_from_original_preferences(
|
|||||||
.features
|
.features
|
||||||
.enable(Feature::TokenBudget)
|
.enable(Feature::TokenBudget)
|
||||||
.expect("enable token-budget feature");
|
.expect("enable token-budget feature");
|
||||||
|
config
|
||||||
|
.features
|
||||||
|
.enable(Feature::Personality)
|
||||||
|
.expect("enable personality");
|
||||||
|
config
|
||||||
|
.features
|
||||||
|
.enable(Feature::MultiAgentV2)
|
||||||
|
.expect("enable multi-agent V2");
|
||||||
|
config.personality = Some(Personality::Pragmatic);
|
||||||
if context_window_model.is_some() {
|
if context_window_model.is_some() {
|
||||||
config.model_context_window = None;
|
config.model_context_window = None;
|
||||||
}
|
}
|
||||||
@@ -426,11 +457,38 @@ async fn active_model_switch_resolves_token_budget_from_original_preferences(
|
|||||||
model.context_window = (slug == context_window_model).then_some(128_000);
|
model.context_window = (slug == context_window_model).then_some(128_000);
|
||||||
model.max_context_window = None;
|
model.max_context_window = None;
|
||||||
}
|
}
|
||||||
model
|
let messages = model.model_messages.as_mut().expect("model messages");
|
||||||
.model_messages
|
messages.instructions_template = Some(format!(
|
||||||
.as_mut()
|
"Instructions for {slug}. {{{{ personality }}}}"
|
||||||
.expect("model messages")
|
));
|
||||||
.token_budget = (initial_model || destination_has_guidance).then(|| ModelTokenBudgetConfig {
|
messages.instructions_variables = Some(ModelInstructionsVariables {
|
||||||
|
personality_default: Some(format!("Default {slug} personality.")),
|
||||||
|
personality_friendly: Some(format!("Friendly {slug} personality.")),
|
||||||
|
personality_pragmatic: Some(format!("Pragmatic {slug} personality.")),
|
||||||
|
});
|
||||||
|
messages.collaboration_modes = Some(CollaborationModeMessages {
|
||||||
|
default: Some(format!("Default collaboration for {slug}.")),
|
||||||
|
plan: None,
|
||||||
|
});
|
||||||
|
messages.multi_agent = Some(MultiAgentMessages {
|
||||||
|
role: Some(MultiAgentRoleMessages {
|
||||||
|
root: Some(format!("Root role for {slug}.")),
|
||||||
|
subagent: None,
|
||||||
|
}),
|
||||||
|
mode: Some(MultiAgentModeMessages {
|
||||||
|
explicit: Some(format!("Delegation policy for {slug}.")),
|
||||||
|
proactive: None,
|
||||||
|
hint_text: None,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
messages.approvals = Some(ApprovalMessages {
|
||||||
|
on_request: Some(format!("Approval instructions for {slug}.")),
|
||||||
|
on_request_auto_review: None,
|
||||||
|
never: None,
|
||||||
|
unless_trusted: None,
|
||||||
|
});
|
||||||
|
messages.token_budget =
|
||||||
|
(initial_model || destination_has_guidance).then(|| ModelTokenBudgetConfig {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
use_history_notes_extension: false,
|
use_history_notes_extension: false,
|
||||||
reminder_threshold_tokens: if initial_model { 8_000 } else { 2_000 },
|
reminder_threshold_tokens: if initial_model { 8_000 } else { 2_000 },
|
||||||
@@ -487,6 +545,64 @@ async fn active_model_switch_resolves_token_budget_from_original_preferences(
|
|||||||
|
|
||||||
let requests = response_mock.requests();
|
let requests = response_mock.requests();
|
||||||
assert_eq!(requests.len(), 3);
|
assert_eq!(requests.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
requests
|
||||||
|
.iter()
|
||||||
|
.map(|request| request.body_json()["model"].clone())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![json!(MODEL_A), json!(MODEL_B), json!(MODEL_B)]
|
||||||
|
);
|
||||||
|
let initial_instructions =
|
||||||
|
format!("Instructions for {MODEL_A}. Pragmatic {MODEL_A} personality.");
|
||||||
|
assert_eq!(requests[0].instructions_text(), initial_instructions);
|
||||||
|
assert!(!requests[0].body_contains_text("<model_switch>"));
|
||||||
|
for text in [
|
||||||
|
format!("Root role for {MODEL_A}."),
|
||||||
|
format!("Delegation policy for {MODEL_A}."),
|
||||||
|
] {
|
||||||
|
assert!(requests[0].body_contains_text(&text));
|
||||||
|
}
|
||||||
|
for pair in requests.windows(/*size*/ 2) {
|
||||||
|
assert!(
|
||||||
|
pair[1].input().starts_with(&pair[0].input()),
|
||||||
|
"context updates must be append-only"
|
||||||
|
);
|
||||||
|
assert_eq!(pair[1].instructions_text(), initial_instructions);
|
||||||
|
assert_eq!(pair[1].body_json()["tools"], pair[0].body_json()["tools"]);
|
||||||
|
assert_eq!(
|
||||||
|
pair[1].message_input_texts("user"),
|
||||||
|
pair[0].message_input_texts("user")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for request in &requests[1..] {
|
||||||
|
let developer_texts = request.message_input_texts("developer");
|
||||||
|
let switches = developer_texts
|
||||||
|
.iter()
|
||||||
|
.filter(|text| text.contains("<model_switch>"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(switches.len(), 1);
|
||||||
|
assert!(switches[0].contains(&format!(
|
||||||
|
"Instructions for {MODEL_B}. Pragmatic {MODEL_B} personality."
|
||||||
|
)));
|
||||||
|
assert!(
|
||||||
|
!request.body_contains_text("<personality_spec>"),
|
||||||
|
"personality is included in the model-switch instructions"
|
||||||
|
);
|
||||||
|
for text in [
|
||||||
|
format!("Default collaboration for {MODEL_B}."),
|
||||||
|
format!("Approval instructions for {MODEL_B}."),
|
||||||
|
format!("Root role for {MODEL_B}."),
|
||||||
|
format!("Delegation policy for {MODEL_B}."),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
developer_texts
|
||||||
|
.iter()
|
||||||
|
.filter(|message| message.contains(&text))
|
||||||
|
.count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
let initial_guidance = format!("Use {MODEL_A} token-budget guidance.");
|
let initial_guidance = format!("Use {MODEL_A} token-budget guidance.");
|
||||||
let initial_guidance_expected =
|
let initial_guidance_expected =
|
||||||
!explicit_default_template && context_window_model != Some(MODEL_B);
|
!explicit_default_template && context_window_model != Some(MODEL_B);
|
||||||
@@ -532,6 +648,89 @@ async fn active_model_switch_resolves_token_budget_from_original_preferences(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_case(Some(ReasoningEffort::Ultra); "selected effort")]
|
||||||
|
#[test_case(None; "captured model default effort")]
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn active_model_switch_updates_multi_agent_policy_from_captured_effort(
|
||||||
|
effort: Option<ReasoningEffort>,
|
||||||
|
) -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
|
let use_model_default = effort.is_none();
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let response_mock = mount_sse_sequence(
|
||||||
|
&server,
|
||||||
|
vec![
|
||||||
|
paused_response("resp-1", "pause-before-effort-switch"),
|
||||||
|
sse_completed("resp-2"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let test = step_settings_test()
|
||||||
|
.with_config(move |config| {
|
||||||
|
config
|
||||||
|
.features
|
||||||
|
.enable(Feature::MultiAgentV2)
|
||||||
|
.expect("enable multi-agent V2");
|
||||||
|
for model in &mut config
|
||||||
|
.model_catalog
|
||||||
|
.as_mut()
|
||||||
|
.expect("controlled model catalog")
|
||||||
|
.models
|
||||||
|
{
|
||||||
|
model
|
||||||
|
.model_messages
|
||||||
|
.as_mut()
|
||||||
|
.expect("model messages")
|
||||||
|
.multi_agent = None;
|
||||||
|
model
|
||||||
|
.supported_reasoning_levels
|
||||||
|
.push(ReasoningEffortPreset {
|
||||||
|
effort: ReasoningEffort::Ultra,
|
||||||
|
description: "Ultra".to_string(),
|
||||||
|
});
|
||||||
|
model.default_reasoning_level =
|
||||||
|
Some(if model.slug == MODEL_B && use_model_default {
|
||||||
|
ReasoningEffort::Ultra
|
||||||
|
} else {
|
||||||
|
ReasoningEffort::Low
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build_with_auto_env(&server)
|
||||||
|
.await?;
|
||||||
|
let request = start_paused_turn(&test.codex).await?;
|
||||||
|
assert_eq!(
|
||||||
|
submit_turn_settings(
|
||||||
|
&test.codex,
|
||||||
|
&request.turn_id,
|
||||||
|
TurnSettingsUpdate {
|
||||||
|
model: Some(MODEL_B.to_string()),
|
||||||
|
effort: Some(effort),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
TurnSettingsUpdateOutcome::Applied
|
||||||
|
);
|
||||||
|
answer_paused_turn(&test.codex, &request.turn_id).await?;
|
||||||
|
wait_for_event(&test.codex, |event| {
|
||||||
|
matches!(event, EventMsg::TurnComplete(_))
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let requests = response_mock.requests();
|
||||||
|
assert_eq!(requests.len(), 2);
|
||||||
|
assert_eq!(requests[0].body_json()["model"], MODEL_A);
|
||||||
|
assert_eq!(requests[1].body_json()["model"], MODEL_B);
|
||||||
|
assert_eq!(requests[1].body_json()["reasoning"]["effort"], "xhigh");
|
||||||
|
let proactive_text = "Proactive multi-agent delegation is active.";
|
||||||
|
assert!(!requests[0].body_contains_text(proactive_text));
|
||||||
|
assert!(requests[1].body_contains_text(proactive_text));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn mcp_confirmation_policy_follows_step_model_changes() -> Result<()> {
|
async fn mcp_confirmation_policy_follows_step_model_changes() -> Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
|
|||||||
Reference in new issue
Block a user