mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Configure multi_agent_v2 spawn agent hints (#17071)
Allow multi_agent_v2 features to have its own temporary configuration under `[features.multi_agent_v2]` ``` [features.multi_agent_v2] enabled = true usage_hint_enabled = false usage_hint_text = "Custom delegation guidance." hide_spawn_agent_metadata = true ``` Absent `usage_hint_text` means use the default hint. ``` [features] multi_agent_v2 = true ``` still works as the boolean shorthand.
This commit is contained in:
committed by
GitHub
Unverified
parent
2250fdd54a
commit
4c07dd4d25
@@ -1746,7 +1746,7 @@ fn feature_table_overrides_legacy_flags() -> std::io::Result<()> {
|
||||
let mut entries = BTreeMap::new();
|
||||
entries.insert("apply_patch_freeform".to_string(), false);
|
||||
let cfg = ConfigToml {
|
||||
features: Some(FeaturesToml { entries }),
|
||||
features: Some(FeaturesToml::from(entries)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1794,7 +1794,7 @@ fn responses_websocket_features_do_not_change_wire_api() -> std::io::Result<()>
|
||||
let mut entries = BTreeMap::new();
|
||||
entries.insert(feature_key.to_string(), true);
|
||||
let cfg = ConfigToml {
|
||||
features: Some(FeaturesToml { entries }),
|
||||
features: Some(FeaturesToml::from(entries)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -3005,14 +3005,14 @@ async fn set_feature_enabled_updates_profile() -> anyhow::Result<()> {
|
||||
profile
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries.get("guardian_approval")),
|
||||
Some(&true),
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
Some(true),
|
||||
);
|
||||
assert_eq!(
|
||||
parsed
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries.get("guardian_approval")),
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -3047,14 +3047,14 @@ async fn set_feature_enabled_persists_default_false_feature_disable_in_profile()
|
||||
profile
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries.get("guardian_approval")),
|
||||
Some(&false),
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
Some(false),
|
||||
);
|
||||
assert_eq!(
|
||||
parsed
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries.get("guardian_approval")),
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -3087,15 +3087,15 @@ async fn set_feature_enabled_profile_disable_overrides_root_enable() -> anyhow::
|
||||
parsed
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries.get("guardian_approval")),
|
||||
Some(&true),
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
Some(true),
|
||||
);
|
||||
assert_eq!(
|
||||
profile
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries.get("guardian_approval")),
|
||||
Some(&false),
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
Some(false),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -4520,6 +4520,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
multi_agent_v2: MultiAgentV2Config::default(),
|
||||
features: Features::with_defaults().into(),
|
||||
suppress_unstable_features_warning: false,
|
||||
active_profile: Some("o3".to_string()),
|
||||
@@ -4665,6 +4666,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
multi_agent_v2: MultiAgentV2Config::default(),
|
||||
features: Features::with_defaults().into(),
|
||||
suppress_unstable_features_warning: false,
|
||||
active_profile: Some("gpt3".to_string()),
|
||||
@@ -4808,6 +4810,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
multi_agent_v2: MultiAgentV2Config::default(),
|
||||
features: Features::with_defaults().into(),
|
||||
suppress_unstable_features_warning: false,
|
||||
active_profile: Some("zdr".to_string()),
|
||||
@@ -4937,6 +4940,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
multi_agent_v2: MultiAgentV2Config::default(),
|
||||
features: Features::with_defaults().into(),
|
||||
suppress_unstable_features_warning: false,
|
||||
active_profile: Some("gpt5".to_string()),
|
||||
@@ -6096,6 +6100,71 @@ smart_approvals = true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_config_from_feature_table() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features.multi_agent_v2]
|
||||
enabled = true
|
||||
usage_hint_enabled = false
|
||||
usage_hint_text = "Custom delegation guidance."
|
||||
hide_spawn_agent_metadata = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert!(config.features.enabled(Feature::MultiAgentV2));
|
||||
assert!(!config.multi_agent_v2.usage_hint_enabled);
|
||||
assert_eq!(
|
||||
config.multi_agent_v2.usage_hint_text.as_deref(),
|
||||
Some("Custom delegation guidance.")
|
||||
);
|
||||
assert!(config.multi_agent_v2.hide_spawn_agent_metadata);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn profile_multi_agent_v2_config_overrides_base() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"profile = "no_hint"
|
||||
|
||||
[features.multi_agent_v2]
|
||||
usage_hint_enabled = true
|
||||
usage_hint_text = "base hint"
|
||||
hide_spawn_agent_metadata = true
|
||||
|
||||
[profiles.no_hint.features.multi_agent_v2]
|
||||
usage_hint_enabled = false
|
||||
usage_hint_text = "profile hint"
|
||||
hide_spawn_agent_metadata = false
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert!(!config.multi_agent_v2.usage_hint_enabled);
|
||||
assert_eq!(
|
||||
config.multi_agent_v2.usage_hint_text.as_deref(),
|
||||
Some("profile hint")
|
||||
);
|
||||
assert!(!config.multi_agent_v2.hide_spawn_agent_metadata);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn feature_requirements_normalize_runtime_feature_mutations() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -202,9 +202,9 @@ fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature
|
||||
let mut explicit_settings = Vec::new();
|
||||
|
||||
if let Some(features) = cfg.features.as_ref() {
|
||||
for (key, enabled) in &features.entries {
|
||||
if let Some(feature) = feature_for_key(key) {
|
||||
explicit_settings.push((format!("features.{key}"), feature, *enabled));
|
||||
for (key, enabled) in features.entries() {
|
||||
if let Some(feature) = feature_for_key(&key) {
|
||||
explicit_settings.push((format!("features.{key}"), feature, enabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,12 +224,12 @@ fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature
|
||||
}
|
||||
for (profile_name, profile) in &cfg.profiles {
|
||||
if let Some(features) = profile.features.as_ref() {
|
||||
for (key, enabled) in &features.entries {
|
||||
if let Some(feature) = feature_for_key(key) {
|
||||
for (key, enabled) in features.entries() {
|
||||
if let Some(feature) = feature_for_key(&key) {
|
||||
explicit_settings.push((
|
||||
format!("profiles.{profile_name}.features.{key}"),
|
||||
feature,
|
||||
*enabled,
|
||||
enabled,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,10 @@ use codex_config::types::WindowsSandboxModeToml;
|
||||
use codex_features::Feature;
|
||||
use codex_features::FeatureConfigSource;
|
||||
use codex_features::FeatureOverrides;
|
||||
use codex_features::FeatureToml;
|
||||
use codex_features::Features;
|
||||
use codex_features::FeaturesToml;
|
||||
use codex_features::MultiAgentV2ConfigToml;
|
||||
use codex_login::AuthManagerConfig;
|
||||
use codex_mcp::McpConfig;
|
||||
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
|
||||
@@ -520,6 +523,9 @@ pub struct Config {
|
||||
/// Settings for ghost snapshots (used for undo).
|
||||
pub ghost_snapshot: GhostSnapshotConfig,
|
||||
|
||||
/// Settings specific to the task-path-based multi-agent tool surface.
|
||||
pub multi_agent_v2: MultiAgentV2Config,
|
||||
|
||||
/// Centralized feature flags; source of truth for feature gating.
|
||||
pub features: ManagedFeatures,
|
||||
|
||||
@@ -564,6 +570,23 @@ pub struct Config {
|
||||
pub otel: codex_config::types::OtelConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MultiAgentV2Config {
|
||||
pub usage_hint_enabled: bool,
|
||||
pub usage_hint_text: Option<String>,
|
||||
pub hide_spawn_agent_metadata: bool,
|
||||
}
|
||||
|
||||
impl Default for MultiAgentV2Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
usage_hint_enabled: true,
|
||||
usage_hint_text: None,
|
||||
hide_spawn_agent_metadata: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthManagerConfig for Config {
|
||||
fn codex_home(&self) -> PathBuf {
|
||||
self.codex_home.clone()
|
||||
@@ -1279,6 +1302,42 @@ fn resolve_web_search_config(
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_multi_agent_v2_config(
|
||||
config_toml: &ConfigToml,
|
||||
config_profile: &ConfigProfile,
|
||||
) -> MultiAgentV2Config {
|
||||
let base = multi_agent_v2_toml_config(config_toml.features.as_ref());
|
||||
let profile = multi_agent_v2_toml_config(config_profile.features.as_ref());
|
||||
let default = MultiAgentV2Config::default();
|
||||
|
||||
let usage_hint_enabled = profile
|
||||
.and_then(|config| config.usage_hint_enabled)
|
||||
.or_else(|| base.and_then(|config| config.usage_hint_enabled))
|
||||
.unwrap_or(default.usage_hint_enabled);
|
||||
let usage_hint_text = profile
|
||||
.and_then(|config| config.usage_hint_text.as_ref())
|
||||
.or_else(|| base.and_then(|config| config.usage_hint_text.as_ref()))
|
||||
.cloned()
|
||||
.or(default.usage_hint_text);
|
||||
let hide_spawn_agent_metadata = profile
|
||||
.and_then(|config| config.hide_spawn_agent_metadata)
|
||||
.or_else(|| base.and_then(|config| config.hide_spawn_agent_metadata))
|
||||
.unwrap_or(default.hide_spawn_agent_metadata);
|
||||
|
||||
MultiAgentV2Config {
|
||||
usage_hint_enabled,
|
||||
usage_hint_text,
|
||||
hide_spawn_agent_metadata,
|
||||
}
|
||||
}
|
||||
|
||||
fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiAgentV2ConfigToml> {
|
||||
match features?.multi_agent_v2.as_ref()? {
|
||||
FeatureToml::Enabled(_) => None,
|
||||
FeatureToml::Config(config) => Some(config),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
web_search_mode: &Constrained<WebSearchMode>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
@@ -1607,6 +1666,7 @@ impl Config {
|
||||
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features)
|
||||
.unwrap_or(WebSearchMode::Cached);
|
||||
let web_search_config = resolve_web_search_config(&cfg, &config_profile);
|
||||
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg, &config_profile);
|
||||
|
||||
let agent_roles =
|
||||
agent_roles::load_agent_roles(&cfg, &config_layer_stack, &mut startup_warnings)?;
|
||||
@@ -2040,6 +2100,7 @@ impl Config {
|
||||
use_experimental_unified_exec_tool,
|
||||
background_terminal_max_timeout,
|
||||
ghost_snapshot,
|
||||
multi_agent_v2,
|
||||
features,
|
||||
suppress_unstable_features_warning: cfg
|
||||
.suppress_unstable_features_warning
|
||||
|
||||
Reference in New Issue
Block a user