gb/prompts/BOTCODER_HYBRID_ARCHITECTURE.md
Rodrigo Rodriguez (Pragmatismo) 334bb9239b
Some checks failed
BotServer CI / build (push) Failing after 9s
chore: Update botui submodule - Fix desktop title branding
Update botui to latest commit which changes desktop title from
'Agent Farm' to 'General Bots' for brand consistency.
2026-03-03 08:42:30 -03:00

31 KiB

BotCoder Hybrid Architecture v2.0

CLI + Optional Multi-Agent Facade (BYOK vs BotServer)

Executive Summary

BotCoder exists as a terminal-based AI coding agent with real-time streaming and tool execution. This document outlines how to extend it into a hybrid CLI/multi-agent OS that can:

  1. Work standalone (BYOK) - Direct LLM access, local execution
  2. Use botserver facade - Leverage Mantis Farm agents when available
  3. Switch dynamically - Fall back to local if botserver unavailable

Current BotCoder Architecture

Existing CLI Implementation (/home/rodriguez/src/pgm/botcoder)

Dependencies:

tokio = "1.42"          # Async runtime
reqwest = "0.12"        # HTTP client
ratatui = "0.29"        # TUI framework
crossterm = "0.29"      # Terminal handling
futures = "0.3"         # Async utilities
regex = "1.10"          # Pattern matching

Core Features:

  • Real-time streaming LLM responses
  • Tool execution (read_file, execute_command, write_file)
  • Delta format parsing (git-style diffs)
  • TPM rate limiting
  • Conversation history management
  • Animated TUI with ratatui

Tool Support:

// Currently supported tools
fn execute_tool(tool: &str, param: &str, project_root: &str) -> String {
    match tool {
        "read_file" => read_file(param),
        "execute_command" => execute_command(param, project_root),
        "write_file" => write_file(param),
        "list_files" => list_files(param, project_root),
        _ => format!("Unknown tool: {}", tool),
    }
}

LLM Integration:

// Direct Azure OpenAI client
mod llm {
    pub struct AzureOpenAIClient {
        endpoint: String,
        api_key: String,
        deployment: String,
    }

    impl LLMProvider for AzureOpenAIClient {
        async fn generate(&self, prompt: &str, params: &serde_json::Value)
            -> Result<String, Box<dyn std::error::Error>>;
    }
}

Proposed Hybrid Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    BOTCODER HYBRID MODE                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │              BOTCODER CLI (main.rs)                      │  │
│  │  - TUI interface (ratatui)                               │  │
│  │  - Tool execution                                        │  │
│  │  - Delta parsing                                         │  │
│  │  - Rate limiting                                         │  │
│  └──────────────────────────────────────────────────────────┘  │
│                            │                                    │
│                            ▼                                    │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │            LLM PROVIDER TRAIT (abstraction)              │  │
│  └──────────────────────────────────────────────────────────┘  │
│                            │                                    │
│            ┌───────────────┴───────────────┐                  │
│            ▼                               ▼                  │
│  ┌─────────────────────┐        ┌─────────────────────┐       │
│  │   DIRECT LLM        │        │  BOTSERVER FACADE    │       │
│  │   (BYOK Mode)       │        │  (Multi-Agent Mode)  │       │
│  │                     │        │                     │       │
│  │ - Azure OpenAI      │        │ - Mantis #1-4       │       │
│  │ - Anthropic         │        │ - Mantis #5-12      │       │
│  │ - OpenAI            │        │ - Orchestrator      │       │
│  │ - Local LLM         │        │ - WebSocket         │       │
│  └─────────────────────┘        └─────────────────────┘       │
│            │                               │                    │
│            │                        (Optional)                 │
│            ▼                               ▼                    │
│  ┌─────────────────────┐        ┌─────────────────────┐       │
│  │  LOCAL EXECUTION    │        │  AGENT EXECUTION    │       │
│  │                     │        │                     │       │
│  │ - File operations   │        │ - Containerized     │       │
│  │ - Command execution │        │ - AgentExecutor     │       │
│  │ - Git operations    │        │ - Browser automation│       │
│  │ - Docker control    │        │ - Test generation   │       │
│  └─────────────────────┘        └─────────────────────┘       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Implementation Plan

Phase 1: LLM Provider Abstraction (Week 1)

Goal: Create trait-based system for multiple LLM backends

File: src/llm/mod.rs

use async_trait::async_trait;

/// Unified LLM provider trait
#[async_trait]
pub trait LLMProvider: Send + Sync {
    /// Generate completion with streaming support
    async fn generate_stream(
        &self,
        prompt: &str,
        params: &GenerationParams,
    ) -> Result<StreamResponse, LLMError>;

    /// Generate completion (non-streaming)
    async fn generate(
        &self,
        prompt: &str,
        params: &GenerationParams,
    ) -> Result<String, LLMError>;

    /// Get provider capabilities
    fn capabilities(&self) -> ProviderCapabilities;

    /// Get provider name
    fn name(&self) -> &str;
}

pub struct GenerationParams {
    pub temperature: f32,
    pub max_tokens: u32,
    pub top_p: f32,
    pub tools: Vec<ToolDefinition>,
    pub system_prompt: Option<String>,
}

pub struct StreamResponse {
    pub content_stream: tokio_stream::wrappers::ReceiverStream<String>,
    pub tool_calls: Vec<ToolCall>,
    pub usage: TokenUsage,
}

pub struct ProviderCapabilities {
    pub streaming: bool,
    pub tools: bool,
    pub max_tokens: u32,
    pub supports_vision: bool,
}

Implementations:

// src/llm/azure_openai.rs
pub struct AzureOpenAIClient {
    endpoint: String,
    api_key: String,
    deployment: String,
    client: reqwest::Client,
}

#[async_trait]
impl LLMProvider for AzureOpenAIClient {
    async fn generate(&self, prompt: &str, params: &GenerationParams)
        -> Result<String, LLMError> {
        // Existing implementation
    }

    fn capabilities(&self) -> ProviderCapabilities {
        ProviderCapabilities {
            streaming: true,
            tools: true,
            max_tokens: 4096,
            supports_vision: false,
        }
    }

    fn name(&self) -> &str {
        "azure-openai"
    }
}

// src/llm/anthropic.rs
pub struct AnthropicClient {
    api_key: String,
    client: reqwest::Client,
}

#[async_trait]
impl LLMProvider for AnthropicClient {
    async fn generate(&self, prompt: &str, params: &GenerationParams)
        -> Result<String, LLMError> {
        // Anthropic API implementation
    }

    fn capabilities(&self) -> ProviderCapabilities {
        ProviderCapabilities {
            streaming: true,
            tools: true,
            max_tokens: 8192,
            supports_vision: true,
        }
    }

    fn name(&self) -> &str {
        "anthropic"
    }
}

// src/llm/botserver_facade.rs
pub struct BotServerFacade {
    base_url: String,
    api_key: Option<String>,
    client: reqwest::Client,
}

#[async_trait]
impl LLMProvider for BotServerFacade {
    async fn generate(&self, prompt: &str, params: &GenerationParams)
        -> Result<String, LLMError> {
        // Instead of direct LLM call, use botserver's orchestrator
        // 1. Classify intent
        // 2. Execute multi-agent pipeline
        // 3. Return aggregated result
    }

    fn capabilities(&self) -> ProviderCapabilities {
        ProviderCapabilities {
            streaming: true,  // Via WebSocket
            tools: true,      // Via AgentExecutor
            max_tokens: 128000,  // Multi-agent consensus
            supports_vision: true,  // Via Browser Agent
        }
    }

    fn name(&self) -> &str {
        "botserver-mantis-farm"
    }
}

Configuration:

// src/config.rs
#[derive(Debug, Clone)]
pub struct BotCoderConfig {
    pub llm_provider: LLMProviderType,
    pub botserver_url: Option<String>,
    pub project_path: PathBuf,
    pub enable_facade: bool,
    pub fallback_to_local: bool,
}

#[derive(Debug, Clone)]
pub enum LLMProviderType {
    AzureOpenAI,
    Anthropic,
    OpenAI,
    LocalLLM,
    BotServerFacade,  // Use Mantis Farm
}

impl BotCoderConfig {
    pub fn from_env() -> Result<Self, ConfigError> {
        let llm_provider = match env::var("LLM_PROVIDER").as_deref() {
            Ok("azure") => LLMProviderType::AzureOpenAI,
            Ok("anthropic") => LLMProviderType::Anthropic,
            Ok("botserver") => LLMProviderType::BotServerFacade,
            _ => LLMProviderType::AzureOpenAI,  // Default
        };

        let botserver_url = env::var("BOTSERVER_URL").ok();
        let enable_facade = env::var("ENABLE_BOTSERVER_FACADE")
            .unwrap_or_else(|_| "false".to_string()) == "true";

        Ok(Self {
            llm_provider,
            botserver_url,
            project_path: env::var("PROJECT_PATH")?.into(),
            enable_facade,
            fallback_to_local: true,
        })
    }
}

Phase 2: Multi-Agent Facade Integration (Week 2)

Goal: Connect to botserver's Mantis Farm when available

File: src/botserver_client.rs

use reqwest::Client;
use serde::{Deserialize, Serialize};

pub struct BotServerClient {
    base_url: String,
    api_key: Option<String>,
    client: Client,
}

impl BotServerClient {
    pub fn new(base_url: String, api_key: Option<String>) -> Self {
        Self {
            base_url,
            api_key,
            client: Client::new(),
        }
    }

    /// Classify intent using botserver's intent classifier
    pub async fn classify_intent(&self, text: &str)
        -> Result<ClassifiedIntent, BotServerError> {
        let url = format!("{}/api/autotask/classify", self.base_url);

        let response = self.client
            .post(&url)
            .json(&serde_json::json!({ "text": text }))
            .header("Authorization", self.api_key.as_ref().map(|k| format!("Bearer {}", k)).unwrap_or_default())
            .send()
            .await?;

        if response.status().is_success() {
            Ok(response.json().await?)
        } else {
            Err(BotServerError::ClassificationFailed(response.text().await?))
        }
    }

    /// Execute multi-agent pipeline
    pub async fn execute_pipeline(&self, classification: &ClassifiedIntent)
        -> Result<OrchestrationResult, BotServerError> {
        let url = format!("{}/api/autotask/execute", self.base_url);

        let response = self.client
            .post(&url)
            .json(classification)
            .header("Authorization", self.api_key.as_ref().map(|k| format!("Bearer {}", k)).unwrap_or_default())
            .send()
            .await?;

        if response.status().is_success() {
            Ok(response.json().await?)
        } else {
            Err(BotServerError::PipelineFailed(response.text().await?))
        }
    }

    /// Subscribe to WebSocket progress updates
    pub async fn subscribe_progress(&self, task_id: &str)
        -> Result<tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>, BotServerError> {
        let ws_url = format!(
            "{}/ws/task-progress/{}",
            self.base_url.replace("http", "ws"),
            task_id
        );

        tokio_tungstenite::connect_async(&ws_url).await
            .map_err(BotServerError::WebSocketError)
    }

    /// Use specialized agents directly
    pub async fn query_agent(&self, agent_id: u8, query: &str)
        -> Result<AgentResponse, BotServerError> {
        match agent_id {
            5 => self.query_editor_agent(query).await,
            6 => self.query_database_agent(query).await,
            7 => self.query_git_agent(query).await,
            8 => self.query_test_agent(query).await,
            9 => self.query_browser_agent(query).await,
            10 => self.query_terminal_agent(query).await,
            11 => self.query_docs_agent(query).await,
            12 => self.query_security_agent(query).await,
            _ => Err(BotServerError::InvalidAgent(agent_id)),
        }
    }

    // Specific agent methods
    async fn query_editor_agent(&self, query: &str)
        -> Result<AgentResponse, BotServerError> {
        // POST /api/botcoder/editor/query
        let url = format!("{}/api/botcoder/editor/query", self.base_url);

        let response = self.client
            .post(&url)
            .json(&serde_json::json!({ "query": query }))
            .send()
            .await?;

        Ok(response.json().await?)
    }

    async fn query_database_agent(&self, query: &str)
        -> Result<AgentResponse, BotServerError> {
        // Query database schema, optimize queries
        let url = format!("{}/api/botcoder/database/query", self.base_url);

        let response = self.client
            .post(&url)
            .json(&serde_json::json!({ "query": query }))
            .send()
            .await?;

        Ok(response.json().await?)
    }

    // ... other agent methods
}

#[derive(Debug, Deserialize)]
pub struct ClassifiedIntent {
    pub intent_type: String,
    pub entities: IntentEntities,
    pub original_text: String,
}

#[derive(Debug, Deserialize)]
pub struct OrchestrationResult {
    pub success: bool,
    pub task_id: String,
    pub stages_completed: u8,
    pub app_url: Option<String>,
    pub message: String,
    pub created_resources: Vec<CreatedResource>,
}

#[derive(Debug, thiserror::Error)]
pub enum BotServerError {
    #[error("Classification failed: {0}")]
    ClassificationFailed(String),

    #[error("Pipeline execution failed: {0}")]
    PipelineFailed(String),

    #[error("WebSocket error: {0}")]
    WebSocketError(#[from] tokio_tungstenite::tungstenite::Error),

    #[error("Invalid agent ID: {0}")]
    InvalidAgent(u8),

    #[error("HTTP error: {0}")]
    HttpError(#[from] reqwest::Error),

    #[error("JSON error: {0}")]
    JsonError(#[from] serde_json::Error),
}

Phase 3: Unified Tool Execution (Week 2-3)

Goal: Abstract tool execution to work locally or via agents

File: src/tools/mod.rs

use async_trait::async_trait;

/// Unified tool execution trait
#[async_trait]
pub trait ToolExecutor: Send + Sync {
    async fn execute(&self, tool: &ToolCall, context: &ExecutionContext)
        -> Result<ToolResult, ToolError>;

    fn supports_tool(&self, tool_name: &str) -> bool;
}

pub struct ToolCall {
    pub name: String,
    pub parameters: serde_json::Value,
    pub agent_id: Option<u8>,  // Which agent should execute
}

pub struct ToolResult {
    pub output: String,
    pub exit_code: i32,
    pub metadata: serde_json::Value,
}

pub struct ExecutionContext {
    pub project_path: PathBuf,
    pub botserver_client: Option<BotServerClient>,
    pub use_local_fallback: bool,
}

/// Local tool executor (existing implementation)
pub struct LocalToolExecutor {
    project_root: PathBuf,
}

#[async_trait]
impl ToolExecutor for LocalToolExecutor {
    async fn execute(&self, tool: &ToolCall, context: &ExecutionContext)
        -> Result<ToolResult, ToolError> {
        match tool.name.as_str() {
            "read_file" => self.read_file(tool.parameters).await,
            "write_file" => self.write_file(tool.parameters).await,
            "execute_command" => self.execute_command(tool.parameters).await,
            "list_files" => self.list_files(tool.parameters).await,
            "git_operation" => self.git_operation(tool.parameters).await,
            _ => Err(ToolError::UnknownTool(tool.name.clone())),
        }
    }

    fn supports_tool(&self, tool_name: &str) -> bool {
        matches!(tool_name,
            "read_file" | "write_file" | "execute_command" |
            "list_files" | "git_operation"
        )
    }
}

/// Agent-based tool executor (via botserver)
pub struct AgentToolExecutor {
    botserver_client: BotServerClient,
}

#[async_trait]
impl ToolExecutor for AgentToolExecutor {
    async fn execute(&self, tool: &ToolCall, context: &ExecutionContext)
        -> Result<ToolResult, ToolError> {
        // Route to appropriate agent
        let agent_id = tool.agent_id.unwrap_or_else(|| {
            self.infer_agent_for_tool(&tool.name)
        });

        match self.botserver_client.query_agent(agent_id, &tool.parameters.to_string()).await {
            Ok(response) => Ok(ToolResult {
                output: response.output,
                exit_code: response.exit_code,
                metadata: response.metadata,
            }),
            Err(e) => {
                // Fallback to local if enabled
                if context.use_local_fallback {
                    warn!("Agent execution failed, falling back to local: {}", e);
                    LocalToolExecutor::new(context.project_path.clone()).execute(tool, context).await?
                } else {
                    Err(ToolError::AgentError(e.to_string()))
                }
            }
        }
    }

    fn supports_tool(&self, tool_name: &str) -> bool {
        matches!(tool_name,
            "database_query" | "schema_visualize" | "git_commit" |
            "test_generate" | "browser_record" | "docs_generate" |
            "security_scan" | "code_refactor" | "optimize_query"
        )
    }

    fn infer_agent_for_tool(&self, tool_name: &str) -> u8 {
        match tool_name {
            "code_refactor" | "syntax_check" => 5,  // Editor Agent
            "database_query" | "schema_visualize" | "optimize_query" => 6,  // Database Agent
            "git_commit" | "git_branch" | "git_merge" => 7,  // Git Agent
            "test_generate" | "coverage_report" => 8,  // Test Agent
            "browser_record" | "page_test" => 9,  // Browser Agent
            "shell_execute" | "docker_build" => 10,  // Terminal Agent
            "docs_generate" | "api_docs" => 11,  // Docs Agent
            "security_scan" | "vulnerability_check" => 12,  // Security Agent
            _ => 2,  // Default to Builder Agent
        }
    }
}

Phase 4: Hybrid Execution Loop (Week 3)

Goal: Main loop that seamlessly switches between local and agent execution

File: src/main.rs (modified)

use llm::LLMProvider;
use tools::{LocalToolExecutor, AgentToolExecutor, ToolExecutor};

struct BotCoder {
    config: BotCoderConfig,
    llm_provider: Box<dyn LLMProvider>,
    local_executor: LocalToolExecutor,
    agent_executor: Option<AgentToolExecutor>,
    botserver_client: Option<BotServerClient>,
}

impl BotCoder {
    pub async fn new(config: BotCoderConfig) -> Result<Self, Box<dyn std::error::Error>> {
        // Initialize LLM provider based on config
        let llm_provider: Box<dyn LLMProvider> = match config.llm_provider {
            LLMProviderType::AzureOpenAI => {
                Box::new(llm::AzureOpenAIClient::new()?)
            }
            LLMProviderType::Anthropic => {
                Box::new(llm::AnthropicClient::new()?)
            }
            LLMProviderType::BotServerFacade => {
                // Will use botserver client
                Box::new(llm::BotServerFacade::new(
                    config.botserver_url.clone().unwrap()
                )?)
            }
            _ => Box::new(llm::AzureOpenAIClient::new()?),
        };

        // Initialize tool executors
        let local_executor = LocalToolExecutor::new(config.project_path.clone());

        let mut agent_executor = None;
        let mut botserver_client = None;

        // Try to connect to botserver if enabled
        if config.enable_facade {
            if let Some(url) = &config.botserver_url {
                match BotServerClient::new(url.clone(), None).health_check().await {
                    Ok(()) => {
                        println!("✓ Connected to botserver at {}", url);
                        let client = BotServerClient::new(url.clone(), None);
                        botserver_client = Some(client.clone());
                        agent_executor = Some(AgentToolExecutor::new(client));
                    }
                    Err(e) => {
                        warn!("Failed to connect to botserver: {}", e);
                        if config.fallback_to_local {
                            println!("⚠ Falling back to local execution");
                        }
                    }
                }
            }
        }

        Ok(Self {
            config,
            llm_provider,
            local_executor,
            agent_executor,
            botserver_client,
        })
    }

    pub async fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        let mut iteration = 0;
        let mut conversation_history: Vec<String> = Vec::new();

        loop {
            iteration += 1;
            println!("=== ITERATION {} ===", iteration);

            // Display execution mode
            if self.agent_executor.is_some() {
                println!("Mode: Multi-Agent (BotServer Facade)");
                println!("Agents Available: Mantis #1-12");
            } else {
                println!("Mode: Local (BYOK)");
            }
            println!();

            // Build context
            let context = self.build_context(&conversation_history);

            // Generate response (streaming)
            let response = match self.llm_provider.generate_stream(&context, &Default::default()).await {
                Ok(r) => r,
                Err(e) => {
                    // Try fallback to local if botserver fails
                    if self.agent_executor.is_some() && self.config.fallback_to_local {
                        warn!("LLM provider failed, trying fallback: {}", e);
                        // Switch to local provider
                        continue;
                    } else {
                        return Err(e.into());
                    }
                }
            };

            // Stream response to TUI
            self.display_streaming_response(response).await?;

            // Extract tools from response
            let tools = self.extract_tools(&full_response);

            // Execute tools (local or agent-based)
            for tool in tools {
                let result = self.execute_tool_hybrid(tool).await?;
                conversation_history.push(format!("Tool: {}\nResult: {}", tool.name, result));
            }

            conversation_history.push(format!("Assistant: {}", full_response));

            // Trim history
            if conversation_history.len() > 20 {
                conversation_history.drain(0..10);
            }
        }
    }

    async fn execute_tool_hybrid(&self, tool: ToolCall) -> Result<ToolResult, ToolError> {
        let context = ExecutionContext {
            project_path: self.config.project_path.clone(),
            botserver_client: self.botserver_client.clone(),
            use_local_fallback: self.config.fallback_to_local,
        };

        // Try agent executor first if available
        if let Some(agent_executor) = &self.agent_executor {
            if agent_executor.supports_tool(&tool.name) {
                println!("🤖 Executing via Mantis Agent: {}", tool.name);
                return agent_executor.execute(&tool, &context).await;
            }
        }

        // Fall back to local executor
        if self.local_executor.supports_tool(&tool.name) {
            println!("🔧 Executing locally: {}", tool.name);
            return self.local_executor.execute(&tool, &context).await;
        }

        Err(ToolError::UnknownTool(tool.name))
    }
}

Usage Examples

Example 1: Local Mode (BYOK)

# .env configuration
LLM_PROVIDER=azure
PROJECT_PATH=/home/user/myproject
ENABLE_BOTSERVER_FACADE=false
$ botcoder
=== ITERATION 1 ===
Mode: Local (BYOK)
✓ Azure OpenAI connected

> Add authentication to this Rust project

[AI Reasoning...]
I'll add JWT authentication using the `jsonwebtoken` crate.

CHANGE: Cargo.toml
<<<<<<< CURRENT
[dependencies]
tokio = "1.0"
=======
[dependencies]
tokio = "1.0"
jsonwebtoken = "9.0"
=======

[EXECUTE] Tool 1/3: write_file -> Cargo.toml
✓ File updated

Example 2: Multi-Agent Mode (BotServer Facade)

# .env configuration
LLM_PROVIDER=botserver
BOTSERVER_URL=http://localhost:8080
PROJECT_PATH=/home/user/myproject
ENABLE_BOTSERVER_FACADE=true
FALLBACK_TO_LOCAL=true
$ botcoder
=== ITERATION 1 ===
✓ Connected to botserver at http://localhost:8080
Mode: Multi-Agent (BotServer Facade)
Agents Available: Mantis #1-12

> Create a CRM system with contacts and deals

[CLASSIFY] Intent: APP_CREATE
[PLAN] Mantis #1 breaking down request...12 sub-tasks identified
  ✓ Estimated: 45 files, 98k tokens, 2.5 hours

[BUILD] Mantis #2 generating code...
  ✓ contacts table schema created
  ✓ deals table schema created
  ✓ Contact Manager page generated
  ✓ Deal Pipeline page generated

[REVIEW] Mantis #3 validating code...
  ✓ HTMX patterns verified
  ✓ Security checks passed
  ✓ 0 vulnerabilities found

[OPTIMIZE] Mantis #5 refactoring...
  ✓ Extracted duplicate code to utils.rs
  ✓ Added error handling wrappers

[TEST] Mantis #8 generating tests...
  ✓ 87% code coverage achieved
  ✓ E2E tests created (chromiumoxide)

[SECURITY] Mantis #12 scanning...0 critical vulnerabilities
  ✓ All dependencies up to date

[DEPLOY] Mantis #4 deploying...
  Target: Internal GB Platform
  ✓ App deployed to /apps/my-crm/
  ✓ Verify at http://localhost:8080/apps/my-crm/

[DOCUMENT] Mantis #11 generating docs...
  ✓ README.md created
  ✓ API documentation generated

✓ Pipeline complete in 1m 47s

Example 3: Hybrid Mode (Automatic Fallback)

$ botcoder
=== ITERATION 1 ===
Mode: Multi-Agent (BotServer Facade)
✓ Connected to botserver

> Refactor this function for better performance

[EDITOR] Mantis #5 analyzing code...
⚠ BotServer connection lost

[FALLBACK] Switching to local mode...
[LOCAL] Analyzing with Azure OpenAI...
✓ Refactoring complete

Benefits of Hybrid Architecture

For Users (BYOK)

  • Privacy - Code never leaves local machine
  • Speed - Direct LLM access, no intermediate hops
  • Cost Control - Use your own API keys
  • Offline Capable - Works with local LLMs (llama.cpp, Ollama)

For Users (BotServer Facade)

  • Multi-Agent Consensus - 12 specialized agents collaborate
  • Advanced Capabilities - Browser automation, security scanning, test generation
  • Visual Debugging - Watch agent reasoning in Vibe Builder UI
  • Enterprise Features - Team sharing, approval workflows, audit trails

Seamless Switching

  • Automatic Fallback - If botserver unavailable, use local
  • Tool Routing - Use agent for complex tasks, local for simple ones
  • Cost Optimization - Reserve expensive agents for hard problems
  • Progressive Enhancement - Start local, upgrade to multi-agent as needed

Configuration Matrix

Scenario LLM Provider Tools When to Use
Local Development Azure/Anthropic (Direct) Local file ops Privacy-critical code
Enterprise Project BotServer Facade Agent-based Complex refactoring
Open Source Local LLM (Ollama) Local No API budget
Learning BotServer Facade Agent-based Study agent reasoning
CI/CD BotServer Facade Agent-based Automated testing
Quick Fix Azure/Anthropic (Direct) Local Fast iteration
Security Audit BotServer Facade Mantis #12 Comprehensive scan

Implementation Roadmap

Week 1: Foundation

  • Extract existing LLM client to trait
  • Implement Azure OpenAI provider
  • Implement Anthropic provider
  • Add BotServerFacade provider (stub)

Week 2: BotServer Integration

  • Implement BotServerClient
  • Add WebSocket progress streaming
  • Implement agent query methods
  • Add health check & fallback logic

Week 3: Tool Execution

  • Refactor existing tools to trait
  • Implement LocalToolExecutor
  • Implement AgentToolExecutor
  • Add tool routing logic

Week 4: Hybrid Loop

  • Modify main loop for provider switching
  • Add streaming TUI updates
  • Implement automatic fallback
  • Add mode indicator to UI

Week 5: Testing & Docs

  • Test all three modes (local, agent, hybrid)
  • Add configuration examples
  • Write migration guide
  • Update README

Conclusion

The hybrid BotCoder gives users the best of both worlds:

  1. CLI First - Fast, local, privacy-focused development
  2. Multi-Agent Power - On-demand access to 12 specialized agents
  3. Seamless Switching - Automatic fallback between modes
  4. Progressive Enhancement - Start simple, scale when needed

Result: A coding agent that works offline for quick fixes but can call in a full multi-agent orchestra when facing complex challenges.

Estimated Effort: 5 weeks (1 developer) Lines of Code: ~2000 new lines (modular, trait-based)

The BotCoder CLI becomes the control plane for the Mantis Farm, offering both direct terminal access and a gateway to the full multi-agent OS when needed.