Skip to content

Commit b68040a

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

File tree

5 files changed

+118
-2
lines changed

5 files changed

+118
-2
lines changed

crates/q_cli/src/cli/chat/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3071,7 +3071,14 @@ fn create_stream(model_responses: serde_json::Value) -> StreamingClient {
30713071

30723072
/// Returns all tools supported by Q chat.
30733073
pub fn load_tools() -> Result<HashMap<String, ToolSpec>> {
3074-
Ok(serde_json::from_str(include_str!("tools/tool_index.json"))?)
3074+
let mut tools: HashMap<String, ToolSpec> = serde_json::from_str(include_str!("tools/tool_index.json"))?;
3075+
3076+
// Only include the think tool if the feature is enabled
3077+
if !tools::think::Think::should_include_in_tools() {
3078+
tools.remove("think");
3079+
}
3080+
3081+
Ok(tools)
30753082
}
30763083

30773084
#[cfg(test)]

crates/q_cli/src/cli/chat/tools/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ pub mod execute_bash;
22
pub mod fs_read;
33
pub mod fs_write;
44
pub mod gh_issue;
5+
pub mod think;
56
pub mod use_aws;
6-
77
use std::collections::HashMap;
88
use std::io::Write;
99
use std::path::{
@@ -24,6 +24,7 @@ use fs_read::FsRead;
2424
use fs_write::FsWrite;
2525
use gh_issue::GhIssue;
2626
use serde::Deserialize;
27+
use think::Think;
2728
use use_aws::UseAws;
2829

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

4648
impl Tool {
@@ -52,6 +54,7 @@ impl Tool {
5254
Tool::ExecuteBash(_) => "execute_bash",
5355
Tool::UseAws(_) => "use_aws",
5456
Tool::GhIssue(_) => "gh_issue",
57+
Tool::Think(_) => "model_think_tool",
5558
}
5659
}
5760

@@ -63,6 +66,7 @@ impl Tool {
6366
Tool::ExecuteBash(execute_bash) => execute_bash.requires_acceptance(),
6467
Tool::UseAws(use_aws) => use_aws.requires_acceptance(),
6568
Tool::GhIssue(_) => false,
69+
Tool::Think(_) => false,
6670
}
6771
}
6872

@@ -74,6 +78,7 @@ impl Tool {
7478
Tool::ExecuteBash(execute_bash) => execute_bash.invoke(updates).await,
7579
Tool::UseAws(use_aws) => use_aws.invoke(context, updates).await,
7680
Tool::GhIssue(gh_issue) => gh_issue.invoke(updates).await,
81+
Tool::Think(think) => think.invoke(updates).await,
7782
}
7883
}
7984

@@ -85,6 +90,7 @@ impl Tool {
8590
Tool::ExecuteBash(execute_bash) => execute_bash.queue_description(updates),
8691
Tool::UseAws(use_aws) => use_aws.queue_description(updates),
8792
Tool::GhIssue(gh_issue) => gh_issue.queue_description(updates),
93+
Tool::Think(think) => Think::queue_description(think, updates),
8894
}
8995
}
9096

@@ -96,6 +102,7 @@ impl Tool {
96102
Tool::ExecuteBash(execute_bash) => execute_bash.validate(ctx).await,
97103
Tool::UseAws(use_aws) => use_aws.validate(ctx).await,
98104
Tool::GhIssue(gh_issue) => gh_issue.validate(ctx).await,
105+
Tool::Think(think) => think.validate(ctx).await,
99106
}
100107
}
101108
}
@@ -118,6 +125,7 @@ impl TryFrom<AssistantToolUse> for Tool {
118125
"execute_bash" => Self::ExecuteBash(serde_json::from_value::<ExecuteBash>(value.args).map_err(map_err)?),
119126
"use_aws" => Self::UseAws(serde_json::from_value::<UseAws>(value.args).map_err(map_err)?),
120127
"report_issue" => Self::GhIssue(serde_json::from_value::<GhIssue>(value.args).map_err(map_err)?),
128+
"think" => Self::Think(serde_json::from_value::<Think>(value.args).map_err(map_err)?),
121129
unknown => {
122130
return Err(ToolUseResult {
123131
tool_use_id: value.id,
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_cli/src/cli/chat/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)