mirror of
https://github.com/openai/codex.git
synced 2026-04-23 22:24:57 +00:00
fix(core): restore parent fork spawn config
This commit is contained in:
@@ -1129,6 +1129,12 @@ impl SessionConfiguration {
|
||||
model: self.collaboration_mode.model().to_string(),
|
||||
model_provider_id: self.original_config_do_not_use.model_provider_id.clone(),
|
||||
service_tier: self.service_tier,
|
||||
plan_mode_reasoning_effort: self.original_config_do_not_use.plan_mode_reasoning_effort,
|
||||
model_verbosity: self.original_config_do_not_use.model_verbosity,
|
||||
model_context_window: self.original_config_do_not_use.model_context_window,
|
||||
model_auto_compact_token_limit: self
|
||||
.original_config_do_not_use
|
||||
.model_auto_compact_token_limit,
|
||||
approval_policy: self.approval_policy.value(),
|
||||
approvals_reviewer: self.approvals_reviewer,
|
||||
sandbox_policy: self.sandbox_policy.get().clone(),
|
||||
@@ -1136,6 +1142,7 @@ impl SessionConfiguration {
|
||||
ephemeral: self.original_config_do_not_use.ephemeral,
|
||||
reasoning_effort: self.collaboration_mode.reasoning_effort(),
|
||||
personality: self.personality,
|
||||
active_profile: self.original_config_do_not_use.active_profile.clone(),
|
||||
session_source: self.session_source.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_features::Feature;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -33,6 +34,10 @@ pub struct ThreadConfigSnapshot {
|
||||
pub model: String,
|
||||
pub model_provider_id: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub plan_mode_reasoning_effort: Option<ReasoningEffort>,
|
||||
pub model_verbosity: Option<Verbosity>,
|
||||
pub model_context_window: Option<i64>,
|
||||
pub model_auto_compact_token_limit: Option<i64>,
|
||||
pub approval_policy: AskForApproval,
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
@@ -40,6 +45,7 @@ pub struct ThreadConfigSnapshot {
|
||||
pub ephemeral: bool,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
pub personality: Option<Personality>,
|
||||
pub active_profile: Option<String>,
|
||||
pub session_source: SessionSource,
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ impl ToolHandler for Handler {
|
||||
apply_role_to_config(&mut config, role_name)
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
if fork_context {
|
||||
restore_forked_spawn_agent_model_config(&mut config, turn.as_ref());
|
||||
}
|
||||
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
|
||||
apply_spawn_agent_overrides(&mut config, child_depth);
|
||||
|
||||
|
||||
@@ -238,6 +238,24 @@ fn build_agent_shared_config(turn: &TurnContext) -> Result<Config, FunctionCallE
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Restores parent-owned model selection after role application on forked spawns.
|
||||
pub(crate) fn restore_forked_spawn_agent_model_config(config: &mut Config, turn: &TurnContext) {
|
||||
config.model = Some(turn.model_info.slug.clone());
|
||||
config.service_tier = turn.config.service_tier;
|
||||
config.model_provider_id = turn.config.model_provider_id.clone();
|
||||
config.model_provider = turn.provider.clone();
|
||||
config.model_reasoning_effort = turn
|
||||
.reasoning_effort
|
||||
.or(turn.model_info.default_reasoning_level);
|
||||
config.plan_mode_reasoning_effort = turn.config.plan_mode_reasoning_effort;
|
||||
config.model_reasoning_summary = Some(turn.reasoning_summary);
|
||||
config.model_verbosity = turn.config.model_verbosity;
|
||||
config.model_context_window = turn.config.model_context_window;
|
||||
config.model_auto_compact_token_limit = turn.config.model_auto_compact_token_limit;
|
||||
config.model_supports_reasoning_summaries = turn.config.model_supports_reasoning_summaries;
|
||||
config.active_profile = turn.config.active_profile.clone();
|
||||
}
|
||||
|
||||
/// Copies runtime-only turn state onto a child config before it is handed to `AgentControl`.
|
||||
///
|
||||
/// These values are chosen by the live turn rather than persisted config, so leaving them stale
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::CodexAuth;
|
||||
use crate::ThreadManager;
|
||||
use crate::built_in_model_providers;
|
||||
use crate::codex::make_session_and_context;
|
||||
use crate::config::AgentRoleConfig;
|
||||
use crate::config::DEFAULT_AGENT_MAX_DEPTH;
|
||||
use crate::config::types::ShellEnvironmentPolicy;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
@@ -34,11 +35,14 @@ use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
@@ -89,6 +93,53 @@ fn thread_manager() -> ThreadManager {
|
||||
)
|
||||
}
|
||||
|
||||
async fn install_role_with_model_provider_and_profile_override(turn: &mut TurnContext) -> String {
|
||||
let role_name = "fork-context-role".to_string();
|
||||
let role_config_path = turn.config.codex_home.join("fork-context-role.toml");
|
||||
tokio::fs::write(
|
||||
&role_config_path,
|
||||
r#"developer_instructions = "Forked children should keep the parent model config."
|
||||
model_provider = "role-provider"
|
||||
model_context_window = 12345
|
||||
model_auto_compact_token_limit = 1234
|
||||
model_verbosity = "low"
|
||||
plan_mode_reasoning_effort = "minimal"
|
||||
profile = "role-profile"
|
||||
service_tier = "fast"
|
||||
|
||||
[profiles.role-profile]
|
||||
model_provider = "role-provider"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("role config should be written");
|
||||
|
||||
let mut config = (*turn.config).clone();
|
||||
let mut role_provider =
|
||||
built_in_model_providers(/* openai_base_url */ /*openai_base_url*/ None)["openai"].clone();
|
||||
role_provider.name = "Role Provider".to_string();
|
||||
config
|
||||
.model_providers
|
||||
.insert("role-provider".to_string(), role_provider);
|
||||
config.service_tier = Some(ServiceTier::Flex);
|
||||
config.plan_mode_reasoning_effort = Some(ReasoningEffort::High);
|
||||
config.model_verbosity = Some(Verbosity::High);
|
||||
config.model_context_window = Some(200_000);
|
||||
config.model_auto_compact_token_limit = Some(180_000);
|
||||
config.agent_roles.insert(
|
||||
role_name.clone(),
|
||||
AgentRoleConfig {
|
||||
description: Some("Role with model-provider and profile overrides".to_string()),
|
||||
config_file: Some(role_config_path),
|
||||
nickname_candidates: None,
|
||||
fork_context: None,
|
||||
},
|
||||
);
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
role_name
|
||||
}
|
||||
|
||||
fn history_contains_inter_agent_communication(
|
||||
history_items: &[ResponseItem],
|
||||
expected: &InterAgentCommunication,
|
||||
@@ -305,7 +356,8 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_agent_fork_context_ignores_child_model_overrides() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let role_name = install_role_with_model_provider_and_profile_override(&mut turn).await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
.start_thread((*turn.config).clone())
|
||||
@@ -314,7 +366,15 @@ async fn spawn_agent_fork_context_ignores_child_model_overrides() {
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
let expected_model = turn.model_info.slug.clone();
|
||||
let expected_model_provider_id = turn.config.model_provider_id.clone();
|
||||
let expected_model_provider_name = turn.provider.name.clone();
|
||||
let expected_active_profile = turn.config.active_profile.clone();
|
||||
let expected_reasoning_effort = turn.reasoning_effort;
|
||||
let expected_service_tier = turn.config.service_tier;
|
||||
let expected_plan_mode_reasoning_effort = turn.config.plan_mode_reasoning_effort;
|
||||
let expected_model_verbosity = turn.config.model_verbosity;
|
||||
let expected_model_context_window = turn.config.model_context_window;
|
||||
let expected_model_auto_compact_token_limit = turn.config.model_auto_compact_token_limit;
|
||||
|
||||
let output = SpawnAgentHandler
|
||||
.handle(invocation(
|
||||
@@ -323,6 +383,7 @@ async fn spawn_agent_fork_context_ignores_child_model_overrides() {
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"message": "inspect this repo",
|
||||
"agent_type": role_name,
|
||||
"model": "not-a-real-model",
|
||||
"reasoning_effort": "low",
|
||||
"fork_context": true
|
||||
@@ -346,12 +407,27 @@ async fn spawn_agent_fork_context_ignores_child_model_overrides() {
|
||||
.await;
|
||||
|
||||
assert_eq!(snapshot.model, expected_model);
|
||||
assert_eq!(snapshot.model_provider_id, expected_model_provider_id);
|
||||
assert_eq!(snapshot.model_provider.name, expected_model_provider_name);
|
||||
assert_eq!(snapshot.active_profile, expected_active_profile);
|
||||
assert_eq!(snapshot.reasoning_effort, expected_reasoning_effort);
|
||||
assert_eq!(snapshot.service_tier, expected_service_tier);
|
||||
assert_eq!(
|
||||
snapshot.plan_mode_reasoning_effort,
|
||||
expected_plan_mode_reasoning_effort
|
||||
);
|
||||
assert_eq!(snapshot.model_verbosity, expected_model_verbosity);
|
||||
assert_eq!(snapshot.model_context_window, expected_model_context_window);
|
||||
assert_eq!(
|
||||
snapshot.model_auto_compact_token_limit,
|
||||
expected_model_auto_compact_token_limit
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_spawn_fork_turns_ignores_child_model_overrides() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let role_name = install_role_with_model_provider_and_profile_override(&mut turn).await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
.start_thread((*turn.config).clone())
|
||||
@@ -369,7 +445,15 @@ async fn multi_agent_v2_spawn_fork_turns_ignores_child_model_overrides() {
|
||||
..turn
|
||||
};
|
||||
let expected_model = turn.model_info.slug.clone();
|
||||
let expected_model_provider_id = turn.config.model_provider_id.clone();
|
||||
let expected_model_provider_name = turn.provider.name.clone();
|
||||
let expected_active_profile = turn.config.active_profile.clone();
|
||||
let expected_reasoning_effort = turn.reasoning_effort;
|
||||
let expected_service_tier = turn.config.service_tier;
|
||||
let expected_plan_mode_reasoning_effort = turn.config.plan_mode_reasoning_effort;
|
||||
let expected_model_verbosity = turn.config.model_verbosity;
|
||||
let expected_model_context_window = turn.config.model_context_window;
|
||||
let expected_model_auto_compact_token_limit = turn.config.model_auto_compact_token_limit;
|
||||
|
||||
let output = SpawnAgentHandlerV2
|
||||
.handle(invocation(
|
||||
@@ -377,7 +461,8 @@ async fn multi_agent_v2_spawn_fork_turns_ignores_child_model_overrides() {
|
||||
Arc::new(turn),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}],
|
||||
"message": "inspect this repo",
|
||||
"agent_type": role_name,
|
||||
"model": "not-a-real-model",
|
||||
"reasoning_effort": "low",
|
||||
"fork_turns": "all",
|
||||
@@ -404,7 +489,21 @@ async fn multi_agent_v2_spawn_fork_turns_ignores_child_model_overrides() {
|
||||
.await;
|
||||
|
||||
assert_eq!(snapshot.model, expected_model);
|
||||
assert_eq!(snapshot.model_provider_id, expected_model_provider_id);
|
||||
assert_eq!(snapshot.model_provider.name, expected_model_provider_name);
|
||||
assert_eq!(snapshot.active_profile, expected_active_profile);
|
||||
assert_eq!(snapshot.reasoning_effort, expected_reasoning_effort);
|
||||
assert_eq!(snapshot.service_tier, expected_service_tier);
|
||||
assert_eq!(
|
||||
snapshot.plan_mode_reasoning_effort,
|
||||
expected_plan_mode_reasoning_effort
|
||||
);
|
||||
assert_eq!(snapshot.model_verbosity, expected_model_verbosity);
|
||||
assert_eq!(snapshot.model_context_window, expected_model_context_window);
|
||||
assert_eq!(
|
||||
snapshot.model_auto_compact_token_limit,
|
||||
expected_model_auto_compact_token_limit
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -81,6 +81,9 @@ impl ToolHandler for Handler {
|
||||
apply_role_to_config(&mut config, role_name)
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
if fork_mode.is_some() {
|
||||
restore_forked_spawn_agent_model_config(&mut config, turn.as_ref());
|
||||
}
|
||||
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
|
||||
apply_spawn_agent_overrides(&mut config, child_depth);
|
||||
|
||||
|
||||
@@ -224,7 +224,9 @@ async fn setup_turn_one_with_custom_spawned_child(
|
||||
test.submit_turn(TURN_1_PROMPT).await?;
|
||||
if child_response_delay.is_none() && wait_for_parent_notification {
|
||||
let _ = wait_for_requests(&child_request_log).await?;
|
||||
let rollout_path = test.codex.rollout_path().expect("rollout path");
|
||||
let Some(rollout_path) = test.codex.rollout_path() else {
|
||||
anyhow::bail!("rollout path");
|
||||
};
|
||||
let deadline = Instant::now() + Duration::from_secs(6);
|
||||
loop {
|
||||
test.codex.ensure_rollout_materialized().await;
|
||||
|
||||
Reference in New Issue
Block a user