From 6ddb747e7687e9e6e3a2482631028c07ddc89cb6 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 1 Jun 2026 19:57:11 +0200 Subject: [PATCH] [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. --- codex-rs/core/src/config/mod.rs | 4 +-- .../src/tools/handlers/multi_agents_spec.rs | 9 +++--- .../tools/handlers/multi_agents_spec_tests.rs | 10 +++---- .../src/tools/handlers/multi_agents_tests.rs | 28 +++++++++-------- .../src/tools/handlers/multi_agents_v2.rs | 4 +-- .../{assign_task.rs => followup_task.rs} | 10 +++---- .../handlers/multi_agents_v2/message_tool.rs | 10 +++---- codex-rs/core/src/tools/spec_plan.rs | 4 +-- codex-rs/core/src/tools/spec_plan_tests.rs | 30 ++++++++++++++++--- codex-rs/rollout-trace/README.md | 2 +- codex-rs/rollout-trace/src/tool_dispatch.rs | 2 +- 11 files changed, 69 insertions(+), 44 deletions(-) rename codex-rs/core/src/tools/handlers/multi_agents_v2/{assign_task.rs => followup_task.rs} (78%) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 0f01e8faf..ff666b51e 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -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. diff --git a/codex-rs/core/src/tools/handlers/multi_agents_spec.rs b/codex-rs/core/src/tools/handlers/multi_agents_spec.rs index 96acb595b..4359445f9 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_spec.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_spec.rs @@ -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, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs index b8b845a72..fc18c312d 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs @@ -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")); diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 9fed92258..bfe2ef3d9 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -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 { diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2.rs index 7612213cc..068f39337 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2.rs @@ -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; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/assign_task.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs similarity index 78% rename from codex-rs/core/src/tools/handlers/multi_agents_v2/assign_task.rs rename to codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs index 932c7a6e6..2fe09f5f2 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/assign_task.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs @@ -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 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 for Handler { invocation: ToolInvocation, ) -> Result, 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, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs index e50ace847..3226c4532 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs @@ -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 { 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 diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index facef69cb..3c8abf8f1 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -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( diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index e2b14b5b1..d82fa55ee 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -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", diff --git a/codex-rs/rollout-trace/README.md b/codex-rs/rollout-trace/README.md index 39ec9838e..540a49463 100644 --- a/codex-rs/rollout-trace/README.md +++ b/codex-rs/rollout-trace/README.md @@ -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"] diff --git a/codex-rs/rollout-trace/src/tool_dispatch.rs b/codex-rs/rollout-trace/src/tool_dispatch.rs index ae793bc67..7082283b4 100644 --- a/codex-rs/rollout-trace/src/tool_dispatch.rs +++ b/codex-rs/rollout-trace/src/tool_dispatch.rs @@ -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 {