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

## What changed

- Add model-catalog messages for root and subagent roles, explicit delegation, and delegation hints.
- Resolve role instructions in config, catalog, then bundled-default order, while preserving empty values as an explicit way to suppress fallback text.
- Refresh catalog-provided role and mode instructions when the model changes, and give full-history forks the selected child model's subagent role without retaining the parent's role guidance.
- Keep existing config overrides and reasoning-effort behavior, including proactive delegation for ultra reasoning effort.

## Testing

- Cover message deserialization and preservation through model overrides.
- Cover precedence, empty overrides, model switches, resumed sessions, and full-history subagent forks.

GitOrigin-RevId: 4625cf7c6a5490176adddfaa0fb99100707daea9
This commit is contained in:
rhan-oai authored and copyberry committed 2026-08-14 18:26:20 +00:00
1 parent 14a6813fa8
commit 395723b238
30 files changed
+734 -231

No files matched your search

@@ -40,6 +40,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}), }),
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
+2
View File
@@ -15,6 +15,7 @@ use crate::config::RolloutBudgetConfig;
use crate::environment_selection::TurnEnvironmentSnapshot; use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::rollout_budget::RolloutBudget; use crate::rollout_budget::RolloutBudget;
use crate::session::emit_subagent_session_started; use crate::session::emit_subagent_session_started;
use crate::session::multi_agents::ResolvedMultiAgentV2UsageHints;
use crate::session_prefix::format_inter_agent_completion_message; use crate::session_prefix::format_inter_agent_completion_message;
use crate::session_prefix::format_subagent_context_line; use crate::session_prefix::format_subagent_context_line;
use crate::session_prefix::format_subagent_notification_message; use crate::session_prefix::format_subagent_notification_message;
@@ -79,6 +80,7 @@ pub(crate) struct SpawnAgentOptions {
pub(crate) parent_turn_id: Option<String>, pub(crate) parent_turn_id: Option<String>,
pub(crate) root_turn_id: Option<String>, pub(crate) root_turn_id: Option<String>,
pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>, pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,
pub(crate) multi_agent_v2_usage_hints: Option<ResolvedMultiAgentV2UsageHints>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
+26 -22
View File
@@ -4,6 +4,8 @@ use crate::agent::role::apply_role_to_config_for_multi_agent_v2;
use crate::config::PermissionProfileSnapshot; use crate::config::PermissionProfileSnapshot;
use crate::context::ContextualUserFragment; use crate::context::ContextualUserFragment;
use crate::context::CurrentTimeReminder; use crate::context::CurrentTimeReminder;
use crate::context::MultiAgentRoleInstructions;
use crate::session::multi_agents::resolve_usage_hints;
use codex_extension_api::ExtensionDataInit; use codex_extension_api::ExtensionDataInit;
const AGENT_NAMES: &str = include_str!("../agent_names.txt"); const AGENT_NAMES: &str = include_str!("../agent_names.txt");
@@ -94,7 +96,8 @@ fn is_fork_excluded_developer_message(item: &ResponseItem, usage_hint_texts: &[S
return false; return false;
}; };
CurrentTimeReminder::matches_text(text) MultiAgentRoleInstructions::matches_text(text)
|| CurrentTimeReminder::matches_text(text)
|| usage_hint_texts || usage_hint_texts
.iter() .iter()
.any(|usage_hint_text| usage_hint_text == text) .any(|usage_hint_text| usage_hint_text == text)
@@ -684,19 +687,13 @@ impl AgentControl {
let multi_agent_v2_usage_hint_texts_to_filter: Vec<String> = let multi_agent_v2_usage_hint_texts_to_filter: Vec<String> =
if multi_agent_version == MultiAgentVersion::V2 { if multi_agent_version == MultiAgentVersion::V2 {
let parent_config = parent_thread.session.get_config().await; let parent_config = parent_thread.session.get_config().await;
[ let parent_usage_hints =
parent_config resolve_usage_hints(&parent_config.multi_agent_v2, /*catalog*/ None);
.multi_agent_v2 [parent_usage_hints.root, parent_usage_hints.subagent]
.root_agent_usage_hint_text .into_iter()
.clone(), .flatten()
parent_config .map(|instructions| instructions.render())
.multi_agent_v2 .collect()
.subagent_usage_hint_text
.clone(),
]
.into_iter()
.flatten()
.collect()
} else { } else {
Vec::new() Vec::new()
}; };
@@ -792,12 +789,17 @@ impl AgentControl {
} }
true true
} }
RolloutItem::WorldState(world_state) => {
if multi_agent_version == MultiAgentVersion::V2 {
world_state.state.remove("multi_agent_usage_hint");
}
true
}
RolloutItem::EventMsg(_) RolloutItem::EventMsg(_)
| RolloutItem::SessionMeta(_) | RolloutItem::SessionMeta(_)
| RolloutItem::TurnContext(_) | RolloutItem::TurnContext(_)
| RolloutItem::InterAgentCommunication(_) | RolloutItem::InterAgentCommunication(_)
| RolloutItem::InterAgentCommunicationMetadata { .. } | RolloutItem::InterAgentCommunicationMetadata { .. } => true,
| RolloutItem::WorldState(_) => true,
RolloutItem::SecurityRiskScore(_) => false, RolloutItem::SecurityRiskScore(_) => false,
} }
}); });
@@ -822,13 +824,15 @@ impl AgentControl {
} }
if preserve_reference_context_item if preserve_reference_context_item
&& multi_agent_version == MultiAgentVersion::V2 && multi_agent_version == MultiAgentVersion::V2
&& let Some(subagent_usage_hint_text) = && let Some(subagent_usage_hint) = options
config.multi_agent_v2.subagent_usage_hint_text.clone() .multi_agent_v2_usage_hints
&& let Some(subagent_usage_hint_message) = .as_ref()
crate::context_manager::updates::build_developer_update_item(vec![ .map(|hints| hints.subagent.clone())
subagent_usage_hint_text, .unwrap_or_else(|| {
]) resolve_usage_hints(&config.multi_agent_v2, /*catalog*/ None).subagent
})
{ {
let subagent_usage_hint_message = ContextualUserFragment::into(subagent_usage_hint);
forked_rollout_items.push(RolloutItem::ResponseItem( forked_rollout_items.push(RolloutItem::ResponseItem(
subagent_usage_hint_message.into(), subagent_usage_hint_message.into(),
)); ));
+25 -7
View File
@@ -9,6 +9,7 @@ use crate::config::AgentRoleConfig;
use crate::config::Config; use crate::config::Config;
use crate::config::ConfigBuilder; use crate::config::ConfigBuilder;
use crate::context::ContextualUserFragment; use crate::context::ContextualUserFragment;
use crate::context::MultiAgentRoleInstructions;
use crate::context::SubagentNotification; use crate::context::SubagentNotification;
use crate::init_state_db; use crate::init_state_db;
use crate::thread_manager::StartThreadOptions; use crate::thread_manager::StartThreadOptions;
@@ -1509,7 +1510,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
no_hint_child_config no_hint_child_config
.multi_agent_v2 .multi_agent_v2
.subagent_developer_instructions = Some(String::new()); .subagent_developer_instructions = Some(String::new());
no_hint_child_config.multi_agent_v2.subagent_usage_hint_text = None; no_hint_child_config.multi_agent_v2.subagent_usage_hint_text = Some(String::new());
let no_hint_child_thread_id = harness let no_hint_child_thread_id = harness
.control .control
.spawn_agent_with_metadata( .spawn_agent_with_metadata(
@@ -1538,8 +1539,12 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
.expect("no-hint child thread should be registered"); .expect("no-hint child thread should be registered");
let no_hint_history = no_hint_child_thread.session.clone_history().await; let no_hint_history = no_hint_child_thread.session.clone_history().await;
assert!( assert!(
!history_contains_text(no_hint_history.raw_items(), "Child subagent guidance."), !history_contains_text(no_hint_history.raw_items(), "Child subagent guidance.")
"full-history forked child should not add empty subagent guidance" && !history_contains_text(
no_hint_history.raw_items(),
"You are an agent in a team of agents"
),
"full-history forked child should not add configured or bundled subagent guidance"
); );
assert!( assert!(
!history_contains_text( !history_contains_text(
@@ -1623,6 +1628,9 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
phase: None, phase: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ContextualUserFragment::into(MultiAgentRoleInstructions::catalog(
"Catalog parent root guidance.",
)),
parent_task.to_model_input_item(), parent_task.to_model_input_item(),
ResponseItem::Message { ResponseItem::Message {
id: None, id: None,
@@ -1691,6 +1699,12 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
SpawnAgentOptions { SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id), fork_parent_spawn_call_id: Some(parent_spawn_call_id),
fork_mode: Some(SpawnAgentForkMode::FullHistory), fork_mode: Some(SpawnAgentForkMode::FullHistory),
multi_agent_v2_usage_hints: Some(ResolvedMultiAgentV2UsageHints {
root: None,
subagent: Some(MultiAgentRoleInstructions::catalog(
"Catalog child subagent guidance.",
)),
}),
..Default::default() ..Default::default()
}, },
) )
@@ -1708,6 +1722,14 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
history_contains_text(history.raw_items(), "compacted parent summary"), history_contains_text(history.raw_items(), "compacted parent summary"),
"forked child history should retain compacted non-hint content" "forked child history should retain compacted non-hint content"
); );
assert!(
!history_contains_text(history.raw_items(), "Catalog parent root guidance."),
"forked child history should strip the resolved parent hint from compacted replacement history"
);
assert!(
history_contains_text(history.raw_items(), "Catalog child subagent guidance."),
"full-history forked child should add the resolved child hint after compacted-history sanitization"
);
assert!( assert!(
!history !history
.raw_items() .raw_items()
@@ -1736,10 +1758,6 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
), ),
"forked child history should preserve unrelated compacted developer fragments" "forked child history should preserve unrelated compacted developer fragments"
); );
assert!(
history_contains_text(history.raw_items(), "Child subagent guidance."),
"full-history forked child should add the child subagent hint after compacted-history sanitization"
);
let _ = harness let _ = harness
.control .control
+91 -24
View File
@@ -1,7 +1,9 @@
use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEdit;
use crate::config::edit::ConfigEditsBuilder; use crate::config::edit::ConfigEditsBuilder;
use crate::config::edit::apply_blocking; use crate::config::edit::apply_blocking;
use crate::context::ContextualUserFragment;
use crate::plugins::plugins_manager_for_config; use crate::plugins::plugins_manager_for_config;
use crate::session::multi_agents::resolve_usage_hints;
use assert_matches::assert_matches; use assert_matches::assert_matches;
use codex_config::CONFIG_TOML_FILE; use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerEntry; use codex_config::ConfigLayerEntry;
@@ -87,6 +89,7 @@ use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement; use codex_protocol::models::SandboxEnforcement;
use codex_protocol::openai_models::MultiAgentRoleMessages;
use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -10991,17 +10994,29 @@ max_concurrent_threads_per_session = 17
let config = resolve_multi_agent_v2_config(&config_toml); let config = resolve_multi_agent_v2_config(&config_toml);
let concurrency_guidance = "There are 17 available concurrency slots, meaning that up to 17 agents can be active at once, including you."; let concurrency_guidance = "There are 17 available concurrency slots, meaning that up to 17 agents can be active at once, including you.";
let expected_suffix = format!( assert!(config.wait_agent_enabled);
"{DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT}\n{DEFAULT_MULTI_AGENT_V2_WAIT_AGENT_USAGE_HINT_TEXT}\n\n{concurrency_guidance}" for wait_agent_enabled in [true, false] {
); let mut config = config.clone();
assert!( config.wait_agent_enabled = wait_agent_enabled;
[ let usage_hints = resolve_usage_hints(&config, /*catalog*/ None);
config.root_agent_usage_hint_text, for hint in [usage_hints.root, usage_hints.subagent] {
config.subagent_usage_hint_text, let hint = hint.expect("default usage hints should be present").body();
] assert!(hint.contains(concurrency_guidance));
.into_iter() assert_eq!(
.all(|hint| hint.is_some_and(|hint| hint.contains(expected_suffix.as_str()))) hint.contains("When calling `wait_agent`, prefer longer waits"),
wait_agent_enabled
);
}
}
let usage_hints = resolve_usage_hints(
&config,
Some(&MultiAgentRoleMessages {
root: Some(String::new()),
subagent: Some(String::new()),
}),
); );
assert!(usage_hints.root.is_none() && usage_hints.subagent.is_none());
} }
#[test] #[test]
@@ -11026,6 +11041,23 @@ expose_spawn_agent_model_overrides = true
config.subagent_usage_hint_text.as_deref(), config.subagent_usage_hint_text.as_deref(),
Some("Subagent guidance.") Some("Subagent guidance.")
); );
let usage_hints = resolve_usage_hints(
&config,
Some(&MultiAgentRoleMessages {
root: Some("Catalog root base.".to_string()),
subagent: Some("Catalog subagent base.".to_string()),
}),
);
assert_eq!(
(
usage_hints.root.map(|hint| hint.body()),
usage_hints.subagent.map(|hint| hint.body()),
),
(
Some("Root guidance.".to_string()),
Some("Subagent guidance.".to_string()),
)
);
} }
#[test] #[test]
@@ -11033,18 +11065,39 @@ fn multi_agent_v2_exposes_model_overrides_by_default() {
let config_toml = let config_toml =
toml::from_str(r#"[features.multi_agent_v2]"#).expect("multi-agent v2 config should parse"); toml::from_str(r#"[features.multi_agent_v2]"#).expect("multi-agent v2 config should parse");
let config = resolve_multi_agent_v2_config(&config_toml); let mut config = resolve_multi_agent_v2_config(&config_toml);
assert!(config.expose_spawn_agent_model_overrides); assert!(config.expose_spawn_agent_model_overrides);
assert!( let usage_hints = resolve_usage_hints(&config, /*catalog*/ None);
[ config.expose_spawn_agent_model_overrides = false;
config.root_agent_usage_hint_text, let usage_hints_without_model_overrides = resolve_usage_hints(&config, /*catalog*/ None);
config.subagent_usage_hint_text,
] for (hint, hint_without_model_overrides) in [
.into_iter() (usage_hints.root, usage_hints_without_model_overrides.root),
.all(|hint| hint.is_some_and(|hint| { (
hint.ends_with(DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT) usage_hints.subagent,
})) usage_hints_without_model_overrides.subagent,
); ),
] {
let hint = hint.expect("default usage hints should be present").body();
let hint_without_model_overrides = hint_without_model_overrides
.expect("default usage hints should be present without model overrides")
.body();
let model_override_guidance = hint
.strip_prefix(hint_without_model_overrides.as_str())
.expect("model-override guidance should extend the base usage hint");
for required_fragment in [
"Full-history forks",
"`fork_turns`",
"`model`",
"`reasoning_effort`",
] {
assert!(
model_override_guidance.contains(required_fragment),
"model-override guidance should contain {required_fragment}"
);
}
}
} }
#[tokio::test] #[tokio::test]
@@ -11108,7 +11161,7 @@ subagent_developer_instructions = " \t "
} }
#[tokio::test] #[tokio::test]
async fn multi_agent_v2_empty_usage_hint_overrides_clear_default_hints() -> std::io::Result<()> { async fn multi_agent_v2_empty_usage_hint_overrides_are_preserved() -> std::io::Result<()> {
let codex_home = TempDir::new()?; let codex_home = TempDir::new()?;
std::fs::write( std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE), codex_home.path().join(CONFIG_TOML_FILE),
@@ -11125,8 +11178,22 @@ subagent_usage_hint_text = ""
.build() .build()
.await?; .await?;
assert_eq!(config.multi_agent_v2.root_agent_usage_hint_text, None); let usage_hints = resolve_usage_hints(
assert_eq!(config.multi_agent_v2.subagent_usage_hint_text, None); &config.multi_agent_v2,
Some(&MultiAgentRoleMessages {
root: Some("catalog root".to_string()),
subagent: Some("catalog subagent".to_string()),
}),
);
assert_eq!(
(
config.multi_agent_v2.root_agent_usage_hint_text.as_deref(),
config.multi_agent_v2.subagent_usage_hint_text.as_deref(),
usage_hints.root,
usage_hints.subagent,
),
(Some(""), Some(""), None, None)
);
Ok(()) Ok(())
} }
+8 -123
View File
@@ -213,69 +213,7 @@ pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION: usiz
pub(crate) const DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS: i64 = 10_000; pub(crate) const DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS: i64 = 10_000;
pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS: i64 = 3600 * 1000; pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS: i64 = 3600 * 1000;
pub(crate) const DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000; pub(crate) const DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000;
const DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT: &str = r#"You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
At the start of your turn, you are the active agent.
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents.
All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent without triggering a turn.
Child agents can also spawn their own sub-agents.
You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter.
You will receive messages in the analysis channel in the form:
```
Message Type: MESSAGE | FINAL_ANSWER
Task name: <recipient>
Sender: <author>
Payload:
<payload text>
```
They may be addressed as to=/root
"#;
const DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT: &str = r#"You are an agent in a team of agents collaborating to complete a task.
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent.
Child agents can also spawn their own sub-agents.
When you provide a response in the final channel, that content is immediately delivered back to your parent agent.
You will receive messages in the analysis channel in the form:
```
Message Type: NEW_TASK | MESSAGE | FINAL_ANSWER
Task name: <recipient>
Sender: <author>
Payload:
<payload text>
```
You may also see them addressed as to=/root/..., which indicates your identity is /root/...
"#;
const DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT: &str = "Full-history forks (`fork_turns` omitted or `\"all\"`) inherit the parent model and reasoning effort and do not accept overrides. Only set `model` or `reasoning_effort` when explicitly requested by the user, applicable `AGENTS.md` instructions, or skill instructions; when doing so, set `fork_turns` to `\"none\"` or a positive integer string.";
const DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE: &str = "collaboration"; const DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE: &str = "collaboration";
const DEFAULT_MULTI_AGENT_V2_WAIT_AGENT_USAGE_HINT_TEXT: &str =
"When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling.";
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.
- All agents use the same current working directory.
- As a result, edits made by one agent are immediately visible to all other agents.
"#;
fn default_multi_agent_v2_usage_hint_text(
usage_hint_text: &str,
max_concurrency: usize,
wait_agent_usage_hint_text: Option<&str>,
) -> String {
let wait_agent_usage_hint_text = match wait_agent_usage_hint_text {
Some(wait_agent_usage_hint_text) => format!("{wait_agent_usage_hint_text}\n\n"),
None => String::new(),
};
format!(
"{usage_hint_text}\n{DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT}\n{wait_agent_usage_hint_text}There are {max_concurrency} available concurrency slots, meaning that up to {max_concurrency} agents can be active at once, including you."
)
}
pub(crate) const HARD_MIN_MULTI_AGENT_V2_TIMEOUT_MS: i64 = 0; pub(crate) const HARD_MIN_MULTI_AGENT_V2_TIMEOUT_MS: i64 = 0;
pub(crate) const HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS: i64 = pub(crate) const HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS: i64 =
@@ -1298,16 +1236,8 @@ impl MultiAgentV2Config {
max_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS, max_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS,
default_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS, default_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS,
usage_hint_text: None, usage_hint_text: None,
root_agent_usage_hint_text: Some(default_multi_agent_v2_usage_hint_text( root_agent_usage_hint_text: None,
DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT, subagent_usage_hint_text: None,
max_concurrent_threads_per_session,
Some(DEFAULT_MULTI_AGENT_V2_WAIT_AGENT_USAGE_HINT_TEXT),
)),
subagent_usage_hint_text: Some(default_multi_agent_v2_usage_hint_text(
DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT,
max_concurrent_threads_per_session,
Some(DEFAULT_MULTI_AGENT_V2_WAIT_AGENT_USAGE_HINT_TEXT),
)),
subagent_developer_instructions: None, subagent_developer_instructions: None,
multi_agent_mode_hint_text: None, multi_agent_mode_hint_text: None,
tool_namespace: Some(DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE.to_string()), tool_namespace: Some(DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE.to_string()),
@@ -2725,42 +2655,15 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
let expose_spawn_agent_model_overrides = base let expose_spawn_agent_model_overrides = base
.and_then(|config| config.expose_spawn_agent_model_overrides) .and_then(|config| config.expose_spawn_agent_model_overrides)
.unwrap_or(default.expose_spawn_agent_model_overrides); .unwrap_or(default.expose_spawn_agent_model_overrides);
let root_agent_usage_hint_text = base
.and_then(|config| config.root_agent_usage_hint_text.as_ref())
.cloned();
let subagent_usage_hint_text = base
.and_then(|config| config.subagent_usage_hint_text.as_ref())
.cloned();
let wait_agent_enabled = base let wait_agent_enabled = base
.and_then(|config| config.wait_agent_enabled) .and_then(|config| config.wait_agent_enabled)
.unwrap_or(default.wait_agent_enabled); .unwrap_or(default.wait_agent_enabled);
let default_wait_agent_usage_hint_text = if wait_agent_enabled {
Some(DEFAULT_MULTI_AGENT_V2_WAIT_AGENT_USAGE_HINT_TEXT)
} else {
None
};
let mut default_root_agent_usage_hint_text = Some(default_multi_agent_v2_usage_hint_text(
DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT,
max_concurrent_threads_per_session,
default_wait_agent_usage_hint_text,
));
let mut default_subagent_usage_hint_text = Some(default_multi_agent_v2_usage_hint_text(
DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT,
max_concurrent_threads_per_session,
default_wait_agent_usage_hint_text,
));
if expose_spawn_agent_model_overrides {
default_root_agent_usage_hint_text = Some(append_usage_hint_text(
default_root_agent_usage_hint_text.as_deref(),
DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT,
));
default_subagent_usage_hint_text = Some(append_usage_hint_text(
default_subagent_usage_hint_text.as_deref(),
DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT,
));
}
let root_agent_usage_hint_text = resolve_optional_prompt_text(
base.map(|config| &config.root_agent_usage_hint_text),
default_root_agent_usage_hint_text,
);
let subagent_usage_hint_text = resolve_optional_prompt_text(
base.map(|config| &config.subagent_usage_hint_text),
default_subagent_usage_hint_text,
);
let subagent_developer_instructions = base let subagent_developer_instructions = base
.and_then(|config| config.subagent_developer_instructions.as_ref()) .and_then(|config| config.subagent_developer_instructions.as_ref())
.map(|instructions| instructions.trim().to_string()); .map(|instructions| instructions.trim().to_string());
@@ -2941,24 +2844,6 @@ fn resolve_terminal_resize_reflow_config(config_toml: &ConfigToml) -> TerminalRe
} }
} }
fn resolve_optional_prompt_text(
configured: Option<&Option<String>>,
default: Option<String>,
) -> Option<String> {
match configured {
Some(Some(value)) if value.is_empty() => None,
Some(Some(value)) => Some(value.clone()),
Some(None) | None => default,
}
}
fn append_usage_hint_text(usage_hint_text: Option<&str>, additional_text: &str) -> String {
match usage_hint_text {
Some(usage_hint_text) => format!("{usage_hint_text}\n\n{additional_text}"),
None => additional_text.to_string(),
}
}
fn code_mode_toml_config(features: Option<&FeaturesToml>) -> Option<&CodeModeConfigToml> { fn code_mode_toml_config(features: Option<&FeaturesToml>) -> Option<&CodeModeConfigToml> {
match features?.code_mode.as_ref()? { match features?.code_mode.as_ref()? {
FeatureToml::Enabled(_) => None, FeatureToml::Enabled(_) => None,
+2
View File
@@ -18,6 +18,7 @@ mod legacy_model_mismatch_warning;
mod legacy_unified_exec_process_limit_warning; mod legacy_unified_exec_process_limit_warning;
mod model_switch_instructions; mod model_switch_instructions;
mod multi_agent_mode_instructions; mod multi_agent_mode_instructions;
mod multi_agent_role_instructions;
mod multi_agent_usage_hint; mod multi_agent_usage_hint;
mod network_rule_saved; mod network_rule_saved;
mod node_repl_review_evidence; mod node_repl_review_evidence;
@@ -63,6 +64,7 @@ pub(crate) use legacy_apply_patch_exec_command_warning::LegacyApplyPatchExecComm
pub(crate) use legacy_model_mismatch_warning::LegacyModelMismatchWarning; pub(crate) use legacy_model_mismatch_warning::LegacyModelMismatchWarning;
pub(crate) use legacy_unified_exec_process_limit_warning::LegacyUnifiedExecProcessLimitWarning; pub(crate) use legacy_unified_exec_process_limit_warning::LegacyUnifiedExecProcessLimitWarning;
pub(crate) use model_switch_instructions::ModelSwitchInstructions; pub(crate) use model_switch_instructions::ModelSwitchInstructions;
pub(crate) use multi_agent_role_instructions::MultiAgentRoleInstructions;
pub(crate) use multi_agent_usage_hint::MultiAgentUsageHint; pub(crate) use multi_agent_usage_hint::MultiAgentUsageHint;
pub(crate) use network_rule_saved::NetworkRuleSaved; pub(crate) use network_rule_saved::NetworkRuleSaved;
pub(crate) use node_repl_review_evidence::NodeReplReviewEvidence; pub(crate) use node_repl_review_evidence::NodeReplReviewEvidence;
@@ -0,0 +1,49 @@
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct MultiAgentRoleInstructions {
text: String,
marked: bool,
}
impl MultiAgentRoleInstructions {
pub(crate) fn unmarked(text: impl Into<String>) -> Self {
Self {
text: text.into(),
marked: false,
}
}
pub(crate) fn catalog(text: impl Into<String>) -> Self {
Self {
text: text.into(),
marked: true,
}
}
}
impl ContextualUserFragment for MultiAgentRoleInstructions {
fn role(&self) -> &'static str {
"developer"
}
fn requires_separate_message(&self) -> bool {
true
}
fn markers(&self) -> (&'static str, &'static str) {
if self.marked {
Self::type_markers()
} else {
("", "")
}
}
fn type_markers() -> (&'static str, &'static str) {
("<multi_agent_role>", "</multi_agent_role>")
}
fn body(&self) -> String {
self.text.clone()
}
}
@@ -1,5 +1,6 @@
use super::super::test_support::render_section_cases; use super::super::test_support::render_section_cases;
use super::*; use super::*;
use crate::context::MultiAgentRoleInstructions;
use crate::context::world_state::WorldState; use crate::context::world_state::WorldState;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
use codex_utils_output_truncation::approx_token_count; use codex_utils_output_truncation::approx_token_count;
@@ -70,7 +71,9 @@ fn persisted_mode_is_restored_only_when_missing_from_history() {
fn unchanged_mode_is_reemitted_after_usage_hint_migration() { fn unchanged_mode_is_reemitted_after_usage_hint_migration() {
let previous = state(Some(MultiAgentMode::Proactive)); let previous = state(Some(MultiAgentMode::Proactive));
let current = MultiAgentModeState::new(Some(MultiAgentMode::Proactive)).with_usage_hint( let current = MultiAgentModeState::new(Some(MultiAgentMode::Proactive)).with_usage_hint(
&MultiAgentUsageHintState::new("Current usage instructions."), &MultiAgentUsageHintState::new(MultiAgentRoleInstructions::unmarked(
"Current usage instructions.",
)),
); );
let instructions = current let instructions = current
@@ -85,6 +88,38 @@ fn unchanged_mode_is_reemitted_after_usage_hint_migration() {
); );
} }
#[test]
fn catalog_role_updates_remain_separate_from_active_mode() {
let previous_hint =
MultiAgentUsageHintState::new(MultiAgentRoleInstructions::catalog("Previous role."));
let previous_mode =
MultiAgentModeState::new(Some(MultiAgentMode::Proactive)).with_usage_hint(&previous_hint);
let mut previous = WorldState::default();
previous.add_section(previous_hint);
previous.add_section(previous_mode);
let current_role = MultiAgentRoleInstructions::catalog("Current role.");
let current_hint = MultiAgentUsageHintState::new(current_role.clone());
let current_mode =
MultiAgentModeState::new(Some(MultiAgentMode::Proactive)).with_usage_hint(&current_hint);
let mut current = WorldState::default();
current.add_section(current_hint);
current.add_section(current_mode);
let updates = crate::context_manager::updates::merge_contextual_fragments(
current.render_diff(&previous.snapshot()),
);
let expected_mode = MultiAgentModeInstructions::from_mode(MultiAgentMode::Proactive)
.expect("proactive mode should render");
assert_eq!(
updates,
vec![
ContextualUserFragment::into(current_role),
ContextualUserFragment::into(expected_mode),
],
);
}
#[test] #[test]
fn custom_mode_is_bounded_before_snapshot_and_rendering() { fn custom_mode_is_bounded_before_snapshot_and_rendering() {
let state = state(Some(MultiAgentMode::Custom("custom mode ".repeat(1_000)))); let state = state(Some(MultiAgentMode::Custom("custom mode ".repeat(1_000))));
@@ -2,19 +2,18 @@ use super::PreviousSectionState;
use super::WorldStateHash; use super::WorldStateHash;
use super::WorldStateSection; use super::WorldStateSection;
use crate::context::ContextualUserFragment; use crate::context::ContextualUserFragment;
use crate::context::MultiAgentRoleInstructions;
use crate::context::MultiAgentUsageHint; use crate::context::MultiAgentUsageHint;
/// Configured multi-agent instructions currently visible to the model. /// Configured or model-owned multi-agent instructions currently visible to the model.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct MultiAgentUsageHintState { pub(crate) struct MultiAgentUsageHintState {
instructions: MultiAgentUsageHint, instructions: MultiAgentRoleInstructions,
} }
impl MultiAgentUsageHintState { impl MultiAgentUsageHintState {
pub(crate) fn new(text: &str) -> Self { pub(crate) fn new(instructions: MultiAgentRoleInstructions) -> Self {
Self { Self { instructions }
instructions: MultiAgentUsageHint::new(text),
}
} }
} }
@@ -38,7 +37,13 @@ impl WorldStateSection for MultiAgentUsageHintState {
PreviousSectionState::Known(previous) if previous == &self.snapshot() => None, PreviousSectionState::Known(previous) if previous == &self.snapshot() => None,
PreviousSectionState::Unknown => None, PreviousSectionState::Unknown => None,
PreviousSectionState::Known(_) | PreviousSectionState::Absent => { PreviousSectionState::Known(_) | PreviousSectionState::Absent => {
Some(Box::new(self.instructions.clone())) if self.instructions.markers().0.is_empty() {
Some(Box::new(MultiAgentUsageHint::new(
&self.instructions.body(),
)))
} else {
Some(Box::new(self.instructions.clone()))
}
} }
} }
} }
@@ -1202,6 +1202,7 @@ fn drop_last_n_user_turns_trims_context_updates_above_rolled_back_turn() {
"{ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG}\nROLLED_BACK_ENVIRONMENT_INSTRUCTIONS" "{ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG}\nROLLED_BACK_ENVIRONMENT_INSTRUCTIONS"
)), )),
developer_msg("<collaboration_mode>ROLLED_BACK_DEV_INSTRUCTIONS</collaboration_mode>"), developer_msg("<collaboration_mode>ROLLED_BACK_DEV_INSTRUCTIONS</collaboration_mode>"),
developer_msg("<multi_agent_role>ROLLED_BACK_MULTI_AGENT_ROLE</multi_agent_role>"),
developer_msg("<multi_agent_mode>ROLLED_BACK_MULTI_AGENT_MODE</multi_agent_mode>"), developer_msg("<multi_agent_mode>ROLLED_BACK_MULTI_AGENT_MODE</multi_agent_mode>"),
user_input_text_msg( user_input_text_msg(
"<environment_context><cwd>PRETURN_CONTEXT_DIFF_CWD</cwd></environment_context>", "<environment_context><cwd>PRETURN_CONTEXT_DIFF_CWD</cwd></environment_context>",
+1
View File
@@ -43,6 +43,7 @@ const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[
"<model_switch>", "<model_switch>",
APPS_INSTRUCTIONS_OPEN_TAG, APPS_INSTRUCTIONS_OPEN_TAG,
COLLABORATION_MODE_OPEN_TAG, COLLABORATION_MODE_OPEN_TAG,
"<multi_agent_role>",
MULTI_AGENT_MODE_OPEN_TAG, MULTI_AGENT_MODE_OPEN_TAG,
ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG, ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG,
"<git_attribution>", "<git_attribution>",
@@ -1808,6 +1808,7 @@ mod tests {
policy_template: Some(catalog_template.to_string()), policy_template: Some(catalog_template.to_string()),
}), }),
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}; };
@@ -1842,6 +1843,7 @@ mod tests {
policy_template: None, policy_template: None,
}), }),
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}; };
@@ -1884,6 +1886,7 @@ mod tests {
policy_template: Some(String::new()), policy_template: Some(String::new()),
}), }),
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}; };
+6
View File
@@ -22,6 +22,7 @@ use crate::config::ManagedFeatures;
use crate::config::resolve_tool_suggest_config_from_layer_stack; use crate::config::resolve_tool_suggest_config_from_layer_stack;
use crate::context::ContextualUserFragment; use crate::context::ContextualUserFragment;
use crate::context::ModelSwitchInstructions; use crate::context::ModelSwitchInstructions;
use crate::context::MultiAgentRoleInstructions;
use crate::context::NetworkRuleSaved; use crate::context::NetworkRuleSaved;
use crate::context::RecommendedPluginsInstructions; use crate::context::RecommendedPluginsInstructions;
use crate::context::world_state::WorldState; use crate::context::world_state::WorldState;
@@ -3603,6 +3604,11 @@ impl Session {
"developer" if fragment.markers().0 == MULTI_AGENT_MODE_OPEN_TAG => { "developer" if fragment.markers().0 == MULTI_AGENT_MODE_OPEN_TAG => {
initial_multi_agent_mode = Some(fragment); initial_multi_agent_mode = Some(fragment);
} }
"developer"
if fragment.markers().0 == MultiAgentRoleInstructions::type_markers().0 =>
{
separate_developer_sections.push(fragment.render());
}
"developer" "developer"
if fragment.requires_separate_message() && fragment.markers().0.is_empty() => if fragment.requires_separate_message() && fragment.markers().0.is_empty() =>
{ {
+139 -21
View File
@@ -1,57 +1,175 @@
use crate::config::MultiAgentV2Config; use crate::config::MultiAgentV2Config;
use crate::context::MultiAgentRoleInstructions;
use crate::session::turn_context::TurnContext; use crate::session::turn_context::TurnContext;
use codex_protocol::config_types::MultiAgentMode; use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::openai_models::MultiAgentRoleMessages;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::SubAgentSource;
pub(super) fn usage_hint_text<'a>( const DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT: &str = r#"You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
turn_context: &'a TurnContext,
At the start of your turn, you are the active agent.
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents.
All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent without triggering a turn.
Child agents can also spawn their own sub-agents.
You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter.
You will receive messages in the analysis channel in the form:
```
Message Type: MESSAGE | FINAL_ANSWER
Task name: <recipient>
Sender: <author>
Payload:
<payload text>
```
They may be addressed as to=/root
"#;
const DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT: &str = r#"You are an agent in a team of agents collaborating to complete a task.
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent.
Child agents can also spawn their own sub-agents.
When you provide a response in the final channel, that content is immediately delivered back to your parent agent.
You will receive messages in the analysis channel in the form:
```
Message Type: NEW_TASK | MESSAGE | FINAL_ANSWER
Task name: <recipient>
Sender: <author>
Payload:
<payload text>
```
You may also see them addressed as to=/root/..., which indicates your identity is /root/...
"#;
const DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT: &str = "Full-history forks (`fork_turns` omitted or `\"all\"`) inherit the parent model and reasoning effort and do not accept overrides. Only set `model` or `reasoning_effort` when explicitly requested by the user, applicable `AGENTS.md` instructions, or skill instructions; when doing so, set `fork_turns` to `\"none\"` or a positive integer string.";
const DEFAULT_MULTI_AGENT_V2_WAIT_AGENT_USAGE_HINT_TEXT: &str =
"When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling.";
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.
- All agents use the same current working directory.
- As a result, edits made by one agent are immediately visible to all other agents.
"#;
#[derive(Clone, Debug, Default)]
pub(crate) struct ResolvedMultiAgentV2UsageHints {
pub(crate) root: Option<MultiAgentRoleInstructions>,
pub(crate) subagent: Option<MultiAgentRoleInstructions>,
}
pub(super) fn usage_hint_text(
turn_context: &TurnContext,
session_source: &SessionSource, session_source: &SessionSource,
) -> Option<&'a str> { ) -> Option<MultiAgentRoleInstructions> {
if turn_context.multi_agent_version != MultiAgentVersion::V2 { if turn_context.multi_agent_version != MultiAgentVersion::V2 {
return None; return None;
} }
let multi_agent_v2 = &turn_context.config.multi_agent_v2; let catalog = turn_context
configured_usage_hint_text_for_source(multi_agent_v2, session_source) .model_info
} .model_messages
.as_ref()
fn configured_usage_hint_text_for_source<'a>( .and_then(|messages| messages.multi_agent.as_ref())
multi_agent_v2: &'a MultiAgentV2Config, .and_then(|messages| messages.role.as_ref());
session_source: &SessionSource, let snapshot = resolve_usage_hints(&turn_context.config.multi_agent_v2, catalog);
) -> Option<&'a str> {
match session_source { match session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) => { SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) => snapshot.subagent,
multi_agent_v2.subagent_usage_hint_text.as_deref()
}
SessionSource::Cli SessionSource::Cli
| SessionSource::VSCode | SessionSource::VSCode
| SessionSource::Exec | SessionSource::Exec
| SessionSource::Mcp | SessionSource::Mcp
| SessionSource::Custom(_) | SessionSource::Custom(_)
| SessionSource::Unknown => multi_agent_v2.root_agent_usage_hint_text.as_deref(), | SessionSource::Unknown => snapshot.root,
SessionSource::Internal(_) | SessionSource::SubAgent(_) => None, SessionSource::Internal(_) | SessionSource::SubAgent(_) => None,
} }
} }
pub(crate) fn resolve_usage_hints(
config: &MultiAgentV2Config,
catalog: Option<&MultiAgentRoleMessages>,
) -> ResolvedMultiAgentV2UsageHints {
let resolve_role = |configured: Option<&str>, catalog: Option<&str>, bundled: &str| {
// Configured roles take precedence; empty configured or catalog roles suppress fallback.
if let Some(configured) = configured {
return (!configured.is_empty())
.then(|| MultiAgentRoleInstructions::unmarked(configured));
}
let base = catalog.unwrap_or(bundled);
if base.is_empty() {
return None;
}
let max_concurrency = config.max_concurrent_threads_per_session;
let wait_agent_guidance = if config.wait_agent_enabled {
format!("{DEFAULT_MULTI_AGENT_V2_WAIT_AGENT_USAGE_HINT_TEXT}\n\n")
} else {
String::new()
};
let mut text = format!(
"{base}\n{DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT}\n{wait_agent_guidance}There are {max_concurrency} available concurrency slots, meaning that up to {max_concurrency} agents can be active at once, including you."
);
if config.expose_spawn_agent_model_overrides {
text.push_str("\n\n");
text.push_str(DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT);
}
Some(if catalog.is_some() {
MultiAgentRoleInstructions::catalog(text)
} else {
MultiAgentRoleInstructions::unmarked(text)
})
};
ResolvedMultiAgentV2UsageHints {
root: resolve_role(
config.root_agent_usage_hint_text.as_deref(),
catalog.and_then(|messages| messages.root.as_deref()),
DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT,
),
subagent: resolve_role(
config.subagent_usage_hint_text.as_deref(),
catalog.and_then(|messages| messages.subagent.as_deref()),
DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT,
),
}
}
pub(crate) fn effective_multi_agent_mode(turn_context: &TurnContext) -> Option<MultiAgentMode> { pub(crate) fn effective_multi_agent_mode(turn_context: &TurnContext) -> Option<MultiAgentMode> {
if turn_context.multi_agent_version != MultiAgentVersion::V2 { if turn_context.multi_agent_version != MultiAgentVersion::V2 {
return None; return None;
} }
// A configured hint, including an empty string, defines a custom policy instead of an let catalog_mode = turn_context
// effort-derived built-in policy. .model_info
let multi_agent_mode = match &turn_context .model_messages
.as_ref()
.and_then(|messages| messages.multi_agent.as_ref())
.and_then(|messages| messages.mode.as_ref());
let mode_hint_text = turn_context
.config .config
.multi_agent_v2 .multi_agent_v2
.multi_agent_mode_hint_text .multi_agent_mode_hint_text
{ .as_deref()
Some(hint_text) => MultiAgentMode::Custom(hint_text.clone()), .or_else(|| catalog_mode.and_then(|mode| mode.hint_text.as_deref()));
// A configured or catalog hint, including an empty string, defines a custom policy instead
// of an effort-derived built-in policy.
let multi_agent_mode = match mode_hint_text {
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) => MultiAgentMode::Proactive,
_ => MultiAgentMode::ExplicitRequestOnly, _ => catalog_mode
.and_then(|messages| messages.explicit.clone())
.map(MultiAgentMode::Custom)
.unwrap_or(MultiAgentMode::ExplicitRequestOnly),
}, },
}; };
+6 -3
View File
@@ -9446,8 +9446,8 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_hint_is_emp
Vec::new(), Vec::new(),
|config| { |config| {
let _ = config.features.enable(Feature::MultiAgentV2); let _ = config.features.enable(Feature::MultiAgentV2);
config.multi_agent_v2.root_agent_usage_hint_text = None; config.multi_agent_v2.root_agent_usage_hint_text = Some(String::new());
config.multi_agent_v2.subagent_usage_hint_text = None; config.multi_agent_v2.subagent_usage_hint_text = Some(String::new());
}, },
) )
.await; .await;
@@ -9460,7 +9460,10 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_hint_is_emp
matches!( matches!(
message.as_slice(), message.as_slice(),
["Root guidance."] | ["Subagent guidance."] ["Root guidance."] | ["Subagent guidance."]
) ) || message.iter().any(|text| {
text.contains("You are `/root`, the primary agent")
|| text.contains("You are an agent in a team of agents")
})
}), }),
"did not expect multi-agent v2 usage hint developer messages, got {developer_messages:?}" "did not expect multi-agent v2 usage hint developer messages, got {developer_messages:?}"
); );
@@ -138,6 +138,7 @@ async fn handle_spawn_agent(
parent_turn_id: Some(turn.sub_id.clone()), parent_turn_id: Some(turn.sub_id.clone()),
root_turn_id: turn.turn_metadata_state.root_turn_id(), root_turn_id: turn.turn_metadata_state.root_turn_id(),
environments: Some(step_context.environments.to_selections()), environments: Some(step_context.environments.to_selections()),
multi_agent_v2_usage_hints: None,
}, },
)) ))
.await .await
@@ -5,10 +5,12 @@ use crate::agent::next_thread_spawn_depth;
use crate::agent::role::DEFAULT_ROLE_NAME; use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent_communication::AgentCommunicationContext; use crate::agent_communication::AgentCommunicationContext;
use crate::agent_communication::AgentCommunicationKind; use crate::agent_communication::AgentCommunicationKind;
use crate::session::multi_agents::resolve_usage_hints;
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions; use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2; use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
use crate::tools::handlers::multi_agents_v2::message_tool::message_content; use crate::tools::handlers::multi_agents_v2::message_tool::message_content;
use codex_protocol::AgentPath; use codex_protocol::AgentPath;
use codex_protocol::protocol::MultiAgentVersion;
use codex_tools::ToolSpec; use codex_tools::ToolSpec;
#[derive(Default)] #[derive(Default)]
@@ -122,6 +124,29 @@ async fn handle_spawn_agent(
/*trigger_turn*/ true, /*trigger_turn*/ true,
); );
let context = AgentCommunicationContext::new(AgentCommunicationKind::Spawn, session.thread_id); let context = AgentCommunicationContext::new(AgentCommunicationKind::Spawn, session.thread_id);
let multi_agent_v2_usage_hints =
if is_full_history_fork && turn.multi_agent_version == MultiAgentVersion::V2 {
let child_model_info = match config.model.as_deref() {
Some(model) if model != turn.model_info.slug => Some(
session
.services
.models_manager
.get_model_info(model, &config.to_models_manager_config())
.await,
),
_ => None,
};
let child_catalog = child_model_info
.as_ref()
.unwrap_or(&turn.model_info)
.model_messages
.as_ref()
.and_then(|messages| messages.multi_agent.as_ref())
.and_then(|messages| messages.role.as_ref());
Some(resolve_usage_hints(&config.multi_agent_v2, child_catalog))
} else {
None
};
let spawned_agent = Box::pin( let spawned_agent = Box::pin(
session session
.services .services
@@ -138,6 +163,7 @@ async fn handle_spawn_agent(
parent_turn_id: Some(turn.sub_id.clone()), parent_turn_id: Some(turn.sub_id.clone()),
root_turn_id: turn.turn_metadata_state.root_turn_id(), root_turn_id: turn.turn_metadata_state.root_turn_id(),
environments: Some(step_context.environments.to_selections()), environments: Some(step_context.environments.to_selections()),
multi_agent_v2_usage_hints,
}, },
), ),
) )
@@ -41,6 +41,7 @@ async fn catalog_permission_message_loaded_from_remote_models_is_sent() -> Resul
workspace_write: None, workspace_write: None,
read_only: Some("remote catalog permissions: {{ network_access }}".to_string()), read_only: Some("remote catalog permissions: {{ network_access }}".to_string()),
}), }),
multi_agent: None,
token_budget: None, token_budget: None,
}); });
let models_mock = mount_models_once( let models_mock = mount_models_once(
@@ -70,6 +70,7 @@ fn model_with_collaboration_messages(
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}); });
model_messages.collaboration_modes = Some(CollaborationModeMessages { model_messages.collaboration_modes = Some(CollaborationModeMessages {
@@ -510,6 +510,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}), }),
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
+160 -11
View File
@@ -3,6 +3,9 @@ use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::MultiAgentMessages;
use codex_protocol::openai_models::MultiAgentModeMessages;
use codex_protocol::openai_models::MultiAgentRoleMessages;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::ReasoningEffortPreset; use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
@@ -21,12 +24,44 @@ use core_test_support::wait_for_event;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_json::Value; use serde_json::Value;
use serde_json::json; use serde_json::json;
use test_case::test_case;
const NO_SPAWN_TEXT: &str = "Any earlier instruction enabling proactive multi-agent delegation no longer applies. Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work."; const NO_SPAWN_TEXT: &str = "Any earlier instruction enabling proactive multi-agent delegation no longer applies. Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work.";
const PROACTIVE_TEXT: &str = "Proactive multi-agent delegation is active."; 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_EXPLICIT_TEXT: &str = "Use explicit delegation from the model catalog.";
const SECOND_MODEL_EXPLICIT_TEXT: &str = "Second model explicit 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."; const ROOT_USAGE_HINT_TEXT: &str = "Root usage hint.";
#[derive(Clone, Copy, Debug)]
enum ModeHintSource {
ConfiguredHint,
CatalogHint,
}
fn set_multi_agent_mode(
model_info: &mut ModelInfo,
explicit: &str,
hint_text: Option<&str>,
root_role: Option<&str>,
) {
if let Some(model_messages) = model_info.model_messages.as_mut() {
model_messages.multi_agent = Some(MultiAgentMessages {
role: root_role.map(|root| MultiAgentRoleMessages {
root: Some(root.to_string()),
subagent: None,
}),
mode: Some(MultiAgentModeMessages {
explicit: Some(explicit.to_string()),
hint_text: hint_text.map(str::to_string),
}),
});
}
}
fn add_ultra_reasoning(model_info: &mut ModelInfo) { fn add_ultra_reasoning(model_info: &mut ModelInfo) {
model_info model_info
.supported_reasoning_levels .supported_reasoning_levels
@@ -125,8 +160,10 @@ async fn ultra_reasoning_uses_max_and_proactive_mode() -> Result<()> {
Ok(()) Ok(())
} }
#[test_case(ModeHintSource::ConfiguredHint; "configured hint overrides catalog")]
#[test_case(ModeHintSource::CatalogHint; "catalog hint overrides reasoning effort")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn configured_mode_hint_uses_custom_mode_across_reasoning_efforts() -> Result<()> { async fn mode_hints_override_reasoning_effort(source: ModeHintSource) -> Result<()> {
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
let server = start_mock_server().await; let server = start_mock_server().await;
@@ -143,8 +180,19 @@ async fn configured_mode_hint_uses_custom_mode_across_reasoning_efforts() -> Res
) )
.await; .await;
let test = test_codex() let test = test_codex()
.with_model_info_override("gpt-5.4", add_ultra_reasoning) .with_model_info_override("gpt-5.4", |model_info| {
.with_config(configure_custom_mode_hint) add_ultra_reasoning(model_info);
set_multi_agent_mode(
model_info,
CATALOG_EXPLICIT_TEXT,
Some(CATALOG_MODE_HINT_TEXT),
/*root_role*/ None,
);
})
.with_config(move |config| match source {
ModeHintSource::ConfiguredHint => configure_custom_mode_hint(config),
ModeHintSource::CatalogHint => configure_multi_agent_v2(config),
})
.build(&server) .build(&server)
.await?; .await?;
submit_turn(&test.codex, "explicit", Some(ReasoningEffort::High)).await?; submit_turn(&test.codex, "explicit", Some(ReasoningEffort::High)).await?;
@@ -155,15 +203,116 @@ async fn configured_mode_hint_uses_custom_mode_across_reasoning_efforts() -> Res
let first_texts = developer_texts(&first_input); let first_texts = developer_texts(&first_input);
let second_input = requests[1].input(); let second_input = requests[1].input();
let second_texts = developer_texts(&second_input); let second_texts = developer_texts(&second_input);
let instruction_counts = |texts: &[&str]| { let (expected_hint, suppressed_hint) = match source {
( ModeHintSource::ConfiguredHint => (CUSTOM_MODE_HINT_TEXT, CATALOG_MODE_HINT_TEXT),
count_containing(texts, CUSTOM_MODE_HINT_TEXT), ModeHintSource::CatalogHint => (CATALOG_MODE_HINT_TEXT, CATALOG_EXPLICIT_TEXT),
count_containing(texts, NO_SPAWN_TEXT),
count_containing(texts, PROACTIVE_TEXT),
)
}; };
assert_eq!(instruction_counts(&first_texts), (1, 0, 0)); for texts in [&first_texts, &second_texts] {
assert_eq!(instruction_counts(&second_texts), (1, 0, 0)); assert_eq!(
(
count_containing(texts, expected_hint),
count_containing(texts, NO_SPAWN_TEXT),
count_containing(texts, PROACTIVE_TEXT),
),
(1, 0, 0)
);
assert_eq!(count_containing(texts, suppressed_hint), 0);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_switch_refreshes_catalog_role_and_explicit_mode() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let responses = mount_sse_sequence(
&server,
(1..=2)
.map(|index| {
sse(vec![
ev_response_created(&format!("resp-{index}")),
ev_completed(&format!("resp-{index}")),
])
})
.collect(),
)
.await;
let test = test_codex()
.with_model_info_override("gpt-5.2", |model_info| {
set_multi_agent_mode(
model_info,
SECOND_MODEL_EXPLICIT_TEXT,
/*hint_text*/ None,
Some(SECOND_MODEL_ROOT_ROLE_TEXT),
);
})
.with_model_info_override("gpt-5.4", |model_info| {
set_multi_agent_mode(
model_info,
CATALOG_EXPLICIT_TEXT,
/*hint_text*/ None,
Some(FIRST_MODEL_ROOT_ROLE_TEXT),
);
})
.with_config(configure_multi_agent_v2)
.build_with_auto_env(&server)
.await?;
submit_turn(&test.codex, "first model", Some(ReasoningEffort::High)).await?;
core_test_support::submit_thread_settings(
&test.codex,
ThreadSettingsOverrides {
model: Some("gpt-5.2".to_string()),
..Default::default()
},
)
.await?;
submit_turn(&test.codex, "second model", Some(ReasoningEffort::High)).await?;
let requests = responses.requests();
for (index, request) in requests.iter().enumerate() {
let input = request.input();
let texts = developer_texts(&input);
assert_eq!(
(
count_containing(&texts, CATALOG_EXPLICIT_TEXT),
count_containing(&texts, SECOND_MODEL_EXPLICIT_TEXT),
count_containing(&texts, NO_SPAWN_TEXT),
count_containing(&texts, PROACTIVE_TEXT),
),
(1, usize::from(index == 1), 0, 0)
);
}
let developer_messages = requests[1].message_input_text_groups("developer");
let [first_role_position, second_role_position] =
[FIRST_MODEL_ROOT_ROLE_TEXT, SECOND_MODEL_ROOT_ROLE_TEXT].map(|role| {
developer_messages
.iter()
.position(|group| {
matches!(
group.as_slice(),
[text] if text.starts_with("<multi_agent_role>")
&& text.ends_with("</multi_agent_role>")
&& text.contains(role)
)
})
.expect("catalog root role should be a standalone marked developer message")
});
let active_mode_position = developer_messages
.iter()
.rposition(|group| {
group
.iter()
.any(|text| text.contains(MULTI_AGENT_MODE_OPEN_TAG))
})
.expect("active multi-agent mode should be present");
assert!(
first_role_position < second_role_position && second_role_position < active_mode_position,
"catalog root roles should remain ordered before the active mode: {developer_messages:?}"
);
Ok(()) Ok(())
} }
@@ -58,6 +58,7 @@ fn model_with_approval_messages(
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}); });
model model
@@ -75,6 +76,7 @@ fn model_with_permission_messages(
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: Some(permissions), permissions: Some(permissions),
multi_agent: None,
token_budget: None, token_budget: None,
}); });
model model
@@ -227,6 +229,7 @@ async fn catalog_non_on_request_approval_messages_are_sent_in_initial_permission
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}); });
let mut builder = test_codex() let mut builder = test_codex()
+2
View File
@@ -643,6 +643,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}), }),
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
@@ -767,6 +768,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}), }),
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
@@ -110,6 +110,7 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}), }),
..specific ..specific
@@ -123,6 +124,7 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}), }),
..generic ..generic
@@ -770,6 +772,7 @@ async fn remote_models_apply_legacy_instructions() -> Result<()> {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}), }),
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
@@ -3,6 +3,7 @@
use anyhow::Result; use anyhow::Result;
use codex_core::config::AgentRoleConfig; use codex_core::config::AgentRoleConfig;
use codex_core::config::Config;
use codex_features::Feature; use codex_features::Feature;
use codex_history::RolloutItem; use codex_history::RolloutItem;
use codex_history::RolloutLine; use codex_history::RolloutLine;
@@ -20,6 +21,7 @@ use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_protocol::openai_models::TruncationPolicyConfig; use codex_protocol::openai_models::TruncationPolicyConfig;
use codex_protocol::openai_models::default_input_modalities; use codex_protocol::openai_models::default_input_modalities;
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG; use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
use core_test_support::responses::ResponsesRequest;
use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created; use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_models_once; use core_test_support::responses::mount_models_once;
@@ -52,6 +54,22 @@ fn spawn_agent_exposes_agent_type(body: &Value, namespace: &str) -> bool {
.is_some() .is_some()
} }
fn resolved_root_usage_hint(config: &Config, request: &ResponsesRequest) -> String {
if let Some(configured) = config.multi_agent_v2.root_agent_usage_hint_text.as_deref() {
return configured.to_string();
}
request
.message_input_text_groups("developer")
.into_iter()
.rev()
.find_map(|group| {
(group.len() == 1 && group[0].contains("available concurrency slots"))
.then(|| group[0].clone())
})
.expect("resolved root usage hint should be a standalone developer message")
}
fn test_model_info( fn test_model_info(
slug: &str, slug: &str,
display_name: &str, display_name: &str,
@@ -483,18 +501,13 @@ async fn multi_agent_v2_cold_resume_refreshes_legacy_usage_hints_once(
} }
}); });
let resumed = resumed_builder.resume(&server, home, rollout_path).await?; let resumed = resumed_builder.resume(&server, home, rollout_path).await?;
let current_usage_hint = resumed
.config
.multi_agent_v2
.root_agent_usage_hint_text
.clone()
.expect("current root usage hint should be configured");
resumed.submit_turn("first turn after upgrading").await?; resumed.submit_turn("first turn after upgrading").await?;
resumed.submit_turn("second turn after upgrading").await?; resumed.submit_turn("second turn after upgrading").await?;
let requests = resumed_responses.requests(); let requests = resumed_responses.requests();
assert_eq!(requests.len(), 2); assert_eq!(requests.len(), 2);
let current_usage_hint = resolved_root_usage_hint(&resumed.config, &requests[0]);
for request in &requests { for request in &requests {
let developer_messages = request.message_input_text_groups("developer"); let developer_messages = request.message_input_text_groups("developer");
let current_usage_hint_positions = developer_messages let current_usage_hint_positions = developer_messages
@@ -624,12 +637,6 @@ async fn multi_agent_v2_resume_refreshes_changed_wait_guidance(
) )
.await; .await;
let resumed = test_codex().resume(&server, home, rollout_path).await?; let resumed = test_codex().resume(&server, home, rollout_path).await?;
let current_usage_hint = resumed
.config
.multi_agent_v2
.root_agent_usage_hint_text
.clone()
.expect("current root usage hint should be configured");
resumed resumed
.submit_turn("first turn with updated wait-agent availability") .submit_turn("first turn with updated wait-agent availability")
@@ -640,6 +647,7 @@ async fn multi_agent_v2_resume_refreshes_changed_wait_guidance(
let requests = resumed_responses.requests(); let requests = resumed_responses.requests();
assert_eq!(requests.len(), 2); assert_eq!(requests.len(), 2);
let current_usage_hint = resolved_root_usage_hint(&resumed.config, &requests[0]);
for request in &requests { for request in &requests {
let developer_messages = request.message_input_text_groups("developer"); let developer_messages = request.message_input_text_groups("developer");
let current_usage_hint_positions = developer_messages let current_usage_hint_positions = developer_messages
@@ -10,6 +10,8 @@ use codex_models_manager::bundled_models_response;
use codex_protocol::ThreadId; use codex_protocol::ThreadId;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::MultiAgentMessages;
use codex_protocol::openai_models::MultiAgentRoleMessages;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
@@ -1141,6 +1143,26 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co
.features .features
.enable(Feature::MultiAgentV2) .enable(Feature::MultiAgentV2)
.expect("test config should allow feature update"); .expect("test config should allow feature update");
let model_catalog = config.model_catalog.get_or_insert_with(|| {
bundled_models_response().expect("bundled models.json should parse")
});
for model in [INHERITED_MODEL, V2_DEFAULT_MODEL, V2_REQUESTED_MODEL] {
let model_info = model_catalog
.models
.iter_mut()
.find(|model_info| model_info.slug == model)
.unwrap_or_else(|| panic!("{model} should exist in bundled models.json"));
let multi_agent = model_info
.model_messages
.as_mut()
.expect("bundled model should include model messages")
.multi_agent
.get_or_insert_with(MultiAgentMessages::default);
multi_agent.role = Some(MultiAgentRoleMessages {
root: Some(format!("{model} root role.")),
subagent: Some(format!("{model} subagent role.")),
});
}
if matches!(selection, FullHistoryV2ModelSelection::WorldStateIdentity) { if matches!(selection, FullHistoryV2ModelSelection::WorldStateIdentity) {
config config
.features .features
@@ -1182,6 +1204,18 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co
let child_request = wait_for_request_with_model(&child_request_log, expected_model).await?; let child_request = wait_for_request_with_model(&child_request_log, expected_model).await?;
assert!(child_request.body_contains_text(TURN_0_FORK_PROMPT)); assert!(child_request.body_contains_text(TURN_0_FORK_PROMPT));
let child_developer_messages = child_request.message_input_texts("developer");
assert_eq!(
child_developer_messages
.iter()
.filter(|message| message.contains(&format!("{expected_model} subagent role.")))
.count(),
1
);
assert!(!child_developer_messages.iter().any(|message| {
message.contains(&format!("{INHERITED_MODEL} root role."))
|| message.contains(&format!("{INHERITED_MODEL} subagent role."))
}));
if matches!(selection, FullHistoryV2ModelSelection::CurrentTimeReminders) { if matches!(selection, FullHistoryV2ModelSelection::CurrentTimeReminders) {
let reminder_count = |request: &ResponsesRequest| { let reminder_count = |request: &ResponsesRequest| {
request request
@@ -57,6 +57,7 @@ pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig)
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}); });
model_messages.instructions_template = Some(base_instructions.clone()); model_messages.instructions_template = Some(base_instructions.clone());
@@ -198,6 +199,7 @@ fn local_model_messages_for_slug(slug: &str) -> ModelMessages {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}, },
_ => ModelMessages { _ => ModelMessages {
@@ -207,6 +209,7 @@ fn local_model_messages_for_slug(slug: &str) -> ModelMessages {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}, },
} }
@@ -5,6 +5,9 @@ use codex_protocol::openai_models::ApprovalMessages;
use codex_protocol::openai_models::AutoReviewMessages; use codex_protocol::openai_models::AutoReviewMessages;
use codex_protocol::openai_models::CollaborationModeMessages; use codex_protocol::openai_models::CollaborationModeMessages;
use codex_protocol::openai_models::ModelTokenBudgetConfig; use codex_protocol::openai_models::ModelTokenBudgetConfig;
use codex_protocol::openai_models::MultiAgentMessages;
use codex_protocol::openai_models::MultiAgentModeMessages;
use codex_protocol::openai_models::MultiAgentRoleMessages;
use codex_protocol::openai_models::PermissionMessages; use codex_protocol::openai_models::PermissionMessages;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -39,6 +42,16 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
workspace_write: Some(String::new()), workspace_write: Some(String::new()),
read_only: None, read_only: None,
}; };
let multi_agent = MultiAgentMessages {
role: Some(MultiAgentRoleMessages {
root: Some("root base".to_string()),
subagent: Some("subagent base".to_string()),
}),
mode: Some(MultiAgentModeMessages {
explicit: Some("explicit mode".to_string()),
hint_text: Some("mode hint".to_string()),
}),
};
let token_budget = ModelTokenBudgetConfig { let token_budget = ModelTokenBudgetConfig {
reminder_threshold_tokens: 128, reminder_threshold_tokens: 128,
reminder_message_template: "budget reminder".to_string(), reminder_message_template: "budget reminder".to_string(),
@@ -57,6 +70,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
collaboration_modes: Some(collaboration_modes.clone()), collaboration_modes: Some(collaboration_modes.clone()),
auto_review: Some(auto_review.clone()), auto_review: Some(auto_review.clone()),
permissions: Some(permissions.clone()), permissions: Some(permissions.clone()),
multi_agent: Some(multi_agent.clone()),
token_budget: Some(token_budget.clone()), token_budget: Some(token_budget.clone()),
}); });
let config = ModelsManagerConfig { let config = ModelsManagerConfig {
@@ -75,6 +89,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
collaboration_modes: Some(collaboration_modes), collaboration_modes: Some(collaboration_modes),
auto_review: Some(auto_review), auto_review: Some(auto_review),
permissions: Some(permissions), permissions: Some(permissions),
multi_agent: Some(multi_agent),
token_budget: Some(token_budget), token_budget: Some(token_budget),
}) })
); );
@@ -104,6 +119,7 @@ fn disabled_personality_bakes_default_and_preserves_catalog_approval_messages()
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}); });
let config = ModelsManagerConfig { let config = ModelsManagerConfig {
@@ -122,6 +138,7 @@ fn disabled_personality_bakes_default_and_preserves_catalog_approval_messages()
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}) })
); );
@@ -147,6 +164,7 @@ fn disabled_personality_uses_plain_base_instructions_for_local_personality_model
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}), }),
"unexpected model messages for {slug}" "unexpected model messages for {slug}"
@@ -186,6 +204,7 @@ fn personality_none_strips_catalog_instruction_sources_through_the_next_h1() {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}); });
+52
View File
@@ -531,6 +531,7 @@ pub struct ModelMessages {
pub collaboration_modes: Option<CollaborationModeMessages>, pub collaboration_modes: Option<CollaborationModeMessages>,
pub auto_review: Option<AutoReviewMessages>, pub auto_review: Option<AutoReviewMessages>,
pub permissions: Option<PermissionMessages>, pub permissions: Option<PermissionMessages>,
pub multi_agent: Option<MultiAgentMessages>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub token_budget: Option<ModelTokenBudgetConfig>, pub token_budget: Option<ModelTokenBudgetConfig>,
} }
@@ -572,6 +573,24 @@ pub struct PermissionMessages {
pub read_only: Option<String>, pub read_only: Option<String>,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct MultiAgentMessages {
pub role: Option<MultiAgentRoleMessages>,
pub mode: Option<MultiAgentModeMessages>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct MultiAgentRoleMessages {
pub root: Option<String>,
pub subagent: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct MultiAgentModeMessages {
pub explicit: Option<String>,
pub hint_text: Option<String>,
}
impl ModelMessages { impl ModelMessages {
fn has_personality_placeholder(&self) -> bool { fn has_personality_placeholder(&self) -> bool {
self.instructions_template self.instructions_template
@@ -737,6 +756,7 @@ where
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}); });
messages.instructions_template = Some(base_instructions); messages.instructions_template = Some(base_instructions);
@@ -925,6 +945,7 @@ mod tests {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
} }
); );
@@ -1018,6 +1039,28 @@ 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":""}}}"#,
)
.expect("multi-agent messages should deserialize");
assert_eq!(
messages.multi_agent,
Some(MultiAgentMessages {
role: Some(MultiAgentRoleMessages {
root: Some(String::new()),
subagent: Some("subagent base".to_string()),
}),
mode: Some(MultiAgentModeMessages {
explicit: Some("explicit mode".to_string()),
hint_text: Some(String::new()),
}),
})
);
}
#[test] #[test]
fn collaboration_mode_messages_preserve_missing_and_empty_values() { fn collaboration_mode_messages_preserve_missing_and_empty_values() {
let messages: ModelMessages = from_str( let messages: ModelMessages = from_str(
@@ -1043,6 +1086,7 @@ mod tests {
}), }),
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
} }
); );
@@ -1123,6 +1167,7 @@ mod tests {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
})); }));
@@ -1144,6 +1189,7 @@ mod tests {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
})); }));
assert_eq!( assert_eq!(
@@ -1166,6 +1212,7 @@ mod tests {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
})); }));
assert_eq!( assert_eq!(
@@ -1199,6 +1246,7 @@ mod tests {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
})); }));
@@ -1238,6 +1286,7 @@ mod tests {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}) })
); );
@@ -1288,6 +1337,7 @@ mod tests {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}))], }))],
}; };
@@ -1324,6 +1374,7 @@ mod tests {
workspace_write: Some("workspace".to_string()), workspace_write: Some("workspace".to_string()),
read_only: None, read_only: None,
}), }),
multi_agent: None,
token_budget: None, token_budget: None,
}; };
let mut value = serde_json::to_value(ModelsResponse { let mut value = serde_json::to_value(ModelsResponse {
@@ -1345,6 +1396,7 @@ mod tests {
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
permissions: None, permissions: None,
multi_agent: None,
token_budget: None, token_budget: None,
}; };
let mut value = serde_json::to_value(ModelsResponse { let mut value = serde_json::to_value(ModelsResponse {