Compare commits

...

1 Commits

Author SHA1 Message Date
jif-oai
91490ced9e feat: spawn agent message as inter agent 2026-03-25 16:16:46 +00:00
3 changed files with 90 additions and 6 deletions

View File

@@ -269,7 +269,9 @@ impl AgentControl {
)
.await;
self.send_input(new_thread.thread_id, items).await?;
if !items.is_empty() {
self.send_input(new_thread.thread_id, items).await?;
}
let child_reference = agent_metadata
.agent_path
.as_ref()

View File

@@ -400,6 +400,24 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
child_snapshot.session_source.get_agent_path().as_deref(),
Some("/root/test_process")
);
assert!(manager.captured_ops().iter().any(|(id, op)| {
*id == child_thread_id
&& matches!(
op,
Op::InterAgentCommunication { communication }
if communication.author == AgentPath::root()
&& communication.recipient.as_str() == "/root/test_process"
&& communication.other_recipients.is_empty()
&& communication.content == "inspect this repo"
&& communication.trigger_turn
)
}));
assert!(
!manager
.captured_ops()
.iter()
.any(|(id, op)| { *id == child_thread_id && matches!(op, Op::UserInput { .. }) })
);
SendMessageHandlerV2
.handle(invocation(
@@ -428,6 +446,42 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
}));
}
#[tokio::test]
async fn multi_agent_v2_spawn_requires_task_name() {
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");
turn.config = Arc::new(config);
let invocation = invocation(
Arc::new(session),
Arc::new(turn),
"spawn_agent",
function_payload(json!({
"message": "inspect this repo"
})),
);
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
panic!("unnamed v2 spawn should be rejected");
};
assert_eq!(
err,
FunctionCallError::RespondToModel(
"spawn_agent in MultiAgentV2 requires task_name".to_string()
)
);
}
#[tokio::test]
async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_message() {
let (mut session, mut turn) = make_session_and_context().await;

View File

@@ -3,6 +3,7 @@ use crate::agent::control::SpawnAgentOptions;
use crate::agent::next_thread_spawn_depth;
use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::role::apply_role_to_config;
use codex_protocol::protocol::InterAgentCommunication;
pub(crate) struct Handler;
@@ -35,6 +36,11 @@ impl ToolHandler for Handler {
.filter(|role| !role.is_empty());
let input_items = parse_collab_input(args.message, args.items)?;
let prompt = input_preview(&input_items);
if args.task_name.is_none() {
return Err(FunctionCallError::RespondToModel(
"spawn_agent in MultiAgentV2 requires task_name".to_string(),
));
}
let session_source = turn.session_source.clone();
let child_depth = next_thread_spawn_depth(&session_source);
let max_depth = turn.config.agent_max_depth;
@@ -77,7 +83,7 @@ impl ToolHandler for Handler {
.agent_control
.spawn_agent_with_metadata(
config,
input_items,
Vec::new(),
Some(thread_spawn_source(
session.conversation_id,
&turn.session_source,
@@ -91,7 +97,7 @@ impl ToolHandler for Handler {
)
.await
.map_err(collab_spawn_error);
let (new_thread_id, new_agent_metadata, status) = match &result {
let (new_thread_id, new_agent_metadata, mut status) = match &result {
Ok(spawned_agent) => (
Some(spawned_agent.thread_id),
Some(spawned_agent.metadata.clone()),
@@ -112,17 +118,39 @@ impl ToolHandler for Handler {
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_agent_path(),
snapshot.session_source.get_nickname(),
snapshot.session_source.get_agent_role(),
),
(None, Some(metadata)) => (
metadata.agent_path.map(String::from),
metadata.agent_path,
metadata.agent_nickname,
metadata.agent_role,
),
(None, None) => (None, None, None),
};
if let (Ok(spawned_agent), Some(agent_path)) = (&result, new_agent_path.clone()) {
let communication = InterAgentCommunication::new(
turn.session_source
.get_agent_path()
.unwrap_or_else(AgentPath::root),
agent_path,
Vec::new(),
prompt.clone(),
/*trigger_turn*/ true,
);
session
.services
.agent_control
.send_inter_agent_communication(spawned_agent.thread_id, communication)
.await
.map_err(|err| collab_agent_error(spawned_agent.thread_id, err))?;
status = session
.services
.agent_control
.get_status(spawned_agent.thread_id)
.await;
}
let effective_model = agent_snapshot
.as_ref()
.map(|snapshot| snapshot.model.clone())
@@ -132,7 +160,7 @@ impl ToolHandler for Handler {
.and_then(|snapshot| snapshot.reasoning_effort)
.unwrap_or(args.reasoning_effort.unwrap_or_default());
let nickname = new_agent_nickname.clone();
let task_name = new_agent_path.clone();
let task_name = new_agent_path.as_ref().map(ToString::to_string);
session
.send_event(
&turn,