Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions codex-rs/app-server/tests/suite/v2/turn_start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2809,7 +2809,12 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()
|req: &wiremock::Request| body_contains(req, PARENT_PROMPT),
responses::sse(vec![
responses::ev_response_created("resp-turn1-1"),
responses::ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args),
responses::ev_function_call_with_namespace(
SPAWN_CALL_ID,
"multi_agent_v1",
"spawn_agent",
&spawn_args,
),
responses::ev_completed("resp-turn1-1"),
]),
)
Expand Down Expand Up @@ -3006,7 +3011,12 @@ async fn turn_start_emits_spawn_agent_item_with_effective_role_model_metadata_v2
|req: &wiremock::Request| body_contains(req, PARENT_PROMPT),
responses::sse(vec![
responses::ev_response_created("resp-turn1-1"),
responses::ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args),
responses::ev_function_call_with_namespace(
SPAWN_CALL_ID,
"multi_agent_v1",
"spawn_agent",
&spawn_args,
),
responses::ev_completed("resp-turn1-1"),
]),
)
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/tools/handlers/multi_agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::context::boxed_tool_output;
pub(crate) use crate::tools::handlers::multi_agents_common::*;
use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub(crate) struct Handler;
#[async_trait::async_trait]
impl ToolExecutor<ToolInvocation> for Handler {
fn tool_name(&self) -> ToolName {
ToolName::plain("close_agent")
ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "close_agent")
}

fn spec(&self) -> Option<ToolSpec> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub(crate) struct Handler;
#[async_trait::async_trait]
impl ToolExecutor<ToolInvocation> for Handler {
fn tool_name(&self) -> ToolName {
ToolName::plain("resume_agent")
ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "resume_agent")
}

fn spec(&self) -> Option<ToolSpec> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub(crate) struct Handler;
#[async_trait::async_trait]
impl ToolExecutor<ToolInvocation> for Handler {
fn tool_name(&self) -> ToolName {
ToolName::plain("send_input")
ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "send_input")
}

fn spec(&self) -> Option<ToolSpec> {
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/tools/handlers/multi_agents/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl Handler {
#[async_trait::async_trait]
impl ToolExecutor<ToolInvocation> for Handler {
fn tool_name(&self) -> ToolName {
ToolName::plain("spawn_agent")
ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "spawn_agent")
}

fn spec(&self) -> Option<ToolSpec> {
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/tools/handlers/multi_agents/wait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ impl Handler {
#[async_trait::async_trait]
impl ToolExecutor<ToolInvocation> for Handler {
fn tool_name(&self) -> ToolName {
ToolName::plain("wait_agent")
ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "wait_agent")
}

fn spec(&self) -> Option<ToolSpec> {
Expand Down
113 changes: 69 additions & 44 deletions codex-rs/core/src/tools/handlers/multi_agents_spec.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use codex_protocol::openai_models::ModelPreset;
use codex_tools::JsonSchema;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ResponsesApiTool;
use codex_tools::ToolSpec;
use serde_json::Value;
use serde_json::json;
use std::collections::BTreeMap;

pub const MULTI_AGENT_V1_NAMESPACE: &str = "multi_agent_v1";
const MULTI_AGENT_V1_NAMESPACE_DESCRIPTION: &str = "Tools for spawning and managing sub-agents.";

const SPAWN_AGENT_INHERITED_MODEL_GUIDANCE: &str = "Spawned agents inherit your current model by default. Omit `model` to use that preferred default; set `model` only when an explicit override is needed.";
const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str = "Optional model override for the new agent. Leave unset to inherit the same model as the parent, which is the preferred default. Only set this when the user explicitly asks for a different model or the task clearly requires one.";
const SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION: &str = "Optional service tier override for the new agent. Leave unset unless the user explicitly asks for one.";
Expand Down Expand Up @@ -48,18 +53,22 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec {
hide_spawn_agent_metadata_options(&mut properties);
}

ToolSpec::Function(ResponsesApiTool {
name: "spawn_agent".to_string(),
description: spawn_agent_tool_description(
available_models_description.as_deref(),
return_value_description,
options.include_usage_hint,
options.usage_hint_text,
),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(properties, /*required*/ None, Some(false.into())),
output_schema: Some(spawn_agent_output_schema_v1()),
ToolSpec::Namespace(ResponsesApiNamespace {
Comment thread
jif-oai marked this conversation as resolved.
name: MULTI_AGENT_V1_NAMESPACE.to_string(),
description: MULTI_AGENT_V1_NAMESPACE_DESCRIPTION.to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "spawn_agent".to_string(),
description: spawn_agent_tool_description(
available_models_description.as_deref(),
return_value_description,
options.include_usage_hint,
options.usage_hint_text,
),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(properties, /*required*/ None, Some(false.into())),
output_schema: Some(spawn_agent_output_schema_v1()),
})],
})
}

Expand Down Expand Up @@ -122,14 +131,18 @@ pub fn create_send_input_tool_v1() -> ToolSpec {
),
]);

ToolSpec::Function(ResponsesApiTool {
name: "send_input".to_string(),
description: "Send a message to an existing agent. Use interrupt=true to redirect work immediately. You should reuse the agent by send_input if you believe your assigned task is highly dependent on the context of a previous task."
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(properties, Some(vec!["target".to_string()]), Some(false.into())),
output_schema: Some(send_input_output_schema()),
ToolSpec::Namespace(ResponsesApiNamespace {
name: MULTI_AGENT_V1_NAMESPACE.to_string(),
description: MULTI_AGENT_V1_NAMESPACE_DESCRIPTION.to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "send_input".to_string(),
description: "Send a message to an existing agent. Use interrupt=true to redirect work immediately. You should reuse the agent by send_input if you believe your assigned task is highly dependent on the context of a previous task."
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(properties, Some(vec!["target".to_string()]), Some(false.into())),
output_schema: Some(send_input_output_schema()),
})],
})
}

Expand Down Expand Up @@ -197,27 +210,35 @@ pub fn create_resume_agent_tool() -> ToolSpec {
JsonSchema::string(Some("Agent id to resume.".to_string())),
)]);

ToolSpec::Function(ResponsesApiTool {
name: "resume_agent".to_string(),
description:
"Resume a previously closed agent by id so it can receive send_input and wait_agent calls."
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(properties, Some(vec!["id".to_string()]), Some(false.into())),
output_schema: Some(resume_agent_output_schema()),
ToolSpec::Namespace(ResponsesApiNamespace {
name: MULTI_AGENT_V1_NAMESPACE.to_string(),
description: MULTI_AGENT_V1_NAMESPACE_DESCRIPTION.to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "resume_agent".to_string(),
description:
"Resume a previously closed agent by id so it can receive send_input and wait_agent calls."
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(properties, Some(vec!["id".to_string()]), Some(false.into())),
output_schema: Some(resume_agent_output_schema()),
})],
})
}

pub fn create_wait_agent_tool_v1(options: WaitAgentTimeoutOptions) -> ToolSpec {
ToolSpec::Function(ResponsesApiTool {
name: "wait_agent".to_string(),
description: "Wait for agents to reach a final status. Completed statuses may include the agent's final message. Returns empty status when timed out. Once the agent reaches a final status, a notification message will be received containing the same completed status."
.to_string(),
strict: false,
defer_loading: None,
parameters: wait_agent_tool_parameters_v1(options),
output_schema: Some(wait_output_schema_v1()),
ToolSpec::Namespace(ResponsesApiNamespace {
name: MULTI_AGENT_V1_NAMESPACE.to_string(),
description: MULTI_AGENT_V1_NAMESPACE_DESCRIPTION.to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "wait_agent".to_string(),
description: "Wait for agents to reach a final status. Completed statuses may include the agent's final message. Returns empty status when timed out. Once the agent reaches a final status, a notification message will be received containing the same completed status."
.to_string(),
strict: false,
defer_loading: None,
parameters: wait_agent_tool_parameters_v1(options),
output_schema: Some(wait_output_schema_v1()),
})],
})
}

Expand Down Expand Up @@ -260,13 +281,17 @@ pub fn create_close_agent_tool_v1() -> ToolSpec {
JsonSchema::string(Some("Agent id to close (from spawn_agent).".to_string())),
)]);

ToolSpec::Function(ResponsesApiTool {
name: "close_agent".to_string(),
description: "Close an agent and any open descendants when they are no longer needed, and return the target agent's previous status before shutdown was requested. Don't keep agents open for too long if they are not needed anymore.".to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(properties, Some(vec!["target".to_string()]), Some(false.into())),
output_schema: Some(close_agent_output_schema()),
ToolSpec::Namespace(ResponsesApiNamespace {
name: MULTI_AGENT_V1_NAMESPACE.to_string(),
description: MULTI_AGENT_V1_NAMESPACE_DESCRIPTION.to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "close_agent".to_string(),
description: "Close an agent and any open descendants when they are no longer needed, and return the target agent's previous status before shutdown was requested. Don't keep agents open for too long if they are not needed anymore.".to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(properties, Some(vec!["target".to_string()]), Some(false.into())),
output_schema: Some(close_agent_output_schema()),
})],
})
}

Expand Down
12 changes: 9 additions & 3 deletions codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,17 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
max_concurrent_threads_per_session: None,
});

let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = tool else {
panic!("spawn_agent should be a function tool");
let ToolSpec::Namespace(namespace) = tool else {
panic!("spawn_agent v1 should be a namespace tool");
};
assert_eq!(namespace.name, MULTI_AGENT_V1_NAMESPACE);
let Some(ResponsesApiNamespaceTool::Function(ResponsesApiTool { parameters, .. })) =
namespace.tools.first()
else {
panic!("spawn_agent should be a namespace function tool");
};
assert_eq!(
parameters.schema_type,
parameters.schema_type.clone(),
Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object))
);
let properties = parameters
Expand Down
33 changes: 29 additions & 4 deletions codex-rs/core/src/tools/spec_plan_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use serde_json::json;

use crate::session::tests::make_session_and_context;
use crate::session::turn_context::TurnContext;
use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE;
use crate::tools::router::ToolRouter;
use crate::tools::router::ToolRouterParams;

Expand Down Expand Up @@ -602,14 +603,27 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
set_feature(turn, Feature::MultiAgentV2, /*enabled*/ false);
})
.await;
v1.assert_visible_contains(&[
v1.assert_visible_contains(&[MULTI_AGENT_V1_NAMESPACE]);
v1.assert_visible_lacks(&[
"spawn_agent",
"send_input",
"resume_agent",
"wait_agent",
"close_agent",
"send_message",
"followup_task",
"list_agents",
]);
v1.assert_visible_lacks(&["send_message", "followup_task", "list_agents"]);
assert_eq!(
v1.namespace_function_names(MULTI_AGENT_V1_NAMESPACE),
&[
"close_agent".to_string(),
"resume_agent".to_string(),
"send_input".to_string(),
"spawn_agent".to_string(),
"wait_agent".to_string(),
]
);

let v2 = probe(|turn| {
set_feature(turn, Feature::MultiAgentV2, /*enabled*/ true);
Expand Down Expand Up @@ -678,8 +692,19 @@ async fn v1_multi_agent_tools_defer_when_tool_search_available() {
"wait_agent",
"close_agent",
] {
plan.assert_registered_contains(&[tool_name]);
assert_eq!(plan.exposure(tool_name), ToolExposure::Deferred);
let namespaced_tool_name = ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, tool_name);
let namespaced_tool_name = namespaced_tool_name.to_string();
assert!(
plan.registered_names.contains(&namespaced_tool_name),
"expected namespaced runtime for {tool_name}"
);
assert!(
!plan
.registered_names
.contains(&ToolName::plain(tool_name).to_string()),
"expected no plain runtime for deferred {tool_name}"
);
assert_eq!(plan.exposure(&namespaced_tool_name), ToolExposure::Deferred);
}
let ToolSpec::ToolSearch { description, .. } = plan.visible_spec("tool_search") else {
panic!("expected visible tool_search spec");
Expand Down
8 changes: 7 additions & 1 deletion codex-rs/core/tests/suite/approvals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use core_test_support::responses::ev_apply_patch_custom_tool_call;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_function_call;
use core_test_support::responses::ev_function_call_with_namespace;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::mount_sse_once_match;
Expand Down Expand Up @@ -2373,7 +2374,12 @@ async fn spawned_subagent_execpolicy_amendment_propagates_to_parent_session() ->
|req: &Request| body_contains(req, PARENT_PROMPT),
sse(vec![
ev_response_created("resp-parent-1"),
ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args),
ev_function_call_with_namespace(
SPAWN_CALL_ID,
"multi_agent_v1",
"spawn_agent",
&spawn_args,
),
ev_completed("resp-parent-1"),
]),
)
Expand Down
9 changes: 7 additions & 2 deletions codex-rs/core/tests/suite/responses_api_proxy_headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use core_test_support::responses::ResponseMock;
use core_test_support::responses::ResponsesRequest;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_function_call;
use core_test_support::responses::ev_function_call_with_namespace;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once_match;
use core_test_support::responses::sse;
Expand Down Expand Up @@ -46,7 +46,12 @@ async fn responses_api_parent_and_subagent_requests_include_identity_headers() -
},
sse(vec![
ev_response_created("resp-parent-1"),
ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args),
ev_function_call_with_namespace(
SPAWN_CALL_ID,
"multi_agent_v1",
"spawn_agent",
&spawn_args,
),
ev_completed("resp-parent-1"),
]),
)
Expand Down
22 changes: 17 additions & 5 deletions codex-rs/core/tests/suite/search_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -745,13 +745,25 @@ async fn tool_search_returns_deferred_v1_multi_agent_tools() -> Result<()> {
);

let tools = tool_search_output_tools(&requests[1], call_id);
let spawn_agent = tools
.iter()
.find(|tool| {
assert!(
!tools.iter().any(|tool| {
tool.get("type").and_then(Value::as_str) == Some("function")
&& tool.get("name").and_then(Value::as_str) == Some("spawn_agent")
})
.unwrap_or_else(|| panic!("expected tool_search to return spawn_agent: {tools:?}"));
}),
"spawn_agent should be returned as a namespace child, not a flat function: {tools:?}"
);
assert!(
tools.iter().any(|tool| {
tool.get("type").and_then(Value::as_str) == Some("namespace")
&& tool.get("name").and_then(Value::as_str) == Some("multi_agent_v1")
}),
"expected tool_search to return multi_agent_v1 namespace: {tools:?}"
);
let output = tool_search_output_item(&requests[1], call_id);
let spawn_agent = namespace_child_tool(&output, "multi_agent_v1", "spawn_agent")
.unwrap_or_else(|| {
panic!("expected tool_search to return multi_agent_v1.spawn_agent: {output:?}")
});
assert_eq!(
spawn_agent.get("defer_loading").and_then(Value::as_bool),
Some(true)
Expand Down
Loading
Loading