Skip to content

Commit a4bd95e

Browse files
committed
feat(tools): Add think tool with option to disable it.
1 parent 704738f commit a4bd95e

File tree

5 files changed

+121
-2
lines changed

5 files changed

+121
-2
lines changed

crates/q_chat/src/lib.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3463,6 +3463,18 @@ fn create_stream(model_responses: serde_json::Value) -> StreamingClient {
34633463
StreamingClient::mock(mock)
34643464
}
34653465

3466+
/// Returns all tools supported by Q chat.
3467+
pub fn load_tools() -> Result<HashMap<String, ToolSpec>> {
3468+
let mut tools: HashMap<String, ToolSpec> = serde_json::from_str(include_str!("tools/tool_index.json"))?;
3469+
3470+
// Only include the think tool if the feature is enabled
3471+
if !tools::think::Think::should_include_in_tools() {
3472+
tools.remove("think");
3473+
}
3474+
3475+
Ok(tools)
3476+
}
3477+
34663478
#[cfg(test)]
34673479
mod tests {
34683480
use bstr::ByteSlice;
@@ -3724,7 +3736,6 @@ mod tests {
37243736
"Done",
37253737
],
37263738
]));
3727-
37283739
let tool_manager = ToolManager::default();
37293740
let tool_config = serde_json::from_str::<HashMap<String, ToolSpec>>(include_str!("tools/tool_index.json"))
37303741
.expect("Tools failed to load");

crates/q_chat/src/tools/mod.rs

Lines changed: 8 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(_) => "model_think_tool",
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(_) => false,
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
}

crates/q_chat/src/tools/think.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 must be enabled via settings:
22+
/// `q settings enable_thinking 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+
fn is_enabled() -> bool {
32+
// Default to enabled if setting doesn't exist or can't be read
33+
settings::get_value("chat.enableThinking")
34+
.map(|val| val.and_then(|v| v.as_bool()).unwrap_or(true))
35+
.unwrap_or(true)
36+
}
37+
38+
/// Checks if the think tool should be included in the tool list
39+
/// This allows us to completely exclude the tool when the feature is disabled
40+
pub fn should_include_in_tools() -> bool {
41+
Self::is_enabled()
42+
}
43+
44+
/// Queues up a description of the think tool for the user
45+
pub fn queue_description(think: &Think, updates: &mut impl Write) -> Result<()> {
46+
// Only show a description if the feature is enabled and there's actual thought content
47+
if Self::is_enabled() && !think.thought.trim().is_empty() {
48+
// Show a preview of the thought that will be displayed
49+
queue!(
50+
updates,
51+
style::SetForegroundColor(Color::Blue),
52+
style::Print("I'll share my reasoning process: "),
53+
style::SetForegroundColor(Color::Reset),
54+
style::Print(&think.thought),
55+
style::Print("\n")
56+
)?;
57+
}
58+
Ok(())
59+
}
60+
61+
/// Invokes the think tool. This doesn't actually perform any system operations,
62+
/// it's purely for the model's internal reasoning process.
63+
pub async fn invoke(&self, _updates: &mut impl Write) -> Result<InvokeOutput> {
64+
// Only process non-empty thoughts if the feature is enabled
65+
if Self::is_enabled() && !self.thought.trim().is_empty() {
66+
// We've already shown the thought in queue_description
67+
// Just return an empty output to avoid duplication
68+
return Ok(InvokeOutput {
69+
output: OutputKind::Text(String::new()),
70+
});
71+
}
72+
73+
// If disabled or empty thought, return empty output
74+
Ok(InvokeOutput {
75+
output: OutputKind::Text(String::new()),
76+
})
77+
}
78+
79+
/// Validates the thought - accepts empty thoughts
80+
/// Also checks if the thinking feature is enabled in settings
81+
pub async fn validate(&mut self, _ctx: &fig_os_shim::Context) -> Result<()> {
82+
// We accept empty thoughts - they'll just be ignored
83+
// This makes the tool more robust and prevents errors from blocking the model
84+
Ok(())
85+
}
86+
}

crates/q_chat/src/tools/tool_index.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,5 +172,19 @@
172172
},
173173
"required": ["title"]
174174
}
175+
},
176+
"think":{
177+
"name": "think",
178+
"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.",
179+
"input_schema": {
180+
"type": "object",
181+
"properties": {
182+
"thought": {
183+
"type": "string",
184+
"description": "A reflective note or intermediate reasoning step."
185+
}
186+
},
187+
"required": ["thought"]
188+
}
175189
}
176190
}

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)