#[must_use]
pub fn compute_effect_fingerprint(state: &PipelineState) -> String {
format!(
"{}:{}:{}:iter={}:pass={}:xsd_retry={}",
state.phase,
state.runtime_drain(),
match state.agent_chain.current_mode {
crate::agents::DrainMode::Normal => "normal",
crate::agents::DrainMode::Continuation => "continuation",
crate::agents::DrainMode::SameAgentRetry => "same-agent-retry",
crate::agents::DrainMode::XsdRetry => "xsd-retry",
},
state.iteration,
state.reviewer_pass,
state.continuation.xsd_retry_pending
)
}
#[cfg(test)]
mod xsd_retry_fingerprint_tests {
use super::compute_effect_fingerprint;
use crate::agents::AgentRole;
use crate::reducer::event::PipelinePhase;
use crate::reducer::state::{AgentChainState, ContinuationState, PipelineState};
#[test]
fn test_effect_fingerprint_ignores_xsd_retry_count() {
let state = PipelineState::initial(1, 1);
let state = PipelineState {
phase: PipelinePhase::Development,
agent_chain: AgentChainState {
current_role: AgentRole::Developer,
..AgentChainState::default()
},
iteration: 1,
reviewer_pass: 0,
continuation: ContinuationState {
xsd_retry_pending: true,
xsd_retry_count: 1,
..Default::default()
},
..state
};
let fp1 = compute_effect_fingerprint(&state);
let state = PipelineState {
continuation: ContinuationState {
xsd_retry_count: 2,
..state.continuation.clone()
},
..state
};
let fp2 = compute_effect_fingerprint(&state);
assert_eq!(fp1, fp2);
}
}
fn review_phase_uses_fix_drain(state: &PipelineState) -> bool {
state.runtime_drain() == crate::agents::AgentDrain::Fix
}
fn fix_drain_is_loaded(state: &PipelineState) -> bool {
state.agent_chain.current_drain == crate::agents::AgentDrain::Fix
}
fn development_retry_uses_analysis_drain(state: &PipelineState) -> bool {
analysis_drain_is_loaded(state)
|| state.analysis_agent_invoked_iteration == Some(state.iteration)
|| (state.development_agent_invoked_iteration == Some(state.iteration)
&& state.development_xml_extracted_iteration != Some(state.iteration)
&& state
.development_validated_outcome
.as_ref()
.is_none_or(|outcome| outcome.iteration != state.iteration))
}
fn analysis_drain_is_loaded(state: &PipelineState) -> bool {
state.agent_chain.current_drain == crate::agents::AgentDrain::Analysis
}
fn derive_xsd_retry_effect(state: &PipelineState) -> Effect {
match state.phase {
PipelinePhase::Planning => Effect::PreparePlanningPrompt {
iteration: state.iteration,
prompt_mode: PromptMode::XsdRetry,
},
PipelinePhase::Development => {
if !development_retry_uses_analysis_drain(state) || !analysis_drain_is_loaded(state) {
return Effect::InitializeAgentChain {
drain: crate::agents::AgentDrain::Analysis,
};
}
Effect::InvokeAnalysisAgent {
iteration: state.iteration,
}
}
PipelinePhase::Review => {
if review_phase_uses_fix_drain(state) {
if !fix_drain_is_loaded(state) {
return Effect::InitializeAgentChain {
drain: crate::agents::AgentDrain::Fix,
};
}
Effect::PrepareFixPrompt {
pass: state.reviewer_pass,
prompt_mode: PromptMode::XsdRetry,
}
} else {
Effect::PrepareReviewPrompt {
pass: state.reviewer_pass,
prompt_mode: PromptMode::XsdRetry,
}
}
}
PipelinePhase::CommitMessage => Effect::PrepareCommitPrompt {
prompt_mode: PromptMode::XsdRetry,
},
_ => Effect::SaveCheckpoint {
trigger: CheckpointTrigger::PhaseTransition,
},
}
}
fn derive_timeout_context_write_effect(state: &PipelineState) -> Effect {
let logfile_path = state
.continuation
.timeout_context_file_path
.clone()
.unwrap_or_else(|| ".agent/logs/unknown.log".to_string());
let context_path = format!(
".agent/tmp/timeout_context_{}.txt",
state.continuation.same_agent_retry_count
);
Effect::WriteTimeoutContext {
role: state.agent_chain.current_drain.role(),
logfile_path,
context_path,
}
}
fn derive_same_agent_retry_effect(state: &PipelineState) -> Effect {
match state.phase {
PipelinePhase::Planning => Effect::PreparePlanningPrompt {
iteration: state.iteration,
prompt_mode: PromptMode::SameAgentRetry,
},
PipelinePhase::Development => {
if development_retry_uses_analysis_drain(state) {
if !analysis_drain_is_loaded(state) {
return Effect::InitializeAgentChain {
drain: crate::agents::AgentDrain::Analysis,
};
}
return Effect::InvokeAnalysisAgent {
iteration: state.iteration,
};
}
Effect::PrepareDevelopmentPrompt {
iteration: state.iteration,
prompt_mode: PromptMode::SameAgentRetry,
}
}
PipelinePhase::Review => {
if review_phase_uses_fix_drain(state) {
if !fix_drain_is_loaded(state) {
return Effect::InitializeAgentChain {
drain: crate::agents::AgentDrain::Fix,
};
}
Effect::PrepareFixPrompt {
pass: state.reviewer_pass,
prompt_mode: PromptMode::SameAgentRetry,
}
} else {
Effect::PrepareReviewPrompt {
pass: state.reviewer_pass,
prompt_mode: PromptMode::SameAgentRetry,
}
}
}
PipelinePhase::CommitMessage => Effect::PrepareCommitPrompt {
prompt_mode: PromptMode::SameAgentRetry,
},
_ => Effect::SaveCheckpoint {
trigger: CheckpointTrigger::PhaseTransition,
},
}
}
fn derive_continuation_effect(state: &PipelineState) -> Effect {
match state.phase {
PipelinePhase::Development => {
if state.continuation.context_write_pending {
let status = state
.continuation
.previous_status
.unwrap_or(super::state::DevelopmentStatus::Failed);
let summary = state
.continuation
.previous_summary
.clone()
.unwrap_or_default();
let files_changed = state.continuation.previous_files_changed.clone();
let next_steps = state.continuation.previous_next_steps.clone();
Effect::WriteContinuationContext(ContinuationContextData {
iteration: state.iteration,
attempt: state.continuation.continuation_attempt,
status,
summary,
files_changed,
next_steps,
})
} else {
Effect::PrepareDevelopmentContext {
iteration: state.iteration,
}
}
}
PipelinePhase::Review if review_phase_uses_fix_drain(state) => {
if fix_drain_is_loaded(state) {
Effect::PrepareFixPrompt {
pass: state.reviewer_pass,
prompt_mode: PromptMode::Continuation,
}
} else {
Effect::InitializeAgentChain {
drain: crate::agents::AgentDrain::Fix,
}
}
}
_ => Effect::SaveCheckpoint {
trigger: CheckpointTrigger::PhaseTransition,
},
}
}