feat: tasks can't be assigned to root agent (#16424)

This commit is contained in:
jif-oai
2026-04-01 12:18:50 +02:00
committed by GitHub
Unverified
parent 3152d1a557
commit 0c776c433b
3 changed files with 92 additions and 2 deletions
@@ -709,6 +709,86 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
}));
}
#[tokio::test]
async fn multi_agent_v2_assign_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");
turn.config = Arc::new(config);
let child_path = AgentPath::try_from("/root/worker").expect("agent path");
let child_thread_id = session
.services
.agent_control
.spawn_agent_with_metadata(
(*turn.config).clone(),
vec![UserInput::Text {
text: "inspect this repo".to_string(),
text_elements: Vec::new(),
}]
.into(),
Some(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::control::SpawnAgentOptions::default(),
)
.await
.expect("worker spawn should succeed")
.thread_id;
session.conversation_id = child_thread_id;
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: root.thread_id,
depth: 1,
agent_path: Some(child_path),
agent_nickname: None,
agent_role: None,
});
let err = AssignTaskHandlerV2
.handle(invocation(
Arc::new(session),
Arc::new(turn),
"assign_task",
function_payload(json!({
"target": "/root",
"message": "run this",
"interrupt": true
})),
))
.await
.expect_err("assign_task should reject the root target");
assert_eq!(
err,
FunctionCallError::RespondToModel("Tasks can't be assigned to the root agent".to_string())
);
let root_ops = manager
.captured_ops()
.into_iter()
.filter_map(|(id, op)| (id == root.thread_id).then_some(op))
.collect::<Vec<_>>();
assert!(!root_ops.iter().any(|op| matches!(op, Op::Interrupt)));
assert!(
!root_ops
.iter()
.any(|op| matches!(op, Op::InterAgentCommunication { .. }))
);
}
#[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;
@@ -6,7 +6,7 @@
use super::*;
use codex_protocol::protocol::InterAgentCommunication;
#[derive(Clone, Copy)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum MessageDeliveryMode {
QueueOnly,
TriggerTurn,
@@ -118,6 +118,16 @@ async fn handle_message_submission(
.agent_control
.get_agent_metadata(receiver_thread_id)
.unwrap_or_default();
if mode == MessageDeliveryMode::TriggerTurn
&& receiver_agent
.agent_path
.as_ref()
.is_some_and(AgentPath::is_root)
{
return Err(FunctionCallError::RespondToModel(
"Tasks can't be assigned to the root agent".to_string(),
));
}
if interrupt {
session
.services
+1 -1
View File
@@ -179,7 +179,7 @@ pub fn create_assign_task_tool() -> ToolSpec {
ToolSpec::Function(ResponsesApiTool {
name: "assign_task".to_string(),
description: "Add a message to an existing agent and trigger a turn in the target. Use interrupt=true to redirect work immediately. In MultiAgentV2, this tool currently supports text content only."
description: "Add a message to an existing non-root agent and trigger a turn in the target. Use interrupt=true to redirect work immediately. In MultiAgentV2, this tool currently supports text content only."
.to_string(),
strict: false,
defer_loading: None,