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
5 changes: 5 additions & 0 deletions crates/forge_api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<Agent>>;

/// Provides lightweight metadata for all agents without requiring a
/// configured provider or model
async fn get_agent_infos(&self) -> Result<Vec<AgentInfo>>;

/// Provides a list of providers available in the current environment
async fn get_providers(&self) -> Result<Vec<AnyProvider>>;

Expand Down
8 changes: 6 additions & 2 deletions crates/forge_api/src/forge_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ impl ForgeAPI<ForgeServices<ForgeRepo<ForgeInfra>>, ForgeRepo<ForgeInfra>> {
/// * `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)
Expand Down Expand Up @@ -92,6 +92,10 @@ impl<
self.services.get_agents().await
}

async fn get_agent_infos(&self) -> Result<Vec<AgentInfo>> {
self.services.get_agent_infos().await
}

async fn get_providers(&self) -> Result<Vec<AnyProvider>> {
Ok(self.services.get_all_providers().await?)
}
Expand Down
12 changes: 6 additions & 6 deletions crates/forge_app/src/infra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,11 +397,11 @@ 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<Vec<forge_domain::Agent>>;
async fn get_agents(&self) -> anyhow::Result<Vec<forge_domain::Agent>>;

/// Load lightweight metadata for all agents without requiring a configured
/// provider or model.
async fn get_agent_infos(&self) -> anyhow::Result<Vec<forge_domain::AgentInfo>>;
}

/// Infrastructure trait for providing shared gRPC channel
Expand All @@ -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<tonic::transport::Channel>;

/// Hydrates the gRPC channel by establishing and then dropping the
/// connection
Expand Down
8 changes: 8 additions & 0 deletions crates/forge_app/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,10 @@ pub trait AgentRegistry: Send + Sync {
/// Get all agents from the registry store
async fn get_agents(&self) -> anyhow::Result<Vec<forge_domain::Agent>>;

/// Get lightweight metadata for all agents without requiring a configured
/// provider or model
async fn get_agent_infos(&self) -> anyhow::Result<Vec<forge_domain::AgentInfo>>;

/// Get agent by ID (from registry store)
async fn get_agent(&self, agent_id: &AgentId) -> anyhow::Result<Option<forge_domain::Agent>>;

Expand Down Expand Up @@ -918,6 +922,10 @@ impl<I: Services> AgentRegistry for I {
self.agent_registry().get_agents().await
}

async fn get_agent_infos(&self) -> anyhow::Result<Vec<forge_domain::AgentInfo>> {
self.agent_registry().get_agent_infos().await
}

async fn get_agent(&self, agent_id: &AgentId) -> anyhow::Result<Option<forge_domain::Agent>> {
self.agent_registry().get_agent(agent_id).await
}
Expand Down
45 changes: 30 additions & 15 deletions crates/forge_domain/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,31 +106,31 @@ pub fn estimate_token_count(count: usize) -> usize {
#[derive(Debug, Clone, PartialEq, Setters, Serialize, Deserialize, JsonSchema)]
#[setters(strip_option, into)]
pub struct Agent {
/// Unique identifier for the agent
pub id: AgentId,

/// Human-readable title for the agent
pub title: Option<String>,

/// Human-readable description of the agent's purpose
pub description: Option<String>,

/// Flag to enable/disable tool support for this agent.
pub tool_supported: Option<bool>,

// Unique identifier for the agent
pub id: AgentId,

/// Path to the agent definition file, if loaded from a file
pub path: Option<String>,

/// Human-readable title for the agent
pub title: Option<String>,

// 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<String>,

// Template for the system prompt provided to the agent
/// Template for the system prompt provided to the agent
pub system_prompt: Option<Template<SystemContext>>,

// Template for the user prompt provided to the agent
/// Template for the user prompt provided to the agent
pub user_prompt: Option<Template<EventContext>>,

/// Tools that the agent can use
Expand Down Expand Up @@ -168,16 +168,31 @@ pub struct Agent {
pub max_requests_per_turn: Option<usize>,
}

/// 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<String>,

/// Human-readable description of the agent's purpose
pub description: Option<String>,
}

impl Agent {
/// Create a new Agent with required provider and model
pub fn new(id: impl Into<AgentId>, provider: ProviderId, model: ModelId) -> Self {
Self {
id: id.into(),
title: Default::default(),
description: 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(),
Expand Down
11 changes: 6 additions & 5 deletions crates/forge_infra/src/forge_infra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,14 @@ 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 config = config_infra.cached_config().unwrap_or(config);

let http_service = Arc::new(ForgeHttpInfra::new(
config_infra.cached_config().unwrap_or(config),
config.clone(),
file_write_service.clone(),
));
let file_read_service = Arc::new(ForgeFileReadService::new());
Expand All @@ -81,7 +82,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 {
Expand Down Expand Up @@ -359,7 +360,7 @@ impl StrategyFactory for ForgeInfra {
}

impl GrpcInfra for ForgeInfra {
fn channel(&self) -> tonic::transport::Channel {
fn channel(&self) -> anyhow::Result<tonic::transport::Channel> {
self.grpc_client.channel()
}

Expand Down
17 changes: 7 additions & 10 deletions crates/forge_infra/src/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use url::Url;
/// on first access.
#[derive(Clone)]
pub struct ForgeGrpcClient {
server_url: Arc<Url>,
server_url: String,
channel: Arc<Mutex<Option<Channel>>>,
}

Expand All @@ -19,30 +19,27 @@ 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<Channel> {
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())
.expect("Invalid server URL")
.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)
Expand All @@ -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
Expand Down
10 changes: 1 addition & 9 deletions crates/forge_main/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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)) => {
Expand All @@ -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;

Expand Down
40 changes: 14 additions & 26 deletions crates/forge_main/src/model.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<Agent>) -> AgentCommandRegistrationResult {
pub fn register_agent_commands(
&self,
agents: Vec<AgentInfo>,
) -> AgentCommandRegistrationResult {
let mut guard = self.commands.lock().unwrap();
let mut result =
AgentCommandRegistrationResult { registered_count: 0, skipped_conflicts: Vec::new() };
Expand Down Expand Up @@ -840,24 +843,15 @@ 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
Expand Down Expand Up @@ -885,18 +879,12 @@ 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);

Expand Down
Loading
Loading