mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
codex: require locked version for child agents
This commit is contained in:
@@ -57,7 +57,6 @@ pub(crate) struct SpawnAgentOptions {
|
||||
pub(crate) fork_mode: Option<SpawnAgentForkMode>,
|
||||
pub(crate) parent_thread_id: Option<ThreadId>,
|
||||
pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
pub(crate) multi_agent_version: Option<MultiAgentVersion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -191,18 +190,30 @@ impl AgentControl {
|
||||
initial_operation: Op,
|
||||
session_source: Option<SessionSource>,
|
||||
) -> CodexResult<ThreadId> {
|
||||
let multi_agent_version = session_source
|
||||
.is_none()
|
||||
.then(|| config.multi_agent_version_from_features())
|
||||
.flatten();
|
||||
let state = self.upgrade()?;
|
||||
let multi_agent_version =
|
||||
if let Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
..
|
||||
})) = session_source.as_ref()
|
||||
{
|
||||
state
|
||||
.get_thread(*parent_thread_id)
|
||||
.await?
|
||||
.multi_agent_version()
|
||||
.await
|
||||
} else {
|
||||
config.multi_agent_version_from_features()
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
CodexErr::InvalidRequest("multi-agent version is unresolved".to_string())
|
||||
})?;
|
||||
let spawned_agent = Box::pin(self.spawn_agent_internal(
|
||||
config,
|
||||
initial_operation,
|
||||
session_source,
|
||||
SpawnAgentOptions {
|
||||
multi_agent_version,
|
||||
..Default::default()
|
||||
},
|
||||
multi_agent_version,
|
||||
SpawnAgentOptions::default(),
|
||||
))
|
||||
.await?;
|
||||
Ok(spawned_agent.thread_id)
|
||||
@@ -214,10 +225,17 @@ impl AgentControl {
|
||||
config: crate::config::Config,
|
||||
initial_operation: Op,
|
||||
session_source: Option<SessionSource>,
|
||||
multi_agent_version: MultiAgentVersion,
|
||||
options: SpawnAgentOptions, // TODO(jif) drop with new fork.
|
||||
) -> CodexResult<LiveAgent> {
|
||||
Box::pin(self.spawn_agent_internal(config, initial_operation, session_source, options))
|
||||
.await
|
||||
Box::pin(self.spawn_agent_internal(
|
||||
config,
|
||||
initial_operation,
|
||||
session_source,
|
||||
multi_agent_version,
|
||||
options,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn spawn_agent_internal(
|
||||
@@ -225,19 +243,10 @@ impl AgentControl {
|
||||
config: crate::config::Config,
|
||||
initial_operation: Op,
|
||||
session_source: Option<SessionSource>,
|
||||
multi_agent_version: MultiAgentVersion,
|
||||
options: SpawnAgentOptions,
|
||||
) -> CodexResult<LiveAgent> {
|
||||
let state = self.upgrade()?;
|
||||
let multi_agent_version = self
|
||||
.inherited_multi_agent_version_for_source(
|
||||
&state,
|
||||
session_source.as_ref(),
|
||||
options.multi_agent_version,
|
||||
)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
CodexErr::InvalidRequest("multi-agent version is unresolved".to_string())
|
||||
})?;
|
||||
let agent_max_threads = config
|
||||
.effective_agent_max_threads(Some(multi_agent_version))
|
||||
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
|
||||
@@ -281,7 +290,7 @@ impl AgentControl {
|
||||
&options,
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
Some(multi_agent_version),
|
||||
multi_agent_version,
|
||||
))
|
||||
.await?
|
||||
}
|
||||
@@ -395,7 +404,7 @@ impl AgentControl {
|
||||
options: &SpawnAgentOptions,
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||
inherited_multi_agent_version: Option<MultiAgentVersion>,
|
||||
multi_agent_version: MultiAgentVersion,
|
||||
) -> CodexResult<crate::thread_manager::NewThread> {
|
||||
if options.fork_parent_spawn_call_id.is_none() {
|
||||
return Err(CodexErr::Fatal(
|
||||
@@ -439,18 +448,6 @@ impl AgentControl {
|
||||
))
|
||||
})?;
|
||||
|
||||
let multi_agent_version = if let Some(parent_thread) = parent_thread.as_ref() {
|
||||
parent_thread.multi_agent_version().await
|
||||
} else {
|
||||
state
|
||||
.resolve_multi_agent_version(
|
||||
&config,
|
||||
&InitialHistory::Forked(parent_history.items.clone()),
|
||||
Some(parent_thread_id),
|
||||
inherited_multi_agent_version,
|
||||
)
|
||||
.await
|
||||
};
|
||||
let mut forked_rollout_items = parent_history.items;
|
||||
if let SpawnAgentForkMode::LastNTurns(last_n_turns) = fork_mode {
|
||||
forked_rollout_items =
|
||||
@@ -458,7 +455,7 @@ impl AgentControl {
|
||||
}
|
||||
let multi_agent_v2_usage_hint_texts_to_filter: Vec<String> =
|
||||
if let Some(parent_thread) = parent_thread.as_ref() {
|
||||
if multi_agent_version == Some(MultiAgentVersion::V2) {
|
||||
if multi_agent_version == MultiAgentVersion::V2 {
|
||||
let parent_config = parent_thread.codex.session.get_config().await;
|
||||
[
|
||||
parent_config
|
||||
@@ -476,7 +473,7 @@ impl AgentControl {
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
} else if multi_agent_version == Some(MultiAgentVersion::V2) {
|
||||
} else if multi_agent_version == MultiAgentVersion::V2 {
|
||||
[
|
||||
config.multi_agent_v2.root_agent_usage_hint_text.clone(),
|
||||
config.multi_agent_v2.subagent_usage_hint_text.clone(),
|
||||
@@ -512,7 +509,7 @@ impl AgentControl {
|
||||
}
|
||||
}
|
||||
if preserve_reference_context_item
|
||||
&& multi_agent_version == Some(MultiAgentVersion::V2)
|
||||
&& multi_agent_version == MultiAgentVersion::V2
|
||||
&& let Some(subagent_usage_hint_text) =
|
||||
config.multi_agent_v2.subagent_usage_hint_text.clone()
|
||||
&& let Some(subagent_usage_hint_message) =
|
||||
@@ -539,7 +536,7 @@ impl AgentControl {
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
options.environments.clone(),
|
||||
multi_agent_version,
|
||||
Some(multi_agent_version),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -628,13 +625,18 @@ impl AgentControl {
|
||||
) -> CodexResult<ThreadId> {
|
||||
let state = self.upgrade()?;
|
||||
let state_db_ctx = state.state_db();
|
||||
let inherited_multi_agent_version = self
|
||||
.inherited_multi_agent_version_for_source(
|
||||
&state,
|
||||
Some(&session_source),
|
||||
/*requested_multi_agent_version*/ None,
|
||||
)
|
||||
.await;
|
||||
let inherited_multi_agent_version =
|
||||
if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id, ..
|
||||
}) = &session_source
|
||||
{
|
||||
match state.get_thread(*parent_thread_id).await {
|
||||
Ok(parent_thread) => parent_thread.multi_agent_version().await,
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let stored_thread = state
|
||||
.read_stored_thread(ReadThreadParams {
|
||||
thread_id,
|
||||
@@ -1204,25 +1206,6 @@ impl AgentControl {
|
||||
parent_thread.codex.session.user_shell().shell_snapshot()
|
||||
}
|
||||
|
||||
async fn inherited_multi_agent_version_for_source(
|
||||
&self,
|
||||
state: &Arc<ThreadManagerState>,
|
||||
session_source: Option<&SessionSource>,
|
||||
requested_multi_agent_version: Option<MultiAgentVersion>,
|
||||
) -> Option<MultiAgentVersion> {
|
||||
if requested_multi_agent_version.is_some() {
|
||||
return requested_multi_agent_version;
|
||||
}
|
||||
if let Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id, ..
|
||||
})) = session_source
|
||||
&& let Ok(parent_thread) = state.get_thread(*parent_thread_id).await
|
||||
{
|
||||
return parent_thread.multi_agent_version().await;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn inherited_exec_policy_for_source(
|
||||
&self,
|
||||
state: &Arc<ThreadManagerState>,
|
||||
|
||||
@@ -719,6 +719,10 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
parent_thread
|
||||
.multi_agent_version()
|
||||
.await
|
||||
.expect("parent thread should have a multi-agent version"),
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
|
||||
fork_mode: Some(SpawnAgentForkMode::FullHistory),
|
||||
@@ -930,6 +934,10 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
parent_thread
|
||||
.multi_agent_version()
|
||||
.await
|
||||
.expect("parent thread should have a multi-agent version"),
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: Some(parent_spawn_call_id),
|
||||
fork_mode: Some(SpawnAgentForkMode::FullHistory),
|
||||
@@ -1000,6 +1008,10 @@ async fn spawn_agent_fork_flushes_parent_rollout_before_loading_history() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
parent_thread
|
||||
.multi_agent_version()
|
||||
.await
|
||||
.expect("parent thread should have a multi-agent version"),
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
|
||||
fork_mode: Some(SpawnAgentForkMode::FullHistory),
|
||||
@@ -1117,6 +1129,10 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
parent_thread
|
||||
.multi_agent_version()
|
||||
.await
|
||||
.expect("parent thread should have a multi-agent version"),
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
|
||||
fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)),
|
||||
@@ -1228,6 +1244,10 @@ async fn spawn_agent_fork_last_n_turns_drops_parent_startup_prefix_when_under_li
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
parent_thread
|
||||
.multi_agent_version()
|
||||
.await
|
||||
.expect("parent thread should have a multi-agent version"),
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: Some(parent_spawn_call_id),
|
||||
fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)),
|
||||
@@ -1338,6 +1358,10 @@ async fn spawn_agent_fork_last_n_turns_strips_parent_usage_hints() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
parent_thread
|
||||
.multi_agent_version()
|
||||
.await
|
||||
.expect("parent thread should have a multi-agent version"),
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: Some(parent_spawn_call_id),
|
||||
fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)),
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::tools::handlers::parse_arguments;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
@@ -89,6 +90,7 @@ struct ReportAgentJobResultToolResult {
|
||||
struct JobRunnerOptions {
|
||||
max_concurrency: usize,
|
||||
spawn_config: Config,
|
||||
multi_agent_version: MultiAgentVersion,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -119,9 +121,14 @@ async fn build_runner_options(
|
||||
"agent depth limit reached; this session cannot spawn more subagents".to_string(),
|
||||
));
|
||||
}
|
||||
let multi_agent_version = turn.multi_agent_version.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"multi-agent version is unresolved; this session cannot spawn workers".to_string(),
|
||||
)
|
||||
})?;
|
||||
let agent_max_threads = turn
|
||||
.config
|
||||
.effective_agent_max_threads(turn.multi_agent_version)
|
||||
.effective_agent_max_threads(Some(multi_agent_version))
|
||||
.map_err(|err| FunctionCallError::Fatal(err.to_string()))?;
|
||||
if agent_max_threads == Some(0) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
@@ -134,6 +141,7 @@ async fn build_runner_options(
|
||||
Ok(JobRunnerOptions {
|
||||
max_concurrency,
|
||||
spawn_config,
|
||||
multi_agent_version,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,10 +221,10 @@ async fn run_agent_job_loop(
|
||||
Some(SessionSource::SubAgent(SubAgentSource::Other(format!(
|
||||
"agent_job:{job_id}"
|
||||
)))),
|
||||
options.multi_agent_version,
|
||||
SpawnAgentOptions {
|
||||
parent_thread_id: Some(session.conversation_id),
|
||||
environments: Some(turn.environments.to_selections()),
|
||||
multi_agent_version: turn.multi_agent_version,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -109,6 +109,9 @@ async fn handle_spawn_agent(
|
||||
)
|
||||
.await?;
|
||||
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
|
||||
let multi_agent_version = turn
|
||||
.multi_agent_version
|
||||
.ok_or_else(|| FunctionCallError::Fatal("multi-agent version is unresolved".to_string()))?;
|
||||
|
||||
let result = Box::pin(session.services.agent_control.spawn_agent_with_metadata(
|
||||
config,
|
||||
@@ -120,12 +123,12 @@ async fn handle_spawn_agent(
|
||||
role_name,
|
||||
/*task_name*/ None,
|
||||
)?),
|
||||
multi_agent_version,
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: args.fork_context.then(|| call_id.clone()),
|
||||
fork_mode: args.fork_context.then_some(SpawnAgentForkMode::FullHistory),
|
||||
parent_thread_id: Some(session.conversation_id),
|
||||
environments: Some(turn.environments.to_selections()),
|
||||
multi_agent_version: turn.multi_agent_version,
|
||||
},
|
||||
))
|
||||
.await
|
||||
|
||||
@@ -1348,18 +1348,23 @@ async fn multi_agent_v2_spawn_rejects_zero_fork_turns() {
|
||||
async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
.start_thread((*turn.config).clone())
|
||||
.await
|
||||
.expect("root thread should start");
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
let mut config = (*turn.config).clone();
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
set_turn_config(&mut turn, config);
|
||||
let root = manager
|
||||
.start_thread((*turn.config).clone())
|
||||
.await
|
||||
.expect("root thread should start");
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
let multi_agent_version = root
|
||||
.thread
|
||||
.multi_agent_version()
|
||||
.await
|
||||
.expect("root thread should have a multi-agent version");
|
||||
|
||||
let child_path = AgentPath::try_from("/root/worker").expect("agent path");
|
||||
let child_thread_id = session
|
||||
@@ -1379,6 +1384,7 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
multi_agent_version,
|
||||
crate::agent::control::SpawnAgentOptions::default(),
|
||||
)
|
||||
.await
|
||||
@@ -1424,18 +1430,23 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
|
||||
async fn multi_agent_v2_followup_task_rejects_root_target_from_child() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
.start_thread((*turn.config).clone())
|
||||
.await
|
||||
.expect("root thread should start");
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
let mut config = (*turn.config).clone();
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
set_turn_config(&mut turn, config);
|
||||
let root = manager
|
||||
.start_thread((*turn.config).clone())
|
||||
.await
|
||||
.expect("root thread should start");
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
let multi_agent_version = root
|
||||
.thread
|
||||
.multi_agent_version()
|
||||
.await
|
||||
.expect("root thread should have a multi-agent version");
|
||||
|
||||
let child_path = AgentPath::try_from("/root/worker").expect("agent path");
|
||||
let child_thread_id = session
|
||||
@@ -1455,6 +1466,7 @@ async fn multi_agent_v2_followup_task_rejects_root_target_from_child() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
multi_agent_version,
|
||||
crate::agent::control::SpawnAgentOptions::default(),
|
||||
)
|
||||
.await
|
||||
@@ -1601,15 +1613,20 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
|
||||
async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let mut config = (*turn.config).clone();
|
||||
let _ = config.features.enable(Feature::MultiAgentV2);
|
||||
set_turn_config(&mut turn, config.clone());
|
||||
let root = manager
|
||||
.start_thread((*turn.config).clone())
|
||||
.await
|
||||
.expect("root thread should start");
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
let mut config = (*turn.config).clone();
|
||||
let _ = config.features.enable(Feature::MultiAgentV2);
|
||||
set_turn_config(&mut turn, config.clone());
|
||||
let multi_agent_version = root
|
||||
.thread
|
||||
.multi_agent_version()
|
||||
.await
|
||||
.expect("root thread should have a multi-agent version");
|
||||
|
||||
let researcher_path = AgentPath::from_string("/root/researcher".to_string()).expect("path");
|
||||
let worker_path = AgentPath::from_string("/root/researcher/worker".to_string()).expect("path");
|
||||
@@ -1630,6 +1647,7 @@ async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
multi_agent_version,
|
||||
crate::agent::control::SpawnAgentOptions::default(),
|
||||
)
|
||||
.await
|
||||
@@ -1651,6 +1669,7 @@ async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
multi_agent_version,
|
||||
crate::agent::control::SpawnAgentOptions::default(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -108,6 +108,9 @@ async fn handle_spawn_agent(
|
||||
)
|
||||
.await?;
|
||||
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
|
||||
let multi_agent_version = turn
|
||||
.multi_agent_version
|
||||
.ok_or_else(|| FunctionCallError::Fatal("multi-agent version is unresolved".to_string()))?;
|
||||
|
||||
let spawn_source = thread_spawn_source(
|
||||
session.conversation_id,
|
||||
@@ -140,12 +143,12 @@ async fn handle_spawn_agent(
|
||||
(_, initial_operation) => initial_operation,
|
||||
},
|
||||
Some(spawn_source),
|
||||
multi_agent_version,
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.clone()),
|
||||
fork_mode,
|
||||
parent_thread_id: Some(session.conversation_id),
|
||||
environments: Some(turn.environments.to_selections()),
|
||||
multi_agent_version: turn.multi_agent_version,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -313,6 +313,7 @@ fn goal_tools_enabled(turn_context: &TurnContext) -> bool {
|
||||
|
||||
fn agent_jobs_tools_enabled(turn_context: &TurnContext) -> bool {
|
||||
turn_context.features.get().enabled(Feature::SpawnCsv)
|
||||
&& turn_context.multi_agent_version.is_some()
|
||||
&& !exceeds_thread_spawn_depth_limit(
|
||||
next_thread_spawn_depth(&turn_context.session_source),
|
||||
turn_context.config.agent_max_depth,
|
||||
|
||||
@@ -535,7 +535,7 @@ async fn host_context_gates_goal_and_agent_job_tools() {
|
||||
turn.multi_agent_version = None;
|
||||
})
|
||||
.await;
|
||||
agent_job_without_multi_agent.assert_visible_contains(&["spawn_agents_on_csv"]);
|
||||
agent_job_without_multi_agent.assert_visible_lacks(&["spawn_agents_on_csv"]);
|
||||
agent_job_without_multi_agent.assert_visible_lacks(&[MULTI_AGENT_V1_NAMESPACE, "spawn_agent"]);
|
||||
|
||||
let worker_agent_job = probe(|turn| {
|
||||
|
||||
Reference in New Issue
Block a user