Skip to main content

car_agents/
researcher.rs

1//! Researcher agent — given a question, gather information and return structured findings.
2//!
3//! Uses the inference engine with memory context to produce grounded answers.
4//! Designed as the first step in most workflows: understand before acting.
5
6use crate::{AgentContext, AgentResult};
7use car_inference::{GenerateParams, GenerateRequest};
8
9/// Researcher agent configuration.
10#[derive(Debug, Clone)]
11pub struct ResearchConfig {
12    /// Maximum tokens for the research response.
13    pub max_tokens: usize,
14    /// Temperature (lower = more focused, higher = more exploratory).
15    pub temperature: f64,
16    /// Optional model override.
17    pub model: Option<String>,
18}
19
20impl Default for ResearchConfig {
21    fn default() -> Self {
22        Self {
23            max_tokens: 4096,
24            temperature: 0.3,
25            model: None,
26        }
27    }
28}
29
30/// Researcher: search, read, gather → structured findings.
31pub struct Researcher {
32    ctx: AgentContext,
33    config: ResearchConfig,
34}
35
36impl Researcher {
37    pub fn new(ctx: AgentContext) -> Self {
38        Self {
39            ctx,
40            config: ResearchConfig::default(),
41        }
42    }
43
44    pub fn with_config(ctx: AgentContext, config: ResearchConfig) -> Self {
45        Self { ctx, config }
46    }
47
48    /// Research a question, optionally grounded in memory context.
49    ///
50    /// The context (AST scan of the codebase, file tree, knowledge maps) is
51    /// inlined directly into the prompt so the LLM definitely sees it. The
52    /// prompt enforces specificity: every finding must cite a concrete file
53    /// path or symbol. Broad/vague questions ("review the codebase") are
54    /// expanded into a multi-axis investigation so the synthesizer has real
55    /// material to work with downstream.
56    pub async fn research(&self, question: &str, context: Option<&str>) -> AgentResult {
57        let q_lower = question.trim().to_lowercase();
58        let is_broad_review = q_lower.split_whitespace().count() < 8
59            && (q_lower.starts_with("review")
60                || q_lower.starts_with("analy")
61                || q_lower.contains("codebase")
62                || q_lower == "what is this"
63                || q_lower.starts_with("describe")
64                || q_lower.starts_with("overview"));
65
66        let inline_context = context
67            .map(|c| format!("\n\n## Codebase snapshot\n{}\n", c.trim()))
68            .unwrap_or_default();
69
70        let investigation_axes = if is_broad_review {
71            "\nBecause the question is a broad review of a whole codebase, your research MUST cover ALL of the following axes. Do not skip any.\n\
72             1. **Architecture & components** — what are the main subsystems/services? For each, give the directory path and the one-line purpose.\n\
73             2. **Key entry points** — where does execution start? Name the specific files and functions.\n\
74             3. **Data & integrations** — what external systems does it talk to? (DBs, APIs, auth providers, cloud services). Cite the files that handle them.\n\
75             4. **Risks & gaps** — what looks fragile, under-tested, or deserves attention? Be concrete with file paths.\n\
76             5. **Next actions** — what are the 3 highest-value things a new engineer could do this week?\n"
77        } else {
78            ""
79        };
80
81        let prompt = format!(
82            "You are a codebase research agent. Your job is to answer the user's question with concrete, specific findings grounded in the actual files and symbols of the repository.{inline_context}\n\
83            ## Question\n\
84            {question}\n\
85            {investigation_axes}\n\
86            ## Rules\n\
87            - Every factual claim must cite a specific file path (and a symbol name when applicable). Vague statements like \"the codebase has good structure\" are forbidden — name the files and structures.\n\
88            - Prefer real evidence from the snapshot above over general assumptions about what codebases usually contain.\n\
89            - If the snapshot does not give you enough signal on a point, say so explicitly rather than inventing content.\n\
90            - Use markdown headings and bullets. Keep each bullet information-dense.\n\n\
91            Write your research below:"
92        );
93
94        let start = std::time::Instant::now();
95        let req = GenerateRequest {
96            prompt,
97            model: self.config.model.clone(),
98            params: GenerateParams {
99                temperature: self.config.temperature,
100                max_tokens: self.config.max_tokens,
101                ..Default::default()
102            },
103            context: context.map(String::from),
104            tools: None,
105            images: None,
106            messages: None,
107            cache_control: false,
108            response_format: None,
109            intent: None,
110        };
111
112        match self.ctx.inference.generate_tracked(req).await {
113            Ok(result) => {
114                // Confidence proxy: research that cites concrete file paths
115                // is much more reliable than vague prose. Count path-shaped
116                // tokens as a specificity signal.
117                let path_hits = result.text.matches('/').count()
118                    + result.text.matches(".rs").count()
119                    + result.text.matches(".ts").count()
120                    + result.text.matches(".tsx").count()
121                    + result.text.matches(".cs").count()
122                    + result.text.matches(".py").count();
123                let confidence = match path_hits {
124                    0 => 0.35,
125                    1..=3 => 0.55,
126                    4..=10 => 0.75,
127                    _ => 0.9,
128                };
129                AgentResult {
130                    agent: "researcher".into(),
131                    output: result.text,
132                    confidence,
133                    model_used: result.model_used,
134                    latency_ms: start.elapsed().as_millis() as u64,
135                }
136            }
137            Err(e) => AgentResult {
138                agent: "researcher".into(),
139                output: format!("Research failed: {}", e),
140                confidence: 0.0,
141                model_used: String::new(),
142                latency_ms: start.elapsed().as_millis() as u64,
143            },
144        }
145    }
146}