use crate::agents::AgentRole;
use crate::files::write_diff_backup_with_workspace;
use crate::phases::PhaseContext;
use crate::prompts::prompt_developer_iteration_xsd_retry_with_context_files_and_log;
use crate::reducer::boundary::MainEffectHandler;
use crate::reducer::effect::EffectResult;
use crate::reducer::event::{AgentEvent, DevelopmentEvent, PipelineEvent, ReviewEvent};
use anyhow::Result;
use std::path::Path;
impl MainEffectHandler {
pub(super) fn invoke_analysis_agent(
&mut self,
ctx: &mut PhaseContext<'_>,
iteration: u32,
) -> Result<EffectResult> {
let plan_content = read_plan_content_with_fallback(ctx);
let diff_content = read_diff_content_with_backup(ctx);
let prompt = if self.state.continuation.xsd_retry_pending {
prepare_analysis_xsd_retry_context(ctx);
prompt_developer_iteration_xsd_retry_with_context_files_and_log(
ctx.template_context,
&read_xsd_retry_error_with_fallback(ctx),
ctx.workspace,
"developer_iteration_xsd_retry",
self.state.continuation.is_continuation(),
)
.content
} else {
crate::prompts::analysis::generate_analysis_prompt(
&plan_content,
&diff_content,
self.state.continuation.is_continuation(),
ctx.workspace,
)
};
let prompt = apply_same_agent_retry_prefix(
prompt,
self.state.continuation.same_agent_retry_pending,
&self.state.continuation,
);
let result = invoke_analysis_agent_with_prompt(self, ctx, prompt)?;
let result = maybe_add_analysis_invoked_event(result, iteration);
Ok(result)
}
pub(super) fn invoke_fix_analysis_agent(
&mut self,
ctx: &mut PhaseContext<'_>,
pass: u32,
) -> Result<EffectResult> {
let issues_content = read_issues_content(ctx);
let diff_content = read_diff_content_with_backup(ctx);
let fix_result_content = read_fix_result_content(ctx);
let prompt = crate::prompts::analysis::generate_fix_analysis_prompt(
&issues_content,
&diff_content,
&fix_result_content,
self.state.continuation.fix_continue_pending,
ctx.workspace,
);
let prompt = apply_xsd_retry_note(prompt, self.state.continuation.xsd_retry_pending);
let prompt = apply_same_agent_retry_prefix(
prompt,
self.state.continuation.same_agent_retry_pending,
&self.state.continuation,
);
let result = invoke_analysis_agent_with_prompt(self, ctx, prompt)?;
let result = maybe_add_fix_analysis_invoked_event(result, pass);
Ok(result)
}
}
fn read_plan_content_with_fallback(ctx: &PhaseContext<'_>) -> String {
let plan_path = Path::new(".agent/PLAN.md");
match ctx.workspace.read(plan_path) {
Ok(s) => s,
Err(err) => {
let xml_fallback = Path::new(".agent/tmp/plan.xml");
match ctx.workspace.read(xml_fallback) {
Ok(xml) => format!(
"[PLAN unavailable: failed to read .agent/PLAN.md ({err}); using fallback .agent/tmp/plan.xml]\n\n{xml}"
),
Err(fallback_err) => format!(
"[PLAN unavailable: failed to read .agent/PLAN.md ({err}); also failed to read .agent/tmp/plan.xml ({fallback_err})]"
),
}
}
}
}
fn read_diff_content_with_backup(ctx: &PhaseContext<'_>) -> String {
match crate::git_helpers::git_diff_in_repo(ctx.repo_root) {
Ok(diff) => {
let _ = write_diff_backup_with_workspace(ctx.workspace, &diff);
diff
}
Err(err) => {
let placeholder = format!("[DIFF unavailable: failed to generate git diff ({err})]");
let _ = write_diff_backup_with_workspace(ctx.workspace, &placeholder);
placeholder
}
}
}
fn read_issues_content(ctx: &PhaseContext<'_>) -> String {
let issues_path = Path::new(".agent/ISSUES.md");
match ctx.workspace.read(issues_path) {
Ok(s) => s,
Err(err) => {
format!("[REVIEW ISSUES unavailable: failed to read .agent/ISSUES.md ({err})]")
}
}
}
fn read_fix_result_content(ctx: &PhaseContext<'_>) -> String {
let fix_result_path = Path::new(".agent/tmp/fix_result.xml");
match ctx.workspace.read(fix_result_path) {
Ok(s) => s,
Err(err) => {
format!("[FIX RESULT unavailable: failed to read .agent/tmp/fix_result.xml ({err})]")
}
}
}
fn apply_xsd_retry_note(prompt: String, xsd_retry_pending: bool) -> String {
if xsd_retry_pending {
let xsd_error_path = ".agent/tmp/development_xsd_error.txt";
let last_output_path = ".agent/tmp/development_result.xml";
format!(
"## XSD Retry Note\n\n\
Your previous XML output failed XSD validation.\n\
- Read the validation error: {xsd_error_path}\n\
- Read your previous invalid output: {last_output_path}\n\
Then produce a corrected development_result.xml that conforms to the schema.\n\n\
{prompt}"
)
} else {
prompt
}
}
fn prepare_analysis_xsd_retry_context(ctx: &PhaseContext<'_>) {
if let Ok(last_output) = ctx
.workspace
.read(Path::new(".agent/tmp/development_result.xml"))
{
let _ = ctx
.workspace
.write(Path::new(".agent/tmp/last_output.xml"), &last_output);
}
}
fn read_xsd_retry_error_with_fallback(ctx: &PhaseContext<'_>) -> String {
ctx.workspace
.read(Path::new(".agent/tmp/development_xsd_error.txt"))
.unwrap_or_else(|err| {
format!(
"XML output failed validation. Read .agent/tmp/development_xsd_error.txt for details if available. ({err})"
)
})
}
fn apply_same_agent_retry_prefix(
prompt: String,
same_agent_retry_pending: bool,
continuation: &crate::reducer::state::ContinuationState,
) -> String {
if same_agent_retry_pending {
let retry_preamble = super::retry_guidance::same_agent_retry_preamble(continuation);
format!("{retry_preamble}\n{prompt}")
} else {
prompt
}
}
fn invoke_analysis_agent_with_prompt(
handler: &mut MainEffectHandler,
ctx: &mut PhaseContext<'_>,
prompt: String,
) -> Result<EffectResult> {
handler.normalize_agent_chain_for_invocation(ctx, crate::agents::AgentDrain::Analysis);
let agent = handler
.state
.agent_chain
.current_agent()
.cloned()
.unwrap_or_else(|| ctx.developer_agent.to_string());
handler.invoke_agent(
ctx,
crate::agents::AgentDrain::Analysis,
AgentRole::Analysis,
&agent,
None,
prompt,
)
}
fn maybe_add_analysis_invoked_event(result: EffectResult, iteration: u32) -> EffectResult {
if result.additional_events.iter().any(|e| {
matches!(
e,
PipelineEvent::Agent(AgentEvent::InvocationSucceeded { .. })
)
}) {
result.with_additional_event(PipelineEvent::Development(
DevelopmentEvent::AnalysisAgentInvoked { iteration },
))
} else {
result
}
}
fn maybe_add_fix_analysis_invoked_event(result: EffectResult, pass: u32) -> EffectResult {
if result.additional_events.iter().any(|e| {
matches!(
e,
PipelineEvent::Agent(AgentEvent::InvocationSucceeded { .. })
)
}) {
result.with_additional_event(PipelineEvent::Review(
ReviewEvent::FixAnalysisAgentInvoked { pass },
))
} else {
result
}
}