mcp_langbase_reasoning/modes/
mod.rs

1//! Reasoning mode implementations.
2//!
3//! This module provides different reasoning modes:
4//! - `LinearMode`: Sequential step-by-step reasoning
5//! - `TreeMode`: Branching exploration with multiple paths
6//! - `DivergentMode`: Creative exploration with multiple perspectives
7//! - `ReflectionMode`: Meta-cognitive analysis
8//! - `BacktrackingMode`: Checkpoint-based state restoration
9//! - `AutoMode`: Intelligent mode selection
10//! - `GotMode`: Graph-of-Thoughts reasoning
11//! - `DecisionMode`: Multi-criteria decision analysis and stakeholder perspectives
12//! - `EvidenceMode`: Evidence assessment and Bayesian probability updates
13//! - `DetectionMode`: Bias and fallacy detection in reasoning
14//!
15//! All modes share common infrastructure via `ModeCore` composition.
16
17mod auto;
18mod backtracking;
19mod core;
20mod decision;
21mod detection;
22mod divergent;
23mod evidence;
24mod got;
25mod linear;
26mod reflection;
27mod tree;
28
29pub use auto::*;
30pub use backtracking::*;
31pub use core::*;
32pub use decision::*;
33pub use detection::*;
34pub use divergent::*;
35pub use evidence::*;
36pub use got::*;
37pub use linear::*;
38pub use reflection::*;
39pub use tree::*;
40
41use serde::{Deserialize, Serialize};
42use tracing::warn;
43
44// ============================================================================
45// Shared Utilities
46// ============================================================================
47
48/// Serialize a value to JSON for logging, with warning on failure.
49///
50/// This helper is used across all reasoning modes for invocation logging.
51/// Instead of panicking or silently failing on serialization errors,
52/// it logs a warning and returns an error object.
53pub(crate) fn serialize_for_log<T: serde::Serialize>(
54    value: &T,
55    context: &str,
56) -> serde_json::Value {
57    serde_json::to_value(value).unwrap_or_else(|e| {
58        warn!(
59            error = %e,
60            context = %context,
61            "Failed to serialize value for invocation log"
62        );
63        serde_json::json!({
64            "serialization_error": e.to_string(),
65            "context": context
66        })
67    })
68}
69
70/// Extract JSON from a completion string, handling markdown code blocks.
71///
72/// Attempts extraction in this order:
73/// 1. Try parsing as raw JSON first (fast path)
74/// 2. Extract from ```json ... ``` code blocks
75/// 3. Extract from ``` ... ``` code blocks
76/// 4. Return error if none work
77///
78/// This helper is used by modes that parse structured responses from Langbase.
79pub(crate) fn extract_json_from_completion(completion: &str) -> Result<&str, String> {
80    // Fast path: raw JSON
81    let trimmed = completion.trim();
82    if trimmed.starts_with('{') || trimmed.starts_with('[') {
83        return Ok(trimmed);
84    }
85
86    // Try ```json ... ``` blocks
87    if completion.contains("```json") {
88        return completion
89            .split("```json")
90            .nth(1)
91            .and_then(|s| s.split("```").next())
92            .map(|s| s.trim())
93            .filter(|s| !s.is_empty())
94            .ok_or_else(|| "Found ```json block but content was empty or malformed".to_string());
95    }
96
97    // Try ``` ... ``` blocks
98    if completion.contains("```") {
99        return completion
100            .split("```")
101            .nth(1)
102            .map(|s| s.trim())
103            .filter(|s| !s.is_empty())
104            .ok_or_else(|| "Found ``` block but content was empty or malformed".to_string());
105    }
106
107    Err(format!(
108        "No JSON found in response. First 100 chars: '{}'",
109        completion.chars().take(100).collect::<String>()
110    ))
111}
112
113/// Reasoning mode types.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum ReasoningMode {
117    /// Sequential step-by-step reasoning.
118    Linear,
119    /// Branching exploration with multiple paths.
120    Tree,
121    /// Creative exploration with multiple perspectives.
122    Divergent,
123    /// Meta-cognitive analysis and quality improvement.
124    Reflection,
125    /// Checkpoint-based state restoration.
126    Backtracking,
127    /// Automatic mode selection based on content.
128    Auto,
129    /// Graph-of-Thoughts reasoning.
130    Got,
131    /// Multi-criteria decision analysis.
132    Decision,
133    /// Evidence assessment and probabilistic reasoning.
134    Evidence,
135}
136
137impl ReasoningMode {
138    /// Get the mode name as a string
139    pub fn as_str(&self) -> &'static str {
140        match self {
141            ReasoningMode::Linear => "linear",
142            ReasoningMode::Tree => "tree",
143            ReasoningMode::Divergent => "divergent",
144            ReasoningMode::Reflection => "reflection",
145            ReasoningMode::Backtracking => "backtracking",
146            ReasoningMode::Auto => "auto",
147            ReasoningMode::Got => "got",
148            ReasoningMode::Decision => "decision",
149            ReasoningMode::Evidence => "evidence",
150        }
151    }
152}
153
154impl std::fmt::Display for ReasoningMode {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        write!(f, "{}", self.as_str())
157    }
158}
159
160impl std::str::FromStr for ReasoningMode {
161    type Err = String;
162
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        match s.to_lowercase().as_str() {
165            "linear" => Ok(ReasoningMode::Linear),
166            "tree" => Ok(ReasoningMode::Tree),
167            "divergent" => Ok(ReasoningMode::Divergent),
168            "reflection" => Ok(ReasoningMode::Reflection),
169            "backtracking" => Ok(ReasoningMode::Backtracking),
170            "auto" => Ok(ReasoningMode::Auto),
171            "got" => Ok(ReasoningMode::Got),
172            "decision" => Ok(ReasoningMode::Decision),
173            "evidence" => Ok(ReasoningMode::Evidence),
174            _ => Err(format!("Unknown reasoning mode: {}", s)),
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_reasoning_mode_as_str() {
185        assert_eq!(ReasoningMode::Linear.as_str(), "linear");
186        assert_eq!(ReasoningMode::Tree.as_str(), "tree");
187        assert_eq!(ReasoningMode::Divergent.as_str(), "divergent");
188        assert_eq!(ReasoningMode::Reflection.as_str(), "reflection");
189        assert_eq!(ReasoningMode::Backtracking.as_str(), "backtracking");
190        assert_eq!(ReasoningMode::Auto.as_str(), "auto");
191        assert_eq!(ReasoningMode::Got.as_str(), "got");
192        assert_eq!(ReasoningMode::Decision.as_str(), "decision");
193        assert_eq!(ReasoningMode::Evidence.as_str(), "evidence");
194    }
195
196    #[test]
197    fn test_reasoning_mode_display() {
198        assert_eq!(format!("{}", ReasoningMode::Linear), "linear");
199        assert_eq!(format!("{}", ReasoningMode::Tree), "tree");
200        assert_eq!(format!("{}", ReasoningMode::Divergent), "divergent");
201        assert_eq!(format!("{}", ReasoningMode::Reflection), "reflection");
202        assert_eq!(format!("{}", ReasoningMode::Backtracking), "backtracking");
203        assert_eq!(format!("{}", ReasoningMode::Auto), "auto");
204        assert_eq!(format!("{}", ReasoningMode::Got), "got");
205        assert_eq!(format!("{}", ReasoningMode::Decision), "decision");
206        assert_eq!(format!("{}", ReasoningMode::Evidence), "evidence");
207    }
208
209    #[test]
210    fn test_reasoning_mode_from_str_valid() {
211        assert_eq!(
212            "linear".parse::<ReasoningMode>().unwrap(),
213            ReasoningMode::Linear
214        );
215        assert_eq!(
216            "tree".parse::<ReasoningMode>().unwrap(),
217            ReasoningMode::Tree
218        );
219        assert_eq!(
220            "divergent".parse::<ReasoningMode>().unwrap(),
221            ReasoningMode::Divergent
222        );
223        assert_eq!(
224            "reflection".parse::<ReasoningMode>().unwrap(),
225            ReasoningMode::Reflection
226        );
227        assert_eq!(
228            "backtracking".parse::<ReasoningMode>().unwrap(),
229            ReasoningMode::Backtracking
230        );
231        assert_eq!(
232            "auto".parse::<ReasoningMode>().unwrap(),
233            ReasoningMode::Auto
234        );
235        assert_eq!("got".parse::<ReasoningMode>().unwrap(), ReasoningMode::Got);
236        assert_eq!(
237            "decision".parse::<ReasoningMode>().unwrap(),
238            ReasoningMode::Decision
239        );
240        assert_eq!(
241            "evidence".parse::<ReasoningMode>().unwrap(),
242            ReasoningMode::Evidence
243        );
244    }
245
246    #[test]
247    fn test_reasoning_mode_from_str_case_insensitive() {
248        assert_eq!(
249            "LINEAR".parse::<ReasoningMode>().unwrap(),
250            ReasoningMode::Linear
251        );
252        assert_eq!(
253            "Tree".parse::<ReasoningMode>().unwrap(),
254            ReasoningMode::Tree
255        );
256        assert_eq!(
257            "DIVERGENT".parse::<ReasoningMode>().unwrap(),
258            ReasoningMode::Divergent
259        );
260    }
261
262    #[test]
263    fn test_reasoning_mode_from_str_invalid() {
264        let result = "invalid".parse::<ReasoningMode>();
265        assert!(result.is_err());
266        assert_eq!(result.unwrap_err(), "Unknown reasoning mode: invalid");
267    }
268
269    #[test]
270    fn test_reasoning_mode_equality() {
271        assert_eq!(ReasoningMode::Linear, ReasoningMode::Linear);
272        assert_ne!(ReasoningMode::Linear, ReasoningMode::Tree);
273    }
274
275    #[test]
276    fn test_reasoning_mode_is_copy() {
277        let mode = ReasoningMode::Divergent;
278        let copied = mode; // Copy trait - no clone needed
279        assert_eq!(mode, copied);
280    }
281
282    #[test]
283    fn test_reasoning_mode_copy() {
284        let mode = ReasoningMode::Auto;
285        let copied = mode; // Copy, not move
286        assert_eq!(mode, copied);
287    }
288
289    // ========================================================================
290    // extract_json_from_completion tests
291    // ========================================================================
292
293    #[test]
294    fn test_extract_json_raw_object() {
295        let result = extract_json_from_completion(r#"{"key": "value"}"#);
296        assert_eq!(result.unwrap(), r#"{"key": "value"}"#);
297    }
298
299    #[test]
300    fn test_extract_json_raw_array() {
301        let result = extract_json_from_completion(r#"[1, 2, 3]"#);
302        assert_eq!(result.unwrap(), r#"[1, 2, 3]"#);
303    }
304
305    #[test]
306    fn test_extract_json_with_whitespace() {
307        let result = extract_json_from_completion("  \n  {\"key\": \"value\"}  \n  ");
308        assert_eq!(result.unwrap(), r#"{"key": "value"}"#);
309    }
310
311    #[test]
312    fn test_extract_json_from_json_code_block() {
313        let input = "Here is the response:\n```json\n{\"result\": true}\n```\nDone.";
314        let result = extract_json_from_completion(input);
315        assert_eq!(result.unwrap(), r#"{"result": true}"#);
316    }
317
318    #[test]
319    fn test_extract_json_from_plain_code_block() {
320        let input = "Response:\n```\n{\"data\": 123}\n```";
321        let result = extract_json_from_completion(input);
322        assert_eq!(result.unwrap(), r#"{"data": 123}"#);
323    }
324
325    #[test]
326    fn test_extract_json_empty_json_block() {
327        let input = "```json\n\n```";
328        let result = extract_json_from_completion(input);
329        assert!(result.is_err());
330        assert!(result.unwrap_err().contains("empty or malformed"));
331    }
332
333    #[test]
334    fn test_extract_json_empty_plain_block() {
335        let input = "```\n   \n```";
336        let result = extract_json_from_completion(input);
337        assert!(result.is_err());
338        assert!(result.unwrap_err().contains("empty or malformed"));
339    }
340
341    #[test]
342    fn test_extract_json_no_json_found() {
343        let input = "This is just plain text without any JSON.";
344        let result = extract_json_from_completion(input);
345        assert!(result.is_err());
346        assert!(result.unwrap_err().contains("No JSON found"));
347    }
348
349    #[test]
350    fn test_extract_json_truncates_long_error_message() {
351        let input = "a".repeat(200);
352        let result = extract_json_from_completion(&input);
353        assert!(result.is_err());
354        let err = result.unwrap_err();
355        assert!(err.contains("First 100 chars"));
356        // Error message should contain truncated content
357        assert!(err.len() < 200);
358    }
359
360    // ========================================================================
361    // serialize_for_log tests
362    // ========================================================================
363
364    #[test]
365    fn test_serialize_for_log_success() {
366        let value = serde_json::json!({"test": "value", "number": 42});
367        let result = serialize_for_log(&value, "test_context");
368        assert_eq!(result["test"], "value");
369        assert_eq!(result["number"], 42);
370    }
371
372    #[test]
373    fn test_serialize_for_log_simple_types() {
374        assert_eq!(serialize_for_log(&"hello", "string"), "hello");
375        assert_eq!(serialize_for_log(&42i32, "int"), 42);
376        assert_eq!(serialize_for_log(&true, "bool"), true);
377    }
378
379    #[test]
380    fn test_serialize_for_log_vec() {
381        let vec = vec![1, 2, 3];
382        let result = serialize_for_log(&vec, "vec");
383        assert_eq!(result, serde_json::json!([1, 2, 3]));
384    }
385}