use super::{KernelObservation, LoopAction, LoopPhase, LoopStateMachine};
use crate::types::milestone::{MilestoneCheckResult, MilestoneContract};
use crate::types::result::TerminationReason;
impl LoopStateMachine {
pub fn load_milestone_contract(&mut self, contract: MilestoneContract) {
self.milestone.load_contract(contract);
}
pub fn current_milestone_phase_id(&self) -> Option<&str> {
self.milestone.current_phase_id()
}
pub fn current_milestone_criteria(&self) -> &[String] {
self.milestone.current_criteria()
}
pub fn is_milestone_complete(&self) -> bool {
self.milestone.is_complete()
}
pub(super) fn handle_milestone_result(&mut self, result: MilestoneCheckResult) -> LoopAction {
self.observations.clear();
if result.passed {
let mut unlocked: Vec<String> = Vec::new();
if let Some(contract) = &self.milestone.contract.clone() {
if let Some(phase) = contract.phases.get(self.milestone.current_phase) {
let mounted_by = Some(format!("milestone:{}", phase.id));
for cap in phase.unlocks.clone() {
let kind_str = cap.kind.label();
let id = cap.id.to_string();
unlocked.push(format!("{}:{}", kind_str, id));
self.mount_capability(
cap,
mounted_by.clone(),
Some("phase_advance".to_string()),
);
}
self.observations.push(KernelObservation::MilestoneAdvanced {
turn: self.turn,
phase_id: phase.id.clone(),
capabilities_unlocked: unlocked,
});
}
}
self.milestone.current_phase += 1;
self.milestone.blocked_count = 0;
if self.is_milestone_complete() {
return self.terminate(TerminationReason::Completed, None);
}
if let Some(criteria) = self
.milestone
.contract
.as_ref()
.and_then(|c| c.phases.get(self.milestone.current_phase))
.map(|p| {
if p.criteria.is_empty() {
format!("[NEXT MILESTONE PHASE: {}]", p.id)
} else {
format!(
"[NEXT MILESTONE PHASE: {} — Criteria: {}]",
p.id,
p.criteria.join("; ")
)
}
})
{
self.ctx.push_signal(criteria);
}
self.phase = LoopPhase::Reason;
self.emit_call_llm()
} else {
self.milestone.blocked_count += 1;
let reason = result.reason.as_deref().unwrap_or("milestone criteria not met");
let (rollback_policy, max_attempts) = self
.milestone
.contract
.as_ref()
.and_then(|c| c.phases.get(self.milestone.current_phase))
.map(|p| {
let max = p
.retry_policy
.as_ref()
.map(|rp| rp.max_attempts)
.unwrap_or(0);
(p.rollback_policy.clone(), max)
})
.unwrap_or_default();
let budget_exceeded = max_attempts > 0
&& self.milestone.blocked_count as u32 >= max_attempts;
if budget_exceeded {
use crate::types::milestone::MilestoneRollbackPolicy;
match rollback_policy {
MilestoneRollbackPolicy::Terminate => {
self.observations.push(KernelObservation::MilestoneBlocked {
turn: self.turn,
phase_id: result.phase_id.clone(),
reason: format!("retry budget exhausted: {reason}"),
});
return self.terminate(TerminationReason::MilestoneExceeded, None);
}
MilestoneRollbackPolicy::Rollback => {
self.observations.push(KernelObservation::MilestoneBlocked {
turn: self.turn,
phase_id: result.phase_id.clone(),
reason: format!("retry budget exhausted (rollback): {reason}"),
});
let rb_reason = crate::runtime::session::RollbackReason::MalformedReplay {
reason: format!("milestone {} retry budget exhausted", result.phase_id),
};
self.rollback(rb_reason);
self.phase = LoopPhase::Reason;
return self.emit_call_llm();
}
MilestoneRollbackPolicy::Continue => {
}
}
}
self.ctx.push_signal(format!(
"[MILESTONE BLOCKED: {} — {}. Address the criteria and try again.]",
result.phase_id, reason
));
self.observations.push(KernelObservation::MilestoneBlocked {
turn: self.turn,
phase_id: result.phase_id,
reason: reason.to_string(),
});
self.phase = LoopPhase::Reason;
self.emit_call_llm()
}
}
}