Route multi-agent spawning through SpawnRequest (#47235)

## What changed

- Route both multi-agent spawn handlers through `LocalAgentControl::spawn`, accepting user input or an agent message.
- Centralize spawn message attribution and canonical path validation, and require message input to use `TriggerTurn`.
- Return the child's configuration snapshot alongside `LiveAgent`, so handlers obtain effective settings directly from spawning without a separate lookup.
- Update child test fixtures to use the shared spawn entry point.

GitOrigin-RevId: 9f9278df08041a62215f9ec3d38688c78fc5e0e2
This commit is contained in:
jif
2026-09-22 09:32:22 +00:00
committed by copyberry
parent 94174e44cb
commit f908e5a968
8 changed files with 178 additions and 177 deletions
+1 -11
View File
@@ -10,7 +10,6 @@ use crate::agent::types::AgentMetadata;
use crate::agent::types::LiveAgent;
use crate::agent_communication::AgentCommunicationContext;
use crate::agent_communication::AgentCommunicationKind;
use crate::codex_thread::ThreadConfigSnapshot;
use crate::config::Config;
use crate::config::RolloutBudgetConfig;
use crate::context::SubagentNotification;
@@ -80,6 +79,7 @@ mod residency;
mod sender_context;
mod service_tier;
mod spawn;
mod spawn_request;
mod target;
mod user_authorization;
@@ -425,16 +425,6 @@ impl LocalAgentControl {
Ok(thread_ids)
}
pub(crate) async fn get_agent_config_snapshot(
&self,
agent_id: ThreadId,
) -> Option<ThreadConfigSnapshot> {
match self.inspect_agent(agent_id).await.ok()? {
crate::agent::api::AgentInfo::Loaded { config, .. } => Some(*config),
crate::agent::api::AgentInfo::Unloaded(_) => None,
}
}
/// Subscribe to status updates for `agent_id`, yielding the latest value and changes.
pub(crate) async fn subscribe_status(
&self,
+9 -40
View File
@@ -8,6 +8,7 @@ use crate::agent::types::SpawnAgentForkMode;
use crate::agent::types::SpawnAgentOptions;
use crate::agents_md_manager::SessionInstructions;
use crate::codex_thread::CodexThread;
use crate::codex_thread::ThreadConfigSnapshot;
use crate::config::PermissionProfileSnapshot;
use crate::context::ContextualUserFragment;
use crate::context::CurrentTimeReminder;
@@ -42,7 +43,7 @@ struct SpawnAgentThreadInheritance {
/// provide user input directly, making an uncontextualized inter-agent communication
/// unrepresentable.
#[allow(clippy::large_enum_variant)]
enum SpawnInitialInput {
pub(super) enum SpawnInitialInput {
UserInput(Vec<UserInput>),
InterAgentCommunication(InterAgentCommunication, AgentCommunicationContext),
}
@@ -257,7 +258,7 @@ impl LocalAgentControl {
initial_input: Vec<UserInput>,
session_source: Option<SessionSource>,
) -> CodexResult<ThreadId> {
let spawned_agent = Box::pin(self.spawn_agent_internal(
let (spawned_agent, _) = Box::pin(self.spawn_agent_internal(
config,
SpawnInitialInput::UserInput(initial_input),
session_source,
@@ -267,40 +268,6 @@ impl LocalAgentControl {
Ok(spawned_agent.thread_id)
}
/// Spawn an agent thread with some metadata.
pub(crate) async fn spawn_agent_with_metadata(
&self,
config: Config,
initial_input: Vec<UserInput>,
session_source: Option<SessionSource>,
options: SpawnAgentOptions, // TODO(jif) drop with new fork.
) -> CodexResult<LiveAgent> {
Box::pin(self.spawn_agent_internal(
config,
SpawnInitialInput::UserInput(initial_input),
session_source,
options,
))
.await
}
pub(crate) async fn spawn_agent_with_communication(
&self,
config: Config,
communication: InterAgentCommunication,
context: AgentCommunicationContext,
session_source: Option<SessionSource>,
options: SpawnAgentOptions,
) -> CodexResult<LiveAgent> {
Box::pin(self.spawn_agent_internal(
config,
SpawnInitialInput::InterAgentCommunication(communication, context),
session_source,
options,
))
.await
}
fn validate_loaded_v2_child(
&self,
thread: &CodexThread,
@@ -629,13 +596,13 @@ impl LocalAgentControl {
}
}
async fn spawn_agent_internal(
pub(super) async fn spawn_agent_internal(
&self,
config: Config,
initial_input: SpawnInitialInput,
session_source: Option<SessionSource>,
options: SpawnAgentOptions,
) -> CodexResult<LiveAgent> {
) -> CodexResult<(LiveAgent, ThreadConfigSnapshot)> {
let state = self.upgrade()?;
let multi_agent_version = state
.effective_multi_agent_version_for_spawn(
@@ -829,11 +796,13 @@ impl LocalAgentControl {
);
}
Ok(LiveAgent {
let agent = LiveAgent {
thread_id: new_thread.thread_id,
metadata: agent_metadata,
status: self.get_status(new_thread.thread_id).await,
})
};
let config = new_thread.thread.config_snapshot().await;
Ok((agent, config))
}
async fn spawn_forked_thread(
@@ -0,0 +1,57 @@
//! Converts captured spawn input into the existing local startup operation.
//! Agent messages keep their canonical paths and spawn attribution.
use super::LocalAgentControl;
use super::spawn::SpawnInitialInput;
use crate::agent::api::AgentInput;
use crate::agent::api::SpawnRequest;
use crate::agent::types::LiveAgent;
use crate::agent::types::MessageDeliveryMode;
use crate::agent_communication::AgentCommunicationContext;
use crate::agent_communication::AgentCommunicationKind;
use crate::codex_thread::ThreadConfigSnapshot;
use codex_protocol::AgentPath;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
impl LocalAgentControl {
pub(crate) async fn spawn(
&self,
request: SpawnRequest,
) -> CodexResult<(LiveAgent, ThreadConfigSnapshot)> {
let SpawnRequest {
caller,
config,
input,
source,
options,
} = request;
let input = match input {
AgentInput::UserInput(input) => SpawnInitialInput::UserInput(input),
AgentInput::Message { message, mode } => {
if mode != MessageDeliveryMode::TriggerTurn {
return Err(CodexErr::InvalidRequest(
"spawn input must start the child turn".to_string(),
));
}
let recipient = source.get_agent_path().ok_or_else(|| {
CodexErr::InvalidRequest(
"spawned agent is missing a canonical task name".to_string(),
)
})?;
let author = recipient
.as_str()
.rsplit_once('/')
.and_then(|(parent, _)| AgentPath::try_from(parent).ok())
.ok_or_else(|| {
CodexErr::InvalidRequest("spawn input needs a child path".to_string())
})?;
SpawnInitialInput::InterAgentCommunication(
message.into_communication(author, recipient, mode),
AgentCommunicationContext::new(AgentCommunicationKind::Spawn, caller),
)
}
};
Box::pin(self.spawn_agent_internal(config, input, Some(source), options)).await
}
}
+49 -23
View File
@@ -4,7 +4,9 @@ use crate::StateDbHandle;
use crate::ThreadManager;
use crate::agent::agent_status_from_event;
use crate::agent::api::AgentInfo;
use crate::agent::api::AgentInput;
use crate::agent::api::AgentTarget;
use crate::agent::api::SpawnRequest;
use crate::agent::next_thread_spawn_depth;
use crate::agent::types::AgentMessage;
use crate::agent::types::LiveAgent;
@@ -101,6 +103,30 @@ use tokio::time::timeout;
use tokio_util::sync::CancellationToken;
use toml::Value as TomlValue;
impl LocalAgentControl {
/// Create a child fixture through the production spawn entry point.
pub(crate) async fn spawn_agent_with_metadata(
&self,
config: Config,
initial_input: Vec<UserInput>,
source: SessionSource,
options: SpawnAgentOptions,
) -> CodexResult<LiveAgent> {
let caller = source
.parent_thread_id()
.expect("child fixture has a parent");
self.spawn(SpawnRequest {
caller,
config,
input: AgentInput::UserInput(initial_input),
source,
options,
})
.await
.map(|(agent, _)| agent)
}
}
async fn test_config_with_cli_overrides(
mut cli_overrides: Vec<(String, TomlValue)>,
) -> (TempDir, Config) {
@@ -261,13 +287,13 @@ impl AgentControlHarness {
.spawn_agent_with_metadata(
self.config.clone(),
text_input("child task"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
}),
options,
)
.await
@@ -826,7 +852,7 @@ async fn spawn_v2_reload_test_child(
.spawn_agent_with_metadata(
config,
text_input("hello child"),
Some(source),
source,
SpawnAgentOptions {
parent_thread_id: Some(parent.session.thread_id),
..Default::default()
@@ -2243,13 +2269,13 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
.spawn_agent_with_metadata(
child_config,
text_input("child task"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
}),
SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
fork_mode: Some(SpawnAgentForkMode::FullHistory),
@@ -2368,13 +2394,13 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
.spawn_agent_with_metadata(
no_hint_child_config,
text_input("child task without hints"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
}),
SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
fork_mode: Some(SpawnAgentForkMode::FullHistory),
@@ -2605,13 +2631,13 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history(
.spawn_agent_with_metadata(
child_config,
text_input("child task"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
}),
SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id),
fork_mode: Some(SpawnAgentForkMode::FullHistory),
@@ -2808,13 +2834,13 @@ async fn spawn_agent_full_fork_restores_instructions_after_compaction_discards_p
.spawn_agent_with_metadata(
child_config,
text_input("child task"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
}),
SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id),
fork_mode: Some(SpawnAgentForkMode::FullHistory),
@@ -2992,13 +3018,13 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
.spawn_agent_with_metadata(
child_config,
text_input("child task"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
}),
SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id.to_string()),
fork_mode: Some(SpawnAgentForkMode::FullHistory),
@@ -3178,13 +3204,13 @@ async fn spawn_agent_fork_flushes_parent_rollout_before_loading_history() {
.spawn_agent_with_metadata(
harness.config.clone(),
text_input("child task"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
}),
SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
fork_mode: Some(SpawnAgentForkMode::FullHistory),
@@ -3318,13 +3344,13 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() {
.spawn_agent_with_metadata(
harness.config.clone(),
text_input("child task"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
}),
SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)),
@@ -3458,13 +3484,13 @@ async fn spawn_agent_fork_last_n_turns_drops_parent_startup_prefix_when_under_li
.spawn_agent_with_metadata(
harness.config.clone(),
text_input("child task"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
}),
SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id),
fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)),
@@ -3594,13 +3620,13 @@ async fn spawn_agent_fork_last_n_turns_strips_parent_usage_hints() {
.spawn_agent_with_metadata(
child_config,
text_input("child task"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
}),
SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id),
fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)),
@@ -4240,13 +4266,13 @@ async fn spawn_thread_subagents_persist_parent_originator_across_new_and_truncat
.spawn_agent_with_metadata(
harness.config.clone(),
text_input("hello forked child"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: parent.thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: Some("explorer".to_string()),
})),
}),
SpawnAgentOptions {
fork_parent_spawn_call_id: Some("spawn-call-last-n".to_string()),
fork_mode: Some(SpawnAgentForkMode::LastNTurns(1)),
+2 -2
View File
@@ -385,13 +385,13 @@ async fn thread_id_generator_applies_to_roots_children_and_forks() {
text: "child task".to_string(),
text_elements: Vec::new(),
}],
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: root.thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
}),
SpawnAgentOptions {
parent_thread_id: Some(root.thread_id),
..Default::default()
@@ -1,4 +1,6 @@
use super::*;
use crate::agent::api::AgentInput;
use crate::agent::api::SpawnRequest;
use crate::agent::child_config::SpawnConfigOptions;
use crate::agent::child_config::SpawnConfigVersion;
use crate::agent::child_config::prepare_agent_spawn_config;
@@ -106,17 +108,18 @@ async fn handle_spawn_agent(
.await
.map_err(FunctionCallError::RespondToModel)?;
let config = prepared.config;
let result = Box::pin(session.services.agent_control.spawn_agent_with_metadata(
let result = Box::pin(session.services.agent_control.spawn(SpawnRequest {
caller: session.thread_id,
config,
input_items,
Some(thread_spawn_source(
input: AgentInput::UserInput(input_items),
source: thread_spawn_source(
session.thread_id,
&turn.session_source,
child_depth,
prepared.role_name.as_deref(),
/*task_name*/ None,
)?),
SpawnAgentOptions {
)?,
options: 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.thread_id),
@@ -127,47 +130,22 @@ async fn handle_spawn_agent(
multi_agent_v2_usage_hints: None,
cyber_access_program: turn.cyber_access_program,
},
))
}))
.await
.map_err(collab_spawn_error);
let (new_thread_id, new_agent_metadata, status) = match &result {
Ok(spawned_agent) => (
Some(spawned_agent.thread_id),
Some(spawned_agent.metadata.clone()),
spawned_agent.status.clone(),
),
Err(_) => (None, None, AgentStatus::NotFound),
let (new_thread_id, status) = match &result {
Ok((spawned_agent, _)) => (Some(spawned_agent.thread_id), spawned_agent.status.clone()),
Err(_) => (None, AgentStatus::NotFound),
};
let agent_snapshot = match new_thread_id {
Some(thread_id) => {
session
.services
.agent_control
.get_agent_config_snapshot(thread_id)
.await
}
None => None,
};
let (_new_agent_path, new_agent_nickname, new_agent_role) =
match (&agent_snapshot, new_agent_metadata) {
(Some(snapshot), _) => (
snapshot.session_source.get_agent_path().map(String::from),
snapshot.session_source.get_nickname(),
snapshot.session_source.get_agent_role(),
),
(None, Some(metadata)) => (
metadata.agent_path.map(String::from),
metadata.agent_nickname,
metadata.agent_role,
),
(None, None) => (None, None, None),
};
let agent_snapshot = result.as_ref().ok().map(|(_, config)| config);
let new_agent_nickname =
agent_snapshot.and_then(|snapshot| snapshot.session_source.get_nickname());
let new_agent_role =
agent_snapshot.and_then(|snapshot| snapshot.session_source.get_agent_role());
let effective_model = agent_snapshot
.as_ref()
.map(|snapshot| snapshot.model.clone())
.unwrap_or_else(|| args.model.clone().unwrap_or_default());
let effective_reasoning_effort = agent_snapshot
.as_ref()
.and_then(|snapshot| snapshot.reasoning_effort.clone())
.unwrap_or(args.reasoning_effort.unwrap_or_default());
let nickname = new_agent_nickname.clone();
@@ -200,7 +178,8 @@ async fn handle_spawn_agent(
}),
)
.await;
let new_thread_id = result?.thread_id;
let (spawned_agent, _) = result?;
let new_thread_id = spawned_agent.thread_id;
let role_tag = role_name.unwrap_or(DEFAULT_ROLE_NAME);
turn.session_telemetry.counter(
"codex.multi_agent.spawn",
@@ -1225,13 +1225,13 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
text: "inspect this repo".to_string(),
text_elements: Vec::new(),
}],
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: root.thread_id,
depth: 1,
agent_path: Some(child_path.clone()),
agent_nickname: None,
agent_role: None,
})),
}),
crate::agent::types::SpawnAgentOptions::default(),
)
.await
@@ -1301,13 +1301,13 @@ async fn multi_agent_v2_followup_task_rejects_root_target_from_child() {
text: "inspect this repo".to_string(),
text_elements: Vec::new(),
}],
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: root.thread_id,
depth: 1,
agent_path: Some(child_path.clone()),
agent_nickname: None,
agent_role: None,
})),
}),
crate::agent::types::SpawnAgentOptions::default(),
)
.await
@@ -1466,13 +1466,13 @@ async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() {
text: "research".to_string(),
text_elements: Vec::new(),
}],
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: root.thread_id,
depth: 1,
agent_path: Some(researcher_path.clone()),
agent_nickname: None,
agent_role: None,
})),
}),
crate::agent::types::SpawnAgentOptions::default(),
)
.await
@@ -1486,13 +1486,13 @@ async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() {
text: "build".to_string(),
text_elements: Vec::new(),
}],
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: root.thread_id,
depth: 2,
agent_path: Some(worker_path.clone()),
agent_nickname: None,
agent_role: None,
})),
}),
crate::agent::types::SpawnAgentOptions::default(),
)
.await
@@ -4016,13 +4016,13 @@ async fn multi_agent_v2_interrupt_agent_rejects_self_target_by_id() {
text: "inspect this repo".to_string(),
text_elements: Vec::new(),
}],
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: root.thread_id,
depth: 1,
agent_path: Some(child_path.clone()),
agent_nickname: None,
agent_role: None,
})),
}),
crate::agent::types::SpawnAgentOptions::default(),
)
.await
@@ -4083,13 +4083,13 @@ async fn multi_agent_v2_interrupt_agent_rejects_self_target_by_task_name() {
text: "inspect this repo".to_string(),
text_elements: Vec::new(),
}],
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: root.thread_id,
depth: 1,
agent_path: Some(child_path.clone()),
agent_nickname: None,
agent_role: None,
})),
}),
crate::agent::types::SpawnAgentOptions::default(),
)
.await
@@ -1,4 +1,6 @@
use super::*;
use crate::agent::api::AgentInput;
use crate::agent::api::SpawnRequest;
use crate::agent::child_config::SpawnConfigOptions;
use crate::agent::child_config::SpawnConfigVersion;
use crate::agent::child_config::prepare_agent_spawn_config;
@@ -7,8 +9,6 @@ use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::types::MessageDeliveryMode;
use crate::agent::types::SpawnAgentForkMode;
use crate::agent::types::SpawnAgentOptions;
use crate::agent_communication::AgentCommunicationContext;
use crate::agent_communication::AgentCommunicationKind;
use crate::codex_thread::ThreadConfigSnapshot;
use crate::session::multi_agents::resolve_usage_hints;
use crate::tools::handlers::multi_agents::collab_tool_call_status;
@@ -17,7 +17,6 @@ use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
use crate::tools::handlers::multi_agents_v2::message_tool::message_content;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_prompts::ResolvedModelMessages;
use codex_protocol::AgentPath;
use codex_protocol::ThreadId;
use codex_protocol::protocol::MultiAgentVersion;
use codex_tools::ToolSpec;
@@ -73,10 +72,7 @@ impl ToolExecutor<ToolInvocation> for Handler {
Default::default(),
),
};
let agent_snapshot = result
.as_ref()
.ok()
.and_then(|(_, _, _, snapshot)| snapshot.as_ref());
let agent_snapshot = result.as_ref().ok().map(|(_, _, _, snapshot)| snapshot);
analytics.track_collab_tool_call(
turn_id,
@@ -109,7 +105,7 @@ async fn handle_spawn_agent(
SpawnAgentResult,
ThreadId,
AgentStatus,
Option<ThreadConfigSnapshot>,
ThreadConfigSnapshot,
),
FunctionCallError,
> {
@@ -161,16 +157,6 @@ async fn handle_spawn_agent(
"spawned agent is missing a canonical task name".to_string(),
)
})?;
let author = turn
.session_source
.get_agent_path()
.unwrap_or_else(AgentPath::root);
let communication = agent_message_from_tool(message, &source).into_communication(
author,
new_agent_path.clone(),
MessageDeliveryMode::TriggerTurn,
);
let context = AgentCommunicationContext::new(AgentCommunicationKind::Spawn, session.thread_id);
let multi_agent_v2_usage_hints =
if is_full_history_fork && turn.multi_agent_version == MultiAgentVersion::V2 {
let child_model_info = match config.model.as_deref() {
@@ -195,40 +181,34 @@ async fn handle_spawn_agent(
} else {
None
};
let spawned_agent = Box::pin(
session
.services
.agent_control
.spawn_agent_with_communication(
config,
communication,
context,
Some(spawn_source),
SpawnAgentOptions {
fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.clone()),
fork_mode,
parent_thread_id: Some(session.thread_id),
parent_turn_id: Some(turn.sub_id.clone()),
root_turn_id: turn.turn_metadata_state.root_turn_id(),
turn_trigger: turn.turn_metadata_state.current_turn_trigger(),
environments: Some(step_context.environments.to_selections()),
multi_agent_v2_usage_hints,
cyber_access_program: turn.cyber_access_program,
},
),
)
.await
.map_err(collab_spawn_error)?;
let (spawned_agent, agent_snapshot) =
Box::pin(session.services.agent_control.spawn(SpawnRequest {
caller: session.thread_id,
config,
input: AgentInput::Message {
message: agent_message_from_tool(message, &source),
mode: MessageDeliveryMode::TriggerTurn,
},
source: spawn_source,
options: SpawnAgentOptions {
fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.clone()),
fork_mode,
parent_thread_id: Some(session.thread_id),
parent_turn_id: Some(turn.sub_id.clone()),
root_turn_id: turn.turn_metadata_state.root_turn_id(),
turn_trigger: turn.turn_metadata_state.current_turn_trigger(),
environments: Some(step_context.environments.to_selections()),
multi_agent_v2_usage_hints,
cyber_access_program: turn.cyber_access_program,
},
}))
.await
.map_err(collab_spawn_error)?;
let new_thread_id = spawned_agent.thread_id;
let agent_status = spawned_agent.status;
let agent_snapshot = session
.services
.agent_control
.get_agent_config_snapshot(new_thread_id)
.await;
let nickname = agent_snapshot
.as_ref()
.and_then(|snapshot| snapshot.session_source.get_nickname())
.session_source
.get_nickname()
.or(spawned_agent.metadata.agent_nickname);
emit_sub_agent_activity(
&session,