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:
@@ -8,7 +8,7 @@ use zesdex_domain::core::{ChatMessage, StreamEvent, ToolDef};
|
||||
use zesdex_domain::main_agent_prompt;
|
||||
|
||||
use crate::ports::ProviderService;
|
||||
use super::ToolExecutor;
|
||||
use super::{ExploreService, ToolExecutor};
|
||||
|
||||
/// Maximum tool-call iterations per agent turn before forcing termination.
|
||||
const MAX_TURN_ITERATIONS: u32 = 50;
|
||||
@@ -107,21 +107,45 @@ fn emit_usage(turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>, usage: Option<(u64,
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Service implementation for executing an agent turn asynchronously.
|
||||
///
|
||||
/// # Explore phase
|
||||
///
|
||||
/// Before the main LLM loop begins, [`AgentTurnServiceImpl`] runs a mandatory
|
||||
/// explore phase that spawns ≥3 parallel subagents (code structure, symbol
|
||||
/// index, semantic context) and injects their consolidated findings as a
|
||||
/// system message. See [`ExploreService`] for the trait contract.
|
||||
pub struct AgentTurnServiceImpl<P: ProviderService, T: ToolExecutor> {
|
||||
provider: Arc<P>,
|
||||
tool_executor: Arc<T>,
|
||||
tool_defs: Vec<ToolDef>,
|
||||
/// Optional explore-phase service. When `Some`, the explore phase runs
|
||||
/// before every turn; when `None` it is skipped (tests, daemon mode).
|
||||
explore_service: Option<Arc<dyn ExploreService>>,
|
||||
}
|
||||
|
||||
impl<P: ProviderService, T: ToolExecutor> AgentTurnServiceImpl<P, T> {
|
||||
pub fn new(provider: Arc<P>, tool_executor: Arc<T>, tool_defs: Vec<ToolDef>) -> Self {
|
||||
pub fn new(
|
||||
provider: Arc<P>,
|
||||
tool_executor: Arc<T>,
|
||||
tool_defs: Vec<ToolDef>,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
tool_executor,
|
||||
tool_defs,
|
||||
explore_service: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach an optional explore-phase service.
|
||||
///
|
||||
/// When set, every call to `run_turn` will first run the explore phase
|
||||
/// and inject the consolidated context as a system message.
|
||||
pub fn with_explore(mut self, service: Arc<dyn ExploreService>) -> Self {
|
||||
self.explore_service = Some(service);
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute a single LLM call with the current message list, handling
|
||||
/// streaming events and error reporting.
|
||||
async fn call_llm(
|
||||
@@ -153,6 +177,61 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
|
||||
params.model
|
||||
);
|
||||
|
||||
// ── Phase 0: Mandatory explore ──────────────────────────────────
|
||||
// Spawn ≥3 parallel subagents to discover code structure, symbols,
|
||||
// and semantic context. The consolidated summary is injected as a
|
||||
// system message before the main agent prompt.
|
||||
if let Some(ref explorer) = self.explore_service {
|
||||
// Determine workspace root from the first message's context or
|
||||
// the first workspace root in params.
|
||||
let user_query = params
|
||||
.messages
|
||||
.last()
|
||||
.map(|m| m.content.clone().unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
let workspace_root = params
|
||||
.workspace_roots
|
||||
.first()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
push_event(
|
||||
¶ms.turn_events,
|
||||
TurnEvent::SystemNote {
|
||||
kind: "info".into(),
|
||||
message: "🔍 Exploring codebase structure...".into(),
|
||||
},
|
||||
);
|
||||
|
||||
match explorer.explore(&user_query, &workspace_root).await {
|
||||
Ok(output) => {
|
||||
// Insert each context message as a system message.
|
||||
// They go at index 0 and are removed after the turn
|
||||
// like the main agent prompt.
|
||||
for ctx_msg in &output.context_messages {
|
||||
params
|
||||
.messages
|
||||
.insert(0, ChatMessage::system(ctx_msg.clone()));
|
||||
}
|
||||
info!(
|
||||
"Explore phase complete: {} context messages, {}",
|
||||
output.context_messages.len(),
|
||||
output.summary
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Explore phase failed (non-fatal): {e}");
|
||||
push_event(
|
||||
¶ms.turn_events,
|
||||
TurnEvent::SystemNote {
|
||||
kind: "warn".into(),
|
||||
message: format!("Explore phase failed: {e}"),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert system prompt at position 0 once and keep it there for the
|
||||
// entire turn, avoiding per-iteration clones of the full message list.
|
||||
// It is removed before emitting the Compacted event so persistence
|
||||
|
||||
Reference in New Issue
Block a user