//! Mandatory explore phase — spawns ≥3 parallel subagents to discover //! codebase context before every agent turn, visible in the TUI workflow tab. //! //! # Flow //! //! `ExploreServiceImpl::explore()` → //! //! 1. Push `WorkflowAgentUpdate { Pending }` for each agent onto the turn-event //! queue so the TUI workflow tab shows all 3. //! 2. Spawn **Code Structure** subagent (thread + tokio runtime). //! 3. Spawn **Symbol Index** subagent (thread + tokio runtime). //! 4. Spawn **Semantic Context** subagent (thread + tokio runtime). //! 5. Join all handles via `spawn_blocking`. //! 6. Push `Completed` / `Failed` events for each agent. //! 7. Consolidate findings into a system message → return. use crate::subagent::context::SubagentContext; use crate::subagent::division::AccessTier; use crate::subagent::engine::run_agent; use crate::tools::ToolCtx; use anyhow::{Context, Result}; use std::collections::VecDeque; use std::future::Future; use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::thread; use tracing::{info, warn}; use zesdex_application::agent::{ExploreOutput, ExploreService}; use zesdex_domain::agent::{AgentStatus, TurnEvent}; /// Number of parallel explore subagents. const EXPLORE_AGENT_COUNT: usize = 3; /// IDs for each explore agent (shown in the workflow tab). const EXPLORE_IDS: [&str; 3] = ["explore-structure", "explore-symbols", "explore-context"]; /// Display names for the TUI workflow tab. const EXPLORE_LABELS: [&str; 3] = [ "📁 Code Structure", "🔣 Symbol Index", "🔍 Semantic Context", ]; /// Directives for each explore subagent. const EXPLORE_DIRECTIVES: [&str; 3] = [ // Agent 0: Code Structure "You are a codebase structure explorer.\n\ 1. List all top-level directories and files in the workspace root.\n\ 2. Read Cargo.toml, package.json, or pyproject.toml at the root.\n\ 3. List the apps/ or src/ directory contents.\n\ 4. Identify main entry points (main.rs, main.py, index.ts, etc.).\n\ 5. Count files by extension type.\n\ Use the ls_dir, read, grep, and glob tools. Be concise.", // Agent 1: Symbol Index "You are a symbol index explorer.\n\ 1. Call the 'rebuild_index' tool to rebuild the symbol index.\n\ 2. Call the 'list_symbols' tool with max_results: 100.\n\ 3. Identify public APIs, entry points, and key types.\n\ 4. Group symbols by language and kind.\n\ Be concise. Report what symbols exist and where they live.", // Agent 2: Semantic Context "You are a semantic context explorer.\n\ 1. Call the 'rebuild_index' tool to ensure the index is fresh.\n\ 2. Search for symbols related to the user's query using semantic_search.\n\ 3. Search for config files, env variables, and settings.\n\ 4. Search for test files and test patterns.\n\ Be concise. Report relevant code areas for the task.\n\ Use the semantic_search, grep, glob, and read tools.", ]; // --------------------------------------------------------------------------- // Credentials // --------------------------------------------------------------------------- /// LLM credentials for explore subagents. pub struct Credentials { pub base_url: String, pub api_key: String, pub model: String, } // --------------------------------------------------------------------------- // ExploreServiceImpl — implements the application-layer trait // --------------------------------------------------------------------------- /// Concrete [`ExploreService`] that the turn service calls. /// /// Owns a shared `ToolCtx` and LLM credentials. Each call to `explore()` /// spawns 3 subagents in parallel with TUI workflow-tab visibility. pub struct ExploreServiceImpl { tool_ctx: ToolCtx, credentials: Credentials, } impl ExploreServiceImpl { pub fn new(tool_ctx: ToolCtx, credentials: Credentials) -> Self { ExploreServiceImpl { tool_ctx, credentials, } } } impl ExploreService for ExploreServiceImpl { fn explore<'a>( &'a self, query: &'a str, workspace_root: &'a str, turn_events: &'a Arc>>, ) -> Pin> + Send + 'a>> { Box::pin(async move { let context = run_explore_phase( query, workspace_root, &self.tool_ctx, &self.credentials, turn_events, ) .await?; Ok(ExploreOutput { context_messages: vec![context], summary: format!("{EXPLORE_AGENT_COUNT} explore agents dispatched"), }) }) } } // --------------------------------------------------------------------------- // Helpers for pushing workflow events // --------------------------------------------------------------------------- fn push_event(events: &Arc>>, event: TurnEvent) { if let Ok(mut q) = events.lock() { q.push_back(event); } } fn emit_pending(events: &Arc>>, agent_id: &str, display: &str) { push_event( events, TurnEvent::WorkflowAgentUpdate { agent_id: agent_id.to_string(), agent_name: display.to_string(), status: AgentStatus::Pending, }, ); } fn emit_running(events: &Arc>>, agent_id: &str, display: &str) { push_event( events, TurnEvent::WorkflowAgentUpdate { agent_id: agent_id.to_string(), agent_name: display.to_string(), status: AgentStatus::Running, }, ); } fn emit_completed(events: &Arc>>, agent_id: &str, display: &str) { push_event( events, TurnEvent::WorkflowAgentUpdate { agent_id: agent_id.to_string(), agent_name: display.to_string(), status: AgentStatus::Completed, }, ); } fn emit_failed(events: &Arc>>, agent_id: &str, display: &str, msg: &str) { push_event( events, TurnEvent::WorkflowAgentUpdate { agent_id: agent_id.to_string(), agent_name: display.to_string(), status: AgentStatus::Failed(msg.to_string()), }, ); } // --------------------------------------------------------------------------- // Core orchestration // --------------------------------------------------------------------------- /// Spawn `EXPLORE_AGENT_COUNT` subagents in parallel, emit workflow events /// for the TUI tab, join, and consolidate. async fn run_explore_phase( query: &str, workspace_root: &str, tool_ctx: &ToolCtx, credentials: &Credentials, turn_events: &Arc>>, ) -> Result { // ── 1. Emit Pending for all agents (appears instantly in workflow tab) ─ for i in 0..EXPLORE_AGENT_COUNT { emit_pending(turn_events, EXPLORE_IDS[i], EXPLORE_LABELS[i]); } // ── 2. Prepare directives ─────────────────────────────────────────── let mut directives: Vec = Vec::with_capacity(EXPLORE_AGENT_COUNT); for i in 0..EXPLORE_AGENT_COUNT { let mut d = EXPLORE_DIRECTIVES[i].to_string(); if i == 2 { d.push_str(&format!("\n\nThe user's current query is: \"{query}\"")); } d.push_str(&format!("\n\nWorkspace root: {workspace_root}")); directives.push(d); } // ── 3. Spawn all agents on threads ────────────────────────────────── let mut handles: Vec<(usize, thread::JoinHandle>)> = Vec::with_capacity(EXPLORE_AGENT_COUNT); for i in 0..EXPLORE_AGENT_COUNT { emit_running(turn_events, EXPLORE_IDS[i], EXPLORE_LABELS[i]); let ctx = SubagentContext::new( directives[i].clone(), tool_ctx.clone(), "read".to_string(), credentials.base_url.clone(), credentials.api_key.clone(), credentials.model.clone(), ); let directive = directives[i].clone(); let tc = tool_ctx.clone(); let handle = thread::spawn(move || { let rt = tokio::runtime::Runtime::new() .context("create explore subagent tokio runtime")?; rt.block_on(run_agent(ctx, &directive, AccessTier::Read, tc)) }); handles.push((i, handle)); } // ── 4. Join handles via spawn_blocking ────────────────────────────── let turn_events_clone = Arc::clone(turn_events); let results: Vec<(usize, String, bool)> = tokio::task::spawn_blocking(move || { let mut out = Vec::with_capacity(EXPLORE_AGENT_COUNT); for (i, handle) in handles { let entry = match handle.join() { Ok(Ok(output)) => { info!(agent = i, "explore subagent completed"); emit_completed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i]); (i, output, true) } Ok(Err(e)) => { warn!(agent = i, error = %e, "explore subagent failed"); emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], &e.to_string()); (i, format!("Error: {e}"), false) } Err(e) => { warn!(agent = i, error = ?e, "explore subagent panicked"); emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], "thread panicked"); (i, format!("Thread panic: {e:?}"), false) } }; out.push(entry); } out }) .await .context("explore join task panicked")?; // ── 5. Build consolidated context ─────────────────────────────────── Ok(build_explore_context(&results)) } // --------------------------------------------------------------------------- // Consolidation // --------------------------------------------------------------------------- /// Format explore results as a system-level context message. fn build_explore_context(results: &[(usize, String, bool)]) -> String { let success_count = results.iter().filter(|r| r.2).count(); let total = results.len(); let mut msg = format!( "[Explore Phase — {success_count}/{total} agents succeeded]\n\n" ); for (i, output, success) in results { let label = EXPLORE_LABELS.get(*i).unwrap_or(&"❓ Unknown"); if *success { msg.push_str(&format!("=== {label} ===\n{output}\n\n")); } else { msg.push_str(&format!("=== {label} (FAILED) ===\n{output}\n\n")); } } msg }