1use crate::cache::CacheManager;
12use anyhow::{Context as AnyhowContext, Result};
13
14use super::config;
15use super::evaluator::{EvaluationConfig, evaluate_results};
16use super::providers::{LlmProvider, create_provider};
17use super::reporter::AgenticReporter;
18use super::schema::{AgenticQueryResponse, QueryResponse};
19use super::schema_agentic::{AgenticResponse, Phase, ToolCall};
20use super::tools::{ToolResult, execute_tool, format_tool_results};
21
22#[derive(Debug, Clone)]
24pub struct AgenticConfig {
25 pub max_iterations: usize,
27
28 pub max_tools_per_phase: usize,
30
31 pub enable_evaluation: bool,
33
34 pub eval_config: EvaluationConfig,
36
37 pub provider_override: Option<String>,
39
40 pub model_override: Option<String>,
42
43 pub show_reasoning: bool,
45
46 pub verbose: bool,
48
49 pub debug: bool,
51}
52
53impl Default for AgenticConfig {
54 fn default() -> Self {
55 Self {
56 max_iterations: 2,
57 max_tools_per_phase: 5,
58 enable_evaluation: true,
59 eval_config: EvaluationConfig::default(),
60 provider_override: None,
61 model_override: None,
62 show_reasoning: false,
63 verbose: false,
64 debug: false,
65 }
66 }
67}
68
69pub async fn run_agentic_loop(
71 question: &str,
72 cache: &CacheManager,
73 config: AgenticConfig,
74 reporter: &dyn AgenticReporter,
75) -> Result<AgenticQueryResponse> {
76 log::info!("Starting agentic loop for question: {}", question);
77
78 if let Err(e) = cache.validate() {
80 let error_msg = e.to_string();
81
82 if error_msg.contains("Cache schema version mismatch") {
84 log::warn!("Cache schema mismatch detected, auto-reindexing...");
85
86 use std::sync::Arc;
88 let progress_callback: crate::indexer::ProgressCallback = Arc::new({
89 move |current: usize, total: usize, message: String| {
93 log::debug!("Reindex progress: [{}/{}] {}", current, total, message);
94 }
95 });
96
97 let workspace_root = cache.workspace_root();
99 let index_config = crate::IndexConfig::default();
100 let indexer = crate::indexer::Indexer::new(cache.clone(), index_config);
101
102 log::info!("Auto-reindexing cache at {:?}", workspace_root);
103 indexer.index_with_callback(&workspace_root, false, Some(progress_callback))?;
104
105 log::info!("Cache reindexing completed successfully");
106 } else {
107 return Err(e);
109 }
110 }
111
112 let provider = initialize_provider(&config, cache)?;
114
115 let (needs_context, initial_response) =
117 phase_1_assess(question, cache, &*provider, reporter, config.debug).await?;
118
119 let (gathered_context, tools_executed) = if needs_context {
121 phase_2_gather(
122 question,
123 initial_response,
124 cache,
125 &*provider,
126 &config,
127 reporter,
128 )
129 .await?
130 } else {
131 (String::new(), Vec::new())
132 };
133
134 let (query_response, query_confidence) = phase_3_generate(
136 question,
137 &gathered_context,
138 cache,
139 &*provider,
140 reporter,
141 config.debug,
142 )
143 .await?;
144
145 let (results, total_count, count_only) =
147 super::executor::execute_queries(query_response.queries.clone(), cache).await?;
148
149 log::info!(
150 "Executed queries: {} file groups, {} total matches",
151 results.len(),
152 total_count
153 );
154
155 if config.enable_evaluation && !count_only {
157 let evaluation = evaluate_results(
158 &results,
159 total_count,
160 question,
161 &config.eval_config,
162 if !gathered_context.is_empty() {
163 Some(gathered_context.as_str())
164 } else {
165 None
166 },
167 query_response.queries.len(),
168 Some(query_confidence),
169 );
170
171 log::info!(
172 "Evaluation: success={}, score={:.2}",
173 evaluation.success,
174 evaluation.score
175 );
176
177 reporter.report_evaluation(&evaluation);
179
180 if !evaluation.success && config.max_iterations > 1 {
182 log::info!("Results unsatisfactory, attempting refinement");
183
184 return phase_6_refine(
185 question,
186 &gathered_context,
187 &query_response,
188 &evaluation,
189 cache,
190 &*provider,
191 &config,
192 reporter,
193 config.debug,
194 )
195 .await;
196 }
197 }
198
199 Ok(AgenticQueryResponse {
201 queries: query_response.queries,
202 results,
203 total_count: if count_only { None } else { Some(total_count) },
204 gathered_context: if !gathered_context.is_empty() {
205 Some(gathered_context)
206 } else {
207 None
208 },
209 tools_executed: if !tools_executed.is_empty() {
210 Some(tools_executed)
211 } else {
212 None
213 },
214 answer: None, })
216}
217
218async fn phase_1_assess(
220 question: &str,
221 cache: &CacheManager,
222 provider: &dyn LlmProvider,
223 reporter: &dyn AgenticReporter,
224 debug: bool,
225) -> Result<(bool, AgenticResponse)> {
226 log::info!("Phase 1: Assessing context needs");
227
228 let prompt = super::prompt_agentic::build_assessment_prompt(question, cache)?;
230
231 if debug {
233 eprintln!("\n{}", "=".repeat(80));
234 eprintln!("DEBUG: Full LLM Prompt (Phase 1: Assessment)");
235 eprintln!("{}", "=".repeat(80));
236 eprintln!("{}", prompt);
237 eprintln!("{}\n", "=".repeat(80));
238 }
239
240 let json_response =
242 call_with_retry(provider, &prompt, 2, super::validate_agentic_response).await?;
243
244 let response: AgenticResponse =
246 serde_json::from_str(&json_response).context("Failed to parse LLM assessment response")?;
247
248 if response.phase != Phase::Assessment && response.phase != Phase::Final {
250 anyhow::bail!(
251 "Expected 'assessment' or 'final' phase, got {:?}",
252 response.phase
253 );
254 }
255
256 let needs_context = response.needs_context && !response.tool_calls.is_empty();
257
258 log::info!(
259 "Assessment complete: needs_context={}, tool_calls={}",
260 needs_context,
261 response.tool_calls.len()
262 );
263
264 reporter.report_assessment(&response.reasoning, needs_context, &response.tool_calls);
266
267 Ok((needs_context, response))
268}
269
270async fn phase_2_gather(
272 _question: &str,
273 initial_response: AgenticResponse,
274 cache: &CacheManager,
275 _provider: &dyn LlmProvider,
276 config: &AgenticConfig,
277 reporter: &dyn AgenticReporter,
278) -> Result<(String, Vec<String>)> {
279 log::info!("Phase 2: Gathering context via tools");
280
281 let mut all_tool_results = Vec::new();
282 let mut tool_descriptions = Vec::new();
283
284 let tool_calls: Vec<ToolCall> = initial_response
286 .tool_calls
287 .into_iter()
288 .take(config.max_tools_per_phase)
289 .collect();
290
291 log::info!("Executing {} tool calls", tool_calls.len());
292
293 for (idx, tool) in tool_calls.iter().enumerate() {
295 log::debug!(
296 "Executing tool {}/{}: {:?}",
297 idx + 1,
298 tool_calls.len(),
299 tool
300 );
301
302 let tool_desc = describe_tool_for_ui(tool);
304 tool_descriptions.push(tool_desc);
305
306 reporter.report_tool_start(idx + 1, tool);
308
309 match execute_tool(tool, cache).await {
310 Ok(result) => {
311 log::info!("Tool {} succeeded: {}", idx + 1, result.description);
312 reporter.report_tool_complete(idx + 1, &result);
313 all_tool_results.push(result);
314 }
315 Err(e) => {
316 log::warn!("Tool {} failed: {}", idx + 1, e);
317 let failed_result = ToolResult {
319 description: format!("Tool {} (failed)", idx + 1),
320 output: format!("Error: {}", e),
321 success: false,
322 };
323 reporter.report_tool_complete(idx + 1, &failed_result);
324 all_tool_results.push(failed_result);
325 }
326 }
327 }
328
329 let gathered_context = format_tool_results(&all_tool_results);
331
332 log::info!(
333 "Context gathering complete: {} chars",
334 gathered_context.len()
335 );
336
337 Ok((gathered_context, tool_descriptions))
338}
339
340fn describe_tool_for_ui(tool: &ToolCall) -> String {
342 match tool {
343 ToolCall::GatherContext { params } => {
344 let mut parts = Vec::new();
345 if params.structure {
346 parts.push("structure");
347 }
348 if params.file_types {
349 parts.push("file types");
350 }
351 if params.project_type {
352 parts.push("project type");
353 }
354 if params.framework {
355 parts.push("frameworks");
356 }
357 if params.entry_points {
358 parts.push("entry points");
359 }
360 if params.test_layout {
361 parts.push("test layout");
362 }
363 if params.config_files {
364 parts.push("config files");
365 }
366
367 if parts.is_empty() {
368 "gather_context: General codebase context".to_string()
369 } else {
370 format!("gather_context: {}", parts.join(", "))
371 }
372 }
373 ToolCall::ExploreCodebase { description, .. } => {
374 format!("explore_codebase: {}", description)
375 }
376 ToolCall::AnalyzeStructure { analysis_type } => {
377 format!("analyze_structure: {:?}", analysis_type)
378 }
379 ToolCall::SearchDocumentation { query, files } => {
380 if let Some(file_list) = files {
381 format!("search_documentation: '{}' in files {:?}", query, file_list)
382 } else {
383 format!("search_documentation: '{}'", query)
384 }
385 }
386 ToolCall::GetStatistics => {
387 "get_statistics: Retrieved file counts and language stats".to_string()
388 }
389 ToolCall::GetDependencies { file_path, reverse } => {
390 if *reverse {
391 format!("get_dependencies: What depends on '{}'", file_path)
392 } else {
393 format!("get_dependencies: Dependencies of '{}'", file_path)
394 }
395 }
396 ToolCall::GetAnalysisSummary { .. } => {
397 "get_analysis_summary: Dependency health overview".to_string()
398 }
399 ToolCall::FindIslands { .. } => "find_islands: Disconnected component analysis".to_string(),
400 }
401}
402
403async fn phase_3_generate(
407 question: &str,
408 gathered_context: &str,
409 cache: &CacheManager,
410 provider: &dyn LlmProvider,
411 reporter: &dyn AgenticReporter,
412 debug: bool,
413) -> Result<(QueryResponse, f32)> {
414 log::info!("Phase 3: Generating final queries");
415
416 let prompt = super::prompt_agentic::build_generation_prompt(question, gathered_context, cache)?;
418
419 if debug {
421 eprintln!("\n{}", "=".repeat(80));
422 eprintln!("DEBUG: Full LLM Prompt (Phase 3: Query Generation)");
423 eprintln!("{}", "=".repeat(80));
424 eprintln!("{}", prompt);
425 eprintln!("{}\n", "=".repeat(80));
426 }
427
428 let json_response = call_with_retry(
430 provider,
431 &prompt,
432 2,
433 super::validate_agentic_or_query_response,
434 )
435 .await?;
436
437 if let Ok(agentic_response) = serde_json::from_str::<AgenticResponse>(&json_response)
440 && agentic_response.phase == Phase::Final
441 {
442 let confidence = agentic_response.confidence;
443
444 reporter.report_generation(
446 Some(&agentic_response.reasoning),
447 agentic_response.queries.len(),
448 confidence,
449 );
450
451 return Ok((
453 QueryResponse {
454 queries: agentic_response.queries,
455 message: None,
456 },
457 confidence,
458 ));
459 }
460
461 let query_response: QueryResponse = serde_json::from_str(&json_response)
463 .context("Failed to parse LLM query generation response")?;
464
465 log::info!("Generated {} queries", query_response.queries.len());
466
467 reporter.report_generation(None, query_response.queries.len(), 1.0);
469
470 Ok((query_response, 1.0))
472}
473
474#[allow(clippy::too_many_arguments)]
476async fn phase_6_refine(
477 question: &str,
478 gathered_context: &str,
479 previous_response: &QueryResponse,
480 evaluation: &super::schema_agentic::EvaluationReport,
481 cache: &CacheManager,
482 provider: &dyn LlmProvider,
483 config: &AgenticConfig,
484 reporter: &dyn AgenticReporter,
485 debug: bool,
486) -> Result<AgenticQueryResponse> {
487 log::info!("Phase 6: Refining queries based on evaluation");
488
489 reporter.report_refinement_start();
491
492 let prompt = super::prompt_agentic::build_refinement_prompt(
494 question,
495 gathered_context,
496 previous_response,
497 evaluation,
498 cache,
499 )?;
500
501 if debug {
503 eprintln!("\n{}", "=".repeat(80));
504 eprintln!("DEBUG: Full LLM Prompt (Phase 6: Refinement)");
505 eprintln!("{}", "=".repeat(80));
506 eprintln!("{}", prompt);
507 eprintln!("{}\n", "=".repeat(80));
508 }
509
510 let json_response =
512 call_with_retry(provider, &prompt, 2, super::validate_query_response).await?;
513
514 let refined_response: QueryResponse =
516 serde_json::from_str(&json_response).context("Failed to parse LLM refinement response")?;
517
518 log::info!(
519 "Refinement complete: {} refined queries",
520 refined_response.queries.len()
521 );
522
523 let (results, total_count, count_only) =
525 super::executor::execute_queries(refined_response.queries.clone(), cache).await?;
526
527 let refined_evaluation = evaluate_results(
529 &results,
530 total_count,
531 question,
532 &config.eval_config,
533 if !gathered_context.is_empty() {
534 Some(gathered_context)
535 } else {
536 None
537 },
538 refined_response.queries.len(),
539 None, );
541
542 log::info!(
543 "Refined evaluation: success={}, score={:.2}",
544 refined_evaluation.success,
545 refined_evaluation.score
546 );
547
548 Ok(AgenticQueryResponse {
550 queries: refined_response.queries,
551 results,
552 total_count: if count_only { None } else { Some(total_count) },
553 gathered_context: if !gathered_context.is_empty() {
554 Some(gathered_context.to_string())
555 } else {
556 None
557 },
558 tools_executed: None, answer: None, })
561}
562
563fn initialize_provider(
565 config: &AgenticConfig,
566 cache: &CacheManager,
567) -> Result<Box<dyn LlmProvider>> {
568 let mut semantic_config = config::load_config(cache.path())?;
570
571 if let Some(provider) = &config.provider_override {
573 semantic_config.provider = provider.clone();
574 }
575
576 let api_key = config::get_api_key(&semantic_config.provider)?;
578
579 let model = config::resolve_model(&semantic_config, config.model_override.as_deref());
580
581 create_provider(
583 &semantic_config.provider,
584 api_key,
585 model,
586 config::get_provider_options(&semantic_config.provider),
587 semantic_config.timeout_seconds,
588 )
589}
590
591async fn call_with_retry(
593 provider: &dyn LlmProvider,
594 prompt: &str,
595 max_retries: usize,
596 validator: impl Fn(&str) -> Result<(), String>,
597) -> Result<String> {
598 super::call_with_retry(provider, prompt, max_retries, validator).await
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 #[test]
606 fn test_agentic_config_defaults() {
607 let config = AgenticConfig::default();
608 assert_eq!(config.max_iterations, 2);
609 assert_eq!(config.max_tools_per_phase, 5);
610 assert!(config.enable_evaluation);
611 }
612
613 #[test]
614 fn test_agentic_config_custom() {
615 let config = AgenticConfig {
616 max_iterations: 3,
617 max_tools_per_phase: 10,
618 enable_evaluation: false,
619 ..Default::default()
620 };
621
622 assert_eq!(config.max_iterations, 3);
623 assert_eq!(config.max_tools_per_phase, 10);
624 assert!(!config.enable_evaluation);
625 }
626}