use std::collections::HashMap;
const STUCK: u32 = 3;
const STILL_STUCK: u32 = 6;
const MAX_NOTICES: u32 = 3;
const RECENCY_WINDOW: u32 = STUCK;
pub const NOTICE_STEM: &str = "Nothing is being learned here:";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rung {
Change,
Delegate,
}
#[derive(Debug, Clone, Default)]
pub struct Escapes {
pub delegate: Option<String>,
}
#[derive(Debug, Default)]
pub struct Boredom {
enabled: bool,
seen: HashMap<u64, (u32, String, u32)>,
turn: u32,
notices: u32,
}
impl Boredom {
pub fn new(enabled: bool) -> Self {
Boredom {
enabled,
..Boredom::default()
}
}
pub fn notices(&self) -> u32 {
self.notices
}
pub fn key(name: &str, input: &serde_json::Value, result: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
crate::compact::target_of(name, input).hash(&mut hasher);
result.hash(&mut hasher);
hasher.finish()
}
pub fn observe_turn<'a>(
&mut self,
turn: impl IntoIterator<Item = (&'a str, u64)>,
) -> Option<(Rung, String)> {
if !self.enabled || self.notices >= MAX_NOTICES {
return None;
}
self.turn += 1;
let now = self.turn;
let mut crossed: Option<(Rung, String)> = None;
let mut this_turn = std::collections::HashSet::new();
for (name, key) in turn {
if !this_turn.insert(key) {
continue;
}
let entry = self.seen.entry(key).or_insert((0, name.to_string(), now));
if now.saturating_sub(entry.2) > RECENCY_WINDOW {
entry.0 = 0;
}
entry.0 += 1;
entry.2 = now;
let rung = match entry.0 {
STUCK => Rung::Change,
STILL_STUCK => Rung::Delegate,
_ => continue,
};
if crossed.as_ref().is_none_or(|(r, _)| *r == Rung::Change) {
crossed = Some((rung, entry.1.clone()));
}
}
if crossed.is_some() {
self.notices += 1;
}
crossed
}
}
impl Rung {
pub fn notice(self, tool: &str, escapes: &Escapes) -> String {
match self {
Rung::Change => format!(
"{NOTICE_STEM} `{tool}` has now returned exactly the same thing \
{STUCK} times. Do not start the task over — keep what you have \
worked out, and either take a different route to this one piece or \
revise the plan if the step itself is the wrong shape."
),
Rung::Delegate => {
let mut s = format!(
"{NOTICE_STEM} `{tool}` has returned the same thing {STILL_STUCK} \
times now, and changing the approach inside this conversation has \
not moved it."
);
match &escapes.delegate {
Some(delegate) => s.push_str(&format!(
" Hand this piece to `{delegate}`, which starts from a clean \
conversation — write the task for someone with no memory of \
this one."
)),
None => s.push_str(
" Say what is blocking it and what you would need, rather than \
trying it again.",
),
}
s
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn turn(b: &mut Boredom, key: u64) -> Option<(Rung, String)> {
b.observe_turn([("build", key)])
}
#[test]
fn ordinary_repetition_is_the_model_s_business() {
let mut b = Boredom::new(true);
assert!(turn(&mut b, 1).is_none());
assert!(turn(&mut b, 1).is_none(), "a retry is how work gets done");
}
#[test]
fn a_third_identical_outcome_crosses_the_first_rung_once() {
let mut b = Boredom::new(true);
turn(&mut b, 1);
turn(&mut b, 1);
assert_eq!(turn(&mut b, 1).unwrap().0, Rung::Change);
assert!(
turn(&mut b, 1).is_none(),
"a rung is crossed once; a notice every turn is the distractor shape"
);
assert!(turn(&mut b, 1).is_none());
assert_eq!(turn(&mut b, 1).unwrap().0, Rung::Delegate);
assert!(
turn(&mut b, 1).is_none(),
"and then the loop guard's problem"
);
}
#[test]
fn a_repeat_far_apart_does_not_accumulate_toward_the_rung() {
let mut b = Boredom::new(true);
assert!(turn(&mut b, 1).is_none());
for k in 100..100 + RECENCY_WINDOW + 1 {
assert!(turn(&mut b, k as u64).is_none());
}
assert!(
turn(&mut b, 1).is_none(),
"the gap past the window should have reset the streak"
);
assert!(
turn(&mut b, 1).is_none(),
"three occurrences spread across a long run are not three in a row"
);
}
#[test]
fn a_gap_inside_the_window_still_counts_toward_the_rung() {
let mut b = Boredom::new(true);
assert!(turn(&mut b, 1).is_none());
for k in 100..100 + RECENCY_WINDOW - 1 {
assert!(turn(&mut b, k as u64).is_none());
}
assert!(turn(&mut b, 1).is_none());
assert_eq!(
turn(&mut b, 1).unwrap().0,
Rung::Change,
"a gap within the window is still one streak"
);
}
#[test]
fn a_changing_result_is_polling_and_never_stuck() {
let mut b = Boredom::new(true);
for key in 0..10 {
assert!(turn(&mut b, key).is_none());
}
}
#[test]
fn the_same_call_twice_in_one_batch_is_waste_and_not_a_loop() {
let mut b = Boredom::new(true);
assert!(b
.observe_turn([("build", 1), ("build", 1), ("build", 1)])
.is_none());
}
#[test]
fn a_run_stops_talking_about_itself_eventually() {
let mut b = Boredom::new(true);
for key in 0..5 {
for _ in 0..STUCK {
turn(&mut b, key);
}
}
assert_eq!(b.notices, MAX_NOTICES);
}
#[test]
fn switched_off_it_says_nothing() {
let mut b = Boredom::new(false);
for _ in 0..20 {
assert!(turn(&mut b, 1).is_none());
}
}
#[test]
fn a_notice_names_only_what_the_run_can_reach() {
let bare = Escapes::default();
let change = Rung::Change.notice("build", &bare);
assert!(change.contains("`build`") && change.contains("different route"));
assert!(
change.starts_with(NOTICE_STEM),
"every notice is recognisable"
);
assert!(change.contains("Do not start the task over"));
let full = Escapes {
delegate: Some("researcher".into()),
};
let delegate = Rung::Delegate.notice("build", &full);
assert!(delegate.starts_with(NOTICE_STEM));
assert!(delegate.contains("`researcher`") && delegate.contains("no memory"));
let alone = Rung::Delegate.notice("build", &bare);
assert!(alone.contains("blocking") && !alone.contains("researcher"));
}
}