Split spawn_csv from multi_agent (#14282)

- make `spawn_csv` a standalone feature for CSV agent jobs
- keep `spawn_csv -> multi_agent` one-way and preserve restricted
subagent disable paths
This commit is contained in:
Ahmed Ibrahim
2026-03-10 18:42:50 -07:00
committed by Michael Bolin
parent 39c1bc1c68
commit a4d884c767
9 changed files with 72 additions and 6 deletions

View File

@@ -385,6 +385,7 @@ impl Codex {
if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = session_source
&& depth >= config.agent_max_depth
{
let _ = config.features.disable(Feature::SpawnCsv);
let _ = config.features.disable(Feature::Collab);
}

View File

@@ -138,6 +138,8 @@ pub enum Feature {
EnableRequestCompression,
/// Enable collab tools.
Collab,
/// Enable CSV-backed agent job tools.
SpawnCsv,
/// Enable apps.
Apps,
/// Enable plugins.
@@ -414,6 +416,9 @@ impl Features {
}
pub(crate) fn normalize_dependencies(&mut self) {
if self.enabled(Feature::SpawnCsv) && !self.enabled(Feature::Collab) {
self.enable(Feature::Collab);
}
if self.enabled(Feature::JsReplToolsOnly) && !self.enabled(Feature::JsRepl) {
tracing::warn!("js_repl_tools_only requires js_repl; disabling js_repl_tools_only");
self.disable(Feature::JsReplToolsOnly);
@@ -693,6 +698,12 @@ pub const FEATURES: &[FeatureSpec] = &[
},
default_enabled: false,
},
FeatureSpec {
id: Feature::SpawnCsv,
key: "spawn_csv",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::Apps,
key: "apps",
@@ -997,6 +1008,27 @@ mod tests {
assert_eq!(feature_for_key("collab"), Some(Feature::Collab));
}
#[test]
fn spawn_csv_is_under_development() {
assert_eq!(Feature::SpawnCsv.stage(), Stage::UnderDevelopment);
assert_eq!(Feature::SpawnCsv.default_enabled(), false);
}
#[test]
fn spawn_csv_normalization_enables_multi_agent_one_way() {
let mut spawn_csv_features = Features::with_defaults();
spawn_csv_features.enable(Feature::SpawnCsv);
spawn_csv_features.normalize_dependencies();
assert_eq!(spawn_csv_features.enabled(Feature::SpawnCsv), true);
assert_eq!(spawn_csv_features.enabled(Feature::Collab), true);
let mut collab_features = Features::with_defaults();
collab_features.enable(Feature::Collab);
collab_features.normalize_dependencies();
assert_eq!(collab_features.enabled(Feature::Collab), true);
assert_eq!(collab_features.enabled(Feature::SpawnCsv), false);
}
#[test]
fn apps_require_feature_flag_and_chatgpt_auth() {
let mut features = Features::with_defaults();

View File

@@ -687,6 +687,7 @@ fn build_guardian_subagent_config(
)?);
}
for feature in [
Feature::SpawnCsv,
Feature::Collab,
Feature::WebSearchRequest,
Feature::WebSearchCached,

View File

@@ -270,6 +270,7 @@ mod agent {
// Approval policy
agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
// Consolidation runs as an internal sub-agent and must not recursively delegate.
let _ = agent_config.features.disable(Feature::SpawnCsv);
let _ = agent_config.features.disable(Feature::Collab);
// Sandbox policy

View File

@@ -100,6 +100,7 @@ async fn start_review_conversation(
{
panic!("by construction Constrained<WebSearchMode> must always support Disabled: {err}");
}
let _ = sub_agent_config.features.disable(Feature::SpawnCsv);
let _ = sub_agent_config.features.disable(Feature::Collab);
// Set explicit review rubric for the sub-agent

View File

@@ -974,6 +974,7 @@ fn apply_spawn_agent_runtime_overrides(
fn apply_spawn_agent_overrides(config: &mut Config, child_depth: i32) {
if child_depth >= config.agent_max_depth {
let _ = config.features.disable(Feature::SpawnCsv);
let _ = config.features.disable(Feature::Collab);
}
}

View File

@@ -135,6 +135,7 @@ impl ToolsConfig {
let include_js_repl_tools_only =
include_js_repl && features.enabled(Feature::JsReplToolsOnly);
let include_collab_tools = features.enabled(Feature::Collab);
let include_agent_jobs = features.enabled(Feature::SpawnCsv);
let include_request_user_input = !matches!(session_source, SessionSource::SubAgent(_));
let include_default_mode_request_user_input =
include_request_user_input && features.enabled(Feature::DefaultModeRequestUserInput);
@@ -143,7 +144,6 @@ impl ToolsConfig {
features.enabled(Feature::Artifact) && codex_artifacts::can_manage_artifact_runtime();
let include_image_gen_tool =
features.enabled(Feature::ImageGeneration) && supports_image_generation(model_info);
let include_agent_jobs = include_collab_tools;
let request_permission_enabled = features.enabled(Feature::RequestPermissions);
let request_permissions_tool_enabled = features.enabled(Feature::RequestPermissionsTool);
let shell_command_backend =
@@ -2631,6 +2631,28 @@ mod tests {
session_source: SessionSource::Cli,
});
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
assert_contains_tool_names(
&tools,
&["spawn_agent", "send_input", "wait", "close_agent"],
);
assert_lacks_tool_name(&tools, "spawn_agents_on_csv");
}
#[test]
fn test_build_specs_spawn_csv_enables_agent_jobs_and_collab_tools() {
let config = test_config();
let model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::SpawnCsv);
features.normalize_dependencies();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
});
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
assert_contains_tool_names(
&tools,
&[
@@ -2668,7 +2690,8 @@ mod tests {
let model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::Collab);
features.enable(Feature::SpawnCsv);
features.normalize_dependencies();
features.enable(Feature::Sqlite);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,