///|
pub(all) enum RetryIssueClass {
NoIssue
FormatIssue
SemanticIssue
SafetyNoRetry
ProviderNoRetry
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn RetryIssueClass::label(self : RetryIssueClass) -> String {
match self {
NoIssue => "no_issue"
FormatIssue => "format_issue"
SemanticIssue => "semantic_issue"
SafetyNoRetry => "safety_no_retry"
ProviderNoRetry => "provider_no_retry"
}
}
///|
pub(all) struct RetryDecisionInput {
stage : @routine.ModelStage
report : @domain.ValidationReport
attempt : Int
policy : @job.RetryPolicy
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct RetryDecision {
stage : @routine.ModelStage
attempt : Int
max_attempts : Int
issue_class : RetryIssueClass
should_retry : Bool
feedback_targets : Array[String]
immutable_fields : Array[String]
message : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn retry_decision_input(
stage : @routine.ModelStage,
report : @domain.ValidationReport,
attempt? : Int = 0,
policy? : @job.RetryPolicy = @job.default_retry_policy(),
) -> RetryDecisionInput {
{ stage, report, attempt, policy }
}
///|
fn issue_text(issue : @domain.ValidationIssue) -> String {
"\{issue.path} \{issue.message}"
}
///|
fn issue_mentions(issue : @domain.ValidationIssue, needle : String) -> Bool {
issue_text(issue).contains(needle)
}
///|
fn any_issue_mentions(
report : @domain.ValidationReport,
needle : String,
) -> Bool {
report.issues.any(fn(issue) { issue_mentions(issue, needle) })
}
///|
fn classify_report(report : @domain.ValidationReport) -> RetryIssueClass {
if report.ok {
NoIssue
} else if any_issue_mentions(report, "provider") ||
any_issue_mentions(report, "quota") ||
any_issue_mentions(report, "gateway") {
ProviderNoRetry
} else if any_issue_mentions(report, "order_direction") ||
any_issue_mentions(report, "broker") ||
any_issue_mentions(report, "safety") {
SafetyNoRetry
} else if any_issue_mentions(report, "confidence") ||
any_issue_mentions(report, "rationale") ||
any_issue_mentions(report, "strategy_files_needed") ||
any_issue_mentions(report, "required") ||
any_issue_mentions(report, "schema") {
FormatIssue
} else {
SemanticIssue
}
}
///|
fn max_attempts_for_class(
stage : @routine.ModelStage,
issue_class : RetryIssueClass,
policy : @job.RetryPolicy,
) -> Int {
if stage is Stage2 && !policy.retry_stage2 {
0
} else {
match issue_class {
NoIssue | SafetyNoRetry | ProviderNoRetry => 0
FormatIssue => policy.max_attempts
SemanticIssue => policy.semantic_max_attempts
}
}
}
///|
fn push_unique(items : Array[String], item : String) -> Unit {
if !items.contains(item) {
items.push(item)
}
}
///|
fn feedback_targets(report : @domain.ValidationReport) -> Array[String] {
let targets : Array[String] = []
for issue in report.issues {
if issue_mentions(issue, "direction") {
push_unique(targets, "direction")
}
if issue_mentions(issue, "cycle_position") {
push_unique(targets, "cycle_position")
}
if issue_mentions(issue, "confidence") {
push_unique(targets, "confidence")
}
if issue_mentions(issue, "rationale") {
push_unique(targets, "rationale")
}
if issue_mentions(issue, "strategy_files_needed") {
push_unique(targets, "strategy_files_needed")
}
}
targets
}
///|
fn immutable_fields(stage : @routine.ModelStage) -> Array[String] {
match stage {
Stage1 => ["direction", "cycle_position"]
Stage2 => ["diagnosis_summary.cycle_position"]
}
}
///|
fn decision_message(
issue_class : RetryIssueClass,
should_retry : Bool,
attempt : Int,
max_attempts : Int,
) -> String {
if issue_class is NoIssue {
"validation passed; no retry needed"
} else if should_retry {
"retry allowed for \{attempt + 1}/\{max_attempts} under \{issue_class.label()}"
} else {
"retry denied for \{issue_class.label()} at attempt \{attempt}/\{max_attempts}"
}
}
///|
pub fn prepare_retry_decision(input : RetryDecisionInput) -> RetryDecision {
let issue_class = classify_report(input.report)
let max_attempts = max_attempts_for_class(
input.stage,
issue_class,
input.policy,
)
let should_retry = !input.report.ok && input.attempt < max_attempts
{
stage: input.stage,
attempt: input.attempt,
max_attempts,
issue_class,
should_retry,
feedback_targets: feedback_targets(input.report),
immutable_fields: immutable_fields(input.stage),
message: decision_message(
issue_class,
should_retry,
input.attempt,
max_attempts,
),
}
}