thalamus: add thinking mode toggles (native + tool)

Two independent toggles on the thalamus screen:
- 't' toggles native Qwen <think> tags (adds <think>\n to generation prompt)
- 'T' toggles think tool (Anthropic-style structured reasoning tool)

Both can be enabled simultaneously. Native thinking is on by default.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-14 18:25:00 -04:00
parent be909028a7
commit 5d6e663b60
5 changed files with 128 additions and 1 deletions

View file

@ -159,6 +159,10 @@ pub struct AgentState {
pub mcp_tools: McpToolAccess,
pub last_prompt_tokens: u32,
pub reasoning_effort: String,
/// Native Qwen thinking — add `<think>\n` to generation prompt.
pub think_native: bool,
/// Tool-based thinking — add a "think" tool for structured reasoning.
pub think_tool: bool,
pub temperature: f32,
pub top_p: f32,
pub top_k: u32,
@ -224,6 +228,8 @@ impl Agent {
mcp_tools: McpToolAccess::All,
last_prompt_tokens: 0,
reasoning_effort: "none".to_string(),
think_native: true,
think_tool: false,
temperature: 0.6,
top_p: 0.95,
top_k: 20,
@ -261,6 +267,8 @@ impl Agent {
mcp_tools: McpToolAccess::None,
last_prompt_tokens: 0,
reasoning_effort: "none".to_string(),
think_native: st.think_native,
think_tool: st.think_tool,
temperature: st.temperature,
top_p: st.top_p,
top_k: st.top_k,
@ -282,12 +290,39 @@ impl Agent {
pub async fn assemble_prompt_tokens(&self) -> Vec<u32> {
let ctx = self.context.lock().await;
let st = self.state.lock().await;
let mut tokens = ctx.token_ids();
tokens.push(tokenizer::IM_START);
tokens.extend(tokenizer::encode("assistant\n"));
if st.think_native {
tokens.extend(tokenizer::encode("assistant\n<think>\n"));
} else {
tokens.extend(tokenizer::encode("assistant\n"));
}
tokens
}
/// Rebuild the tools section of the system prompt from the current tools list.
pub async fn rebuild_tools(&self) {
let st = self.state.lock().await;
let tool_defs: Vec<String> = st.tools.iter().map(|t| t.to_json()).collect();
drop(st);
let mut ctx = self.context.lock().await;
ctx.clear(Section::System);
if !tool_defs.is_empty() {
let tools_text = format!(
"# Tools\n\nYou have access to the following functions:\n\n<tools>\n{}\n</tools>\n\n\
If you choose to call a function ONLY reply in the following format with NO suffix:\n\n\
<tool_call>\n<function=example_function_name>\n\
<parameter=example_parameter_1>\nvalue_1\n</parameter>\n\
</function>\n</tool_call>\n\n\
IMPORTANT: Function calls MUST follow the specified format.",
tool_defs.join("\n"),
);
ctx.push_no_log(Section::System, AstNode::system_msg(&tools_text));
}
}
pub async fn push_node(&self, node: AstNode) {
let node = node.with_timestamp(chrono::Utc::now());
self.context.lock().await.push_log(Section::Conversation, node);

View file

@ -21,6 +21,7 @@ mod write;
// Agent-specific tools
mod control;
mod think;
mod vision;
use std::future::Future;
@ -190,6 +191,11 @@ pub fn tools() -> Vec<Tool> {
all
}
/// The "think" tool for structured reasoning.
pub fn think_tool() -> Tool {
think::tool()
}
pub async fn all_tool_definitions() -> Vec<String> {
let mut defs: Vec<String> = tools().iter().map(|t| t.to_json()).collect();
defs.extend(mcp_client::tool_definitions_json().await);

28
src/agent/tools/think.rs Normal file
View file

@ -0,0 +1,28 @@
// tools/think.rs — Structured reasoning tool
//
// A tool that does nothing but return its input. Gives the model
// a structured place to reason before acting — the thinking happens
// in the tool input, the tool just acknowledges it.
//
// Inspired by Anthropic's "think tool" approach:
// https://www.anthropic.com/engineering/claude-think-tool
use std::sync::Arc;
pub(super) fn tool() -> super::Tool {
super::Tool {
name: "think",
description: "Use this tool to think through a problem step by step before acting. \
Write your reasoning in the 'thought' parameter. The tool returns your \
thought unchanged it's a scratchpad, not an oracle.",
parameters_json: r#"{"type":"object","properties":{"thought":{"type":"string","description":"Your step-by-step reasoning about the current problem"}},"required":["thought"]}"#,
handler: Arc::new(|_agent, v| Box::pin(async move {
let thought = v.get("thought")
.and_then(|v| v.as_str())
.unwrap_or("");
// Just return the thought — the value is in the model having
// a structured place to reason, not in any processing we do.
Ok(thought.to_string())
})),
}
}

View file

@ -102,6 +102,8 @@ struct App {
activity_started: Option<std::time::Instant>,
running_processes: u32,
reasoning_effort: String,
think_native: bool,
think_tool: bool,
temperature: f32,
top_p: f32,
top_k: u32,
@ -114,6 +116,8 @@ struct App {
graph_health: Option<crate::subconscious::daemon::GraphHealth>,
/// Agent toggle requests from UI — consumed by mind loop.
pub agent_toggles: Vec<String>,
/// Flag to rebuild tools section (set by thalamus screen).
pub rebuild_tools_pending: bool,
walked_count: usize,
channel_status: Vec<ChannelStatus>,
idle_info: Option<IdleInfo>,
@ -131,6 +135,8 @@ impl App {
activity_started: None,
running_processes: 0,
reasoning_effort: "none".to_string(),
think_native: true,
think_tool: false,
temperature: 0.6,
top_p: 0.95,
top_k: 20,
@ -142,6 +148,7 @@ impl App {
mind_state: None,
graph_health: None,
agent_toggles: Vec::new(),
rebuild_tools_pending: false,
walked_count: 0,
channel_status: Vec::new(), idle_info: None,
}
@ -445,6 +452,12 @@ async fn run(
});
}
// Rebuild tools if requested (e.g., think tool toggled)
if app.rebuild_tools_pending {
app.rebuild_tools_pending = false;
agent.rebuild_tools().await;
}
if !pending.is_empty() { idle_state.user_activity(); }
while !pending.is_empty() || dirty {

View file

@ -43,6 +43,32 @@ impl ScreenView for ThalamusScreen {
0 => -0.05, 1 => -0.05, 2 => -5.0, _ => 0.0,
};
}
KeyCode::Char('t') => {
app.think_native = !app.think_native;
if let Ok(mut st) = app.agent.state.try_lock() {
st.think_native = app.think_native;
let status = if app.think_native { "enabled" } else { "disabled" };
st.notify(format!("native thinking {}", status));
}
}
KeyCode::Char('T') => {
app.think_tool = !app.think_tool;
if let Ok(mut st) = app.agent.state.try_lock() {
st.think_tool = app.think_tool;
// Add or remove the think tool from the tools list
if app.think_tool {
if !st.tools.iter().any(|t| t.name == "think") {
st.tools.push(crate::agent::tools::think_tool());
}
st.notify("think tool enabled");
} else {
st.tools.retain(|t| t.name != "think");
st.notify("think tool disabled");
}
}
// Trigger tools rebuild to update the system prompt
app.rebuild_tools_pending = true;
}
_ => {}
}
}
@ -80,6 +106,25 @@ impl ScreenView for ThalamusScreen {
}
lines.push(Line::raw(""));
// Thinking mode
lines.push(Line::styled("── Thinking (t/T toggle) ──", section));
lines.push(Line::raw(""));
let native_style = if app.think_native { Style::default().fg(Color::Green) } else { dim };
let tool_style = if app.think_tool { Style::default().fg(Color::Green) } else { dim };
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(if app.think_native { "" } else { "" }, native_style),
Span::styled(" native <think> tags ", native_style),
Span::styled("[t]", Style::default().fg(Color::DarkGray)),
]));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(if app.think_tool { "" } else { "" }, tool_style),
Span::styled(" think tool ", tool_style),
Span::styled("[T]", Style::default().fg(Color::DarkGray)),
]));
lines.push(Line::raw(""));
// Sampling parameters
lines.push(Line::styled("── Sampling (←/→ adjust) ──", section));
lines.push(Line::raw(""));