diff --git a/refact-agent/engine/src/agentic/generate_follow_up_message.rs b/refact-agent/engine/src/agentic/generate_follow_up_message.rs index 45974f520..d2a5e1fa0 100644 --- a/refact-agent/engine/src/agentic/generate_follow_up_message.rs +++ b/refact-agent/engine/src/agentic/generate_follow_up_message.rs @@ -11,19 +11,23 @@ const PROMPT: &str = r#" Your task is to do two things for a conversation between a user and an assistant: 1. **Follow-Up Messages:** - - Create up to 3 short follow-up messages that the user might send after the assistant’s last message. + - Create up to 5 super short follow-up messages that the user might send after the assistant's last message. - The first message should invite the assistant to keep talking. - Each message should have a different meaning. - - If there is no clear follow-up or the conversation isn’t asking a question, return an empty list. + - If the assistant's last message contains a question, generate different replies that address that question. + - Maybe include a suggestion to think. + - Maybe include a suggestion to explore more context (e.g., "Can we look at more files?", "Is there additional context I should provide?"). + - Maybe include a suggestion to create knowledge. + - If there is no clear follow-up or the conversation isn't asking a question, return an empty list. 2. **Topic Change Detection:** - - Decide if the user’s latest message is about a different topic from the previous conversation. + - Decide if the user's latest message is about a different topic or a different project or a different problem from the previous conversation. - A topic change means the new topic is not related to the previous discussion. Return the result in this JSON format (without extra formatting): { - "follow_ups": ["Follow-up 1", "Follow-up 2"], + "follow_ups": ["Follow-up 1", "Follow-up 2", "Follow-up 3", "Follow-up 4", "Follow-up 5"], "topic_changed": true } "#; diff --git a/refact-agent/engine/src/at_commands/at_tree.rs b/refact-agent/engine/src/at_commands/at_tree.rs index 6f7b8ef0a..b88db8bd1 100644 --- a/refact-agent/engine/src/at_commands/at_tree.rs +++ b/refact-agent/engine/src/at_commands/at_tree.rs @@ -32,6 +32,12 @@ impl AtTree { #[derive(Debug, Clone)] pub struct PathsHolderNodeArc(Arc>); +impl PathsHolderNodeArc { + pub fn read(&self) -> std::sync::RwLockReadGuard<'_, PathsHolderNode> { + self.0.read().unwrap() + } +} + impl PartialEq for PathsHolderNodeArc { fn eq(&self, other: &Self) -> bool { self.0.read().unwrap().path == other.0.read().unwrap().path @@ -50,6 +56,14 @@ impl PathsHolderNode { pub fn file_name(&self) -> String { self.path.file_name().unwrap_or_default().to_string_lossy().to_string() } + + pub fn child_paths(&self) -> &Vec { + &self.child_paths + } + + pub fn get_path(&self) -> &PathBuf { + &self.path + } } pub fn construct_tree_out_of_flat_list_of_paths(paths_from_anywhere: &Vec) -> Vec { diff --git a/refact-agent/engine/src/http/routers/v1/chat.rs b/refact-agent/engine/src/http/routers/v1/chat.rs index 5a72d784b..0df6f0fa6 100644 --- a/refact-agent/engine/src/http/routers/v1/chat.rs +++ b/refact-agent/engine/src/http/routers/v1/chat.rs @@ -49,7 +49,7 @@ pub fn available_tools_by_chat_mode(current_tools: Vec, chat_mode: &ChatM vec![] }, ChatMode::EXPLORE | ChatMode::AGENT => filter_out_tools(¤t_tools, &vec!["think"]), - ChatMode::THINKING_AGENT => filter_out_tools(¤t_tools, &vec!["knowledge"]), + ChatMode::THINKING_AGENT => current_tools, ChatMode::CONFIGURE => filter_out_tools(¤t_tools, &vec!["tree", "locate", "knowledge", "search"]), ChatMode::PROJECT_SUMMARY => keep_tools(¤t_tools, &vec!["cat", "tree", "bash"]), } diff --git a/refact-agent/engine/src/http/routers/v1/subchat.rs b/refact-agent/engine/src/http/routers/v1/subchat.rs index 81352b3fa..6c49d43f8 100644 --- a/refact-agent/engine/src/http/routers/v1/subchat.rs +++ b/refact-agent/engine/src/http/routers/v1/subchat.rs @@ -48,6 +48,7 @@ pub async fn handle_v1_subchat( None, None, None, + Some(false), ).await.map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, format!("Error: {}", e)))?; let new_messages = new_messages.into_iter() @@ -102,7 +103,7 @@ pub async fn handle_v1_subchat_single( None, post.n, None, - true, + false, None, None, None, diff --git a/refact-agent/engine/src/subchat.rs b/refact-agent/engine/src/subchat.rs index dfa353e8a..037787604 100644 --- a/refact-agent/engine/src/subchat.rs +++ b/refact-agent/engine/src/subchat.rs @@ -365,6 +365,7 @@ pub async fn subchat( temperature: Option, tx_toolid_mb: Option, tx_chatid_mb: Option, + prepend_system_prompt: Option, ) -> Result>, String> { let mut messages = messages.clone(); let mut usage_collector = ChatUsage { ..Default::default() }; @@ -400,7 +401,7 @@ pub async fn subchat( None, 1, None, - true, + prepend_system_prompt.unwrap_or(false), Some(&mut usage_collector), tx_toolid_mb.clone(), tx_chatid_mb.clone(), @@ -423,7 +424,7 @@ pub async fn subchat( None, 1, None, - true, + prepend_system_prompt.unwrap_or(false), Some(&mut usage_collector), tx_toolid_mb.clone(), tx_chatid_mb.clone(), @@ -435,18 +436,42 @@ pub async fn subchat( ccx.clone(), model_name, messages, - Some(vec![]), - Some("none".to_string()), + Some(tools_subset.clone()), + Some("auto".to_string()), false, temperature, None, wrap_up_n, None, - true, + prepend_system_prompt.unwrap_or(false), Some(&mut usage_collector), tx_toolid_mb.clone(), tx_chatid_mb.clone(), ).await?; + for messages in choices.iter() { + let last_message = messages.last().unwrap(); + if let Some(tool_calls) = &last_message.tool_calls { + if !tool_calls.is_empty() { + _ = subchat_single( + ccx.clone(), + model_name, + messages.clone(), + Some(vec![]), + Some("none".to_string()), + true, // <-- only runs tool calls + temperature, + None, + 1, + None, + prepend_system_prompt.unwrap_or(false), + Some(&mut usage_collector), + tx_toolid_mb.clone(), + tx_chatid_mb.clone(), + ).await?[0].clone(); + } + } + + } // if let Some(last_message) = messages.last_mut() { // last_message.usage = Some(usage_collector); // } diff --git a/refact-agent/engine/src/tools/mod.rs b/refact-agent/engine/src/tools/mod.rs index 37588eb4c..e0137f0f3 100644 --- a/refact-agent/engine/src/tools/mod.rs +++ b/refact-agent/engine/src/tools/mod.rs @@ -18,4 +18,8 @@ mod tool_search; mod tool_knowledge; #[cfg(feature="vecdb")] mod tool_locate_search; +#[cfg(feature="vecdb")] +mod tool_create_knowledge; +#[cfg(feature="vecdb")] +mod tool_create_memory_bank; pub mod file_edit; diff --git a/refact-agent/engine/src/tools/tool_create_knowledge.rs b/refact-agent/engine/src/tools/tool_create_knowledge.rs new file mode 100644 index 000000000..965df9a17 --- /dev/null +++ b/refact-agent/engine/src/tools/tool_create_knowledge.rs @@ -0,0 +1,100 @@ +use std::collections::HashMap; +use std::sync::Arc; +use async_trait::async_trait; +use serde_json::Value; +use tracing::info; +use tokio::sync::Mutex as AMutex; + +use crate::at_commands::at_commands::AtCommandsContext; +use crate::call_validation::{ChatMessage, ChatContent, ContextEnum}; +use crate::tools::tools_description::Tool; + +pub struct ToolCreateKnowledge; + +#[async_trait] +impl Tool for ToolCreateKnowledge { + fn as_any(&self) -> &dyn std::any::Any { self } + + async fn tool_execute( + &mut self, + ccx: Arc>, + tool_call_id: &String, + args: &HashMap, + ) -> Result<(bool, Vec), String> { + info!("run @create-knowledge with args: {:?}", args); + + let gcx = { + let ccx_locked = ccx.lock().await; + ccx_locked.global_context.clone() + }; + + let im_going_to_use_tools = match args.get("im_going_to_use_tools") { + Some(Value::String(s)) => s.clone(), + Some(v) => return Err(format!("argument `im_going_to_use_tools` is not a string: {:?}", v)), + None => return Err("argument `im_going_to_use_tools` is missing".to_string()) + }; + + let im_going_to_apply_to = match args.get("im_going_to_apply_to") { + Some(Value::String(s)) => s.clone(), + Some(v) => return Err(format!("argument `im_going_to_apply_to` is not a string: {:?}", v)), + None => return Err("argument `im_going_to_apply_to` is missing".to_string()) + }; + + let search_key = match args.get("search_key") { + Some(Value::String(s)) => s.clone(), + Some(v) => return Err(format!("argument `search_key` is not a string: {:?}", v)), + None => return Err("argument `search_key` is missing".to_string()) + }; + + let language_slash_framework = match args.get("language_slash_framework") { + Some(Value::String(s)) => s.clone(), + Some(v) => return Err(format!("argument `language_slash_framework` is not a string: {:?}", v)), + None => return Err("argument `language_slash_framework` is missing".to_string()) + }; + + let knowledge_entry = match args.get("knowledge_entry") { + Some(Value::String(s)) => s.clone(), + Some(v) => return Err(format!("argument `knowledge_entry` is not a string: {:?}", v)), + None => return Err("argument `knowledge_entry` is missing".to_string()) + }; + + let vec_db = gcx.read().await.vec_db.clone(); + + // Store the memory with type "knowledge-entry" + let memid = match crate::vecdb::vdb_highlev::memories_add( + vec_db.clone(), + "knowledge-entry", + &search_key, + &im_going_to_apply_to, + &knowledge_entry, + "user-created" + ).await { + Ok(id) => id, + Err(e) => return Err(format!("Failed to store knowledge: {}", e)) + }; + + let message = format!("Knowledge entry created successfully with ID: {}\nTools: {}\nApply to: {}\nSearch Key: {}\nLanguage/Framework: {}\nEntry: {}", + memid, + im_going_to_use_tools, + im_going_to_apply_to, + search_key, + language_slash_framework, + knowledge_entry + ); + + let mut results = vec![]; + results.push(ContextEnum::ChatMessage(ChatMessage { + role: "tool".to_string(), + content: ChatContent::SimpleText(message), + tool_calls: None, + tool_call_id: tool_call_id.clone(), + ..Default::default() + })); + + Ok((false, results)) + } + + fn tool_depends_on(&self) -> Vec { + vec!["vecdb".to_string()] + } +} \ No newline at end of file diff --git a/refact-agent/engine/src/tools/tool_create_memory_bank.rs b/refact-agent/engine/src/tools/tool_create_memory_bank.rs new file mode 100644 index 000000000..5266602c3 --- /dev/null +++ b/refact-agent/engine/src/tools/tool_create_memory_bank.rs @@ -0,0 +1,495 @@ +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, + sync::Arc, +}; + +use async_trait::async_trait; +use chrono::Local; +use serde_json::Value; +use tokio::sync::{Mutex as AMutex, RwLock as ARwLock}; + +use crate::{ + at_commands::{ + at_commands::AtCommandsContext, + at_tree::{construct_tree_out_of_flat_list_of_paths, PathsHolderNodeArc}, + }, + cached_tokenizers, + call_validation::{ChatContent, ChatMessage, ChatUsage, ContextEnum, ContextFile, PostprocessSettings}, + files_correction::{get_project_dirs, paths_from_anywhere}, + files_in_workspace::{get_file_text_from_memory_or_disk, ls_files}, + global_context::GlobalContext, + postprocessing::pp_context_files::postprocess_context_files, + subchat::subchat, + tools::tools_description::Tool, +}; +use crate::global_context::try_load_caps_quickly_if_not_present; + +const MAX_EXPLORATION_STEPS: usize = 1000; + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +struct ExplorationTarget { + target_name: String, +} + +#[derive(Debug)] +struct ExplorationState { + explored: HashSet, + to_explore: Vec, + project_tree: Option>, +} + +impl ExplorationState { + fn get_tree_stats(tree: &[PathsHolderNodeArc]) -> (usize, f64) { + fn traverse(node: &PathsHolderNodeArc) -> (usize, Vec) { + let node_ref = node.read(); + let children = node_ref.child_paths(); + if children.is_empty() { + (1, vec![1]) + } else { + let child_stats: Vec<_> = children.iter().map(traverse).collect(); + let max_depth = 1 + child_stats.iter().map(|(d, _)| *d).max().unwrap_or(0); + let mut sizes = vec![children.len()]; + sizes.extend(child_stats.into_iter().flat_map(|(_, s)| s)); + (max_depth, sizes) + } + } + + let stats: Vec<_> = tree.iter().map(traverse).collect(); + let max_depth = stats.iter().map(|(d, _)| *d).max().unwrap_or(1); + let sizes: Vec<_> = stats.into_iter().flat_map(|(_, s)| s).collect(); + let avg_size = sizes.iter().sum::() as f64 / sizes.len() as f64; + (max_depth, avg_size) + } + + fn calculate_importance_score( + node: &PathsHolderNodeArc, + depth: usize, + max_tree_depth: usize, + avg_dir_size: f64, + project_dirs: &[std::path::PathBuf], + ) -> Option { + let node_ref = node.read(); + let node_path = node_ref.get_path(); + + // Check if the current node is one of the project directories + let is_project_dir = project_dirs.iter().any(|pd| pd == node_path); + + // Only filter out node if it is NOT a project directory + if !is_project_dir && (node_ref.file_name().starts_with('.') || node_ref.child_paths().is_empty()) { + return None; + } + + let relative_depth = depth as f64 / max_tree_depth as f64; + let direct_children = node_ref.child_paths().len() as f64; + let total_children = { + fn count(n: &PathsHolderNodeArc) -> usize { + let count_direct = n.read().child_paths().len(); + count_direct + n.read().child_paths().iter().map(count).sum::() + } + count(node) as f64 + }; + + // For deep-first exploration: lower score = higher priority (we sort ascending) + // Invert relative_depth so deeper directories get lower scores + let depth_score = 1.0 - relative_depth; // Now deeper dirs get higher relative_depth but lower depth_score + + // Size score - smaller directories get lower scores (preferred) + let size_score = ((direct_children + total_children) as f64 / avg_dir_size).min(1.0); + + // Deep directory bonus (subtracts from score for deeper directories) + let deep_bonus = if relative_depth > 0.8 { 1.0 } else { 0.0 }; + + // Calculate final score - lower scores will be explored first + // Increased depth weight, reduced size impact, increased deep bonus + Some(depth_score * 0.8 + size_score * 0.1 - deep_bonus * 0.2) + } + + async fn collect_targets_from_tree( + tree: &[PathsHolderNodeArc], + gcx: Arc>, + ) -> Vec { + let (max_depth, avg_size) = Self::get_tree_stats(tree); + let project_dirs = get_project_dirs(gcx.clone()).await; + + fn traverse( + node: &PathsHolderNodeArc, + depth: usize, + max_depth: usize, + avg_size: f64, + project_dirs: &[std::path::PathBuf], + ) -> Vec<(ExplorationTarget, f64)> { + let mut targets = Vec::new(); + + if let Some(score) = ExplorationState::calculate_importance_score(node, depth, max_depth, avg_size, project_dirs) { + let node_ref = node.read(); + targets.push(( + ExplorationTarget { + target_name: node_ref.get_path().to_string_lossy().to_string(), + }, + score + )); + + for child in node_ref.child_paths() { + targets.extend(traverse(child, depth + 1, max_depth, avg_size, project_dirs)); + } + } + targets + } + + let mut scored_targets: Vec<_> = tree.iter() + .flat_map(|node| traverse(node, 0, max_depth, avg_size, &project_dirs)) + .collect(); + + scored_targets.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)); + scored_targets.into_iter().map(|(target, _)| target).collect() + } + + async fn new(gcx: Arc>) -> Result { + let project_dirs = get_project_dirs(gcx.clone()).await; + let relative_paths: Vec = paths_from_anywhere(gcx.clone()).await + .into_iter() + .filter_map(|path| + project_dirs.iter() + .find(|dir| path.starts_with(dir)) + .map(|dir| { + // Get the project directory name + let project_name = dir.file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or_default(); + + // If path is deeper than project dir, append the rest of the path + if let Ok(rest) = path.strip_prefix(dir) { + if rest.as_os_str().is_empty() { + PathBuf::from(&project_name) + } else { + PathBuf::from(&project_name).join(rest) + } + } else { + PathBuf::from(&project_name) + } + })) + .collect(); + + let tree = construct_tree_out_of_flat_list_of_paths(&relative_paths); + let to_explore = Self::collect_targets_from_tree(&tree, gcx.clone()).await; + + Ok(Self { + explored: HashSet::new(), + to_explore, + project_tree: Some(tree), + }) + } + + fn get_next_target(&self) -> Option { + self.to_explore.first().cloned() + } + + fn mark_explored(&mut self, target: ExplorationTarget) { + self.explored.insert(target.clone()); + self.to_explore.retain(|x| x != &target); + } + + fn has_unexplored_targets(&self) -> bool { + !self.to_explore.is_empty() + } + + fn get_exploration_summary(&self) -> String { + let dir_count = self.explored.len(); + format!( + "Explored {} directories", + dir_count + ) + } + + fn project_tree_summary(&self) -> String { + self.project_tree.as_ref().map_or_else(String::new, |nodes| { + fn traverse(node: &PathsHolderNodeArc, depth: usize) -> String { + let node_ref = node.read(); + let mut result = format!("{}{}\n", " ".repeat(depth), node_ref.file_name()); + for child in node_ref.child_paths() { + result.push_str(&traverse(child, depth + 1)); + } + result + } + nodes.iter().map(|n| traverse(n, 0)).collect() + }) + } +} + +async fn read_and_compress_directory( + gcx: Arc>, + dir_relative: String, + tokens_limit: usize, + model: String, +) -> Result { + let project_dirs = get_project_dirs(gcx.clone()).await; + let base_dir = project_dirs.get(0).ok_or("No project directory found")?; + let abs_dir = base_dir.join(&dir_relative); + + let files = ls_files( + &*crate::files_blocklist::reload_indexing_everywhere_if_needed(gcx.clone()).await, + &abs_dir, + false + ).unwrap_or_default(); + tracing::info!( + target = "memory_bank", + directory = dir_relative, + files_count = files.len(), + token_limit = tokens_limit, + "Reading and compressing directory" + ); + + if files.is_empty() { + return Ok("Directory is empty; no files to read.".to_string()); + } + + let mut context_files = Vec::with_capacity(files.len()); + for f in &files { + let text = get_file_text_from_memory_or_disk(gcx.clone(), f) + .await + .unwrap_or_default(); + let lines = text.lines().count().max(1); + context_files.push(ContextFile { + file_name: f.to_string_lossy().to_string(), + file_content: text, + line1: 1, + line2: lines, + symbols: vec![], + gradient_type: -1, + usefulness: 0.0, + }); + } + + let caps = try_load_caps_quickly_if_not_present(gcx.clone(), 0).await.map_err(|x| x.message)?; + let tokenizer = cached_tokenizers::cached_tokenizer(caps, gcx.clone(), model).await.map_err(|e| format!("Tokenizer error: {}", e))?; + let mut pp_settings = PostprocessSettings::new(); + pp_settings.max_files_n = context_files.len(); + let compressed = postprocess_context_files( + gcx.clone(), + &mut context_files, + tokenizer, + tokens_limit, + false, + &pp_settings, + ).await; + + Ok(compressed.into_iter() + .map(|cf| format!("Filename: {}\n```\n{}\n```\n\n", cf.file_name, cf.file_content)) + .collect()) +} + +pub struct ToolCreateMemoryBank; + +const MB_SYSTEM_PROMPT: &str = r###"• Objective: + – Create a clear, natural language description of the project structure while building a comprehensive architectural understanding. + Do NOT call create_knowledge() until instructed + +• Analysis Guidelines: + 1. Start with knowledge(); examine existing context: + - Review previous descriptions of related components + - Understand known architectural patterns + - Map existing module relationships + + 2. Describe project structure in natural language: + - Explain what this directory/module is for + - Describe key files and their purposes + - Detail how files work together + - Note any interesting implementation details + - Explain naming patterns and organization + + 3. Analyze code architecture: + - Module's role and responsibilities + - Key types, traits, functions, and their purposes + - Public interfaces and abstraction boundaries + - Error handling and data flow patterns + - Cross-cutting concerns and utilities + + 4. Document relationships: + - Which modules use this one and how + - What this module uses from others + - How components communicate + - Integration patterns and dependencies + + 5. Map architectural patterns: + - Design patterns used and why + - How it fits in the layered architecture + - State management approaches + - Extension and plugin points + + 6. Compare with existing knowledge: + - "This builds upon X from module Y by..." + - "Unlike module X, this takes a different approach to Y by..." + - "This introduces a new way to handle X through..." + + 7. Use structured format: + • Purpose: [clear description of what this does] + • Files: [key files and their roles] + • Architecture: [design patterns and module relationships] + • Key Symbols: [important types/traits/functions] + • Integration: [how it works with other parts] + +• Operational Constraint: + – Do NOT call create_knowledge() until instructed."###; + +const MB_EXPERT_WRAP_UP: &str = r###"Call create_knowledge() now with your complete and full analysis from the previous step if you haven't called it yet. Otherwise just type "Finished"."###; + +impl ToolCreateMemoryBank { + fn build_step_prompt( + state: &ExplorationState, + target: &ExplorationTarget, + file_context: Option<&String>, + ) -> String { + let mut prompt = String::new(); + prompt.push_str(MB_SYSTEM_PROMPT); + prompt.push_str(&format!("\n\nNow exploring directory: '{}' from the project '{}'", target.target_name, target.target_name.split('/').next().unwrap_or(""))); + { + prompt.push_str("\nFocus on details like purpose, organization, and notable files. Here is the project structure:\n"); + prompt.push_str(&state.project_tree_summary()); + if let Some(ctx) = file_context { + prompt.push_str("\n\nFiles context:\n"); + prompt.push_str(ctx); + } + } + prompt + } +} + +#[async_trait] +impl Tool for ToolCreateMemoryBank { + fn as_any(&self) -> &dyn std::any::Any { self } + + async fn tool_execute( + &mut self, + ccx: Arc>, + tool_call_id: &String, + _args: &HashMap + ) -> Result<(bool, Vec), String> { + let gcx = ccx.lock().await.global_context.clone(); + let params = crate::tools::tools_execute::unwrap_subchat_params(ccx.clone(), "create_memory_bank").await?; + + let ccx_subchat = { + let ccx_lock = ccx.lock().await; + let mut ctx = AtCommandsContext::new( + ccx_lock.global_context.clone(), + params.subchat_n_ctx, + 7, + false, + ccx_lock.messages.clone(), + ccx_lock.chat_id.clone(), + ccx_lock.should_execute_remotely, + ).await; + ctx.subchat_tx = ccx_lock.subchat_tx.clone(); + ctx.subchat_rx = ccx_lock.subchat_rx.clone(); + Arc::new(AMutex::new(ctx)) + }; + + let mut state = ExplorationState::new(gcx.clone()).await?; + let mut final_results = Vec::new(); + let mut step = 0; + let mut usage_collector = ChatUsage::default(); + + while state.has_unexplored_targets() && step < MAX_EXPLORATION_STEPS { + step += 1; + let log_prefix = Local::now().format("%Y%m%d-%H%M%S").to_string(); + if let Some(target) = state.get_next_target() { + tracing::info!( + target = "memory_bank", + step = step, + max_steps = MAX_EXPLORATION_STEPS, + directory = target.target_name, + "Starting directory exploration" + ); + let file_context = read_and_compress_directory( + gcx.clone(), + target.target_name.clone(), + params.subchat_tokens_for_rag, + params.subchat_model.clone(), + ).await.map_err(|e| { + tracing::warn!("Failed to read/compress files for {}: {}", target.target_name, e); + e + }).ok(); + + let step_msg = ChatMessage::new( + "user".to_string(), + Self::build_step_prompt(&state, &target, file_context.as_ref()) + ); + + let subchat_result = subchat( + ccx_subchat.clone(), + params.subchat_model.as_str(), + vec![step_msg], + vec!["knowledge".to_string(), "create_knowledge".to_string()], + 8, + params.subchat_max_new_tokens, + MB_EXPERT_WRAP_UP, + 1, + None, + Some(tool_call_id.clone()), + Some(format!("{log_prefix}-memory-bank-dir-{}", target.target_name.replace("/", "_"))), + Some(false), + ).await?[0].clone(); + + // Update usage from subchat result + if let Some(last_msg) = subchat_result.last() { + crate::tools::tool_relevant_files::update_usage_from_message(&mut usage_collector, last_msg); + tracing::info!( + target = "memory_bank", + directory = target.target_name, + prompt_tokens = usage_collector.prompt_tokens, + completion_tokens = usage_collector.completion_tokens, + total_tokens = usage_collector.total_tokens, + "Updated token usage" + ); + } + + state.mark_explored(target.clone()); + let total = state.to_explore.len() + state.explored.len(); + tracing::info!( + target = "memory_bank", + directory = target.target_name, + remaining_dirs = state.to_explore.len(), + explored_dirs = state.explored.len(), + total_dirs = total, + progress = format!("{}/{}", state.to_explore.len(), total), + "Completed directory exploration" + ); + } else { + break; + } + } + + final_results.push(ContextEnum::ChatMessage(ChatMessage { + role: "tool".to_string(), + content: ChatContent::SimpleText(format!( + "Memory bank creation completed. Steps: {}, {}. Total directories: {}. Usage: {} prompt tokens, {} completion tokens", + step, + state.get_exploration_summary(), + state.explored.len() + state.to_explore.len(), + usage_collector.prompt_tokens, + usage_collector.completion_tokens, + )), + usage: Some(usage_collector), + tool_calls: None, + tool_call_id: tool_call_id.clone(), + ..Default::default() + })); + + Ok((false, final_results)) + } + + fn tool_depends_on(&self) -> Vec { + vec!["ast".to_string(), "vecdb".to_string()] + } + + fn tool_description(&self) -> crate::tools::tools_description::ToolDesc { + crate::tools::tools_description::ToolDesc { + name: "create_memory_bank".into(), + agentic: true, + experimental: true, + description: "Gathers information about the project structure (modules, file relations, classes, etc.) and saves this data into the memory bank.".into(), + parameters: Vec::new(), + parameters_required: Vec::new(), + } + } +} \ No newline at end of file diff --git a/refact-agent/engine/src/tools/tool_deep_thinking.rs b/refact-agent/engine/src/tools/tool_deep_thinking.rs index 38c8ff587..804068e56 100644 --- a/refact-agent/engine/src/tools/tool_deep_thinking.rs +++ b/refact-agent/engine/src/tools/tool_deep_thinking.rs @@ -28,7 +28,7 @@ async fn _make_prompt( let caps = try_load_caps_quickly_if_not_present(gcx.clone(), 0).await.map_err(|x| x.message)?; let tokenizer = cached_tokenizers::cached_tokenizer(caps, gcx.clone(), subchat_params.subchat_model.to_string()).await .map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, format!("Error loading tokenizer: {}", e))).map_err(|x| x.message)?; - let mut tokens_budget: i64 = (subchat_params.subchat_n_ctx - subchat_params.subchat_max_new_tokens - subchat_params.subchat_tokens_for_rag) as i64; + let mut tokens_budget: i64 = (subchat_params.subchat_n_ctx - subchat_params.subchat_max_new_tokens - subchat_params.subchat_tokens_for_rag - 1000) as i64; let final_message = format!("***Problem:***\n{problem_statement}\n\n***Problem context:***\n"); tokens_budget -= count_tokens(&tokenizer.read().unwrap(), &final_message) as i64; let mut context = "".to_string(); diff --git a/refact-agent/engine/src/tools/tool_locate_search.rs b/refact-agent/engine/src/tools/tool_locate_search.rs index 408873090..baec1dbd9 100644 --- a/refact-agent/engine/src/tools/tool_locate_search.rs +++ b/refact-agent/engine/src/tools/tool_locate_search.rs @@ -205,6 +205,7 @@ async fn find_relevant_files_with_search( Some(0.1), Some(tool_call_id.clone()), Some(format!("{log_prefix}-locate-search")), + Some(false), ).await?[0].clone(); crate::tools::tool_relevant_files::check_for_inspected_files(&mut inspected_files, &result); diff --git a/refact-agent/engine/src/tools/tool_relevant_files.rs b/refact-agent/engine/src/tools/tool_relevant_files.rs index aa01337eb..0c2c89537 100644 --- a/refact-agent/engine/src/tools/tool_relevant_files.rs +++ b/refact-agent/engine/src/tools/tool_relevant_files.rs @@ -397,6 +397,7 @@ async fn find_relevant_files( Some(0.4), Some(tool_call_id.clone()), Some(format!("{log_prefix}-rf-step1-treeguess")), + Some(false), )); // ----- VECDBSEARCH ------ @@ -421,6 +422,7 @@ async fn find_relevant_files( Some(0.4), Some(tool_call_id.clone()), Some(format!("{log_prefix}-rf-step1-gotodef")), + Some(false), )); let results: Vec>> = join_all(futures).await.into_iter().filter_map(|x| x.ok()).collect(); @@ -464,6 +466,7 @@ async fn find_relevant_files( Some(0.0), Some(tool_call_id.clone()), Some(format!("{log_prefix}-rf-step2-reduce")), + Some(false), ).await?[0].clone(); check_for_inspected_files(&mut inspected_files, &result); diff --git a/refact-agent/engine/src/tools/tools_description.rs b/refact-agent/engine/src/tools/tools_description.rs index f4ea736dc..449b197ea 100644 --- a/refact-agent/engine/src/tools/tools_description.rs +++ b/refact-agent/engine/src/tools/tools_description.rs @@ -139,6 +139,12 @@ pub async fn tools_merged_and_filtered( ("rm".to_string(), Box::new(crate::tools::tool_rm::ToolRm{}) as Box), ("mv".to_string(), Box::new(crate::tools::tool_mv::ToolMv{}) as Box), ("think".to_string(), Box::new(crate::tools::tool_deep_thinking::ToolDeepThinking{}) as Box), + #[cfg(feature="vecdb")] + ("knowledge".to_string(), Box::new(crate::tools::tool_knowledge::ToolGetKnowledge{}) as Box), + #[cfg(feature="vecdb")] + ("create_knowledge".to_string(), Box::new(crate::tools::tool_create_knowledge::ToolCreateKnowledge{}) as Box), + #[cfg(feature="vecdb")] + ("create_memory_bank".to_string(), Box::new(crate::tools::tool_create_memory_bank::ToolCreateMemoryBank{}) as Box), // ("locate".to_string(), Box::new(crate::tools::tool_locate::ToolLocate{}) as Box))), // ("locate".to_string(), Box::new(crate::tools::tool_relevant_files::ToolRelevantFiles{}) as Box))), #[cfg(feature="vecdb")] @@ -147,9 +153,6 @@ pub async fn tools_merged_and_filtered( ("locate".to_string(), Box::new(crate::tools::tool_locate_search::ToolLocateSearch{}) as Box), ]); - #[cfg(feature="vecdb")] - tools_all.insert("knowledge".to_string(), Box::new(crate::tools::tool_knowledge::ToolGetKnowledge{}) as Box); - let integrations = crate::integrations::running_integrations::load_integration_tools( gcx.clone(), allow_experimental, @@ -458,6 +461,38 @@ tools: - "im_going_to_apply_to" - "goal" - "language_slash_framework" + + - name: "create_knowledge" + agentic: true + description: "Creates a new knowledge entry in the vector database to help with future tasks." + parameters: + - name: "im_going_to_use_tools" + type: "string" + description: "Which tools are you about to use? Comma-separated list, examples: hg, git, gitlab, rust debugger" + - name: "im_going_to_apply_to" + type: "string" + description: "What your actions will be applied to? List all you can identify, starting with the project name. Comma-separated list, examples: project1, file1.cpp, MyClass, PRs, issues" + - name: "search_key" + type: "string" + description: "Search keys for the knowledge database. Write combined elements from all fields (tools, project components, objectives, and language/framework). This field is used for vector similarity search." + - name: "language_slash_framework" + type: "string" + description: "What programming language and framework is the current project using? Use lowercase, dashes and dots. Examples: python/django, typescript/node.js, rust/tokio, ruby/rails, php/laravel, c++/boost-asio" + - name: "knowledge_entry" + type: "string" + description: "The detailed knowledge content to be stored. Include comprehensive information about implementation details, code patterns, architectural decisions, troubleshooting steps, or solution approaches. Document what was done, how it was done, why certain choices were made, and any important observations or lessons learned. This field should contain the rich, detailed content that future searches will retrieve." + parameters_required: + - "im_going_to_use_tools" + - "im_going_to_apply_to" + - "search_key" + - "language_slash_framework" + - "knowledge_entry" + + - name: "create_memory_bank" + agentic: true + description: "Gathers information about the project structure (modules, file relations, classes, etc.) and saves this data into the memory bank." + parameters: [] + parameters_required: [] "####; diff --git a/refact-agent/engine/src/vecdb/vdb_highlev.rs b/refact-agent/engine/src/vecdb/vdb_highlev.rs index 14fab4d5b..0a9c62828 100644 --- a/refact-agent/engine/src/vecdb/vdb_highlev.rs +++ b/refact-agent/engine/src/vecdb/vdb_highlev.rs @@ -536,6 +536,19 @@ pub async fn memories_search( let score_b = calculate_score(b.distance, b.mstat_times_used); score_a.partial_cmp(&score_b).unwrap_or(std::cmp::Ordering::Equal) }); + + let rejection_threshold = model_to_rejection_threshold(constants.embedding_model.as_str()); + let mut filtered_results = Vec::new(); + for rec in results.iter() { + if rec.distance.abs() >= rejection_threshold { + info!("distance {:.3} -> dropped memory {}", rec.distance, rec.memid); + } else { + info!("distance {:.3} -> kept memory {}", rec.distance, rec.memid); + filtered_results.push(rec.clone()); + } + } + results = filtered_results; + Ok(MemoSearchResult { query_text: query.clone(), results }) } diff --git a/refact-agent/engine/src/yaml_configs/customization_compiled_in.yaml b/refact-agent/engine/src/yaml_configs/customization_compiled_in.yaml index ff5cecc2e..d84e02e20 100644 --- a/refact-agent/engine/src/yaml_configs/customization_compiled_in.yaml +++ b/refact-agent/engine/src/yaml_configs/customization_compiled_in.yaml @@ -66,14 +66,20 @@ PROMPT_AGENTIC_TOOLS: | Core Principles 1. Use knowledge() - Always use knowledge() first when you encounter an agentic (complex) task. - - This tool can access external data, including successful “trajectories” (examples of past solutions). - - External database records begin with the icon “🗃️” followed by a record identifier. + - This tool can access external data, including successful "trajectories" (examples of past solutions). + - External database records begin with the icon "🗃️" followed by a record identifier. - Use these records to help solve your tasks by analogy. 2. Use locate() with the Full Problem Statement - - Provide the entire user request in the problem_statement argument to avoid losing any details (“telephone game” effect). - - Include user’s emotional stance, code snippets, formatting, instructions—everything word-for-word. - - Only omit parts of the user’s request if they are unrelated to the final solution. + - Provide the entire user request in the problem_statement argument to avoid losing any details ("telephone game" effect). + - Include user's emotional stance, code snippets, formatting, instructions—everything word-for-word. + - Only omit parts of the user's request if they are unrelated to the final solution. - Avoid using locate() if the problem is quite simple and can be solved without extensive project analysis. + 3. Execute Changes and Validate + - When a solution requires file modifications, use the appropriate *_textdoc() tools. + - After making changes, perform a validation step by reviewing modified files using cat() or similar tools. + - Check for available build tools (like cmdline_cargo_check, cmdline_cargo_build, etc.) and use them to validate changes. + - Ensure all changes are complete and consistent with the project's standards. + - If build validation fails or other issues are found, collect additional context and revise the changes. Answering Strategy 1. If the user’s question is unrelated to the project @@ -89,6 +95,24 @@ PROMPT_AGENTIC_TOOLS: | - When you explore different ideas, use multiple parallel methods. 2. Project-Related Questions - For any project question, always call knowledge() before taking any action. + 3. Knowledge Building (Automatic) + - After completing any significant task, AUTOMATICALLY use create_knowledge() without waiting for user prompting: + * Important code patterns and their usage locations + * Key relationships between classes/functions + * File dependencies and project structure insights + * Successful solution patterns for future reference + - Proactively create knowledge entries whenever you: + * Solve a problem or implement a feature + * Discover patterns in the codebase + * Learn something about project structure or dependencies + * Fix a bug or identify potential issues + * Analyze placeholders, test data, or configuration files + - Consider each interaction an opportunity to build the knowledge base - don't wait for explicit instructions + 4. Continuous Learning + - Treat every interaction as a learning opportunity + - When you encounter interesting code patterns, project structures, or implementation details, document them + - If you analyze placeholders, test data, or configuration files, record your findings + - Don't wait for the user to ask you to remember - proactively build the knowledge base %SHELL_INSTRUCTIONS% @@ -99,41 +123,72 @@ PROMPT_AGENTIC_TOOLS: | %WORKSPACE_INFO% %PROJECT_SUMMARY% + + IMPORTANT: Knowledge creation is not optional. After EVERY significant task, AUTOMATICALLY create a knowledge entry using create_knowledge() without waiting for user prompting. This is a core part of your workflow. PROMPT_THINKING_AGENT: | - [mode3] You are Refact Agent, an autonomous bot for coding tasks. - + [mode3] You are Refact Agent, an autonomous bot for coding tasks with thinking capabilities. STRATEGY - 1. Gather Maximum Context - - **Objective**: Expand your view of the project so no relevant information is overlooked. - - Use `tree()` to explore the project structure. - - Use `locate()` With the Full Problem Statement - - Use all other tools such as `search()`, `cat()`, `definition()`, etc. to collect every piece of relevant context. - - Open all files that might be indirectly referenced by the code. - 2. Plan Thoroughly With `think()` + 1. Use knowledge() + - Always call knowledge() first when facing any agentic (complex) task. + - This tool can access external data, including successful "trajectories" (examples of past solutions) marked by "🗃️." + - Use these records to help solve tasks by analogy. + + 2. Gather Maximum Context + - **Objective**: Ensure no relevant information is overlooked. + - Use tree() to explore the project structure. + - Use locate() with the full problem statement. + - Use search(), cat(), definition() and any related tool to collect all available context. + - Open all files that might be indirectly referenced. + + 3. Plan Thoroughly with think() - **Objective**: Develop a precise plan before making any changes. - - Provide the full problem statement again in the `problem_statement` argument of `think()`. - - Clearly define the expected output format. - - **Do not** make or apply changes at this point—only plan. - - Always gather required context (Step 1) before calling `think()`. - 3. Execute the Plan and Modify the Project - - **Objective**: Implement the step-by-step plan generated by `think()`. - - Make changes incrementally, using tools `*_textdoc()`. - - It's a good practice to call cat() to track changes for changed files. - - If any unexpected issues emerge, collect additional context before proceeding. - - Ensure modifications match the original objective and remain consistent across the project. - + - Call think() with the full problem statement and specify the expected output format. + - DO NOT make or apply changes until the plan is fully validated. + - Always gather sufficient context (Step 1) before invoking think(). + - If at any point you encounter any problem or ambiguity, call think() immediately to reanalyze and refine your plan. + + 4. Execute the Plan and Modify the Project Incrementally + - **Objective**: Implement the validated plan step by step. + - Make changes using appropriate *_textdoc() tools. + - After each modification, call cat() (or similar) to review the changes. + - If unexpected issues arise during execution, pause to gather additional context and call think() to adjust your plan accordingly. + + 5. Validate Changes with think() and Build Tools + - **Objective**: Verify that all changes are complete, correct, and buildable. + - After implementation, use think() to analyze the impact and consistency of your modifications. + - Check for available build tools (cmdline_cargo_check, cmdline_cargo_build, etc.) and run them to validate changes. + - Provide the full context of the modifications and build results in the problem_statement. + - If build validation fails or think() highlights issues, return to Step 4 to resolve them. ### **IMPORTANT NOTES** 1. **Parallel Exploration** - - You may use multiple methods in parallel (e.g., searching or opening files) to ensure complete understanding. - 2. **Do Not Modify Files Before `think()`** - - Strictly avoid editing the project until a thorough plan is established in `think()`. - 3. **No Premature `think()`** - - Only call `think()` after you have gathered the necessary context in Step 2. - + - Use multiple tools in parallel (e.g., tree, search, cat) to ensure complete understanding. + 2. **Do Not Modify Files Prematurely** + - Do not modify any file until a thorough and validated plan is established via think(). + 3. **Immediate Problem-Solving** + - If you run into any problem, even mid-step, pause and invoke think() to reassess your approach. + 4. **Knowledge Building (Automatic)** + - Once the task is successfully completed, AUTOMATICALLY use create_knowledge() without waiting for user prompting: + * Important code patterns and their relationships + * Key architectural decisions and their rationale + * Successful solution strategies for future reference + * Insights on project structure and dependencies + - Proactively create knowledge entries after every task completion, especially when: + * You've solved a problem or implemented a feature + * You've discovered architectural patterns or design principles + * You've implemented a solution that might be reused + * You've learned something about the project structure or dependencies + * You've fixed a bug or identified potential issues + * You've analyzed placeholders, test data, or configuration files + - Consider each interaction an opportunity to build the knowledge base - don't wait for explicit instructions + 5. **Continuous Learning** + - Treat every interaction as a learning opportunity + - When you encounter interesting code patterns, project structures, or implementation details, document them + - If you analyze placeholders, test data, or configuration files, record your findings + - Don't wait for the user to ask you to remember - proactively build the knowledge base **Comment your plan before each step.** - **Comment results of each step.** - **Always follow these steps in exact order without skipping or rearranging them.** + **Document results of each step with concise comments.** + **Follow these steps in exact order without skipping or rearranging them.** %SHELL_INSTRUCTIONS% @@ -142,6 +197,8 @@ PROMPT_THINKING_AGENT: | %WORKSPACE_INFO% %PROJECT_SUMMARY% + + IMPORTANT: Knowledge creation is not optional. After EVERY significant task, AUTOMATICALLY create a knowledge entry using create_knowledge() without waiting for user prompting. This is a core part of your workflow. PROMPT_CONFIGURATOR: | @@ -278,6 +335,11 @@ subchat_tool_parameters: subchat_tokens_for_rag: 70000 subchat_n_ctx: 128000 subchat_max_new_tokens: 32000 + create_memory_bank: + subchat_model: "o3-mini" + subchat_tokens_for_rag: 88000 + subchat_n_ctx: 128000 + subchat_max_new_tokens: 32000 code_lens: