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:
@@ -0,0 +1,52 @@
|
||||
//! Mandatory explore phase — spawns parallel subagents to discover context
|
||||
//! before the main agent begins its turn.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! Before the main agent's LLM loop, [`ExploreService::explore`] dispatches
|
||||
//! at least 3 subagents in parallel (code-structure scan, symbol-index query,
|
||||
//! semantic-context search). Their findings are consolidated into a single
|
||||
//! system message that is prepended to the conversation.
|
||||
//!
|
||||
//! # Why mandatory
|
||||
//!
|
||||
//! Without structured exploration the main agent works from an empty context
|
||||
//! window. The explore phase guarantees that every turn starts with a compact
|
||||
//! snapshot of what the codebase contains and where relevant code lives.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// The consolidated output of an explore phase — a set of system-level
|
||||
/// context messages injected before the main agent prompt.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExploreOutput {
|
||||
/// One or more system messages summarising what the explore subagents
|
||||
/// discovered. Prepended to the conversation by the turn service.
|
||||
pub context_messages: Vec<String>,
|
||||
/// Short human-readable summary of what was explored.
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
/// Service trait for the mandatory pre-turn exploration phase.
|
||||
///
|
||||
/// Implementors spawn ≥3 parallel subagents, each analysing a different
|
||||
/// aspect of the workspace, and return a consolidated summary.
|
||||
///
|
||||
/// # Object safety
|
||||
///
|
||||
/// This trait is `dyn`-safe — it returns `Pin<Box<dyn Future>>` so it can
|
||||
/// be stored as `Arc<dyn ExploreService>`.
|
||||
pub trait ExploreService: Send + Sync {
|
||||
/// Run the explore phase.
|
||||
///
|
||||
/// `query` — the user's current input phrase.
|
||||
/// `workspace_root` — absolute path to the workspace root.
|
||||
/// Returns structured context messages and a summary blob.
|
||||
fn explore<'a>(
|
||||
&'a self,
|
||||
query: &'a str,
|
||||
workspace_root: &'a str,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ExploreOutput>> + Send + 'a>>;
|
||||
}
|
||||
@@ -22,4 +22,8 @@ pub trait AgentTurnService: Send + Sync {
|
||||
) -> impl Future<Output = Result<()>> + Send;
|
||||
}
|
||||
|
||||
pub mod explore;
|
||||
pub mod turn_service;
|
||||
|
||||
pub use explore::{ExploreOutput, ExploreService};
|
||||
pub use turn_service::{compact_messages_with_ai, AgentTurnServiceImpl};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,4 +51,7 @@ pub use cms::{
|
||||
settings_service::SettingsServiceImpl,
|
||||
};
|
||||
|
||||
pub use agent::{AgentTurnService, ToolExecutor, turn_service::{AgentTurnServiceImpl, compact_messages_with_ai}};
|
||||
pub use agent::{
|
||||
AgentTurnService, ExploreOutput, ExploreService, ToolExecutor,
|
||||
turn_service::{AgentTurnServiceImpl, compact_messages_with_ai},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user