mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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()),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -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()),
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user