Skip to content

Commit 5bd0b94

Browse files
refactor(config): read ForgeConfig once at startup and thread it through the stack (#2850)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent bfd9f7f commit 5bd0b94

34 files changed

+630
-369
lines changed

crates/forge_api/src/api.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use std::path::PathBuf;
33
use anyhow::Result;
44
use forge_app::dto::ToolsOverview;
55
use forge_app::{User, UserUsage};
6-
use forge_config::ForgeConfig;
76
use forge_domain::{AgentId, Effort, ModelId, ProviderModels};
87
use forge_stream::MpscStream;
98
use futures::stream::BoxStream;
@@ -51,9 +50,6 @@ pub trait API: Sync + Send {
5150
/// Returns the current environment
5251
fn environment(&self) -> Environment;
5352

54-
/// Returns the full application configuration.
55-
fn get_config(&self) -> ForgeConfig;
56-
5753
/// Adds a new conversation to the conversation store
5854
async fn upsert_conversation(&self, conversation: Conversation) -> Result<()>;
5955

crates/forge_api/src/forge_api.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,35 @@ use crate::API;
2424
pub struct ForgeAPI<S, F> {
2525
services: Arc<S>,
2626
infra: Arc<F>,
27+
config: forge_config::ForgeConfig,
2728
}
2829

2930
impl<A, F> ForgeAPI<A, F> {
30-
pub fn new(services: Arc<A>, infra: Arc<F>) -> Self {
31-
Self { services, infra }
31+
pub fn new(services: Arc<A>, infra: Arc<F>, config: forge_config::ForgeConfig) -> Self {
32+
Self { services, infra, config }
3233
}
3334

3435
/// Creates a ForgeApp instance with the current services
3536
fn app(&self) -> ForgeApp<A>
3637
where
3738
A: Services,
3839
{
39-
ForgeApp::new(self.services.clone())
40+
ForgeApp::new(self.services.clone(), self.config.clone())
4041
}
4142
}
4243

4344
impl ForgeAPI<ForgeServices<ForgeRepo<ForgeInfra>>, ForgeRepo<ForgeInfra>> {
44-
pub fn init(cwd: PathBuf) -> Self {
45-
let infra = Arc::new(ForgeInfra::new(cwd));
46-
let repo = Arc::new(ForgeRepo::new(infra.clone()));
47-
let app = Arc::new(ForgeServices::new(repo.clone()));
48-
ForgeAPI::new(app, repo)
45+
/// Creates a fully-initialized [`ForgeAPI`] from a pre-read configuration.
46+
///
47+
/// # Arguments
48+
/// * `cwd` - The working directory path for environment and file resolution
49+
/// * `config` - Pre-read application configuration (from startup)
50+
/// * `services_url` - Pre-validated URL for the gRPC workspace server
51+
pub fn init(cwd: PathBuf, config: ForgeConfig, services_url: Url) -> Self {
52+
let infra = Arc::new(ForgeInfra::new(cwd, config.clone(), services_url));
53+
let repo = Arc::new(ForgeRepo::new(infra.clone(), config.clone()));
54+
let app = Arc::new(ForgeServices::new(repo.clone(), config.clone()));
55+
ForgeAPI::new(app, repo, config)
4956
}
5057

5158
pub async fn get_skills_internal(&self) -> Result<Vec<Skill>> {
@@ -91,7 +98,7 @@ impl<A: Services, F: CommandInfra + EnvironmentInfra + SkillRepository + GrpcInf
9198
diff: Option<String>,
9299
additional_context: Option<String>,
93100
) -> Result<forge_app::CommitResult> {
94-
let git_app = GitApp::new(self.services.clone());
101+
let git_app = GitApp::new(self.services.clone(), self.config.clone());
95102
let result = git_app
96103
.commit_message(max_diff_size, diff, additional_context)
97104
.await?;
@@ -147,10 +154,6 @@ impl<A: Services, F: CommandInfra + EnvironmentInfra + SkillRepository + GrpcInf
147154
self.services.get_environment().clone()
148155
}
149156

150-
fn get_config(&self) -> ForgeConfig {
151-
self.infra.get_config()
152-
}
153-
154157
async fn conversation(
155158
&self,
156159
conversation_id: &ConversationId,

crates/forge_app/src/agent.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub trait AgentService: Send + Sync + 'static {
3030
agent: &Agent,
3131
context: &ToolCallContext,
3232
call: ToolCallFull,
33+
config: &ForgeConfig,
3334
) -> ToolResult;
3435

3536
/// Synchronize the on-going conversation
@@ -60,8 +61,9 @@ impl<T: Services> AgentService for T {
6061
agent: &Agent,
6162
context: &ToolCallContext,
6263
call: ToolCallFull,
64+
config: &ForgeConfig,
6365
) -> ToolResult {
64-
let registry = ToolRegistry::new(Arc::new(self.clone()));
66+
let registry = ToolRegistry::new(Arc::new(self.clone()), config.clone());
6567
registry.call(agent, context, call).await
6668
}
6769

crates/forge_app/src/agent_executor.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::sync::Arc;
22

33
use anyhow::Context;
44
use convert_case::{Case, Casing};
5+
use forge_config::ForgeConfig;
56
use forge_domain::{
67
AgentId, ChatRequest, ChatResponse, ChatResponseContent, Conversation, Event, TitleFormat,
78
ToolCallContext, ToolDefinition, ToolName, ToolOutput,
@@ -16,12 +17,13 @@ use crate::{AgentRegistry, ConversationService, Services};
1617
#[derive(Clone)]
1718
pub struct AgentExecutor<S> {
1819
services: Arc<S>,
20+
config: ForgeConfig,
1921
pub tool_agents: Arc<RwLock<Option<Vec<ToolDefinition>>>>,
2022
}
2123

2224
impl<S: Services> AgentExecutor<S> {
23-
pub fn new(services: Arc<S>) -> Self {
24-
Self { services, tool_agents: Arc::new(RwLock::new(None)) }
25+
pub fn new(services: Arc<S>, config: ForgeConfig) -> Self {
26+
Self { services, config, tool_agents: Arc::new(RwLock::new(None)) }
2527
}
2628

2729
/// Returns a list of tool definitions for all available agents.
@@ -63,7 +65,7 @@ impl<S: Services> AgentExecutor<S> {
6365
.upsert_conversation(conversation.clone())
6466
.await?;
6567
// Execute the request through the ForgeApp
66-
let app = crate::ForgeApp::new(self.services.clone());
68+
let app = crate::ForgeApp::new(self.services.clone(), self.config.clone());
6769
let mut response_stream = app
6870
.chat(
6971
agent_id.clone(),

crates/forge_app/src/app.rs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,17 @@ pub(crate) fn build_template_config(config: &ForgeConfig) -> forge_domain::Templ
4444
pub struct ForgeApp<S> {
4545
services: Arc<S>,
4646
tool_registry: ToolRegistry<S>,
47+
config: ForgeConfig,
4748
}
4849

4950
impl<S: Services> ForgeApp<S> {
50-
/// Creates a new ForgeApp instance with the provided services.
51-
pub fn new(services: Arc<S>) -> Self {
52-
Self { tool_registry: ToolRegistry::new(services.clone()), services }
51+
/// Creates a new ForgeApp instance with the provided services and config.
52+
pub fn new(services: Arc<S>, config: ForgeConfig) -> Self {
53+
Self {
54+
tool_registry: ToolRegistry::new(services.clone(), config.clone()),
55+
services,
56+
config,
57+
}
5358
}
5459

5560
/// Executes a chat request and returns a stream of responses.
@@ -69,7 +74,7 @@ impl<S: Services> ForgeApp<S> {
6974
.expect("conversation for the request should've been created at this point.");
7075

7176
// Discover files using the discovery service
72-
let forge_config = services.get_config();
77+
let forge_config = self.config.clone();
7378
let environment = services.get_environment();
7479

7580
let files = services.list_current_directory().await?;
@@ -132,7 +137,7 @@ impl<S: Services> ForgeApp<S> {
132137

133138
// Detect and render externally changed files notification
134139
let conversation = ChangedFiles::new(services.clone(), agent.clone())
135-
.update_file_stats(conversation)
140+
.update_file_stats(conversation, forge_config.max_parallel_file_reads)
136141
.await;
137142

138143
let conversation = InitConversationMetrics::new(current_time).apply(conversation);
@@ -157,11 +162,17 @@ impl<S: Services> ForgeApp<S> {
157162

158163
let retry_config = forge_config.retry.clone().unwrap_or_default();
159164

160-
let orch = Orchestrator::new(services.clone(), retry_config, conversation, agent)
161-
.error_tracker(ToolErrorTracker::new(max_tool_failure_per_turn))
162-
.tool_definitions(tool_definitions)
163-
.models(models)
164-
.hook(Arc::new(hook));
165+
let orch = Orchestrator::new(
166+
services.clone(),
167+
retry_config,
168+
conversation,
169+
agent,
170+
forge_config,
171+
)
172+
.error_tracker(ToolErrorTracker::new(max_tool_failure_per_turn))
173+
.tool_definitions(tool_definitions)
174+
.models(models)
175+
.hook(Arc::new(hook));
165176

166177
// Create and return the stream
167178
let stream = MpscStream::spawn(
@@ -219,7 +230,7 @@ impl<S: Services> ForgeApp<S> {
219230
let original_messages = context.messages.len();
220231
let original_token_count = *context.token_count();
221232

222-
let forge_config = self.services.get_config();
233+
let forge_config = self.config.clone();
223234

224235
// Get agent and apply workflow config
225236
let agent = self.services.get_agent(&active_agent_id).await?;

crates/forge_app/src/changed_files.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ impl<S: FsReadService + EnvironmentInfra> ChangedFiles<S> {
2424
/// Detects externally changed files and renders a notification if changes
2525
/// are found. Updates file hashes in conversation metrics to prevent
2626
/// duplicate notifications.
27-
pub async fn update_file_stats(&self, mut conversation: Conversation) -> Conversation {
27+
pub async fn update_file_stats(
28+
&self,
29+
mut conversation: Conversation,
30+
parallel_file_reads: usize,
31+
) -> Conversation {
2832
use crate::file_tracking::FileChangeDetector;
29-
let parallel_file_reads = self.services.get_config().max_parallel_file_reads;
3033
let changes = FileChangeDetector::new(self.services.clone(), parallel_file_reads)
3134
.detect(&conversation.metrics)
3235
.await;
@@ -133,13 +136,6 @@ mod tests {
133136
env
134137
}
135138

136-
fn get_config(&self) -> forge_config::ForgeConfig {
137-
forge_config::ConfigReader::default()
138-
.read_defaults()
139-
.build()
140-
.unwrap()
141-
}
142-
143139
async fn update_environment(
144140
&self,
145141
_ops: Vec<forge_domain::ConfigOperation>,
@@ -203,7 +199,7 @@ mod tests {
203199
Some(ModelId::new("test")),
204200
)));
205201

206-
let actual = service.update_file_stats(conversation.clone()).await;
202+
let actual = service.update_file_stats(conversation.clone(), 4).await;
207203

208204
assert_eq!(actual.context.clone().unwrap_or_default().messages.len(), 1);
209205
assert_eq!(actual.context, conversation.context);
@@ -219,7 +215,7 @@ mod tests {
219215
[("/test/file.txt".into(), Some(old_hash))].into(),
220216
);
221217

222-
let actual = service.update_file_stats(conversation).await;
218+
let actual = service.update_file_stats(conversation, 4).await;
223219

224220
let messages = &actual.context.unwrap().messages;
225221
assert_eq!(messages.len(), 1);
@@ -239,7 +235,7 @@ mod tests {
239235
[("/test/file.txt".into(), Some(old_hash))].into(),
240236
);
241237

242-
let actual = service.update_file_stats(conversation).await;
238+
let actual = service.update_file_stats(conversation, 4).await;
243239

244240
let updated_hash = actual
245241
.metrics
@@ -265,7 +261,7 @@ mod tests {
265261
.into(),
266262
);
267263

268-
let actual = service.update_file_stats(conversation).await;
264+
let actual = service.update_file_stats(conversation, 4).await;
269265

270266
let message = actual.context.unwrap().messages[0]
271267
.content()
@@ -288,7 +284,7 @@ mod tests {
288284
Some(cwd),
289285
);
290286

291-
let actual = service.update_file_stats(conversation).await;
287+
let actual = service.update_file_stats(conversation, 4).await;
292288

293289
let message = actual.context.unwrap().messages[0]
294290
.content()

crates/forge_app/src/command_generator.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,6 @@ mod tests {
144144
self.environment.clone()
145145
}
146146

147-
fn get_config(&self) -> forge_config::ForgeConfig {
148-
forge_config::ConfigReader::default()
149-
.read_defaults()
150-
.build()
151-
.unwrap()
152-
}
153-
154147
async fn update_environment(
155148
&self,
156149
_ops: Vec<forge_domain::ConfigOperation>,

crates/forge_app/src/git_app.rs

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ use forge_domain::*;
66
use schemars::JsonSchema;
77
use serde::Deserialize;
88

9-
use crate::{
10-
AgentProviderResolver, AgentRegistry, AppConfigService, EnvironmentInfra, ProviderAuthService,
11-
ProviderService, ShellService, TemplateService,
9+
use crate::services::{
10+
AgentRegistry, AppConfigService, ProviderAuthService, ProviderService, ShellService,
11+
TemplateService,
1212
};
13+
use crate::{AgentProviderResolver, Services};
1314

1415
/// Errors specific to GitApp operations
1516
#[derive(thiserror::Error, Debug)]
@@ -21,6 +22,7 @@ pub enum GitAppError {
2122
/// GitApp handles git-related operations like commit message generation.
2223
pub struct GitApp<S> {
2324
services: Arc<S>,
25+
config: forge_config::ForgeConfig,
2426
}
2527

2628
/// Result of a commit operation
@@ -65,9 +67,9 @@ struct DiffContext {
6567
}
6668

6769
impl<S> GitApp<S> {
68-
/// Creates a new GitApp instance with the provided services.
69-
pub fn new(services: Arc<S>) -> Self {
70-
Self { services }
70+
/// Creates a new GitApp instance with the provided services and config.
71+
pub fn new(services: Arc<S>, config: forge_config::ForgeConfig) -> Self {
72+
Self { services, config }
7173
}
7274

7375
/// Truncates diff content if it exceeds the maximum size
@@ -92,16 +94,7 @@ impl<S> GitApp<S> {
9294
}
9395
}
9496

95-
impl<S> GitApp<S>
96-
where
97-
S: EnvironmentInfra
98-
+ ShellService
99-
+ AgentRegistry
100-
+ TemplateService
101-
+ ProviderService
102-
+ AppConfigService
103-
+ ProviderAuthService,
104-
{
97+
impl<S: Services> GitApp<S> {
10598
/// Generates a commit message without committing
10699
///
107100
/// # Arguments
@@ -220,7 +213,7 @@ where
220213
additional_context,
221214
};
222215

223-
let retry_config = self.services.get_config().retry.unwrap_or_default();
216+
let retry_config = self.config.retry.clone().unwrap_or_default();
224217
crate::retry::retry_with_config(
225218
&retry_config,
226219
|| self.generate_message_from_diff(ctx.clone()),
@@ -231,7 +224,7 @@ where
231224

232225
/// Fetches git context (branch name and recent commits)
233226
async fn fetch_git_context(&self, cwd: &Path) -> Result<(String, String)> {
234-
let max_commit_count = self.services.get_config().max_commit_count;
227+
let max_commit_count = self.config.max_commit_count;
235228
let git_log_cmd =
236229
format!("git log --pretty=format:%s --abbrev-commit --max-count={max_commit_count}");
237230
let (recent_commits, branch_name) = tokio::join!(

crates/forge_app/src/hooks/title_generation.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ mod tests {
189189
_agent: &Agent,
190190
_context: &ToolCallContext,
191191
_call: ToolCallFull,
192+
_config: &forge_config::ForgeConfig,
192193
) -> ToolResult {
193194
unreachable!("Not used in tests")
194195
}

0 commit comments

Comments
 (0)