use crate::{AgentContext, AgentResult};
use car_inference::{GenerateParams, GenerateRequest};
#[derive(Debug, Clone)]
pub struct ResearchConfig {
pub max_tokens: usize,
pub temperature: f64,
pub model: Option<String>,
}
impl Default for ResearchConfig {
fn default() -> Self {
Self {
max_tokens: 4096,
temperature: 0.3,
model: None,
}
}
}
pub struct Researcher {
ctx: AgentContext,
config: ResearchConfig,
}
impl Researcher {
pub fn new(ctx: AgentContext) -> Self {
Self { ctx, config: ResearchConfig::default() }
}
pub fn with_config(ctx: AgentContext, config: ResearchConfig) -> Self {
Self { ctx, config }
}
pub async fn research(&self, question: &str, context: Option<&str>) -> AgentResult {
let q_lower = question.trim().to_lowercase();
let is_broad_review = q_lower.split_whitespace().count() < 8
&& (q_lower.starts_with("review")
|| q_lower.starts_with("analy")
|| q_lower.contains("codebase")
|| q_lower == "what is this"
|| q_lower.starts_with("describe")
|| q_lower.starts_with("overview"));
let inline_context = context
.map(|c| format!("\n\n## Codebase snapshot\n{}\n", c.trim()))
.unwrap_or_default();
let investigation_axes = if is_broad_review {
"\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\
1. **Architecture & components** — what are the main subsystems/services? For each, give the directory path and the one-line purpose.\n\
2. **Key entry points** — where does execution start? Name the specific files and functions.\n\
3. **Data & integrations** — what external systems does it talk to? (DBs, APIs, auth providers, cloud services). Cite the files that handle them.\n\
4. **Risks & gaps** — what looks fragile, under-tested, or deserves attention? Be concrete with file paths.\n\
5. **Next actions** — what are the 3 highest-value things a new engineer could do this week?\n"
} else {
""
};
let prompt = format!(
"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\
## Question\n\
{question}\n\
{investigation_axes}\n\
## Rules\n\
- 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\
- Prefer real evidence from the snapshot above over general assumptions about what codebases usually contain.\n\
- If the snapshot does not give you enough signal on a point, say so explicitly rather than inventing content.\n\
- Use markdown headings and bullets. Keep each bullet information-dense.\n\n\
Write your research below:"
);
let start = std::time::Instant::now();
let req = GenerateRequest {
prompt,
model: self.config.model.clone(),
params: GenerateParams {
temperature: self.config.temperature,
max_tokens: self.config.max_tokens,
..Default::default()
},
context: context.map(String::from),
tools: None,
images: None,
messages: None,
cache_control: false,
response_format: None,
intent: None,
};
match self.ctx.inference.generate_tracked(req).await {
Ok(result) => {
let path_hits = result.text.matches('/').count()
+ result.text.matches(".rs").count()
+ result.text.matches(".ts").count()
+ result.text.matches(".tsx").count()
+ result.text.matches(".cs").count()
+ result.text.matches(".py").count();
let confidence = match path_hits {
0 => 0.35,
1..=3 => 0.55,
4..=10 => 0.75,
_ => 0.9,
};
AgentResult {
agent: "researcher".into(),
output: result.text,
confidence,
model_used: result.model_used,
latency_ms: start.elapsed().as_millis() as u64,
}
}
Err(e) => AgentResult {
agent: "researcher".into(),
output: format!("Research failed: {}", e),
confidence: 0.0,
model_used: String::new(),
latency_ms: start.elapsed().as_millis() as u64,
},
}
}
}