1use crate::{generate_with, AgentContext, AgentGenerator, AgentResult};
7use car_inference::{GenerateParams, GenerateRequest};
8use std::sync::Arc;
9
10#[derive(Debug, Clone)]
12pub struct ResearchConfig {
13 pub max_tokens: usize,
15 pub temperature: f64,
17 pub model: Option<String>,
19}
20
21impl Default for ResearchConfig {
22 fn default() -> Self {
23 Self {
24 max_tokens: 4096,
25 temperature: 0.3,
26 model: None,
27 }
28 }
29}
30
31pub struct Researcher {
33 ctx: AgentContext,
34 config: ResearchConfig,
35 generator: Option<Arc<AgentGenerator>>,
36}
37
38impl Researcher {
39 pub fn new(ctx: AgentContext) -> Self {
40 Self {
41 ctx,
42 config: ResearchConfig::default(),
43 generator: None,
44 }
45 }
46
47 pub fn with_config(ctx: AgentContext, config: ResearchConfig) -> Self {
48 Self {
49 ctx,
50 config,
51 generator: None,
52 }
53 }
54
55 pub fn with_generator(
57 ctx: AgentContext,
58 config: ResearchConfig,
59 generator: Arc<AgentGenerator>,
60 ) -> Self {
61 Self {
62 ctx,
63 config,
64 generator: Some(generator),
65 }
66 }
67
68 pub async fn research(&self, question: &str, context: Option<&str>) -> AgentResult {
77 let q_lower = question.trim().to_lowercase();
78 let is_broad_review = q_lower.split_whitespace().count() < 8
79 && (q_lower.starts_with("review")
80 || q_lower.starts_with("analy")
81 || q_lower.contains("codebase")
82 || q_lower == "what is this"
83 || q_lower.starts_with("describe")
84 || q_lower.starts_with("overview"));
85
86 let inline_context = context
87 .map(|c| format!("\n\n## Codebase snapshot\n{}\n", c.trim()))
88 .unwrap_or_default();
89
90 let investigation_axes = if is_broad_review {
91 "\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\
92 1. **Architecture & components** — what are the main subsystems/services? For each, give the directory path and the one-line purpose.\n\
93 2. **Key entry points** — where does execution start? Name the specific files and functions.\n\
94 3. **Data & integrations** — what external systems does it talk to? (DBs, APIs, auth providers, cloud services). Cite the files that handle them.\n\
95 4. **Risks & gaps** — what looks fragile, under-tested, or deserves attention? Be concrete with file paths.\n\
96 5. **Next actions** — what are the 3 highest-value things a new engineer could do this week?\n"
97 } else {
98 ""
99 };
100
101 let prompt = format!(
102 "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\
103 ## Question\n\
104 {question}\n\
105 {investigation_axes}\n\
106 ## Rules\n\
107 - 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\
108 - Prefer real evidence from the snapshot above over general assumptions about what codebases usually contain.\n\
109 - If the snapshot does not give you enough signal on a point, say so explicitly rather than inventing content.\n\
110 - Use markdown headings and bullets. Keep each bullet information-dense.\n\n\
111 Write your research below:"
112 );
113
114 let start = std::time::Instant::now();
115 let req = GenerateRequest {
116 prompt,
117 model: self.config.model.clone(),
118 params: GenerateParams {
119 temperature: self.config.temperature,
120 max_tokens: self.config.max_tokens,
121 ..Default::default()
122 },
123 context: context.map(String::from),
124 context_stable_prefix: None,
125 tools: None,
126 images: None,
127 messages: None,
128 cache_control: false,
129 response_format: None,
130 intent: None,
131 client_ref: None,
132 expected_row_digest: None,
133 expected_catalog_revision: None,
134 caller: None,
135 };
136
137 match generate_with(&self.ctx, self.generator.as_ref(), req).await {
138 Ok(result) => {
139 let path_hits = result.text.matches('/').count()
143 + result.text.matches(".rs").count()
144 + result.text.matches(".ts").count()
145 + result.text.matches(".tsx").count()
146 + result.text.matches(".cs").count()
147 + result.text.matches(".py").count();
148 let confidence = match path_hits {
149 0 => 0.35,
150 1..=3 => 0.55,
151 4..=10 => 0.75,
152 _ => 0.9,
153 };
154 AgentResult {
155 agent: "researcher".into(),
156 output: result.text,
157 confidence,
158 model_used: result.model_used,
159 latency_ms: start.elapsed().as_millis() as u64,
160 }
161 }
162 Err(e) => AgentResult {
163 agent: "researcher".into(),
164 output: format!("Research failed: {}", e),
165 confidence: 0.0,
166 model_used: String::new(),
167 latency_ms: start.elapsed().as_millis() as u64,
168 },
169 }
170 }
171}