2026-07-21 07:41:28 +07:00
|
|
|
//! Mandatory explore phase — spawns ≥3 parallel subagents to discover
|
2026-07-21 07:47:35 +07:00
|
|
|
//! codebase context before every agent turn, visible in the TUI workflow tab.
|
2026-07-21 07:41:28 +07:00
|
|
|
//!
|
|
|
|
|
//! # Flow
|
|
|
|
|
//!
|
|
|
|
|
//! `ExploreServiceImpl::explore()` →
|
|
|
|
|
//!
|
2026-07-21 07:47:35 +07:00
|
|
|
//! 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.
|
2026-07-21 07:41:28 +07:00
|
|
|
|
|
|
|
|
use crate::subagent::context::SubagentContext;
|
|
|
|
|
use crate::subagent::division::AccessTier;
|
|
|
|
|
use crate::subagent::engine::run_agent;
|
|
|
|
|
use crate::tools::ToolCtx;
|
|
|
|
|
use anyhow::{Context, Result};
|
2026-07-21 07:47:35 +07:00
|
|
|
use std::collections::VecDeque;
|
2026-07-21 07:41:28 +07:00
|
|
|
use std::future::Future;
|
|
|
|
|
use std::pin::Pin;
|
2026-07-21 07:47:35 +07:00
|
|
|
use std::sync::{Arc, Mutex};
|
2026-07-21 07:41:28 +07:00
|
|
|
use std::thread;
|
|
|
|
|
use tracing::{info, warn};
|
|
|
|
|
use zesdex_application::agent::{ExploreOutput, ExploreService};
|
2026-07-21 07:47:35 +07:00
|
|
|
use zesdex_domain::agent::{AgentStatus, TurnEvent};
|
2026-07-21 07:41:28 +07:00
|
|
|
|
|
|
|
|
/// Number of parallel explore subagents.
|
|
|
|
|
const EXPLORE_AGENT_COUNT: usize = 3;
|
|
|
|
|
|
2026-07-21 07:47:35 +07:00
|
|
|
/// 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.
|
2026-07-21 07:41:28 +07:00
|
|
|
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\
|
2026-07-21 07:47:35 +07:00
|
|
|
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\
|
2026-07-21 07:41:28 +07:00
|
|
|
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()`
|
2026-07-21 07:47:35 +07:00
|
|
|
/// spawns 3 subagents in parallel with TUI workflow-tab visibility.
|
2026-07-21 07:41:28 +07:00
|
|
|
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,
|
2026-07-21 07:47:35 +07:00
|
|
|
turn_events: &'a Arc<Mutex<VecDeque<TurnEvent>>>,
|
2026-07-21 07:41:28 +07:00
|
|
|
) -> Pin<Box<dyn Future<Output = Result<ExploreOutput>> + Send + 'a>> {
|
|
|
|
|
Box::pin(async move {
|
|
|
|
|
let context = run_explore_phase(
|
|
|
|
|
query,
|
|
|
|
|
workspace_root,
|
|
|
|
|
&self.tool_ctx,
|
|
|
|
|
&self.credentials,
|
2026-07-21 07:47:35 +07:00
|
|
|
turn_events,
|
2026-07-21 07:41:28 +07:00
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(ExploreOutput {
|
|
|
|
|
context_messages: vec![context],
|
|
|
|
|
summary: format!("{EXPLORE_AGENT_COUNT} explore agents dispatched"),
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 07:47:35 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Helpers for pushing workflow events
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
fn push_event(events: &Arc<Mutex<VecDeque<TurnEvent>>>, event: TurnEvent) {
|
|
|
|
|
if let Ok(mut q) = events.lock() {
|
|
|
|
|
q.push_back(event);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn emit_pending(events: &Arc<Mutex<VecDeque<TurnEvent>>>, 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<Mutex<VecDeque<TurnEvent>>>, 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<Mutex<VecDeque<TurnEvent>>>, 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<Mutex<VecDeque<TurnEvent>>>, 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()),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 07:41:28 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Core orchestration
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-07-21 07:47:35 +07:00
|
|
|
/// Spawn `EXPLORE_AGENT_COUNT` subagents in parallel, emit workflow events
|
|
|
|
|
/// for the TUI tab, join, and consolidate.
|
2026-07-21 07:41:28 +07:00
|
|
|
async fn run_explore_phase(
|
|
|
|
|
query: &str,
|
|
|
|
|
workspace_root: &str,
|
|
|
|
|
tool_ctx: &ToolCtx,
|
|
|
|
|
credentials: &Credentials,
|
2026-07-21 07:47:35 +07:00
|
|
|
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
|
2026-07-21 07:41:28 +07:00
|
|
|
) -> Result<String> {
|
2026-07-21 07:47:35 +07:00
|
|
|
// ── 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 ───────────────────────────────────────────
|
2026-07-21 07:41:28 +07:00
|
|
|
let mut directives: Vec<String> = 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 07:47:35 +07:00
|
|
|
// ── 3. Spawn all agents on threads ──────────────────────────────────
|
2026-07-21 07:41:28 +07:00
|
|
|
let mut handles: Vec<(usize, thread::JoinHandle<Result<String>>)> =
|
|
|
|
|
Vec::with_capacity(EXPLORE_AGENT_COUNT);
|
|
|
|
|
|
|
|
|
|
for i in 0..EXPLORE_AGENT_COUNT {
|
2026-07-21 07:47:35 +07:00
|
|
|
emit_running(turn_events, EXPLORE_IDS[i], EXPLORE_LABELS[i]);
|
|
|
|
|
|
2026-07-21 07:41:28 +07:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 07:47:35 +07:00
|
|
|
// ── 4. Join handles via spawn_blocking ──────────────────────────────
|
|
|
|
|
let turn_events_clone = Arc::clone(turn_events);
|
2026-07-21 07:41:28 +07:00
|
|
|
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");
|
2026-07-21 07:47:35 +07:00
|
|
|
emit_completed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i]);
|
2026-07-21 07:41:28 +07:00
|
|
|
(i, output, true)
|
|
|
|
|
}
|
|
|
|
|
Ok(Err(e)) => {
|
|
|
|
|
warn!(agent = i, error = %e, "explore subagent failed");
|
2026-07-21 07:47:35 +07:00
|
|
|
emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], &e.to_string());
|
2026-07-21 07:41:28 +07:00
|
|
|
(i, format!("Error: {e}"), false)
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(agent = i, error = ?e, "explore subagent panicked");
|
2026-07-21 07:47:35 +07:00
|
|
|
emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], "thread panicked");
|
2026-07-21 07:41:28 +07:00
|
|
|
(i, format!("Thread panic: {e:?}"), false)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
out.push(entry);
|
|
|
|
|
}
|
|
|
|
|
out
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.context("explore join task panicked")?;
|
|
|
|
|
|
2026-07-21 07:47:35 +07:00
|
|
|
// ── 5. Build consolidated context ───────────────────────────────────
|
2026-07-21 07:41:28 +07:00
|
|
|
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
|
|
|
|
|
}
|