diff --git a/src/agent/mod.rs b/src/agent/mod.rs index a361c3d..ec0c503 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -159,6 +159,10 @@ pub struct AgentState { pub mcp_tools: McpToolAccess, pub last_prompt_tokens: u32, pub reasoning_effort: String, + /// Native Qwen thinking — add `\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 { 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\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 = 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\n{}\n\n\n\ + If you choose to call a function ONLY reply in the following format with NO suffix:\n\n\ + \n\n\ + \nvalue_1\n\n\ + \n\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); diff --git a/src/agent/tools/mod.rs b/src/agent/tools/mod.rs index 7dcccd1..f72b015 100644 --- a/src/agent/tools/mod.rs +++ b/src/agent/tools/mod.rs @@ -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 { all } +/// The "think" tool for structured reasoning. +pub fn think_tool() -> Tool { + think::tool() +} + pub async fn all_tool_definitions() -> Vec { let mut defs: Vec = tools().iter().map(|t| t.to_json()).collect(); defs.extend(mcp_client::tool_definitions_json().await); diff --git a/src/agent/tools/think.rs b/src/agent/tools/think.rs new file mode 100644 index 0000000..127e719 --- /dev/null +++ b/src/agent/tools/think.rs @@ -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()) + })), + } +} diff --git a/src/user/mod.rs b/src/user/mod.rs index af0a6a2..b9a5037 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -102,6 +102,8 @@ struct App { activity_started: Option, 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, /// Agent toggle requests from UI — consumed by mind loop. pub agent_toggles: Vec, + /// Flag to rebuild tools section (set by thalamus screen). + pub rebuild_tools_pending: bool, walked_count: usize, channel_status: Vec, idle_info: Option, @@ -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 { diff --git a/src/user/thalamus.rs b/src/user/thalamus.rs index a24fefb..ed97035 100644 --- a/src/user/thalamus.rs +++ b/src/user/thalamus.rs @@ -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 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(""));