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:
pakrym-oai
2026-04-08 08:42:18 -07:00
committed by GitHub
parent 2250fdd54a
commit 4c07dd4d25
18 changed files with 501 additions and 62 deletions

View File

@@ -0,0 +1,23 @@
use crate::FeatureConfig;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MultiAgentV2ConfigToml {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_hint_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_hint_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hide_spawn_agent_metadata: Option<bool>,
}
impl FeatureConfig for MultiAgentV2ConfigToml {
fn enabled(&self) -> Option<bool> {
self.enabled
}
}

View File

@@ -14,7 +14,9 @@ use std::collections::BTreeMap;
use std::collections::BTreeSet;
use toml::Table;
mod feature_configs;
mod legacy;
pub use feature_configs::MultiAgentV2ConfigToml;
use legacy::LegacyFeatureToggles;
pub use legacy::legacy_feature_keys;
@@ -138,8 +140,6 @@ pub enum Feature {
Collab,
/// Enable task-path-based multi-agent routing.
MultiAgentV2,
/// Hide spawn_agent agent/model override fields from the model-visible tool schema.
DebugHideSpawnAgentMetadata,
/// Enable CSV-backed agent job tools.
SpawnCsv,
/// Enable apps.
@@ -398,7 +398,7 @@ impl Features {
.apply(&mut features);
if let Some(feature_entries) = source.features {
features.apply_map(&feature_entries.entries);
features.apply_toml(feature_entries);
}
}
@@ -492,8 +492,61 @@ pub fn is_known_feature_key(key: &str) -> bool {
/// Deserializable features table for TOML.
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
pub struct FeaturesToml {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub multi_agent_v2: Option<FeatureToml<MultiAgentV2ConfigToml>>,
/// Boolean feature toggles keyed by canonical or legacy feature name.
#[serde(flatten)]
pub entries: BTreeMap<String, bool>,
entries: BTreeMap<String, bool>,
}
impl Features {
fn apply_toml(&mut self, features: &FeaturesToml) {
let entries = features.entries();
self.apply_map(&entries);
}
}
impl FeaturesToml {
pub fn entries(&self) -> BTreeMap<String, bool> {
let mut entries = self.entries.clone();
if let Some(enabled) = self.multi_agent_v2.as_ref().and_then(FeatureToml::enabled) {
entries.insert(Feature::MultiAgentV2.key().to_string(), enabled);
}
entries
}
}
impl From<BTreeMap<String, bool>> for FeaturesToml {
fn from(entries: BTreeMap<String, bool>) -> Self {
Self {
entries,
..Default::default()
}
}
}
// To be used for features that need more configuration than just enabled/disabled and
// require a custom config struct under `[features]`.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
#[serde(untagged)]
pub enum FeatureToml<T> {
Enabled(bool),
Config(T),
}
impl<T: FeatureConfig> FeatureToml<T> {
pub fn enabled(&self) -> Option<bool> {
match self {
Self::Enabled(enabled) => Some(*enabled),
Self::Config(config) => config.enabled(),
}
}
}
// A trait to be implemented by custom feature config structs when defining a feature that needs more configuration than
// just enabled/disabled.
pub trait FeatureConfig {
fn enabled(&self) -> Option<bool>;
}
/// Single, easy-to-read registry of all feature definitions.
@@ -708,12 +761,6 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::DebugHideSpawnAgentMetadata,
key: "debug_hide_spawn_agent_metadata",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::SpawnCsv,
key: "enable_fanout",

View File

@@ -1,6 +1,7 @@
use crate::Feature;
use crate::FeatureConfigSource;
use crate::FeatureOverrides;
use crate::FeatureToml;
use crate::Features;
use crate::FeaturesToml;
use crate::Stage;
@@ -211,12 +212,14 @@ fn from_sources_applies_base_profile_and_overrides() {
base_entries.insert("plugins".to_string(), true);
let base_features = FeaturesToml {
entries: base_entries,
..Default::default()
};
let mut profile_entries = BTreeMap::new();
profile_entries.insert("code_mode_only".to_string(), true);
let profile_features = FeaturesToml {
entries: profile_entries,
..Default::default()
};
let features = Features::from_sources(
@@ -242,6 +245,81 @@ fn from_sources_applies_base_profile_and_overrides() {
assert_eq!(features.enabled(Feature::WebSearchRequest), false);
}
#[test]
fn multi_agent_v2_feature_config_deserializes_boolean_toggle() {
let features: FeaturesToml = toml::from_str(
r#"
multi_agent_v2 = true
"#,
)
.expect("features table should deserialize");
assert_eq!(
features.entries(),
BTreeMap::from([("multi_agent_v2".to_string(), true)])
);
assert_eq!(features.multi_agent_v2, Some(FeatureToml::Enabled(true)));
}
#[test]
fn multi_agent_v2_feature_config_deserializes_table() {
let features: FeaturesToml = toml::from_str(
r#"
[multi_agent_v2]
enabled = true
usage_hint_enabled = false
usage_hint_text = "Custom delegation guidance."
hide_spawn_agent_metadata = true
"#,
)
.expect("features table should deserialize");
assert_eq!(
features.entries(),
BTreeMap::from([("multi_agent_v2".to_string(), true)])
);
assert_eq!(
features.multi_agent_v2,
Some(crate::FeatureToml::Config(crate::MultiAgentV2ConfigToml {
enabled: Some(true),
usage_hint_enabled: Some(false),
usage_hint_text: Some("Custom delegation guidance.".to_string()),
hide_spawn_agent_metadata: Some(true),
}))
);
}
#[test]
fn multi_agent_v2_feature_config_usage_hint_enabled_does_not_enable_feature() {
let features_toml: FeaturesToml = toml::from_str(
r#"
[multi_agent_v2]
usage_hint_enabled = false
"#,
)
.expect("features table should deserialize");
let features = Features::from_sources(
FeatureConfigSource {
features: Some(&features_toml),
..Default::default()
},
FeatureConfigSource::default(),
FeatureOverrides::default(),
);
assert_eq!(features.enabled(Feature::MultiAgentV2), false);
assert_eq!(features_toml.entries(), BTreeMap::new());
assert_eq!(
features_toml.multi_agent_v2,
Some(crate::FeatureToml::Config(crate::MultiAgentV2ConfigToml {
enabled: None,
usage_hint_enabled: Some(false),
usage_hint_text: None,
hide_spawn_agent_metadata: None,
}))
);
}
#[test]
fn unstable_warning_event_only_mentions_enabled_under_development_features() {
let mut configured_features = Table::new();