gigi-cli 1.0.0

Gigi — A Claude Code-like AI coding assistant CLI in Rust
use anyhow::Result;
use colored::*;

use crate::config::AppConfig;
use crate::query::types::{ContentBlock, Message, StopReason};
use crate::query::QueryEngine;
use crate::session::Session;
use crate::tools::ToolRegistry;

// =============================================================================
// Agent — The core agentic loop
//
// Mirrors the Claude Code pipeline:
// 1. User input → 2. Append to history → 3. Query model →
// 4. If tool_use → execute tools → loop back to 3
// 5. If end_turn → print response → 6. Save session → 7. Await next input
// =============================================================================

pub struct Agent {
    pub engine: QueryEngine,
    pub tools: ToolRegistry,
    pub session: Session,
    pub config: AppConfig,
}

impl Agent {
    pub fn new(
        engine: QueryEngine,
        tools: ToolRegistry,
        session: Session,
        config: AppConfig,
    ) -> Self {
        Self {
            engine,
            tools,
            session,
            config,
        }
    }

    /// Run a single turn of the agent loop.
    ///
    /// This takes user input, queries the model, executes any tool calls,
    /// re-queries if needed, and repeats until the model produces a final
    /// text response (or hits the safety limit).
    pub async fn run_turn(&mut self, user_input: &str) -> Result<()> {
        // 1. Append user message to history
        let user_msg = Message::user(user_input);
        self.session.add_message(user_msg);

        let mut tool_turn_count = 0;

        loop {
            if tool_turn_count > 0 {
                // Throttle queries during rapid tool execution loops to avoid rate limiting
                tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
            }

            // 2. Query the model with full history (wrapped in a connection recovery loop)
            let mut rate_limit_retry_count = 0;
            let response = loop {
                match self.engine.query(&self.session.messages).await {
                    Ok(resp) => break resp,
                    Err(e) => {
                        let err_str = e.to_string();
                        let is_rate_limit = err_str.contains("429")
                            || err_str.contains("413")
                            || err_str.to_lowercase().contains("rate limit")
                            || err_str.to_lowercase().contains("too many requests")
                            || err_str.to_lowercase().contains("rate_limit");

                        if is_rate_limit && rate_limit_retry_count < 3 {
                            rate_limit_retry_count += 1;
                            let delay_secs = rate_limit_retry_count * 3;
                            println!(
                                "{}",
                                format!(
                                    "\n⚠ Rate limit/TPM limit hit. Automatically retrying in {} seconds (attempt {}/3)...",
                                    delay_secs, rate_limit_retry_count
                                )
                                .yellow()
                            );
                            tokio::time::sleep(std::time::Duration::from_secs(delay_secs)).await;
                            continue;
                        }

                        println!("{}", format!("\n✗ Model connection failed: {}", e).red());
                        println!("{}", "What would you like to do?".bold());
                        println!("  [r] Retry the request");
                        println!("  [l] Switch to local Ollama provider");
                        println!("  [p] Switch to another provider");
                        println!("  [s] Save session and exit");
                        print!("Select option (r/l/p/s): ");
                        use std::io::Write;
                        let _ = std::io::stdout().flush();

                        let mut selection = String::new();
                        if std::io::stdin().read_line(&mut selection).is_err() {
                            return Err(e);
                        }

                        match selection.trim().to_lowercase().as_str() {
                            "r" | "retry" => {
                                println!("{}", "Retrying query...".cyan());
                                continue;
                            }
                            "l" | "local" => {
                                println!("{}", "Switching to local Ollama provider...".cyan());
                                match crate::query::create_provider(&self.config, "ollama", None) {
                                    Ok(provider) => {
                                        self.engine.switch_provider(provider);
                                        self.session.provider_name = self.engine.provider_name().to_string();
                                        self.session.model_name = self.engine.model_id().to_string();
                                        println!("{}", format!("✓ Switched active model to: {}", self.model_info()).green());
                                        continue;
                                    }
                                    Err(err) => {
                                        println!("{}", format!("✗ Failed to switch to Ollama: {}", err).red());
                                    }
                                }
                            }
                            "p" | "provider" => {
                                println!("\nAvailable providers: anthropic, groq, google, ollama, lm_studio, llama_cpp, custom");
                                print!("Enter provider name: ");
                                let _ = std::io::stdout().flush();
                                let mut prov = String::new();
                                if std::io::stdin().read_line(&mut prov).is_ok() {
                                    let provider_name = prov.trim();
                                    print!("Enter model name (optional, press Enter for default): ");
                                    let _ = std::io::stdout().flush();
                                    let mut model = String::new();
                                    let model_opt = if std::io::stdin().read_line(&mut model).is_ok() && !model.trim().is_empty() {
                                        Some(model.trim().to_string())
                                    } else {
                                        None
                                    };
                                    let mut config = self.config.clone();
                                    match config.prompt_for_key_if_missing(provider_name) {
                                        Ok(_) => {
                                            self.config = config;
                                            match crate::query::create_provider(&self.config, provider_name, model_opt) {
                                                Ok(provider) => {
                                                    self.engine.switch_provider(provider);
                                                    self.session.provider_name = self.engine.provider_name().to_string();
                                                    self.session.model_name = self.engine.model_id().to_string();
                                                    println!("{}", format!("✓ Switched active model to: {}", self.model_info()).green());
                                                    continue;
                                                }
                                                Err(err) => {
                                                    println!("{}", format!("✗ Failed to switch provider: {}", err).red());
                                                }
                                            }
                                        }
                                        Err(err) => {
                                            println!("{}", format!("✗ Failed to configure key: {}", err).red());
                                        }
                                    }
                                }
                            }
                            "s" | "save" | "exit" => {
                                if let Err(err) = self.session.save(&self.config.session_dir).await {
                                    println!("{}", format!("Failed to save session: {}", err).red());
                                } else {
                                    println!("{}", "Session saved successfully.".green());
                                }
                                std::process::exit(1);
                            }
                            _ => {
                                println!("{}", "Invalid option. Please try again.".red());
                            }
                        }
                    }
                }
            };


            // 3. Create assistant message from response
            let assistant_msg = Message {
                role: crate::query::types::Role::Assistant,
                content: response.content.clone(),
            };
            self.session.add_message(assistant_msg);

            // 4. Print any text content from the response
            for block in &response.content {
                if let ContentBlock::Text { text } = block {
                    println!("\n{}", text);
                }
            }

            // 5. Check if we need to execute tools
            if response.stop_reason == StopReason::ToolUse {
                tool_turn_count += 1;

                if tool_turn_count > self.config.max_tool_turns {
                    println!(
                        "\n{}",
                        format!(
                            "⚠ Reached maximum tool turns ({}). Stopping.",
                            self.config.max_tool_turns
                        )
                        .yellow()
                    );
                    break;
                }

                // Collect all tool use blocks
                let tool_uses: Vec<(String, String, serde_json::Value)> = response
                    .content
                    .iter()
                    .filter_map(|block| match block {
                        ContentBlock::ToolUse { id, name, input } => {
                            Some((id.clone(), name.clone(), input.clone()))
                        }
                        _ => None,
                    })
                    .collect();

                // Execute each tool and collect results
                let mut tool_results: Vec<Message> = Vec::new();

                for (tool_id, tool_name, tool_input) in &tool_uses {
                    println!(
                        "\n{}",
                        format!("🔧 Running tool: {} ", tool_name).cyan()
                    );

                    let result = self.tools.execute(tool_name, tool_input.clone()).await?;

                    // Print a truncated preview of the result
                    let preview: String = result.content.chars().take(200).collect();
                    if result.is_error {
                        println!("{}", format!("  ✗ Error: {}", preview).red());
                    } else {
                        println!(
                            "{}",
                            format!(
                                "{}{}",
                                preview,
                                if result.content.len() > 200 { "..." } else { "" }
                            )
                            .dimmed()
                        );
                    }

                    tool_results.push(Message::tool_result(
                        tool_id,
                        &result.content,
                        result.is_error,
                    ));
                }

                // Add all tool results to history
                for result_msg in tool_results {
                    self.session.add_message(result_msg);
                }

                // Loop back to re-query the model with tool results
                continue;
            }

            // 6. Print usage stats if available
            if let Some(usage) = &response.usage {
                println!(
                    "{}",
                    format!(
                        "\n  [tokens: {} in / {} out]",
                        usage.input_tokens, usage.output_tokens
                    )
                    .dimmed()
                );
            }

            // No more tool calls — we're done with this turn
            break;
        }

        // 7. Auto-save the session after every turn
        if let Err(e) = self.session.save(&self.config.session_dir).await {
            eprintln!(
                "{}",
                format!("Warning: Failed to save session: {}", e).yellow()
            );
        }

        Ok(())
    }

    /// Get a display string for the current model info.
    pub fn model_info(&self) -> String {
        format!(
            "{} ({})",
            self.engine.model_id(),
            self.engine.provider_name()
        )
    }
}