mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
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:
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,
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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(),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(¤t_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>",
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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() =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user