feat: implement mandatory explore phase with parallel subagents

- Added `ExploreService` trait and `ExploreServiceImpl` struct to handle the exploration of codebase context before agent turns.
- Implemented three parallel subagents: Code Structure, Symbol Index, and Semantic Context, each with specific directives.
- Integrated the explore phase into the agent turn process, ensuring that each turn starts with a consolidated context message.
- Enhanced `spawn_agent_turn` function to include explore service wiring and context preparation.
This commit is contained in:
asepharyana
2026-07-21 07:41:28 +07:00
parent 8ecc588a3e
commit f368f3a1c0
9 changed files with 1726 additions and 291 deletions
@@ -0,0 +1,229 @@
//! Mandatory explore phase — spawns ≥3 parallel subagents to discover
//! codebase context before every agent turn.
//!
//! # Flow
//!
//! `ExploreServiceImpl::explore()` →
//!
//! 1. **Code Structure** subagent — walks the workspace tree, reads
//! `Cargo.toml` / `package.json`, lists top-level modules.
//! 2. **Symbol Index** subagent — rebuilds the multi-language symbol index,
//! then lists symbols.
//! 3. **Semantic Context** subagent — searches code for context related to
//! the user's query.
//!
//! All three run on **dedicated OS threads** in parallel with their own
//! tokio runtimes. Joins are offloaded to `spawn_blocking` so they do not
//! stall the async runtime.
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::future::Future;
use std::pin::Pin;
use std::thread;
use tracing::{info, warn};
use zesdex_application::agent::{ExploreOutput, ExploreService};
/// Number of parallel explore subagents.
const EXPLORE_AGENT_COUNT: usize = 3;
/// Labels for each explore agent.
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 top-level directories and files in the workspace root.\n\
2. Read Cargo.toml, package.json, or pyproject.toml at 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, waits for all to finish, and returns
/// a consolidated context message.
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,
) -> 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,
)
.await?;
Ok(ExploreOutput {
context_messages: vec![context],
summary: format!("{EXPLORE_AGENT_COUNT} explore agents dispatched"),
})
})
}
}
// ---------------------------------------------------------------------------
// Core orchestration
// ---------------------------------------------------------------------------
/// Spawn EXPLORE_AGENT_COUNT subagents in parallel, join, consolidate.
async fn run_explore_phase(
query: &str,
workspace_root: &str,
tool_ctx: &ToolCtx,
credentials: &Credentials,
) -> Result<String> {
// ── Prepare directives ──────────────────────────────────────────────
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();
// Agent 2 gets the user query for targeted search.
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);
}
// ── Spawn all agents on threads with their own tokio runtime ────────
let mut handles: Vec<(usize, thread::JoinHandle<Result<String>>)> =
Vec::with_capacity(EXPLORE_AGENT_COUNT);
for i in 0..EXPLORE_AGENT_COUNT {
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));
}
// ── Join handles via spawn_blocking (blocking join, NOT async) ──────
// We move handles into a blocking task so the async executor stays free.
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");
(i, output, true)
}
Ok(Err(e)) => {
warn!(agent = i, error = %e, "explore subagent failed");
(i, format!("Error: {e}"), false)
}
Err(e) => {
warn!(agent = i, error = ?e, "explore subagent panicked");
(i, format!("Thread panic: {e:?}"), false)
}
};
out.push(entry);
}
out
})
.await
.context("explore join task panicked")?;
// ── 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
}
@@ -22,6 +22,7 @@
pub mod arch_audit;
pub mod code_quality;
pub mod commit;
pub mod explore;
pub mod skills;
use anyhow::Result;
@@ -44,6 +44,7 @@ pub fn all_tools() -> Vec<Box<dyn super::Tool>> {
Box::new(super::web_search::WebSearch),
Box::new(super::semantic_search::SemanticSearch),
Box::new(super::semantic_search::RebuildIndex),
Box::new(super::semantic_search::ListSymbols),
Box::new(super::parallel_delegate::ParallelDelegate),
// ── Best-practice tools (built-in) ─────────────────────────
Box::new(super::best_practice::BestPractice),
File diff suppressed because it is too large Load Diff