diff --git a/crates/forge_api/src/forge_api.rs b/crates/forge_api/src/forge_api.rs index c3f6557046..0e4d8f2cd9 100644 --- a/crates/forge_api/src/forge_api.rs +++ b/crates/forge_api/src/forge_api.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::Result; use forge_app::{ ConversationService, EnvironmentService, FileDiscoveryService, ForgeApp, McpConfigManager, - ProviderService, Services, WorkflowService, + ProviderService, Services, Walker, WorkflowService, }; use forge_domain::*; use forge_infra::ForgeInfra; @@ -35,7 +35,9 @@ impl ForgeAPI, ForgeInfra> { #[async_trait::async_trait] impl API for ForgeAPI { async fn discover(&self) -> Result> { - self.app.collect(None).await + let environment = self.app.get_environment(); + let config = Walker::unlimited().cwd(environment.cwd); + self.app.collect_files(config).await } async fn tools(&self) -> anyhow::Result> { diff --git a/crates/forge_app/src/app.rs b/crates/forge_app/src/app.rs index a9de1f7a9a..4deb49b941 100644 --- a/crates/forge_app/src/app.rs +++ b/crates/forge_app/src/app.rs @@ -11,7 +11,7 @@ use crate::services::TemplateService; use crate::tool_registry::ToolRegistry; use crate::{ AttachmentService, ConversationService, EnvironmentService, FileDiscoveryService, - ProviderService, Services, WorkflowService, + ProviderService, Services, Walker, WorkflowService, }; /// ForgeApp handles the core chat functionality by orchestrating various @@ -48,20 +48,23 @@ impl ForgeApp { let models = services.models().await?; // Discover files using the discovery service - let workflow = WorkflowService::read_workflow(services.as_ref(), None) - .await - .unwrap_or_default(); + let workflow = services.read_merged(None).await.unwrap_or_default(); let max_depth = workflow.max_walker_depth; + let environment = services.get_environment(); + + let mut walker = Walker::conservative().cwd(environment.cwd.clone()); + + if let Some(depth) = max_depth { + walker = walker.max_depth(depth); + }; + let files = services - .collect(max_depth) + .collect_files(walker) .await? .into_iter() .map(|f| f.path) .collect::>(); - // Get environment for orchestrator creation - let environment = services.get_environment(); - // Register templates using workflow path or environment fallback let template_path = workflow .templates diff --git a/crates/forge_app/src/lib.rs b/crates/forge_app/src/lib.rs index 86540206aa..b69d89d99b 100644 --- a/crates/forge_app/src/lib.rs +++ b/crates/forge_app/src/lib.rs @@ -14,7 +14,9 @@ mod tool_executor; mod tool_registry; mod truncation; mod utils; +mod walker; pub use app::*; pub use fmt_output::*; pub use services::*; +pub use walker::*; diff --git a/crates/forge_app/src/services.rs b/crates/forge_app/src/services.rs index 0a8010c43b..9093e6c3ab 100644 --- a/crates/forge_app/src/services.rs +++ b/crates/forge_app/src/services.rs @@ -7,6 +7,8 @@ use forge_domain::{ }; use merge::Merge; +use crate::Walker; + #[derive(Debug)] pub struct ShellOutput { pub output: CommandOutput, @@ -180,7 +182,7 @@ pub trait WorkflowService { #[async_trait::async_trait] pub trait FileDiscoveryService: Send + Sync { - async fn collect(&self, max_depth: Option) -> anyhow::Result>; + async fn collect_files(&self, config: Walker) -> anyhow::Result>; } #[async_trait::async_trait] @@ -425,8 +427,8 @@ impl WorkflowService for I { #[async_trait::async_trait] impl FileDiscoveryService for I { - async fn collect(&self, max_depth: Option) -> anyhow::Result> { - self.file_discovery_service().collect(max_depth).await + async fn collect_files(&self, config: Walker) -> anyhow::Result> { + self.file_discovery_service().collect_files(config).await } } diff --git a/crates/forge_app/src/walker.rs b/crates/forge_app/src/walker.rs new file mode 100644 index 0000000000..2d87367ce7 --- /dev/null +++ b/crates/forge_app/src/walker.rs @@ -0,0 +1,75 @@ +use std::path::PathBuf; + +use derive_setters::Setters; + +/// Configuration for filesystem walking operations +#[derive(Debug, Clone, Setters)] +#[setters(strip_option, into)] +pub struct Walker { + /// Base directory to start walking from + pub cwd: PathBuf, + /// Maximum depth of directory traversal (None for unlimited) + pub max_depth: Option, + /// Maximum number of entries per directory (None for unlimited) + pub max_breadth: Option, + /// Maximum size of individual files to process (None for unlimited) + pub max_file_size: Option, + /// Maximum number of files to process in total (None for unlimited) + pub max_files: Option, + /// Maximum total size of all files combined (None for unlimited) + pub max_total_size: Option, + /// Whether to skip binary files + pub skip_binary: bool, +} + +impl Walker { + /// Creates a new WalkerConfig with conservative default limits + pub fn conservative() -> Self { + Self { + cwd: PathBuf::new(), + max_depth: Some(5), + max_breadth: Some(10), + max_file_size: Some(1024 * 1024), // 1MB + max_files: Some(100), + max_total_size: Some(10 * 1024 * 1024), // 10MB + skip_binary: true, + } + } + + /// Creates a new WalkerConfig with no limits (use with caution) + pub fn unlimited() -> Self { + Self { + cwd: PathBuf::new(), + max_depth: None, + max_breadth: None, + max_file_size: None, + max_files: None, + max_total_size: None, + skip_binary: false, + } + } +} + +impl Default for Walker { + fn default() -> Self { + Self::conservative() + } +} + +/// Represents a file or directory found during filesystem traversal +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WalkedFile { + /// Relative path from the base directory + pub path: String, + /// File name (None for root directory) + pub file_name: Option, + /// Size in bytes + pub size: u64, +} + +impl WalkedFile { + /// Returns true if this represents a directory + pub fn is_dir(&self) -> bool { + self.path.ends_with('/') + } +} diff --git a/crates/forge_infra/src/forge_infra.rs b/crates/forge_infra/src/forge_infra.rs index 74224e90d6..fe68d74cf3 100644 --- a/crates/forge_infra/src/forge_infra.rs +++ b/crates/forge_infra/src/forge_infra.rs @@ -205,10 +205,7 @@ impl McpServerInfra for ForgeInfra { #[async_trait::async_trait] impl WalkerInfra for ForgeInfra { - async fn walk( - &self, - config: forge_services::WalkerConfig, - ) -> anyhow::Result> { + async fn walk(&self, config: forge_app::Walker) -> anyhow::Result> { self.walker_service.walk(config).await } } diff --git a/crates/forge_infra/src/walker.rs b/crates/forge_infra/src/walker.rs index 77eb2b7e9e..f88757b195 100644 --- a/crates/forge_infra/src/walker.rs +++ b/crates/forge_infra/src/walker.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use forge_services::{WalkedFile, WalkerConfig}; +use forge_app::{WalkedFile, Walker}; pub struct ForgeWalkerService; @@ -8,7 +8,7 @@ impl ForgeWalkerService { Self } - pub async fn walk(&self, config: WalkerConfig) -> Result> { + pub async fn walk(&self, config: Walker) -> Result> { // Convert domain config to forge_walker config let mut walker = if config.max_depth.is_none() && config.max_breadth.is_none() @@ -65,7 +65,7 @@ mod tests { std::fs::write(fixture.path().join("test.txt"), "test content").unwrap(); let service = ForgeWalkerService::new(); - let config = WalkerConfig::conservative().cwd(fixture.path().to_path_buf()); + let config = Walker::conservative().cwd(fixture.path().to_path_buf()); let actual = service.walk(config).await.unwrap(); @@ -80,7 +80,7 @@ mod tests { std::fs::write(fixture.path().join("test.txt"), "test content").unwrap(); let service = ForgeWalkerService::new(); - let config = WalkerConfig::unlimited().cwd(fixture.path().to_path_buf()); + let config = Walker::unlimited().cwd(fixture.path().to_path_buf()); let actual = service.walk(config).await.unwrap(); diff --git a/crates/forge_services/src/discovery.rs b/crates/forge_services/src/discovery.rs index 1e88101979..9d9bae82b9 100644 --- a/crates/forge_services/src/discovery.rs +++ b/crates/forge_services/src/discovery.rs @@ -1,10 +1,10 @@ use std::sync::Arc; use anyhow::Result; -use forge_app::FileDiscoveryService; +use forge_app::{FileDiscoveryService, Walker}; use forge_domain::File; -use crate::{EnvironmentInfra, WalkerConfig, WalkerInfra}; +use crate::{EnvironmentInfra, WalkerInfra}; pub struct ForgeDiscoveryService { service: Arc, @@ -17,15 +17,7 @@ impl ForgeDiscoveryService { } impl ForgeDiscoveryService { - async fn discover_with_depth(&self, max_depth: Option) -> Result> { - let cwd = self.service.get_environment().cwd.clone(); - - let config = if let Some(depth) = max_depth { - WalkerConfig::unlimited().cwd(cwd).max_depth(depth) - } else { - WalkerConfig::unlimited().cwd(cwd) - }; - + async fn discover_with_config(&self, config: Walker) -> Result> { let files = self.service.walk(config).await?; Ok(files .into_iter() @@ -38,7 +30,7 @@ impl ForgeDiscoveryService { impl FileDiscoveryService for ForgeDiscoveryService { - async fn collect(&self, max_depth: Option) -> Result> { - self.discover_with_depth(max_depth).await + async fn collect_files(&self, config: Walker) -> Result> { + self.discover_with_config(config).await } } diff --git a/crates/forge_services/src/infra.rs b/crates/forge_services/src/infra.rs index e6962edd3a..7009e604ac 100644 --- a/crates/forge_services/src/infra.rs +++ b/crates/forge_services/src/infra.rs @@ -2,83 +2,12 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use bytes::Bytes; -use derive_setters::Setters; +use forge_app::{WalkedFile, Walker}; use forge_domain::{ CommandOutput, Environment, McpServerConfig, ToolDefinition, ToolName, ToolOutput, }; use forge_snaps::Snapshot; -/// Configuration for filesystem walking operations -#[derive(Debug, Clone, Setters)] -#[setters(strip_option, into)] -pub struct WalkerConfig { - /// Base directory to start walking from - pub cwd: PathBuf, - /// Maximum depth of directory traversal (None for unlimited) - pub max_depth: Option, - /// Maximum number of entries per directory (None for unlimited) - pub max_breadth: Option, - /// Maximum size of individual files to process (None for unlimited) - pub max_file_size: Option, - /// Maximum number of files to process in total (None for unlimited) - pub max_files: Option, - /// Maximum total size of all files combined (None for unlimited) - pub max_total_size: Option, - /// Whether to skip binary files - pub skip_binary: bool, -} - -impl WalkerConfig { - /// Creates a new WalkerConfig with conservative default limits - pub fn conservative() -> Self { - Self { - cwd: PathBuf::new(), - max_depth: Some(5), - max_breadth: Some(10), - max_file_size: Some(1024 * 1024), // 1MB - max_files: Some(100), - max_total_size: Some(10 * 1024 * 1024), // 10MB - skip_binary: true, - } - } - - /// Creates a new WalkerConfig with no limits (use with caution) - pub fn unlimited() -> Self { - Self { - cwd: PathBuf::new(), - max_depth: None, - max_breadth: None, - max_file_size: None, - max_files: None, - max_total_size: None, - skip_binary: false, - } - } -} - -impl Default for WalkerConfig { - fn default() -> Self { - Self::conservative() - } -} -/// Represents a file or directory found during filesystem traversal -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct WalkedFile { - /// Relative path from the base directory - pub path: String, - /// File name (None for root directory) - pub file_name: Option, - /// Size in bytes - pub size: u64, -} - -impl WalkedFile { - /// Returns true if this represents a directory - pub fn is_dir(&self) -> bool { - self.path.ends_with('/') - } -} - pub trait EnvironmentInfra: Send + Sync { fn get_environment(&self) -> Environment; } @@ -227,5 +156,5 @@ pub trait McpServerInfra: Send + Sync + 'static { pub trait WalkerInfra: Send + Sync { /// Walks the filesystem starting from the given directory with the /// specified configuration - async fn walk(&self, config: WalkerConfig) -> anyhow::Result>; + async fn walk(&self, config: Walker) -> anyhow::Result>; } diff --git a/crates/forge_services/src/tool_services/fs_search.rs b/crates/forge_services/src/tool_services/fs_search.rs index 83408fac59..5fec511105 100644 --- a/crates/forge_services/src/tool_services/fs_search.rs +++ b/crates/forge_services/src/tool_services/fs_search.rs @@ -3,12 +3,11 @@ use std::path::Path; use std::sync::Arc; use anyhow::Context; -use forge_app::{FsSearchService, Match, MatchResult, SearchResult}; +use forge_app::{FsSearchService, Match, MatchResult, SearchResult, Walker}; use grep_searcher::sinks::UTF8; use crate::infra::WalkerInfra; use crate::utils::assert_absolute_path; -use crate::WalkerConfig; // Using FSSearchInput from forge_domain @@ -168,7 +167,7 @@ impl ForgeFsSearch { #[allow(unused_mut)] let mut paths = self .walker - .walk(WalkerConfig::unlimited().cwd(dir.to_path_buf())) + .walk(Walker::unlimited().cwd(dir.to_path_buf())) .await .with_context(|| format!("Failed to walk directory '{}'", dir.display()))? .into_iter() @@ -191,10 +190,10 @@ impl ForgeFsSearch { mod test { use std::sync::Arc; + use forge_app::{WalkedFile, Walker}; use tokio::fs; use super::*; - use crate::infra::WalkedFile; use crate::utils::TempDir; // Mock WalkerInfra for testing @@ -202,7 +201,7 @@ mod test { #[async_trait::async_trait] impl WalkerInfra for MockInfra { - async fn walk(&self, config: crate::WalkerConfig) -> anyhow::Result> { + async fn walk(&self, config: Walker) -> anyhow::Result> { // Simple mock that just returns files in the directory let mut files = Vec::new(); let metadata = tokio::fs::metadata(&config.cwd).await?; diff --git a/forge.default.yaml b/forge.default.yaml index 4fbdeb1aed..88fe0d3211 100644 --- a/forge.default.yaml +++ b/forge.default.yaml @@ -5,6 +5,7 @@ variables: top_p: 0.8 top_k: 30 max_tokens: 20480 +max_walker_depth: 1 updates: frequency: "daily" @@ -46,7 +47,6 @@ agents: - forge_tool_net_fetch - forge_tool_fs_search - forge_tool_fs_undo - max_walker_depth: 1 - id: muse title: "Analysis and planning focussed" @@ -77,4 +77,3 @@ agents: - forge_tool_fs_search - forge_tool_fs_create - forge_tool_fs_patch - max_walker_depth: 1