Skip to content

Commit 8099446

Browse files
feat(tools): Add think tool with option to disable it. (#1346)
1 parent f93ff7a commit 8099446

File tree

6 files changed

+103
-3
lines changed

6 files changed

+103
-3
lines changed

crates/q_chat/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3719,7 +3719,6 @@ mod tests {
37193719
"Done",
37203720
],
37213721
]));
3722-
37233722
let tool_manager = ToolManager::default();
37243723
let tool_config = serde_json::from_str::<HashMap<String, ToolSpec>>(include_str!("tools/tool_index.json"))
37253724
.expect("Tools failed to load");

crates/q_chat/src/tool_manager.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,12 @@ impl ToolManager {
507507
let tx = self.loading_status_sender.take();
508508
let display_task = self.loading_display_task.take();
509509
let tool_specs = {
510-
let tool_specs = serde_json::from_str::<HashMap<String, ToolSpec>>(include_str!("tools/tool_index.json"))?;
510+
let mut tool_specs =
511+
serde_json::from_str::<HashMap<String, ToolSpec>>(include_str!("tools/tool_index.json"))?;
512+
if !crate::tools::think::Think::is_enabled() {
513+
tool_specs.remove("q_think_tool");
514+
}
515+
511516
Arc::new(Mutex::new(tool_specs))
512517
};
513518
let conversation_id = self.conversation_id.clone();
@@ -684,6 +689,9 @@ impl ToolManager {
684689
"execute_bash" => Tool::ExecuteBash(serde_json::from_value::<ExecuteBash>(value.args).map_err(map_err)?),
685690
"use_aws" => Tool::UseAws(serde_json::from_value::<UseAws>(value.args).map_err(map_err)?),
686691
"report_issue" => Tool::GhIssue(serde_json::from_value::<GhIssue>(value.args).map_err(map_err)?),
692+
"q_think_tool" => {
693+
Tool::Think(serde_json::from_value::<crate::tools::think::Think>(value.args).map_err(map_err)?)
694+
},
687695
// Note that this name is namespaced with server_name{DELIMITER}tool_name
688696
name => {
689697
let name = self.tn_map.get(name).map_or(name, String::as_str);

crates/q_chat/src/tools/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ pub mod execute_bash;
33
pub mod fs_read;
44
pub mod fs_write;
55
pub mod gh_issue;
6+
pub mod think;
67
pub mod use_aws;
7-
88
use std::collections::HashMap;
99
use std::io::Write;
1010
use std::path::{
@@ -28,6 +28,7 @@ use serde::{
2828
Deserialize,
2929
Serialize,
3030
};
31+
use think::Think;
3132
use use_aws::UseAws;
3233

3334
use super::consts::MAX_TOOL_RESPONSE_SIZE;
@@ -41,6 +42,7 @@ pub enum Tool {
4142
UseAws(UseAws),
4243
Custom(CustomTool),
4344
GhIssue(GhIssue),
45+
Think(Think),
4446
}
4547

4648
impl Tool {
@@ -53,6 +55,7 @@ impl Tool {
5355
Tool::UseAws(_) => "use_aws",
5456
Tool::Custom(custom_tool) => &custom_tool.name,
5557
Tool::GhIssue(_) => "gh_issue",
58+
Tool::Think(_) => "q_think_tool (beta)",
5659
}
5760
.to_owned()
5861
}
@@ -66,6 +69,7 @@ impl Tool {
6669
Tool::UseAws(use_aws) => use_aws.requires_acceptance(),
6770
Tool::Custom(_) => true,
6871
Tool::GhIssue(_) => false,
72+
Tool::Think(_) => true,
6973
}
7074
}
7175

@@ -78,6 +82,7 @@ impl Tool {
7882
Tool::UseAws(use_aws) => use_aws.invoke(context, updates).await,
7983
Tool::Custom(custom_tool) => custom_tool.invoke(context, updates).await,
8084
Tool::GhIssue(gh_issue) => gh_issue.invoke(updates).await,
85+
Tool::Think(think) => think.invoke(updates).await,
8186
}
8287
}
8388

@@ -90,6 +95,7 @@ impl Tool {
9095
Tool::UseAws(use_aws) => use_aws.queue_description(updates),
9196
Tool::Custom(custom_tool) => custom_tool.queue_description(updates),
9297
Tool::GhIssue(gh_issue) => gh_issue.queue_description(updates),
98+
Tool::Think(think) => Think::queue_description(think, updates),
9399
}
94100
}
95101

@@ -102,6 +108,7 @@ impl Tool {
102108
Tool::UseAws(use_aws) => use_aws.validate(ctx).await,
103109
Tool::Custom(custom_tool) => custom_tool.validate(ctx).await,
104110
Tool::GhIssue(gh_issue) => gh_issue.validate(ctx).await,
111+
Tool::Think(think) => think.validate(ctx).await,
105112
}
106113
}
107114
}
@@ -175,6 +182,7 @@ impl ToolPermissions {
175182
"execute_bash" => "trust read-only commands".dark_grey(),
176183
"use_aws" => "trust read-only commands".dark_grey(),
177184
"report_issue" => "trusted".dark_green().bold(),
185+
"q_think_tool" => "trusted (beta)".dark_green().bold(),
178186
_ => "not trusted".dark_grey(),
179187
};
180188

crates/q_chat/src/tools/think.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use std::io::Write;
2+
3+
use crossterm::queue;
4+
use crossterm::style::{
5+
self,
6+
Color,
7+
};
8+
use eyre::Result;
9+
use fig_settings::settings;
10+
use serde::Deserialize;
11+
12+
use super::{
13+
InvokeOutput,
14+
OutputKind,
15+
};
16+
17+
/// The Think tool allows the model to reason through complex problems during response generation.
18+
/// It provides a dedicated space for the model to process information from tool call results,
19+
/// navigate complex decision trees, and improve the quality of responses in multi-step scenarios.
20+
///
21+
/// This is a beta feature that can be enabled/disabled via settings:
22+
/// `q settings chat.enableThinking true`
23+
#[derive(Debug, Clone, Deserialize)]
24+
pub struct Think {
25+
/// The thought content that the model wants to process
26+
pub thought: String,
27+
}
28+
29+
impl Think {
30+
/// Checks if the thinking feature is enabled in settings
31+
pub fn is_enabled() -> bool {
32+
// Default to enabled if setting doesn't exist or can't be read
33+
settings::get_bool_or("chat.enableThinking", true)
34+
}
35+
36+
/// Queues up a description of the think tool for the user
37+
pub fn queue_description(think: &Think, updates: &mut impl Write) -> Result<()> {
38+
// Only show a description if there's actual thought content
39+
if !think.thought.trim().is_empty() {
40+
// Show a preview of the thought that will be displayed
41+
queue!(
42+
updates,
43+
style::SetForegroundColor(Color::Blue),
44+
style::Print("I'll share my reasoning process: "),
45+
style::SetForegroundColor(Color::Reset),
46+
style::Print(&think.thought),
47+
style::Print("\n")
48+
)?;
49+
}
50+
Ok(())
51+
}
52+
53+
/// Invokes the think tool. This doesn't actually perform any system operations,
54+
/// it's purely for the model's internal reasoning process.
55+
pub async fn invoke(&self, _updates: &mut impl Write) -> Result<InvokeOutput> {
56+
// The think tool always returns an empty output because:
57+
// 1. When enabled with content: We've already shown the thought in queue_description
58+
// 2. When disabled or empty: Nothing should be shown
59+
Ok(InvokeOutput {
60+
output: OutputKind::Text(String::new()),
61+
})
62+
}
63+
64+
/// Validates the thought - accepts empty thoughts
65+
pub async fn validate(&mut self, _ctx: &fig_os_shim::Context) -> Result<()> {
66+
// We accept empty thoughts - they'll just be ignored
67+
// This makes the tool more robust and prevents errors from blocking the model
68+
Ok(())
69+
}
70+
}

crates/q_chat/src/tools/tool_index.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,5 +176,19 @@
176176
},
177177
"required": ["title"]
178178
}
179+
},
180+
"q_think_tool":{
181+
"name": "q_think_tool",
182+
"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.",
183+
"input_schema": {
184+
"type": "object",
185+
"properties": {
186+
"thought": {
187+
"type": "string",
188+
"description": "A reflective note or intermediate reasoning step."
189+
}
190+
},
191+
"required": ["thought"]
192+
}
179193
}
180194
}

crates/q_cli/src/cli/settings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ impl SettingsArgs {
106106

107107
Ok(ExitCode::SUCCESS)
108108
},
109+
109110
None => match &self.key {
110111
Some(key) => match (&self.value, self.delete) {
111112
(None, false) => match fig_settings::settings::get_value(key)? {

0 commit comments

Comments
 (0)