Namespace multi-agent v2 tools under collaboration (#29067)

## Summary

Multi-agent v2 tools now use the fixed `collaboration` namespace when
namespace tools are available. This keeps the model-visible hint and the
actual tool surface aligned around `functions.collaboration.*`, without
exposing an unshipped namespace knob to users.

The PR also removes the old `features.multi_agent_v2.tool_namespace`
config/schema surface, updates the MAv2 test fixtures for namespaced
calls, and fixes stale `TurnContext.features` references that were
breaking `codex-core` builds.

## Changes

- Expose MAv2 tools under `collaboration` instead of relying on a
configurable namespace.
- Remove `tool_namespace` from MAv2 TOML config, resolved config,
validation, schema, and tests.
- Update tool-planning and integration fixtures to assert or emit
namespaced MAv2 tool calls.
- Read feature state through `TurnContext.config.features` in the
multi-agent mode context paths.

## Testing

- `just write-config-schema`
- `just test -p codex-features`
This commit is contained in:
jif
2026-06-23 13:15:20 +01:00
committed by GitHub
Unverified
parent 55bc38a22b
commit 49614a0391
8 changed files with 138 additions and 39 deletions
+3 -2
View File
@@ -246,7 +246,8 @@ Payload:
```
You may also see them addressed as to=/root/..., which indicates your identity is /root/...
"#;
const DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT: &str = r#"Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_message`, `followup_task`, `wait_agent`, `interrupt_agent`, and `list_agents` only as direct tool calls using the recipient shown in their tool definitions, such as `to=functions.spawn_agent` without a configured namespace or `to=functions.agents.spawn_agent` with `tool_namespace = "agents"`, since they are intentionally absent from the `functions.exec` `tools.*` namespace. Available tools in `functions.exec` are explicitly described with a `tools` namespace in the developer message.
const DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE: &str = "collaboration";
const DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT: &str = r#"Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_message`, `followup_task`, `wait_agent`, `interrupt_agent`, and `list_agents` only as direct tool calls using the recipient shown in their tool definitions, such as `to=functions.collaboration.spawn_agent`, since they are intentionally absent from the `functions.exec` `tools.*` namespace. Available tools in `functions.exec` are explicitly described with a `tools` namespace in the developer message.
All agents share the same directory. In detail:
- All agents have access to the same container and filesystem as you.
@@ -1157,7 +1158,7 @@ impl MultiAgentV2Config {
DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT,
max_concurrent_threads_per_session,
)),
tool_namespace: None,
tool_namespace: Some(DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE.to_string()),
hide_spawn_agent_metadata: true,
non_code_mode_only: true,
}
+51 -15
View File
@@ -39,6 +39,8 @@ use crate::tools::router::ToolRouterParams;
use crate::tools::router::ToolSuggestCandidates;
use crate::tools::router::ToolSuggestPresentation;
const MULTI_AGENT_V2_NAMESPACE: &str = "collaboration";
#[derive(Default)]
struct ToolPlanInputs {
mcp_tools: Option<Vec<ToolInfo>>,
@@ -1151,19 +1153,48 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
});
})
.await;
v2.assert_visible_contains(&[
v2.assert_visible_contains(&[MULTI_AGENT_V2_NAMESPACE]);
v2.assert_visible_lacks(&[
"spawn_agent",
"send_message",
"followup_task",
"wait_agent",
"interrupt_agent",
"list_agents",
"send_input",
"resume_agent",
"assign_task",
"close_agent",
]);
v2.assert_visible_lacks(&["send_input", "resume_agent", "assign_task", "close_agent"]);
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:?}"),
for tool_name in [
"spawn_agent",
"send_message",
"followup_task",
"wait_agent",
"interrupt_agent",
"list_agents",
] {
assert!(
v2.namespace_function_names(MULTI_AGENT_V2_NAMESPACE)
.iter()
.any(|name| name == tool_name),
"expected {tool_name} in {MULTI_AGENT_V2_NAMESPACE} namespace"
);
}
let ToolSpec::Namespace(namespace) = v2.visible_spec(MULTI_AGENT_V2_NAMESPACE) else {
panic!("expected {MULTI_AGENT_V2_NAMESPACE} namespace");
};
let Some(ResponsesApiNamespaceTool::Function(spawn_agent)) =
namespace.tools.iter().find(|tool| {
matches!(
tool,
ResponsesApiNamespaceTool::Function(tool) if tool.name == "spawn_agent"
)
})
else {
panic!("expected spawn_agent in {MULTI_AGENT_V2_NAMESPACE} namespace");
};
let spawn_agent_description = spawn_agent.description.as_str();
assert!(!spawn_agent_description.contains("max_concurrent_threads_per_session"));
assert!(spawn_agent_description.contains(
"Note that passing `fork_turns=\"none\"` will not pass any surrounding context to the spawned subagent"
@@ -1183,9 +1214,11 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
});
})
.await;
direct_model_only.assert_visible_contains(&["spawn_agent", "send_message", "wait_agent"]);
direct_model_only.assert_visible_contains(&[MULTI_AGENT_V2_NAMESPACE]);
direct_model_only.assert_visible_lacks(&["spawn_agent", "send_message", "wait_agent"]);
assert_eq!(
direct_model_only.exposure("spawn_agent"),
direct_model_only
.exposure(&ToolName::namespaced(MULTI_AGENT_V2_NAMESPACE, "spawn_agent").to_string()),
ToolExposure::DirectModelOnly
);
}
@@ -1196,9 +1229,17 @@ async fn multi_agent_v2_message_schemas_are_encrypted() {
set_feature(turn, Feature::MultiAgentV2, /*enabled*/ true);
})
.await;
let ToolSpec::Namespace(namespace) = plan.visible_spec(MULTI_AGENT_V2_NAMESPACE) else {
panic!("expected {MULTI_AGENT_V2_NAMESPACE} namespace");
};
for tool_name in ["spawn_agent", "send_message", "followup_task"] {
let ToolSpec::Function(tool) = plan.visible_spec(tool_name) else {
panic!("expected {tool_name} function spec");
let Some(ResponsesApiNamespaceTool::Function(tool)) = namespace.tools.iter().find(|tool| {
matches!(
tool,
ResponsesApiNamespaceTool::Function(tool) if tool.name == tool_name
)
}) else {
panic!("expected {tool_name} in {MULTI_AGENT_V2_NAMESPACE} namespace");
};
let properties = tool
.parameters
@@ -1464,12 +1505,7 @@ async fn hosted_tools_follow_provider_auth_model_and_config_gates() {
codex_code_mode::WAIT_TOOL_NAME,
"request_user_input",
// Multi-agent v2 tools.
"spawn_agent",
"send_message",
"followup_task",
"wait_agent",
"interrupt_agent",
"list_agents",
MULTI_AGENT_V2_NAMESPACE,
// Hosted Responses tools.
"web_search",
"image_generation",