diff --git a/crates/forge_config/.forge.toml b/crates/forge_config/.forge.toml index 45a08e9231..8742b1c283 100644 --- a/crates/forge_config/.forge.toml +++ b/crates/forge_config/.forge.toml @@ -28,6 +28,7 @@ tool_timeout_secs = 300 top_k = 30 top_p = 0.8 verify_todos = true +research_subagent = false [retry] backoff_factor = 2 diff --git a/crates/forge_config/src/config.rs b/crates/forge_config/src/config.rs index 6b9baaa213..4bd19b099c 100644 --- a/crates/forge_config/src/config.rs +++ b/crates/forge_config/src/config.rs @@ -281,6 +281,13 @@ pub struct ForgeConfig { /// when a task ends and reminds the LLM about them. #[serde(default)] pub verify_todos: bool, + + /// Whether the deep research agent is available. + /// + /// When set to `true`, the Sage agent is added to the agent list and + /// the `:sage` app command is enabled. Defaults to `false`. + #[serde(default)] + pub research_subagent: bool, } impl ForgeConfig { diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index 9967c7e317..b128024785 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -2014,7 +2014,11 @@ impl A + Send + Sync> UI self.on_agent_change(AgentId::MUSE).await?; } AppCommand::Sage => { - self.on_agent_change(AgentId::SAGE).await?; + if !self.config.research_subagent { + self.writeln("Sage agent is disabled. Set `research_subagent = true` in .forge.toml to enable it.")?; + } else { + self.on_agent_change(AgentId::SAGE).await?; + } } AppCommand::Help => { let info = Info::from(self.command.as_ref()); diff --git a/crates/forge_repo/src/agent.rs b/crates/forge_repo/src/agent.rs index 2e225e8eb9..cc24938206 100644 --- a/crates/forge_repo/src/agent.rs +++ b/crates/forge_repo/src/agent.rs @@ -162,16 +162,17 @@ impl + DirectoryReader AgentRepository for ForgeAgentRepository { async fn get_agents(&self) -> anyhow::Result> { + let config = self.infra.get_config()?; let agent_defs = self.load_agents().await?; - let session = self - .infra - .get_config()? + let session = config .session + .clone() .ok_or(forge_domain::Error::NoDefaultSession)?; Ok(agent_defs .into_iter() + .filter(|def| filter_agent(def, &config)) .map(|def| { def.into_agent( ProviderId::from(session.provider_id.clone()), @@ -182,9 +183,11 @@ impl + DirectoryReader } async fn get_agent_infos(&self) -> anyhow::Result> { + let config = self.infra.get_config()?; let agent_defs = self.load_agents().await?; Ok(agent_defs .into_iter() + .filter(|def| filter_agent(def, &config)) .map(|def| forge_domain::AgentInfo { id: def.id, title: def.title, @@ -194,6 +197,15 @@ impl + DirectoryReader } } +/// Returns `false` for agents that are disabled by a feature flag in the +/// configuration, `true` for all others. +fn filter_agent(def: &AgentDefinition, config: &ForgeConfig) -> bool { + if def.id.as_str() == forge_domain::AgentId::SAGE.as_str() && !config.research_subagent { + return false; + } + true +} + #[cfg(test)] mod tests { use pretty_assertions::assert_eq; diff --git a/forge.schema.json b/forge.schema.json index 43cc190609..00409d79b3 100644 --- a/forge.schema.json +++ b/forge.schema.json @@ -241,6 +241,11 @@ } ] }, + "research_subagent": { + "description": "Whether the deep research agent is available.\n\nWhen set to `true`, the Sage agent is added to the agent list and\nthe `:sage` app command is enabled. Defaults to `false`.", + "type": "boolean", + "default": false + }, "restricted": { "description": "Whether restricted mode is active; when enabled, tool execution requires\nexplicit permission grants.", "type": "boolean",