pub mod app; pub mod ui; use app::{AppState, CandidateAttemptInfo, TuiMode}; use brokecrew::budget::BudgetTracker; use brokecrew::client::OpenRouterClient; use brokecrew::config::AppConfig; use brokecrew::persona::Tier; use brokecrew::pipeline::{PipelineEvent, PipelineExecutor}; use brokecrew::registry::ModelRegistry; use brokecrew::router::{RouteStage, Router}; use brokecrew::session::Session; use brokecrew::tool::ToolEngine; use crossterm::{ event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use futures_util::StreamExt; use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; struct TerminalGuard; impl Drop for TerminalGuard { fn drop(&mut self) { let _ = disable_raw_mode(); let _ = execute!(io::stdout(), LeaveAlternateScreen, crossterm::cursor::Show); } } pub enum WorkerEvent { PipelineStarted { pipeline_name: String, steps: Vec, }, RouterResolved { persona: String, stage: RouteStage, details: String, complexity_score: u8, }, CandidateAttempt { model: String, attempt: usize, total: usize, tier: Tier, }, CandidateFailed { model: String, error: String, }, ToolExecutionStarted { command: String, }, ToolExecutionCompleted { command: String, exit_code: i32, output: String, duration_ms: u64, }, StepStarted { step_index: usize, total_steps: usize, persona: String, model: String, tier: Tier, }, Chunk { persona: String, model: String, tier: Tier, delta: String, }, StepCompleted { fell_back: bool, }, Finished { fell_back: bool, model_used: String, error: Option, calls_made: usize, paid_calls_made: usize, tokens_in: usize, tokens_out: usize, }, } pub async fn run_tui( config: AppConfig, mut budget_tracker: BudgetTracker, model_registry: Arc, allow_stealth: bool, escalate: bool, ) -> Result<(), Box> { // Enable raw mode and alternate screen enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, crossterm::cursor::Show)?; let _guard = TerminalGuard; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; // Prepare personas list for AppState let personas_summary: Vec<(String, String)> = config .persona_registry .list() .iter() .map(|p| { let desc = if !p.trigger_words.is_empty() { format!("triggers: {}", p.trigger_words.join(", ")) } else { p.system_prompt.chars().take(50).collect::() }; (p.name.clone(), desc) }) .collect(); let (minute_limit, daily_limit) = match budget_tracker.check_budget() { Ok(s) => (s.minute_limit, s.daily_limit), Err(_) => (20, 50), }; let mut app = AppState::new( personas_summary, config.pipelines.clone(), minute_limit, daily_limit, config.config_dir.clone(), config.theme.clone(), ); app.allow_stealth = allow_stealth || config.allow_stealth; app.escalate = escalate; // Refresh model catalog in background let reg_clone = Arc::clone(&model_registry); let url_clone = config.openrouter_base_url.clone(); tokio::spawn(async move { let _ = reg_clone.fetch_live(&url_clone).await; }); let client = Arc::new(OpenRouterClient::with_base_url( config.api_key.clone().unwrap_or_default(), config.openrouter_base_url.clone(), )); let router = Arc::new(Router::new( config.default_persona.clone(), config.router_model.clone(), config.complexity_threshold, )); let (worker_tx, mut worker_rx) = mpsc::channel::(100); let mut event_stream = EventStream::new(); let mut tick_interval = tokio::time::interval(Duration::from_millis(80)); loop { // Update budget counters in AppState if let Ok(st) = budget_tracker.check_budget() { app.minute_requests = st.minute_requests; app.daily_requests = st.daily_requests; app.minute_limit = st.minute_limit; app.daily_limit = st.daily_limit; } app.models_cached = model_registry.model_count().await; // Render UI terminal.draw(|f| ui::render(f, &mut app))?; tokio::select! { _ = tick_interval.tick() => { if app.is_generating { app.spinner_tick = app.spinner_tick.wrapping_add(1); } } Some(event) = worker_rx.recv() => { handle_worker_event(&mut app, &mut budget_tracker, event); } maybe_event = event_stream.next() => { match maybe_event { Some(Ok(Event::Key(key_event))) => { let should_quit = handle_key_event( &mut app, key_event, &config, &model_registry, &client, &router, worker_tx.clone(), ); if should_quit { break; } } Some(Ok(Event::Resize(_, _))) => { terminal.autoresize()?; } _ => {} } } } } Ok(()) } fn handle_worker_event(app: &mut AppState, budget_tracker: &mut BudgetTracker, event: WorkerEvent) { match event { WorkerEvent::PipelineStarted { pipeline_name, steps, } => { app.workings.active_pipeline = Some(pipeline_name.clone()); app.workings.pipeline_steps = steps .into_iter() .map(|p| app::PipelineStepTelemetry { persona: p, model: "evaluating...".to_string(), tier: Tier::Free, is_active: false, is_completed: false, fell_back: false, }) .collect(); if let Some(first) = app.workings.pipeline_steps.first_mut() { first.is_active = true; } app.log_working_event(&format!( "[pipeline:start] '{}' with {} steps", pipeline_name, app.workings.pipeline_steps.len() )); } WorkerEvent::RouterResolved { persona, stage, details, complexity_score, } => { let stage_name = match stage { RouteStage::StageAKeyword => "Stage A (Keyword Filter)", RouteStage::StageBLlm => "Stage B (LLM Classifier)", }; app.workings.route_stage = Some(stage_name.to_string()); app.workings.route_details = Some(details.clone()); app.workings.complexity_score = Some(complexity_score); app.log_working_event(&format!( "[route] {} ➔ {} (complexity: {}/10)", stage_name, persona, complexity_score )); } WorkerEvent::CandidateAttempt { model, attempt, total, tier, } => { app.workings.candidate_attempts.push(CandidateAttemptInfo { model: model.clone(), tier, status: format!("Attempting ({attempt}/{total})..."), is_error: false, is_success: false, }); app.log_working_event(&format!( "[candidate] trying {} ({}/{})", model, attempt, total )); } WorkerEvent::CandidateFailed { model, error } => { if let Some(att) = app .workings .candidate_attempts .iter_mut() .find(|a| a.model == model) { att.status = error.clone(); att.is_error = true; } app.log_working_event(&format!("[fallback] failed {}: {}", model, error)); } WorkerEvent::ToolExecutionStarted { command } => { app.status_message = format!("Running tool: {command}"); app.log_working_event(&format!("[tool:start] $ {command}")); } WorkerEvent::ToolExecutionCompleted { command, exit_code, output, duration_ms, } => { app.add_tool_result_message(command.clone(), exit_code, output, duration_ms); let dur_s = (duration_ms as f64) / 1000.0; app.log_working_event(&format!( "[tool:done] $ {command} -> exit {exit_code} ({dur_s:.1}s)" )); } WorkerEvent::StepStarted { step_index, total_steps, persona, model, tier, } => { if step_index < app.workings.pipeline_steps.len() { for (i, st) in app.workings.pipeline_steps.iter_mut().enumerate() { if i < step_index { st.is_completed = true; st.is_active = false; } else if i == step_index { st.is_active = true; st.model = model.clone(); st.tier = tier; } else { st.is_active = false; } } } else if app.workings.pipeline_steps.is_empty() { app.workings .pipeline_steps .push(app::PipelineStepTelemetry { persona: persona.clone(), model: model.clone(), tier, is_active: true, is_completed: false, fell_back: false, }); } if total_steps > 1 { app.start_pipeline_step_message(step_index, total_steps, &persona, &model, tier); app.log_working_event(&format!( "[pipeline] step {}/{}: {} ({})", step_index + 1, total_steps, persona, model )); } else { app.log_working_event(&format!("[dispatch] {} ({})", persona, model)); } } WorkerEvent::Chunk { persona, model, tier, delta, } => { // Lazily ensure assistant message exists so no empty boxes exist let needs_start = match app.messages.last() { Some(msg) => matches!( msg.sender, app::MessageSender::User | app::MessageSender::System ), None => true, }; if needs_start { app.start_assistant_message(&persona, &model, tier); } app.append_to_current_message(&delta); } WorkerEvent::StepCompleted { fell_back } => { if let Some(st) = app.workings.pipeline_steps.iter_mut().find(|s| s.is_active) { st.is_completed = true; st.is_active = false; st.fell_back = fell_back; } if fell_back { app.mark_current_message_fallback(); } } WorkerEvent::Finished { fell_back, model_used, error, calls_made, paid_calls_made, tokens_in, tokens_out, } => { app.is_generating = false; app.status_message = "Ready".to_string(); app.tokens_in += tokens_in; app.tokens_out += tokens_out; for st in app.workings.pipeline_steps.iter_mut() { if !st.is_completed { st.is_completed = true; st.is_active = false; } } if !model_used.is_empty() && model_used != "none" { app.update_current_message_model(&model_used); if let Some(att) = app .workings .candidate_attempts .iter_mut() .find(|a| a.model == model_used) { att.status = "Connected & Active".to_string(); att.is_success = true; } } if fell_back { app.mark_current_message_fallback(); } if let Some(err) = error { app.add_system_message(format!("! error: {err}")); app.log_working_event(&format!("[error] {err}")); } else { app.log_working_event(&format!("[complete] response finished ({model_used})")); } let free_calls = calls_made.saturating_sub(paid_calls_made); for _ in 0..free_calls { let _ = budget_tracker.record_call(false); } for _ in 0..paid_calls_made { let _ = budget_tracker.record_call(true); } // Auto-save session state app.save_current_session(); } } } fn handle_key_event( app: &mut AppState, key: KeyEvent, config: &AppConfig, model_registry: &Arc, client: &Arc, router: &Arc, worker_tx: mpsc::Sender, ) -> bool { // Global quit on Ctrl+C if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') { return true; } match app.mode { TuiMode::PersonaSelect => match key.code { KeyCode::Esc => { app.mode = TuiMode::Normal; } KeyCode::Up => { app.selected_persona_idx = app.selected_persona_idx.saturating_sub(1); } KeyCode::Down => { if app.selected_persona_idx + 1 < app.persona_items.len() { app.selected_persona_idx += 1; } } KeyCode::Enter => { app.active_pipeline_for_prompt = None; app.mode = TuiMode::Normal; app.log_working_event(&format!( "[persona] switched to {}", app.current_persona_name() )); } _ => {} }, TuiMode::PipelineSelect => match key.code { KeyCode::Esc => { app.mode = TuiMode::Normal; } KeyCode::Up => { app.selected_pipeline_idx = app.selected_pipeline_idx.saturating_sub(1); } KeyCode::Down => { if app.selected_pipeline_idx + 1 < app.pipelines.len() { app.selected_pipeline_idx += 1; } } KeyCode::Enter => { if let Some(pipe) = app.pipelines.get(app.selected_pipeline_idx) { app.active_pipeline_for_prompt = Some(pipe.name.clone()); app.log_working_event(&format!("[pipeline] selected {}", pipe.name)); } app.mode = TuiMode::Normal; } _ => {} }, TuiMode::SessionSelect => match key.code { KeyCode::Esc => { app.mode = TuiMode::Normal; } KeyCode::Up => { app.selected_session_idx = app.selected_session_idx.saturating_sub(1); } KeyCode::Down => { if app.selected_session_idx + 1 < app.session_summaries.len() { app.selected_session_idx += 1; } } KeyCode::Enter => { if let Some(summary) = app.session_summaries.get(app.selected_session_idx).cloned() { let _ = app.load_session_by_id(&summary.id); } app.mode = TuiMode::Normal; } KeyCode::Char('d') => { if let Some(summary) = app.session_summaries.get(app.selected_session_idx).cloned() { let _ = Session::delete(&app.config_dir, &summary.id); app.session_summaries = Session::list(&app.config_dir).unwrap_or_default(); if app.selected_session_idx >= app.session_summaries.len() { app.selected_session_idx = app.session_summaries.len().saturating_sub(1); } app.log_working_event(&format!("[session] deleted {}", summary.id)); } } _ => {} }, TuiMode::ThemeSelect => match key.code { KeyCode::Esc => { app.mode = TuiMode::Normal; } KeyCode::Up => { app.selected_theme_idx = app.selected_theme_idx.saturating_sub(1); } KeyCode::Down => { if app.selected_theme_idx + 1 < app::THEMES.len() { app.selected_theme_idx += 1; } } KeyCode::Enter => { if let Some((name, _)) = app::THEMES.get(app.selected_theme_idx) { app.current_theme = name.to_string(); let _ = AppConfig::save_theme(&app.config_dir, name); app.log_working_event(&format!("[theme] selected {}", name)); } app.mode = TuiMode::Normal; } _ => {} }, TuiMode::Normal => { // Mode switching and shortcuts if key.modifiers.contains(KeyModifiers::CONTROL) { app.esc_pressed_at = None; match key.code { KeyCode::Char('p') => { app.mode = TuiMode::PipelineSelect; return false; } KeyCode::Char('s') => { app.session_summaries = Session::list(&app.config_dir).unwrap_or_default(); app.selected_session_idx = 0; app.mode = TuiMode::SessionSelect; return false; } KeyCode::Char('n') => { app.start_new_session(); return false; } KeyCode::Char('o') => { app.expand_tool_outputs = !app.expand_tool_outputs; app.log_working_event(if app.expand_tool_outputs { "[tools] output expanded" } else { "[tools] output collapsed" }); return false; } KeyCode::Char('l') => { app.clear_chat(); return false; } _ => {} } } match key.code { KeyCode::Esc => { let now = std::time::Instant::now(); if let Some(prev) = app.esc_pressed_at { if now.duration_since(prev) <= Duration::from_millis(2000) { return true; } } app.esc_pressed_at = Some(now); app.status_message = "Press Esc again to quit".to_string(); app.log_working_event("[esc] press Esc again to quit"); return false; } _ => { app.esc_pressed_at = None; } } // Command suggestions navigation when typing slash command if app.input.starts_with('/') { let suggestions = app.get_matching_suggestions(); if !suggestions.is_empty() { match key.code { KeyCode::Up => { app.selected_suggestion_idx = app.selected_suggestion_idx.saturating_sub(1); return false; } KeyCode::Down => { if app.selected_suggestion_idx + 1 < suggestions.len() { app.selected_suggestion_idx += 1; } return false; } KeyCode::Tab => { if let Some(sug) = suggestions.get(app.selected_suggestion_idx) { app.input = format!("{} ", sug.name); app.cursor_pos = app.input.len(); app.selected_suggestion_idx = 0; } return false; } KeyCode::Enter if !app.input.contains(' ') => { // If input is purely a command without arguments, select the highlighted suggestion if let Some(sug) = suggestions.get(app.selected_suggestion_idx) { let cmd = sug.name.to_string(); app.input.clear(); app.cursor_pos = 0; app.selected_suggestion_idx = 0; handle_slash_command(app, config, &cmd); return false; } } _ => {} } } } match key.code { KeyCode::Esc => unreachable!(), KeyCode::Tab => { app.mode = TuiMode::PersonaSelect; } KeyCode::PageUp => { app.scroll_up(5); } KeyCode::PageDown => { app.scroll_down(5); } KeyCode::Left => { app.move_cursor_left(); } KeyCode::Right => { app.move_cursor_right(); } KeyCode::Home => { app.move_cursor_start(); } KeyCode::End => { app.move_cursor_end(); } KeyCode::Backspace => { app.delete_char_before_cursor(); app.selected_suggestion_idx = 0; } KeyCode::Delete => { app.delete_char_under_cursor(); app.selected_suggestion_idx = 0; } KeyCode::Enter => { let prompt = app.input.trim().to_string(); if !prompt.is_empty() && !app.is_generating { app.input.clear(); app.cursor_pos = 0; app.selected_suggestion_idx = 0; if prompt.starts_with('/') { handle_slash_command(app, config, &prompt); return false; } app.add_user_message(prompt.clone()); app.is_generating = true; app.status_message = "Dispatching...".to_string(); let target_persona = app.current_persona_name().to_string(); let pipe_name = app.active_pipeline_for_prompt.clone(); app.reset_turn_telemetry(&target_persona, pipe_name); app.log_working_event(&format!("[prompt] \"{prompt}\"")); spawn_dispatch_task( prompt, app, config, model_registry, client, router, worker_tx, ); } } KeyCode::Char(c) => { app.insert_char(c); app.selected_suggestion_idx = 0; } _ => {} } } } false } fn handle_slash_command(app: &mut AppState, config: &AppConfig, input: &str) { let parts: Vec<&str> = input.split_whitespace().collect(); let cmd = parts.first().copied().unwrap_or(""); match cmd { "/new" => { app.start_new_session(); } "/session" | "/sessions" => { app.session_summaries = Session::list(&app.config_dir).unwrap_or_default(); app.selected_session_idx = 0; app.mode = TuiMode::SessionSelect; } "/config" => { let key_status = if config.api_key.is_some() { "configured" } else { "not configured" }; app.add_system_message(format!( "Config Overview:\n Router Model: {}\n Default Persona: {}\n Stealth Candidates: {}\n Complexity Escalation: {}\n OpenRouter Key: {}\n Base URL: {}\n Config Dir: {}", config.router_model, config.default_persona, app.allow_stealth, app.escalate, key_status, config.openrouter_base_url, config.config_dir.display() )); } "/usage" => { app.add_system_message(format!( "Rate Limit & Budget Usage:\n 1-Minute Rate: {}/{} requests\n 24-Hour Budget: {}/{} requests\n Tokens In: {} (est)\n Tokens Out: {} (est)\n Cached Models: {} available", app.minute_requests, app.minute_limit, app.daily_requests, app.daily_limit, app::AppState::format_tokens(app.tokens_in), app::AppState::format_tokens(app.tokens_out), app.models_cached )); } "/theme" => { if let Some(arg) = parts.get(1) { if *arg == "next" { app.cycle_theme(); let theme_name = app.current_theme.clone(); let _ = AppConfig::save_theme(&app.config_dir, &theme_name); app.add_system_message(format!("Theme set to: {theme_name}")); return; } else if let Some((name, _)) = app::THEMES.iter().find(|(n, _)| n == arg) { app.current_theme = name.to_string(); let _ = AppConfig::save_theme(&app.config_dir, name); app.add_system_message(format!("Theme set to: {name}")); return; } } if let Some(pos) = app::THEMES .iter() .position(|(name, _)| *name == app.current_theme) { app.selected_theme_idx = pos; } app.mode = TuiMode::ThemeSelect; } "/settings" => { app.add_system_message(format!( "Runtime Settings:\n Stealth Models: {} (toggle with /stealth)\n Complexity Escalate: {} (toggle with /escalate)\n Active Persona: {}\n Active Pipeline: {}", if app.allow_stealth { "enabled" } else { "disabled" }, if app.escalate { "enabled" } else { "disabled" }, app.current_persona_name(), app.active_pipeline_for_prompt.as_deref().unwrap_or("none") )); } "/stealth" => { app.allow_stealth = !app.allow_stealth; app.add_system_message(format!( "Stealth candidate models are now: {}", if app.allow_stealth { "ENABLED" } else { "DISABLED" } )); } "/escalate" => { app.escalate = !app.escalate; app.add_system_message(format!( "Complexity escalation is now: {}", if app.escalate { "ENABLED" } else { "DISABLED" } )); } "/clear" => { app.clear_chat(); } "/persona" | "/personas" => { app.mode = TuiMode::PersonaSelect; } "/pipeline" | "/pipelines" => { app.mode = TuiMode::PipelineSelect; } "/help" => { app.add_system_message( "Available Commands:\n /new - Start a fresh session\n /session - Open session switcher modal\n /config - View active system configuration\n /usage - View budget and rate-limit counters\n /theme - Cycle color theme (default, warm, forest, mono)\n /settings - View and adjust runtime settings\n /stealth - Toggle stealth model eligibility\n /escalate - Toggle complexity escalation\n /persona - Open persona selection modal\n /pipeline - Open pipeline selection modal\n /clear - Clear chat screen\n /help - Show this help summary" .to_string(), ); } other => { app.add_system_message(format!( "Unknown command '{other}'. Type /help to view available commands." )); } } } fn spawn_dispatch_task( prompt: String, app: &AppState, config: &AppConfig, model_registry: &Arc, client: &Arc, router: &Arc, worker_tx: mpsc::Sender, ) { let pipeline_name = app.active_pipeline_for_prompt.clone(); let is_auto = app.is_auto_route(); let direct_persona = app.current_persona_name().to_string(); let allow_stealth = app.allow_stealth; let escalate = app.escalate; let persona_registry = config.persona_registry.clone(); let pipelines = config.pipelines.clone(); let default_persona = config.default_persona.clone(); let model_registry = Arc::clone(model_registry); let client = Arc::clone(client); let router = Arc::clone(router); tokio::spawn(async move { if let Some(pipe_name) = pipeline_name { if let Some(pipe) = pipelines.iter().find(|p| p.name == pipe_name) { let steps: Vec = pipe.steps.iter().map(|s| s.persona.clone()).collect(); let _ = worker_tx .send(WorkerEvent::PipelineStarted { pipeline_name: pipe_name.clone(), steps, }) .await; let (event_tx, mut event_rx) = mpsc::channel::(50); let w_tx = worker_tx.clone(); let forwarder = tokio::spawn(async move { while let Some(event) = event_rx.recv().await { match event { PipelineEvent::StepStarted { step_index, total_steps, persona, model, tier, } => { let _ = w_tx .send(WorkerEvent::StepStarted { step_index, total_steps, persona, model, tier, }) .await; } PipelineEvent::CandidateAttempt { model, attempt, total_candidates, tier, .. } => { let _ = w_tx .send(WorkerEvent::CandidateAttempt { model, attempt, total: total_candidates, tier, }) .await; } PipelineEvent::CandidateFailed { model, error, .. } => { let _ = w_tx .send(WorkerEvent::CandidateFailed { model, error }) .await; } PipelineEvent::Chunk { delta, .. } => { let _ = w_tx .send(WorkerEvent::Chunk { persona: "pipeline".to_string(), model: "pipeline".to_string(), tier: Tier::Free, delta, }) .await; } PipelineEvent::StepCompleted { fell_back, .. } => { let _ = w_tx.send(WorkerEvent::StepCompleted { fell_back }).await; } } } }); let res = PipelineExecutor::execute( pipe, &persona_registry, &model_registry, client.as_ref(), &prompt, allow_stealth, escalate, Some(event_tx), ) .await; let _ = forwarder.await; match res { Ok(results) => { let total_calls: usize = results.iter().map(|r| r.attempts.len()).sum(); let paid_calls: usize = results.iter().filter(|r| r.tier_used == Tier::Paid).count(); let fell_back = results.iter().any(|r| r.fell_back); let tok_in = brokecrew::estimate_tokens(&prompt); let tok_out: usize = results .iter() .map(|r| brokecrew::estimate_tokens(&r.reply)) .sum(); let _ = worker_tx .send(WorkerEvent::Finished { fell_back, model_used: "pipeline completed".to_string(), error: None, calls_made: total_calls + results.len(), paid_calls_made: paid_calls, tokens_in: tok_in, tokens_out: tok_out, }) .await; } Err(e) => { let _ = worker_tx .send(WorkerEvent::Finished { fell_back: false, model_used: "none".to_string(), error: Some(e.to_string()), calls_made: 1, paid_calls_made: 0, tokens_in: brokecrew::estimate_tokens(&prompt), tokens_out: 0, }) .await; } } return; } } // Determine target persona via router let (target_persona, route_stage, details, complexity_score) = if is_auto { match router .route(&persona_registry, client.as_ref(), &prompt, escalate) .await { Ok(decision) => { let d = match decision.stage_used { RouteStage::StageAKeyword => "Matched keyword in prompt".to_string(), RouteStage::StageBLlm => { format!("LLM classifier (model: {})", router.router_model) } }; ( decision.persona, Some(decision.stage_used), d, decision.complexity_score, ) } Err(err) => ( default_persona, None, format!("Router fallback to default: {err}"), 1, ), } } else { ( direct_persona, None, "Direct persona selected by user".to_string(), 1, ) }; if let Some(stage) = route_stage { let _ = worker_tx .send(WorkerEvent::RouterResolved { persona: target_persona.clone(), stage, details, complexity_score, }) .await; } let (chunk_tx, mut chunk_rx) = mpsc::channel::(100); let (event_tx, mut event_rx) = mpsc::channel::(50); let w_tx = worker_tx.clone(); let target_clone = target_persona.clone(); // Forward candidate events from dispatch_persona_with_events let event_forwarder = tokio::spawn(async move { while let Some(event) = event_rx.recv().await { match event { PipelineEvent::CandidateAttempt { model, attempt, total_candidates, tier, .. } => { let _ = w_tx .send(WorkerEvent::CandidateAttempt { model, attempt, total: total_candidates, tier, }) .await; } PipelineEvent::CandidateFailed { model, error, .. } => { let _ = w_tx .send(WorkerEvent::CandidateFailed { model, error }) .await; } _ => {} } } }); let w_tx2 = worker_tx.clone(); let chunk_forwarder = tokio::spawn(async move { while let Some(chunk) = chunk_rx.recv().await { let _ = w_tx2 .send(WorkerEvent::Chunk { persona: target_clone.clone(), model: "streaming".to_string(), tier: Tier::Free, delta: chunk, }) .await; } }); let persona_registry_tools = persona_registry.with_tool_capabilities(); let res = PipelineExecutor::dispatch_persona_with_events( &target_persona, &persona_registry_tools, &model_registry, client.as_ref(), &prompt, allow_stealth, escalate, Some(chunk_tx), Some(event_tx), ) .await; let _ = chunk_forwarder.await; let _ = event_forwarder.await; match res { Ok(disp) => { let commands = ToolEngine::extract_command_calls(&disp.reply); if !commands.is_empty() { let mut tool_results_summary = String::new(); for cmd in commands { let _ = worker_tx .send(WorkerEvent::ToolExecutionStarted { command: cmd.clone(), }) .await; let (exit_code, formatted, duration_ms) = match ToolEngine::execute_bash(&cmd, None, Duration::from_secs(30)) .await { Ok(out) => (out.exit_code, out.format_for_model(), out.duration_ms), Err(e) => (-1, format!("[Command: `{cmd}` failed: {e}]\n"), 0), }; let _ = worker_tx .send(WorkerEvent::ToolExecutionCompleted { command: cmd, exit_code, output: formatted.clone(), duration_ms, }) .await; tool_results_summary.push_str(&formatted); tool_results_summary.push('\n'); } let followup_prompt = format!( "{prompt}\n\n[Tool Output]:\n{tool_results_summary}\nPlease analyze the tool results above and provide your response to the user." ); let (chunk_tx2, mut chunk_rx2) = mpsc::channel::(100); let w_tx3 = worker_tx.clone(); let target_clone2 = target_persona.clone(); let chunk_forwarder2 = tokio::spawn(async move { while let Some(chunk) = chunk_rx2.recv().await { let _ = w_tx3 .send(WorkerEvent::Chunk { persona: target_clone2.clone(), model: "streaming".to_string(), tier: Tier::Free, delta: chunk, }) .await; } }); let followup_res = PipelineExecutor::dispatch_persona_with_events( &target_persona, &persona_registry_tools, &model_registry, client.as_ref(), &followup_prompt, allow_stealth, escalate, Some(chunk_tx2), None, ) .await; let _ = chunk_forwarder2.await; match followup_res { Ok(f_disp) => { let tok_in = brokecrew::estimate_tokens(&prompt) + brokecrew::estimate_tokens(&tool_results_summary); let tok_out = brokecrew::estimate_tokens(&disp.reply) + brokecrew::estimate_tokens(&f_disp.reply); let paid = if disp.tier_used == Tier::Paid || f_disp.tier_used == Tier::Paid { 1 } else { 0 }; let _ = worker_tx .send(WorkerEvent::Finished { fell_back: disp.fell_back || f_disp.fell_back, model_used: f_disp.model_used, error: None, calls_made: disp.attempts.len() + f_disp.attempts.len() + 2, paid_calls_made: paid, tokens_in: tok_in, tokens_out: tok_out, }) .await; } Err(e) => { let tok_in = brokecrew::estimate_tokens(&prompt) + brokecrew::estimate_tokens(&tool_results_summary); let tok_out = brokecrew::estimate_tokens(&disp.reply); let _ = worker_tx .send(WorkerEvent::Finished { fell_back: disp.fell_back, model_used: disp.model_used, error: Some(format!("Tool synthesis failed: {e}")), calls_made: disp.attempts.len() + 2, paid_calls_made: if disp.tier_used == Tier::Paid { 1 } else { 0 }, tokens_in: tok_in, tokens_out: tok_out, }) .await; } } } else { let tok_in = brokecrew::estimate_tokens(&prompt); let tok_out = brokecrew::estimate_tokens(&disp.reply); let _ = worker_tx .send(WorkerEvent::Finished { fell_back: disp.fell_back, model_used: disp.model_used, error: None, calls_made: disp.attempts.len() + 1, paid_calls_made: if disp.tier_used == Tier::Paid { 1 } else { 0 }, tokens_in: tok_in, tokens_out: tok_out, }) .await; } } Err(e) => { let _ = worker_tx .send(WorkerEvent::Finished { fell_back: false, model_used: "none".to_string(), error: Some(e.to_string()), calls_made: 1, paid_calls_made: 0, tokens_in: brokecrew::estimate_tokens(&prompt), tokens_out: 0, }) .await; } } }); }