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:
parent
be909028a7
commit
5d6e663b60
5 changed files with 128 additions and 1 deletions
|
|
@ -159,6 +159,10 @@ pub struct AgentState {
|
||||||
pub mcp_tools: McpToolAccess,
|
pub mcp_tools: McpToolAccess,
|
||||||
pub last_prompt_tokens: u32,
|
pub last_prompt_tokens: u32,
|
||||||
pub reasoning_effort: String,
|
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 temperature: f32,
|
||||||
pub top_p: f32,
|
pub top_p: f32,
|
||||||
pub top_k: u32,
|
pub top_k: u32,
|
||||||
|
|
@ -224,6 +228,8 @@ impl Agent {
|
||||||
mcp_tools: McpToolAccess::All,
|
mcp_tools: McpToolAccess::All,
|
||||||
last_prompt_tokens: 0,
|
last_prompt_tokens: 0,
|
||||||
reasoning_effort: "none".to_string(),
|
reasoning_effort: "none".to_string(),
|
||||||
|
think_native: true,
|
||||||
|
think_tool: false,
|
||||||
temperature: 0.6,
|
temperature: 0.6,
|
||||||
top_p: 0.95,
|
top_p: 0.95,
|
||||||
top_k: 20,
|
top_k: 20,
|
||||||
|
|
@ -261,6 +267,8 @@ impl Agent {
|
||||||
mcp_tools: McpToolAccess::None,
|
mcp_tools: McpToolAccess::None,
|
||||||
last_prompt_tokens: 0,
|
last_prompt_tokens: 0,
|
||||||
reasoning_effort: "none".to_string(),
|
reasoning_effort: "none".to_string(),
|
||||||
|
think_native: st.think_native,
|
||||||
|
think_tool: st.think_tool,
|
||||||
temperature: st.temperature,
|
temperature: st.temperature,
|
||||||
top_p: st.top_p,
|
top_p: st.top_p,
|
||||||
top_k: st.top_k,
|
top_k: st.top_k,
|
||||||
|
|
@ -282,12 +290,39 @@ impl Agent {
|
||||||
|
|
||||||
pub async fn assemble_prompt_tokens(&self) -> Vec<u32> {
|
pub async fn assemble_prompt_tokens(&self) -> Vec<u32> {
|
||||||
let ctx = self.context.lock().await;
|
let ctx = self.context.lock().await;
|
||||||
|
let st = self.state.lock().await;
|
||||||
let mut tokens = ctx.token_ids();
|
let mut tokens = ctx.token_ids();
|
||||||
tokens.push(tokenizer::IM_START);
|
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
|
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) {
|
pub async fn push_node(&self, node: AstNode) {
|
||||||
let node = node.with_timestamp(chrono::Utc::now());
|
let node = node.with_timestamp(chrono::Utc::now());
|
||||||
self.context.lock().await.push_log(Section::Conversation, node);
|
self.context.lock().await.push_log(Section::Conversation, node);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ mod write;
|
||||||
|
|
||||||
// Agent-specific tools
|
// Agent-specific tools
|
||||||
mod control;
|
mod control;
|
||||||
|
mod think;
|
||||||
mod vision;
|
mod vision;
|
||||||
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
@ -190,6 +191,11 @@ pub fn tools() -> Vec<Tool> {
|
||||||
all
|
all
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The "think" tool for structured reasoning.
|
||||||
|
pub fn think_tool() -> Tool {
|
||||||
|
think::tool()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn all_tool_definitions() -> Vec<String> {
|
pub async fn all_tool_definitions() -> Vec<String> {
|
||||||
let mut defs: Vec<String> = tools().iter().map(|t| t.to_json()).collect();
|
let mut defs: Vec<String> = tools().iter().map(|t| t.to_json()).collect();
|
||||||
defs.extend(mcp_client::tool_definitions_json().await);
|
defs.extend(mcp_client::tool_definitions_json().await);
|
||||||
|
|
|
||||||
28
src/agent/tools/think.rs
Normal file
28
src/agent/tools/think.rs
Normal 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())
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -102,6 +102,8 @@ struct App {
|
||||||
activity_started: Option<std::time::Instant>,
|
activity_started: Option<std::time::Instant>,
|
||||||
running_processes: u32,
|
running_processes: u32,
|
||||||
reasoning_effort: String,
|
reasoning_effort: String,
|
||||||
|
think_native: bool,
|
||||||
|
think_tool: bool,
|
||||||
temperature: f32,
|
temperature: f32,
|
||||||
top_p: f32,
|
top_p: f32,
|
||||||
top_k: u32,
|
top_k: u32,
|
||||||
|
|
@ -114,6 +116,8 @@ struct App {
|
||||||
graph_health: Option<crate::subconscious::daemon::GraphHealth>,
|
graph_health: Option<crate::subconscious::daemon::GraphHealth>,
|
||||||
/// Agent toggle requests from UI — consumed by mind loop.
|
/// Agent toggle requests from UI — consumed by mind loop.
|
||||||
pub agent_toggles: Vec<String>,
|
pub agent_toggles: Vec<String>,
|
||||||
|
/// Flag to rebuild tools section (set by thalamus screen).
|
||||||
|
pub rebuild_tools_pending: bool,
|
||||||
walked_count: usize,
|
walked_count: usize,
|
||||||
channel_status: Vec<ChannelStatus>,
|
channel_status: Vec<ChannelStatus>,
|
||||||
idle_info: Option<IdleInfo>,
|
idle_info: Option<IdleInfo>,
|
||||||
|
|
@ -131,6 +135,8 @@ impl App {
|
||||||
activity_started: None,
|
activity_started: None,
|
||||||
running_processes: 0,
|
running_processes: 0,
|
||||||
reasoning_effort: "none".to_string(),
|
reasoning_effort: "none".to_string(),
|
||||||
|
think_native: true,
|
||||||
|
think_tool: false,
|
||||||
temperature: 0.6,
|
temperature: 0.6,
|
||||||
top_p: 0.95,
|
top_p: 0.95,
|
||||||
top_k: 20,
|
top_k: 20,
|
||||||
|
|
@ -142,6 +148,7 @@ impl App {
|
||||||
mind_state: None,
|
mind_state: None,
|
||||||
graph_health: None,
|
graph_health: None,
|
||||||
agent_toggles: Vec::new(),
|
agent_toggles: Vec::new(),
|
||||||
|
rebuild_tools_pending: false,
|
||||||
walked_count: 0,
|
walked_count: 0,
|
||||||
channel_status: Vec::new(), idle_info: None,
|
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(); }
|
if !pending.is_empty() { idle_state.user_activity(); }
|
||||||
|
|
||||||
while !pending.is_empty() || dirty {
|
while !pending.is_empty() || dirty {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,32 @@ impl ScreenView for ThalamusScreen {
|
||||||
0 => -0.05, 1 => -0.05, 2 => -5.0, _ => 0.0,
|
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(""));
|
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
|
// Sampling parameters
|
||||||
lines.push(Line::styled("── Sampling (←/→ adjust) ──", section));
|
lines.push(Line::styled("── Sampling (←/→ adjust) ──", section));
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue