use super::*;
#[derive(Debug, Clone, Copy)]
pub struct WatchdogParams {
pub think_loop_min_repeats: usize,
pub think_loop_scan_window: usize,
pub confidence_early_stop: bool,
pub confidence_run_length: u32,
pub fuzzy_repeat_tolerance_div: usize,
pub max_inter_tool_prose: u32,
pub max_post_think_content_tokens: u32,
pub enable_think_loop_watchdog: bool,
pub min_reasoning_floor: u32,
pub rollback_resteer: bool,
pub content_loop_min_repeats: Option<u32>,
pub thinking_steering: bool,
}
const DEFAULT_WATCHDOG_PARAMS: WatchdogParams = WatchdogParams {
think_loop_min_repeats: THINK_LOOP_MIN_REPEATS,
think_loop_scan_window: THINK_LOOP_SCAN_WINDOW,
confidence_early_stop: true,
confidence_run_length: crate::scheduler::confidence::CONFIDENCE_RUN_LIMIT,
fuzzy_repeat_tolerance_div: 12,
max_inter_tool_prose: MAX_INTER_TOOL_PROSE,
max_post_think_content_tokens: MAX_POST_THINK_CONTENT_TOKENS,
rollback_resteer: true,
enable_think_loop_watchdog: true,
content_loop_min_repeats: None,
min_reasoning_floor: 16,
thinking_steering: true,
};
impl Default for WatchdogParams {
fn default() -> Self {
DEFAULT_WATCHDOG_PARAMS
}
}
impl WatchdogParams {
pub const DEFAULT_FUZZY_TOLERANCE_DIV: usize =
DEFAULT_WATCHDOG_PARAMS.fuzzy_repeat_tolerance_div;
pub fn from_behavior(
b: &metrale_kernels::ModelBehavior,
max_inter_tool_prose_cli: Option<u32>,
content_loop_min_repeats_cli: Option<u32>,
) -> Self {
let steering = b.thinking_steering;
let mut p = Self {
min_reasoning_floor: if steering {
b.min_reasoning_floor_tokens
} else {
0
},
think_loop_min_repeats: b.think_loop_min_repeats as usize,
think_loop_scan_window: b.think_loop_scan_window as usize,
confidence_early_stop: b.confidence_early_stop && steering,
confidence_run_length: b.confidence_run_length,
fuzzy_repeat_tolerance_div: b.fuzzy_repeat_tolerance_div as usize,
max_inter_tool_prose: b.max_inter_tool_prose,
max_post_think_content_tokens: b.max_post_think_content_tokens,
rollback_resteer: b.rollback_resteer,
enable_think_loop_watchdog: b.enable_think_loop_watchdog && steering,
content_loop_min_repeats: None,
thinking_steering: steering,
};
let env = match std::env::var("METRALE_MAX_INTER_TOOL_PROSE") {
Ok(v) => match v.parse::<u32>() {
Ok(n) => Some(n),
Err(_) => {
tracing::warn!(
value = %v,
"METRALE_MAX_INTER_TOOL_PROSE is set but not a u32; ignoring it"
);
None
}
},
Err(_) => None,
};
p.max_inter_tool_prose =
resolve_max_inter_tool_prose(p.max_inter_tool_prose, env, max_inter_tool_prose_cli);
p.content_loop_min_repeats = content_loop_min_repeats_cli.or(parse_env_u32(
"METRALE_CONTENT_LOOP_MIN_REPEATS",
std::env::var("METRALE_CONTENT_LOOP_MIN_REPEATS")
.ok()
.as_deref(),
));
p
}
pub fn content_loop_params(
&self,
request: Option<crate::api::inference_types::RepetitionDetectionParams>,
) -> Option<crate::api::inference_types::RepetitionDetectionParams> {
request.or_else(|| {
self.content_loop_min_repeats.map(|n| {
crate::api::inference_types::RepetitionDetectionParams {
min_pattern_size: CONTENT_LOOP_PERIOD_MIN as u32,
max_pattern_size: CONTENT_LOOP_PERIOD_MAX as u32,
min_count: n,
}
})
})
}
}
fn parse_env_u32(name: &str, v: Option<&str>) -> Option<u32> {
let v = v?;
match v.trim().parse::<u32>() {
Ok(n) => Some(n),
Err(_) => {
tracing::warn!(value = %v, "{name} is set but not a u32; ignoring it");
None
}
}
}
pub fn resolve_content_loop_watchdog(toml: bool, env: Option<&str>, cli: Option<bool>) -> bool {
if let Some(cli) = cli {
return cli;
}
match env {
None => toml,
Some(v) => {
let v = v.trim();
if v == "1" || v.eq_ignore_ascii_case("true") {
true
} else if v == "0" || v.eq_ignore_ascii_case("false") {
false
} else {
tracing::warn!(
value = %v,
"METRALE_CONTENT_LOOP_WATCHDOG is set but not 1/true/0/false; \
keeping the MODEL.toml value"
);
toml
}
}
}
}
pub fn resolve_max_inter_tool_prose(toml: u32, env: Option<u32>, cli: Option<u32>) -> u32 {
let v = cli.or(env).unwrap_or(toml);
if v == 0 { u32::MAX } else { v }
}
pub const MAX_INTER_TOOL_PROSE: u32 = metrale_kernels::DEFAULT_MAX_INTER_TOOL_PROSE;
pub const MAX_POST_THINK_CONTENT_TOKENS: u32 = 100_000;