diff --git a/src/agent/context.rs b/src/agent/context.rs index c25cdbe..bdaa3a5 100644 --- a/src/agent/context.rs +++ b/src/agent/context.rs @@ -22,6 +22,8 @@ pub enum ConversationEntry { Memory { key: String, message: Message, score: Option }, /// DMN heartbeat/autonomous prompt — evicted aggressively during compaction. Dmn(Message), + /// Model thinking/reasoning — not sent to the API, 0 tokens for budgeting. + Thinking(String), /// Debug/status log line — written to conversation log for tracing, /// skipped on read-back. Log(String), @@ -291,8 +293,12 @@ impl Serialize for ConversationEntry { } map.end() } + Self::Thinking(text) => { + let mut map = s.serialize_map(Some(1))?; + map.serialize_entry("thinking", text)?; + map.end() + } Self::Log(text) => { - use serde::ser::SerializeMap; let mut map = s.serialize_map(Some(1))?; map.serialize_entry("log", text)?; map.end() @@ -304,7 +310,10 @@ impl Serialize for ConversationEntry { impl<'de> Deserialize<'de> for ConversationEntry { fn deserialize>(d: D) -> Result { let mut json: serde_json::Value = serde_json::Value::deserialize(d)?; - // Log entries — skip on read-back + if json.get("thinking").is_some() { + let text = json["thinking"].as_str().unwrap_or("").to_string(); + return Ok(Self::Thinking(text)); + } if json.get("log").is_some() { let text = json["log"].as_str().unwrap_or("").to_string(); return Ok(Self::Log(text)); @@ -330,10 +339,14 @@ impl ConversationEntry { match self { Self::System(m) | Self::Message(m) | Self::Dmn(m) => m, Self::Memory { message, .. } => message, - Self::Log(_) => panic!("Log entries have no API message"), + Self::Thinking(_) | Self::Log(_) => panic!("Thinking/Log entries have no API message"), } } + pub fn is_thinking(&self) -> bool { + matches!(self, Self::Thinking(_)) + } + pub fn is_memory(&self) -> bool { matches!(self, Self::Memory { .. }) } @@ -352,6 +365,12 @@ impl ConversationEntry { match self { Self::System(_) => "system: [system prompt]".to_string(), Self::Dmn(_) => "dmn: [heartbeat]".to_string(), + Self::Thinking(text) => { + let preview: String = text.chars().take(60).collect(); + let preview = preview.replace('\n', " "); + if text.len() > 60 { format!("thinking: {}...", preview) } + else { format!("thinking: {}", preview) } + } Self::Log(text) => { let preview: String = text.chars().take(60).collect(); format!("log: {}", preview.replace('\n', " ")) @@ -390,17 +409,17 @@ impl ConversationEntry { match self { Self::System(m) | Self::Message(m) | Self::Dmn(m) => m, Self::Memory { message, .. } => message, - Self::Log(_) => panic!("Log entries have no message"), + Self::Thinking(_) | Self::Log(_) => panic!("Thinking/Log entries have no message"), } } /// Get a mutable reference to the inner message. - /// Panics on Log entries. + /// Panics on Thinking/Log entries. pub fn message_mut(&mut self) -> &mut Message { match self { Self::System(m) | Self::Message(m) | Self::Dmn(m) => m, Self::Memory { message, .. } => message, - Self::Log(_) => panic!("Log entries have no message"), + Self::Thinking(_) | Self::Log(_) => panic!("Thinking/Log entries have no message"), } } } diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 8abea91..4d8f41c 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -305,7 +305,7 @@ impl Agent { } // Conversation entries msgs.extend(self.context.conversation.entries().iter() - .filter(|e| !e.entry.is_log()) + .filter(|e| !e.entry.is_log() && !e.entry.is_thinking()) .map(|e| e.entry.api_message().clone())); msgs } @@ -324,7 +324,7 @@ impl Agent { eprintln!("warning: failed to log entry: {:#}", e); } } - let tokens = if entry.is_log() { 0 } else { + let tokens = if entry.is_log() || entry.is_thinking() { 0 } else { context::msg_token_count(&self.tokenizer, entry.api_message()) }; self.context.conversation.push(ContextEntry { diff --git a/src/user/widgets.rs b/src/user/widgets.rs index 0050924..3c705b9 100644 --- a/src/user/widgets.rs +++ b/src/user/widgets.rs @@ -8,7 +8,7 @@ use ratatui::{ Frame, crossterm::event::KeyCode, }; -use crate::agent::context::ContextSection; +use crate::agent::context::{ContextSection, ConversationEntry}; /// UI-only tree node for the section tree display. /// Built from ContextSection data; not used for budgeting. @@ -26,10 +26,10 @@ pub struct SectionView { /// Each entry becomes a child with label + expandable content. pub fn section_to_view(section: &ContextSection) -> SectionView { let children: Vec = section.entries().iter().map(|ce| { - let content = if ce.entry.is_log() { - String::new() - } else { - ce.entry.message().content_text().to_string() + let content = match &ce.entry { + ConversationEntry::Log(_) => String::new(), + ConversationEntry::Thinking(text) => text.clone(), + _ => ce.entry.message().content_text().to_string(), }; SectionView { name: ce.entry.label(),