Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 9 additions & 27 deletions crates/forge_api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,17 +122,13 @@ pub trait API: Sync + Send {
/// Retrieves the provider configuration for the default agent
async fn get_default_provider(&self) -> anyhow::Result<Provider<Url>>;

/// Sets the default provider for all the agents
async fn set_default_provider(&self, provider_id: ProviderId) -> anyhow::Result<()>;

/// Updates the caller's default provider and model together, ensuring all
/// commands resolve a consistent pair without requiring a follow-up model
/// selection call.
async fn set_default_provider_and_model(
&self,
provider_id: ProviderId,
model: ModelId,
) -> anyhow::Result<()>;
/// Applies one or more configuration mutations atomically.
///
/// Each operation in `ops` is applied in order and persisted as a single
/// atomic write. Use [`forge_domain::ConfigOperation`] variants to describe
/// each mutation. Provider and model changes also invalidate the agent
/// cache so the next request picks up the updated configuration.
async fn update_config(&self, ops: Vec<forge_domain::ConfigOperation>) -> anyhow::Result<()>;

/// Retrieves information about the currently authenticated user
async fn user_info(&self) -> anyhow::Result<Option<User>>;
Expand All @@ -152,31 +148,17 @@ pub trait API: Sync + Send {
/// Gets the default model
async fn get_default_model(&self) -> Option<ModelId>;

/// Sets the operating model
async fn set_default_model(&self, model_id: ModelId) -> anyhow::Result<()>;

/// Gets the commit configuration (provider and model for commit message
/// generation).
async fn get_commit_config(&self) -> anyhow::Result<Option<forge_domain::CommitConfig>>;

/// Sets the commit configuration (provider and model for commit message
/// generation).
async fn set_commit_config(&self, config: forge_domain::CommitConfig) -> anyhow::Result<()>;
async fn get_commit_config(&self) -> anyhow::Result<Option<forge_domain::ModelConfig>>;

/// Gets the suggest configuration (provider and model for command
/// suggestion generation).
async fn get_suggest_config(&self) -> anyhow::Result<Option<forge_domain::SuggestConfig>>;

/// Sets the suggest configuration (provider and model for command
/// suggestion generation).
async fn set_suggest_config(&self, config: forge_domain::SuggestConfig) -> anyhow::Result<()>;
async fn get_suggest_config(&self) -> anyhow::Result<Option<forge_domain::ModelConfig>>;

/// Gets the current reasoning effort setting.
async fn get_reasoning_effort(&self) -> anyhow::Result<Option<Effort>>;

/// Sets the reasoning effort level applied to all agents.
async fn set_reasoning_effort(&self, effort: Effort) -> anyhow::Result<()>;

/// Refresh MCP caches by fetching fresh data
async fn reload_mcp(&self) -> Result<()>;

Expand Down
101 changes: 40 additions & 61 deletions crates/forge_api/src/forge_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@ use crate::API;
pub struct ForgeAPI<S, F> {
services: Arc<S>,
infra: Arc<F>,
config: forge_config::ForgeConfig,
}

impl<A, F> ForgeAPI<A, F> {
pub fn new(services: Arc<A>, infra: Arc<F>, config: forge_config::ForgeConfig) -> Self {
Self { services, infra, config }
pub fn new(services: Arc<A>, infra: Arc<F>) -> Self {
Self { services, infra }
}

/// Creates a ForgeApp instance with the current services
/// Creates a ForgeApp instance with the current services and latest config.
fn app(&self) -> ForgeApp<A>
where
A: Services,
A: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>,
F: EnvironmentInfra<Config = forge_config::ForgeConfig>,
{
ForgeApp::new(self.services.clone(), self.config.clone())
ForgeApp::new(self.services.clone())
}
}

Expand All @@ -49,10 +49,10 @@ impl ForgeAPI<ForgeServices<ForgeRepo<ForgeInfra>>, ForgeRepo<ForgeInfra>> {
/// * `config` - Pre-read application configuration (from startup)
/// * `services_url` - Pre-validated URL for the gRPC workspace server
pub fn init(cwd: PathBuf, config: ForgeConfig, services_url: Url) -> Self {
let infra = Arc::new(ForgeInfra::new(cwd, config.clone(), services_url));
let repo = Arc::new(ForgeRepo::new(infra.clone(), config.clone()));
let app = Arc::new(ForgeServices::new(repo.clone(), config.clone()));
ForgeAPI::new(app, repo, config)
let infra = Arc::new(ForgeInfra::new(cwd, config, services_url));
let repo = Arc::new(ForgeRepo::new(infra.clone()));
let app = Arc::new(ForgeServices::new(repo.clone()));
ForgeAPI::new(app, repo)
}

pub async fn get_skills_internal(&self) -> Result<Vec<Skill>> {
Expand All @@ -62,8 +62,13 @@ impl ForgeAPI<ForgeServices<ForgeRepo<ForgeInfra>>, ForgeRepo<ForgeInfra>> {
}

#[async_trait::async_trait]
impl<A: Services, F: CommandInfra + EnvironmentInfra + SkillRepository + GrpcInfra> API
for ForgeAPI<A, F>
impl<
A: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>,
F: CommandInfra
+ EnvironmentInfra<Config = forge_config::ForgeConfig>
+ SkillRepository
+ GrpcInfra,
> API for ForgeAPI<A, F>
{
async fn discover(&self) -> Result<Vec<File>> {
let environment = self.services.get_environment();
Expand Down Expand Up @@ -98,7 +103,7 @@ impl<A: Services, F: CommandInfra + EnvironmentInfra + SkillRepository + GrpcInf
diff: Option<String>,
additional_context: Option<String>,
) -> Result<forge_app::CommitResult> {
let git_app = GitApp::new(self.services.clone(), self.config.clone());
let git_app = GitApp::new(self.services.clone());
let result = git_app
.commit_message(max_diff_size, diff, additional_context)
.await?;
Expand Down Expand Up @@ -225,13 +230,31 @@ impl<A: Services, F: CommandInfra + EnvironmentInfra + SkillRepository + GrpcInf
agent_provider_resolver.get_provider(Some(agent_id)).await
}

async fn set_default_provider(&self, provider_id: ProviderId) -> anyhow::Result<()> {
let result = self.services.set_default_provider(provider_id).await;
// Invalidate cache for agents
let _ = self.services.reload_agents().await;
async fn update_config(&self, ops: Vec<forge_domain::ConfigOperation>) -> anyhow::Result<()> {
// Determine whether any op affects provider/model resolution before writing,
// so we can invalidate the agent cache afterwards.
let needs_agent_reload = ops
.iter()
.any(|op| matches!(op, forge_domain::ConfigOperation::SetSessionConfig(_)));
let result = self.services.update_config(ops).await;
if needs_agent_reload {
let _ = self.services.reload_agents().await;
}
result
}

async fn get_commit_config(&self) -> anyhow::Result<Option<ModelConfig>> {
self.services.get_commit_config().await
}

async fn get_suggest_config(&self) -> anyhow::Result<Option<ModelConfig>> {
self.services.get_suggest_config().await
}

async fn get_reasoning_effort(&self) -> anyhow::Result<Option<Effort>> {
self.services.get_reasoning_effort().await
}

async fn user_info(&self) -> Result<Option<User>> {
let provider = self.get_default_provider().await?;
if let Some(api_key) = provider.api_key() {
Expand Down Expand Up @@ -273,50 +296,6 @@ impl<A: Services, F: CommandInfra + EnvironmentInfra + SkillRepository + GrpcInf
async fn get_default_model(&self) -> Option<ModelId> {
self.services.get_provider_model(None).await.ok()
}
async fn set_default_model(&self, model_id: ModelId) -> anyhow::Result<()> {
let result = self.services.set_default_model(model_id).await;
// Invalidate cache for agents
let _ = self.services.reload_agents().await;

result
}

async fn set_default_provider_and_model(
&self,
provider_id: ProviderId,
model: ModelId,
) -> anyhow::Result<()> {
let result = self
.services
.set_default_provider_and_model(provider_id, model)
.await;
let _ = self.services.reload_agents().await;
result
}

async fn get_commit_config(&self) -> anyhow::Result<Option<CommitConfig>> {
self.services.get_commit_config().await
}

async fn set_commit_config(&self, config: CommitConfig) -> anyhow::Result<()> {
self.services.set_commit_config(config).await
}

async fn get_suggest_config(&self) -> anyhow::Result<Option<SuggestConfig>> {
self.services.get_suggest_config().await
}

async fn set_suggest_config(&self, config: SuggestConfig) -> anyhow::Result<()> {
self.services.set_suggest_config(config).await
}

async fn get_reasoning_effort(&self) -> anyhow::Result<Option<Effort>> {
self.services.get_reasoning_effort().await
}

async fn set_reasoning_effort(&self, effort: Effort) -> anyhow::Result<()> {
self.services.set_reasoning_effort(effort).await
}

async fn reload_mcp(&self) -> Result<()> {
self.services.mcp_service().reload_mcp().await
Expand Down
8 changes: 3 additions & 5 deletions crates/forge_app/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use merge::Merge;

use crate::services::AppConfigService;
use crate::tool_registry::ToolRegistry;
use crate::{ConversationService, ProviderService, Services};
use crate::{ConversationService, EnvironmentInfra, ProviderService, Services};

/// Agent service trait that provides core chat and tool call functionality.
/// This trait abstracts the essential operations needed by the Orchestrator.
Expand All @@ -30,7 +30,6 @@ pub trait AgentService: Send + Sync + 'static {
agent: &Agent,
context: &ToolCallContext,
call: ToolCallFull,
config: &ForgeConfig,
) -> ToolResult;

/// Synchronize the on-going conversation
Expand All @@ -39,7 +38,7 @@ pub trait AgentService: Send + Sync + 'static {

/// Blanket implementation of AgentService for any type that implements Services
#[async_trait::async_trait]
impl<T: Services> AgentService for T {
impl<T: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>> AgentService for T {
async fn chat_agent(
&self,
id: &ModelId,
Expand All @@ -61,9 +60,8 @@ impl<T: Services> AgentService for T {
agent: &Agent,
context: &ToolCallContext,
call: ToolCallFull,
config: &ForgeConfig,
) -> ToolResult {
let registry = ToolRegistry::new(Arc::new(self.clone()), config.clone());
let registry = ToolRegistry::new(Arc::new(self.clone()));
registry.call(agent, context, call).await
}

Expand Down
13 changes: 5 additions & 8 deletions crates/forge_app/src/agent_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use std::sync::Arc;

use anyhow::Context;
use convert_case::{Case, Casing};
use forge_config::ForgeConfig;
use forge_domain::{
AgentId, ChatRequest, ChatResponse, ChatResponseContent, Conversation, ConversationId, Event,
TitleFormat, ToolCallContext, ToolDefinition, ToolName, ToolOutput,
Expand All @@ -12,18 +11,16 @@ use futures::StreamExt;
use tokio::sync::RwLock;

use crate::error::Error;
use crate::{AgentRegistry, ConversationService, Services};

use crate::{AgentRegistry, ConversationService, EnvironmentInfra, Services};
#[derive(Clone)]
pub struct AgentExecutor<S> {
services: Arc<S>,
config: ForgeConfig,
pub tool_agents: Arc<RwLock<Option<Vec<ToolDefinition>>>>,
}

impl<S: Services> AgentExecutor<S> {
pub fn new(services: Arc<S>, config: ForgeConfig) -> Self {
Self { services, config, tool_agents: Arc::new(RwLock::new(None)) }
impl<S: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>> AgentExecutor<S> {
pub fn new(services: Arc<S>) -> Self {
Self { services, tool_agents: Arc::new(RwLock::new(None)) }
}

/// Returns a list of tool definitions for all available agents.
Expand Down Expand Up @@ -79,7 +76,7 @@ impl<S: Services> AgentExecutor<S> {
conversation
};
// Execute the request through the ForgeApp
let app = crate::ForgeApp::new(self.services.clone(), self.config.clone());
let app = crate::ForgeApp::new(self.services.clone());
let mut response_stream = app
.chat(
agent_id.clone(),
Expand Down
28 changes: 10 additions & 18 deletions crates/forge_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ use crate::tool_registry::ToolRegistry;
use crate::tool_resolver::ToolResolver;
use crate::user_prompt::UserPromptGenerator;
use crate::{
AgentExt, AgentProviderResolver, ConversationService, FileDiscoveryService, ProviderService,
Services,
AgentExt, AgentProviderResolver, ConversationService, EnvironmentInfra, FileDiscoveryService,
ProviderService, Services,
};

/// Builds a [`TemplateConfig`] from a [`ForgeConfig`].
Expand All @@ -44,17 +44,12 @@ pub(crate) fn build_template_config(config: &ForgeConfig) -> forge_domain::Templ
pub struct ForgeApp<S> {
services: Arc<S>,
tool_registry: ToolRegistry<S>,
config: ForgeConfig,
}

impl<S: Services> ForgeApp<S> {
/// Creates a new ForgeApp instance with the provided services and config.
pub fn new(services: Arc<S>, config: ForgeConfig) -> Self {
Self {
tool_registry: ToolRegistry::new(services.clone(), config.clone()),
services,
config,
}
impl<S: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>> ForgeApp<S> {
/// Creates a new ForgeApp instance with the provided services.
pub fn new(services: Arc<S>) -> Self {
Self { tool_registry: ToolRegistry::new(services.clone()), services }
}

/// Executes a chat request and returns a stream of responses.
Expand All @@ -73,7 +68,7 @@ impl<S: Services> ForgeApp<S> {
.ok_or_else(|| forge_domain::Error::ConversationNotFound(chat.conversation_id))?;

// Discover files using the discovery service
let forge_config = self.config.clone();
let forge_config = self.services.get_config()?;
let environment = services.get_environment();

let files = services.list_current_directory().await?;
Expand Down Expand Up @@ -136,7 +131,7 @@ impl<S: Services> ForgeApp<S> {

// Detect and render externally changed files notification
let conversation = ChangedFiles::new(services.clone(), agent.clone())
.update_file_stats(conversation, forge_config.max_parallel_file_reads)
.update_file_stats(conversation)
.await;

let conversation = InitConversationMetrics::new(current_time).apply(conversation);
Expand All @@ -159,14 +154,11 @@ impl<S: Services> ForgeApp<S> {
.on_toolcall_end(tracing_handler.clone())
.on_end(tracing_handler.and(title_handler));

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

let orch = Orchestrator::new(
services.clone(),
retry_config,
conversation,
agent,
forge_config,
self.services.get_config()?,
)
.error_tracker(ToolErrorTracker::new(max_tool_failure_per_turn))
.tool_definitions(tool_definitions)
Expand Down Expand Up @@ -229,7 +221,7 @@ impl<S: Services> ForgeApp<S> {
let original_messages = context.messages.len();
let original_token_count = *context.token_count();

let forge_config = self.config.clone();
let forge_config = self.services.get_config()?;

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