mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix: root as std agent (#15881)
This commit is contained in:
committed by
GitHub
Unverified
parent
0bd34c28c7
commit
970386e8b2
@@ -44,6 +44,7 @@ use tracing::warn;
|
||||
|
||||
const AGENT_NAMES: &str = include_str!("agent_names.txt");
|
||||
const FORKED_SPAWN_AGENT_OUTPUT_MESSAGE: &str = "You are the newly spawned agent. The prior conversation history was forked from your parent agent. Treat the next user message as your new task, and use the forked history only as background context.";
|
||||
const ROOT_LAST_TASK_MESSAGE: &str = "Main thread";
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct SpawnAgentOptions {
|
||||
@@ -650,12 +651,6 @@ impl AgentControl {
|
||||
let agent_path = current_agent_path
|
||||
.resolve(agent_reference)
|
||||
.map_err(CodexErr::UnsupportedOperation)?;
|
||||
if agent_path.is_root() {
|
||||
return Err(CodexErr::UnsupportedOperation(
|
||||
"root is not a spawned agent".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(thread_id) = self.state.agent_id_for_path(&agent_path) {
|
||||
return Ok(thread_id);
|
||||
}
|
||||
@@ -737,7 +732,21 @@ impl AgentControl {
|
||||
})
|
||||
});
|
||||
|
||||
let mut agents = Vec::with_capacity(live_agents.len());
|
||||
let root_path = AgentPath::root();
|
||||
let mut agents = Vec::with_capacity(live_agents.len().saturating_add(1));
|
||||
if resolved_prefix
|
||||
.as_ref()
|
||||
.is_none_or(|prefix| agent_matches_prefix(Some(&root_path), prefix))
|
||||
&& let Some(root_thread_id) = self.state.agent_id_for_path(&root_path)
|
||||
&& let Ok(root_thread) = state.get_thread(root_thread_id).await
|
||||
{
|
||||
agents.push(ListedAgent {
|
||||
agent_name: root_path.to_string(),
|
||||
agent_status: root_thread.agent_status().await,
|
||||
last_task_message: Some(ROOT_LAST_TASK_MESSAGE.to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
for metadata in live_agents {
|
||||
let Some(thread_id) = metadata.agent_id else {
|
||||
continue;
|
||||
|
||||
@@ -430,6 +430,81 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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");
|
||||
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(),
|
||||
}],
|
||||
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.clone()),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
|
||||
SendMessageHandlerV2
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"send_message",
|
||||
function_payload(json!({
|
||||
"target": "/root",
|
||||
"items": [{"type": "text", "text": "done"}]
|
||||
})),
|
||||
))
|
||||
.await
|
||||
.expect("send_message should accept the root agent path");
|
||||
|
||||
assert!(manager.captured_ops().iter().any(|(id, op)| {
|
||||
*id == root.thread_id
|
||||
&& matches!(
|
||||
op,
|
||||
Op::InterAgentCommunication { communication }
|
||||
if communication.author == child_path
|
||||
&& communication.recipient == AgentPath::root()
|
||||
&& communication.other_recipients.is_empty()
|
||||
&& communication.content == "done"
|
||||
&& !communication.trigger_turn
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
#[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;
|
||||
@@ -496,11 +571,26 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
|
||||
let result: ListAgentsResult =
|
||||
serde_json::from_str(&content).expect("list_agents result should be json");
|
||||
|
||||
assert_eq!(result.agents.len(), 1);
|
||||
assert_eq!(result.agents[0].agent_name, "/root/worker");
|
||||
assert_eq!(result.agents[0].agent_status, json!({"completed": "done"}));
|
||||
let agent_names = result
|
||||
.agents
|
||||
.iter()
|
||||
.map(|agent| agent.agent_name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(agent_names, vec!["/root", "/root/worker"]);
|
||||
let root_agent = result
|
||||
.agents
|
||||
.iter()
|
||||
.find(|agent| agent.agent_name == "/root")
|
||||
.expect("root agent should be listed");
|
||||
assert_eq!(root_agent.last_task_message.as_deref(), Some("Main thread"));
|
||||
let worker = result
|
||||
.agents
|
||||
.iter()
|
||||
.find(|agent| agent.agent_name == "/root/worker")
|
||||
.expect("worker agent should be listed");
|
||||
assert_eq!(worker.agent_status, json!({"completed": "done"}));
|
||||
assert_eq!(
|
||||
result.agents[0].last_task_message.as_deref(),
|
||||
worker.last_task_message.as_deref(),
|
||||
Some("inspect this repo")
|
||||
);
|
||||
assert_eq!(success, Some(true));
|
||||
@@ -647,7 +737,12 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() {
|
||||
let result: ListAgentsResult =
|
||||
serde_json::from_str(&content).expect("list_agents result should be json");
|
||||
|
||||
assert!(result.agents.is_empty());
|
||||
assert_eq!(result.agents.len(), 1);
|
||||
assert_eq!(result.agents[0].agent_name, "/root");
|
||||
assert_eq!(
|
||||
result.agents[0].last_task_message.as_deref(),
|
||||
Some("Main thread")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2036,6 +2131,54 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_close_agent_rejects_root_target_and_id() {
|
||||
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 session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
let root_path_error = CloseAgentHandlerV2
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
"close_agent",
|
||||
function_payload(json!({"target": "/root"})),
|
||||
))
|
||||
.await
|
||||
.expect_err("close_agent should reject the root path");
|
||||
assert_eq!(
|
||||
root_path_error,
|
||||
FunctionCallError::RespondToModel("root is not a spawned agent".to_string())
|
||||
);
|
||||
|
||||
let root_id_error = CloseAgentHandlerV2
|
||||
.handle(invocation(
|
||||
session,
|
||||
turn,
|
||||
"close_agent",
|
||||
function_payload(json!({"target": root.thread_id.to_string()})),
|
||||
))
|
||||
.await
|
||||
.expect_err("close_agent should reject the root thread id");
|
||||
assert_eq!(
|
||||
root_id_error,
|
||||
FunctionCallError::RespondToModel("root is not a spawned agent".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn close_agent_submits_shutdown_and_returns_previous_status() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
|
||||
@@ -30,6 +30,15 @@ impl ToolHandler for Handler {
|
||||
.agent_control
|
||||
.get_agent_metadata(agent_id)
|
||||
.unwrap_or_default();
|
||||
if receiver_agent
|
||||
.agent_path
|
||||
.as_ref()
|
||||
.is_some_and(AgentPath::is_root)
|
||||
{
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"root is not a spawned agent".to_string(),
|
||||
));
|
||||
}
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
|
||||
Reference in New Issue
Block a user