Skip to main content

sparrow/reasoning/
mod.rs

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