From deade5060f858b3c3a9ff4331a6ac1194462cac2 Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 9 Apr 2026 18:46:32 +0530 Subject: [PATCH 1/6] chore: drop unneccesary serviceurl --- crates/forge_api/src/forge_api.rs | 4 ++-- crates/forge_app/src/infra.rs | 2 +- crates/forge_infra/src/forge_infra.rs | 14 ++++++-------- crates/forge_infra/src/grpc.rs | 17 +++++++---------- crates/forge_main/src/main.rs | 10 +--------- crates/forge_repo/src/context_engine.rs | 18 +++++++++--------- crates/forge_repo/src/forge_repo.rs | 2 +- crates/forge_repo/src/fuzzy_search.rs | 2 +- crates/forge_repo/src/skill.rs | 7 +------ crates/forge_repo/src/validation.rs | 2 +- 10 files changed, 30 insertions(+), 48 deletions(-) diff --git a/crates/forge_api/src/forge_api.rs b/crates/forge_api/src/forge_api.rs index d53ce3cd59..4b5fa3893c 100644 --- a/crates/forge_api/src/forge_api.rs +++ b/crates/forge_api/src/forge_api.rs @@ -48,8 +48,8 @@ impl ForgeAPI>, ForgeRepo> { /// * `cwd` - The working directory path for environment and file resolution /// * `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, services_url)); + pub fn init(cwd: PathBuf, config: ForgeConfig) -> Self { + let infra = Arc::new(ForgeInfra::new(cwd, config)); let repo = Arc::new(ForgeRepo::new(infra.clone())); let app = Arc::new(ForgeServices::new(repo.clone())); ForgeAPI::new(app, repo) diff --git a/crates/forge_app/src/infra.rs b/crates/forge_app/src/infra.rs index 9659c3eb4c..2e2d340e9f 100644 --- a/crates/forge_app/src/infra.rs +++ b/crates/forge_app/src/infra.rs @@ -411,7 +411,7 @@ pub trait AgentRepository: Send + Sync { /// cheaply across multiple clients. pub trait GrpcInfra: Send + Sync { /// Returns a cloned gRPC channel for the workspace server - fn channel(&self) -> tonic::transport::Channel; + fn channel(&self) -> anyhow::Result; /// Hydrates the gRPC channel by establishing and then dropping the /// connection diff --git a/crates/forge_infra/src/forge_infra.rs b/crates/forge_infra/src/forge_infra.rs index 0815066da0..3db029866b 100644 --- a/crates/forge_infra/src/forge_infra.rs +++ b/crates/forge_infra/src/forge_infra.rs @@ -64,15 +64,13 @@ impl ForgeInfra { /// * `config` - Pre-read application configuration; used only at /// construction time to initialize infrastructure services /// * `services_url` - Pre-validated URL for the gRPC workspace server - pub fn new(cwd: PathBuf, config: forge_config::ForgeConfig, services_url: Url) -> Self { + pub fn new(cwd: PathBuf, config: forge_config::ForgeConfig) -> Self { let env = to_environment(cwd.clone()); let config_infra = Arc::new(ForgeEnvironmentInfra::new(cwd, config.clone())); - let file_write_service = Arc::new(ForgeFileWriteService::new()); - let http_service = Arc::new(ForgeHttpInfra::new( - config_infra.cached_config().unwrap_or(config), - file_write_service.clone(), - )); + let config = config_infra.cached_config().unwrap_or(config); + + let http_service = Arc::new(ForgeHttpInfra::new(config.clone(), file_write_service.clone())); let file_read_service = Arc::new(ForgeFileReadService::new()); let file_meta_service = Arc::new(ForgeFileMetaService); let directory_reader_service = Arc::new(ForgeDirectoryReaderService::new( @@ -81,7 +79,7 @@ impl ForgeInfra { .map(|c| c.max_parallel_file_reads) .unwrap_or(4), )); - let grpc_client = Arc::new(ForgeGrpcClient::new(services_url)); + let grpc_client = Arc::new(ForgeGrpcClient::new(config.services_url.clone())); let output_printer = Arc::new(StdConsoleWriter::default()); Self { @@ -359,7 +357,7 @@ impl StrategyFactory for ForgeInfra { } impl GrpcInfra for ForgeInfra { - fn channel(&self) -> tonic::transport::Channel { + fn channel(&self) -> anyhow::Result { self.grpc_client.channel() } diff --git a/crates/forge_infra/src/grpc.rs b/crates/forge_infra/src/grpc.rs index 9b5b873992..c669222221 100644 --- a/crates/forge_infra/src/grpc.rs +++ b/crates/forge_infra/src/grpc.rs @@ -10,7 +10,7 @@ use url::Url; /// on first access. #[derive(Clone)] pub struct ForgeGrpcClient { - server_url: Arc, + server_url: String, channel: Arc>>, } @@ -19,22 +19,19 @@ impl ForgeGrpcClient { /// /// # Arguments /// * `server_url` - The URL of the gRPC server - pub fn new(server_url: Url) -> Self { - Self { - server_url: Arc::new(server_url), - channel: Arc::new(Mutex::new(None)), - } + pub fn new(server_url: String) -> Self { + Self { server_url, channel: Arc::new(Mutex::new(None)) } } /// Returns a clone of the underlying gRPC channel /// /// Channels are cheap to clone and can be shared across multiple clients. /// The channel is created on first call and cached for subsequent calls. - pub fn channel(&self) -> Channel { + pub fn channel(&self) -> anyhow::Result { let mut guard = self.channel.lock().unwrap(); if let Some(channel) = guard.as_ref() { - return channel.clone(); + return Ok(channel.clone()); } let mut channel = Channel::from_shared(self.server_url.to_string()) @@ -42,7 +39,7 @@ impl ForgeGrpcClient { .concurrency_limit(256); // Enable TLS for https URLs (webpki-roots is faster than native-roots) - if self.server_url.scheme().contains("https") { + if Url::parse(&self.server_url)?.scheme().contains("https") { let tls_config = tonic::transport::ClientTlsConfig::new().with_webpki_roots(); channel = channel .tls_config(tls_config) @@ -51,7 +48,7 @@ impl ForgeGrpcClient { let new_channel = channel.connect_lazy(); *guard = Some(new_channel.clone()); - new_channel + Ok(new_channel) } /// Hydrates the gRPC channel by forcing its initialization diff --git a/crates/forge_main/src/main.rs b/crates/forge_main/src/main.rs index b5d4748100..0b68b7e30b 100644 --- a/crates/forge_main/src/main.rs +++ b/crates/forge_main/src/main.rs @@ -8,7 +8,6 @@ use forge_api::ForgeAPI; use forge_config::ForgeConfig; use forge_domain::TitleFormat; use forge_main::{Cli, Sandbox, TitleDisplayExt, UI, tracker}; -use url::Url; /// Enables ENABLE_VIRTUAL_TERMINAL_PROCESSING on the stdout console handle. /// @@ -106,13 +105,6 @@ async fn run() -> Result<()> { let config = ForgeConfig::read().context("Failed to read Forge configuration from .forge.toml")?; - // Pre-validate services_url so a malformed URL produces a clear error - // message at startup instead of panicking inside the constructor. - let services_url: Url = config - .services_url - .parse() - .context("services_url in configuration must be a valid URL")?; - // Handle worktree creation if specified let cwd: PathBuf = match (&cli.sandbox, &cli.directory) { (Some(sandbox), Some(cli)) => { @@ -129,7 +121,7 @@ async fn run() -> Result<()> { }; let mut ui = UI::init(cli, config, move |config| { - ForgeAPI::init(cwd.clone(), config, services_url.clone()) + ForgeAPI::init(cwd.clone(), config) })?; ui.run().await; diff --git a/crates/forge_repo/src/context_engine.rs b/crates/forge_repo/src/context_engine.rs index ab9b34abd1..97f5bf68ef 100644 --- a/crates/forge_repo/src/context_engine.rs +++ b/crates/forge_repo/src/context_engine.rs @@ -116,7 +116,7 @@ impl ForgeContextEngineRepository { #[async_trait] impl WorkspaceIndexRepository for ForgeContextEngineRepository { async fn authenticate(&self) -> Result { - let channel = self.infra.channel(); + let channel = self.infra.channel()?; let mut client = ForgeServiceClient::new(channel); let request = tonic::Request::new(CreateApiKeyRequest { user_id: None }); @@ -143,7 +143,7 @@ impl WorkspaceIndexRepository for ForgeContextEngineRepository let request = self.with_auth(request, auth_token)?; - let channel = self.infra.channel(); + let channel = self.infra.channel()?; let mut client = ForgeServiceClient::new(channel); let response = client.create_workspace(request).await?.into_inner(); @@ -173,7 +173,7 @@ impl WorkspaceIndexRepository for ForgeContextEngineRepository let request = self.with_auth(request, auth_token)?; - let channel = self.infra.channel(); + let channel = self.infra.channel()?; let mut client = ForgeServiceClient::new(channel); let response = client.upload_files(request).await?; @@ -212,7 +212,7 @@ impl WorkspaceIndexRepository for ForgeContextEngineRepository let request = self.with_auth(request, auth_token)?; - let channel = self.infra.channel(); + let channel = self.infra.channel()?; let mut client = ForgeServiceClient::new(channel); let response = client.search(request).await?; @@ -283,7 +283,7 @@ impl WorkspaceIndexRepository for ForgeContextEngineRepository let request = tonic::Request::new(ListWorkspacesRequest {}); let request = self.with_auth(request, auth_token)?; - let channel = self.infra.channel(); + let channel = self.infra.channel()?; let mut client = ForgeServiceClient::new(channel); let response = client.list_workspaces(request).await?; @@ -306,7 +306,7 @@ impl WorkspaceIndexRepository for ForgeContextEngineRepository }); let request = self.with_auth(request, auth_token)?; - let channel = self.infra.channel(); + let channel = self.infra.channel()?; let mut client = ForgeServiceClient::new(channel); let response = client.get_workspace_info(request).await?; @@ -328,7 +328,7 @@ impl WorkspaceIndexRepository for ForgeContextEngineRepository let request = self.with_auth(request, auth_token)?; - let channel = self.infra.channel(); + let channel = self.infra.channel()?; let mut client = ForgeServiceClient::new(channel); let response = client.list_files(request).await?; @@ -359,7 +359,7 @@ impl WorkspaceIndexRepository for ForgeContextEngineRepository let request = self.with_auth(request, auth_token)?; - let channel = self.infra.channel(); + let channel = self.infra.channel()?; let mut client = ForgeServiceClient::new(channel); client.delete_files(request).await?; @@ -377,7 +377,7 @@ impl WorkspaceIndexRepository for ForgeContextEngineRepository let request = self.with_auth(request, auth_token)?; - let channel = self.infra.channel(); + let channel = self.infra.channel()?; let mut client = ForgeServiceClient::new(channel); client.delete_workspace(request).await?; diff --git a/crates/forge_repo/src/forge_repo.rs b/crates/forge_repo/src/forge_repo.rs index 229989738d..a938480d9a 100644 --- a/crates/forge_repo/src/forge_repo.rs +++ b/crates/forge_repo/src/forge_repo.rs @@ -632,7 +632,7 @@ impl FuzzySearchRepository for ForgeRepo { } impl GrpcInfra for ForgeRepo { - fn channel(&self) -> tonic::transport::Channel { + fn channel(&self) -> anyhow::Result { self.infra.channel() } diff --git a/crates/forge_repo/src/fuzzy_search.rs b/crates/forge_repo/src/fuzzy_search.rs index 6423abd8ca..5a85e8a1e9 100644 --- a/crates/forge_repo/src/fuzzy_search.rs +++ b/crates/forge_repo/src/fuzzy_search.rs @@ -39,7 +39,7 @@ impl FuzzySearchRepository for ForgeFuzzySearchRepository { }); // Call gRPC API - let channel = self.infra.channel(); + let channel = self.infra.channel()?; let mut client = ForgeServiceClient::new(channel); let response = client .fuzzy_search(request) diff --git a/crates/forge_repo/src/skill.rs b/crates/forge_repo/src/skill.rs index c1dbd77a29..776a438a18 100644 --- a/crates/forge_repo/src/skill.rs +++ b/crates/forge_repo/src/skill.rs @@ -313,12 +313,7 @@ mod tests { let skill_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("src/fixtures/skills_with_resources"); let config = ForgeConfig::read().unwrap_or_default(); - let services_url = config.services_url.parse().unwrap(); - let infra = Arc::new(ForgeInfra::new( - std::env::current_dir().unwrap(), - config, - services_url, - )); + let infra = Arc::new(ForgeInfra::new(std::env::current_dir().unwrap(), config)); let repo = ForgeSkillRepository::new(infra); (repo, skill_dir) } diff --git a/crates/forge_repo/src/validation.rs b/crates/forge_repo/src/validation.rs index d4382bdfc2..536a3ce847 100644 --- a/crates/forge_repo/src/validation.rs +++ b/crates/forge_repo/src/validation.rs @@ -42,7 +42,7 @@ impl ValidationRepository for ForgeValidationRepository { let request = tonic::Request::new(ValidateFilesRequest { files: vec![proto_file] }); // Call gRPC API - let channel = self.infra.channel(); + let channel = self.infra.channel()?; let mut client = ForgeServiceClient::new(channel); let response = client .validate_files(request) From 5b046c8c7beb5a3014276150460d7fc190833041 Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 9 Apr 2026 19:45:55 +0530 Subject: [PATCH 2/6] refactor(agent): move provider/model resolution into AgentRepository impl --- crates/forge_app/src/infra.rs | 6 +--- crates/forge_repo/src/agent.rs | 32 +++++++++++++++++++-- crates/forge_repo/src/forge_repo.rs | 17 ++++------- crates/forge_services/src/agent_registry.rs | 10 ++----- 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/crates/forge_app/src/infra.rs b/crates/forge_app/src/infra.rs index 2e2d340e9f..41d5c092e0 100644 --- a/crates/forge_app/src/infra.rs +++ b/crates/forge_app/src/infra.rs @@ -397,11 +397,7 @@ pub trait AgentRepository: Send + Sync { /// * `provider_id` - Default provider applied to agents that do not specify /// one /// * `model_id` - Default model applied to agents that do not specify one - async fn get_agents( - &self, - provider_id: forge_domain::ProviderId, - model_id: forge_domain::ModelId, - ) -> anyhow::Result>; + async fn get_agents(&self) -> anyhow::Result>; } /// Infrastructure trait for providing shared gRPC channel diff --git a/crates/forge_repo/src/agent.rs b/crates/forge_repo/src/agent.rs index f0a8f67368..a76c204c10 100644 --- a/crates/forge_repo/src/agent.rs +++ b/crates/forge_repo/src/agent.rs @@ -1,8 +1,9 @@ use std::sync::Arc; use anyhow::{Context, Result}; -use forge_app::{DirectoryReaderInfra, EnvironmentInfra, FileInfoInfra}; -use forge_domain::Template; +use forge_app::{AgentRepository, DirectoryReaderInfra, EnvironmentInfra, FileInfoInfra}; +use forge_config::ForgeConfig; +use forge_domain::{ModelId, ProviderId, Template}; use gray_matter::Matter; use gray_matter::engine::YAML; @@ -43,7 +44,7 @@ impl ForgeAgentRepository { impl ForgeAgentRepository { /// Load all agent definitions from all available sources with conflict /// resolution. - pub(crate) async fn load_agents(&self) -> anyhow::Result> { + async fn load_agents(&self) -> anyhow::Result> { self.load_all_agents().await } @@ -156,6 +157,31 @@ fn parse_agent_file(content: &str) -> Result { Ok(agent) } +#[async_trait::async_trait] +impl + DirectoryReaderInfra> + AgentRepository for ForgeAgentRepository +{ + async fn get_agents(&self) -> anyhow::Result> { + let agent_defs = self.load_agents().await?; + + let session = self + .infra + .get_config()? + .session + .ok_or(forge_domain::Error::NoDefaultSession)?; + + Ok(agent_defs + .into_iter() + .map(|def| { + def.into_agent( + ProviderId::from(session.provider_id.clone()), + ModelId::from(session.model_id.clone()), + ) + }) + .collect()) + } +} + #[cfg(test)] mod tests { use pretty_assertions::assert_eq; diff --git a/crates/forge_repo/src/forge_repo.rs b/crates/forge_repo/src/forge_repo.rs index a938480d9a..d2646d1252 100644 --- a/crates/forge_repo/src/forge_repo.rs +++ b/crates/forge_repo/src/forge_repo.rs @@ -8,6 +8,7 @@ use forge_app::{ FileInfoInfra, FileReaderInfra, FileRemoverInfra, FileWriterInfra, GrpcInfra, HttpInfra, KVStore, McpServerInfra, StrategyFactory, UserInfra, WalkedFile, Walker, WalkerInfra, }; +use forge_config::ForgeConfig; use forge_domain::{ AnyProvider, AuthCredential, ChatCompletionMessage, ChatRepository, CommandOutput, Context, Conversation, ConversationId, ConversationRepository, Environment, FileInfo, @@ -487,19 +488,11 @@ where } #[async_trait::async_trait] -impl AgentRepository - for ForgeRepo +impl + DirectoryReaderInfra + Send + Sync> + AgentRepository for ForgeRepo { - async fn get_agents( - &self, - provider_id: forge_domain::ProviderId, - model_id: forge_domain::ModelId, - ) -> anyhow::Result> { - let agent_defs = self.agent_repository.load_agents().await?; - Ok(agent_defs - .into_iter() - .map(|def| def.into_agent(provider_id.clone(), model_id.clone())) - .collect()) + async fn get_agents(&self) -> anyhow::Result> { + self.agent_repository.get_agents().await } } diff --git a/crates/forge_services/src/agent_registry.rs b/crates/forge_services/src/agent_registry.rs index b9df201ef1..2a5f1aaae4 100644 --- a/crates/forge_services/src/agent_registry.rs +++ b/crates/forge_services/src/agent_registry.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use dashmap::DashMap; -use forge_app::domain::{AgentId, Error, ModelId, ProviderId}; +use forge_app::domain::AgentId; use forge_app::{AgentRepository, EnvironmentInfra}; use forge_domain::Agent; use tokio::sync::RwLock; @@ -72,13 +72,7 @@ impl> /// do not specify their own provider/model receive the session-level /// defaults. async fn load_agents(&self) -> anyhow::Result> { - let config = self.repository.get_config()?; - let session = config.session.as_ref().ok_or(Error::NoDefaultSession)?; - let provider_id = ProviderId::from(session.provider_id.clone()); - let model_id = ModelId::new(session.model_id.clone()); - - let agents = self.repository.get_agents(provider_id, model_id).await?; - + let agents = self.repository.get_agents().await?; let agents_map = DashMap::new(); for agent in agents { agents_map.insert(agent.id.as_str().to_string(), agent); From 5380ae7523fa55c5b7b07c66d6a81957d1fe4c50 Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 9 Apr 2026 20:27:09 +0530 Subject: [PATCH 3/6] feat(agent): introduce AgentInfo for lightweight agent metadata listing --- FORGE_TMP/.forge_history | 0 crates/forge_api/src/api.rs | 5 ++ crates/forge_api/src/forge_api.rs | 4 ++ crates/forge_app/src/app.rs | 2 +- crates/forge_app/src/hooks/compaction.rs | 4 +- crates/forge_app/src/hooks/doom_loop.rs | 2 +- crates/forge_app/src/hooks/tracing.rs | 8 +-- crates/forge_app/src/infra.rs | 4 ++ crates/forge_app/src/orch.rs | 4 +- crates/forge_app/src/services.rs | 8 +++ crates/forge_app/src/system_prompt.rs | 2 +- crates/forge_app/src/tool_registry.rs | 18 ++++--- crates/forge_domain/src/agent.rs | 54 ++++++++++++--------- crates/forge_main/src/model.rs | 34 +++---------- crates/forge_main/src/ui.rs | 37 +++++++------- crates/forge_repo/src/agent.rs | 12 +++++ crates/forge_repo/src/agent_definition.rs | 12 +++-- crates/forge_repo/src/forge_repo.rs | 4 ++ crates/forge_services/src/agent_registry.rs | 8 ++- 19 files changed, 132 insertions(+), 90 deletions(-) create mode 100644 FORGE_TMP/.forge_history diff --git a/FORGE_TMP/.forge_history b/FORGE_TMP/.forge_history new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/forge_api/src/api.rs b/crates/forge_api/src/api.rs index dfd144cac5..ceef7975d9 100644 --- a/crates/forge_api/src/api.rs +++ b/crates/forge_api/src/api.rs @@ -32,6 +32,11 @@ pub trait API: Sync + Send { /// Provides a list of agents available in the current environment async fn get_agents(&self) -> Result>; + + /// Provides lightweight metadata for all agents without requiring a + /// configured provider or model + async fn get_agent_infos(&self) -> Result>; + /// Provides a list of providers available in the current environment async fn get_providers(&self) -> Result>; diff --git a/crates/forge_api/src/forge_api.rs b/crates/forge_api/src/forge_api.rs index 4b5fa3893c..8569b21d6b 100644 --- a/crates/forge_api/src/forge_api.rs +++ b/crates/forge_api/src/forge_api.rs @@ -92,6 +92,10 @@ impl< self.services.get_agents().await } + async fn get_agent_infos(&self) -> Result> { + self.services.get_agent_infos().await + } + async fn get_providers(&self) -> Result> { Ok(self.services.get_all_providers().await?) } diff --git a/crates/forge_app/src/app.rs b/crates/forge_app/src/app.rs index c8fec71741..fe8350640d 100644 --- a/crates/forge_app/src/app.rs +++ b/crates/forge_app/src/app.rs @@ -91,7 +91,7 @@ impl> ForgeAp .set_compact_model_if_none(); let agent_provider = agent_provider_resolver - .get_provider(Some(agent.id.clone())) + .get_provider(Some(agent.info.id.clone())) .await?; let agent_provider = self .services diff --git a/crates/forge_app/src/hooks/compaction.rs b/crates/forge_app/src/hooks/compaction.rs index 76e58df83d..1845902d38 100644 --- a/crates/forge_app/src/hooks/compaction.rs +++ b/crates/forge_app/src/hooks/compaction.rs @@ -37,13 +37,13 @@ impl EventHandle> for CompactionHandler { if let Some(context) = &conversation.context { let token_count = context.token_count(); if self.agent.compact.should_compact(context, *token_count) { - info!(agent_id = %self.agent.id, "Compaction triggered by hook"); + info!(agent_id = %self.agent.info.id, "Compaction triggered by hook"); let compacted = Compactor::new(self.agent.compact.clone(), self.environment.clone()) .compact(context.clone(), false)?; conversation.context = Some(compacted); } else { - debug!(agent_id = %self.agent.id, "Compaction not needed"); + debug!(agent_id = %self.agent.info.id, "Compaction not needed"); } } Ok(()) diff --git a/crates/forge_app/src/hooks/doom_loop.rs b/crates/forge_app/src/hooks/doom_loop.rs index 3515b74e7b..73dbf1d8f4 100644 --- a/crates/forge_app/src/hooks/doom_loop.rs +++ b/crates/forge_app/src/hooks/doom_loop.rs @@ -227,7 +227,7 @@ impl EventHandle> for DoomLoopDetector { ) -> anyhow::Result<()> { if let Some(consecutive_calls) = self.detect_from_conversation(conversation) { warn!( - agent_id = %event.agent.id, + agent_id = %event.agent.info.id, request_count = event.payload.request_count, consecutive_calls, "Doom loop detected from conversation context before next request" diff --git a/crates/forge_app/src/hooks/tracing.rs b/crates/forge_app/src/hooks/tracing.rs index 94755f2b2c..de98c24155 100644 --- a/crates/forge_app/src/hooks/tracing.rs +++ b/crates/forge_app/src/hooks/tracing.rs @@ -33,7 +33,7 @@ impl EventHandle> for TracingHandler { ) -> anyhow::Result<()> { debug!( conversation_id = %conversation.id, - agent = %event.agent.id, + agent = %event.agent.info.id, model = %event.model_id, "Initializing agent" ); @@ -78,7 +78,7 @@ impl EventHandle> for TracingHandler { } debug!( - agent_id = %event.agent.id, + agent_id = %event.agent.info.id, tool_call_count = message.tool_calls.len(), "Tool call count" ); @@ -97,7 +97,7 @@ impl EventHandle> for TracingHandler { let tool_call = &event.payload.tool_call; debug!( - agent_id = %event.agent.id, + agent_id = %event.agent.info.id, tool_name = %tool_call.name, call_id = ?tool_call.call_id, arguments = %tool_call.arguments.to_owned().into_string(), @@ -120,7 +120,7 @@ impl EventHandle> for TracingHandler { if result.is_error() { warn!( - agent_id = %event.agent.id, + agent_id = %event.agent.info.id, name = %tool_call.name, call_id = ?tool_call.call_id, arguments = %tool_call.arguments.to_owned().into_string(), diff --git a/crates/forge_app/src/infra.rs b/crates/forge_app/src/infra.rs index 41d5c092e0..e4b49bfb66 100644 --- a/crates/forge_app/src/infra.rs +++ b/crates/forge_app/src/infra.rs @@ -398,6 +398,10 @@ pub trait AgentRepository: Send + Sync { /// one /// * `model_id` - Default model applied to agents that do not specify one async fn get_agents(&self) -> anyhow::Result>; + + /// Load lightweight metadata for all agents without requiring a configured + /// provider or model. + async fn get_agent_infos(&self) -> anyhow::Result>; } /// Infrastructure trait for providing shared gRPC channel diff --git a/crates/forge_app/src/orch.rs b/crates/forge_app/src/orch.rs index 86157c24e2..7638e18cf5 100644 --- a/crates/forge_app/src/orch.rs +++ b/crates/forge_app/src/orch.rs @@ -278,7 +278,7 @@ impl> Orc }, self.sender.as_ref().map(|sender| { let sender = sender.clone(); - let agent_id = self.agent.id.clone(); + let agent_id = self.agent.info.id.clone(); let model_id = model_id.clone(); move |error: &anyhow::Error, duration: Duration| { let root_cause = error.root_cause(); @@ -380,7 +380,7 @@ impl> Orc if request_count >= max_request_allowed { // Log warning - important for understanding conversation interruptions warn!( - agent_id = %self.agent.id, + agent_id = %self.agent.info.id, model_id = %model_id, request_count, max_request_allowed, diff --git a/crates/forge_app/src/services.rs b/crates/forge_app/src/services.rs index ee1919b6d9..4ec2c49809 100644 --- a/crates/forge_app/src/services.rs +++ b/crates/forge_app/src/services.rs @@ -469,6 +469,10 @@ pub trait AgentRegistry: Send + Sync { /// Get all agents from the registry store async fn get_agents(&self) -> anyhow::Result>; + /// Get lightweight metadata for all agents without requiring a configured + /// provider or model + async fn get_agent_infos(&self) -> anyhow::Result>; + /// Get agent by ID (from registry store) async fn get_agent(&self, agent_id: &AgentId) -> anyhow::Result>; @@ -918,6 +922,10 @@ impl AgentRegistry for I { self.agent_registry().get_agents().await } + async fn get_agent_infos(&self) -> anyhow::Result> { + self.agent_registry().get_agent_infos().await + } + async fn get_agent(&self, agent_id: &AgentId) -> anyhow::Result> { self.agent_registry().get_agent(agent_id).await } diff --git a/crates/forge_app/src/system_prompt.rs b/crates/forge_app/src/system_prompt.rs index f4deb24643..3a575cc5b5 100644 --- a/crates/forge_app/src/system_prompt.rs +++ b/crates/forge_app/src/system_prompt.rs @@ -152,7 +152,7 @@ impl SystemPrompt { }; debug!( - agent_id = %agent.id, + agent_id = %agent.info.id, model_id = %model_id, tool_supported, "Tool support check" diff --git a/crates/forge_app/src/tool_registry.rs b/crates/forge_app/src/tool_registry.rs index 0db7741760..43a848e43b 100644 --- a/crates/forge_app/src/tool_registry.rs +++ b/crates/forge_app/src/tool_registry.rs @@ -722,9 +722,12 @@ fn create_test_agents() -> Vec { ProviderId::ANTHROPIC, ModelId::new("claude-3-5-sonnet-20241022"), ) - .id(AgentId::new("sage")) - .title("Research Agent") - .description("Specialized in researching codebases") + .info( + forge_domain::AgentInfo::default() + .id(AgentId::new("sage")) + .title("Research Agent") + .description("Specialized in researching codebases"), + ) .tools(vec![ ToolName::new("read"), ToolName::new("fs_search"), @@ -736,9 +739,12 @@ fn create_test_agents() -> Vec { ProviderId::ANTHROPIC, ModelId::new("claude-3-5-sonnet-20241022"), ) - .id(AgentId::new("debug")) - .title("Debug Agent") - .description("Specialized in debugging issues") + .info( + forge_domain::AgentInfo::default() + .id(AgentId::new("debug")) + .title("Debug Agent") + .description("Specialized in debugging issues"), + ) .tools(vec![ ToolName::new("read"), ToolName::new("shell"), diff --git a/crates/forge_domain/src/agent.rs b/crates/forge_domain/src/agent.rs index 7def44bc97..c6d919859b 100644 --- a/crates/forge_domain/src/agent.rs +++ b/crates/forge_domain/src/agent.rs @@ -106,31 +106,26 @@ pub fn estimate_token_count(count: usize) -> usize { #[derive(Debug, Clone, PartialEq, Setters, Serialize, Deserialize, JsonSchema)] #[setters(strip_option, into)] pub struct Agent { + /// Lightweight metadata (id, title, description) shared with AgentInfo + #[serde(flatten)] + pub info: AgentInfo, + /// Flag to enable/disable tool support for this agent. pub tool_supported: Option, - // Unique identifier for the agent - pub id: AgentId, - /// Path to the agent definition file, if loaded from a file pub path: Option, - /// Human-readable title for the agent - pub title: Option, - - // Required provider for the agent + /// Required provider for the agent pub provider: ProviderId, - // Required language model ID to be used by this agent + /// Required language model ID to be used by this agent pub model: ModelId, - // Human-readable description of the agent's purpose - pub description: Option, - - // Template for the system prompt provided to the agent + /// Template for the system prompt provided to the agent pub system_prompt: Option>, - // Template for the user prompt provided to the agent + /// Template for the user prompt provided to the agent pub user_prompt: Option>, /// Tools that the agent can use @@ -168,16 +163,29 @@ pub struct Agent { pub max_requests_per_turn: Option, } +/// Lightweight metadata about an agent, used for listing without requiring a +/// configured provider or model. +#[derive(Debug, Default, Clone, PartialEq, Setters, Serialize, Deserialize, JsonSchema)] +#[setters(strip_option, into)] +pub struct AgentInfo { + /// Unique identifier for the agent + pub id: AgentId, + + /// Human-readable title for the agent + pub title: Option, + + /// Human-readable description of the agent's purpose + pub description: Option, +} + impl Agent { /// Create a new Agent with required provider and model pub fn new(id: impl Into, provider: ProviderId, model: ModelId) -> Self { Self { - id: id.into(), + info: AgentInfo { id: id.into(), ..Default::default() }, provider, model, - title: Default::default(), tool_supported: Default::default(), - description: Default::default(), system_prompt: Default::default(), user_prompt: Default::default(), tools: Default::default(), @@ -201,11 +209,13 @@ impl Agent { /// /// Returns an error if the agent has no description pub fn tool_definition(&self) -> Result { - if self.description.is_none() || self.description.as_ref().is_none_or(|d| d.is_empty()) { - return Err(Error::MissingAgentDescription(self.id.clone())); + if self.info.description.is_none() + || self.info.description.as_ref().is_none_or(|d| d.is_empty()) + { + return Err(Error::MissingAgentDescription(self.info.id.clone())); } - Ok(ToolDefinition::new(self.id.as_str().to_string()) - .description(self.description.clone().unwrap())) + Ok(ToolDefinition::new(self.info.id.as_str().to_string()) + .description(self.info.description.clone().unwrap())) } /// Sets the model in compaction config if not already set @@ -227,8 +237,8 @@ impl Agent { impl From for ToolDefinition { fn from(value: Agent) -> Self { - let description = value.description.unwrap_or_default(); - let name = ToolName::new(value.id); + let description = value.info.description.unwrap_or_default(); + let name = ToolName::new(value.info.id); ToolDefinition { name, description, diff --git a/crates/forge_main/src/model.rs b/crates/forge_main/src/model.rs index f81e11bd95..610a217bac 100644 --- a/crates/forge_main/src/model.rs +++ b/crates/forge_main/src/model.rs @@ -1,6 +1,6 @@ use std::sync::{Arc, Mutex}; -use forge_api::{Agent, Model, Template}; +use forge_api::{AgentInfo, Model, Template}; use forge_domain::UserCommand; use strum::{EnumProperty, IntoEnumIterator}; use strum_macros::{EnumIter, EnumProperty}; @@ -143,7 +143,10 @@ impl ForgeCommandManager { /// Registers agent commands to the manager. /// Returns information about the registration process. - pub fn register_agent_commands(&self, agents: Vec) -> AgentCommandRegistrationResult { + pub fn register_agent_commands( + &self, + agents: Vec, + ) -> AgentCommandRegistrationResult { let mut guard = self.commands.lock().unwrap(); let mut result = AgentCommandRegistrationResult { registered_count: 0, skipped_conflicts: Vec::new() }; @@ -840,24 +843,11 @@ mod tests { #[test] fn test_register_agent_commands() { - use forge_api::Agent; - use forge_domain::{ModelId, ProviderId}; - // Setup let fixture = ForgeCommandManager::default(); let agents = vec![ - Agent::new( - "test-agent", - ProviderId::ANTHROPIC, - ModelId::new("claude-3-5-sonnet-20241022"), - ) - .title("Test Agent".to_string()), - Agent::new( - "another", - ProviderId::ANTHROPIC, - ModelId::new("claude-3-5-sonnet-20241022"), - ) - .title("Another Agent".to_string()), + forge_domain::AgentInfo::default().id("test-agent").title("Test Agent".to_string()), + forge_domain::AgentInfo::default().id("another").title("Another Agent".to_string()), ]; // Execute @@ -885,18 +875,10 @@ mod tests { #[test] fn test_parse_agent_switch_command() { - use forge_api::Agent; - use forge_domain::{ModelId, ProviderId}; - // Setup let fixture = ForgeCommandManager::default(); let agents = vec![ - Agent::new( - "test-agent", - ProviderId::ANTHROPIC, - ModelId::new("claude-3-5-sonnet-20241022"), - ) - .title("Test Agent".to_string()), + forge_domain::AgentInfo::default().id("test-agent").title("Test Agent".to_string()), ]; let _result = fixture.register_agent_commands(agents); diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index 5ba5de69e4..8eea3b6fdc 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -183,11 +183,10 @@ impl A + Send + Sync> UI // Convert string to AgentId for validation let agent = self .api - .get_agents() + .get_agent_infos() .await? - .iter() - .find(|agent| agent.id == agent_id) - .cloned() + .into_iter() + .find(|info| info.id == agent_id) .ok_or(anyhow::anyhow!("Undefined agent: {agent_id}"))?; // Update the app config with the new operating agent. @@ -374,7 +373,7 @@ impl A + Send + Sync> UI let api = self.api.clone(); tokio::spawn(async move { api.get_tools().await }); let api = self.api.clone(); - tokio::spawn(async move { api.get_agents().await }); + tokio::spawn(async move { api.get_agent_infos().await }); let api = self.api.clone(); tokio::spawn(async move { let _ = api.hydrate_channel(); @@ -1090,7 +1089,7 @@ impl A + Send + Sync> UI async fn build_agents_info(&self, custom: bool) -> anyhow::Result { let mut agents = self.api.get_agents().await?; // Sort agents alphabetically by ID - agents.sort_by(|a, b| a.id.as_str().cmp(b.id.as_str())); + agents.sort_by(|a, b| a.info.id.as_str().cmp(b.info.id.as_str())); // Filter agents based on custom flag if custom { @@ -1100,14 +1099,15 @@ impl A + Send + Sync> UI let mut info = Info::new(); for agent in agents.iter() { - let id = agent.id.as_str().to_string(); + let id = agent.info.id.as_str().to_string(); let title = agent + .info .title .as_deref() .map(|title| title.lines().collect::>().join(" ")); // Get provider and model for this agent - let provider_name = match self.get_provider(Some(agent.id.clone())).await { + let provider_name = match self.get_provider(Some(agent.info.id.clone())).await { Ok(p) => p.id.to_string(), Err(e) => format!("Error: [{}]", e), }; @@ -1145,7 +1145,7 @@ impl A + Send + Sync> UI } async fn on_show_agents(&mut self, porcelain: bool, custom: bool) -> anyhow::Result<()> { - let agents = self.api.get_agents().await?; + let agents = self.api.get_agent_infos().await?; if agents.is_empty() { return Ok(()); @@ -1336,14 +1336,15 @@ impl A + Send + Sync> UI "Planning and strategy agent [alias for: muse]", ); - // Fetch agents and add them to the commands list - let agents = self.api.get_agents().await?; - for agent in agents { - let title = agent + // Fetch agent infos and add them to the commands list. + // Uses get_agent_infos() so no provider/model is required for listing. + let agent_infos = self.api.get_agent_infos().await?; + for agent_info in agent_infos { + let title = agent_info .title .map(|title| title.lines().collect::>().join(" ")); info = info - .add_title(agent.id.to_string()) + .add_title(agent_info.id.to_string()) .add_key_value("type", CommandType::Agent) .add_key_value("description", title); } @@ -1469,7 +1470,7 @@ impl A + Send + Sync> UI self.spinner.start(Some("Loading"))?; let all_tools = self.api.get_tools().await?; let agents = self.api.get_agents().await?; - let agent = agents.into_iter().find(|agent| agent.id == agent_id); + let agent = agents.into_iter().find(|agent| agent.info.id == agent_id); let agent_tools = if let Some(agent) = agent { let resolver = ToolResolver::new(all_tools.clone().into()); resolver @@ -1999,7 +2000,7 @@ impl A + Send + Sync> UI } } - let agents = self.api.get_agents().await?; + let agents = self.api.get_agent_infos().await?; if agents.is_empty() { return Ok(false); @@ -2077,7 +2078,7 @@ impl A + Send + Sync> UI } SlashCommand::AgentSwitch(agent_id) => { // Validate that the agent exists by checking against loaded agents - let agents = self.api.get_agents().await?; + let agents = self.api.get_agent_infos().await?; let agent_exists = agents.iter().any(|agent| agent.id.as_str() == agent_id); if agent_exists { @@ -3096,7 +3097,7 @@ impl A + Send + Sync> UI // Execute independent operations in parallel to improve performance let (agents_result, commands_result) = - tokio::join!(self.api.get_agents(), self.api.get_commands()); + tokio::join!(self.api.get_agent_infos(), self.api.get_commands()); // Register agent commands with proper error handling and user feedback match agents_result { diff --git a/crates/forge_repo/src/agent.rs b/crates/forge_repo/src/agent.rs index a76c204c10..2e225e8eb9 100644 --- a/crates/forge_repo/src/agent.rs +++ b/crates/forge_repo/src/agent.rs @@ -180,6 +180,18 @@ impl + DirectoryReader }) .collect()) } + + async fn get_agent_infos(&self) -> anyhow::Result> { + let agent_defs = self.load_agents().await?; + Ok(agent_defs + .into_iter() + .map(|def| forge_domain::AgentInfo { + id: def.id, + title: def.title, + description: def.description, + }) + .collect()) + } } #[cfg(test)] diff --git a/crates/forge_repo/src/agent_definition.rs b/crates/forge_repo/src/agent_definition.rs index eccdcc377a..7ae77bf7d4 100644 --- a/crates/forge_repo/src/agent_definition.rs +++ b/crates/forge_repo/src/agent_definition.rs @@ -1,7 +1,7 @@ use derive_setters::Setters; use forge_domain::{ - Agent, AgentId, Compact, EventContext, MaxTokens, ModelId, ProviderId, ReasoningConfig, - SystemContext, Temperature, Template, ToolName, TopK, TopP, + Agent, AgentId, AgentInfo, Compact, EventContext, MaxTokens, ModelId, ProviderId, + ReasoningConfig, SystemContext, Temperature, Template, ToolName, TopK, TopP, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -142,10 +142,12 @@ impl AgentDefinition { /// * `model_id` - Default model to use when the definition has none pub fn into_agent(self, provider_id: ProviderId, model_id: ModelId) -> Agent { Agent { + info: AgentInfo { + id: self.id, + title: self.title, + description: self.description, + }, tool_supported: self.tool_supported, - id: self.id, - title: self.title, - description: self.description, provider: self.provider.unwrap_or(provider_id), model: self.model.unwrap_or(model_id), system_prompt: self.system_prompt, diff --git a/crates/forge_repo/src/forge_repo.rs b/crates/forge_repo/src/forge_repo.rs index d2646d1252..34d1bb8498 100644 --- a/crates/forge_repo/src/forge_repo.rs +++ b/crates/forge_repo/src/forge_repo.rs @@ -494,6 +494,10 @@ impl + DirectoryReader async fn get_agents(&self) -> anyhow::Result> { self.agent_repository.get_agents().await } + + async fn get_agent_infos(&self) -> anyhow::Result> { + self.agent_repository.get_agent_infos().await + } } #[async_trait::async_trait] diff --git a/crates/forge_services/src/agent_registry.rs b/crates/forge_services/src/agent_registry.rs index 2a5f1aaae4..7bc93c91ed 100644 --- a/crates/forge_services/src/agent_registry.rs +++ b/crates/forge_services/src/agent_registry.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use dashmap::DashMap; use forge_app::domain::AgentId; use forge_app::{AgentRepository, EnvironmentInfra}; -use forge_domain::Agent; +use forge_domain::{Agent, AgentInfo}; use tokio::sync::RwLock; /// AgentRegistryService manages the active-agent ID and a registry of runtime @@ -75,7 +75,7 @@ impl> let agents = self.repository.get_agents().await?; let agents_map = DashMap::new(); for agent in agents { - agents_map.insert(agent.id.as_str().to_string(), agent); + agents_map.insert(agent.info.id.as_str().to_string(), agent); } Ok(agents_map) @@ -102,6 +102,10 @@ impl + Ok(agents.iter().map(|entry| entry.value().clone()).collect()) } + async fn get_agent_infos(&self) -> anyhow::Result> { + self.repository.get_agent_infos().await + } + async fn get_agent(&self, agent_id: &AgentId) -> anyhow::Result> { let agents = self.ensure_agents_loaded().await?; Ok(agents.get(agent_id.as_str()).map(|v| v.value().clone())) From 803c3347d02ca06d8fc5097c6c892b80de8d92dc Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 9 Apr 2026 20:34:08 +0530 Subject: [PATCH 4/6] refactor(agent): flatten AgentInfo fields directly into Agent struct --- crates/forge_app/src/app.rs | 2 +- crates/forge_app/src/hooks/compaction.rs | 4 +-- crates/forge_app/src/hooks/doom_loop.rs | 2 +- crates/forge_app/src/hooks/tracing.rs | 8 +++--- crates/forge_app/src/orch.rs | 4 +-- crates/forge_app/src/system_prompt.rs | 2 +- crates/forge_app/src/tool_registry.rs | 16 +++-------- crates/forge_domain/src/agent.rs | 30 +++++++++++++-------- crates/forge_main/src/ui.rs | 9 +++---- crates/forge_repo/src/agent_definition.rs | 10 +++---- crates/forge_services/src/agent_registry.rs | 2 +- 11 files changed, 43 insertions(+), 46 deletions(-) diff --git a/crates/forge_app/src/app.rs b/crates/forge_app/src/app.rs index fe8350640d..c8fec71741 100644 --- a/crates/forge_app/src/app.rs +++ b/crates/forge_app/src/app.rs @@ -91,7 +91,7 @@ impl> ForgeAp .set_compact_model_if_none(); let agent_provider = agent_provider_resolver - .get_provider(Some(agent.info.id.clone())) + .get_provider(Some(agent.id.clone())) .await?; let agent_provider = self .services diff --git a/crates/forge_app/src/hooks/compaction.rs b/crates/forge_app/src/hooks/compaction.rs index 1845902d38..76e58df83d 100644 --- a/crates/forge_app/src/hooks/compaction.rs +++ b/crates/forge_app/src/hooks/compaction.rs @@ -37,13 +37,13 @@ impl EventHandle> for CompactionHandler { if let Some(context) = &conversation.context { let token_count = context.token_count(); if self.agent.compact.should_compact(context, *token_count) { - info!(agent_id = %self.agent.info.id, "Compaction triggered by hook"); + info!(agent_id = %self.agent.id, "Compaction triggered by hook"); let compacted = Compactor::new(self.agent.compact.clone(), self.environment.clone()) .compact(context.clone(), false)?; conversation.context = Some(compacted); } else { - debug!(agent_id = %self.agent.info.id, "Compaction not needed"); + debug!(agent_id = %self.agent.id, "Compaction not needed"); } } Ok(()) diff --git a/crates/forge_app/src/hooks/doom_loop.rs b/crates/forge_app/src/hooks/doom_loop.rs index 73dbf1d8f4..3515b74e7b 100644 --- a/crates/forge_app/src/hooks/doom_loop.rs +++ b/crates/forge_app/src/hooks/doom_loop.rs @@ -227,7 +227,7 @@ impl EventHandle> for DoomLoopDetector { ) -> anyhow::Result<()> { if let Some(consecutive_calls) = self.detect_from_conversation(conversation) { warn!( - agent_id = %event.agent.info.id, + agent_id = %event.agent.id, request_count = event.payload.request_count, consecutive_calls, "Doom loop detected from conversation context before next request" diff --git a/crates/forge_app/src/hooks/tracing.rs b/crates/forge_app/src/hooks/tracing.rs index de98c24155..94755f2b2c 100644 --- a/crates/forge_app/src/hooks/tracing.rs +++ b/crates/forge_app/src/hooks/tracing.rs @@ -33,7 +33,7 @@ impl EventHandle> for TracingHandler { ) -> anyhow::Result<()> { debug!( conversation_id = %conversation.id, - agent = %event.agent.info.id, + agent = %event.agent.id, model = %event.model_id, "Initializing agent" ); @@ -78,7 +78,7 @@ impl EventHandle> for TracingHandler { } debug!( - agent_id = %event.agent.info.id, + agent_id = %event.agent.id, tool_call_count = message.tool_calls.len(), "Tool call count" ); @@ -97,7 +97,7 @@ impl EventHandle> for TracingHandler { let tool_call = &event.payload.tool_call; debug!( - agent_id = %event.agent.info.id, + agent_id = %event.agent.id, tool_name = %tool_call.name, call_id = ?tool_call.call_id, arguments = %tool_call.arguments.to_owned().into_string(), @@ -120,7 +120,7 @@ impl EventHandle> for TracingHandler { if result.is_error() { warn!( - agent_id = %event.agent.info.id, + agent_id = %event.agent.id, name = %tool_call.name, call_id = ?tool_call.call_id, arguments = %tool_call.arguments.to_owned().into_string(), diff --git a/crates/forge_app/src/orch.rs b/crates/forge_app/src/orch.rs index 7638e18cf5..86157c24e2 100644 --- a/crates/forge_app/src/orch.rs +++ b/crates/forge_app/src/orch.rs @@ -278,7 +278,7 @@ impl> Orc }, self.sender.as_ref().map(|sender| { let sender = sender.clone(); - let agent_id = self.agent.info.id.clone(); + let agent_id = self.agent.id.clone(); let model_id = model_id.clone(); move |error: &anyhow::Error, duration: Duration| { let root_cause = error.root_cause(); @@ -380,7 +380,7 @@ impl> Orc if request_count >= max_request_allowed { // Log warning - important for understanding conversation interruptions warn!( - agent_id = %self.agent.info.id, + agent_id = %self.agent.id, model_id = %model_id, request_count, max_request_allowed, diff --git a/crates/forge_app/src/system_prompt.rs b/crates/forge_app/src/system_prompt.rs index 3a575cc5b5..f4deb24643 100644 --- a/crates/forge_app/src/system_prompt.rs +++ b/crates/forge_app/src/system_prompt.rs @@ -152,7 +152,7 @@ impl SystemPrompt { }; debug!( - agent_id = %agent.info.id, + agent_id = %agent.id, model_id = %model_id, tool_supported, "Tool support check" diff --git a/crates/forge_app/src/tool_registry.rs b/crates/forge_app/src/tool_registry.rs index 43a848e43b..25b6c5e1eb 100644 --- a/crates/forge_app/src/tool_registry.rs +++ b/crates/forge_app/src/tool_registry.rs @@ -722,12 +722,8 @@ fn create_test_agents() -> Vec { ProviderId::ANTHROPIC, ModelId::new("claude-3-5-sonnet-20241022"), ) - .info( - forge_domain::AgentInfo::default() - .id(AgentId::new("sage")) - .title("Research Agent") - .description("Specialized in researching codebases"), - ) + .title("Research Agent") + .description("Specialized in researching codebases") .tools(vec![ ToolName::new("read"), ToolName::new("fs_search"), @@ -739,12 +735,8 @@ fn create_test_agents() -> Vec { ProviderId::ANTHROPIC, ModelId::new("claude-3-5-sonnet-20241022"), ) - .info( - forge_domain::AgentInfo::default() - .id(AgentId::new("debug")) - .title("Debug Agent") - .description("Specialized in debugging issues"), - ) + .title("Debug Agent") + .description("Specialized in debugging issues") .tools(vec![ ToolName::new("read"), ToolName::new("shell"), diff --git a/crates/forge_domain/src/agent.rs b/crates/forge_domain/src/agent.rs index c6d919859b..f44c7c4a2d 100644 --- a/crates/forge_domain/src/agent.rs +++ b/crates/forge_domain/src/agent.rs @@ -102,13 +102,19 @@ pub fn estimate_token_count(count: usize) -> usize { count / 4 } + /// Runtime agent representation with required model and provider #[derive(Debug, Clone, PartialEq, Setters, Serialize, Deserialize, JsonSchema)] #[setters(strip_option, into)] pub struct Agent { - /// Lightweight metadata (id, title, description) shared with AgentInfo - #[serde(flatten)] - pub info: AgentInfo, + /// Unique identifier for the agent + pub id: AgentId, + + /// Human-readable title for the agent + pub title: Option, + + /// Human-readable description of the agent's purpose + pub description: Option, /// Flag to enable/disable tool support for this agent. pub tool_supported: Option, @@ -182,7 +188,9 @@ impl Agent { /// Create a new Agent with required provider and model pub fn new(id: impl Into, provider: ProviderId, model: ModelId) -> Self { Self { - info: AgentInfo { id: id.into(), ..Default::default() }, + id: id.into(), + title: Default::default(), + description: Default::default(), provider, model, tool_supported: Default::default(), @@ -209,13 +217,13 @@ impl Agent { /// /// Returns an error if the agent has no description pub fn tool_definition(&self) -> Result { - if self.info.description.is_none() - || self.info.description.as_ref().is_none_or(|d| d.is_empty()) + if self.description.is_none() + || self.description.as_ref().is_none_or(|d| d.is_empty()) { - return Err(Error::MissingAgentDescription(self.info.id.clone())); + return Err(Error::MissingAgentDescription(self.id.clone())); } - Ok(ToolDefinition::new(self.info.id.as_str().to_string()) - .description(self.info.description.clone().unwrap())) + Ok(ToolDefinition::new(self.id.as_str().to_string()) + .description(self.description.clone().unwrap())) } /// Sets the model in compaction config if not already set @@ -237,8 +245,8 @@ impl Agent { impl From for ToolDefinition { fn from(value: Agent) -> Self { - let description = value.info.description.unwrap_or_default(); - let name = ToolName::new(value.info.id); + let description = value.description.unwrap_or_default(); + let name = ToolName::new(value.id); ToolDefinition { name, description, diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index 8eea3b6fdc..ca01a0b657 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -1089,7 +1089,7 @@ impl A + Send + Sync> UI async fn build_agents_info(&self, custom: bool) -> anyhow::Result { let mut agents = self.api.get_agents().await?; // Sort agents alphabetically by ID - agents.sort_by(|a, b| a.info.id.as_str().cmp(b.info.id.as_str())); + agents.sort_by(|a, b| a.id.as_str().cmp(b.id.as_str())); // Filter agents based on custom flag if custom { @@ -1099,15 +1099,14 @@ impl A + Send + Sync> UI let mut info = Info::new(); for agent in agents.iter() { - let id = agent.info.id.as_str().to_string(); + let id = agent.id.as_str().to_string(); let title = agent - .info .title .as_deref() .map(|title| title.lines().collect::>().join(" ")); // Get provider and model for this agent - let provider_name = match self.get_provider(Some(agent.info.id.clone())).await { + let provider_name = match self.get_provider(Some(agent.id.clone())).await { Ok(p) => p.id.to_string(), Err(e) => format!("Error: [{}]", e), }; @@ -1470,7 +1469,7 @@ impl A + Send + Sync> UI self.spinner.start(Some("Loading"))?; let all_tools = self.api.get_tools().await?; let agents = self.api.get_agents().await?; - let agent = agents.into_iter().find(|agent| agent.info.id == agent_id); + let agent = agents.into_iter().find(|agent| agent.id == agent_id); let agent_tools = if let Some(agent) = agent { let resolver = ToolResolver::new(all_tools.clone().into()); resolver diff --git a/crates/forge_repo/src/agent_definition.rs b/crates/forge_repo/src/agent_definition.rs index 7ae77bf7d4..86140b204c 100644 --- a/crates/forge_repo/src/agent_definition.rs +++ b/crates/forge_repo/src/agent_definition.rs @@ -1,6 +1,6 @@ use derive_setters::Setters; use forge_domain::{ - Agent, AgentId, AgentInfo, Compact, EventContext, MaxTokens, ModelId, ProviderId, + Agent, AgentId, Compact, EventContext, MaxTokens, ModelId, ProviderId, ReasoningConfig, SystemContext, Temperature, Template, ToolName, TopK, TopP, }; use schemars::JsonSchema; @@ -142,11 +142,9 @@ impl AgentDefinition { /// * `model_id` - Default model to use when the definition has none pub fn into_agent(self, provider_id: ProviderId, model_id: ModelId) -> Agent { Agent { - info: AgentInfo { - id: self.id, - title: self.title, - description: self.description, - }, + id: self.id, + title: self.title, + description: self.description, tool_supported: self.tool_supported, provider: self.provider.unwrap_or(provider_id), model: self.model.unwrap_or(model_id), diff --git a/crates/forge_services/src/agent_registry.rs b/crates/forge_services/src/agent_registry.rs index 7bc93c91ed..5e547bd212 100644 --- a/crates/forge_services/src/agent_registry.rs +++ b/crates/forge_services/src/agent_registry.rs @@ -75,7 +75,7 @@ impl> let agents = self.repository.get_agents().await?; let agents_map = DashMap::new(); for agent in agents { - agents_map.insert(agent.info.id.as_str().to_string(), agent); + agents_map.insert(agent.id.as_str().to_string(), agent); } Ok(agents_map) From f09dc7d73340121bb291a81b335348c56b771ae4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:06:29 +0000 Subject: [PATCH 5/6] [autofix.ci] apply automated fixes --- crates/forge_domain/src/agent.rs | 5 +---- crates/forge_infra/src/forge_infra.rs | 5 ++++- crates/forge_main/src/model.rs | 12 +++++++++--- crates/forge_repo/src/agent_definition.rs | 4 ++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/forge_domain/src/agent.rs b/crates/forge_domain/src/agent.rs index f44c7c4a2d..86c9a3a3e8 100644 --- a/crates/forge_domain/src/agent.rs +++ b/crates/forge_domain/src/agent.rs @@ -102,7 +102,6 @@ pub fn estimate_token_count(count: usize) -> usize { count / 4 } - /// Runtime agent representation with required model and provider #[derive(Debug, Clone, PartialEq, Setters, Serialize, Deserialize, JsonSchema)] #[setters(strip_option, into)] @@ -217,9 +216,7 @@ impl Agent { /// /// Returns an error if the agent has no description pub fn tool_definition(&self) -> Result { - if self.description.is_none() - || self.description.as_ref().is_none_or(|d| d.is_empty()) - { + if self.description.is_none() || self.description.as_ref().is_none_or(|d| d.is_empty()) { return Err(Error::MissingAgentDescription(self.id.clone())); } Ok(ToolDefinition::new(self.id.as_str().to_string()) diff --git a/crates/forge_infra/src/forge_infra.rs b/crates/forge_infra/src/forge_infra.rs index 3db029866b..3a3e602d17 100644 --- a/crates/forge_infra/src/forge_infra.rs +++ b/crates/forge_infra/src/forge_infra.rs @@ -70,7 +70,10 @@ impl ForgeInfra { let file_write_service = Arc::new(ForgeFileWriteService::new()); let config = config_infra.cached_config().unwrap_or(config); - let http_service = Arc::new(ForgeHttpInfra::new(config.clone(), file_write_service.clone())); + let http_service = Arc::new(ForgeHttpInfra::new( + config.clone(), + file_write_service.clone(), + )); let file_read_service = Arc::new(ForgeFileReadService::new()); let file_meta_service = Arc::new(ForgeFileMetaService); let directory_reader_service = Arc::new(ForgeDirectoryReaderService::new( diff --git a/crates/forge_main/src/model.rs b/crates/forge_main/src/model.rs index 610a217bac..f88cf4715b 100644 --- a/crates/forge_main/src/model.rs +++ b/crates/forge_main/src/model.rs @@ -846,8 +846,12 @@ mod tests { // Setup let fixture = ForgeCommandManager::default(); let agents = vec![ - forge_domain::AgentInfo::default().id("test-agent").title("Test Agent".to_string()), - forge_domain::AgentInfo::default().id("another").title("Another Agent".to_string()), + forge_domain::AgentInfo::default() + .id("test-agent") + .title("Test Agent".to_string()), + forge_domain::AgentInfo::default() + .id("another") + .title("Another Agent".to_string()), ]; // Execute @@ -878,7 +882,9 @@ mod tests { // Setup let fixture = ForgeCommandManager::default(); let agents = vec![ - forge_domain::AgentInfo::default().id("test-agent").title("Test Agent".to_string()), + forge_domain::AgentInfo::default() + .id("test-agent") + .title("Test Agent".to_string()), ]; let _result = fixture.register_agent_commands(agents); diff --git a/crates/forge_repo/src/agent_definition.rs b/crates/forge_repo/src/agent_definition.rs index 86140b204c..2819cbdae5 100644 --- a/crates/forge_repo/src/agent_definition.rs +++ b/crates/forge_repo/src/agent_definition.rs @@ -1,7 +1,7 @@ use derive_setters::Setters; use forge_domain::{ - Agent, AgentId, Compact, EventContext, MaxTokens, ModelId, ProviderId, - ReasoningConfig, SystemContext, Temperature, Template, ToolName, TopK, TopP, + Agent, AgentId, Compact, EventContext, MaxTokens, ModelId, ProviderId, ReasoningConfig, + SystemContext, Temperature, Template, ToolName, TopK, TopP, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; From f3b5a7ebf7c7f3dee7a511b37c5caa6640a520a0 Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 9 Apr 2026 20:39:48 +0530 Subject: [PATCH 6/6] fix(tool-registry): assign ids to test agents and fix field ordering in agent conversion --- FORGE_TMP/.forge_history | 0 crates/forge_app/src/tool_registry.rs | 2 ++ crates/forge_repo/src/agent_definition.rs | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 FORGE_TMP/.forge_history diff --git a/FORGE_TMP/.forge_history b/FORGE_TMP/.forge_history deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/forge_app/src/tool_registry.rs b/crates/forge_app/src/tool_registry.rs index 25b6c5e1eb..0db7741760 100644 --- a/crates/forge_app/src/tool_registry.rs +++ b/crates/forge_app/src/tool_registry.rs @@ -722,6 +722,7 @@ fn create_test_agents() -> Vec { ProviderId::ANTHROPIC, ModelId::new("claude-3-5-sonnet-20241022"), ) + .id(AgentId::new("sage")) .title("Research Agent") .description("Specialized in researching codebases") .tools(vec![ @@ -735,6 +736,7 @@ fn create_test_agents() -> Vec { ProviderId::ANTHROPIC, ModelId::new("claude-3-5-sonnet-20241022"), ) + .id(AgentId::new("debug")) .title("Debug Agent") .description("Specialized in debugging issues") .tools(vec![ diff --git a/crates/forge_repo/src/agent_definition.rs b/crates/forge_repo/src/agent_definition.rs index 2819cbdae5..eccdcc377a 100644 --- a/crates/forge_repo/src/agent_definition.rs +++ b/crates/forge_repo/src/agent_definition.rs @@ -142,10 +142,10 @@ impl AgentDefinition { /// * `model_id` - Default model to use when the definition has none pub fn into_agent(self, provider_id: ProviderId, model_id: ModelId) -> Agent { Agent { + tool_supported: self.tool_supported, id: self.id, title: self.title, description: self.description, - tool_supported: self.tool_supported, provider: self.provider.unwrap_or(provider_id), model: self.model.unwrap_or(model_id), system_prompt: self.system_prompt,