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 authored and copyberry committed 2026-08-29 03:28:19 +00:00
1 parent 2181224dad
commit 03861e69ef
4 files changed
+99 -11

No files matched your search

+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 { let multi_agent_mode = match mode_hint_text {
Some(hint_text) => MultiAgentMode::Custom(hint_text.to_string()), Some(hint_text) => MultiAgentMode::Custom(hint_text.to_string()),
None => match turn_context.effective_reasoning_effort() { 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 _ => catalog_mode
.and_then(|messages| messages.explicit.clone()) .and_then(|messages| messages.explicit.clone())
.map(MultiAgentMode::Custom) .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 CUSTOM_MODE_HINT_TEXT: &str = "Use the configured delegation policy.";
const CATALOG_MODE_HINT_TEXT: &str = "Use the model catalog 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_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_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 FIRST_MODEL_ROOT_ROLE_TEXT: &str = "First model root role.";
const SECOND_MODEL_ROOT_ROLE_TEXT: &str = "Second model root role."; const SECOND_MODEL_ROOT_ROLE_TEXT: &str = "Second model root role.";
const ROOT_USAGE_HINT_TEXT: &str = "Root usage hint."; const ROOT_USAGE_HINT_TEXT: &str = "Root usage hint.";
@@ -45,6 +47,7 @@ enum ModeHintSource {
fn set_multi_agent_mode( fn set_multi_agent_mode(
model_info: &mut ModelInfo, model_info: &mut ModelInfo,
explicit: &str, explicit: &str,
proactive: Option<&str>,
hint_text: Option<&str>, hint_text: Option<&str>,
root_role: Option<&str>, root_role: Option<&str>,
) { ) {
@@ -56,6 +59,7 @@ fn set_multi_agent_mode(
}), }),
mode: Some(MultiAgentModeMessages { mode: Some(MultiAgentModeMessages {
explicit: Some(explicit.to_string()), explicit: Some(explicit.to_string()),
proactive: proactive.map(str::to_string),
hint_text: hint_text.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( set_multi_agent_mode(
model_info, model_info,
CATALOG_EXPLICIT_TEXT, CATALOG_EXPLICIT_TEXT,
Some(CATALOG_PROACTIVE_TEXT),
Some(CATALOG_MODE_HINT_TEXT), Some(CATALOG_MODE_HINT_TEXT),
/*root_role*/ None, /*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, expected_hint),
count_containing(texts, NO_SPAWN_TEXT), count_containing(texts, NO_SPAWN_TEXT),
count_containing(texts, PROACTIVE_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); assert_eq!(count_containing(texts, suppressed_hint), 0);
} }
@@ -222,8 +228,66 @@ async fn mode_hints_override_reasoning_effort(source: ModeHintSource) -> Result<
Ok(()) 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)] #[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(())); skip_if_no_network!(Ok(()));
let server = start_mock_server().await; let server = start_mock_server().await;
@@ -241,17 +305,21 @@ async fn model_switch_refreshes_catalog_role_and_explicit_mode() -> Result<()> {
.await; .await;
let test = test_codex() let test = test_codex()
.with_model_info_override("gpt-5.2", |model_info| { .with_model_info_override("gpt-5.2", |model_info| {
add_ultra_reasoning(model_info);
set_multi_agent_mode( set_multi_agent_mode(
model_info, model_info,
SECOND_MODEL_EXPLICIT_TEXT, SECOND_MODEL_EXPLICIT_TEXT,
Some(SECOND_MODEL_PROACTIVE_TEXT),
/*hint_text*/ None, /*hint_text*/ None,
Some(SECOND_MODEL_ROOT_ROLE_TEXT), Some(SECOND_MODEL_ROOT_ROLE_TEXT),
); );
}) })
.with_model_info_override("gpt-5.4", |model_info| { .with_model_info_override("gpt-5.4", |model_info| {
add_ultra_reasoning(model_info);
set_multi_agent_mode( set_multi_agent_mode(
model_info, model_info,
CATALOG_EXPLICIT_TEXT, CATALOG_EXPLICIT_TEXT,
Some(CATALOG_PROACTIVE_TEXT),
/*hint_text*/ None, /*hint_text*/ None,
Some(FIRST_MODEL_ROOT_ROLE_TEXT), 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) .with_config(configure_multi_agent_v2)
.build_with_auto_env(&server) .build_with_auto_env(&server)
.await?; .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( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
ThreadSettingsOverrides { ThreadSettingsOverrides {
@@ -268,7 +336,7 @@ async fn model_switch_refreshes_catalog_role_and_explicit_mode() -> Result<()> {
}, },
) )
.await?; .await?;
submit_turn(&test.codex, "second model", Some(ReasoningEffort::High)).await?; submit_turn(&test.codex, "second model", Some(effort)).await?;
let requests = responses.requests(); let requests = responses.requests();
for (index, request) in requests.iter().enumerate() { 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); let texts = developer_texts(&input);
assert_eq!( assert_eq!(
( (
count_containing(&texts, CATALOG_EXPLICIT_TEXT), count_containing(&texts, expected_hints[0]),
count_containing(&texts, SECOND_MODEL_EXPLICIT_TEXT), count_containing(&texts, expected_hints[1]),
count_containing(&texts, NO_SPAWN_TEXT), count_containing(&texts, NO_SPAWN_TEXT),
count_containing(&texts, PROACTIVE_TEXT), count_containing(&texts, PROACTIVE_TEXT),
), ),
@@ -316,8 +384,10 @@ async fn model_switch_refreshes_catalog_role_and_explicit_mode() -> Result<()> {
Ok(()) Ok(())
} }
#[test_case(ReasoningEffort::High; "explicit mode")]
#[test_case(ReasoningEffort::Ultra; "proactive mode")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[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(())); skip_if_no_network!(Ok(()));
let server = start_mock_server().await; let server = start_mock_server().await;
@@ -327,14 +397,24 @@ async fn empty_configured_mode_hint_emits_no_mode_message() -> Result<()> {
) )
.await; .await;
let test = test_codex() 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| { .with_config(|config| {
configure_multi_agent_v2(config); configure_multi_agent_v2(config);
config.multi_agent_v2.multi_agent_mode_hint_text = Some(String::new()); config.multi_agent_v2.multi_agent_mode_hint_text = Some(String::new());
}) })
.build(&server) .build_with_auto_env(&server)
.await?; .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 input = response.single_request().input();
let texts = developer_texts(&input); let texts = developer_texts(&input);
@@ -54,6 +54,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
}), }),
mode: Some(MultiAgentModeMessages { mode: Some(MultiAgentModeMessages {
explicit: Some("explicit mode".to_string()), explicit: Some("explicit mode".to_string()),
proactive: Some("proactive mode".to_string()),
hint_text: Some("mode hint".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)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct MultiAgentModeMessages { pub struct MultiAgentModeMessages {
pub explicit: Option<String>, 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>, pub hint_text: Option<String>,
} }
@@ -1110,7 +1113,7 @@ mod tests {
#[test] #[test]
fn multi_agent_messages_preserve_missing_and_empty_values() { fn multi_agent_messages_preserve_missing_and_empty_values() {
let messages: ModelMessages = from_str( 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"); .expect("multi-agent messages should deserialize");
@@ -1123,6 +1126,7 @@ mod tests {
}), }),
mode: Some(MultiAgentModeMessages { mode: Some(MultiAgentModeMessages {
explicit: Some("explicit mode".to_string()), explicit: Some("explicit mode".to_string()),
proactive: Some(String::new()),
hint_text: Some(String::new()), hint_text: Some(String::new()),
}), }),
}) })