From 9034c3d4e54fd652e7266e0e7a588d01e8566ba5 Mon Sep 17 00:00:00 2001 From: Amit Singh Date: Fri, 10 Apr 2026 08:44:44 +0530 Subject: [PATCH 1/6] feat(agent): add enable_subagents feature flag for task/sage tool switching --- crates/forge_app/src/system_prompt.rs | 10 +- crates/forge_config/.forge.toml | 1 + crates/forge_config/src/config.rs | 7 ++ crates/forge_repo/src/agent.rs | 119 +++++++++++++++--- crates/forge_repo/src/agents/forge.md | 11 +- ..._agent_file_subagents_disabled_prompt.snap | 5 + ...e_agent_file_subagents_disabled_tools.snap | 6 + ...se_agent_file_subagents_enabled_tools.snap | 6 + forge.schema.json | 5 + 9 files changed, 151 insertions(+), 19 deletions(-) create mode 100644 crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_prompt.snap create mode 100644 crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_tools.snap create mode 100644 crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_enabled_tools.snap diff --git a/crates/forge_app/src/system_prompt.rs b/crates/forge_app/src/system_prompt.rs index f4deb24643..a692f55bbf 100644 --- a/crates/forge_app/src/system_prompt.rs +++ b/crates/forge_app/src/system_prompt.rs @@ -97,12 +97,20 @@ impl SystemPrompt { // Fetch extension statistics from git let extensions = self.fetch_extensions(self.max_extensions).await; - // Build tool_names map from all available tools for template rendering + // Build tool_names map filtered to only the tools this agent actually has. + // This allows templates to use {{#if tool_names.task}} to conditionally + // render content based on whether the agent has access to a given tool. + let agent_tool_names: std::collections::HashSet = self + .tool_definitions + .iter() + .map(|def| def.name.to_string()) + .collect(); let tool_names: Map = ToolCatalog::iter() .map(|tool| { let def = tool.definition(); (def.name.to_string(), json!(def.name.to_string())) }) + .filter(|(name, _)| agent_tool_names.contains(name)) .collect(); let ctx = SystemContext { diff --git a/crates/forge_config/.forge.toml b/crates/forge_config/.forge.toml index 1abdbc915c..2e1e390c63 100644 --- a/crates/forge_config/.forge.toml +++ b/crates/forge_config/.forge.toml @@ -27,6 +27,7 @@ tool_timeout_secs = 300 top_k = 30 top_p = 0.8 verify_todos = true +enable_subagents = true [retry] backoff_factor = 2 diff --git a/crates/forge_config/src/config.rs b/crates/forge_config/src/config.rs index 6b9baaa213..e282c26bd5 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, + + /// Enables subagent support via the task tool; when true the forge agent + /// gains access to the `task` tool for delegating work to specialised + /// sub-agents, and the `sage` research-only agent tool is removed. + /// When false the `task` tool is disabled and `sage` is available instead. + #[serde(default)] + pub enable_subagents: bool, } impl ForgeConfig { diff --git a/crates/forge_repo/src/agent.rs b/crates/forge_repo/src/agent.rs index 2e225e8eb9..32ab8378f3 100644 --- a/crates/forge_repo/src/agent.rs +++ b/crates/forge_repo/src/agent.rs @@ -1,11 +1,14 @@ use std::sync::Arc; use anyhow::{Context, Result}; -use forge_app::{AgentRepository, DirectoryReaderInfra, EnvironmentInfra, FileInfoInfra}; +use forge_app::{ + AgentRepository, DirectoryReaderInfra, EnvironmentInfra, FileInfoInfra, TemplateEngine, +}; use forge_config::ForgeConfig; use forge_domain::{ModelId, ProviderId, Template}; use gray_matter::Matter; use gray_matter::engine::YAML; +use serde::Serialize; use crate::agent_definition::AgentDefinition; @@ -41,26 +44,29 @@ impl ForgeAgentRepository { } } -impl ForgeAgentRepository { +impl + DirectoryReaderInfra> + ForgeAgentRepository +{ /// Load all agent definitions from all available sources with conflict /// resolution. async fn load_agents(&self) -> anyhow::Result> { - self.load_all_agents().await + let config = self.infra.get_config()?; + self.load_all_agents(&config).await } /// Load all agent definitions from all available sources - async fn load_all_agents(&self) -> anyhow::Result> { + async fn load_all_agents(&self, config: &ForgeConfig) -> anyhow::Result> { // Load built-in agents (no path - will display as "BUILT IN") - let mut agents = self.init_default().await?; + let mut agents = self.init_default(config).await?; // Load custom agents from global directory let dir = self.infra.get_environment().agent_path(); - let custom_agents = self.init_agent_dir(&dir).await?; + let custom_agents = self.init_agent_dir(&dir, config).await?; agents.extend(custom_agents); // Load custom agents from CWD let dir = self.infra.get_environment().agent_cwd_path(); - let cwd_agents = self.init_agent_dir(&dir).await?; + let cwd_agents = self.init_agent_dir(&dir, config).await?; agents.extend(cwd_agents); // Handle agent ID conflicts by keeping the last occurrence @@ -68,7 +74,7 @@ impl ForgeAgentRepos Ok(resolve_agent_conflicts(agents)) } - async fn init_default(&self) -> anyhow::Result> { + async fn init_default(&self, config: &ForgeConfig) -> anyhow::Result> { parse_agent_iter( [ ("forge", include_str!("agents/forge.md")), @@ -77,10 +83,15 @@ impl ForgeAgentRepos ] .into_iter() .map(|(name, content)| (name.to_string(), content.to_string())), + config, ) } - async fn init_agent_dir(&self, dir: &std::path::Path) -> anyhow::Result> { + async fn init_agent_dir( + &self, + dir: &std::path::Path, + config: &ForgeConfig, + ) -> anyhow::Result> { if !self.infra.exists(dir).await? { return Ok(vec![]); } @@ -94,7 +105,7 @@ impl ForgeAgentRepos let mut agents = Vec::new(); for (path, content) in files { - let mut agent = parse_agent_file(&content) + let mut agent = parse_agent_file(&content, config) .with_context(|| format!("Failed to parse agent: {}", path.display()))?; // Store the file path @@ -126,6 +137,7 @@ fn resolve_agent_conflicts(agents: Vec) -> Vec fn parse_agent_iter, Content: AsRef>( contents: I, + config: &ForgeConfig, ) -> anyhow::Result> where I: Iterator, @@ -133,7 +145,7 @@ where let mut agents = vec![]; for (name, content) in contents { - let agent = parse_agent_file(content.as_ref()) + let agent = parse_agent_file(content.as_ref(), config) .with_context(|| format!("Failed to parse agent: {}", name.as_ref()))?; agents.push(agent); @@ -142,11 +154,40 @@ where Ok(agents) } +#[derive(Serialize)] +struct AgentTemplateContext<'a> { + config: &'a ForgeConfig, +} + +fn render_agent_frontmatter(content: &str, config: &ForgeConfig) -> Result { + let Some((newline, content)) = content + .strip_prefix("---\r\n") + .map(|content| ("\r\n", content)) + .or_else(|| content.strip_prefix("---\n").map(|content| ("\n", content))) + else { + return Ok(content.to_string()); + }; + + let delimiter = format!("{newline}---{newline}"); + let (frontmatter, body) = content + .split_once(&delimiter) + .context("Failed to find end of agent frontmatter")?; + + let rendered_frontmatter = TemplateEngine::default().render_template( + Template::new(frontmatter), + &AgentTemplateContext { config }, + )?; + + Ok(format!("---{newline}{rendered_frontmatter}{delimiter}{body}")) +} + /// Parse raw content into an AgentDefinition with YAML frontmatter -fn parse_agent_file(content: &str) -> Result { +fn parse_agent_file(content: &str, config: &ForgeConfig) -> Result { + let rendered_content = render_agent_frontmatter(content, config)?; + // Parse the frontmatter using gray_matter with type-safe deserialization let gray_matter = Matter::::new(); - let result = gray_matter.parse::(content)?; + let result = gray_matter.parse::(&rendered_content)?; // Extract the frontmatter let agent = result @@ -196,6 +237,8 @@ impl + DirectoryReader #[cfg(test)] mod tests { + use forge_domain::AgentId; + use insta::{assert_snapshot, assert_yaml_snapshot}; use pretty_assertions::assert_eq; use super::*; @@ -204,7 +247,7 @@ mod tests { async fn test_parse_basic_agent() { let content = forge_test_kit::fixture!("/src/fixtures/agents/basic.md").await; - let actual = parse_agent_file(&content).unwrap(); + let actual = parse_agent_file(&content, &ForgeConfig::default()).unwrap(); assert_eq!(actual.id.as_str(), "test-basic"); assert_eq!(actual.title.as_ref().unwrap(), "Basic Test Agent"); @@ -222,7 +265,7 @@ mod tests { async fn test_parse_advanced_agent() { let content = forge_test_kit::fixture!("/src/fixtures/agents/advanced.md").await; - let actual = parse_agent_file(&content).unwrap(); + let actual = parse_agent_file(&content, &ForgeConfig::default()).unwrap(); assert_eq!(actual.id.as_str(), "test-advanced"); assert_eq!(actual.title.as_ref().unwrap(), "Advanced Test Agent"); @@ -231,4 +274,50 @@ mod tests { "An advanced test agent with full configuration" ); } + + #[test] + fn test_parse_agent_file_renders_conditional_frontmatter_when_subagents_enabled() { + let fixture = r#"--- +id: "test" +tools: + - read + {{#if config.subagent_enabled}} + - task + {{else}} + - sage + {{/if}} +--- +Body keeps {{tool_names.read}} untouched. +"#; + let config = ForgeConfig { enable_subagents: true, ..Default::default() }; + + let actual = parse_agent_file(fixture, &config).unwrap(); + + assert_eq!(actual.id, AgentId::new("test")); + assert_eq!(actual.system_prompt.unwrap().template, "Body keeps {{tool_names.read}} untouched."); + assert_yaml_snapshot!("parse_agent_file_subagents_enabled_tools", actual.tools); + } + + #[test] + fn test_parse_agent_file_renders_conditional_frontmatter_when_subagents_disabled() { + let fixture = r#"--- +id: "test" +tools: + - read + {{#if config.subagent_enabled}} + - task + {{else}} + - sage + {{/if}} +--- +Body keeps {{tool_names.read}} untouched. +"#; + let config = ForgeConfig { enable_subagents: false, ..Default::default() }; + + let actual = parse_agent_file(fixture, &config).unwrap(); + + assert_eq!(actual.id, AgentId::new("test")); + assert_snapshot!("parse_agent_file_subagents_disabled_prompt", actual.system_prompt.unwrap().template); + assert_yaml_snapshot!("parse_agent_file_subagents_disabled_tools", actual.tools); + } } diff --git a/crates/forge_repo/src/agents/forge.md b/crates/forge_repo/src/agents/forge.md index 1bb59d4b4e..27c7f63e33 100644 --- a/crates/forge_repo/src/agents/forge.md +++ b/crates/forge_repo/src/agents/forge.md @@ -5,7 +5,6 @@ description: "Hands-on implementation agent that executes software development t reasoning: enabled: true tools: - - task - sem_search - fs_search - read @@ -19,6 +18,11 @@ tools: - skill - todo_write - todo_read + {{#if config.enable_subagents}} + - task + {{else}} + - sage + {{/if}} - mcp_* user_prompt: |- <{{event.name}}>{{event.value}} @@ -127,9 +131,10 @@ Choose tools based on the nature of the task: - **Read**: When you already know the file location and need to examine its contents. - You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. Never use placeholders or guess missing parameters in tool calls. -- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple {{tool_names.task}} tool calls. +{{#if tool_names.task}}- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple {{tool_names.task}} tool calls.{{/if}} - Use specialized tools instead of shell commands when possible. For file operations, use dedicated tools: {{tool_names.read}} for reading files instead of cat/head/tail, {{tool_names.patch}} for editing instead of sed/awk, and {{tool_names.write}} for creating files instead of echo redirection. Reserve {{tool_names.shell}} exclusively for actual system commands and terminal operations that require shell execution. -- When NOT to use the {{tool_names.task}} tool: Do NOT launch a sub-agent for initial codebase exploration or simple lookups. Always use semantic search directly first. +{{#if tool_names.task}}- When NOT to use the {{tool_names.task}} tool: Do NOT launch a sub-agent for initial codebase exploration or simple lookups. Always use semantic search directly first.{{/if}} +{{#if tool_names.sage}}- Use the {{tool_names.sage}} tool for deep research tasks that require comprehensive, read-only investigation across multiple files. Do NOT use it for code modifications — choose direct tools instead.{{/if}} ## Code Output Guidelines: diff --git a/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_prompt.snap b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_prompt.snap new file mode 100644 index 0000000000..f6f147b36b --- /dev/null +++ b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_prompt.snap @@ -0,0 +1,5 @@ +--- +source: crates/forge_repo/src/agent.rs +expression: actual.system_prompt.unwrap().template +--- +Body keeps {{tool_names.read}} untouched. diff --git a/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_tools.snap b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_tools.snap new file mode 100644 index 0000000000..8ba311fd0c --- /dev/null +++ b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_tools.snap @@ -0,0 +1,6 @@ +--- +source: crates/forge_repo/src/agent.rs +expression: actual.tools +--- +- read +- sage diff --git a/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_enabled_tools.snap b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_enabled_tools.snap new file mode 100644 index 0000000000..4a64bd2697 --- /dev/null +++ b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_enabled_tools.snap @@ -0,0 +1,6 @@ +--- +source: crates/forge_repo/src/agent.rs +expression: actual.tools +--- +- read +- task diff --git a/forge.schema.json b/forge.schema.json index 43cc190609..d0dd290974 100644 --- a/forge.schema.json +++ b/forge.schema.json @@ -66,6 +66,11 @@ "null" ] }, + "enable_subagents": { + "description": "Enables subagent support via the task tool; when true the forge agent\ngains access to the `task` tool for delegating work to specialised\nsub-agents, and the `sage` research-only agent tool is removed.\nWhen false the `task` tool is disabled and `sage` is available instead.", + "type": "boolean", + "default": false + }, "http": { "description": "HTTP client settings including proxy, TLS, and timeout configuration.", "anyOf": [ From 5289333e0095c8b653a4b0ea7afb3d4893dff5cc Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 03:17:00 +0000 Subject: [PATCH 2/6] [autofix.ci] apply automated fixes --- crates/forge_repo/src/agent.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/forge_repo/src/agent.rs b/crates/forge_repo/src/agent.rs index 32ab8378f3..9ac86b2cda 100644 --- a/crates/forge_repo/src/agent.rs +++ b/crates/forge_repo/src/agent.rs @@ -173,12 +173,12 @@ fn render_agent_frontmatter(content: &str, config: &ForgeConfig) -> Result Date: Fri, 10 Apr 2026 09:04:26 +0530 Subject: [PATCH 3/6] chore(agent): rename subagent_enabled to enable_subagents and scope template rendering to tools block --- crates/forge_repo/src/agent.rs | 79 +++++++++++++++++-- ...eserves_runtime_user_prompt_variables.snap | 6 ++ ...s_runtime_user_prompt_variables_tools.snap | 6 ++ 3 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables.snap create mode 100644 crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables_tools.snap diff --git a/crates/forge_repo/src/agent.rs b/crates/forge_repo/src/agent.rs index 9ac86b2cda..7f78942d78 100644 --- a/crates/forge_repo/src/agent.rs +++ b/crates/forge_repo/src/agent.rs @@ -159,7 +159,7 @@ struct AgentTemplateContext<'a> { config: &'a ForgeConfig, } -fn render_agent_frontmatter(content: &str, config: &ForgeConfig) -> Result { +fn render_tools_frontmatter_block(content: &str, config: &ForgeConfig) -> Result { let Some((newline, content)) = content .strip_prefix("---\r\n") .map(|content| ("\r\n", content)) @@ -173,17 +173,52 @@ fn render_agent_frontmatter(content: &str, config: &ForgeConfig) -> Result Result { + let lines = frontmatter + .split_inclusive('\n') + .map(ToString::to_string) + .collect::>(); + + let Some(start) = lines.iter().position(|line| line.trim_end() == "tools:") else { + return Ok(frontmatter.to_string()); + }; + + let end = lines[start + 1..] + .iter() + .position(|line| { + let trimmed = line.trim_end(); + + !trimmed.is_empty() + && !line.starts_with([' ', '\t']) + && !trimmed.starts_with("#") + && !trimmed.starts_with("---") + }) + .map(|index| start + 1 + index) + .unwrap_or(lines.len()); + + let rendered_tools_block = TemplateEngine::default().render_template( + Template::new(&lines[start..end].join("")), + &AgentTemplateContext { config }, + )?; + + Ok(format!( + "{}{}{}", + lines[..start].join(""), + rendered_tools_block, + lines[end..].join("") + )) +} + /// Parse raw content into an AgentDefinition with YAML frontmatter fn parse_agent_file(content: &str, config: &ForgeConfig) -> Result { - let rendered_content = render_agent_frontmatter(content, config)?; + let rendered_content = render_tools_frontmatter_block(content, config)?; // Parse the frontmatter using gray_matter with type-safe deserialization let gray_matter = Matter::::new(); @@ -281,7 +316,7 @@ mod tests { id: "test" tools: - read - {{#if config.subagent_enabled}} + {{#if config.enable_subagents}} - task {{else}} - sage @@ -307,7 +342,7 @@ Body keeps {{tool_names.read}} untouched. id: "test" tools: - read - {{#if config.subagent_enabled}} + {{#if config.enable_subagents}} - task {{else}} - sage @@ -326,4 +361,36 @@ Body keeps {{tool_names.read}} untouched. ); assert_yaml_snapshot!("parse_agent_file_subagents_disabled_tools", actual.tools); } + + #[test] + fn test_parse_agent_file_preserves_runtime_user_prompt_variables() { + let fixture = r#"--- +id: "test" +tools: + - read + {{#if config.enable_subagents}} + - task + {{else}} + - sage + {{/if}} +user_prompt: |- + <{{event.name}}>{{event.value}} + {{current_date}} +--- +Body keeps {{tool_names.read}} untouched. +"#; + let config = ForgeConfig { enable_subagents: true, ..Default::default() }; + + let actual = parse_agent_file(fixture, &config).unwrap(); + + assert_eq!(actual.id, AgentId::new("test")); + assert_snapshot!( + "parse_agent_file_preserves_runtime_user_prompt_variables", + actual.user_prompt.unwrap().template + ); + assert_yaml_snapshot!( + "parse_agent_file_preserves_runtime_user_prompt_variables_tools", + actual.tools + ); + } } diff --git a/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables.snap b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables.snap new file mode 100644 index 0000000000..38a4cbbcfa --- /dev/null +++ b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables.snap @@ -0,0 +1,6 @@ +--- +source: crates/forge_repo/src/agent.rs +expression: actual.user_prompt.unwrap().template +--- +<{{event.name}}>{{event.value}} +{{current_date}} diff --git a/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables_tools.snap b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables_tools.snap new file mode 100644 index 0000000000..4a64bd2697 --- /dev/null +++ b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables_tools.snap @@ -0,0 +1,6 @@ +--- +source: crates/forge_repo/src/agent.rs +expression: actual.tools +--- +- read +- task From f09891a347a257b5eba4ebcf2b1c2a2a3490d55f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 03:37:07 +0000 Subject: [PATCH 4/6] [autofix.ci] apply automated fixes --- crates/forge_repo/src/agent.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge_repo/src/agent.rs b/crates/forge_repo/src/agent.rs index 7f78942d78..51dcd7afbd 100644 --- a/crates/forge_repo/src/agent.rs +++ b/crates/forge_repo/src/agent.rs @@ -204,7 +204,7 @@ fn render_tools_block(frontmatter: &str, config: &ForgeConfig) -> Result .unwrap_or(lines.len()); let rendered_tools_block = TemplateEngine::default().render_template( - Template::new(&lines[start..end].join("")), + Template::new(lines[start..end].join("")), &AgentTemplateContext { config }, )?; From ab1c0a158691847c11345b677a904cef28aaa688 Mon Sep 17 00:00:00 2001 From: Amit Singh Date: Fri, 10 Apr 2026 10:20:42 +0530 Subject: [PATCH 5/6] chore(agent): refactor subagent tool configuration to use explicit list manipulation --- crates/forge_repo/src/agent.rs | 130 ++++++------------ crates/forge_repo/src/agents/forge.md | 3 - ...s_runtime_user_prompt_variables_tools.snap | 3 +- ...e_agent_file_subagents_disabled_tools.snap | 1 + ...se_agent_file_subagents_enabled_tools.snap | 1 + 5 files changed, 46 insertions(+), 92 deletions(-) diff --git a/crates/forge_repo/src/agent.rs b/crates/forge_repo/src/agent.rs index 51dcd7afbd..9d77bf9883 100644 --- a/crates/forge_repo/src/agent.rs +++ b/crates/forge_repo/src/agent.rs @@ -1,14 +1,11 @@ use std::sync::Arc; use anyhow::{Context, Result}; -use forge_app::{ - AgentRepository, DirectoryReaderInfra, EnvironmentInfra, FileInfoInfra, TemplateEngine, -}; +use forge_app::{AgentRepository, DirectoryReaderInfra, EnvironmentInfra, FileInfoInfra}; use forge_config::ForgeConfig; -use forge_domain::{ModelId, ProviderId, Template}; +use forge_domain::{ModelId, ProviderId, Template, ToolName}; use gray_matter::Matter; use gray_matter::engine::YAML; -use serde::Serialize; use crate::agent_definition::AgentDefinition; @@ -105,7 +102,7 @@ impl + DirectoryReader let mut agents = Vec::new(); for (path, content) in files { - let mut agent = parse_agent_file(&content, config) + let mut agent = apply_subagent_tool_config(parse_agent_file(&content)?, config) .with_context(|| format!("Failed to parse agent: {}", path.display()))?; // Store the file path @@ -145,7 +142,7 @@ where let mut agents = vec![]; for (name, content) in contents { - let agent = parse_agent_file(content.as_ref(), config) + let agent = apply_subagent_tool_config(parse_agent_file(content.as_ref())?, config) .with_context(|| format!("Failed to parse agent: {}", name.as_ref()))?; agents.push(agent); @@ -154,75 +151,36 @@ where Ok(agents) } -#[derive(Serialize)] -struct AgentTemplateContext<'a> { - config: &'a ForgeConfig, -} +fn apply_subagent_tool_config(mut agent: AgentDefinition, config: &ForgeConfig) -> Result { + if agent.id.as_str() != "forge" { + return Ok(agent); + } -fn render_tools_frontmatter_block(content: &str, config: &ForgeConfig) -> Result { - let Some((newline, content)) = content - .strip_prefix("---\r\n") - .map(|content| ("\r\n", content)) - .or_else(|| content.strip_prefix("---\n").map(|content| ("\n", content))) - else { - return Ok(content.to_string()); + let Some(tools) = agent.tools.as_mut() else { + return Ok(agent); }; - let delimiter = format!("{newline}---{newline}"); - let (frontmatter, body) = content - .split_once(&delimiter) - .context("Failed to find end of agent frontmatter")?; - - let rendered_frontmatter = render_tools_block(frontmatter, config)?; - - Ok(format!( - "---{newline}{rendered_frontmatter}{delimiter}{body}" - )) -} - -fn render_tools_block(frontmatter: &str, config: &ForgeConfig) -> Result { - let lines = frontmatter - .split_inclusive('\n') - .map(ToString::to_string) - .collect::>(); + tools.retain(|tool| !matches!(tool.as_str(), "task" | "sage")); - let Some(start) = lines.iter().position(|line| line.trim_end() == "tools:") else { - return Ok(frontmatter.to_string()); + let delegated_tool = if config.enable_subagents { + ToolName::new("task") + } else { + ToolName::new("sage") }; - - let end = lines[start + 1..] + let insert_index = tools .iter() - .position(|line| { - let trimmed = line.trim_end(); - - !trimmed.is_empty() - && !line.starts_with([' ', '\t']) - && !trimmed.starts_with("#") - && !trimmed.starts_with("---") - }) - .map(|index| start + 1 + index) - .unwrap_or(lines.len()); - - let rendered_tools_block = TemplateEngine::default().render_template( - Template::new(lines[start..end].join("")), - &AgentTemplateContext { config }, - )?; - - Ok(format!( - "{}{}{}", - lines[..start].join(""), - rendered_tools_block, - lines[end..].join("") - )) + .position(|tool| tool.as_str() == "mcp_*") + .unwrap_or(tools.len()); + tools.insert(insert_index, delegated_tool); + + Ok(agent) } /// Parse raw content into an AgentDefinition with YAML frontmatter -fn parse_agent_file(content: &str, config: &ForgeConfig) -> Result { - let rendered_content = render_tools_frontmatter_block(content, config)?; - +fn parse_agent_file(content: &str) -> Result { // Parse the frontmatter using gray_matter with type-safe deserialization let gray_matter = Matter::::new(); - let result = gray_matter.parse::(&rendered_content)?; + let result = gray_matter.parse::(content)?; // Extract the frontmatter let agent = result @@ -282,7 +240,7 @@ mod tests { async fn test_parse_basic_agent() { let content = forge_test_kit::fixture!("/src/fixtures/agents/basic.md").await; - let actual = parse_agent_file(&content, &ForgeConfig::default()).unwrap(); + let actual = parse_agent_file(&content).unwrap(); assert_eq!(actual.id.as_str(), "test-basic"); assert_eq!(actual.title.as_ref().unwrap(), "Basic Test Agent"); @@ -300,7 +258,7 @@ mod tests { async fn test_parse_advanced_agent() { let content = forge_test_kit::fixture!("/src/fixtures/agents/advanced.md").await; - let actual = parse_agent_file(&content, &ForgeConfig::default()).unwrap(); + let actual = parse_agent_file(&content).unwrap(); assert_eq!(actual.id.as_str(), "test-advanced"); assert_eq!(actual.title.as_ref().unwrap(), "Advanced Test Agent"); @@ -313,22 +271,20 @@ mod tests { #[test] fn test_parse_agent_file_renders_conditional_frontmatter_when_subagents_enabled() { let fixture = r#"--- -id: "test" +id: "forge" tools: - read - {{#if config.enable_subagents}} - task - {{else}} - sage - {{/if}} + - mcp_* --- Body keeps {{tool_names.read}} untouched. "#; let config = ForgeConfig { enable_subagents: true, ..Default::default() }; - let actual = parse_agent_file(fixture, &config).unwrap(); + let actual = apply_subagent_tool_config(parse_agent_file(fixture).unwrap(), &config).unwrap(); - assert_eq!(actual.id, AgentId::new("test")); + assert_eq!(actual.id, AgentId::new("forge")); assert_eq!( actual.system_prompt.unwrap().template, "Body keeps {{tool_names.read}} untouched." @@ -339,22 +295,20 @@ Body keeps {{tool_names.read}} untouched. #[test] fn test_parse_agent_file_renders_conditional_frontmatter_when_subagents_disabled() { let fixture = r#"--- -id: "test" +id: "forge" tools: - read - {{#if config.enable_subagents}} - task - {{else}} - sage - {{/if}} + - mcp_* --- Body keeps {{tool_names.read}} untouched. "#; let config = ForgeConfig { enable_subagents: false, ..Default::default() }; - let actual = parse_agent_file(fixture, &config).unwrap(); + let actual = apply_subagent_tool_config(parse_agent_file(fixture).unwrap(), &config).unwrap(); - assert_eq!(actual.id, AgentId::new("test")); + assert_eq!(actual.id, AgentId::new("forge")); assert_snapshot!( "parse_agent_file_subagents_disabled_prompt", actual.system_prompt.unwrap().template @@ -365,32 +319,32 @@ Body keeps {{tool_names.read}} untouched. #[test] fn test_parse_agent_file_preserves_runtime_user_prompt_variables() { let fixture = r#"--- -id: "test" +id: "forge" tools: - read - {{#if config.enable_subagents}} - task - {{else}} - sage - {{/if}} + - mcp_* user_prompt: |- <{{event.name}}>{{event.value}} {{current_date}} --- Body keeps {{tool_names.read}} untouched. "#; - let config = ForgeConfig { enable_subagents: true, ..Default::default() }; - let actual = parse_agent_file(fixture, &config).unwrap(); + let actual = parse_agent_file(fixture).unwrap(); + let actual_user_prompt = actual.user_prompt.clone().unwrap().template; - assert_eq!(actual.id, AgentId::new("test")); + assert_eq!(actual.id, AgentId::new("forge")); assert_snapshot!( "parse_agent_file_preserves_runtime_user_prompt_variables", - actual.user_prompt.unwrap().template + actual_user_prompt ); assert_yaml_snapshot!( "parse_agent_file_preserves_runtime_user_prompt_variables_tools", - actual.tools + apply_subagent_tool_config(actual, &ForgeConfig { enable_subagents: true, ..Default::default() }) + .unwrap() + .tools ); } } diff --git a/crates/forge_repo/src/agents/forge.md b/crates/forge_repo/src/agents/forge.md index 27c7f63e33..7767177994 100644 --- a/crates/forge_repo/src/agents/forge.md +++ b/crates/forge_repo/src/agents/forge.md @@ -18,11 +18,8 @@ tools: - skill - todo_write - todo_read - {{#if config.enable_subagents}} - task - {{else}} - sage - {{/if}} - mcp_* user_prompt: |- <{{event.name}}>{{event.value}} diff --git a/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables_tools.snap b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables_tools.snap index 4a64bd2697..8e3ef417e1 100644 --- a/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables_tools.snap +++ b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_preserves_runtime_user_prompt_variables_tools.snap @@ -1,6 +1,7 @@ --- source: crates/forge_repo/src/agent.rs -expression: actual.tools +expression: "apply_subagent_tool_config(actual, &ForgeConfig\n{ enable_subagents: true, ..Default::default() }).unwrap().tools" --- - read - task +- mcp_* diff --git a/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_tools.snap b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_tools.snap index 8ba311fd0c..2127c05e5d 100644 --- a/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_tools.snap +++ b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_disabled_tools.snap @@ -4,3 +4,4 @@ expression: actual.tools --- - read - sage +- mcp_* diff --git a/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_enabled_tools.snap b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_enabled_tools.snap index 4a64bd2697..03d7176f33 100644 --- a/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_enabled_tools.snap +++ b/crates/forge_repo/src/snapshots/forge_repo__agent__tests__parse_agent_file_subagents_enabled_tools.snap @@ -4,3 +4,4 @@ expression: actual.tools --- - read - task +- mcp_* From 9ef4ca09be2b40c6cc8077d09f26786aa403cfd4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 04:52:35 +0000 Subject: [PATCH 6/6] [autofix.ci] apply automated fixes --- crates/forge_repo/src/agent.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/forge_repo/src/agent.rs b/crates/forge_repo/src/agent.rs index 9d77bf9883..2993a8e8c6 100644 --- a/crates/forge_repo/src/agent.rs +++ b/crates/forge_repo/src/agent.rs @@ -151,7 +151,10 @@ where Ok(agents) } -fn apply_subagent_tool_config(mut agent: AgentDefinition, config: &ForgeConfig) -> Result { +fn apply_subagent_tool_config( + mut agent: AgentDefinition, + config: &ForgeConfig, +) -> Result { if agent.id.as_str() != "forge" { return Ok(agent); } @@ -282,7 +285,8 @@ Body keeps {{tool_names.read}} untouched. "#; let config = ForgeConfig { enable_subagents: true, ..Default::default() }; - let actual = apply_subagent_tool_config(parse_agent_file(fixture).unwrap(), &config).unwrap(); + let actual = + apply_subagent_tool_config(parse_agent_file(fixture).unwrap(), &config).unwrap(); assert_eq!(actual.id, AgentId::new("forge")); assert_eq!( @@ -306,7 +310,8 @@ Body keeps {{tool_names.read}} untouched. "#; let config = ForgeConfig { enable_subagents: false, ..Default::default() }; - let actual = apply_subagent_tool_config(parse_agent_file(fixture).unwrap(), &config).unwrap(); + let actual = + apply_subagent_tool_config(parse_agent_file(fixture).unwrap(), &config).unwrap(); assert_eq!(actual.id, AgentId::new("forge")); assert_snapshot!( @@ -342,9 +347,12 @@ Body keeps {{tool_names.read}} untouched. ); assert_yaml_snapshot!( "parse_agent_file_preserves_runtime_user_prompt_variables_tools", - apply_subagent_tool_config(actual, &ForgeConfig { enable_subagents: true, ..Default::default() }) - .unwrap() - .tools + apply_subagent_tool_config( + actual, + &ForgeConfig { enable_subagents: true, ..Default::default() } + ) + .unwrap() + .tools ); } }