1use crate::{AgentContext, AgentResult};
7use car_inference::{GenerateParams, GenerateRequest};
8
9#[derive(Debug, Clone)]
11pub struct ResearchConfig {
12 pub max_tokens: usize,
14 pub temperature: f64,
16 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
30pub 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 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 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}