Skip to content
Open
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
572 changes: 558 additions & 14 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions crates/forge_api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ pub trait API: Sync + Send {
/// credentials file doesn't exist.
async fn migrate_env_credentials(&self) -> Result<Option<forge_domain::MigrationResult>>;

/// Clears the application caches
async fn clear_cache(&self) -> Result<()>;

async fn generate_data(
&self,
data_parameters: DataGenerationParameters,
Expand Down
12 changes: 8 additions & 4 deletions crates/forge_api/src/forge_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;

use anyhow::Result;
use anyhow::{Result, Context};
use forge_app::dto::ToolsOverview;
use forge_app::{
AgentProviderResolver, AgentRegistry, AppConfigService, AuthService, CommandInfra,
Expand Down Expand Up @@ -236,11 +236,11 @@ impl<
let needs_agent_reload = ops
.iter()
.any(|op| matches!(op, forge_domain::ConfigOperation::SetSessionConfig(_)));
let result = self.services.update_config(ops).await;
self.services.update_config(ops).await?;
if needs_agent_reload {
let _ = self.services.reload_agents().await;
self.services.reload_agents().await.context("Failed to reload agent configurations")?;
}
result
Ok(())
}

async fn get_commit_config(&self) -> anyhow::Result<Option<ModelConfig>> {
Expand Down Expand Up @@ -391,6 +391,10 @@ impl<
Ok(self.services.migrate_env_credentials().await?)
}

async fn clear_cache(&self) -> Result<()> {
self.services.clear_cache().await
}

async fn generate_data(
&self,
data_parameters: DataGenerationParameters,
Expand Down
8 changes: 8 additions & 0 deletions crates/forge_api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#![allow(clippy::unwrap_used, reason = "Legacy code needs migration")]
#![allow(clippy::indexing_slicing, reason = "Legacy code needs migration")]
#![allow(clippy::panic, reason = "Legacy code needs migration")]
#![allow(clippy::arithmetic_side_effects, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_truncation, reason = "Legacy code needs migration")]
#![allow(clippy::cast_sign_loss, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_wrap, reason = "Legacy code needs migration")]

mod api;
mod forge_api;

Expand Down
7 changes: 6 additions & 1 deletion crates/forge_app/src/agent_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ impl<S: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>> AgentEx
if let Some(tool_agents) = self.tool_agents.read().await.clone() {
return Ok(tool_agents);
}
let mut lock = self.tool_agents.write().await;
// Double-check the cache condition after acquiring the write lock
if let Some(tool_agents) = lock.clone() {
return Ok(tool_agents);
}
let agents = self.services.get_agents().await?;
let tools: Vec<ToolDefinition> = agents.into_iter().map(Into::into).collect();
*self.tool_agents.write().await = Some(tools.clone());
*lock = Some(tools.clone());
Ok(tools)
}

Expand Down
16 changes: 12 additions & 4 deletions crates/forge_app/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::sync::Arc;

use anyhow::Result;
use anyhow::{Context, Result};
use chrono::Local;
use forge_config::ForgeConfig;
use forge_domain::*;
Expand Down Expand Up @@ -177,9 +177,17 @@ impl<S: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>> ForgeAp
let conversation = orch.get_conversation().clone();
let save_result = services.upsert_conversation(conversation).await;

// Send any error to the stream (prioritize dispatch error over save error)
#[allow(clippy::collapsible_if)]
if let Some(err) = dispatch_result.err().or(save_result.err()) {
// Send any error to the stream
let final_err = match (dispatch_result, save_result) {
(Err(d_err), Err(s_err)) => {
Some(d_err.context(format!("Save also failed: {}", s_err)))
}
(Err(d_err), Ok(_)) => Some(d_err),
(Ok(_), Err(s_err)) => Some(s_err.context("Failed to save conversation")),
(Ok(_), Ok(_)) => None,
};

if let Some(err) = final_err {
if let Err(e) = tx.send(Err(err)).await {
tracing::error!("Failed to send error to stream: {}", e);
}
Expand Down
5 changes: 5 additions & 0 deletions crates/forge_app/src/dto/anthropic/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ impl TryFrom<ContextMessage> for Message {
}
}

if content.is_empty() {
// Provide a fallback space to satisfy API constraints
content.push(Content::Text { text: " ".to_string(), cache_control: None });
}

match chat_message.role {
forge_domain::Role::User => Message { role: Role::User, content },
forge_domain::Role::Assistant => Message { role: Role::Assistant, content },
Expand Down
1 change: 1 addition & 0 deletions crates/forge_app/src/dto/anthropic/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ impl From<Model> for forge_domain::Model {
|| value.id.contains("claude-sonnet")
|| value.id.contains("claude-opus")
|| value.id.contains("claude-haiku")
|| value.id.contains("gemini")
{
vec![
forge_domain::InputModality::Text,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::sync::LazyLock;
use forge_domain::Transformer;
use regex::Regex;

static TOOL_ID_SANITIZER: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^a-zA-Z0-9_-]").unwrap());

use crate::dto::anthropic::{Content, Request};

/// Transformer that sanitizes tool call IDs for Anthropic/Vertex Anthropic
Expand Down Expand Up @@ -30,16 +33,14 @@ impl Transformer for SanitizeToolIds {
type Value = Request;

fn transform(&mut self, mut request: Self::Value) -> Self::Value {
let regex = Regex::new(r"[^a-zA-Z0-9_-]").unwrap();

for message in &mut request.messages {
for content in &mut message.content {
match content {
Content::ToolUse { id, .. } => {
*id = regex.replace_all(id, "_").to_string();
*id = TOOL_ID_SANITIZER.replace_all(id, "_").to_string();
}
Content::ToolResult { tool_use_id, .. } => {
*tool_use_id = regex.replace_all(tool_use_id, "_").to_string();
*tool_use_id = TOOL_ID_SANITIZER.replace_all(tool_use_id, "_").to_string();
}
_ => {}
}
Expand Down
12 changes: 12 additions & 0 deletions crates/forge_app/src/dto/anthropic/transforms/set_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ impl Transformer for SetCache {

// Cache the very first system message, ideally you should keep static content
// in it.
let mut first_msg_cached = false;
if let Some(system_messages) = request.system.as_mut()
&& let Some(first_message) = system_messages.first_mut()
{
Expand All @@ -35,6 +36,17 @@ impl Transformer for SetCache {
// conversation.
if let Some(first_message) = request.get_messages_mut().first_mut() {
*first_message = std::mem::take(first_message).cached(true);
first_msg_cached = true;
}
}

let msg_len = request.get_messages().len();
if msg_len > 0 {
let start_idx = if first_msg_cached { 1 } else { 0 };
for (i, msg) in request.get_messages_mut().iter_mut().enumerate() {
if i >= start_idx && i < msg_len - 1 {
*msg = std::mem::take(msg).cached(false);
}
}
}

Expand Down
9 changes: 9 additions & 0 deletions crates/forge_app/src/dto/google/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,15 @@ impl From<forge_domain::TextMessage> for Content {
parts.extend(tool_calls.into_iter().map(Part::from));
}

if parts.is_empty() {
parts.push(Part::Text {
text: " ".to_string(),
thought: None,
thought_signature: None,
cache_control: None,
});
}

Content { role, parts }
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/forge_app/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#![allow(clippy::unwrap_used, reason = "Legacy code needs migration")]
#![allow(clippy::indexing_slicing, reason = "Legacy code needs migration")]
#![allow(clippy::panic, reason = "Legacy code needs migration")]
#![allow(clippy::arithmetic_side_effects, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_truncation, reason = "Legacy code needs migration")]
#![allow(clippy::cast_sign_loss, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_wrap, reason = "Legacy code needs migration")]

mod agent;
mod agent_executor;
mod agent_provider_resolver;
Expand Down
3 changes: 3 additions & 0 deletions crates/forge_app/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,9 @@ pub trait Services: Send + Sync + 'static + Clone + EnvironmentInfra {
fn provider_auth_service(&self) -> &Self::ProviderAuthService;
fn workspace_service(&self) -> &Self::WorkspaceService;
fn skill_fetch_service(&self) -> &Self::SkillFetchService;

/// Clears the application caches
fn clear_cache(&self) -> impl std::future::Future<Output = anyhow::Result<()>> + Send;
}

#[async_trait::async_trait]
Expand Down
8 changes: 8 additions & 0 deletions crates/forge_ci/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#![allow(clippy::unwrap_used, reason = "Legacy code needs migration")]
#![allow(clippy::indexing_slicing, reason = "Legacy code needs migration")]
#![allow(clippy::panic, reason = "Legacy code needs migration")]
#![allow(clippy::arithmetic_side_effects, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_truncation, reason = "Legacy code needs migration")]
#![allow(clippy::cast_sign_loss, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_wrap, reason = "Legacy code needs migration")]

mod jobs;
mod release_matrix;
pub mod steps;
Expand Down
6 changes: 3 additions & 3 deletions crates/forge_config/src/compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ impl Compact {
turn_threshold: None,
message_threshold: None,
model: None,
eviction_window: Percentage::new(0.2).unwrap(),
eviction_window: Percentage::new(0.2).expect("0.2 is a valid percentage between 0 and 1"),
retention_window: 0,
on_turn_end: None,
}
Expand Down Expand Up @@ -133,7 +133,7 @@ mod tests {
#[test]
fn test_f64_eviction_window_round_trip() {
let fixture = Compact {
eviction_window: Percentage::new(0.2).unwrap(),
eviction_window: Percentage::new(0.2).expect("0.2 is a valid percentage between 0 and 1"),
..Compact::new()
};

Expand All @@ -148,7 +148,7 @@ mod tests {
#[test]
fn test_f64_eviction_window_deserialize_round_trip() {
let fixture = Compact {
eviction_window: Percentage::new(0.2).unwrap(),
eviction_window: Percentage::new(0.2).expect("0.2 is a valid percentage between 0 and 1"),
..Compact::new()
};

Expand Down
2 changes: 1 addition & 1 deletion crates/forge_config/src/decimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ impl fake::Dummy<fake::Faker> for Decimal {

impl serde::Serialize for Decimal {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let formatted: f64 = format!("{:.2}", self.0).parse().unwrap();
let formatted: f64 = format!("{:.2}", self.0).parse().expect("Formatted float is valid f64");
serializer.serialize_f64(formatted)
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/forge_config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#![allow(clippy::unwrap_used, reason = "Legacy code needs migration")]
#![allow(clippy::indexing_slicing, reason = "Legacy code needs migration")]
#![allow(clippy::panic, reason = "Legacy code needs migration")]
#![allow(clippy::arithmetic_side_effects, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_truncation, reason = "Legacy code needs migration")]
#![allow(clippy::cast_sign_loss, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_wrap, reason = "Legacy code needs migration")]

mod auto_dump;
mod compact;
mod config;
Expand Down
16 changes: 10 additions & 6 deletions crates/forge_display/src/grep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,23 @@ impl<'a> ParsedLine<'a> {
return None;
}

let path = parts.first()?.trim();
let line_num = parts.get(1)?.trim();
let content = parts.get(2)?.trim();

// Validate that path and line number parts are not empty
// and that line number contains only digits
if parts[0].is_empty()
|| parts[1].is_empty()
|| !parts[1].chars().all(|c| c.is_ascii_digit())
if path.is_empty()
|| line_num.is_empty()
|| !line_num.chars().all(|c| c.is_ascii_digit())
{
return None;
}

Some(Self {
path: parts[0].trim(),
line_num: parts[1].trim(),
content: parts[2].trim(),
path,
line_num,
content,
})
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/forge_display/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#![allow(clippy::unwrap_used, reason = "Legacy code needs migration")]
#![allow(clippy::indexing_slicing, reason = "Legacy code needs migration")]
#![allow(clippy::panic, reason = "Legacy code needs migration")]
#![allow(clippy::arithmetic_side_effects, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_truncation, reason = "Legacy code needs migration")]
#![allow(clippy::cast_sign_loss, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_wrap, reason = "Legacy code needs migration")]

mod code;
pub mod diff;
pub mod grep;
Expand Down
4 changes: 2 additions & 2 deletions crates/forge_display/src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ impl MarkdownFormat {
if content.is_empty() {
return String::new();
}
Regex::new(&format!(r"\n{{{},}}", self.max_consecutive_newlines + 1))
.unwrap()
Regex::new(&format!(r"\n{{{},}}", self.max_consecutive_newlines.saturating_add(1)))
.expect("Valid regex pattern constructed with integers")
.replace_all(content, "\n".repeat(self.max_consecutive_newlines))
.into()
}
Expand Down
8 changes: 8 additions & 0 deletions crates/forge_domain/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#![allow(clippy::unwrap_used, reason = "Legacy code needs migration")]
#![allow(clippy::indexing_slicing, reason = "Legacy code needs migration")]
#![allow(clippy::panic, reason = "Legacy code needs migration")]
#![allow(clippy::arithmetic_side_effects, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_truncation, reason = "Legacy code needs migration")]
#![allow(clippy::cast_sign_loss, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_wrap, reason = "Legacy code needs migration")]

mod agent;
mod attachment;
mod auth;
Expand Down
8 changes: 8 additions & 0 deletions crates/forge_embed/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#![allow(clippy::unwrap_used, reason = "Legacy code needs migration")]
#![allow(clippy::indexing_slicing, reason = "Legacy code needs migration")]
#![allow(clippy::panic, reason = "Legacy code needs migration")]
#![allow(clippy::arithmetic_side_effects, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_truncation, reason = "Legacy code needs migration")]
#![allow(clippy::cast_sign_loss, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_wrap, reason = "Legacy code needs migration")]

use handlebars::Handlebars;
use include_dir::{Dir, DirEntry, File};

Expand Down
8 changes: 8 additions & 0 deletions crates/forge_fs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#![allow(clippy::unwrap_used, reason = "Legacy code needs migration")]
#![allow(clippy::indexing_slicing, reason = "Legacy code needs migration")]
#![allow(clippy::panic, reason = "Legacy code needs migration")]
#![allow(clippy::arithmetic_side_effects, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_truncation, reason = "Legacy code needs migration")]
#![allow(clippy::cast_sign_loss, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_wrap, reason = "Legacy code needs migration")]

//! # ForgeFS
//!
//! A file system abstraction layer that standardizes error handling for file
Expand Down
8 changes: 8 additions & 0 deletions crates/forge_infra/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#![allow(clippy::unwrap_used, reason = "Legacy code needs migration")]
#![allow(clippy::indexing_slicing, reason = "Legacy code needs migration")]
#![allow(clippy::panic, reason = "Legacy code needs migration")]
#![allow(clippy::arithmetic_side_effects, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_truncation, reason = "Legacy code needs migration")]
#![allow(clippy::cast_sign_loss, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_wrap, reason = "Legacy code needs migration")]

mod console;
mod env;
pub mod executor;
Expand Down
8 changes: 8 additions & 0 deletions crates/forge_json_repair/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#![allow(clippy::unwrap_used, reason = "Legacy code needs migration")]
#![allow(clippy::indexing_slicing, reason = "Legacy code needs migration")]
#![allow(clippy::panic, reason = "Legacy code needs migration")]
#![allow(clippy::arithmetic_side_effects, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_truncation, reason = "Legacy code needs migration")]
#![allow(clippy::cast_sign_loss, reason = "Legacy code needs migration")]
#![allow(clippy::cast_possible_wrap, reason = "Legacy code needs migration")]

mod error;
mod parser;
mod schema_coercion;
Expand Down
2 changes: 2 additions & 0 deletions crates/forge_json_repair/src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(clippy::arithmetic_side_effects, reason = "parsing bounds are already checked and indices won't overflow")]

use serde::Deserialize;

use crate::error::{JsonRepairError, Result};
Expand Down
Loading
Loading