diff --git a/crates/q_chat/src/lib.rs b/crates/q_chat/src/lib.rs index a0523d0c73..21f12823d9 100644 --- a/crates/q_chat/src/lib.rs +++ b/crates/q_chat/src/lib.rs @@ -3724,7 +3724,6 @@ mod tests { "Done", ], ])); - let tool_manager = ToolManager::default(); let tool_config = serde_json::from_str::>(include_str!("tools/tool_index.json")) .expect("Tools failed to load"); diff --git a/crates/q_chat/src/tool_manager.rs b/crates/q_chat/src/tool_manager.rs index 4245cd1009..db5457072a 100644 --- a/crates/q_chat/src/tool_manager.rs +++ b/crates/q_chat/src/tool_manager.rs @@ -496,7 +496,12 @@ impl ToolManager { let tx = self.loading_status_sender.take(); let display_task = self.loading_display_task.take(); let tool_specs = { - let tool_specs = serde_json::from_str::>(include_str!("tools/tool_index.json"))?; + let mut tool_specs = + serde_json::from_str::>(include_str!("tools/tool_index.json"))?; + if !crate::tools::think::Think::is_enabled() { + tool_specs.remove("q_think_tool"); + } + Arc::new(Mutex::new(tool_specs)) }; let conversation_id = self.conversation_id.clone(); @@ -671,6 +676,9 @@ impl ToolManager { "execute_bash" => Tool::ExecuteBash(serde_json::from_value::(value.args).map_err(map_err)?), "use_aws" => Tool::UseAws(serde_json::from_value::(value.args).map_err(map_err)?), "report_issue" => Tool::GhIssue(serde_json::from_value::(value.args).map_err(map_err)?), + "q_think_tool" => { + Tool::Think(serde_json::from_value::(value.args).map_err(map_err)?) + }, // Note that this name is namespaced with server_name{DELIMITER}tool_name name => { let name = self.tn_map.get(name).map_or(name, String::as_str); diff --git a/crates/q_chat/src/tools/mod.rs b/crates/q_chat/src/tools/mod.rs index 279586736b..4fe7a94c87 100644 --- a/crates/q_chat/src/tools/mod.rs +++ b/crates/q_chat/src/tools/mod.rs @@ -3,8 +3,8 @@ pub mod execute_bash; pub mod fs_read; pub mod fs_write; pub mod gh_issue; +pub mod think; pub mod use_aws; - use std::collections::HashMap; use std::io::Write; use std::path::{ @@ -28,6 +28,7 @@ use serde::{ Deserialize, Serialize, }; +use think::Think; use use_aws::UseAws; use super::consts::MAX_TOOL_RESPONSE_SIZE; @@ -41,6 +42,7 @@ pub enum Tool { UseAws(UseAws), Custom(CustomTool), GhIssue(GhIssue), + Think(Think), } impl Tool { @@ -53,6 +55,7 @@ impl Tool { Tool::UseAws(_) => "use_aws", Tool::Custom(custom_tool) => &custom_tool.name, Tool::GhIssue(_) => "gh_issue", + Tool::Think(_) => "q_think_tool (beta)", } .to_owned() } @@ -66,6 +69,7 @@ impl Tool { Tool::UseAws(use_aws) => use_aws.requires_acceptance(), Tool::Custom(_) => true, Tool::GhIssue(_) => false, + Tool::Think(_) => true, } } @@ -78,6 +82,7 @@ impl Tool { Tool::UseAws(use_aws) => use_aws.invoke(context, updates).await, Tool::Custom(custom_tool) => custom_tool.invoke(context, updates).await, Tool::GhIssue(gh_issue) => gh_issue.invoke(updates).await, + Tool::Think(think) => think.invoke(updates).await, } } @@ -90,6 +95,7 @@ impl Tool { Tool::UseAws(use_aws) => use_aws.queue_description(updates), Tool::Custom(custom_tool) => custom_tool.queue_description(updates), Tool::GhIssue(gh_issue) => gh_issue.queue_description(updates), + Tool::Think(think) => Think::queue_description(think, updates), } } @@ -102,6 +108,7 @@ impl Tool { Tool::UseAws(use_aws) => use_aws.validate(ctx).await, Tool::Custom(custom_tool) => custom_tool.validate(ctx).await, Tool::GhIssue(gh_issue) => gh_issue.validate(ctx).await, + Tool::Think(think) => think.validate(ctx).await, } } } @@ -175,6 +182,7 @@ impl ToolPermissions { "execute_bash" => "trust read-only commands".dark_grey(), "use_aws" => "trust read-only commands".dark_grey(), "report_issue" => "trusted".dark_green().bold(), + "q_think_tool" => "trusted (beta)".dark_green().bold(), _ => "not trusted".dark_grey(), }; diff --git a/crates/q_chat/src/tools/think.rs b/crates/q_chat/src/tools/think.rs new file mode 100644 index 0000000000..0ef1152d22 --- /dev/null +++ b/crates/q_chat/src/tools/think.rs @@ -0,0 +1,70 @@ +use std::io::Write; + +use crossterm::queue; +use crossterm::style::{ + self, + Color, +}; +use eyre::Result; +use fig_settings::settings; +use serde::Deserialize; + +use super::{ + InvokeOutput, + OutputKind, +}; + +/// The Think tool allows the model to reason through complex problems during response generation. +/// It provides a dedicated space for the model to process information from tool call results, +/// navigate complex decision trees, and improve the quality of responses in multi-step scenarios. +/// +/// This is a beta feature that can be enabled/disabled via settings: +/// `q settings chat.enableThinking true` +#[derive(Debug, Clone, Deserialize)] +pub struct Think { + /// The thought content that the model wants to process + pub thought: String, +} + +impl Think { + /// Checks if the thinking feature is enabled in settings + pub fn is_enabled() -> bool { + // Default to enabled if setting doesn't exist or can't be read + settings::get_bool_or("chat.enableThinking", true) + } + + /// Queues up a description of the think tool for the user + pub fn queue_description(think: &Think, updates: &mut impl Write) -> Result<()> { + // Only show a description if there's actual thought content + if !think.thought.trim().is_empty() { + // Show a preview of the thought that will be displayed + queue!( + updates, + style::SetForegroundColor(Color::Blue), + style::Print("I'll share my reasoning process: "), + style::SetForegroundColor(Color::Reset), + style::Print(&think.thought), + style::Print("\n") + )?; + } + Ok(()) + } + + /// Invokes the think tool. This doesn't actually perform any system operations, + /// it's purely for the model's internal reasoning process. + pub async fn invoke(&self, _updates: &mut impl Write) -> Result { + // The think tool always returns an empty output because: + // 1. When enabled with content: We've already shown the thought in queue_description + // 2. When disabled or empty: Nothing should be shown + Ok(InvokeOutput { + output: OutputKind::Text(String::new()), + }) + } + + /// Validates the thought - accepts empty thoughts + pub async fn validate(&mut self, _ctx: &fig_os_shim::Context) -> Result<()> { + // We accept empty thoughts - they'll just be ignored + // This makes the tool more robust and prevents errors from blocking the model + Ok(()) + } +} diff --git a/crates/q_chat/src/tools/tool_index.json b/crates/q_chat/src/tools/tool_index.json index 397d856cfa..cc0f4dfd46 100644 --- a/crates/q_chat/src/tools/tool_index.json +++ b/crates/q_chat/src/tools/tool_index.json @@ -172,5 +172,19 @@ }, "required": ["title"] } + }, + "q_think_tool":{ + "name": "q_think_tool", + "description": "The Think Tool is an internal reasoning mechanism enabling the model to systematically approach complex tasks by logically breaking them down before responding or acting; use it specifically for multi-step problems requiring step-by-step dependencies, reasoning through multiple constraints, synthesizing results from previous tool calls, planning intricate sequences of actions, troubleshooting complex errors, or making decisions involving multiple trade-offs. Avoid using it for straightforward tasks, basic information retrieval, summaries, always clearly define the reasoning challenge, structure thoughts explicitly, consider multiple perspectives, and summarize key insights before important decisions or complex tool interactions.", + "input_schema": { + "type": "object", + "properties": { + "thought": { + "type": "string", + "description": "A reflective note or intermediate reasoning step." + } + }, + "required": ["thought"] + } } } diff --git a/crates/q_cli/src/cli/settings.rs b/crates/q_cli/src/cli/settings.rs index 388051bdef..e99ff7b286 100644 --- a/crates/q_cli/src/cli/settings.rs +++ b/crates/q_cli/src/cli/settings.rs @@ -106,6 +106,7 @@ impl SettingsArgs { Ok(ExitCode::SUCCESS) }, + None => match &self.key { Some(key) => match (&self.value, self.delete) { (None, false) => match fig_settings::settings::get_value(key)? {