Skip to content

Commit 8f640fc

Browse files
committed
fix: root as std agent
1 parent 6d2f4aa commit 8f640fc

File tree

4 files changed

+148
-15
lines changed

4 files changed

+148
-15
lines changed

codex-rs/core/src/agent/control.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -650,12 +650,6 @@ impl AgentControl {
650650
let agent_path = current_agent_path
651651
.resolve(agent_reference)
652652
.map_err(CodexErr::UnsupportedOperation)?;
653-
if agent_path.is_root() {
654-
return Err(CodexErr::UnsupportedOperation(
655-
"root is not a spawned agent".to_string(),
656-
));
657-
}
658-
659653
if let Some(thread_id) = self.state.agent_id_for_path(&agent_path) {
660654
return Ok(thread_id);
661655
}

codex-rs/core/src/agent/registry.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,7 @@ impl AgentRegistry {
158158
.unwrap_or_else(std::sync::PoisonError::into_inner)
159159
.agent_tree
160160
.values()
161-
.filter(|metadata| {
162-
metadata.agent_id.is_some()
163-
&& !metadata.agent_path.as_ref().is_some_and(AgentPath::is_root)
164-
})
161+
.filter(|metadata| metadata.agent_id.is_some())
165162
.cloned()
166163
.collect()
167164
}

codex-rs/core/src/tools/handlers/multi_agents_tests.rs

Lines changed: 138 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,81 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
430430
}));
431431
}
432432

433+
#[tokio::test]
434+
async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
435+
let (mut session, mut turn) = make_session_and_context().await;
436+
let manager = thread_manager();
437+
let root = manager
438+
.start_thread((*turn.config).clone())
439+
.await
440+
.expect("root thread should start");
441+
session.services.agent_control = manager.agent_control();
442+
session.conversation_id = root.thread_id;
443+
let mut config = (*turn.config).clone();
444+
config
445+
.features
446+
.enable(Feature::MultiAgentV2)
447+
.expect("test config should allow feature update");
448+
turn.config = Arc::new(config);
449+
450+
let child_path = AgentPath::try_from("/root/worker").expect("agent path");
451+
let child_thread_id = session
452+
.services
453+
.agent_control
454+
.spawn_agent_with_metadata(
455+
(*turn.config).clone(),
456+
vec![UserInput::Text {
457+
text: "inspect this repo".to_string(),
458+
text_elements: Vec::new(),
459+
}],
460+
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
461+
parent_thread_id: root.thread_id,
462+
depth: 1,
463+
agent_path: Some(child_path.clone()),
464+
agent_nickname: None,
465+
agent_role: None,
466+
})),
467+
crate::agent::control::SpawnAgentOptions::default(),
468+
)
469+
.await
470+
.expect("worker spawn should succeed")
471+
.thread_id;
472+
session.conversation_id = child_thread_id;
473+
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
474+
parent_thread_id: root.thread_id,
475+
depth: 1,
476+
agent_path: Some(child_path.clone()),
477+
agent_nickname: None,
478+
agent_role: None,
479+
});
480+
481+
SendMessageHandlerV2
482+
.handle(invocation(
483+
Arc::new(session),
484+
Arc::new(turn),
485+
"send_message",
486+
function_payload(json!({
487+
"target": "/root",
488+
"items": [{"type": "text", "text": "done"}]
489+
})),
490+
))
491+
.await
492+
.expect("send_message should accept the root agent path");
493+
494+
assert!(manager.captured_ops().iter().any(|(id, op)| {
495+
*id == root.thread_id
496+
&& matches!(
497+
op,
498+
Op::InterAgentCommunication { communication }
499+
if communication.author == child_path
500+
&& communication.recipient == AgentPath::root()
501+
&& communication.other_recipients.is_empty()
502+
&& communication.content == "done"
503+
&& !communication.trigger_turn
504+
)
505+
}));
506+
}
507+
433508
#[tokio::test]
434509
async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_message() {
435510
let (mut session, mut turn) = make_session_and_context().await;
@@ -496,11 +571,20 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
496571
let result: ListAgentsResult =
497572
serde_json::from_str(&content).expect("list_agents result should be json");
498573

499-
assert_eq!(result.agents.len(), 1);
500-
assert_eq!(result.agents[0].agent_name, "/root/worker");
501-
assert_eq!(result.agents[0].agent_status, json!({"completed": "done"}));
574+
let agent_names = result
575+
.agents
576+
.iter()
577+
.map(|agent| agent.agent_name.as_str())
578+
.collect::<Vec<_>>();
579+
assert_eq!(agent_names, vec!["/root", "/root/worker"]);
580+
let worker = result
581+
.agents
582+
.iter()
583+
.find(|agent| agent.agent_name == "/root/worker")
584+
.expect("worker agent should be listed");
585+
assert_eq!(worker.agent_status, json!({"completed": "done"}));
502586
assert_eq!(
503-
result.agents[0].last_task_message.as_deref(),
587+
worker.last_task_message.as_deref(),
504588
Some("inspect this repo")
505589
);
506590
assert_eq!(success, Some(true));
@@ -647,7 +731,8 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() {
647731
let result: ListAgentsResult =
648732
serde_json::from_str(&content).expect("list_agents result should be json");
649733

650-
assert!(result.agents.is_empty());
734+
assert_eq!(result.agents.len(), 1);
735+
assert_eq!(result.agents[0].agent_name, "/root");
651736
}
652737

653738
#[tokio::test]
@@ -2036,6 +2121,54 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() {
20362121
);
20372122
}
20382123

2124+
#[tokio::test]
2125+
async fn multi_agent_v2_close_agent_rejects_root_target_and_id() {
2126+
let (mut session, mut turn) = make_session_and_context().await;
2127+
let manager = thread_manager();
2128+
let root = manager
2129+
.start_thread((*turn.config).clone())
2130+
.await
2131+
.expect("root thread should start");
2132+
session.services.agent_control = manager.agent_control();
2133+
session.conversation_id = root.thread_id;
2134+
let mut config = (*turn.config).clone();
2135+
config
2136+
.features
2137+
.enable(Feature::MultiAgentV2)
2138+
.expect("test config should allow feature update");
2139+
turn.config = Arc::new(config);
2140+
2141+
let session = Arc::new(session);
2142+
let turn = Arc::new(turn);
2143+
let root_path_error = CloseAgentHandlerV2
2144+
.handle(invocation(
2145+
session.clone(),
2146+
turn.clone(),
2147+
"close_agent",
2148+
function_payload(json!({"target": "/root"})),
2149+
))
2150+
.await
2151+
.expect_err("close_agent should reject the root path");
2152+
assert_eq!(
2153+
root_path_error,
2154+
FunctionCallError::RespondToModel("root is not a spawned agent".to_string())
2155+
);
2156+
2157+
let root_id_error = CloseAgentHandlerV2
2158+
.handle(invocation(
2159+
session,
2160+
turn,
2161+
"close_agent",
2162+
function_payload(json!({"target": root.thread_id.to_string()})),
2163+
))
2164+
.await
2165+
.expect_err("close_agent should reject the root thread id");
2166+
assert_eq!(
2167+
root_id_error,
2168+
FunctionCallError::RespondToModel("root is not a spawned agent".to_string())
2169+
);
2170+
}
2171+
20392172
#[tokio::test]
20402173
async fn close_agent_submits_shutdown_and_returns_previous_status() {
20412174
let (mut session, turn) = make_session_and_context().await;

codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ impl ToolHandler for Handler {
3030
.agent_control
3131
.get_agent_metadata(agent_id)
3232
.unwrap_or_default();
33+
if receiver_agent
34+
.agent_path
35+
.as_ref()
36+
.is_some_and(AgentPath::is_root)
37+
{
38+
return Err(FunctionCallError::RespondToModel(
39+
"root is not a spawned agent".to_string(),
40+
));
41+
}
3342
session
3443
.send_event(
3544
&turn,

0 commit comments

Comments
 (0)