mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Merge remote-tracking branch 'upstream/dev/friel/fork-context-inherits-parent-model' into repair/collab-stack-refresh-20260402
# Conflicts: # codex-rs/core/config.schema.json # codex-rs/core/src/agent/role.rs # codex-rs/core/src/tools/handlers/multi_agents/spawn.rs # codex-rs/core/src/tools/handlers/multi_agents_tests.rs # codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs # codex-rs/core/tests/suite/subagent_notifications.rs # codex-rs/tools/src/agent_tool_tests.rs
This commit is contained in:
@@ -8911,6 +8911,10 @@ mod tests {
|
||||
model: "gpt-5".to_string(),
|
||||
model_provider_id: "openai".to_string(),
|
||||
service_tier: Some(codex_protocol::config_types::ServiceTier::Flex),
|
||||
plan_mode_reasoning_effort: None,
|
||||
model_verbosity: None,
|
||||
model_context_window: None,
|
||||
model_auto_compact_token_limit: None,
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::OnRequest,
|
||||
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
|
||||
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
@@ -8919,6 +8923,7 @@ mod tests {
|
||||
agent_use_function_call_inbox: false,
|
||||
reasoning_effort: None,
|
||||
personality: None,
|
||||
active_profile: None,
|
||||
session_source: SessionSource::Cli,
|
||||
};
|
||||
|
||||
|
||||
@@ -1880,6 +1880,11 @@ async fn turn_start_emits_spawn_agent_item_with_effective_role_model_metadata_v2
|
||||
const CHILD_PROMPT: &str = "child: do work";
|
||||
const PARENT_PROMPT: &str = "spawn a child and continue";
|
||||
const SPAWN_CALL_ID: &str = "spawn-call-1";
|
||||
const INHERITED_MODEL: &str = "gpt-5.2-codex";
|
||||
// thread/start only sets the model here; the session keeps the resolved collaboration-mode
|
||||
// effort for that thread, so fork-context children should inherit `Medium`, not the model
|
||||
// catalog default.
|
||||
const INHERITED_REASONING_EFFORT: ReasoningEffort = ReasoningEffort::Medium;
|
||||
const REQUESTED_MODEL: &str = "gpt-5.1";
|
||||
const REQUESTED_REASONING_EFFORT: ReasoningEffort = ReasoningEffort::Low;
|
||||
const ROLE_MODEL: &str = "gpt-5.1-codex-max";
|
||||
@@ -2022,8 +2027,8 @@ config_file = "./custom-role.toml"
|
||||
assert_eq!(sender_thread_id, thread.id);
|
||||
assert_eq!(receiver_thread_ids, vec![receiver_thread_id.clone()]);
|
||||
assert_eq!(prompt, Some(CHILD_PROMPT.to_string()));
|
||||
assert_eq!(model, Some(ROLE_MODEL.to_string()));
|
||||
assert_eq!(reasoning_effort, Some(ROLE_REASONING_EFFORT));
|
||||
assert_eq!(model, Some(INHERITED_MODEL.to_string()));
|
||||
assert_eq!(reasoning_effort, Some(INHERITED_REASONING_EFFORT));
|
||||
let agent_state = agents_states
|
||||
.get(&receiver_thread_id)
|
||||
.expect("spawn completion should include child agent state");
|
||||
|
||||
@@ -137,7 +137,7 @@ pub(crate) fn default_fork_context_for_role(config: &Config, role_name: Option<&
|
||||
let role_name = role_name.unwrap_or(DEFAULT_ROLE_NAME);
|
||||
resolve_role_config(config, role_name)
|
||||
.and_then(|role| role.fork_context)
|
||||
.unwrap_or(false)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn preservation_policy(config: &Config, role_layer_toml: &TomlValue) -> (bool, bool) {
|
||||
|
||||
@@ -72,6 +72,109 @@ async fn apply_role_returns_error_for_unknown_role() {
|
||||
assert_eq!(err, "unknown agent_type 'missing-role'");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_fork_context_for_role_defaults_unspecified_custom_roles_to_true() {
|
||||
let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some("Custom role".to_string()),
|
||||
config_file: None,
|
||||
nickname_candidates: None,
|
||||
fork_context: None,
|
||||
},
|
||||
);
|
||||
|
||||
assert!(default_fork_context_for_role(&config, Some("custom")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_fork_context_for_role_defaults_discovered_role_files_to_true() {
|
||||
let codex_home = TempDir::new().expect("create temp dir");
|
||||
let repo_root = TempDir::new().expect("create temp dir");
|
||||
let nested_cwd = repo_root.path().join("packages").join("app");
|
||||
fs::create_dir_all(repo_root.path().join(".git")).expect("create git dir");
|
||||
fs::create_dir_all(&nested_cwd).expect("create nested cwd");
|
||||
|
||||
let workspace_key = repo_root.path().to_string_lossy().replace('\\', "\\\\");
|
||||
tokio::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
format!(
|
||||
r#"[projects."{workspace_key}"]
|
||||
trust_level = "trusted"
|
||||
"#
|
||||
),
|
||||
)
|
||||
.await
|
||||
.expect("write config");
|
||||
|
||||
let agents_dir = repo_root.path().join(".codex").join("agents");
|
||||
tokio::fs::create_dir_all(&agents_dir)
|
||||
.await
|
||||
.expect("create agents dir");
|
||||
tokio::fs::write(
|
||||
agents_dir.join("custom.toml"),
|
||||
r#"
|
||||
name = "custom"
|
||||
description = "Custom role"
|
||||
developer_instructions = "Stay focused"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write role file");
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
cwd: Some(nested_cwd),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
|
||||
assert!(default_fork_context_for_role(&config, Some("custom")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_fork_context_for_role_uses_explicit_custom_role_override() {
|
||||
let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some("Custom role".to_string()),
|
||||
config_file: None,
|
||||
nickname_candidates: None,
|
||||
fork_context: Some(false),
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!default_fork_context_for_role(&config, Some("custom")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_fork_context_for_role_uses_explicit_custom_role_override_from_config_toml() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"[agents.custom]
|
||||
description = "Custom role"
|
||||
fork_context = false
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config");
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load test config");
|
||||
|
||||
assert!(!default_fork_context_for_role(&config, Some("custom")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "No role requiring it for now"]
|
||||
async fn apply_explorer_role_sets_model_and_adds_session_flags_layer() {
|
||||
|
||||
@@ -1247,6 +1247,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(),
|
||||
@@ -1257,6 +1263,7 @@ impl SessionConfiguration {
|
||||
.agent_use_function_call_inbox,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,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,
|
||||
@@ -41,6 +46,7 @@ pub struct ThreadConfigSnapshot {
|
||||
pub agent_use_function_call_inbox: bool,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
pub personality: Option<Personality>,
|
||||
pub active_profile: Option<String>,
|
||||
pub session_source: SessionSource,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::agent::WatchdogRegistration;
|
||||
use crate::agent::control::SpawnAgentForkMode;
|
||||
use crate::agent::control::SpawnAgentOptions;
|
||||
use crate::agent::control::render_input_preview;
|
||||
use crate::agent::exceeds_thread_spawn_depth_limit;
|
||||
use crate::agent::next_thread_spawn_depth;
|
||||
use crate::agent::role::DEFAULT_ROLE_NAME;
|
||||
use crate::agent::role::apply_role_to_config;
|
||||
@@ -96,7 +97,6 @@ impl ToolHandler for Handler {
|
||||
.await;
|
||||
let config =
|
||||
build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
|
||||
|
||||
let mut candidates_to_try = collect_spawn_agent_model_candidates(
|
||||
args.model_fallback_list.as_ref(),
|
||||
args.model.as_deref(),
|
||||
@@ -112,17 +112,22 @@ impl ToolHandler for Handler {
|
||||
let mut spawn_result = None;
|
||||
for (idx, candidate) in candidates_to_try.iter().enumerate() {
|
||||
let mut candidate_config = config.clone();
|
||||
apply_requested_spawn_agent_model_overrides(
|
||||
&session,
|
||||
turn.as_ref(),
|
||||
&mut candidate_config,
|
||||
candidate.model.as_deref(),
|
||||
candidate.reasoning_effort,
|
||||
)
|
||||
.await?;
|
||||
if !fork_context {
|
||||
apply_requested_spawn_agent_model_overrides(
|
||||
&session,
|
||||
turn.as_ref(),
|
||||
&mut candidate_config,
|
||||
candidate.model.as_deref(),
|
||||
candidate.reasoning_effort,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
apply_role_to_config(&mut candidate_config, role_name)
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
if fork_context {
|
||||
restore_forked_spawn_agent_model_config(&mut candidate_config, turn.as_ref());
|
||||
}
|
||||
apply_spawn_agent_runtime_overrides(&mut candidate_config, turn.as_ref())?;
|
||||
apply_spawn_agent_overrides(&mut candidate_config, child_depth);
|
||||
let attempt_result = session
|
||||
|
||||
@@ -332,6 +332,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
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::ThreadManager;
|
||||
use crate::agent::WatchdogRegistration;
|
||||
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;
|
||||
@@ -24,11 +25,14 @@ use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
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::AgentStatus;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -56,6 +60,7 @@ use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::timeout;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing_test::traced_test;
|
||||
|
||||
fn invocation(
|
||||
session: Arc<crate::codex::Session>,
|
||||
@@ -139,6 +144,52 @@ async fn attach_watchdog_helper_for_tests(
|
||||
helper.thread_id
|
||||
}
|
||||
|
||||
async fn install_role_with_model_provider_and_profile_override(turn: &mut TurnContext) -> String {
|
||||
let role_name = "fork-context-role".to_string();
|
||||
tokio::fs::create_dir_all(&turn.config.codex_home)
|
||||
.await
|
||||
.expect("codex home should be created");
|
||||
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 = "openai"
|
||||
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 = "openai"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("role config should be written");
|
||||
|
||||
let mut config = (*turn.config).clone();
|
||||
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()),
|
||||
model: None,
|
||||
config_file: Some(role_config_path),
|
||||
watchdog_interval_s: None,
|
||||
nickname_candidates: None,
|
||||
fork_context: None,
|
||||
},
|
||||
);
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
role_name
|
||||
}
|
||||
|
||||
fn history_contains_inter_agent_communication(
|
||||
history_items: &[ResponseItem],
|
||||
expected: &InterAgentCommunication,
|
||||
@@ -360,8 +411,10 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[traced_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())
|
||||
@@ -370,7 +423,14 @@ 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_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(
|
||||
@@ -379,6 +439,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
|
||||
@@ -402,12 +463,26 @@ 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.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_context_ignores_child_model_overrides() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
async fn multi_agent_v2_spawn_fork_turns_ignores_child_model_overrides() {
|
||||
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())
|
||||
@@ -425,7 +500,14 @@ async fn multi_agent_v2_spawn_fork_context_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_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(
|
||||
@@ -434,9 +516,10 @@ async fn multi_agent_v2_spawn_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,
|
||||
"fork_turns": "all",
|
||||
"task_name": "fork_context_v2"
|
||||
})),
|
||||
))
|
||||
@@ -460,7 +543,20 @@ async fn multi_agent_v2_spawn_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.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]
|
||||
|
||||
@@ -39,7 +39,8 @@ impl ToolHandler for Handler {
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|role| !role.is_empty());
|
||||
let fork_mode = args.fork_mode(&turn.config, role_name)?;
|
||||
let fork_context = default_fork_context_for_role(&turn.config, role_name);
|
||||
let fork_mode = args.fork_mode(fork_context)?;
|
||||
|
||||
let initial_operation = parse_collab_input(Some(args.message), /*items*/ None)?;
|
||||
let prompt = render_input_preview(&initial_operation);
|
||||
@@ -120,17 +121,22 @@ impl ToolHandler for Handler {
|
||||
let mut spawn_result = None;
|
||||
for (idx, candidate) in candidates_to_try.iter().enumerate() {
|
||||
let mut candidate_config = config.clone();
|
||||
apply_requested_spawn_agent_model_overrides(
|
||||
&session,
|
||||
turn.as_ref(),
|
||||
&mut candidate_config,
|
||||
candidate.model.as_deref(),
|
||||
candidate.reasoning_effort,
|
||||
)
|
||||
.await?;
|
||||
if fork_mode.is_none() {
|
||||
apply_requested_spawn_agent_model_overrides(
|
||||
&session,
|
||||
turn.as_ref(),
|
||||
&mut candidate_config,
|
||||
candidate.model.as_deref(),
|
||||
candidate.reasoning_effort,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
apply_role_to_config(&mut candidate_config, role_name)
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
if fork_mode.is_some() {
|
||||
restore_forked_spawn_agent_model_config(&mut candidate_config, turn.as_ref());
|
||||
}
|
||||
apply_spawn_agent_runtime_overrides(&mut candidate_config, turn.as_ref())?;
|
||||
apply_spawn_agent_overrides(&mut candidate_config, child_depth);
|
||||
let attempt_result = session
|
||||
@@ -268,36 +274,42 @@ struct SpawnAgentArgs {
|
||||
impl SpawnAgentArgs {
|
||||
fn fork_mode(
|
||||
&self,
|
||||
config: &crate::config::Config,
|
||||
role_name: Option<&str>,
|
||||
default_fork_context: bool,
|
||||
) -> Result<Option<SpawnAgentForkMode>, FunctionCallError> {
|
||||
if let Some(fork_turns) = self.fork_turns.as_deref() {
|
||||
let trimmed = fork_turns.trim();
|
||||
return match trimmed {
|
||||
"none" => Ok(None),
|
||||
"all" => Ok(Some(SpawnAgentForkMode::FullHistory)),
|
||||
_ => {
|
||||
let Ok(last_n_turns) = trimmed.parse::<usize>() else {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"fork_turns must be `none`, `all`, or a positive integer string"
|
||||
.to_string(),
|
||||
));
|
||||
};
|
||||
if last_n_turns == 0 {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"fork_turns must be `none`, `all`, or a positive integer string"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(Some(SpawnAgentForkMode::LastNTurns(last_n_turns)))
|
||||
}
|
||||
};
|
||||
if self.fork_context.is_some() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"fork_context is not supported in MultiAgentV2; use fork_turns instead".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let fork_context = self
|
||||
.fork_context
|
||||
.unwrap_or_else(|| default_fork_context_for_role(config, role_name));
|
||||
Ok(fork_context.then_some(SpawnAgentForkMode::FullHistory))
|
||||
let Some(fork_turns) = self
|
||||
.fork_turns
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|fork_turns| !fork_turns.is_empty())
|
||||
else {
|
||||
return Ok(default_fork_context.then_some(SpawnAgentForkMode::FullHistory));
|
||||
};
|
||||
|
||||
if fork_turns.eq_ignore_ascii_case("none") {
|
||||
return Ok(None);
|
||||
}
|
||||
if fork_turns.eq_ignore_ascii_case("all") {
|
||||
return Ok(Some(SpawnAgentForkMode::FullHistory));
|
||||
}
|
||||
|
||||
let last_n_turns = fork_turns.parse::<usize>().map_err(|_| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"fork_turns must be `none`, `all`, or a positive integer string".to_string(),
|
||||
)
|
||||
})?;
|
||||
if last_n_turns == 0 {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"fork_turns must be `none`, `all`, or a positive integer string".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(SpawnAgentForkMode::LastNTurns(last_n_turns)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -290,7 +290,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;
|
||||
@@ -506,7 +508,8 @@ async fn spawn_agent_inherits_parent_model_and_reasoning_without_role() -> Resul
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn spawn_agent_role_overrides_requested_model_and_reasoning_settings() -> Result<()> {
|
||||
async fn spawn_agent_role_overrides_requested_model_and_reasoning_settings_without_fork_context()
|
||||
-> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -517,6 +520,7 @@ async fn spawn_agent_role_overrides_requested_model_and_reasoning_settings() ->
|
||||
"agent_type": "custom",
|
||||
"model": REQUESTED_MODEL,
|
||||
"reasoning_effort": REQUESTED_REASONING_EFFORT,
|
||||
"fork_context": false,
|
||||
}),
|
||||
|builder| {
|
||||
builder.with_config(|config| {
|
||||
|
||||
@@ -699,7 +699,7 @@ fn spawn_agent_common_properties(agent_type_description: &str) -> BTreeMap<Strin
|
||||
"model".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Optional model override for the new agent. Replaces the inherited model."
|
||||
"Optional model override for the new agent. Replaces the inherited model only when fork_context is false; forked children always inherit the parent model."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
@@ -722,7 +722,7 @@ fn spawn_agent_common_properties(agent_type_description: &str) -> BTreeMap<Strin
|
||||
"reasoning_effort".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Optional reasoning effort override for the new agent. Replaces the inherited reasoning effort."
|
||||
"Optional reasoning effort override for the new agent. Replaces the inherited reasoning effort only when fork_context is false; forked children always inherit the parent reasoning effort."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
@@ -787,7 +787,7 @@ fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap<St
|
||||
"model".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Optional model override for the new agent. Replaces the inherited model."
|
||||
"Optional model override for the new agent. Replaces the inherited model only when fork_turns is `none`; forked children always inherit the parent model."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
@@ -810,7 +810,7 @@ fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap<St
|
||||
"reasoning_effort".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Optional reasoning effort override for the new agent. Replaces the inherited reasoning effort."
|
||||
"Optional reasoning effort override for the new agent. Replaces the inherited reasoning effort only when fork_turns is `none`; forked children always inherit the parent reasoning effort."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
|
||||
@@ -115,6 +115,60 @@ fn spawn_agent_tool_v1_includes_model_fallback_list() {
|
||||
panic!("model_fallback_list should be an array");
|
||||
};
|
||||
assert!(properties.contains_key("model_fallback_list"));
|
||||
assert!(properties.contains_key("fork_context"));
|
||||
assert!(!properties.contains_key("fork_turns"));
|
||||
assert_eq!(
|
||||
properties.get("model"),
|
||||
Some(&JsonSchema::String {
|
||||
description: Some(
|
||||
"Optional model override for the new agent. Replaces the inherited model only when fork_context is false; forked children always inherit the parent model."
|
||||
.to_string(),
|
||||
),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
properties.get("reasoning_effort"),
|
||||
Some(&JsonSchema::String {
|
||||
description: Some(
|
||||
"Optional reasoning effort override for the new agent. Replaces the inherited reasoning effort only when fork_context is false; forked children always inherit the parent reasoning effort."
|
||||
.to_string(),
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_tool_v2_documents_that_forked_children_ignore_model_overrides() {
|
||||
let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {
|
||||
available_models: &[],
|
||||
agent_type_description: "role help".to_string(),
|
||||
});
|
||||
|
||||
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = tool else {
|
||||
panic!("spawn_agent should be a function tool");
|
||||
};
|
||||
let JsonSchema::Object { properties, .. } = parameters else {
|
||||
panic!("spawn_agent should use object params");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
properties.get("model"),
|
||||
Some(&JsonSchema::String {
|
||||
description: Some(
|
||||
"Optional model override for the new agent. Replaces the inherited model only when fork_turns is `none`; forked children always inherit the parent model."
|
||||
.to_string(),
|
||||
),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
properties.get("reasoning_effort"),
|
||||
Some(&JsonSchema::String {
|
||||
description: Some(
|
||||
"Optional reasoning effort override for the new agent. Replaces the inherited reasoning effort only when fork_turns is `none`; forked children always inherit the parent reasoning effort."
|
||||
.to_string(),
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user