pub const MAX_AUTO_CONTINUATIONS: u32 = 10;
#[derive(Debug, PartialEq, Eq)]
pub enum TurnOutcome {
Finished,
FinalAnswer,
Incomplete,
EmptyTruncated,
Empty,
Error,
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmptyKind {
Truncated,
Blocked,
Blank,
}
pub fn classify_empty(finish_note: Option<&str>, saw_thinking: bool) -> EmptyKind {
let note = finish_note.unwrap_or("").to_lowercase();
if note.contains("max token") {
EmptyKind::Truncated
} else if note.contains("safety")
|| note.contains("blocklist")
|| note.contains("prohibited")
|| note.contains("refusal")
|| note.contains("recitation")
|| note.contains("content filter")
{
EmptyKind::Blocked
} else if saw_thinking {
EmptyKind::Truncated
} else {
EmptyKind::Blank
}
}
pub fn empty_message(kind: EmptyKind) -> &'static str {
match kind {
EmptyKind::Truncated => {
"(the request was too large to finish in one step — try breaking it \
into smaller asks.)"
}
EmptyKind::Blocked => {
"(the model stopped this response under its safety filter. Try \
rephrasing the request.)"
}
EmptyKind::Blank => {
"(empty response — the model returned no text. If you're on platform \
credits, check your session/balance in the account tab.)"
}
}
}
pub fn classify_turn(
saw_finish: bool,
saw_question: bool,
saw_tool_call: bool,
any_visible: bool,
retryable_empty: bool,
) -> TurnOutcome {
if saw_finish {
TurnOutcome::Finished
} else if saw_question {
TurnOutcome::FinalAnswer
} else if !any_visible {
if retryable_empty {
TurnOutcome::EmptyTruncated
} else {
TurnOutcome::Empty
}
} else if saw_tool_call {
TurnOutcome::Incomplete
} else {
TurnOutcome::FinalAnswer
}
}
#[cfg(test)]
mod tests {
use super::{classify_empty, classify_turn, EmptyKind, TurnOutcome, MAX_AUTO_CONTINUATIONS};
#[test]
fn finish_wins_over_everything() {
assert_eq!(
classify_turn(true, false, true, true, false),
TurnOutcome::Finished
);
assert_eq!(
classify_turn(true, false, false, true, false),
TurnOutcome::Finished
);
assert_eq!(
classify_turn(true, true, true, true, false),
TurnOutcome::Finished
);
}
#[test]
fn ask_question_stops_the_loop_not_incomplete() {
assert_eq!(
classify_turn(false, true, false, true, false),
TurnOutcome::FinalAnswer
);
assert_eq!(
classify_turn(false, true, true, true, false),
TurnOutcome::FinalAnswer
);
}
#[test]
fn goal_step_tool_only_auto_continues() {
assert_eq!(
classify_turn(false, false, true, true, false),
TurnOutcome::Incomplete
);
}
#[test]
fn finish_stops_the_loop_in_every_shape() {
assert_eq!(
classify_turn(true, false, false, true, false),
TurnOutcome::Finished
);
assert_eq!(
classify_turn(true, false, true, true, false),
TurnOutcome::Finished
);
assert_eq!(
classify_turn(true, false, false, false, false),
TurnOutcome::Finished
);
}
#[test]
fn pure_tool_without_finish_continues() {
assert_eq!(
classify_turn(false, false, true, true, false),
TurnOutcome::Incomplete
);
}
#[test]
fn pure_text_reply_is_final_answer() {
assert_eq!(
classify_turn(false, false, false, true, false),
TurnOutcome::FinalAnswer
);
}
#[test]
fn nothing_visible_is_empty() {
assert_eq!(
classify_turn(false, false, false, false, false),
TurnOutcome::Empty
);
assert_eq!(
classify_turn(false, false, true, false, false),
TurnOutcome::Empty
);
}
#[test]
fn truncated_empty_is_retryable_not_dead_end() {
assert_eq!(
classify_turn(false, false, false, false, true),
TurnOutcome::EmptyTruncated
);
assert_eq!(
classify_turn(true, false, false, false, true),
TurnOutcome::Finished
);
}
#[test]
fn classify_empty_max_tokens_note_is_truncated() {
assert_eq!(
classify_empty(Some("stopped at max tokens"), false),
EmptyKind::Truncated
);
}
#[test]
fn classify_empty_all_thinking_no_note_is_truncated() {
assert_eq!(classify_empty(None, true), EmptyKind::Truncated);
}
#[test]
fn classify_empty_safety_note_is_blocked() {
assert_eq!(
classify_empty(Some("stopped by safety policy"), true),
EmptyKind::Blocked
);
assert_eq!(
classify_empty(Some("stopped by blocklist"), false),
EmptyKind::Blocked
);
assert_eq!(
classify_empty(Some("stopped by refusal"), false),
EmptyKind::Blocked
);
}
#[test]
fn classify_empty_recitation_and_content_filter_are_blocked() {
assert_eq!(
classify_empty(Some("stopped to avoid recitation"), false),
EmptyKind::Blocked
);
assert_eq!(
classify_empty(Some("stopped to avoid recitation"), true),
EmptyKind::Blocked
);
assert_eq!(
classify_empty(Some("stopped by content filter"), false),
EmptyKind::Blocked
);
}
#[test]
fn classify_empty_nothing_at_all_is_blank() {
assert_eq!(classify_empty(None, false), EmptyKind::Blank);
assert_eq!(classify_empty(Some(""), false), EmptyKind::Blank);
}
#[test]
fn only_incomplete_or_truncated_continues() {
let continues =
|o: TurnOutcome| o == TurnOutcome::Incomplete || o == TurnOutcome::EmptyTruncated;
assert!(!continues(classify_turn(true, false, false, true, false))); assert!(!continues(classify_turn(false, true, false, true, false))); assert!(!continues(classify_turn(false, false, false, true, false))); assert!(!continues(classify_turn(false, false, false, false, false))); assert!(continues(classify_turn(false, false, true, true, false))); assert!(continues(classify_turn(false, false, false, false, true))); }
#[test]
fn auto_continuation_is_bounded() {
let mut auto: u32 = 0;
let mut iterations = 0u32;
loop {
iterations += 1;
if matches!(
classify_turn(false, false, true, true, false),
TurnOutcome::Incomplete
) {
if auto >= MAX_AUTO_CONTINUATIONS {
break;
}
auto += 1;
} else {
break;
}
assert!(iterations < MAX_AUTO_CONTINUATIONS + 5, "loop did not terminate");
}
assert_eq!(auto, MAX_AUTO_CONTINUATIONS);
assert_eq!(iterations, MAX_AUTO_CONTINUATIONS + 1);
}
#[test]
fn truncated_retry_is_bounded() {
let mut auto: u32 = 0;
let mut iterations = 0u32;
loop {
iterations += 1;
if matches!(
classify_turn(false, false, false, false, true),
TurnOutcome::EmptyTruncated
) {
if auto >= MAX_AUTO_CONTINUATIONS {
break;
}
auto += 1;
} else {
break;
}
assert!(iterations < MAX_AUTO_CONTINUATIONS + 5, "truncated retry did not terminate");
}
assert_eq!(auto, MAX_AUTO_CONTINUATIONS);
assert_eq!(iterations, MAX_AUTO_CONTINUATIONS + 1);
}
}