ultrafast_mcp_sequential_thinking/thinking/
mod.rs

1//! # Thinking Module
2//!
3//! Core thinking functionality for the sequential thinking MCP server and client.
4//!
5//! This module provides the fundamental types and logic for handling sequential
6//! thinking processes, including thought data structures, processing logic,
7//! and the main thinking engine.
8
9pub mod client;
10pub mod error;
11pub mod server;
12
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use uuid::Uuid;
16
17/// Core data structure for a single thought in the sequential thinking process
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct ThoughtData {
20    /// The actual thought content
21    pub thought: String,
22    /// Current thought number in the sequence
23    pub thought_number: u32,
24    /// Estimated total number of thoughts needed
25    pub total_thoughts: u32,
26    /// Whether another thought step is needed
27    pub next_thought_needed: bool,
28    /// Whether this thought revises previous thinking
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub is_revision: Option<bool>,
31    /// Which thought is being reconsidered (if this is a revision)
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub revises_thought: Option<u32>,
34    /// Branching point thought number (if this is a branch)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub branch_from_thought: Option<u32>,
37    /// Branch identifier (if this is a branch)
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub branch_id: Option<String>,
40    /// Whether more thoughts are needed (if reaching end but realizing more needed)
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub needs_more_thoughts: Option<bool>,
43    /// Timestamp when this thought was created
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
46    /// Metadata associated with this thought
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub metadata: Option<HashMap<String, serde_json::Value>>,
49}
50
51impl Default for ThoughtData {
52    fn default() -> Self {
53        Self {
54            thought: String::new(),
55            thought_number: 1,
56            total_thoughts: 1,
57            next_thought_needed: true,
58            is_revision: None,
59            revises_thought: None,
60            branch_from_thought: None,
61            branch_id: None,
62            needs_more_thoughts: None,
63            timestamp: Some(chrono::Utc::now()),
64            metadata: None,
65        }
66    }
67}
68
69impl ThoughtData {
70    /// Create a new thought with basic information
71    pub fn new(thought: String, thought_number: u32, total_thoughts: u32) -> Self {
72        Self {
73            thought,
74            thought_number,
75            total_thoughts,
76            next_thought_needed: true,
77            is_revision: None,
78            revises_thought: None,
79            branch_from_thought: None,
80            branch_id: None,
81            needs_more_thoughts: None,
82            timestamp: Some(chrono::Utc::now()),
83            metadata: None,
84        }
85    }
86
87    /// Create a revision thought
88    pub fn revision(thought: String, thought_number: u32, revises_thought: u32) -> Self {
89        Self {
90            thought,
91            thought_number,
92            total_thoughts: thought_number,
93            next_thought_needed: true,
94            is_revision: Some(true),
95            revises_thought: Some(revises_thought),
96            branch_from_thought: None,
97            branch_id: None,
98            needs_more_thoughts: None,
99            timestamp: Some(chrono::Utc::now()),
100            metadata: None,
101        }
102    }
103
104    /// Create a branch thought
105    pub fn branch(
106        thought: String,
107        thought_number: u32,
108        branch_from_thought: u32,
109        branch_id: String,
110    ) -> Self {
111        Self {
112            thought,
113            thought_number,
114            total_thoughts: thought_number,
115            next_thought_needed: true,
116            is_revision: None,
117            revises_thought: None,
118            branch_from_thought: Some(branch_from_thought),
119            branch_id: Some(branch_id),
120            needs_more_thoughts: None,
121            timestamp: Some(chrono::Utc::now()),
122            metadata: None,
123        }
124    }
125
126    /// Check if this thought is a revision
127    pub fn is_revision(&self) -> bool {
128        self.is_revision.unwrap_or(false)
129    }
130
131    /// Check if this thought is a branch
132    pub fn is_branch(&self) -> bool {
133        self.branch_from_thought.is_some()
134    }
135
136    /// Get the branch ID if this is a branch
137    pub fn get_branch_id(&self) -> Option<&str> {
138        self.branch_id.as_deref()
139    }
140
141    /// Get the thought being revised if this is a revision
142    pub fn get_revised_thought(&self) -> Option<u32> {
143        self.revises_thought
144    }
145
146    /// Add metadata to this thought
147    pub fn with_metadata(mut self, key: String, value: serde_json::Value) -> Self {
148        if self.metadata.is_none() {
149            self.metadata = Some(HashMap::new());
150        }
151        if let Some(ref mut metadata) = self.metadata {
152            metadata.insert(key, value);
153        }
154        self
155    }
156
157    /// Validate the thought data
158    pub fn validate(&self) -> Result<(), String> {
159        if self.thought.is_empty() {
160            return Err("Thought content cannot be empty".to_string());
161        }
162        if self.thought_number == 0 {
163            return Err("Thought number must be greater than 0".to_string());
164        }
165        if self.total_thoughts == 0 {
166            return Err("Total thoughts must be greater than 0".to_string());
167        }
168        if self.thought_number > self.total_thoughts {
169            // This is actually allowed for dynamic adjustment
170        }
171        if self.is_revision() && self.revises_thought.is_none() {
172            return Err(
173                "Revision thoughts must specify which thought is being revised".to_string(),
174            );
175        }
176        if self.is_branch() && self.branch_id.is_none() {
177            return Err("Branch thoughts must have a branch ID".to_string());
178        }
179        Ok(())
180    }
181}
182
183/// A collection of thoughts that form a branch
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ThoughtBranch {
186    /// Branch identifier
187    pub branch_id: String,
188    /// Parent thought number
189    pub parent_thought: u32,
190    /// Thoughts in this branch
191    pub thoughts: Vec<ThoughtData>,
192    /// Branch metadata
193    pub metadata: HashMap<String, serde_json::Value>,
194    /// When the branch was created
195    pub created_at: chrono::DateTime<chrono::Utc>,
196}
197
198impl ThoughtBranch {
199    /// Create a new branch
200    pub fn new(branch_id: String, parent_thought: u32) -> Self {
201        Self {
202            branch_id,
203            parent_thought,
204            thoughts: Vec::new(),
205            metadata: HashMap::new(),
206            created_at: chrono::Utc::now(),
207        }
208    }
209
210    /// Add a thought to this branch
211    pub fn add_thought(&mut self, thought: ThoughtData) {
212        self.thoughts.push(thought);
213    }
214
215    /// Get the number of thoughts in this branch
216    pub fn thought_count(&self) -> usize {
217        self.thoughts.len()
218    }
219
220    /// Get the latest thought in this branch
221    pub fn latest_thought(&self) -> Option<&ThoughtData> {
222        self.thoughts.last()
223    }
224}
225
226/// Progress information for a thinking session
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct ThinkingProgress {
229    /// Current thought number
230    pub current_thought: u32,
231    /// Total estimated thoughts
232    pub total_thoughts: u32,
233    /// Number of completed thoughts
234    pub completed_thoughts: u32,
235    /// Number of active branches
236    pub active_branches: usize,
237    /// Whether more thoughts are needed
238    pub needs_more_thoughts: bool,
239    /// Progress percentage (0.0 to 1.0)
240    pub progress_percentage: f64,
241    /// Estimated time remaining (if available)
242    pub estimated_time_remaining: Option<std::time::Duration>,
243}
244
245impl ThinkingProgress {
246    /// Create new progress information
247    pub fn new(current_thought: u32, total_thoughts: u32) -> Self {
248        let completed_thoughts = current_thought.saturating_sub(1);
249        let progress_percentage = if total_thoughts > 0 {
250            completed_thoughts as f64 / total_thoughts as f64
251        } else {
252            0.0
253        };
254
255        Self {
256            current_thought,
257            total_thoughts,
258            completed_thoughts,
259            active_branches: 0,
260            needs_more_thoughts: true,
261            progress_percentage,
262            estimated_time_remaining: None,
263        }
264    }
265
266    /// Update progress with new thought information
267    pub fn update(&mut self, thought: &ThoughtData) {
268        self.current_thought = thought.thought_number;
269        self.total_thoughts = thought.total_thoughts;
270        self.completed_thoughts = thought.thought_number.saturating_sub(1);
271        self.needs_more_thoughts = thought.next_thought_needed;
272
273        self.progress_percentage = if self.total_thoughts > 0 {
274            self.completed_thoughts as f64 / self.total_thoughts as f64
275        } else {
276            0.0
277        };
278    }
279
280    /// Check if the thinking process is complete
281    pub fn is_complete(&self) -> bool {
282        !self.needs_more_thoughts && self.completed_thoughts >= self.total_thoughts
283    }
284}
285
286/// Trait for processing thoughts
287#[async_trait::async_trait]
288pub trait ThoughtProcessor: Send + Sync {
289    /// Process a single thought
290    async fn process_thought(&self, thought: ThoughtData) -> Result<ThoughtData, String>;
291
292    /// Validate a thought before processing
293    async fn validate_thought(&self, thought: &ThoughtData) -> Result<(), String>;
294
295    /// Get processing statistics
296    async fn get_stats(&self) -> Result<ThinkingStats, String>;
297}
298
299/// Statistics about thinking processing
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct ThinkingStats {
302    /// Total thoughts processed
303    pub total_thoughts: u64,
304    /// Total revisions made
305    pub total_revisions: u64,
306    /// Total branches created
307    pub total_branches: u64,
308    /// Average processing time per thought
309    pub avg_processing_time_ms: f64,
310    /// Total processing time
311    pub total_processing_time_ms: u64,
312    /// Total length of all thoughts (for avg_thought_length)
313    pub total_thought_length: u64,
314}
315
316impl Default for ThinkingStats {
317    fn default() -> Self {
318        Self {
319            total_thoughts: 0,
320            total_revisions: 0,
321            total_branches: 0,
322            avg_processing_time_ms: 0.0,
323            total_processing_time_ms: 0,
324            total_thought_length: 0,
325        }
326    }
327}
328
329/// Main thinking engine that coordinates the thinking process
330#[derive(Debug)]
331pub struct ThinkingEngine {
332    /// Unique identifier for this engine instance
333    id: Uuid,
334    /// Current thinking session
335    session_id: Option<String>,
336    /// Thoughts in the current session
337    thoughts: Vec<ThoughtData>,
338    /// Active branches
339    branches: HashMap<String, ThoughtBranch>,
340    /// Thinking progress
341    progress: ThinkingProgress,
342    /// Processing statistics
343    stats: ThinkingStats,
344    /// Whether thought logging is disabled
345    disable_logging: bool,
346}
347
348impl ThinkingEngine {
349    /// Create a new thinking engine
350    pub fn new() -> Self {
351        Self {
352            id: Uuid::new_v4(),
353            session_id: None,
354            thoughts: Vec::new(),
355            branches: HashMap::new(),
356            progress: ThinkingProgress::new(1, 1),
357            stats: ThinkingStats::default(),
358            disable_logging: false,
359        }
360    }
361
362    /// Create a new thinking engine with logging configuration
363    pub fn with_logging(disable_logging: bool) -> Self {
364        Self {
365            disable_logging,
366            ..Self::new()
367        }
368    }
369
370    /// Start a new thinking session
371    pub fn start_session(&mut self, session_id: String) {
372        self.session_id = Some(session_id);
373        self.thoughts.clear();
374        self.branches.clear();
375        self.progress = ThinkingProgress::new(1, 1);
376        self.stats = ThinkingStats::default();
377    }
378
379    /// Process a thought and add it to the session
380    pub async fn process_thought(&mut self, thought: ThoughtData) -> Result<ThoughtData, String> {
381        let start_time = std::time::Instant::now();
382
383        // Validate the thought
384        thought.validate()?;
385
386        // Adjust total thoughts if needed
387        let mut processed_thought = thought.clone();
388        if processed_thought.thought_number > processed_thought.total_thoughts {
389            processed_thought.total_thoughts = processed_thought.thought_number;
390        }
391
392        // Add to main thoughts
393        self.thoughts.push(processed_thought.clone());
394
395        // Handle branching
396        if let (Some(branch_from), Some(branch_id)) = (
397            processed_thought.branch_from_thought,
398            &processed_thought.branch_id,
399        ) {
400            let branch = self
401                .branches
402                .entry(branch_id.clone())
403                .or_insert_with(|| ThoughtBranch::new(branch_id.clone(), branch_from));
404            branch.add_thought(processed_thought.clone());
405        }
406
407        // Update progress
408        self.progress.update(&processed_thought);
409
410        // Update statistics
411        let processing_time = start_time.elapsed();
412        self.stats.total_thoughts += 1;
413        self.stats.total_processing_time_ms += processing_time.as_millis() as u64;
414        self.stats.avg_processing_time_ms =
415            self.stats.total_processing_time_ms as f64 / self.stats.total_thoughts as f64;
416
417        if processed_thought.is_revision() {
418            self.stats.total_revisions += 1;
419        }
420        if processed_thought.is_branch() {
421            self.stats.total_branches += 1;
422        }
423
424        // Log the thought if logging is enabled
425        if !self.disable_logging {
426            self.log_thought(&processed_thought);
427        }
428
429        Ok(processed_thought)
430    }
431
432    /// Get the current thinking progress
433    pub fn get_progress(&self) -> &ThinkingProgress {
434        &self.progress
435    }
436
437    /// Get all thoughts in the current session
438    pub fn get_thoughts(&self) -> &[ThoughtData] {
439        &self.thoughts
440    }
441
442    /// Get all branches in the current session
443    pub fn get_branches(&self) -> &HashMap<String, ThoughtBranch> {
444        &self.branches
445    }
446
447    /// Get thinking statistics
448    pub fn get_stats(&self) -> &ThinkingStats {
449        &self.stats
450    }
451
452    /// Check if the thinking session is complete
453    pub fn is_complete(&self) -> bool {
454        self.progress.is_complete()
455    }
456
457    /// Get the session ID
458    pub fn session_id(&self) -> Option<&str> {
459        self.session_id.as_deref()
460    }
461
462    /// Get the engine ID
463    pub fn engine_id(&self) -> Uuid {
464        self.id
465    }
466
467    /// Log a thought to stderr (for compatibility with official implementation)
468    fn log_thought(&self, thought: &ThoughtData) {
469        let prefix = if thought.is_revision() {
470            "šŸ”„ Revision"
471        } else if thought.is_branch() {
472            "🌿 Branch"
473        } else {
474            "šŸ’­ Thought"
475        };
476
477        let context = if thought.is_revision() {
478            format!(
479                " (revising thought {})",
480                thought.revises_thought.unwrap_or(0)
481            )
482        } else if thought.is_branch() {
483            format!(
484                " (from thought {}, ID: {})",
485                thought.branch_from_thought.unwrap_or(0),
486                thought.branch_id.as_deref().unwrap_or("unknown")
487            )
488        } else {
489            String::new()
490        };
491
492        let header = format!(
493            "{} {}/{}",
494            prefix, thought.thought_number, thought.total_thoughts
495        );
496        let border_length = std::cmp::max(header.len() + context.len(), thought.thought.len()) + 4;
497        let border = "─".repeat(border_length);
498
499        eprintln!(
500            "\nā”Œ{}┐\n│ {}{} │\nā”œ{}┤\n│ {} │\nā””{}ā”˜",
501            border, header, context, border, thought.thought, border
502        );
503    }
504}
505
506impl Default for ThinkingEngine {
507    fn default() -> Self {
508        Self::new()
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515
516    #[test]
517    fn test_thought_data_creation() {
518        let thought = ThoughtData::new("Test thought".to_string(), 1, 5);
519        assert_eq!(thought.thought, "Test thought");
520        assert_eq!(thought.thought_number, 1);
521        assert_eq!(thought.total_thoughts, 5);
522        assert!(thought.next_thought_needed);
523        assert!(!thought.is_revision());
524        assert!(!thought.is_branch());
525    }
526
527    #[test]
528    fn test_revision_thought() {
529        let thought = ThoughtData::revision("Revised thought".to_string(), 3, 1);
530        assert!(thought.is_revision());
531        assert_eq!(thought.get_revised_thought(), Some(1));
532        assert_eq!(thought.revises_thought, Some(1));
533    }
534
535    #[test]
536    fn test_branch_thought() {
537        let thought =
538            ThoughtData::branch("Branch thought".to_string(), 4, 2, "branch-1".to_string());
539        assert!(thought.is_branch());
540        assert_eq!(thought.get_branch_id(), Some("branch-1"));
541        assert_eq!(thought.branch_from_thought, Some(2));
542    }
543
544    #[test]
545    fn test_thought_validation() {
546        let valid_thought = ThoughtData::new("Valid thought".to_string(), 1, 5);
547        assert!(valid_thought.validate().is_ok());
548
549        let invalid_thought = ThoughtData {
550            thought: String::new(),
551            thought_number: 1,
552            total_thoughts: 5,
553            next_thought_needed: true,
554            ..Default::default()
555        };
556        assert!(invalid_thought.validate().is_err());
557    }
558
559    #[tokio::test]
560    async fn test_thinking_engine() {
561        let mut engine = ThinkingEngine::new();
562        engine.start_session("test-session".to_string());
563
564        let thought = ThoughtData::new("First thought".to_string(), 1, 3);
565        let processed = engine.process_thought(thought).await.unwrap();
566
567        assert_eq!(processed.thought, "First thought");
568        assert_eq!(engine.get_thoughts().len(), 1);
569        assert!(!engine.is_complete());
570    }
571
572    #[test]
573    fn test_thinking_progress() {
574        let mut progress = ThinkingProgress::new(1, 5);
575        assert_eq!(progress.current_thought, 1);
576        assert_eq!(progress.total_thoughts, 5);
577        assert_eq!(progress.progress_percentage, 0.0);
578
579        let thought = ThoughtData::new("Test".to_string(), 3, 5);
580        progress.update(&thought);
581        assert_eq!(progress.current_thought, 3);
582        assert_eq!(progress.completed_thoughts, 2);
583        assert_eq!(progress.progress_percentage, 0.4);
584    }
585}