Skip to content

Commit d39a722

Browse files
authored
feat: description multi-agent v2 (openai#17338)
1 parent 8d58899 commit d39a722

3 files changed

Lines changed: 101 additions & 21 deletions

File tree

codex-rs/core/src/tools/spec_tests.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -661,8 +661,11 @@ fn spawn_agent_description_omits_usage_hint_when_disabled() {
661661
r#"(?sx)
662662
^\s*
663663
No\ picker-visible\ models\ are\ currently\ loaded\.
664-
\s+Spawn\ a\ sub-agent\ for\ a\ well-scoped\ task\.
665-
\s+Returns\ the\ canonical\ task\ name\ for\ the\ spawned\ agent,\ plus\ the\ user-facing\ nickname\ when\ available\.
664+
\s+Spawns\ an\ agent\ to\ work\ on\ the\ specified\ task\.\ If\ your\ current\ task\ is\ `/root/task1`\ and\ you\ spawn_agent\ with\ task_name\ "task_3"\ the\ agent\ will\ have\ canonical\ task\ name\ `/root/task1/task_3`\.
665+
\s+You\ are\ then\ able\ to\ refer\ to\ this\ agent\ as\ `task_3`\ or\ `/root/task1/task_3`\ interchangeably\.\ However\ an\ agent\ `/root/task2/task_3`\ would\ only\ be\ able\ to\ communicate\ with\ this\ agent\ via\ its\ canonical\ name\ `/root/task1/task_3`\.
666+
\s+The\ spawned\ agent\ will\ have\ the\ same\ tools\ as\ you\ and\ the\ ability\ to\ spawn\ its\ own\ subagents\.
667+
\s+It\ will\ be\ able\ to\ send\ you\ and\ other\ running\ agents\ messages,\ and\ its\ final\ answer\ will\ be\ provided\ to\ you\ when\ it\ finishes\.
668+
\s+The\ new\ agent's\ canonical\ task\ name\ will\ be\ provided\ to\ it\ along\ with\ the\ message\.
666669
\s*$
667670
"#,
668671
&description,
@@ -680,8 +683,11 @@ fn spawn_agent_description_uses_configured_usage_hint_text() {
680683
r#"(?sx)
681684
^\s*
682685
No\ picker-visible\ models\ are\ currently\ loaded\.
683-
\s+Spawn\ a\ sub-agent\ for\ a\ well-scoped\ task\.
684-
\s+Returns\ the\ canonical\ task\ name\ for\ the\ spawned\ agent,\ plus\ the\ user-facing\ nickname\ when\ available\.
686+
\s+Spawns\ an\ agent\ to\ work\ on\ the\ specified\ task\.\ If\ your\ current\ task\ is\ `/root/task1`\ and\ you\ spawn_agent\ with\ task_name\ "task_3"\ the\ agent\ will\ have\ canonical\ task\ name\ `/root/task1/task_3`\.
687+
\s+You\ are\ then\ able\ to\ refer\ to\ this\ agent\ as\ `task_3`\ or\ `/root/task1/task_3`\ interchangeably\.\ However\ an\ agent\ `/root/task2/task_3`\ would\ only\ be\ able\ to\ communicate\ with\ this\ agent\ via\ its\ canonical\ name\ `/root/task1/task_3`\.
688+
\s+The\ spawned\ agent\ will\ have\ the\ same\ tools\ as\ you\ and\ the\ ability\ to\ spawn\ its\ own\ subagents\.
689+
\s+It\ will\ be\ able\ to\ send\ you\ and\ other\ running\ agents\ messages,\ and\ its\ final\ answer\ will\ be\ provided\ to\ you\ when\ it\ finishes\.
690+
\s+The\ new\ agent's\ canonical\ task\ name\ will\ be\ provided\ to\ it\ along\ with\ the\ message\.
685691
\s+Custom\ delegation\ guidance\ only\.
686692
\s*$
687693
"#,

codex-rs/tools/src/agent_tool.rs

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,6 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions<'_>) -> ToolSpe
5050
pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions<'_>) -> ToolSpec {
5151
let available_models_description = (!options.hide_agent_type_model_reasoning)
5252
.then(|| spawn_agent_models_description(options.available_models));
53-
let return_value_description = if options.hide_agent_type_model_reasoning {
54-
"Returns the canonical task name for the spawned agent."
55-
} else {
56-
"Returns the canonical task name for the spawned agent, plus the user-facing nickname when available."
57-
};
5853
let mut properties = spawn_agent_common_properties_v2(&options.agent_type_description);
5954
if options.hide_agent_type_model_reasoning {
6055
hide_spawn_agent_metadata_options(&mut properties);
@@ -69,9 +64,8 @@ pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions<'_>) -> ToolSpe
6964

7065
ToolSpec::Function(ResponsesApiTool {
7166
name: "spawn_agent".to_string(),
72-
description: spawn_agent_tool_description(
67+
description: spawn_agent_tool_description_v2(
7368
available_models_description.as_deref(),
74-
return_value_description,
7569
options.include_usage_hint,
7670
options.usage_hint_text,
7771
),
@@ -127,7 +121,7 @@ pub fn create_send_message_tool() -> ToolSpec {
127121
(
128122
"target".to_string(),
129123
JsonSchema::string(Some(
130-
"Agent id or canonical task name to message (from spawn_agent).".to_string(),
124+
"Relative or canonical task name to message (from spawn_agent).".to_string(),
131125
)),
132126
),
133127
(
@@ -140,11 +134,15 @@ pub fn create_send_message_tool() -> ToolSpec {
140134

141135
ToolSpec::Function(ResponsesApiTool {
142136
name: "send_message".to_string(),
143-
description: "Add a message to an existing agent without triggering a new turn. In MultiAgentV2, this tool currently supports text content only."
137+
description: "Send a string message to an existing agent without triggering a new turn."
144138
.to_string(),
145139
strict: false,
146140
defer_loading: None,
147-
parameters: JsonSchema::object(properties, Some(vec!["target".to_string(), "message".to_string()]), Some(false.into())),
141+
parameters: JsonSchema::object(
142+
properties,
143+
Some(vec!["target".to_string(), "message".to_string()]),
144+
Some(false.into()),
145+
),
148146
output_schema: None,
149147
})
150148
}
@@ -166,15 +164,15 @@ pub fn create_followup_task_tool() -> ToolSpec {
166164
(
167165
"interrupt".to_string(),
168166
JsonSchema::boolean(Some(
169-
"When true, stop the agent's current task and handle this immediately. When false (default), queue this message."
167+
"When true, stop the agent's current task and handle this immediately. When false (default), queue this message; if the target is already running, it starts the target's next turn after the current turn completes."
170168
.to_string(),
171169
)),
172170
),
173171
]);
174172

175173
ToolSpec::Function(ResponsesApiTool {
176174
name: "followup_task".to_string(),
177-
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."
175+
description: "Send a string message to an existing non-root agent and trigger a turn in the target. Use interrupt=true to redirect work immediately. If interrupt=false and the target's turn has not completed, the message is queued and starts the target's next turn after the current turn completes."
178176
.to_string(),
179177
strict: false,
180178
defer_loading: None,
@@ -216,7 +214,7 @@ pub fn create_wait_agent_tool_v1(options: WaitAgentTimeoutOptions) -> ToolSpec {
216214
pub fn create_wait_agent_tool_v2(options: WaitAgentTimeoutOptions) -> ToolSpec {
217215
ToolSpec::Function(ResponsesApiTool {
218216
name: "wait_agent".to_string(),
219-
description: "Wait for a mailbox update from any live agent, including queued messages and final-status notifications. Returns a brief wait summary instead of agent content, or a timeout summary if no mailbox update arrives before the deadline."
217+
description: "Wait for a mailbox update from any live agent, including queued messages and final-status notifications. Does not return the content; returns either a summary of which agents have updates (if any), or a timeout summary if no mailbox update arrives before the deadline."
220218
.to_string(),
221219
strict: false,
222220
defer_loading: None,
@@ -229,7 +227,7 @@ pub fn create_list_agents_tool() -> ToolSpec {
229227
let properties = BTreeMap::from([(
230228
"path_prefix".to_string(),
231229
JsonSchema::string(Some(
232-
"Optional task-path prefix. Accepts the same relative or absolute task-path syntax as other MultiAgentV2 agent targets."
230+
"Optional task-path prefix (not ending with trailing slash). Accepts the same relative or absolute task-path syntax."
233231
.to_string(),
234232
)),
235233
)]);
@@ -448,7 +446,7 @@ fn wait_output_schema_v2() -> Value {
448446
},
449447
"timed_out": {
450448
"type": "boolean",
451-
"description": "Whether the wait call returned due to timeout before any agent reached a final status."
449+
"description": "Whether the wait call returned because no mailbox update arrived before the timeout."
452450
}
453451
},
454452
"required": ["message", "timed_out"],
@@ -556,7 +554,7 @@ fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap<St
556554
(
557555
"fork_turns".to_string(),
558556
JsonSchema::string(Some(
559-
"Optional MultiAgentV2 fork mode. Use `none`, `all`, or a positive integer string such as `3` to fork only the most recent turns."
557+
"Optional number of turns to fork. Defaults to `all`. Use `none`, `all`, or a positive integer string such as `3` to fork only the most recent turns."
560558
.to_string(),
561559
)),
562560
),
@@ -652,6 +650,36 @@ Requests for depth, thoroughness, research, investigation, or detailed codebase
652650
)
653651
}
654652

653+
fn spawn_agent_tool_description_v2(
654+
available_models_description: Option<&str>,
655+
include_usage_hint: bool,
656+
usage_hint_text: Option<String>,
657+
) -> String {
658+
let agent_role_guidance = available_models_description.unwrap_or_default();
659+
660+
let tool_description = format!(
661+
r#"
662+
{agent_role_guidance}
663+
Spawns an agent to work on the specified task. If your current task is `/root/task1` and you spawn_agent with task_name "task_3" the agent will have canonical task name `/root/task1/task_3`.
664+
You are then able to refer to this agent as `task_3` or `/root/task1/task_3` interchangeably. However an agent `/root/task2/task_3` would only be able to communicate with this agent via its canonical name `/root/task1/task_3`.
665+
The spawned agent will have the same tools as you and the ability to spawn its own subagents.
666+
It will be able to send you and other running agents messages, and its final answer will be provided to you when it finishes.
667+
The new agent's canonical task name will be provided to it along with the message."#
668+
);
669+
670+
if !include_usage_hint {
671+
return tool_description;
672+
}
673+
if let Some(usage_hint_text) = usage_hint_text {
674+
return format!(
675+
r#"
676+
{tool_description}
677+
{usage_hint_text}"#
678+
);
679+
}
680+
tool_description
681+
}
682+
655683
fn spawn_agent_models_description(models: &[ModelPreset]) -> String {
656684
let visible_models: Vec<&ModelPreset> =
657685
models.iter().filter(|model| model.show_in_picker).collect();
@@ -713,7 +741,7 @@ fn wait_agent_tool_parameters_v2(options: WaitAgentTimeoutOptions) -> JsonSchema
713741
let properties = BTreeMap::from([(
714742
"timeout_ms".to_string(),
715743
JsonSchema::number(Some(format!(
716-
"Optional timeout in milliseconds. Defaults to {}, min {}, max {}. Prefer longer waits (minutes) to avoid busy polling.",
744+
"Optional timeout in milliseconds. Defaults to {}, min {}, max {}.",
717745
options.default_timeout_ms, options.min_timeout_ms, options.max_timeout_ms,
718746
))),
719747
)]);

codex-rs/tools/src/agent_tool_tests.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
5959
.properties
6060
.as_ref()
6161
.expect("spawn_agent should use object params");
62+
assert!(description.contains("Spawns an agent to work on the specified task."));
63+
assert!(description.contains("The spawned agent will have the same tools as you"));
6264
assert!(description.contains("visible display (`visible-model`)"));
6365
assert!(!description.contains("hidden display (`hidden-model`)"));
6466
assert!(properties.contains_key("task_name"));
@@ -109,6 +111,7 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
109111
#[test]
110112
fn send_message_tool_requires_message_and_has_no_output_schema() {
111113
let ToolSpec::Function(ResponsesApiTool {
114+
description,
112115
parameters,
113116
output_schema,
114117
..
@@ -128,6 +131,16 @@ fn send_message_tool_requires_message_and_has_no_output_schema() {
128131
assert!(properties.contains_key("message"));
129132
assert!(!properties.contains_key("interrupt"));
130133
assert!(!properties.contains_key("items"));
134+
assert_eq!(
135+
description,
136+
"Send a string message to an existing agent without triggering a new turn."
137+
);
138+
assert_eq!(
139+
properties
140+
.get("target")
141+
.and_then(|schema| schema.description.as_deref()),
142+
Some("Relative or canonical task name to message (from spawn_agent).")
143+
);
131144
assert_eq!(
132145
parameters.required.as_ref(),
133146
Some(&vec!["target".to_string(), "message".to_string()])
@@ -138,6 +151,7 @@ fn send_message_tool_requires_message_and_has_no_output_schema() {
138151
#[test]
139152
fn followup_task_tool_requires_message_and_has_no_output_schema() {
140153
let ToolSpec::Function(ResponsesApiTool {
154+
description,
141155
parameters,
142156
output_schema,
143157
..
@@ -157,6 +171,20 @@ fn followup_task_tool_requires_message_and_has_no_output_schema() {
157171
assert!(properties.contains_key("message"));
158172
assert!(properties.contains_key("interrupt"));
159173
assert!(!properties.contains_key("items"));
174+
assert!(description.contains(
175+
"Send a string message to an existing non-root agent and trigger a turn in the target."
176+
));
177+
assert!(description.contains(
178+
"If interrupt=false and the target's turn has not completed, the message is queued"
179+
));
180+
assert_eq!(
181+
properties
182+
.get("interrupt")
183+
.and_then(|schema| schema.description.as_deref()),
184+
Some(
185+
"When true, stop the agent's current task and handle this immediately. When false (default), queue this message; if the target is already running, it starts the target's next turn after the current turn completes."
186+
)
187+
);
160188
assert_eq!(
161189
parameters.required.as_ref(),
162190
Some(&vec!["target".to_string(), "message".to_string()])
@@ -167,6 +195,7 @@ fn followup_task_tool_requires_message_and_has_no_output_schema() {
167195
#[test]
168196
fn wait_agent_tool_v2_uses_timeout_only_summary_output() {
169197
let ToolSpec::Function(ResponsesApiTool {
198+
description,
170199
parameters,
171200
output_schema,
172201
..
@@ -188,6 +217,15 @@ fn wait_agent_tool_v2_uses_timeout_only_summary_output() {
188217
.expect("wait_agent should use object params");
189218
assert!(!properties.contains_key("targets"));
190219
assert!(properties.contains_key("timeout_ms"));
220+
assert!(description.contains(
221+
"Does not return the content; returns either a summary of which agents have updates (if any)"
222+
));
223+
assert_eq!(
224+
properties
225+
.get("timeout_ms")
226+
.and_then(|schema| schema.description.as_deref()),
227+
Some("Optional timeout in milliseconds. Defaults to 30000, min 10000, max 3600000.")
228+
);
191229
assert_eq!(parameters.required.as_ref(), None);
192230
assert_eq!(
193231
output_schema.expect("wait output schema")["properties"]["message"]["description"],
@@ -214,6 +252,14 @@ fn list_agents_tool_includes_path_prefix_and_agent_fields() {
214252
.as_ref()
215253
.expect("list_agents should use object params");
216254
assert!(properties.contains_key("path_prefix"));
255+
assert_eq!(
256+
properties
257+
.get("path_prefix")
258+
.and_then(|schema| schema.description.as_deref()),
259+
Some(
260+
"Optional task-path prefix (not ending with trailing slash). Accepts the same relative or absolute task-path syntax."
261+
)
262+
);
217263
assert_eq!(
218264
output_schema.expect("list_agents output schema")["properties"]["agents"]["items"]["required"],
219265
json!(["agent_name", "agent_status", "last_task_message"])

0 commit comments

Comments
 (0)