use crate::cm_agent::plan_artifact::PlanStepV1;
use crate::cm_types::{Message, ToolCall};
use super::completion_suppression::{
plan_steps_are_redundant_after_completion, plan_steps_require_formal_execution,
tool_calls_are_redundant_when_goal_satisfied,
};
use super::task_level_evidence::{
GoalCompletionEvidenceCheck, check_active_user_goal_completion_evidence,
generic_task_intent_implies_build_or_test,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TurnCompletionDecision {
AllowEarlyStop,
DenyEarlyStop {
reason: &'static str,
},
AllowSuppressReplanning,
DenySuppressReplanning {
reason: &'static str,
},
AllowRedundantTools,
DenyRedundantTools {
reason: &'static str,
},
AllowMissingFinalAnswerFeedback,
DenyMissingFinalAnswerFeedback {
reason: &'static str,
},
}
impl TurnCompletionDecision {
pub fn as_trace_str(self) -> &'static str {
match self {
Self::AllowEarlyStop => "allow_early_stop",
Self::DenyEarlyStop { .. } => "deny_early_stop",
Self::AllowSuppressReplanning => "allow_suppress_replanning",
Self::DenySuppressReplanning { .. } => "deny_suppress_replanning",
Self::AllowRedundantTools => "allow_redundant_tools",
Self::DenyRedundantTools { .. } => "deny_redundant_tools",
Self::AllowMissingFinalAnswerFeedback => "allow_missing_final_answer_feedback",
Self::DenyMissingFinalAnswerFeedback { .. } => "deny_missing_final_answer_feedback",
}
}
pub fn deny_reason(self) -> Option<&'static str> {
match self {
Self::DenyEarlyStop { reason }
| Self::DenySuppressReplanning { reason }
| Self::DenyRedundantTools { reason }
| Self::DenyMissingFinalAnswerFeedback { reason } => Some(reason),
_ => None,
}
}
pub fn is_allow(self) -> bool {
matches!(
self,
Self::AllowEarlyStop
| Self::AllowSuppressReplanning
| Self::AllowRedundantTools
| Self::AllowMissingFinalAnswerFeedback
)
}
}
pub fn log_turn_completion_decision(decision: TurnCompletionDecision, check: &'static str) {
tracing::debug!(
target: "crabmate::agent_turn",
turn_completion_check = check,
turn_completion_decision = decision.as_trace_str(),
turn_completion_deny_reason = decision.deny_reason(),
"turn_completion decision"
);
}
fn turn_early_stop_allowed_core(messages: &[Message]) -> TurnCompletionDecision {
if !matches!(
check_active_user_goal_completion_evidence(messages),
GoalCompletionEvidenceCheck::Satisfied
) {
return TurnCompletionDecision::DenyEarlyStop {
reason: "evidence_not_satisfied",
};
}
let Some(task) = crate::cm_types::last_real_user_task_content(messages, false) else {
return TurnCompletionDecision::DenyEarlyStop {
reason: "no_active_user_task",
};
};
if generic_task_intent_implies_build_or_test(task) {
TurnCompletionDecision::DenyEarlyStop {
reason: "build_or_test_intent",
}
} else {
TurnCompletionDecision::AllowEarlyStop
}
}
pub fn evaluate_turn_early_stop(messages: &[Message]) -> TurnCompletionDecision {
let decision = turn_early_stop_allowed_core(messages);
log_turn_completion_decision(decision, "early_stop");
decision
}
pub fn evaluate_turn_suppress_replanning(
messages: &[Message],
entered_from_step_execution_round: bool,
steps: &[PlanStepV1],
) -> TurnCompletionDecision {
let decision = if !entered_from_step_execution_round {
TurnCompletionDecision::DenySuppressReplanning {
reason: "not_from_step_execution_round",
}
} else if steps.is_empty() {
TurnCompletionDecision::DenySuppressReplanning {
reason: "empty_steps",
}
} else if plan_steps_require_formal_execution(steps) {
TurnCompletionDecision::DenySuppressReplanning {
reason: "formal_execution_required",
}
} else if !turn_early_stop_allowed_core(messages).is_allow() {
TurnCompletionDecision::DenySuppressReplanning {
reason: "early_stop_not_allowed",
}
} else if !plan_steps_are_redundant_after_completion(steps) {
TurnCompletionDecision::DenySuppressReplanning {
reason: "steps_not_redundant",
}
} else {
TurnCompletionDecision::AllowSuppressReplanning
};
log_turn_completion_decision(decision, "suppress_replanning");
decision
}
pub fn evaluate_turn_redundant_tools(
tool_calls: &[ToolCall],
messages: &[Message],
) -> TurnCompletionDecision {
let decision = if !tool_calls_are_redundant_when_goal_satisfied(tool_calls, messages) {
TurnCompletionDecision::DenyRedundantTools {
reason: "tools_not_redundant",
}
} else if !turn_early_stop_allowed_core(messages).is_allow() {
TurnCompletionDecision::DenyRedundantTools {
reason: "early_stop_not_allowed",
}
} else {
TurnCompletionDecision::AllowRedundantTools
};
log_turn_completion_decision(decision, "redundant_tools");
decision
}