Skip to main content

sparrow/reasoning/
mod.rs

1use crate::provider::{ContentBlock, Msg};
2
3// ─── Reasoning depth ───────────────────────────────────────────────────────────
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
6pub enum ReasoningDepth {
7    Fast,
8    Adaptive,
9    Deep,
10}
11
12impl Default for ReasoningDepth {
13    fn default() -> Self {
14        ReasoningDepth::Adaptive
15    }
16}
17
18// ─── Anti-simulation guard ─────────────────────────────────────────────────────
19
20/// The anti-simulation guard checks every assistant turn for fabricated results.
21/// Rule: an agent MUST NOT claim a result without real tool execution.
22/// If the assistant asserts a result (pass/fail/output = X/returned Y) without
23/// a matching ToolOutput in the same turn, the engine rejects and asks for real execution.
24pub struct AntiSimulationGuard;
25
26impl AntiSimulationGuard {
27    /// Keywords that suggest a fabricated result claim
28    const CLAIM_KEYWORDS: &'static [&'static str] = &[
29        "all tests pass",
30        "tests pass",
31        "build succeeds",
32        "build successful",
33        "output:",
34        "returns:",
35        "returned:",
36        "the result is",
37        "got:",
38        "passed",
39        "succeeded",
40        "compiled",
41        "ran successfully",
42        "no errors",
43        "0 errors",
44        "zero failures",
45        "green",
46    ];
47
48    /// Check if an assistant message contains a result claim without having tool output
49    pub fn check(turn_messages: &[Msg], tool_outputs_in_turn: bool) -> Option<String> {
50        if tool_outputs_in_turn {
51            return None; // Results are backed by real tool execution
52        }
53
54        for msg in turn_messages.iter().filter(|m| m.role == "assistant") {
55            for block in &msg.content {
56                if let ContentBlock::Text { text } = block {
57                    let lower = text.to_lowercase();
58                    for kw in Self::CLAIM_KEYWORDS {
59                        if lower.contains(kw) {
60                            return Some(format!(
61                                "anti-simulation: assistant claimed result (matched '{}') without executing a tool. Execute the tool first, then report the RAW output.",
62                                kw
63                            ));
64                        }
65                    }
66                }
67            }
68        }
69        None
70    }
71
72    /// Check if an assertion about code was made without reading the file first
73    pub fn check_code_claim(text: &str, had_fs_read: bool, had_search: bool) -> Option<String> {
74        if had_fs_read || had_search {
75            return None;
76        }
77        let lower = text.to_lowercase();
78        let code_claims = [
79            "function", "struct", "impl", "trait", "fn ", "pub fn", "mod ", "class ",
80        ];
81        let assertion_patterns = [
82            "exists",
83            "is defined",
84            "takes",
85            "returns",
86            "has a",
87            "contains",
88        ];
89
90        let has_code_ref = code_claims.iter().any(|c| lower.contains(c));
91        let has_assertion = assertion_patterns.iter().any(|a| lower.contains(a));
92
93        if has_code_ref && has_assertion {
94            return Some(
95                "hallucination-guard: you made an assertion about the code without reading it first. Use fs_read or search to verify before claiming.".into()
96            );
97        }
98        None
99    }
100}
101
102// ─── Hallucination guard ───────────────────────────────────────────────────────
103
104pub struct HallucinationGuard;
105
106impl HallucinationGuard {
107    /// Before claiming a fact about code, verify via real read/search.
108    /// Returns a corrective message if the assistant made unverified claims.
109    pub fn verify(text: &str, tools_called: &[String]) -> Option<String> {
110        let has_read = tools_called.iter().any(|t| t == "fs_read" || t == "search");
111        AntiSimulationGuard::check_code_claim(text, has_read, has_read)
112    }
113}
114
115// ─── Self-critique ─────────────────────────────────────────────────────────────
116
117pub struct SelfCritique;
118
119impl SelfCritique {
120    /// Before a mutating batch, self-review the changes against constraints
121    pub fn pre_mutation_review(diffs: &[crate::event::FileDiff], spec: Option<&str>) -> String {
122        let mut review = String::from("## Self-critique (pre-mutation review)\n\n");
123
124        if diffs.is_empty() {
125            review.push_str("No diffs to review.\n");
126            return review;
127        }
128
129        review.push_str(&format!("Files to change: {}\n", diffs.len()));
130        for d in diffs {
131            review.push_str(&format!("- {} (+{} -{})\n", d.file, d.plus, d.minus));
132        }
133
134        review.push_str("\n### Checklist\n");
135        review.push_str("- [ ] Each change addresses the spec/requirement\n");
136        review.push_str("- [ ] No unnecessary formatting changes\n");
137        review.push_str("- [ ] Tests still pass after changes\n");
138        review.push_str("- [ ] No secrets or credentials exposed\n");
139        review.push_str("- [ ] Edge cases considered\n");
140
141        if let Some(s) = spec {
142            review.push_str(&format!("\nRefer to spec: {}\n", s));
143        }
144
145        review
146    }
147}
148
149// ─── Uncertainty ───────────────────────────────────────────────────────────────
150
151pub struct UncertaintyEstimator;
152
153impl UncertaintyEstimator {
154    /// Estimate confidence based on whether the agent has read relevant files
155    pub fn confidence(has_read_files: bool, has_run_tests: bool, task_complexity: &str) -> f64 {
156        let mut confidence: f64 = 0.3;
157
158        if has_read_files {
159            confidence += 0.3;
160        }
161        if has_run_tests {
162            confidence += 0.3;
163        }
164
165        match task_complexity {
166            "trivial" => confidence += 0.1,
167            "small" => confidence += 0.05,
168            "medium" => confidence += 0.0,
169            "hard" => confidence -= 0.1,
170            "vision" => confidence -= 0.05,
171            _ => {}
172        }
173
174        confidence.max(0.0).min(1.0)
175    }
176
177    /// Generate uncertainty message
178    pub fn uncertain_message() -> &'static str {
179        "I'm not sure about this — let me verify before proceeding."
180    }
181}
182
183// ─── Stop and ask ──────────────────────────────────────────────────────────────
184
185pub struct StopAndAsk;
186
187impl StopAndAsk {
188    /// Determine if the current situation warrants asking the user
189    pub fn should_ask(
190        ambiguity_level: f64,
191        is_irreversible: bool,
192        autonomy_allows: bool,
193        constraints_missing: bool,
194    ) -> Option<String> {
195        if ambiguity_level > 0.7 && !autonomy_allows {
196            return Some(
197                "High ambiguity detected. Could you clarify:\n- What exact behavior do you expect?\n- Are there specific constraints I should know?".into()
198            );
199        }
200
201        if is_irreversible && !autonomy_allows {
202            return Some(
203                "This action is irreversible. Before proceeding, please confirm:\n- Is this the expected outcome?\n- Are there any backups or safeguards in place?".into()
204            );
205        }
206
207        if constraints_missing {
208            return Some(
209                "Missing constraints. Could you specify:\n- Target environment/OS?\n- Performance requirements?\n- Compatibility constraints?".into()
210            );
211        }
212
213        None
214    }
215
216    /// Ask exactly ONE targeted question (never more than one)
217    pub fn ask_single(question: &str) -> String {
218        format!("❓ {}", question)
219    }
220}
221
222// ─── Reasoning engine (wired into main engine loop) ────────────────────────────
223
224pub struct ReasoningEngine {
225    pub depth: ReasoningDepth,
226    pub anti_simulation: bool,
227    pub hallucination_guard: bool,
228    pub self_critique: bool,
229}
230
231impl Default for ReasoningEngine {
232    fn default() -> Self {
233        Self {
234            depth: ReasoningDepth::Adaptive,
235            anti_simulation: true,
236            hallucination_guard: true,
237            self_critique: true,
238        }
239    }
240}
241
242impl ReasoningEngine {
243    /// Decide planning depth based on task complexity
244    pub fn plan_depth(&self, task: &str) -> usize {
245        let tier = crate::router::TaskTier::from_str(if task.len() > 100 {
246            "hard"
247        } else if task.len() > 30 {
248            "medium"
249        } else {
250            "small"
251        });
252        match self.depth {
253            ReasoningDepth::Fast => 1,
254            ReasoningDepth::Deep => 5,
255            ReasoningDepth::Adaptive => match tier {
256                crate::router::TaskTier::Trivial => 1,
257                crate::router::TaskTier::Small => 2,
258                crate::router::TaskTier::Medium => 3,
259                crate::router::TaskTier::Hard => 4,
260                crate::router::TaskTier::Vision => 3,
261            },
262        }
263    }
264
265    /// Run the anti-simulation guard on an assistant turn
266    pub fn guard_turn(&self, messages: &[Msg], tool_outputs_in_turn: bool) -> Option<String> {
267        if self.anti_simulation {
268            AntiSimulationGuard::check(messages, tool_outputs_in_turn)
269        } else {
270            None
271        }
272    }
273}
274
275// ─── Event extensions for reasoning ────────────────────────────────────────────
276
277/// Reasoning events emitted during the think phase
278#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
279pub enum ReasoningEvent {
280    /// Agent is planning sub-steps before acting
281    Planning { steps: Vec<String> },
282    /// Agent is self-critiquing before a mutation
283    SelfCritique { review: String },
284    /// Agent is uncertain and verifying
285    UncertaintyCheck { message: String },
286    /// Agent is asking the user a question
287    AskUser { question: String },
288    /// Anti-simulation guard rejected a turn
289    FabricationRejected { reason: String },
290    /// Hallucination guard caught an unverified claim
291    ClaimRejected { reason: String },
292}