Skip to main content

brainwires_reasoning/
strategies.rs

1//! Named Reasoning Strategies — ReAct, Reflexion, Chain of Thought, Tree of Thoughts
2//!
3//! Provides [`ReasoningStrategy`] trait and concrete implementations for
4//! well-known LLM reasoning patterns. Each strategy wraps the system prompt
5//! and controls the reasoning loop structure.
6//!
7//! # Strategies
8//!
9//! - [`ReActStrategy`] — Thought → Action → Observation loop (Yao et al., 2022)
10//! - [`ReflexionStrategy`] — Self-critique after each action (Shinn et al., 2023)
11//! - [`ChainOfThoughtStrategy`] — "Let's think step by step" (Wei et al., 2022)
12//! - [`TreeOfThoughtsStrategy`] — Multi-branch exploration with pruning (Yao et al., 2023)
13//!
14//! # Usage
15//!
16//! ```rust,ignore
17//! use brainwires_agents::reasoning::strategies::{ReActStrategy, ReasoningStrategy};
18//!
19//! let strategy = ReActStrategy::new(10); // max 10 reasoning steps
20//! let system_prompt = strategy.system_prompt("agent-1", "/project");
21//! let is_done = strategy.is_complete(&steps);
22//! ```
23
24use std::fmt;
25
26use serde::{Deserialize, Serialize};
27
28// ── Strategy Step ────────────────────────────────────────────────────────────
29
30/// A single step in a reasoning trace.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub enum StrategyStep {
33    /// Internal reasoning / thought.
34    Thought(String),
35    /// An action to execute (tool call).
36    Action {
37        /// Tool name.
38        tool: String,
39        /// Tool arguments.
40        args: serde_json::Value,
41    },
42    /// Result of an action / tool call.
43    Observation(String),
44    /// Self-critique and revised plan (Reflexion).
45    Reflection {
46        /// What went wrong or could be improved.
47        critique: String,
48        /// Revised plan based on the critique.
49        revised_plan: String,
50    },
51    /// A candidate branch in Tree of Thoughts.
52    Branch {
53        /// Branch identifier.
54        branch_id: usize,
55        /// The candidate thought.
56        thought: String,
57        /// Score assigned to this branch (0.0–1.0).
58        score: f64,
59    },
60    /// The final answer / completion signal.
61    FinalAnswer(String),
62}
63
64impl fmt::Display for StrategyStep {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            StrategyStep::Thought(t) => write!(f, "Thought: {}", t),
68            StrategyStep::Action { tool, .. } => write!(f, "Action: {}", tool),
69            StrategyStep::Observation(o) => write!(f, "Observation: {}", o),
70            StrategyStep::Reflection { critique, .. } => write!(f, "Reflection: {}", critique),
71            StrategyStep::Branch {
72                branch_id,
73                thought,
74                score,
75            } => {
76                write!(f, "Branch[{}] (score={:.2}): {}", branch_id, score, thought)
77            }
78            StrategyStep::FinalAnswer(a) => write!(f, "Final: {}", a),
79        }
80    }
81}
82
83// ── ReasoningStrategy trait ──────────────────────────────────────────────────
84
85/// A named reasoning strategy that controls the agent's thinking structure.
86///
87/// Strategies generate specialized system prompts and determine when the
88/// reasoning loop should terminate.
89pub trait ReasoningStrategy: Send + Sync {
90    /// Human-readable strategy name (e.g., "ReAct", "Reflexion").
91    fn name(&self) -> &str;
92
93    /// Brief description of how this strategy works.
94    fn description(&self) -> &str;
95
96    /// Generate a system prompt that instructs the LLM to follow this
97    /// reasoning pattern.
98    ///
99    /// The returned prompt is used as the system message (or prepended to
100    /// the existing system prompt).
101    fn system_prompt(&self, agent_id: &str, working_directory: &str) -> String;
102
103    /// Check whether the reasoning process has converged.
104    ///
105    /// Returns `true` when the strategy considers the reasoning complete
106    /// (e.g., a `FinalAnswer` step has been emitted, or max steps reached).
107    fn is_complete(&self, steps: &[StrategyStep]) -> bool;
108
109    /// Maximum reasoning steps before forced termination.
110    fn max_steps(&self) -> usize;
111}
112
113// ── ReAct Strategy ───────────────────────────────────────────────────────────
114
115/// ReAct: Reasoning + Acting (Yao et al., 2022)
116///
117/// The agent alternates between Thought, Action, and Observation steps.
118/// Each cycle: think about what to do, take an action, observe the result.
119#[derive(Debug, Clone)]
120pub struct ReActStrategy {
121    max_steps: usize,
122}
123
124impl ReActStrategy {
125    /// Create a new ReAct strategy with the given maximum step count.
126    pub fn new(max_steps: usize) -> Self {
127        Self { max_steps }
128    }
129}
130
131impl Default for ReActStrategy {
132    fn default() -> Self {
133        Self { max_steps: 15 }
134    }
135}
136
137impl ReasoningStrategy for ReActStrategy {
138    fn name(&self) -> &str {
139        "ReAct"
140    }
141
142    fn description(&self) -> &str {
143        "Reasoning + Acting: alternate between Thought, Action, and Observation steps"
144    }
145
146    fn system_prompt(&self, agent_id: &str, working_directory: &str) -> String {
147        format!(
148            r#"You are a task agent (ID: {agent_id}) using the ReAct reasoning framework.
149
150Working Directory: {working_directory}
151
152# ReAct FRAMEWORK
153
154You MUST follow this strict loop for every step:
155
1561. **Thought**: Reason about what you need to do next. What information do
157   you have? What do you still need? What is the best next action?
158
1592. **Action**: Execute exactly ONE tool call based on your thought.
160
1613. **Observation**: Analyze the result. Did it succeed? What did you learn?
162   Does the result change your plan?
163
164Then repeat: Thought → Action → Observation → Thought → ...
165
166## Format
167
168Always structure your response as:
169
170Thought: <your reasoning about what to do next>
171Action: <use a tool>
172Observation: <analyze the result after receiving it>
173
174## Rules
175
176- Each cycle must have exactly ONE action (tool call)
177- Never skip the Thought step — always reason before acting
178- If a tool call fails, reflect in your next Thought and adjust
179- When the task is complete, state your final answer clearly
180- Maximum {max_steps} reasoning cycles allowed
181
182## Completion
183
184When the task is fully complete, provide your final summary.
185Do NOT continue cycling after the task is done."#,
186            agent_id = agent_id,
187            working_directory = working_directory,
188            max_steps = self.max_steps,
189        )
190    }
191
192    fn is_complete(&self, steps: &[StrategyStep]) -> bool {
193        if steps.len() >= self.max_steps {
194            return true;
195        }
196        steps
197            .iter()
198            .any(|s| matches!(s, StrategyStep::FinalAnswer(_)))
199    }
200
201    fn max_steps(&self) -> usize {
202        self.max_steps
203    }
204}
205
206// ── Reflexion Strategy ───────────────────────────────────────────────────────
207
208/// Reflexion: Self-critique and iterative refinement (Shinn et al., 2023)
209///
210/// After each action-observation cycle, the agent reflects on what could
211/// be improved and adjusts its plan accordingly.
212#[derive(Debug, Clone)]
213pub struct ReflexionStrategy {
214    max_steps: usize,
215    /// How many reflection cycles before forcing completion.
216    max_reflections: usize,
217}
218
219impl ReflexionStrategy {
220    /// Create a new Reflexion strategy.
221    pub fn new(max_steps: usize, max_reflections: usize) -> Self {
222        Self {
223            max_steps,
224            max_reflections,
225        }
226    }
227}
228
229impl Default for ReflexionStrategy {
230    fn default() -> Self {
231        Self {
232            max_steps: 20,
233            max_reflections: 5,
234        }
235    }
236}
237
238impl ReasoningStrategy for ReflexionStrategy {
239    fn name(&self) -> &str {
240        "Reflexion"
241    }
242
243    fn description(&self) -> &str {
244        "Self-critique after each action cycle with iterative plan refinement"
245    }
246
247    fn system_prompt(&self, agent_id: &str, working_directory: &str) -> String {
248        format!(
249            r#"You are a task agent (ID: {agent_id}) using the Reflexion reasoning framework.
250
251Working Directory: {working_directory}
252
253# REFLEXION FRAMEWORK
254
255You follow a 4-phase loop:
256
2571. **Plan**: State your current plan and what you intend to do
2582. **Act**: Execute your planned action using a tool
2593. **Observe**: Analyze the result
2604. **Reflect**: Critically evaluate your approach:
261   - What went well?
262   - What could be improved?
263   - Should I change my plan?
264   - Am I making progress toward the goal?
265
266Then update your plan and repeat.
267
268## Format
269
270Structure each cycle as:
271
272Plan: <current plan and next intended action>
273Act: <use a tool>
274Observe: <what happened>
275Reflect: <self-critique — what worked, what didn't, revised plan>
276
277## Rules
278
279- Every action MUST be followed by reflection
280- Be honest in your self-critique — identify mistakes early
281- Update your plan after each reflection if needed
282- If you've made the same mistake twice, try a completely different approach
283- Maximum {max_reflections} reflection cycles before you must finalize
284- Maximum {max_steps} total steps allowed
285
286## Completion
287
288After your final reflection confirms the task is complete, provide
289your summary."#,
290            agent_id = agent_id,
291            working_directory = working_directory,
292            max_reflections = self.max_reflections,
293            max_steps = self.max_steps,
294        )
295    }
296
297    fn is_complete(&self, steps: &[StrategyStep]) -> bool {
298        if steps.len() >= self.max_steps {
299            return true;
300        }
301        let reflection_count = steps
302            .iter()
303            .filter(|s| matches!(s, StrategyStep::Reflection { .. }))
304            .count();
305        if reflection_count >= self.max_reflections {
306            return true;
307        }
308        steps
309            .iter()
310            .any(|s| matches!(s, StrategyStep::FinalAnswer(_)))
311    }
312
313    fn max_steps(&self) -> usize {
314        self.max_steps
315    }
316}
317
318// ── Chain of Thought Strategy ────────────────────────────────────────────────
319
320/// Chain of Thought: Step-by-step reasoning (Wei et al., 2022)
321///
322/// The agent reasons through the problem step by step before taking
323/// action. Best for problems requiring logical deduction.
324#[derive(Debug, Clone)]
325pub struct ChainOfThoughtStrategy {
326    max_steps: usize,
327}
328
329impl ChainOfThoughtStrategy {
330    /// Create a new Chain of Thought strategy.
331    pub fn new(max_steps: usize) -> Self {
332        Self { max_steps }
333    }
334}
335
336impl Default for ChainOfThoughtStrategy {
337    fn default() -> Self {
338        Self { max_steps: 15 }
339    }
340}
341
342impl ReasoningStrategy for ChainOfThoughtStrategy {
343    fn name(&self) -> &str {
344        "Chain-of-Thought"
345    }
346
347    fn description(&self) -> &str {
348        "Step-by-step reasoning before taking action"
349    }
350
351    fn system_prompt(&self, agent_id: &str, working_directory: &str) -> String {
352        format!(
353            r#"You are a task agent (ID: {agent_id}) using Chain-of-Thought reasoning.
354
355Working Directory: {working_directory}
356
357# CHAIN OF THOUGHT FRAMEWORK
358
359Before taking ANY action, you MUST reason through the problem step by step.
360
361## Process
362
3631. **Decompose**: Break the task into numbered steps
3642. **Reason**: For each step, explain your logic:
365   - What do I know?
366   - What do I need to find out?
367   - What is the logical next step?
3683. **Act**: After reasoning, execute the steps using tools
3694. **Verify**: Confirm each step's result before proceeding
370
371## Format
372
373Let's think step by step:
374
375Step 1: <describe what you need to do and why>
376Step 2: <next logical step>
377Step 3: ...
378
379Then execute each step, verifying results between actions.
380
381## Rules
382
383- Always number your reasoning steps
384- Show your work — explain WHY, not just WHAT
385- If a step's result is unexpected, re-reason from that point
386- Do not skip steps — complete each one before moving on
387- Maximum {max_steps} steps allowed
388
389## Completion
390
391After all steps are verified, provide your final answer."#,
392            agent_id = agent_id,
393            working_directory = working_directory,
394            max_steps = self.max_steps,
395        )
396    }
397
398    fn is_complete(&self, steps: &[StrategyStep]) -> bool {
399        if steps.len() >= self.max_steps {
400            return true;
401        }
402        steps
403            .iter()
404            .any(|s| matches!(s, StrategyStep::FinalAnswer(_)))
405    }
406
407    fn max_steps(&self) -> usize {
408        self.max_steps
409    }
410}
411
412// ── Tree of Thoughts Strategy ────────────────────────────────────────────────
413
414/// Tree of Thoughts: Multi-branch exploration with pruning (Yao et al., 2023)
415///
416/// The agent generates multiple candidate approaches, scores them, and
417/// pursues the most promising branch. Useful for complex problems with
418/// multiple valid solution paths.
419#[derive(Debug, Clone)]
420pub struct TreeOfThoughtsStrategy {
421    max_steps: usize,
422    /// Number of candidate branches to generate at each decision point.
423    branching_factor: usize,
424    /// Minimum score (0.0–1.0) for a branch to be pursued.
425    pruning_threshold: f64,
426}
427
428impl TreeOfThoughtsStrategy {
429    /// Create a new Tree of Thoughts strategy.
430    pub fn new(max_steps: usize, branching_factor: usize, pruning_threshold: f64) -> Self {
431        Self {
432            max_steps,
433            branching_factor,
434            pruning_threshold,
435        }
436    }
437}
438
439impl Default for TreeOfThoughtsStrategy {
440    fn default() -> Self {
441        Self {
442            max_steps: 25,
443            branching_factor: 3,
444            pruning_threshold: 0.4,
445        }
446    }
447}
448
449impl ReasoningStrategy for TreeOfThoughtsStrategy {
450    fn name(&self) -> &str {
451        "Tree-of-Thoughts"
452    }
453
454    fn description(&self) -> &str {
455        "Multi-branch exploration with scoring and pruning"
456    }
457
458    fn system_prompt(&self, agent_id: &str, working_directory: &str) -> String {
459        format!(
460            r#"You are a task agent (ID: {agent_id}) using Tree-of-Thoughts reasoning.
461
462Working Directory: {working_directory}
463
464# TREE OF THOUGHTS FRAMEWORK
465
466At each decision point, generate {branching_factor} candidate approaches,
467evaluate them, and pursue the best one.
468
469## Process
470
4711. **Generate**: Propose {branching_factor} different approaches to the
472   current sub-problem
4732. **Evaluate**: Score each approach (0.0 to 1.0) based on:
474   - Likelihood of success
475   - Efficiency (fewer steps = higher score)
476   - Risk of side effects
477   - Alignment with the overall goal
4783. **Select**: Choose the approach with the highest score
479   (must be above {pruning_threshold:.1} to proceed)
4804. **Execute**: Implement the selected approach
4815. **Backtrack**: If the selected approach fails, try the next-best candidate
482
483## Format
484
485=== Decision Point ===
486Candidate 1: <approach description> → Score: X.X
487Candidate 2: <approach description> → Score: X.X
488Candidate 3: <approach description> → Score: X.X
489
490Selected: Candidate N (score: X.X)
491Reason: <why this is the best approach>
492
493Then execute the selected approach.
494
495## Rules
496
497- Generate exactly {branching_factor} candidates at each major decision
498- Score honestly — don't inflate scores to justify a preference
499- If all candidates score below {pruning_threshold:.1}, step back and reconsider
500  the problem framing
501- Keep a mental note of unexplored branches in case backtracking is needed
502- Maximum {max_steps} total steps allowed
503
504## Completion
505
506When the task is complete, summarize which branches were explored and why
507the final approach was chosen."#,
508            agent_id = agent_id,
509            working_directory = working_directory,
510            branching_factor = self.branching_factor,
511            pruning_threshold = self.pruning_threshold,
512            max_steps = self.max_steps,
513        )
514    }
515
516    fn is_complete(&self, steps: &[StrategyStep]) -> bool {
517        if steps.len() >= self.max_steps {
518            return true;
519        }
520        steps
521            .iter()
522            .any(|s| matches!(s, StrategyStep::FinalAnswer(_)))
523    }
524
525    fn max_steps(&self) -> usize {
526        self.max_steps
527    }
528}
529
530// ── Strategy registry ────────────────────────────────────────────────────────
531
532/// Well-known strategy presets.
533#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
534pub enum StrategyPreset {
535    /// ReAct: Thought → Action → Observation loop
536    ReAct,
537    /// Reflexion: Self-critique after each cycle
538    Reflexion,
539    /// Chain of Thought: Step-by-step reasoning
540    ChainOfThought,
541    /// Tree of Thoughts: Multi-branch exploration
542    TreeOfThoughts,
543}
544
545impl StrategyPreset {
546    /// Create a default instance of the strategy.
547    pub fn create(&self) -> Box<dyn ReasoningStrategy> {
548        match self {
549            StrategyPreset::ReAct => Box::new(ReActStrategy::default()),
550            StrategyPreset::Reflexion => Box::new(ReflexionStrategy::default()),
551            StrategyPreset::ChainOfThought => Box::new(ChainOfThoughtStrategy::default()),
552            StrategyPreset::TreeOfThoughts => Box::new(TreeOfThoughtsStrategy::default()),
553        }
554    }
555
556    /// All available presets.
557    pub fn all() -> &'static [StrategyPreset] {
558        &[
559            StrategyPreset::ReAct,
560            StrategyPreset::Reflexion,
561            StrategyPreset::ChainOfThought,
562            StrategyPreset::TreeOfThoughts,
563        ]
564    }
565}
566
567impl fmt::Display for StrategyPreset {
568    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569        match self {
570            StrategyPreset::ReAct => write!(f, "ReAct"),
571            StrategyPreset::Reflexion => write!(f, "Reflexion"),
572            StrategyPreset::ChainOfThought => write!(f, "Chain-of-Thought"),
573            StrategyPreset::TreeOfThoughts => write!(f, "Tree-of-Thoughts"),
574        }
575    }
576}
577
578// ── Tests ────────────────────────────────────────────────────────────────────
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583
584    #[test]
585    fn test_react_system_prompt() {
586        let strategy = ReActStrategy::new(10);
587        let prompt = strategy.system_prompt("agent-1", "/project");
588        assert!(prompt.contains("ReAct"));
589        assert!(prompt.contains("Thought"));
590        assert!(prompt.contains("Action"));
591        assert!(prompt.contains("Observation"));
592        assert!(prompt.contains("agent-1"));
593    }
594
595    #[test]
596    fn test_react_completion() {
597        let strategy = ReActStrategy::new(3);
598        let steps = vec![
599            StrategyStep::Thought("thinking".to_string()),
600            StrategyStep::Action {
601                tool: "read".to_string(),
602                args: serde_json::json!({}),
603            },
604        ];
605        assert!(!strategy.is_complete(&steps));
606
607        let steps_with_answer = vec![
608            StrategyStep::Thought("thinking".to_string()),
609            StrategyStep::FinalAnswer("done".to_string()),
610        ];
611        assert!(strategy.is_complete(&steps_with_answer));
612    }
613
614    #[test]
615    fn test_react_max_steps() {
616        let strategy = ReActStrategy::new(2);
617        let steps = vec![
618            StrategyStep::Thought("a".to_string()),
619            StrategyStep::Thought("b".to_string()),
620        ];
621        assert!(strategy.is_complete(&steps));
622    }
623
624    #[test]
625    fn test_reflexion_system_prompt() {
626        let strategy = ReflexionStrategy::new(15, 3);
627        let prompt = strategy.system_prompt("agent-2", "/work");
628        assert!(prompt.contains("Reflexion"));
629        assert!(prompt.contains("Reflect"));
630        assert!(prompt.contains("self-critique"));
631    }
632
633    #[test]
634    fn test_reflexion_max_reflections() {
635        let strategy = ReflexionStrategy::new(20, 2);
636        let steps = vec![
637            StrategyStep::Reflection {
638                critique: "a".into(),
639                revised_plan: "b".into(),
640            },
641            StrategyStep::Reflection {
642                critique: "c".into(),
643                revised_plan: "d".into(),
644            },
645        ];
646        assert!(strategy.is_complete(&steps));
647    }
648
649    #[test]
650    fn test_cot_system_prompt() {
651        let strategy = ChainOfThoughtStrategy::new(10);
652        let prompt = strategy.system_prompt("agent-3", "/code");
653        assert!(prompt.contains("Chain-of-Thought") || prompt.contains("Chain of Thought"));
654        assert!(prompt.contains("step by step"));
655    }
656
657    #[test]
658    fn test_tot_system_prompt() {
659        let strategy = TreeOfThoughtsStrategy::new(20, 3, 0.4);
660        let prompt = strategy.system_prompt("agent-4", "/proj");
661        assert!(prompt.contains("Tree-of-Thoughts") || prompt.contains("Tree of Thoughts"));
662        assert!(prompt.contains("3")); // branching factor
663    }
664
665    #[test]
666    fn test_strategy_preset_create() {
667        for preset in StrategyPreset::all() {
668            let strategy = preset.create();
669            assert!(!strategy.name().is_empty());
670            assert!(!strategy.description().is_empty());
671            assert!(strategy.max_steps() > 0);
672        }
673    }
674
675    #[test]
676    fn test_strategy_step_display() {
677        let step = StrategyStep::Thought("testing".to_string());
678        assert_eq!(format!("{}", step), "Thought: testing");
679
680        let step = StrategyStep::Branch {
681            branch_id: 1,
682            thought: "try X".to_string(),
683            score: 0.85,
684        };
685        assert!(format!("{}", step).contains("0.85"));
686    }
687}