mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Rename multi-agent v2 assign_task to followup_task (#25636)
## Summary Renames the MultiAgentV2 turn-triggering tool from `assign_task` to `followup_task` so the exposed tool name better describes sending an additional task to an existing agent. This updates the tool spec, handler/module names, registry wiring, default multi-agent v2 usage hints, and tests. Rollout trace classification keeps accepting legacy `assign_task` events so older traces still reduce correctly, while docs show the new tool name. ## Test plan - `just test -p codex-core followup_task` - `just test -p codex-core -E 'test(multi_agent_feature_selects_one_agent_tool_family) | test(multi_agent_v2_can_use_configured_tool_namespace) | test(code_mode_only_can_expose_namespaced_multi_agent_v2_as_normal_tools)'` - `just test -p codex-rollout-trace` - `just fix -p codex-core` - `just fix -p codex-rollout-trace` Notes: `just fmt` ran `cargo fmt` but failed in the Python ruff phase because the local environment could not resolve `hatchling>=1.27.0` from the configured internal registry. A full `just test -p codex-core` also hit unrelated environment-sensitive integration failures involving missing spawned test binaries/sandbox behavior; the changed multi-agent spec/handler tests passed in the filtered runs above.
This commit is contained in:
committed by
GitHub
Unverified
parent
fb94703b21
commit
6ddb747e76
@@ -195,7 +195,7 @@ At the start of your turn, you are the active agent.
|
||||
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents.
|
||||
All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
|
||||
|
||||
You can use `spawn_agent` to create a new agent, `assign_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent without triggering a turn.
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent without triggering a turn.
|
||||
Child agents can also spawn their own sub-agents.
|
||||
You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter.
|
||||
|
||||
@@ -212,7 +212,7 @@ const DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT: &str = r#"You are an agen
|
||||
|
||||
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
|
||||
|
||||
You can use `spawn_agent` to create a new agent, `assign_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent.
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent.
|
||||
Child agents can also spawn their own sub-agents.
|
||||
|
||||
When you provide a response in the final channel, that content is immediately delivered back to your parent agent.
|
||||
|
||||
@@ -179,12 +179,13 @@ pub fn create_send_message_tool() -> ToolSpec {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_assign_task_tool() -> ToolSpec {
|
||||
pub fn create_followup_task_tool() -> ToolSpec {
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
"target".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Agent id or canonical task name to message (from spawn_agent).".to_string(),
|
||||
"Agent id or canonical task name to send a follow-up task to (from spawn_agent)."
|
||||
.to_string(),
|
||||
)),
|
||||
),
|
||||
(
|
||||
@@ -196,8 +197,8 @@ pub fn create_assign_task_tool() -> ToolSpec {
|
||||
]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "assign_task".to_string(),
|
||||
description: "Send a message to an existing non-root target agent and trigger a turn in that target. If the target is currently mid-turn, the message is queued and will be used to start the target's next turn, after the current turn completes."
|
||||
name: "followup_task".to_string(),
|
||||
description: "Send a follow-up task to an existing non-root target agent and trigger a turn in that target. If the target is currently mid-turn, the message is queued and will be used to start the target's next turn, after the current turn completes."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
|
||||
@@ -247,17 +247,17 @@ fn send_message_tool_requires_message_and_has_no_output_schema() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assign_task_tool_requires_message_and_has_no_output_schema() {
|
||||
fn followup_task_tool_requires_message_and_has_no_output_schema() {
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
name,
|
||||
parameters,
|
||||
output_schema,
|
||||
..
|
||||
}) = create_assign_task_tool()
|
||||
}) = create_followup_task_tool()
|
||||
else {
|
||||
panic!("assign_task should be a function tool");
|
||||
panic!("followup_task should be a function tool");
|
||||
};
|
||||
assert_eq!(name, "assign_task");
|
||||
assert_eq!(name, "followup_task");
|
||||
assert_eq!(
|
||||
parameters.schema_type,
|
||||
Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object))
|
||||
@@ -265,7 +265,7 @@ fn assign_task_tool_requires_message_and_has_no_output_schema() {
|
||||
let properties = parameters
|
||||
.properties
|
||||
.as_ref()
|
||||
.expect("assign_task should use object params");
|
||||
.expect("followup_task should use object params");
|
||||
assert!(properties.contains_key("target"));
|
||||
assert!(properties.contains_key("message"));
|
||||
assert!(!properties.contains_key("items"));
|
||||
|
||||
@@ -8,8 +8,8 @@ use crate::session::tests::make_session_and_context;
|
||||
use crate::session_prefix::format_subagent_notification_message;
|
||||
use crate::thread_manager::thread_store_from_config;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::handlers::multi_agents_v2::AssignTaskHandler as AssignTaskHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
|
||||
@@ -1413,7 +1413,7 @@ 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() {
|
||||
async fn multi_agent_v2_followup_task_rejects_root_target_from_child() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
@@ -1461,11 +1461,11 @@ async fn multi_agent_v2_assign_task_rejects_root_target_from_child() {
|
||||
agent_role: None,
|
||||
});
|
||||
|
||||
let Err(err) = AssignTaskHandlerV2
|
||||
let Err(err) = FollowupTaskHandlerV2
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"assign_task",
|
||||
"followup_task",
|
||||
function_payload(json!({
|
||||
"target": "/root",
|
||||
"message": "run this",
|
||||
@@ -1473,12 +1473,14 @@ async fn multi_agent_v2_assign_task_rejects_root_target_from_child() {
|
||||
))
|
||||
.await
|
||||
else {
|
||||
panic!("assign_task should reject the root target");
|
||||
panic!("followup_task should reject the root target");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
FunctionCallError::RespondToModel("Tasks can't be assigned to the root agent".to_string())
|
||||
FunctionCallError::RespondToModel(
|
||||
"Follow-up tasks can't target the root agent".to_string()
|
||||
)
|
||||
);
|
||||
let root_ops = manager
|
||||
.captured_ops()
|
||||
@@ -1868,7 +1870,7 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_assign_task_completion_notifies_parent_on_every_turn() {
|
||||
async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
@@ -1923,18 +1925,18 @@ async fn multi_agent_v2_assign_task_completion_notifies_parent_on_every_turn() {
|
||||
)
|
||||
.await;
|
||||
|
||||
AssignTaskHandlerV2
|
||||
FollowupTaskHandlerV2
|
||||
.handle(invocation(
|
||||
session,
|
||||
turn,
|
||||
"assign_task",
|
||||
"followup_task",
|
||||
function_payload(json!({
|
||||
"target": agent_id.to_string(),
|
||||
"message": "continue",
|
||||
})),
|
||||
))
|
||||
.await
|
||||
.expect("assign_task should succeed");
|
||||
.expect("followup_task should succeed");
|
||||
|
||||
let second_turn = thread.codex.session.new_default_turn().await;
|
||||
thread
|
||||
@@ -2003,7 +2005,7 @@ async fn multi_agent_v2_assign_task_completion_notifies_parent_on_every_turn() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_assign_task_rejects_legacy_items_field() {
|
||||
async fn multi_agent_v2_followup_task_rejects_legacy_items_field() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
@@ -2039,14 +2041,14 @@ async fn multi_agent_v2_assign_task_rejects_legacy_items_field() {
|
||||
let invocation = invocation(
|
||||
session,
|
||||
turn,
|
||||
"assign_task",
|
||||
"followup_task",
|
||||
function_payload(json!({
|
||||
"target": agent_id.to_string(),
|
||||
"items": [{"type": "text", "text": "continue"}],
|
||||
})),
|
||||
);
|
||||
|
||||
let Err(err) = AssignTaskHandlerV2.handle(invocation).await else {
|
||||
let Err(err) = FollowupTaskHandlerV2.handle(invocation).await else {
|
||||
panic!("legacy items field should be rejected in v2");
|
||||
};
|
||||
let FunctionCallError::RespondToModel(message) = err else {
|
||||
|
||||
@@ -28,15 +28,15 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
pub(crate) use assign_task::Handler as AssignTaskHandler;
|
||||
pub(crate) use close_agent::Handler as CloseAgentHandler;
|
||||
pub(crate) use followup_task::Handler as FollowupTaskHandler;
|
||||
pub(crate) use list_agents::Handler as ListAgentsHandler;
|
||||
pub(crate) use send_message::Handler as SendMessageHandler;
|
||||
pub(crate) use spawn::Handler as SpawnAgentHandler;
|
||||
pub(crate) use wait::Handler as WaitAgentHandler;
|
||||
|
||||
mod assign_task;
|
||||
mod close_agent;
|
||||
mod followup_task;
|
||||
mod list_agents;
|
||||
mod message_tool;
|
||||
mod send_message;
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
use super::message_tool::AssignTaskArgs;
|
||||
use super::message_tool::FollowupTaskArgs;
|
||||
use super::message_tool::MessageDeliveryMode;
|
||||
use super::message_tool::handle_message_string_tool;
|
||||
use super::*;
|
||||
use crate::tools::handlers::multi_agents_spec::create_assign_task_tool;
|
||||
use crate::tools::handlers::multi_agents_spec::create_followup_task_tool;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
@@ -10,11 +10,11 @@ pub(crate) struct Handler;
|
||||
#[async_trait::async_trait]
|
||||
impl ToolExecutor<ToolInvocation> for Handler {
|
||||
fn tool_name(&self) -> ToolName {
|
||||
ToolName::plain("assign_task")
|
||||
ToolName::plain("followup_task")
|
||||
}
|
||||
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_assign_task_tool()
|
||||
create_followup_task_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
@@ -22,7 +22,7 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
invocation: ToolInvocation,
|
||||
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {
|
||||
let arguments = function_arguments(invocation.payload.clone())?;
|
||||
let args: AssignTaskArgs = parse_arguments(&arguments)?;
|
||||
let args: FollowupTaskArgs = parse_arguments(&arguments)?;
|
||||
handle_message_string_tool(
|
||||
invocation,
|
||||
MessageDeliveryMode::TriggerTurn,
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Shared argument parsing and dispatch for the v2 text-only agent messaging tools.
|
||||
//!
|
||||
//! `send_message` and `assign_task` share the same submission path and differ only in whether the
|
||||
//! `send_message` and `followup_task` share the same submission path and differ only in whether the
|
||||
//! resulting `InterAgentCommunication` should wake the target immediately.
|
||||
|
||||
use super::*;
|
||||
@@ -40,8 +40,8 @@ pub(crate) struct SendMessageArgs {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
/// Input for the MultiAgentV2 `assign_task` tool.
|
||||
pub(crate) struct AssignTaskArgs {
|
||||
/// Input for the MultiAgentV2 `followup_task` tool.
|
||||
pub(crate) struct FollowupTaskArgs {
|
||||
pub(crate) target: String,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
@@ -55,7 +55,7 @@ fn message_content(message: String) -> Result<String, FunctionCallError> {
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// Handles the shared MultiAgentV2 plain-text message flow for both `send_message` and `assign_task`.
|
||||
/// Handles the shared MultiAgentV2 plain-text message flow for both `send_message` and `followup_task`.
|
||||
pub(crate) async fn handle_message_string_tool(
|
||||
invocation: ToolInvocation,
|
||||
mode: MessageDeliveryMode,
|
||||
@@ -82,7 +82,7 @@ pub(crate) async fn handle_message_string_tool(
|
||||
.is_some_and(AgentPath::is_root)
|
||||
{
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"Tasks can't be assigned to the root agent".to_string(),
|
||||
"Follow-up tasks can't target the root agent".to_string(),
|
||||
));
|
||||
}
|
||||
session
|
||||
|
||||
@@ -38,8 +38,8 @@ use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
|
||||
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
|
||||
use crate::tools::handlers::multi_agents_v2::AssignTaskHandler as AssignTaskHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
|
||||
@@ -686,7 +686,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
|
||||
exposure,
|
||||
));
|
||||
planned_tools.add_arc(override_tool_exposure(
|
||||
multi_agent_v2_handler(AssignTaskHandlerV2, tool_namespace),
|
||||
multi_agent_v2_handler(FollowupTaskHandlerV2, tool_namespace),
|
||||
exposure,
|
||||
));
|
||||
planned_tools.add_arc(override_tool_exposure(
|
||||
|
||||
@@ -766,6 +766,7 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
"send_message",
|
||||
"followup_task",
|
||||
"assign_task",
|
||||
"list_agents",
|
||||
]);
|
||||
@@ -790,12 +791,12 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
|
||||
v2.assert_visible_contains(&[
|
||||
"spawn_agent",
|
||||
"send_message",
|
||||
"assign_task",
|
||||
"followup_task",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
"list_agents",
|
||||
]);
|
||||
v2.assert_visible_lacks(&["send_input", "resume_agent"]);
|
||||
v2.assert_visible_lacks(&["send_input", "resume_agent", "assign_task"]);
|
||||
let spawn_agent_description = match v2.visible_spec("spawn_agent") {
|
||||
ToolSpec::Function(tool) => tool.description.as_str(),
|
||||
other => panic!("expected spawn_agent function spec, got {other:?}"),
|
||||
@@ -892,10 +893,24 @@ async fn multi_agent_v2_can_use_configured_tool_namespace() {
|
||||
.await;
|
||||
|
||||
namespaced.assert_visible_contains(&["agents"]);
|
||||
namespaced.assert_visible_lacks(&["assign_task"]);
|
||||
assert!(
|
||||
!namespaced
|
||||
.registered_names
|
||||
.contains(&ToolName::namespaced("agents", "assign_task").to_string()),
|
||||
"expected no namespaced runtime for assign_task"
|
||||
);
|
||||
assert!(
|
||||
!namespaced
|
||||
.namespace_function_names("agents")
|
||||
.iter()
|
||||
.any(|name| name == "assign_task"),
|
||||
"expected assign_task to be absent from agents namespace"
|
||||
);
|
||||
for tool_name in [
|
||||
"spawn_agent",
|
||||
"send_message",
|
||||
"assign_task",
|
||||
"followup_task",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
"list_agents",
|
||||
@@ -966,10 +981,17 @@ async fn code_mode_only_can_expose_namespaced_multi_agent_v2_as_normal_tools() {
|
||||
.await;
|
||||
|
||||
assert_eq!(plan.visible_names, vec!["exec", "wait", "agents"]);
|
||||
assert!(
|
||||
!plan
|
||||
.namespace_function_names("agents")
|
||||
.iter()
|
||||
.any(|name| name == "assign_task"),
|
||||
"expected assign_task to be absent from agents namespace"
|
||||
);
|
||||
for tool_name in [
|
||||
"spawn_agent",
|
||||
"send_message",
|
||||
"assign_task",
|
||||
"followup_task",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
"list_agents",
|
||||
|
||||
@@ -177,7 +177,7 @@ the edges between them.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
RootTool["root ToolCall\nspawn_agent / assign_task / send_message"]
|
||||
RootTool["root ToolCall\nspawn_agent / followup_task / send_message"]
|
||||
ChildInput["child ConversationItem\ninjected task/message"]
|
||||
ChildThread["child AgentThread"]
|
||||
ChildResult["child assistant ConversationItem\nresult message"]
|
||||
|
||||
@@ -267,7 +267,7 @@ fn dispatched_tool_kind(tool_name: &str, _payload: &ToolDispatchPayload) -> Tool
|
||||
"image_generation" | "image_query" => ToolCallKind::ImageGeneration,
|
||||
"spawn_agent" => ToolCallKind::SpawnAgent,
|
||||
"send_message" => ToolCallKind::SendMessage,
|
||||
"assign_task" | "followup_task" => ToolCallKind::AssignAgentTask,
|
||||
"followup_task" | "assign_task" => ToolCallKind::AssignAgentTask,
|
||||
"wait_agent" => ToolCallKind::WaitAgent,
|
||||
"close_agent" => ToolCallKind::CloseAgent,
|
||||
other => ToolCallKind::Other {
|
||||
|
||||
Reference in New Issue
Block a user