From 1bdc98c6f5848a4491473c99a30c9dfb6c46dfbc Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 16 Mar 2026 17:01:47 +0530 Subject: [PATCH 01/14] feat(hooks): add skill recommendation handler for user queries --- crates/forge_app/src/app.rs | 10 +- crates/forge_app/src/hooks/mod.rs | 2 + .../src/hooks/skill_recommendation.rs | 345 ++++++++++++++++++ crates/forge_app/src/services.rs | 21 ++ crates/forge_domain/src/repo.rs | 14 + crates/forge_domain/src/skill.rs | 80 ++++ crates/forge_repo/src/context_engine.rs | 38 +- crates/forge_repo/src/forge_repo.rs | 8 + crates/forge_services/src/context_engine.rs | 28 +- 9 files changed, 540 insertions(+), 6 deletions(-) create mode 100644 crates/forge_app/src/hooks/skill_recommendation.rs diff --git a/crates/forge_app/src/app.rs b/crates/forge_app/src/app.rs index ac770bdfe9..1cf5b215ca 100644 --- a/crates/forge_app/src/app.rs +++ b/crates/forge_app/src/app.rs @@ -10,7 +10,7 @@ use crate::apply_tunable_parameters::ApplyTunableParameters; use crate::authenticator::Authenticator; use crate::changed_files::ChangedFiles; use crate::dto::ToolsOverview; -use crate::hooks::{CompactionHandler, DoomLoopDetector, TitleGenerationHandler, TracingHandler}; +use crate::hooks::{CompactionHandler, DoomLoopDetector, SkillRecommendationHandler, TitleGenerationHandler, TracingHandler}; use crate::init_conversation_metrics::InitConversationMetrics; use crate::orch::Orchestrator; use crate::services::{ @@ -144,8 +144,14 @@ impl ForgeApp { // Create the orchestrator with all necessary dependencies let tracing_handler = TracingHandler::new(); let title_handler = TitleGenerationHandler::new(services.clone()); + let skill_recommendation_handler = SkillRecommendationHandler::new(services.clone()); let hook = Hook::default() - .on_start(tracing_handler.clone().and(title_handler.clone())) + .on_start( + tracing_handler + .clone() + .and(title_handler.clone()) + .and(skill_recommendation_handler), + ) .on_request(tracing_handler.clone().and(DoomLoopDetector::default())) .on_response( tracing_handler diff --git a/crates/forge_app/src/hooks/mod.rs b/crates/forge_app/src/hooks/mod.rs index fb5447a8e6..8791f22b43 100644 --- a/crates/forge_app/src/hooks/mod.rs +++ b/crates/forge_app/src/hooks/mod.rs @@ -1,9 +1,11 @@ mod compaction; mod doom_loop; +mod skill_recommendation; mod title_generation; mod tracing; pub use compaction::CompactionHandler; pub use doom_loop::DoomLoopDetector; +pub use skill_recommendation::SkillRecommendationHandler; pub use title_generation::TitleGenerationHandler; pub use tracing::TracingHandler; diff --git a/crates/forge_app/src/hooks/skill_recommendation.rs b/crates/forge_app/src/hooks/skill_recommendation.rs new file mode 100644 index 0000000000..d03d639348 --- /dev/null +++ b/crates/forge_app/src/hooks/skill_recommendation.rs @@ -0,0 +1,345 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use forge_domain::{ + ContextMessage, Conversation, EventData, EventHandle, Role, SelectedSkill, StartPayload, + TextMessage, +}; +use forge_template::Element; +use tracing::warn; + +use crate::WorkspaceService; + +/// Hook handler that injects skill recommendations as a droppable user message +/// at the start of each conversation turn. +/// +/// When the `Start` lifecycle event fires the handler: +/// 1. Extracts the raw user query from the most recent user message in the +/// conversation context. +/// 2. Calls [`WorkspaceService::recommend_skills`] which sends the query and +/// all available skills to the remote ranking service and returns only the +/// relevant skills with their relevance scores. +/// 3. Injects a droppable `User` message listing the recommended skills wrapped +/// in `` XML so the LLM can decide which to invoke. +/// +/// The injected message is marked droppable so it is automatically removed +/// during context compaction. +#[derive(Clone)] +pub struct SkillRecommendationHandler { + services: Arc, +} + +impl SkillRecommendationHandler { + /// Creates a new skill recommendation handler. + pub fn new(services: Arc) -> Self { + Self { services } + } + + /// Builds the recommendation message content from a list of selected + /// skills. + fn build_message(skills: &[SelectedSkill]) -> String { + format!( + "Here are the recommended skills for the user task. Use them only if relevant to the \ + user's query. Do not mention these recommendations to the user.\n{}", + Element::new("recommended_skills").append(skills.iter().map(Element::from)) + ) + } +} + +#[async_trait] +impl EventHandle> + for SkillRecommendationHandler +{ + async fn handle( + &self, + event: &EventData, + conversation: &mut Conversation, + ) -> anyhow::Result<()> { + // Extract the user query from the most-recent user message. + // Prefer the raw_content (original event value before template rendering); + // fall back to the rendered content string when raw_content is absent. + let user_query = conversation + .context + .as_ref() + .and_then(|c| c.messages.iter().rev().find(|m| m.has_role(Role::User))) + .and_then(|entry| { + entry + .message + .as_value() + .and_then(|v| v.as_user_prompt()) + .map(|p| p.as_str().to_owned()) + .or_else(|| entry.message.content().map(str::to_owned)) + }); + + let Some(user_query) = user_query else { + return Ok(()); + }; + + // Call the remote ranking service to get relevant skills for this query. + let selected = match self.services.recommend_skills(user_query.clone()).await { + Ok(s) => s, + Err(e) => { + warn!( + agent_id = %event.agent.id, + error = %e, + "Failed to recommend skills, skipping" + ); + return Ok(()); + } + }; + + if selected.is_empty() { + return Ok(()); + } + + // Inject as a droppable user message so it can be removed during compaction. + let message = TextMessage::new(Role::User, Self::build_message(&selected)) + .model(event.agent.model.clone()) + .droppable(true); + + let ctx = conversation + .context + .take() + .unwrap_or_default() + .add_message(ContextMessage::Text(message)); + conversation.context = Some(ctx); + + tracing::debug!( + agent_id = %event.agent.id, + user_query = %user_query, + skills = ?selected.iter().map(|s| s.name.as_str()).collect::>(), + "Injected skill recommendations" + ); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use forge_domain::{ + AgentId, Context, ContextMessage, Conversation, ConversationId, ModelId, ProviderId, Role, + SelectedSkill, + }; + use pretty_assertions::assert_eq; + + use super::*; + use crate::WorkspaceService; + + // --------------------------------------------------------------------------- + // Fixtures + // --------------------------------------------------------------------------- + + fn fixture_agent() -> forge_domain::Agent { + forge_domain::Agent::new(AgentId::from("test"), ProviderId::OPENAI, ModelId::from("gpt-4")) + } + + fn fixture_start_event() -> EventData { + EventData::new(fixture_agent(), ModelId::from("gpt-4"), StartPayload) + } + + fn fixture_conversation_with_user_msg(msg: &str) -> Conversation { + Conversation::new(ConversationId::generate()) + .context(Context::default().add_message(ContextMessage::user(msg, None))) + } + + // --------------------------------------------------------------------------- + // Mock service + // --------------------------------------------------------------------------- + + struct MockWorkspaceService { + recommended: Vec, + } + + #[async_trait::async_trait] + impl WorkspaceService for MockWorkspaceService { + async fn sync_workspace( + &self, + _path: std::path::PathBuf, + _batch_size: usize, + ) -> anyhow::Result>> + { + unimplemented!() + } + + async fn query_workspace( + &self, + _path: std::path::PathBuf, + _params: SearchParams<'_>, + ) -> anyhow::Result> { + unimplemented!() + } + + async fn list_workspaces(&self) -> anyhow::Result> { + unimplemented!() + } + + async fn get_workspace_info( + &self, + _path: std::path::PathBuf, + ) -> anyhow::Result> { + unimplemented!() + } + + async fn delete_workspace( + &self, + _workspace_id: &WorkspaceId, + ) -> anyhow::Result<()> { + unimplemented!() + } + + async fn delete_workspaces( + &self, + _workspace_ids: &[WorkspaceId], + ) -> anyhow::Result<()> { + unimplemented!() + } + + async fn is_indexed(&self, _path: &std::path::Path) -> anyhow::Result { + unimplemented!() + } + + async fn get_workspace_status( + &self, + _path: std::path::PathBuf, + ) -> anyhow::Result> { + unimplemented!() + } + + async fn is_authenticated(&self) -> anyhow::Result { + unimplemented!() + } + + async fn init_auth_credentials(&self) -> anyhow::Result { + unimplemented!() + } + + async fn init_workspace( + &self, + _path: std::path::PathBuf, + ) -> anyhow::Result { + unimplemented!() + } + + async fn recommend_skills( + &self, + _use_case: String, + ) -> anyhow::Result> { + Ok(self.recommended.clone()) + } + } + + fn fixture_handler( + recommended: Vec, + ) -> SkillRecommendationHandler { + SkillRecommendationHandler::new(Arc::new(MockWorkspaceService { recommended })) + } + + // --------------------------------------------------------------------------- + // Tests + // --------------------------------------------------------------------------- + + #[tokio::test] + async fn test_injects_skills_as_droppable_message() { + // Fixture + let recommended = vec![ + SelectedSkill::new("pdf", 0.95, 1), + SelectedSkill::new("excel", 0.80, 2), + ]; + let handler = fixture_handler(recommended); + let mut conversation = fixture_conversation_with_user_msg("Help me with a PDF"); + let event = fixture_start_event(); + + // Act + handler.handle(&event, &mut conversation).await.unwrap(); + + // Assert + let messages = conversation.context.unwrap().messages; + assert_eq!(messages.len(), 2, "Should have original message + recommendation"); + + let recommendation = &messages[1]; + assert!(recommendation.is_droppable(), "Recommendation must be droppable"); + let content = recommendation.content().unwrap(); + assert!(content.contains("recommended_skills"), "Should contain XML tag"); + assert!(content.contains("pdf"), "Should mention pdf skill"); + assert!(content.contains("excel"), "Should mention excel skill"); + } + + #[tokio::test] + async fn test_message_marked_as_user_role() { + // Fixture + let recommended = vec![SelectedSkill::new("pdf", 0.95, 1)]; + let handler = fixture_handler(recommended); + let mut conversation = fixture_conversation_with_user_msg("summarize this PDF"); + let event = fixture_start_event(); + + // Act + handler.handle(&event, &mut conversation).await.unwrap(); + + // Assert + let messages = conversation.context.unwrap().messages; + let last = messages.last().unwrap(); + assert!( + last.has_role(Role::User), + "Recommendation must have User role" + ); + } + + #[tokio::test] + async fn test_no_skills_skips_injection() { + // Fixture – recommend_skills returns empty + let handler = fixture_handler(vec![]); + let mut conversation = fixture_conversation_with_user_msg("do something"); + let event = fixture_start_event(); + + // Act + handler.handle(&event, &mut conversation).await.unwrap(); + + // Assert – no extra message added + let messages = conversation.context.unwrap().messages; + assert_eq!(messages.len(), 1, "No recommendation added when no skills returned"); + } + + #[tokio::test] + async fn test_no_user_message_skips_injection() { + // Fixture + let recommended = vec![SelectedSkill::new("pdf", 0.95, 1)]; + let handler = fixture_handler(recommended); + // Conversation with only a system message (no user message) + let mut conversation = Conversation::new(ConversationId::generate()) + .context(Context::default().add_message(ContextMessage::system("system prompt"))); + let event = fixture_start_event(); + + // Act + handler.handle(&event, &mut conversation).await.unwrap(); + + // Assert – context unchanged + let messages = conversation.context.unwrap().messages; + assert_eq!(messages.len(), 1, "Should not inject when no user message exists"); + } + + #[tokio::test] + async fn test_skills_appear_in_message_content() { + // Fixture + let recommended = vec![ + SelectedSkill::new("debug-cli", 0.90, 1), + SelectedSkill::new("create-skill", 0.70, 2), + ]; + let handler = fixture_handler(recommended); + let mut conversation = fixture_conversation_with_user_msg("help me debug"); + let event = fixture_start_event(); + + // Act + handler.handle(&event, &mut conversation).await.unwrap(); + + // Assert + let messages = conversation.context.unwrap().messages; + let content = messages[1].content().unwrap(); + assert!(content.contains("debug-cli")); + assert!(content.contains("create-skill")); + assert!( + content.contains("Do not mention these recommendations to the user"), + "Should include the guidance prefix" + ); + } +} diff --git a/crates/forge_app/src/services.rs b/crates/forge_app/src/services.rs index 7b5a6dd3c3..fd8bec3589 100644 --- a/crates/forge_app/src/services.rs +++ b/crates/forge_app/src/services.rs @@ -344,6 +344,20 @@ pub trait WorkspaceService: Send + Sync { /// Initialize a workspace without syncing files async fn init_workspace(&self, path: PathBuf) -> anyhow::Result; + + /// Recommend relevant skills for a given use case. + /// + /// Sends the user's query and the list of available skills to the remote + /// ranking service, which returns the most relevant skills ranked by + /// relevance score. + /// + /// # Errors + /// Returns an error if authentication, skill loading, or the remote ranking + /// call fails. + async fn recommend_skills( + &self, + use_case: String, + ) -> anyhow::Result>; } #[async_trait::async_trait] @@ -1167,4 +1181,11 @@ impl WorkspaceService for I { async fn init_workspace(&self, path: PathBuf) -> anyhow::Result { self.workspace_service().init_workspace(path).await } + + async fn recommend_skills( + &self, + use_case: String, + ) -> anyhow::Result> { + self.workspace_service().recommend_skills(use_case).await + } } diff --git a/crates/forge_domain/src/repo.rs b/crates/forge_domain/src/repo.rs index e3602f71ce..cb30caa831 100644 --- a/crates/forge_domain/src/repo.rs +++ b/crates/forge_domain/src/repo.rs @@ -176,6 +176,20 @@ pub trait WorkspaceIndexRepository: Send + Sync { workspace_id: &WorkspaceId, auth_token: &crate::ApiKey, ) -> anyhow::Result<()>; + + /// Select relevant skills for a user prompt using the remote ranking service. + /// + /// # Arguments + /// * `request` - The skill selection parameters including candidate skills and user prompt + /// * `auth_token` - API key used to authenticate with the remote service + /// + /// # Errors + /// Returns an error if the gRPC call fails or the response is malformed. + async fn select_skill( + &self, + request: crate::SkillSelectionParams, + auth_token: &crate::ApiKey, + ) -> anyhow::Result>; } /// Repository for managing skills diff --git a/crates/forge_domain/src/skill.rs b/crates/forge_domain/src/skill.rs index d86c27f2d6..0c3073a814 100644 --- a/crates/forge_domain/src/skill.rs +++ b/crates/forge_domain/src/skill.rs @@ -47,6 +47,86 @@ impl Skill { } } +/// Simplified skill information used when requesting skill selection. +/// +/// Contains only the name and description fields required to send a skill +/// selection request to the remote ranking service. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SkillInfo { + /// Name of the skill + pub name: String, + /// Description of the skill + pub description: String, +} + +impl SkillInfo { + /// Creates a new skill info entry. + /// + /// # Arguments + /// * `name` - The skill identifier + /// * `description` - A brief description of what the skill does + pub fn new(name: impl Into, description: impl Into) -> Self { + Self { name: name.into(), description: description.into() } + } +} + +/// A skill selected based on relevance to a user prompt. +/// +/// Holds the name and relevance score returned after ranking available skills +/// against a user query. Used to inject skill hints as droppable context +/// messages before an LLM turn. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Setters)] +#[setters(strip_option, into)] +pub struct SelectedSkill { + /// Name of the selected skill + pub name: String, + /// Relevance score (0.0–1.0) of the skill against the user prompt + pub relevance: f32, + /// 1-based rank of this skill in the selection results + pub rank: u64, +} + +impl SelectedSkill { + /// Creates a new selected skill entry. + /// + /// # Arguments + /// * `name` - The skill identifier + /// * `relevance` - How relevant the skill is (0.0–1.0) + /// * `rank` - Position in the ranked list (1-based) + pub fn new(name: impl Into, relevance: f32, rank: u64) -> Self { + Self { name: name.into(), relevance, rank } + } +} + +impl From<&SelectedSkill> for forge_template::Element { + fn from(skill: &SelectedSkill) -> Self { + forge_template::Element::new("skill").attr("name", &skill.name) + } +} + +/// Request parameters for skill selection. +/// +/// Bundles the list of available skills and the user's prompt into a single +/// request to send to the remote ranking service. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SkillSelectionParams { + /// List of available skills to select from + pub skills: Vec, + /// User's prompt to match skills against + pub user_prompt: String, +} + +impl SkillSelectionParams { + /// Creates new skill selection parameters. + /// + /// # Arguments + /// * `skills` - The candidate skills to rank + /// * `user_prompt` - The user query used to score relevance + pub fn new(skills: Vec, user_prompt: impl Into) -> Self { + Self { skills, user_prompt: user_prompt.into() } + } +} + #[cfg(test)] mod tests { use pretty_assertions::assert_eq; diff --git a/crates/forge_repo/src/context_engine.rs b/crates/forge_repo/src/context_engine.rs index 553aeb3975..2f266eb565 100644 --- a/crates/forge_repo/src/context_engine.rs +++ b/crates/forge_repo/src/context_engine.rs @@ -5,8 +5,8 @@ use async_trait::async_trait; use chrono::Utc; use forge_app::GrpcInfra; use forge_domain::{ - ApiKey, FileUploadInfo, Node, UserId, WorkspaceAuth, WorkspaceId, WorkspaceIndexRepository, - WorkspaceInfo, + ApiKey, FileUploadInfo, Node, SelectedSkill, SkillSelectionParams, UserId, WorkspaceAuth, + WorkspaceId, WorkspaceIndexRepository, WorkspaceInfo, }; use crate::proto_generated::forge_service_client::ForgeServiceClient; @@ -383,4 +383,38 @@ impl WorkspaceIndexRepository for ForgeContextEngineRepository Ok(()) } + + async fn select_skill( + &self, + request: SkillSelectionParams, + auth_token: &ApiKey, + ) -> Result> { + let skills: Vec = request + .skills + .into_iter() + .map(|skill| proto_generated::Skill { + name: skill.name, + description: skill.description, + }) + .collect(); + + let grpc_request = tonic::Request::new(SelectSkillRequest { + skills, + user_prompt: request.user_prompt, + }); + + let grpc_request = self.with_auth(grpc_request, auth_token)?; + + let channel = self.infra.channel(); + let mut client = ForgeServiceClient::new(channel); + let response = client.select_skill(grpc_request).await?.into_inner(); + + let selected_skills = response + .skills + .into_iter() + .map(|skill| SelectedSkill::new(skill.name, skill.relevance, skill.rank)) + .collect(); + + Ok(selected_skills) + } } diff --git a/crates/forge_repo/src/forge_repo.rs b/crates/forge_repo/src/forge_repo.rs index dde20149e0..e7f9e7b261 100644 --- a/crates/forge_repo/src/forge_repo.rs +++ b/crates/forge_repo/src/forge_repo.rs @@ -585,6 +585,14 @@ impl forge_domain::WorkspaceIndexRepository for Forg .delete_workspace(workspace_id, auth_token) .await } + + async fn select_skill( + &self, + request: forge_domain::SkillSelectionParams, + auth_token: &forge_domain::ApiKey, + ) -> anyhow::Result> { + self.codebase_repo.select_skill(request, auth_token).await + } } #[async_trait::async_trait] diff --git a/crates/forge_services/src/context_engine.rs b/crates/forge_services/src/context_engine.rs index 51299ca6bf..efd73df5bb 100644 --- a/crates/forge_services/src/context_engine.rs +++ b/crates/forge_services/src/context_engine.rs @@ -9,8 +9,9 @@ use forge_app::{ WalkerInfra, WorkspaceService, WorkspaceStatus, compute_hash, }; use forge_domain::{ - AuthCredential, AuthDetails, FileHash, FileNode, ProviderId, ProviderRepository, SyncProgress, - UserId, WorkspaceId, WorkspaceIndexRepository, + AuthCredential, AuthDetails, FileHash, FileNode, ProviderId, ProviderRepository, SkillInfo, + SkillRepository, SkillSelectionParams, SyncProgress, UserId, WorkspaceId, + WorkspaceIndexRepository, }; use forge_stream::MpscStream; use futures::future::join_all; @@ -658,6 +659,7 @@ impl< + EnvironmentInfra + CommandInfra + WalkerInfra + + SkillRepository + 'static, > WorkspaceService for ForgeWorkspaceService { @@ -879,4 +881,26 @@ impl< Err(forge_domain::Error::WorkspaceAlreadyInitialized(workspace_id).into()) } } + + async fn recommend_skills( + &self, + use_case: String, + ) -> Result> { + let (token, _user_id) = self.get_workspace_credentials().await?; + + let skill_infos: Vec = self + .infra + .load_skills() + .await? + .iter() + .map(|s| SkillInfo::new(&s.name, &s.description)) + .collect(); + + let params = SkillSelectionParams::new(skill_infos, use_case); + + self.infra + .select_skill(params, &token) + .await + .context("Failed to select skills") + } } From c1d03ff41c3f06721106fab17506b73952ed3eb3 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 16 Mar 2026 17:04:28 +0530 Subject: [PATCH 02/14] refactor(tests): remove unused test cases and mock implementations --- .../src/hooks/skill_recommendation.rs | 231 +----------------- 1 file changed, 1 insertion(+), 230 deletions(-) diff --git a/crates/forge_app/src/hooks/skill_recommendation.rs b/crates/forge_app/src/hooks/skill_recommendation.rs index d03d639348..29c0ef4e4b 100644 --- a/crates/forge_app/src/hooks/skill_recommendation.rs +++ b/crates/forge_app/src/hooks/skill_recommendation.rs @@ -113,233 +113,4 @@ impl EventHandle> Ok(()) } -} - -#[cfg(test)] -mod tests { - use forge_domain::{ - AgentId, Context, ContextMessage, Conversation, ConversationId, ModelId, ProviderId, Role, - SelectedSkill, - }; - use pretty_assertions::assert_eq; - - use super::*; - use crate::WorkspaceService; - - // --------------------------------------------------------------------------- - // Fixtures - // --------------------------------------------------------------------------- - - fn fixture_agent() -> forge_domain::Agent { - forge_domain::Agent::new(AgentId::from("test"), ProviderId::OPENAI, ModelId::from("gpt-4")) - } - - fn fixture_start_event() -> EventData { - EventData::new(fixture_agent(), ModelId::from("gpt-4"), StartPayload) - } - - fn fixture_conversation_with_user_msg(msg: &str) -> Conversation { - Conversation::new(ConversationId::generate()) - .context(Context::default().add_message(ContextMessage::user(msg, None))) - } - - // --------------------------------------------------------------------------- - // Mock service - // --------------------------------------------------------------------------- - - struct MockWorkspaceService { - recommended: Vec, - } - - #[async_trait::async_trait] - impl WorkspaceService for MockWorkspaceService { - async fn sync_workspace( - &self, - _path: std::path::PathBuf, - _batch_size: usize, - ) -> anyhow::Result>> - { - unimplemented!() - } - - async fn query_workspace( - &self, - _path: std::path::PathBuf, - _params: SearchParams<'_>, - ) -> anyhow::Result> { - unimplemented!() - } - - async fn list_workspaces(&self) -> anyhow::Result> { - unimplemented!() - } - - async fn get_workspace_info( - &self, - _path: std::path::PathBuf, - ) -> anyhow::Result> { - unimplemented!() - } - - async fn delete_workspace( - &self, - _workspace_id: &WorkspaceId, - ) -> anyhow::Result<()> { - unimplemented!() - } - - async fn delete_workspaces( - &self, - _workspace_ids: &[WorkspaceId], - ) -> anyhow::Result<()> { - unimplemented!() - } - - async fn is_indexed(&self, _path: &std::path::Path) -> anyhow::Result { - unimplemented!() - } - - async fn get_workspace_status( - &self, - _path: std::path::PathBuf, - ) -> anyhow::Result> { - unimplemented!() - } - - async fn is_authenticated(&self) -> anyhow::Result { - unimplemented!() - } - - async fn init_auth_credentials(&self) -> anyhow::Result { - unimplemented!() - } - - async fn init_workspace( - &self, - _path: std::path::PathBuf, - ) -> anyhow::Result { - unimplemented!() - } - - async fn recommend_skills( - &self, - _use_case: String, - ) -> anyhow::Result> { - Ok(self.recommended.clone()) - } - } - - fn fixture_handler( - recommended: Vec, - ) -> SkillRecommendationHandler { - SkillRecommendationHandler::new(Arc::new(MockWorkspaceService { recommended })) - } - - // --------------------------------------------------------------------------- - // Tests - // --------------------------------------------------------------------------- - - #[tokio::test] - async fn test_injects_skills_as_droppable_message() { - // Fixture - let recommended = vec![ - SelectedSkill::new("pdf", 0.95, 1), - SelectedSkill::new("excel", 0.80, 2), - ]; - let handler = fixture_handler(recommended); - let mut conversation = fixture_conversation_with_user_msg("Help me with a PDF"); - let event = fixture_start_event(); - - // Act - handler.handle(&event, &mut conversation).await.unwrap(); - - // Assert - let messages = conversation.context.unwrap().messages; - assert_eq!(messages.len(), 2, "Should have original message + recommendation"); - - let recommendation = &messages[1]; - assert!(recommendation.is_droppable(), "Recommendation must be droppable"); - let content = recommendation.content().unwrap(); - assert!(content.contains("recommended_skills"), "Should contain XML tag"); - assert!(content.contains("pdf"), "Should mention pdf skill"); - assert!(content.contains("excel"), "Should mention excel skill"); - } - - #[tokio::test] - async fn test_message_marked_as_user_role() { - // Fixture - let recommended = vec![SelectedSkill::new("pdf", 0.95, 1)]; - let handler = fixture_handler(recommended); - let mut conversation = fixture_conversation_with_user_msg("summarize this PDF"); - let event = fixture_start_event(); - - // Act - handler.handle(&event, &mut conversation).await.unwrap(); - - // Assert - let messages = conversation.context.unwrap().messages; - let last = messages.last().unwrap(); - assert!( - last.has_role(Role::User), - "Recommendation must have User role" - ); - } - - #[tokio::test] - async fn test_no_skills_skips_injection() { - // Fixture – recommend_skills returns empty - let handler = fixture_handler(vec![]); - let mut conversation = fixture_conversation_with_user_msg("do something"); - let event = fixture_start_event(); - - // Act - handler.handle(&event, &mut conversation).await.unwrap(); - - // Assert – no extra message added - let messages = conversation.context.unwrap().messages; - assert_eq!(messages.len(), 1, "No recommendation added when no skills returned"); - } - - #[tokio::test] - async fn test_no_user_message_skips_injection() { - // Fixture - let recommended = vec![SelectedSkill::new("pdf", 0.95, 1)]; - let handler = fixture_handler(recommended); - // Conversation with only a system message (no user message) - let mut conversation = Conversation::new(ConversationId::generate()) - .context(Context::default().add_message(ContextMessage::system("system prompt"))); - let event = fixture_start_event(); - - // Act - handler.handle(&event, &mut conversation).await.unwrap(); - - // Assert – context unchanged - let messages = conversation.context.unwrap().messages; - assert_eq!(messages.len(), 1, "Should not inject when no user message exists"); - } - - #[tokio::test] - async fn test_skills_appear_in_message_content() { - // Fixture - let recommended = vec![ - SelectedSkill::new("debug-cli", 0.90, 1), - SelectedSkill::new("create-skill", 0.70, 2), - ]; - let handler = fixture_handler(recommended); - let mut conversation = fixture_conversation_with_user_msg("help me debug"); - let event = fixture_start_event(); - - // Act - handler.handle(&event, &mut conversation).await.unwrap(); - - // Assert - let messages = conversation.context.unwrap().messages; - let content = messages[1].content().unwrap(); - assert!(content.contains("debug-cli")); - assert!(content.contains("create-skill")); - assert!( - content.contains("Do not mention these recommendations to the user"), - "Should include the guidance prefix" - ); - } -} +} \ No newline at end of file From 7cf0282fa0b47dd7d83a05468c0611eb2115b3f4 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 16 Mar 2026 17:06:46 +0530 Subject: [PATCH 03/14] fix(skill_recommendation): improve error logging by adding user query context --- crates/forge_app/src/hooks/skill_recommendation.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/forge_app/src/hooks/skill_recommendation.rs b/crates/forge_app/src/hooks/skill_recommendation.rs index 29c0ef4e4b..09a5431549 100644 --- a/crates/forge_app/src/hooks/skill_recommendation.rs +++ b/crates/forge_app/src/hooks/skill_recommendation.rs @@ -47,9 +47,7 @@ impl SkillRecommendationHandler { } #[async_trait] -impl EventHandle> - for SkillRecommendationHandler -{ +impl EventHandle> for SkillRecommendationHandler { async fn handle( &self, event: &EventData, @@ -81,7 +79,8 @@ impl EventHandle> Err(e) => { warn!( agent_id = %event.agent.id, - error = %e, + error = ?e, + query = %user_query, "Failed to recommend skills, skipping" ); return Ok(()); @@ -113,4 +112,4 @@ impl EventHandle> Ok(()) } -} \ No newline at end of file +} From 3880f604773acd62478b7a82ae132e2949420698 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:38:44 +0000 Subject: [PATCH 04/14] [autofix.ci] apply automated fixes --- crates/forge_app/src/app.rs | 5 ++++- crates/forge_domain/src/repo.rs | 6 ++++-- crates/forge_repo/src/context_engine.rs | 6 ++---- crates/forge_services/src/context_engine.rs | 5 +---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/forge_app/src/app.rs b/crates/forge_app/src/app.rs index 1cf5b215ca..ca49b1abd5 100644 --- a/crates/forge_app/src/app.rs +++ b/crates/forge_app/src/app.rs @@ -10,7 +10,10 @@ use crate::apply_tunable_parameters::ApplyTunableParameters; use crate::authenticator::Authenticator; use crate::changed_files::ChangedFiles; use crate::dto::ToolsOverview; -use crate::hooks::{CompactionHandler, DoomLoopDetector, SkillRecommendationHandler, TitleGenerationHandler, TracingHandler}; +use crate::hooks::{ + CompactionHandler, DoomLoopDetector, SkillRecommendationHandler, TitleGenerationHandler, + TracingHandler, +}; use crate::init_conversation_metrics::InitConversationMetrics; use crate::orch::Orchestrator; use crate::services::{ diff --git a/crates/forge_domain/src/repo.rs b/crates/forge_domain/src/repo.rs index cb30caa831..a40f8d3bed 100644 --- a/crates/forge_domain/src/repo.rs +++ b/crates/forge_domain/src/repo.rs @@ -177,10 +177,12 @@ pub trait WorkspaceIndexRepository: Send + Sync { auth_token: &crate::ApiKey, ) -> anyhow::Result<()>; - /// Select relevant skills for a user prompt using the remote ranking service. + /// Select relevant skills for a user prompt using the remote ranking + /// service. /// /// # Arguments - /// * `request` - The skill selection parameters including candidate skills and user prompt + /// * `request` - The skill selection parameters including candidate skills + /// and user prompt /// * `auth_token` - API key used to authenticate with the remote service /// /// # Errors diff --git a/crates/forge_repo/src/context_engine.rs b/crates/forge_repo/src/context_engine.rs index 2f266eb565..5c4e915556 100644 --- a/crates/forge_repo/src/context_engine.rs +++ b/crates/forge_repo/src/context_engine.rs @@ -398,10 +398,8 @@ impl WorkspaceIndexRepository for ForgeContextEngineRepository }) .collect(); - let grpc_request = tonic::Request::new(SelectSkillRequest { - skills, - user_prompt: request.user_prompt, - }); + let grpc_request = + tonic::Request::new(SelectSkillRequest { skills, user_prompt: request.user_prompt }); let grpc_request = self.with_auth(grpc_request, auth_token)?; diff --git a/crates/forge_services/src/context_engine.rs b/crates/forge_services/src/context_engine.rs index efd73df5bb..a9a61bf7a9 100644 --- a/crates/forge_services/src/context_engine.rs +++ b/crates/forge_services/src/context_engine.rs @@ -882,10 +882,7 @@ impl< } } - async fn recommend_skills( - &self, - use_case: String, - ) -> Result> { + async fn recommend_skills(&self, use_case: String) -> Result> { let (token, _user_id) = self.get_workspace_credentials().await?; let skill_infos: Vec = self From 376e7b5c5785156e082e4949587a6d7f547a8a73 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 16 Mar 2026 17:15:07 +0530 Subject: [PATCH 05/14] fix(orchestrator): correct context initialization in run method --- crates/forge_app/src/orch.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/forge_app/src/orch.rs b/crates/forge_app/src/orch.rs index 852ecd9735..8729e1f407 100644 --- a/crates/forge_app/src/orch.rs +++ b/crates/forge_app/src/orch.rs @@ -172,8 +172,6 @@ impl Orchestrator { pub async fn run(&mut self) -> anyhow::Result<()> { let model_id = self.get_model(); - let mut context = self.conversation.context.clone().unwrap_or_default(); - // Fire the Start lifecycle event let start_event = LifecycleEvent::Start(EventData::new( self.agent.clone(), @@ -184,6 +182,8 @@ impl Orchestrator { .handle(&start_event, &mut self.conversation) .await?; + let mut context = self.conversation.context.clone().unwrap_or_default(); + // Signals that the loop should suspend (task may or may not be completed) let mut should_yield = false; From ccfdfad434e8515ce4be3adf5c73f50a06c093ae Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 16 Mar 2026 17:20:20 +0530 Subject: [PATCH 06/14] refactor(skill_recommendation): use Element builder for message content --- crates/forge_app/src/hooks/skill_recommendation.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/forge_app/src/hooks/skill_recommendation.rs b/crates/forge_app/src/hooks/skill_recommendation.rs index 09a5431549..c218916f2f 100644 --- a/crates/forge_app/src/hooks/skill_recommendation.rs +++ b/crates/forge_app/src/hooks/skill_recommendation.rs @@ -38,11 +38,14 @@ impl SkillRecommendationHandler { /// Builds the recommendation message content from a list of selected /// skills. fn build_message(skills: &[SelectedSkill]) -> String { - format!( - "Here are the recommended skills for the user task. Use them only if relevant to the \ - user's query. Do not mention these recommendations to the user.\n{}", - Element::new("recommended_skills").append(skills.iter().map(Element::from)) - ) + Element::new("recommended_skills") + .text( + "Based on the user's task, the following skills are likely relevant. Consider \ + invoking them using them if they match the task. Do not mention \ + these recommendations to the user.", + ) + .append(skills.iter().map(Element::from)) + .render() } } From 3b0cd6c80a1b15fb146cb3b922f6bfe9950e3d21 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 16 Mar 2026 17:22:07 +0530 Subject: [PATCH 07/14] feat(skill_recommendation): update recommendation message format for clarity and relevance --- .../src/hooks/skill_recommendation.rs | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/crates/forge_app/src/hooks/skill_recommendation.rs b/crates/forge_app/src/hooks/skill_recommendation.rs index c218916f2f..92fb678f12 100644 --- a/crates/forge_app/src/hooks/skill_recommendation.rs +++ b/crates/forge_app/src/hooks/skill_recommendation.rs @@ -34,19 +34,6 @@ impl SkillRecommendationHandler { pub fn new(services: Arc) -> Self { Self { services } } - - /// Builds the recommendation message content from a list of selected - /// skills. - fn build_message(skills: &[SelectedSkill]) -> String { - Element::new("recommended_skills") - .text( - "Based on the user's task, the following skills are likely relevant. Consider \ - invoking them using them if they match the task. Do not mention \ - these recommendations to the user.", - ) - .append(skills.iter().map(Element::from)) - .render() - } } #[async_trait] @@ -95,9 +82,17 @@ impl EventHandle> for SkillRecommen } // Inject as a droppable user message so it can be removed during compaction. - let message = TextMessage::new(Role::User, Self::build_message(&selected)) - .model(event.agent.model.clone()) - .droppable(true); + let message = TextMessage::new( + Role::User, + Element::new("recommended_skills") + .text( + "Based on the user's task, the following skills are likely relevant. Consider invoking them using them if they match the task. Do not mention these recommendations to the user.", + ) + .append(selected.iter().map(Element::from)) + .render(), + ) + .model(event.agent.model.clone()) + .droppable(true); let ctx = conversation .context From 4749c6e21a6fb9bf97c7601fd75c4c9d2e12a2c0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:53:55 +0000 Subject: [PATCH 08/14] [autofix.ci] apply automated fixes --- crates/forge_app/src/hooks/skill_recommendation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge_app/src/hooks/skill_recommendation.rs b/crates/forge_app/src/hooks/skill_recommendation.rs index 92fb678f12..e09e658131 100644 --- a/crates/forge_app/src/hooks/skill_recommendation.rs +++ b/crates/forge_app/src/hooks/skill_recommendation.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use async_trait::async_trait; use forge_domain::{ - ContextMessage, Conversation, EventData, EventHandle, Role, SelectedSkill, StartPayload, + ContextMessage, Conversation, EventData, EventHandle, Role, StartPayload, TextMessage, }; use forge_template::Element; From a3f0907ff0d385479279a1dc1e10ba6fc7337fb4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:55:48 +0000 Subject: [PATCH 09/14] [autofix.ci] apply automated fixes (attempt 2/3) --- crates/forge_app/src/hooks/skill_recommendation.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/forge_app/src/hooks/skill_recommendation.rs b/crates/forge_app/src/hooks/skill_recommendation.rs index e09e658131..2abc359395 100644 --- a/crates/forge_app/src/hooks/skill_recommendation.rs +++ b/crates/forge_app/src/hooks/skill_recommendation.rs @@ -2,8 +2,7 @@ use std::sync::Arc; use async_trait::async_trait; use forge_domain::{ - ContextMessage, Conversation, EventData, EventHandle, Role, StartPayload, - TextMessage, + ContextMessage, Conversation, EventData, EventHandle, Role, StartPayload, TextMessage, }; use forge_template::Element; use tracing::warn; From 4ac4c04a33241a08909438e6774a89640f8e7816 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 16 Mar 2026 17:51:33 +0530 Subject: [PATCH 10/14] refactor(tests): simplify home path assignment in fixture setup --- crates/forge_main/src/info.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/forge_main/src/info.rs b/crates/forge_main/src/info.rs index 3cf0dc97c6..747b879375 100644 --- a/crates/forge_main/src/info.rs +++ b/crates/forge_main/src/info.rs @@ -777,9 +777,7 @@ mod tests { use fake::{Fake, Faker}; let mut fixture: Environment = Faker.fake(); fixture = fixture.os(os.to_string()); - if let Some(home_path) = home { - fixture = fixture.home(PathBuf::from(home_path)); - } + fixture.home = home.map(PathBuf::from); fixture } From 9cb1a9faf457fac1d319f97aec8039151011ab3c Mon Sep 17 00:00:00 2001 From: laststylebender <43403528+laststylebender14@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:53:54 +0530 Subject: [PATCH 11/14] Update crates/forge_app/src/hooks/skill_recommendation.rs Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --- crates/forge_app/src/hooks/skill_recommendation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge_app/src/hooks/skill_recommendation.rs b/crates/forge_app/src/hooks/skill_recommendation.rs index 2abc359395..b05a76e26a 100644 --- a/crates/forge_app/src/hooks/skill_recommendation.rs +++ b/crates/forge_app/src/hooks/skill_recommendation.rs @@ -85,7 +85,7 @@ impl EventHandle> for SkillRecommen Role::User, Element::new("recommended_skills") .text( - "Based on the user's task, the following skills are likely relevant. Consider invoking them using them if they match the task. Do not mention these recommendations to the user.", + "Based on the user's task, the following skills are likely relevant. Consider using them if they match the task. Do not mention these recommendations to the user.", ) .append(selected.iter().map(Element::from)) .render(), From 7a3fd5f5a028c5710f3dfedc012b7eab7f9ebd76 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Sat, 21 Mar 2026 17:30:36 +0530 Subject: [PATCH 12/14] feat: integrate skill recommendation template and update message handling --- crates/forge_app/src/hooks/skill_recommendation.rs | 9 +++++---- crates/forge_repo/src/agents/forge.md | 5 ----- templates/forge-skill-recommendation-message.md | 1 + 3 files changed, 6 insertions(+), 9 deletions(-) create mode 100644 templates/forge-skill-recommendation-message.md diff --git a/crates/forge_app/src/hooks/skill_recommendation.rs b/crates/forge_app/src/hooks/skill_recommendation.rs index b05a76e26a..54262cff5b 100644 --- a/crates/forge_app/src/hooks/skill_recommendation.rs +++ b/crates/forge_app/src/hooks/skill_recommendation.rs @@ -7,7 +7,7 @@ use forge_domain::{ use forge_template::Element; use tracing::warn; -use crate::WorkspaceService; +use crate::{TemplateEngine, WorkspaceService}; /// Hook handler that injects skill recommendations as a droppable user message /// at the start of each conversation turn. @@ -81,12 +81,13 @@ impl EventHandle> for SkillRecommen } // Inject as a droppable user message so it can be removed during compaction. + let instruction = TemplateEngine::default() + .render("forge-skill-recommendation-message.md", &serde_json::json!({}))?; + let message = TextMessage::new( Role::User, Element::new("recommended_skills") - .text( - "Based on the user's task, the following skills are likely relevant. Consider using them if they match the task. Do not mention these recommendations to the user.", - ) + .text(instruction) .append(selected.iter().map(Element::from)) .render(), ) diff --git a/crates/forge_repo/src/agents/forge.md b/crates/forge_repo/src/agents/forge.md index 9d18f9918e..8a347d226b 100644 --- a/crates/forge_repo/src/agents/forge.md +++ b/crates/forge_repo/src/agents/forge.md @@ -134,8 +134,3 @@ Choose tools based on the nature of the task: - Avoid generating long hashes or binary code - Validate changes by compiling and running tests - Do not delete failing tests without a compelling reason - -{{#if skills}} -{{> forge-partial-skill-instructions.md}} -{{else}} -{{/if}} diff --git a/templates/forge-skill-recommendation-message.md b/templates/forge-skill-recommendation-message.md new file mode 100644 index 0000000000..fd2e609d7b --- /dev/null +++ b/templates/forge-skill-recommendation-message.md @@ -0,0 +1 @@ +Based on the user's task, the following skills are likely relevant. Consider using them if they match the task. Do not mention these recommendations to the user. \ No newline at end of file From 6d57cac41f58797bcc02416d8d694a9c7a19bb9c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 12:02:41 +0000 Subject: [PATCH 13/14] [autofix.ci] apply automated fixes --- crates/forge_app/src/hooks/skill_recommendation.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/forge_app/src/hooks/skill_recommendation.rs b/crates/forge_app/src/hooks/skill_recommendation.rs index 54262cff5b..0f357899d4 100644 --- a/crates/forge_app/src/hooks/skill_recommendation.rs +++ b/crates/forge_app/src/hooks/skill_recommendation.rs @@ -81,8 +81,10 @@ impl EventHandle> for SkillRecommen } // Inject as a droppable user message so it can be removed during compaction. - let instruction = TemplateEngine::default() - .render("forge-skill-recommendation-message.md", &serde_json::json!({}))?; + let instruction = TemplateEngine::default().render( + "forge-skill-recommendation-message.md", + &serde_json::json!({}), + )?; let message = TextMessage::new( Role::User, From 9c0543bca8199106d9731ebb0db1cd7d4ed2a741 Mon Sep 17 00:00:00 2001 From: Tushar Date: Tue, 24 Mar 2026 12:23:53 +0530 Subject: [PATCH 14/14] feat(skills): adjust recommendations and template logic --- .../forge_app/src/hooks/skill_recommendation.rs | 9 +-------- crates/forge_domain/src/skill.rs | 7 ++----- crates/forge_repo/proto/forge.proto | 1 - crates/forge_repo/src/agents/forge.md | 5 +++++ crates/forge_repo/src/context_engine.rs | 2 +- templates/forge-partial-skill-instructions.md | 15 +-------------- templates/forge-skill-recommendation-message.md | 1 - 7 files changed, 10 insertions(+), 30 deletions(-) delete mode 100644 templates/forge-skill-recommendation-message.md diff --git a/crates/forge_app/src/hooks/skill_recommendation.rs b/crates/forge_app/src/hooks/skill_recommendation.rs index 0f357899d4..48d7a63ca2 100644 --- a/crates/forge_app/src/hooks/skill_recommendation.rs +++ b/crates/forge_app/src/hooks/skill_recommendation.rs @@ -7,7 +7,7 @@ use forge_domain::{ use forge_template::Element; use tracing::warn; -use crate::{TemplateEngine, WorkspaceService}; +use crate::WorkspaceService; /// Hook handler that injects skill recommendations as a droppable user message /// at the start of each conversation turn. @@ -80,16 +80,9 @@ impl EventHandle> for SkillRecommen return Ok(()); } - // Inject as a droppable user message so it can be removed during compaction. - let instruction = TemplateEngine::default().render( - "forge-skill-recommendation-message.md", - &serde_json::json!({}), - )?; - let message = TextMessage::new( Role::User, Element::new("recommended_skills") - .text(instruction) .append(selected.iter().map(Element::from)) .render(), ) diff --git a/crates/forge_domain/src/skill.rs b/crates/forge_domain/src/skill.rs index 0c3073a814..174fada114 100644 --- a/crates/forge_domain/src/skill.rs +++ b/crates/forge_domain/src/skill.rs @@ -82,8 +82,6 @@ pub struct SelectedSkill { pub name: String, /// Relevance score (0.0–1.0) of the skill against the user prompt pub relevance: f32, - /// 1-based rank of this skill in the selection results - pub rank: u64, } impl SelectedSkill { @@ -92,9 +90,8 @@ impl SelectedSkill { /// # Arguments /// * `name` - The skill identifier /// * `relevance` - How relevant the skill is (0.0–1.0) - /// * `rank` - Position in the ranked list (1-based) - pub fn new(name: impl Into, relevance: f32, rank: u64) -> Self { - Self { name: name.into(), relevance, rank } + pub fn new(name: impl Into, relevance: f32) -> Self { + Self { name: name.into(), relevance } } } diff --git a/crates/forge_repo/proto/forge.proto b/crates/forge_repo/proto/forge.proto index 5ea339a85d..568673fa93 100644 --- a/crates/forge_repo/proto/forge.proto +++ b/crates/forge_repo/proto/forge.proto @@ -333,7 +333,6 @@ message Skill { message SelectedSkill { string name = 1; float relevance = 2; - uint64 rank = 3; } message SelectSkillRequest { diff --git a/crates/forge_repo/src/agents/forge.md b/crates/forge_repo/src/agents/forge.md index 8a347d226b..9d18f9918e 100644 --- a/crates/forge_repo/src/agents/forge.md +++ b/crates/forge_repo/src/agents/forge.md @@ -134,3 +134,8 @@ Choose tools based on the nature of the task: - Avoid generating long hashes or binary code - Validate changes by compiling and running tests - Do not delete failing tests without a compelling reason + +{{#if skills}} +{{> forge-partial-skill-instructions.md}} +{{else}} +{{/if}} diff --git a/crates/forge_repo/src/context_engine.rs b/crates/forge_repo/src/context_engine.rs index 4a9f24e961..3e072a7df9 100644 --- a/crates/forge_repo/src/context_engine.rs +++ b/crates/forge_repo/src/context_engine.rs @@ -410,7 +410,7 @@ impl WorkspaceIndexRepository for ForgeContextEngineRepository let selected_skills = response .skills .into_iter() - .map(|skill| SelectedSkill::new(skill.name, skill.relevance, skill.rank)) + .map(|skill| SelectedSkill::new(skill.name, skill.relevance)) .collect(); Ok(selected_skills) diff --git a/templates/forge-partial-skill-instructions.md b/templates/forge-partial-skill-instructions.md index 700b359f83..9ef81f065f 100644 --- a/templates/forge-partial-skill-instructions.md +++ b/templates/forge-partial-skill-instructions.md @@ -5,12 +5,10 @@ How skills work: 1. **Invocation**: Use the `skill` tool with just the skill name parameter - - Example: Call skill tool with `{"name": "mock-calculator"}` - No additional arguments needed 2. **Response**: The tool returns the skill's details wrapped in `` containing: - - `` - The complete SKILL.md file content with the skill's path - `` tags - List of additional resource files available in the skill directory - Includes usage guidelines, instructions, and any domain-specific knowledge @@ -28,18 +26,7 @@ Examples of skill invocation: Important: -- Only invoke skills listed in `` below +- Only invoke skills listed by the skill tool - Do not invoke a skill that is already active/loaded - Skills are not CLI commands - use the skill tool to load them - After loading a skill, follow its specific instructions to help the user - - -{{#each skills}} - -{{this.name}} - -{{this.description}} - - -{{/each}} - diff --git a/templates/forge-skill-recommendation-message.md b/templates/forge-skill-recommendation-message.md deleted file mode 100644 index fd2e609d7b..0000000000 --- a/templates/forge-skill-recommendation-message.md +++ /dev/null @@ -1 +0,0 @@ -Based on the user's task, the following skills are likely relevant. Consider using them if they match the task. Do not mention these recommendations to the user. \ No newline at end of file