Source proactive multi-agent instructions from the model catalog (#41457)

## What changed

- Add an optional `proactive` multi-agent mode message to model metadata.
- Use the catalog's proactive message for `Ultra` reasoning when no general mode hint is configured. A missing value falls back to the built-in proactive instructions, while an empty value suppresses the mode message.
- Keep explicit mode behavior for other reasoning efforts and refresh the applicable catalog message when the model changes.

## Testing

- Cover proactive overrides, built-in fallback, empty-value suppression, mode-hint precedence, non-`Ultra` behavior, and model switches.

GitOrigin-RevId: da0a9ebd9b58cf04ef13a703c210e5da2eed0884
This commit is contained in:
rhan-oai
2026-08-29 03:28:19 +00:00
committed by copyberry
parent 2181224dad
commit 03861e69ef
4 changed files with 99 additions and 11 deletions
+4 -1
View File
@@ -165,7 +165,10 @@ pub(crate) fn effective_multi_agent_mode(turn_context: &TurnContext) -> Option<M
let multi_agent_mode = match mode_hint_text {
Some(hint_text) => MultiAgentMode::Custom(hint_text.to_string()),
None => match turn_context.effective_reasoning_effort() {
Some(ReasoningEffort::Ultra) => MultiAgentMode::Proactive,
Some(ReasoningEffort::Ultra) => catalog_mode
.and_then(|messages| messages.proactive.clone())
.map(MultiAgentMode::Custom)
.unwrap_or(MultiAgentMode::Proactive),
_ => catalog_mode
.and_then(|messages| messages.explicit.clone())
.map(MultiAgentMode::Custom)
+89 -9
View File
@@ -31,7 +31,9 @@ const PROACTIVE_TEXT: &str = "Proactive multi-agent delegation is active.";
const CUSTOM_MODE_HINT_TEXT: &str = "Use the configured delegation policy.";
const CATALOG_MODE_HINT_TEXT: &str = "Use the model catalog delegation policy.";
const CATALOG_EXPLICIT_TEXT: &str = "Use explicit delegation from the model catalog.";
const CATALOG_PROACTIVE_TEXT: &str = "Use proactive delegation from the model catalog.";
const SECOND_MODEL_EXPLICIT_TEXT: &str = "Second model explicit mode.";
const SECOND_MODEL_PROACTIVE_TEXT: &str = "Second model proactive mode.";
const FIRST_MODEL_ROOT_ROLE_TEXT: &str = "First model root role.";
const SECOND_MODEL_ROOT_ROLE_TEXT: &str = "Second model root role.";
const ROOT_USAGE_HINT_TEXT: &str = "Root usage hint.";
@@ -45,6 +47,7 @@ enum ModeHintSource {
fn set_multi_agent_mode(
model_info: &mut ModelInfo,
explicit: &str,
proactive: Option<&str>,
hint_text: Option<&str>,
root_role: Option<&str>,
) {
@@ -56,6 +59,7 @@ fn set_multi_agent_mode(
}),
mode: Some(MultiAgentModeMessages {
explicit: Some(explicit.to_string()),
proactive: proactive.map(str::to_string),
hint_text: hint_text.map(str::to_string),
}),
});
@@ -185,6 +189,7 @@ async fn mode_hints_override_reasoning_effort(source: ModeHintSource) -> Result<
set_multi_agent_mode(
model_info,
CATALOG_EXPLICIT_TEXT,
Some(CATALOG_PROACTIVE_TEXT),
Some(CATALOG_MODE_HINT_TEXT),
/*root_role*/ None,
);
@@ -213,8 +218,9 @@ async fn mode_hints_override_reasoning_effort(source: ModeHintSource) -> Result<
count_containing(texts, expected_hint),
count_containing(texts, NO_SPAWN_TEXT),
count_containing(texts, PROACTIVE_TEXT),
count_containing(texts, CATALOG_PROACTIVE_TEXT),
),
(1, 0, 0)
(1, 0, 0, 0)
);
assert_eq!(count_containing(texts, suppressed_hint), 0);
}
@@ -222,8 +228,66 @@ async fn mode_hints_override_reasoning_effort(source: ModeHintSource) -> Result<
Ok(())
}
#[test_case(ReasoningEffort::Ultra, Some(CATALOG_PROACTIVE_TEXT), Some(CATALOG_PROACTIVE_TEXT); "ultra uses proactive override")]
#[test_case(ReasoningEffort::High, Some(CATALOG_PROACTIVE_TEXT), Some(CATALOG_EXPLICIT_TEXT); "non ultra ignores proactive override")]
#[test_case(ReasoningEffort::Ultra, None, Some(PROACTIVE_TEXT); "ultra falls back to built in")]
#[test_case(ReasoningEffort::Ultra, Some(""), None; "empty proactive suppresses ultra mode")]
#[test_case(ReasoningEffort::High, Some(""), Some(CATALOG_EXPLICIT_TEXT); "empty proactive leaves non ultra unchanged")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_switch_refreshes_catalog_role_and_explicit_mode() -> Result<()> {
async fn catalog_proactive_mode_is_ultra_only(
effort: ReasoningEffort,
proactive: Option<&'static str>,
expected_hint: Option<&str>,
) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let response = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let test = test_codex()
.with_model_info_override("gpt-5.4", move |model_info| {
add_ultra_reasoning(model_info);
set_multi_agent_mode(
model_info,
CATALOG_EXPLICIT_TEXT,
proactive,
/*hint_text*/ None,
/*root_role*/ None,
);
})
.with_config(configure_multi_agent_v2)
.build_with_auto_env(&server)
.await?;
submit_turn(&test.codex, "hello", Some(effort)).await?;
let input = response.single_request().input();
let texts = developer_texts(&input);
assert_eq!(
count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG),
usize::from(expected_hint.is_some())
);
if let Some(expected_hint) = expected_hint {
assert_eq!(count_containing(&texts, expected_hint), 1);
}
assert_eq!(
count_containing(&texts, CATALOG_PROACTIVE_TEXT),
usize::from(expected_hint == Some(CATALOG_PROACTIVE_TEXT))
);
Ok(())
}
#[test_case(ReasoningEffort::High, [CATALOG_EXPLICIT_TEXT, SECOND_MODEL_EXPLICIT_TEXT]; "explicit mode")]
#[test_case(ReasoningEffort::Ultra, [CATALOG_PROACTIVE_TEXT, SECOND_MODEL_PROACTIVE_TEXT]; "proactive mode")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_switch_refreshes_catalog_role_and_mode(
effort: ReasoningEffort,
expected_hints: [&str; 2],
) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -241,17 +305,21 @@ async fn model_switch_refreshes_catalog_role_and_explicit_mode() -> Result<()> {
.await;
let test = test_codex()
.with_model_info_override("gpt-5.2", |model_info| {
add_ultra_reasoning(model_info);
set_multi_agent_mode(
model_info,
SECOND_MODEL_EXPLICIT_TEXT,
Some(SECOND_MODEL_PROACTIVE_TEXT),
/*hint_text*/ None,
Some(SECOND_MODEL_ROOT_ROLE_TEXT),
);
})
.with_model_info_override("gpt-5.4", |model_info| {
add_ultra_reasoning(model_info);
set_multi_agent_mode(
model_info,
CATALOG_EXPLICIT_TEXT,
Some(CATALOG_PROACTIVE_TEXT),
/*hint_text*/ None,
Some(FIRST_MODEL_ROOT_ROLE_TEXT),
);
@@ -259,7 +327,7 @@ async fn model_switch_refreshes_catalog_role_and_explicit_mode() -> Result<()> {
.with_config(configure_multi_agent_v2)
.build_with_auto_env(&server)
.await?;
submit_turn(&test.codex, "first model", Some(ReasoningEffort::High)).await?;
submit_turn(&test.codex, "first model", Some(effort.clone())).await?;
core_test_support::submit_thread_settings(
&test.codex,
ThreadSettingsOverrides {
@@ -268,7 +336,7 @@ async fn model_switch_refreshes_catalog_role_and_explicit_mode() -> Result<()> {
},
)
.await?;
submit_turn(&test.codex, "second model", Some(ReasoningEffort::High)).await?;
submit_turn(&test.codex, "second model", Some(effort)).await?;
let requests = responses.requests();
for (index, request) in requests.iter().enumerate() {
@@ -276,8 +344,8 @@ async fn model_switch_refreshes_catalog_role_and_explicit_mode() -> Result<()> {
let texts = developer_texts(&input);
assert_eq!(
(
count_containing(&texts, CATALOG_EXPLICIT_TEXT),
count_containing(&texts, SECOND_MODEL_EXPLICIT_TEXT),
count_containing(&texts, expected_hints[0]),
count_containing(&texts, expected_hints[1]),
count_containing(&texts, NO_SPAWN_TEXT),
count_containing(&texts, PROACTIVE_TEXT),
),
@@ -316,8 +384,10 @@ async fn model_switch_refreshes_catalog_role_and_explicit_mode() -> Result<()> {
Ok(())
}
#[test_case(ReasoningEffort::High; "explicit mode")]
#[test_case(ReasoningEffort::Ultra; "proactive mode")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn empty_configured_mode_hint_emits_no_mode_message() -> Result<()> {
async fn empty_configured_mode_hint_emits_no_mode_message(effort: ReasoningEffort) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -327,14 +397,24 @@ async fn empty_configured_mode_hint_emits_no_mode_message() -> Result<()> {
)
.await;
let test = test_codex()
.with_model_info_override("gpt-5.4", |model_info| {
add_ultra_reasoning(model_info);
set_multi_agent_mode(
model_info,
CATALOG_EXPLICIT_TEXT,
Some(CATALOG_PROACTIVE_TEXT),
Some(CATALOG_MODE_HINT_TEXT),
/*root_role*/ None,
);
})
.with_config(|config| {
configure_multi_agent_v2(config);
config.multi_agent_v2.multi_agent_mode_hint_text = Some(String::new());
})
.build(&server)
.build_with_auto_env(&server)
.await?;
submit_turn(&test.codex, "hello", Some(ReasoningEffort::High)).await?;
submit_turn(&test.codex, "hello", Some(effort)).await?;
let input = response.single_request().input();
let texts = developer_texts(&input);
@@ -54,6 +54,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
}),
mode: Some(MultiAgentModeMessages {
explicit: Some("explicit mode".to_string()),
proactive: Some("proactive mode".to_string()),
hint_text: Some("mode hint".to_string()),
}),
};
+5 -1
View File
@@ -627,6 +627,9 @@ pub struct MultiAgentRoleMessages {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct MultiAgentModeMessages {
pub explicit: Option<String>,
/// Ultra-only mode instructions. Missing or null uses the built-in proactive hint;
/// an empty string suppresses the mode message. `hint_text` takes precedence.
pub proactive: Option<String>,
pub hint_text: Option<String>,
}
@@ -1110,7 +1113,7 @@ mod tests {
#[test]
fn multi_agent_messages_preserve_missing_and_empty_values() {
let messages: ModelMessages = from_str(
r#"{"instructions_template":null,"instructions_variables":null,"multi_agent":{"role":{"root":"","subagent":"subagent base"},"mode":{"explicit":"explicit mode","hint_text":""}}}"#,
r#"{"instructions_template":null,"instructions_variables":null,"multi_agent":{"role":{"root":"","subagent":"subagent base"},"mode":{"explicit":"explicit mode","proactive":"","hint_text":""}}}"#,
)
.expect("multi-agent messages should deserialize");
@@ -1123,6 +1126,7 @@ mod tests {
}),
mode: Some(MultiAgentModeMessages {
explicit: Some("explicit mode".to_string()),
proactive: Some(String::new()),
hint_text: Some(String::new()),
}),
})